query:按相似度搜

🎉摘要:本文详解Chroma向量数据库的相似度搜索核心原理,梳理collection.query方法的参数规则,覆盖文本检索、自定向量查询、批量检索、过滤条件组合等场景的实战代码,帮助开发者快速掌握语义检索能力,适配RAG开发需求。

前面介绍的 get 方法是根据过滤条件返回数据,不是通过相识度进行搜索,这和数据库查询数据效果一样。而我们使用向量数据库的最终目的是进行相似度查询,而不是根据条件精确查找。

什么是相似度搜索?

相似度搜索:

不是按完全相等、包含某个字、ID 匹配这种精确条件去查,而是:

把你输入的一段话,转成向量,然后在向量库里,找出向量距离最近的那些文档,代表语义上最相近。

它比关键词匹配聪明:

  • 关键词搜索:必须有一样的字才命中(比如搜“苹果”,文本写“红富士”就搜不到)

  • 相似度搜索:理解语义,搜“水果”,存的“红富士苹果”也能排前面

方法签名

方法签名如下:

collection.query(
    query_embeddings=None,   # 直接给向量
    query_texts=None,        # 给文本,让嵌入函数算
    query_images=None,       # 给图片(多模态)
    query_uris=None,         # 给地址(多模态)
    ids=None,                # 只在这批 id 里搜
    n_results=10,            # 每个 query 返回几条
    where=None,              # 元数据过滤
    where_document=None,     # 文档内容过滤
    include=["metadatas", "documents", "distances"],
)

参数说明:

  • query_embeddings:类型为 list[list[float]] | None,直接传入已经计算好的向量数组,不再调用 embedding 函数。适合自己提前算好向量的场景。例如:query_embeddings=[[0.1,0.2,...]]

  • query_texts:类型为 list[str] | None,传入查询文本列表,Chroma 会自动调用集合绑定的嵌入函数,把文本转为向量再检索。日常 RAG 最常用。例如:query_texts=["怎么排查接口超时"]

  • query_images:类型为 list | None,多模态专用,传入图片对象,用于以图搜图,依赖 OpenCLIP 等多模态嵌入函数。

  • query_uris:类型为 list[str] | None,多模态专用,传入图片文件路径,需要集合提前配置 data_loader,否则会报 URI 加载错误。

  • ids:类型为 list[str] | None,用来限定搜索范围,只在你给出的这些 id 里面做相似度排序,库中其他数据直接跳过。

  • n_results:类型为 int,每一条查询对应的返回数量。比如 query_texts 给 2 段文字,每一段都会返回 n_results 条相似数据。

  • where:类型为 dict | None,用于元数据条件过滤(和 get 里面 where 语法一致)。 执行逻辑:先按 where 筛选出符合元数据的数据,再在筛选结果里面做相似度排序。

  • where_document:类型为 dict | None,文档文本过滤,仅支持 $contains。同样是先过滤、再算相似度。

  • include:类型为 list[str],指定返回携带的字段,可选值如下:

    • "distances":向量距离(Chroma 默认欧式距离,数值越小代表越相似)

    • "documents":原文

    • "metadatas":元数据

    • "embeddings":匹配到的文档对应的向量(开销大,按需开启)

    • "uris":资源地址

注意:query_embeddings / query_texts / query_images / query_uris 四选一,不能同时多个生效。

最简单的用法

下面通过简单示例介绍相似度检索的用法:

import chromadb
from common.custom_embedding import AliQwenEmbeddingFunction

# 实例化嵌入函数
ef = AliQwenEmbeddingFunction()

client = chromadb.EphemeralClient()
col = client.get_or_create_collection("demo_col", embedding_function=ef)
col.add(
    ids=["r1", "r2", "r3", "r4", "r5"],
    documents=[
        "退款流程:在订单页点申请退款,3 个工作日内到账",
        "发票开具:在订单详情页申请电子发票",
        "物流查询:发货后可在物流详情里看到进度",
        "退货政策:7 天无理由退货,运费买家承担",
        "账号安全:如何修改密码和绑定手机",
    ],
    metadatas=[
        {"cat": "售后", "year": 2024, "score": 0.9, "tags": ["退款"]},
        {"cat": "财务", "year": 2023, "score": 0.5, "tags": ["发票"]},
        {"cat": "物流", "year": 2024, "score": 0.7, "tags": ["物流"]},
        {"cat": "售后", "year": 2022, "score": 0.3, "tags": ["退货"]},
        {"cat": "账号", "year": 2024, "score": 0.6, "tags": ["安全"]},
    ],
)

res = col.query(query_texts=["怎么退款"], n_results=2)
print(f"ids={res["ids"]}")
print(f"documents={res["documents"]}")
print(f"distances={res["distances"]}") # 值越小,越相似

运行示例,输出如下:

ids=[['r1', 'r4']]
documents=[['退款流程:在订单页点申请退款,3 个工作日内到账', '退货政策:7 天无理由退货,运费买家承担']]
metadatas=[[{'tags': ['退款'], 'cat': '售后', 'year': 2024, 'score': 0.9}, {'tags': ['退货'], 'cat': '售后', 'year': 2022, 'score': 0.3}]]
distances=[[0.5935589075088501, 1.046556830406189]]

从输出可以看出,“退款流程:在订单页点申请退款,3 个工作日内到账”问题的相似度为 0.5935589075088501,最为相似。

注意:嵌入函数不要使用默认的嵌入函数,它对中文支持基本为零,需要自己实现嵌入函数,可以选择调用现有大模型平台的嵌入模型,如阿里云的 qwen3.7-text-embedding-flash 等。

用向量查

使用向量进行查询和使用文本查询类似,需要自己将查询文本转化为向量,传递给 query 方法进行查询。注意,转化向量的模型需要和写入数据使用的向量模型已知,维度大小一致,才能查询出有效数据。

例如:自定义一个 AliEmbedding 类,封装使用 qwen3.7-text-embedding-flash 计算向量的便捷方法,如下:

class AliEmbedding:
    def __init__(self):
        # 在环境变量添加自己的 API KEY
        self.api_key = os.getenv("OPENAI_API_KEY", "")
        self.model_name = "qwen3.7-text-embedding-flash"
        # 这里断点地址也替换为自己的
        self.endpoint = "https://llm-0sb8vr1k7aav4kn3.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/embeddings"
        self.headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
        }

    def get_embedding(self, content: str) -> List[float]:
        """
        对单个文本content调用阿里云qwen3.7-text-embedding-flash向量接口,返回向量数组
        :param content: 输入文本字符串
        :return: float向量列表
        """
        payload = {
            "input": content,
            "model": self.model_name
        }
        resp = requests.post(self.endpoint, headers=self.headers, json=payload, timeout=60)
        resp.raise_for_status()  # http非2xx直接抛异常
        data = resp.json()
        embedding = data["data"][0]["embedding"]
        return embedding

    def batch_get_embedding(self, contents: List[str]) -> List[List[float]]:
        """
        批量向量化,一次传入多条文本
        :param contents: 文本列表
        :return: 向量列表
        """
        payload = {
            "input": contents,
            "model": self.model_name
        }
        resp = requests.post(self.endpoint, headers=self.headers, json=payload, timeout=60)
        resp.raise_for_status()
        data = resp.json()
        # 按index排序保证顺序一致
        items = sorted(data["data"], key=lambda x: x["index"])
        embeddings = [item["embedding"] for item in items]
        return embeddings

引入自定义的 AliEmbedding 类,使用提供的方法计算向量:

import chromadb
from common.custom_embedding import AliEmbedding, AliQwenEmbeddingFunction

# 注意:保证写入和查询使用同一个嵌入模型
# 实例化自己封装的嵌入类
ali_embedding = AliEmbedding()
ef = AliQwenEmbeddingFunction()

client = chromadb.EphemeralClient()
col = client.get_or_create_collection("demo_col", embedding_function=None)

# 也自己计算向量
col.add(
    ids=["r1", "r2", "r3", "r4", "r5"],
    documents=[
        "退款流程:在订单页点申请退款,3 个工作日内到账",
        "发票开具:在订单详情页申请电子发票",
        "物流查询:发货后可在物流详情里看到进度",
        "退货政策:7 天无理由退货,运费买家承担",
        "账号安全:如何修改密码和绑定手机",
    ],
    # 添加数据时,批量计算向量
    embeddings=ali_embedding.batch_get_embedding([
        "退款流程:在订单页点申请退款,3 个工作日内到账",
        "发票开具:在订单详情页申请电子发票",
        "物流查询:发货后可在物流详情里看到进度",
        "退货政策:7 天无理由退货,运费买家承担",
        "账号安全:如何修改密码和绑定手机",
    ]),
    metadatas=[
        {"cat": "售后", "year": 2024, "score": 0.9, "tags": ["退款"]},
        {"cat": "财务", "year": 2023, "score": 0.5, "tags": ["发票"]},
        {"cat": "物流", "year": 2024, "score": 0.7, "tags": ["物流"]},
        {"cat": "售后", "year": 2022, "score": 0.3, "tags": ["退货"]},
        {"cat": "账号", "year": 2024, "score": 0.6, "tags": ["安全"]},
    ],
)

# 查询,计算向量,通过 query_embeddings 将向量传递进去
res = col.query(query_embeddings=[
        ali_embedding.get_embedding("怎么退款")
    ], n_results=2)
print(f"ids={res["ids"]}")
print(f"documents={res["documents"]}")
print(f"distances={res["distances"]}") # 值越小,越相似

运行代码,输出结果如下:

ids=[['r1', 'r4']]
documents=[['退款流程:在订单页点申请退款,3 个工作日内到账', '退货政策:7 天无理由退货,运费买家承担']]
distances=[[0.6103367805480957, 1.0393226146697998]]

注意,query_embeddings 是“向量的列表”,哪怕只查一个,也要套一层。

批量查询

这是 Chroma 一个很好用但经常被忽略的能力,一次查多个问题。用法如下:

res = col.query(
    # 提供了两组向量
    query_embeddings=[
        [1, 0, 0],   # 问题一的向量
        [0, 1, 0]	 # 问题二的向量
    ],
    n_results=2,
)
print(res["ids"])
# [['a', 'b'], ['b', 'a']]      ← 两组结果
print(res["distances"])
# [[0.0, 2.0], [0.0, 2.0]]		← 两组相似度结果

批量比循环单查快不少,因为省了网络往返和索引遍历的开销。做评测(比如算 recall@10)的时候特别爽。

n_results 超过实际数量

如果 query 方法 n_results 参数指定 的值大于集合中数据总数,Chroma 不会报错,而是返回实际有的那么多。例如:

# 假设集合只有 3 条,n_results 设置为 10
res = col.query(query_embeddings=[[1, 0, 0]], n_results=10)
print(len(res["ids"][0]))   # 3

上面代码不会报错哦,会返回集合全部数据,只是返回的数据的 distances 会有差别,顺序有差别。

跟过滤条件组合

如果 query 添加了 ids、where、where_document 等过滤条件,执行逻辑是:先过滤数据,再在过滤后的子集里做向量检索。

例如:先检索 metadatas 中 t 等于 y 的记录,然后再进行相似度检索。

res = col.query(
    query_embeddings=[[1, 0, 0]],
    n_results=3,
    where={"t": "y"},          # 只搜 t='y' 的
)
print(res["ids"][0], res["distances"][0])
# ['c'] [2.0]     ← 只有 1 条符合过滤,就只返回 1 条

使用 id 进行精确过滤:

res = col.query(query_embeddings=[[1, 0, 0]], n_results=3, ids=["b"])
print(res["ids"][0])
# ['b']           ← 只在 id='b' 里搜

重要:这是“过滤后检索”(pre-filter),不是“检索后过滤”。如果过滤条件把候选集砍得很小,返回的条数会少于 n_results。反过来,如果过滤条件很宽松(比如 100 万条里过滤掉 100 条),HNSW 在大量被过滤掉的节点上会退化,召回率和延迟都会变糟。第 6 章详细说这个。

include 默认带 distances

query 的默认 include 是 ["metadatas", "documents", "distances"],比 get 多一个 distances。distances 表示距离,值越小,表示向量越接近。

如果想省流量可以不带,就像这样:

res = col.query(query_texts=["x"], n_results=5, include=["documents"])
# res["distances"] 是 None


说说我的看法
全部评论()
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号