未记录事实核验 · 状态:维护中
记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。
warning 适用边界与维护说明
本文适用于已创建并加载 Milvus Collection、配置匹配索引且能提供查询向量的读者,用于查询接口学习、混合检索设计与调优实验;密集和稀疏向量生成函数需自行实现,不是从空环境直接运行的完整项目。当前范围搜索仍有端点表述冲突:L2 示例的 radius 注释使用严格小于,但两处区间公式使用小于等于;search_iterator 的输出文字也将配置中的相似度 > 0.7 写为 >= 0.7,不宜据此判断边界命中。离散 in 列表不能一般性地视为连续数值区间,迭代器导言中的分页性能断言也应结合实际环境测试。范围搜索和搜索迭代器不保证穷尽阈值内记录,批量加速倍数、固定相似度阈值及搜索参数建议均需实际评测。本次仅作正文结构审阅,未执行代码或进行外部事实核验。 已取消无法确定目标的可点击引用「07-Schema设计与数据建模」,保留原提及;Ambiguous link target: 07-Schema设计与数据建模 已取消无法确定目标的可点击引用「10-RAG集成实战」,保留原提及;Ambiguous link target: 10-RAG集成实战 已取消无法确定目标的可点击引用「03-核心概念详解」,保留原提及;Ambiguous link target: 03-核心概念详解
查询与搜索详解
abstract 本文能给你什么
- 彻底理解 Search 和 Query 的本质差异,知道什么场景该用哪个
- 掌握所有向量搜索变体:基础 ANN、过滤搜索、范围搜索、分组搜索
- 学会混合搜索:多向量融合 + RRF/加权重排序的调用示例,向量生成部分需自行实现
- 理解相似度分数的含义,能正确解读和处理搜索结果
- 学会迭代器的使用,处理大批量数据遍历场景
搜索是 Milvus 的核心能力,但很多人只用到了最基础的 search,不知道 Milvus 还有过滤搜索、范围搜索、分组搜索、混合搜索等丰富的查询模式。每种查询模式背后都有特定的适用场景,用对了能大幅提升检索质量,用错了只会让结果更差。
本文从本质理解出发,覆盖常用查询接口,并提供调用示例和参数解释。示例依赖已创建、建索引并加载的 Collection,以及读者提供的查询向量;混合检索中的向量生成函数为待实现占位,不是独立可运行的完整项目。
目录
- Search vs Query 的本质区别
- 向量搜索(Search)完整解析
- 过滤搜索(Filtered Search)
- 标量查询(Query)
- 混合搜索(Hybrid Search)
- 范围搜索(Range Search)
- 分组搜索(Grouping Search)
- 迭代器(Iterator)
- 搜索结果处理
- 性能调优与最佳实践
Search vs Query 的本质区别
Milvus 有两个完全不同的检索接口:search 和 query。很多初学者会混用它们,导致拿到错误的结果或者用了错误的方法。
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TB
Input(["用户的检索需求"]) --> Decision{{"核心问题:<br/>你有查询向量吗?"}}
Decision -->|"有向量<br/>需要找语义相似的数据"| Search["search 接口<br/>向量相似性检索"]
Decision -->|"没有向量<br/>需要精确过滤数据"| Query["query 接口<br/>标量条件查询"]
Search --> SearchOut["返回:最相似的 Top-K 条<br/>按相似度分数排序<br/>支持附加 filter 条件"]
Query --> QueryOut["返回:满足条件的所有数据<br/>无排序(或按标量字段排序)<br/>支持 offset + limit 分页"]
SearchOut --> S1["典型场景:<br/>- 语义搜索<br/>- 推荐系统<br/>- 图像检索<br/>- RAG 召回"]
QueryOut --> Q1["典型场景:<br/>- 按 ID 查单条<br/>- 列举某类别的数据<br/>- 数据统计和分页<br/>- 验证数据是否存在"]
style Search fill:#dbeafe,stroke:#3b82f6
style Query fill:#dcfce7,stroke:#16a34a
style SearchOut fill:#eff6ff,stroke:#93c5fd
style QueryOut fill:#f0fdf4,stroke:#86efac
note 一句话区分
search:给我一个向量,找最像它的 K 条数据(向量相似性问题)query:给我一个布尔表达式,找所有满足条件的数据(集合过滤问题)
向量搜索(Search)完整解析
基础 ANN 搜索
最基本的向量搜索:给一个查询向量,返回最相似的 K 条结果。
from pymilvus import MilvusClient
client = MilvusClient(uri="http://localhost:19530")
# 基础搜索
results = client.search(
collection_name="movies", # 要搜索的 Collection
data=[query_vector], # 查询向量列表,支持批量查询(多个向量)
anns_field="embedding", # 向量字段名(有多个向量字段时必须指定)
limit=10, # 返回最相似的前 K 条(Top-K)
output_fields=["title", "genre", "year"], # 返回的标量字段
search_params={
"metric_type": "COSINE", # 相似度度量方式
"params": {"ef": 100} # 索引特定的搜索参数
}
)
# 结果结构
for hits in results: # results 是一个列表,对应每个查询向量的结果
for hit in hits: # hits 是该查询向量的 Top-K 结果
print(f"ID: {hit['id']}")
print(f"相似度分数: {hit['distance']}")
print(f"标题: {hit['entity']['title']}")
print(f"类型: {hit['entity']['genre']}")
print("---")
参数逐一解析
data:查询向量
支持批量查询,同时传入多个查询向量:
# 单个查询
data=[query_vector] # query_vector 是长度为 dim 的 list[float]
# 批量查询(同时搜索多个向量,性能比循环调用 search 更好)
data=[query_vec_1, query_vec_2, query_vec_3]
# 返回 results[0]、results[1]、results[2] 分别对应三个查询向量的结果
limit:Top-K 数量
limit 不是越大越好。增大 limit 会增加返回及后处理的数据量,也会改变下游可用候选数量;最终召回效果需结合索引、搜索参数、候选规模和评测指标判断,不能只由 limit 或 search_params 单独决定。
# 常见配置建议
limit=10 # 搜索结果直接展示给用户
limit=50 # 后续有精排(reranker)逻辑
limit=100 # 混合搜索的候选集
output_fields:输出字段
只返回必要的字段,减少网络传输:
# 只返回 ID(最轻量,适合只需要 ID 再去另一个存储取详情的场景)
output_fields=["id"]
# 返回展示所需字段
output_fields=["title", "genre", "rating"]
# 返回所有字段(慎用,向量字段通常很大)
output_fields=["*"]
# 包含向量字段本身(通常不需要,向量很大会拖慢速度)
output_fields=["title", "embedding"]
warning output_fields 包含向量字段的代价
向量字段数据量巨大(1536维 float32 = 6KB/条),如果 output_fields 包含向量字段,返回 100 条结果就需要传输 600KB 的向量数据。除非你有特殊需求(比如做二次计算),否则不要在 output_fields 中包含向量字段。
search_params:控制搜索精度与速度的核心参数
search_params 的具体参数取决于索引类型,不同索引的搜索参数完全不同:
# HNSW 索引的搜索参数
search_params_hnsw = {
"metric_type": "COSINE",
"params": {
"ef": 100 # 搜索时候选节点数,越大召回率越高但越慢
# 建议范围:limit 的 2-10 倍,常用 64~512
}
}
# IVF_FLAT 索引的搜索参数
search_params_ivf = {
"metric_type": "L2",
"params": {
"nprobe": 16 # 搜索的桶数量,越大召回率越高但越慢
# 建议范围:4~128,常用 16~32
}
}
# SCANN 索引的搜索参数
search_params_scann = {
"metric_type": "COSINE",
"params": {
"reorder_k": 100 # 重排序候选数,比 limit 大即可
}
}
# DiskANN 索引的搜索参数(适合大规模数据,内存不足时)
search_params_diskann = {
"metric_type": "COSINE",
"params": {
"search_list": 100 # 候选列表大小,越大越准但 I/O 越多
}
}
tip 如何选 ef / nprobe 的值
通用原则:先用较小的值(ef=50, nprobe=8),测试召回率(ground truth 对比)。如果召回率不够,逐步加大,找到"召回率达标的最小搜索参数",以此平衡精度和延迟。
批量查询性能对比
import time
query_vectors = [get_embedding(q) for q in queries] # 假设有 100 个查询
# 方式一:循环单次 search(不推荐)
start = time.time()
results_loop = []
for vec in query_vectors:
r = client.search("movies", data=[vec], limit=10)
results_loop.append(r[0])
print(f"循环方式耗时:{time.time() - start:.2f}s")
# 方式二:批量 search(推荐)
start = time.time()
results_batch = client.search("movies", data=query_vectors, limit=10)
print(f"批量方式耗时:{time.time() - start:.2f}s")
# 批量方式可减少网络往返次数;可将加速 5-20 倍作为待测假设,但本文未提供支持该倍数的实测结果,实际差异取决于批大小、网络、索引和服务端负载
过滤搜索(Filtered Search)
过滤搜索是在向量相似性搜索的基础上,附加标量字段的过滤条件。这是 Milvus 最常用的复合查询模式。
过滤表达式完整语法指南
比较运算符
# 等于与不等于
filter='genre == "sci-fi"'
filter='genre != "horror"'
# 数值比较
filter='year > 2020'
filter='year >= 2020'
filter='rating < 8.0'
filter='rating <= 7.5'
逻辑运算符
# AND:同时满足多个条件
filter='genre == "sci-fi" and year > 2020'
filter='rating > 7.0 and year >= 2015 and genre != "horror"'
# OR:满足任意条件
filter='genre == "sci-fi" or genre == "fantasy"'
# NOT:取反
filter='not (genre == "horror")'
# 混合使用(括号控制优先级)
filter='(genre == "sci-fi" or genre == "action") and year > 2015 and rating > 7.0'
范围运算符
# in:在集合中
filter='genre in ["sci-fi", "fantasy", "action"]'
# not in:不在集合中
filter='genre not in ["horror", "adult"]'
# 数值范围(等价于 >= and <=)
filter='year in [2020, 2021, 2022, 2023]' # 离散值列表
字符串运算符
# like:模糊匹配(% 是通配符)
filter='title like "Star%"' # 以 "Star" 开头
filter='title like "%Wars%"' # 包含 "Wars"
filter='title like "%Jedi"' # 以 "Jedi" 结尾
# 注意:like 查询的执行代价取决于匹配模式、已建索引及版本支持,不能一概视为全表扫描;大数据量下应结合实际配置测试
JSON 字段查询
# JSON 字段的简单键值查询
filter='metadata["source"] == "arxiv"'
filter='metadata["year"] > 2022'
filter='metadata["is_public"] == true'
# JSON 嵌套查询
filter='metadata["location"]["city"] == "Beijing"'
# JSON 数组包含查询
filter='json_contains(metadata["authors"], "张三")'
# 多条件组合
filter='metadata["source"] == "arxiv" and metadata["citations"] > 100'
Array 字段查询
# 包含特定元素
filter='array_contains(tags, "机器学习")'
# 包含所有指定元素(AND)
filter='array_contains_all(tags, ["机器学习", "NLP"])'
# 包含任意一个元素(OR)
filter='array_contains_any(tags, ["深度学习", "机器学习", "AI"])'
# 数组长度
filter='array_length(tags) >= 3'
filter='array_length(tags) == 0' # 空标签的数据
完整过滤搜索示例
from pymilvus import MilvusClient
client = MilvusClient(uri="http://localhost:19530")
# 场景:在电影向量库中,找与查询向量语义最近的科幻片
# 条件:2015年后上映、评分 >= 7.5、不是恐怖类型
results = client.search(
collection_name="movies",
data=[query_vector],
anns_field="embedding",
limit=10,
filter='genre == "sci-fi" and year > 2015 and rating >= 7.5',
output_fields=["title", "genre", "year", "rating"],
search_params={
"metric_type": "COSINE",
"params": {"ef": 200} # 示例搜索参数;是否需要增大 ef 应结合过滤模式、索引和召回评测判断
}
)
for hits in results:
for hit in hits:
print(f"[{hit['distance']:.4f}] {hit['entity']['title']} "
f"({hit['entity']['year']}) - {hit['entity']['rating']}分")
Filter 对性能和召回率的影响
warning 高选择性过滤会降低有效召回率
严格的 filter 条件可能影响搜索延迟、召回率和返回数量,但不能统一解释为先获取 ANN Top-K 再删除不满足条件的候选。标准过滤先按标量条件限定搜索范围,迭代过滤则在搜索迭代中检查候选;返回数量还受满足条件的数据量及搜索配置影响。
解决方案:
- 加大
ef(HNSW)或nprobe(IVF),扫描更多候选 - 检查满足过滤条件的记录数,并根据实际过滤模式和索引评测搜索参数;
range_filter用于限定距离范围,不是补足过滤候选的通用方法 - 考虑将高选择性条件单独用
query接口处理,再做二次向量排序
标量字段索引加速 Filter
为高频过滤的标量字段添加索引,可以大幅加速 filter 的执行:
# 为 genre 字段添加倒排索引
index_params = MilvusClient.prepare_index_params()
index_params.add_index(
field_name="genre",
index_type="INVERTED" # 适合 VarChar、Bool 等离散值字段
)
# 为 year 字段添加 STL_SORT 索引(适合范围查询)
index_params.add_index(
field_name="year",
index_type="STL_SORT" # 适合数值范围查询
)
# 为 tags(Array字段)添加倒排索引
index_params.add_index(
field_name="tags",
index_type="INVERTED"
)
client.create_index("movies", index_params)
标量查询(Query)
query 接口不使用向量,只根据标量条件过滤数据,返回满足条件的所有记录。
基础查询
from pymilvus import MilvusClient
client = MilvusClient(uri="http://localhost:19530")
# 基础条件查询
results = client.query(
collection_name="movies",
filter='genre == "sci-fi" and year > 2020',
output_fields=["id", "title", "genre", "year"],
limit=100 # 最多返回 100 条
)
for item in results:
print(f"ID: {item['id']}, 标题: {item['title']}, 年份: {item['year']}")
按 ID 精确查询
# 通过主键 ID 查询单条数据
result = client.get(
collection_name="movies",
ids=[12345] # 支持批量查询多个 ID
)
# 等价的 query 写法
result = client.query(
collection_name="movies",
filter="id in [12345, 67890, 11111]",
output_fields=["*"]
)
分页查询(Pagination)
page_size = 20
# 第一页
page1 = client.query(
collection_name="movies",
filter='genre == "sci-fi"',
output_fields=["id", "title", "year"],
limit=page_size,
offset=0 # 从第 0 条开始
)
# 第二页
page2 = client.query(
collection_name="movies",
filter='genre == "sci-fi"',
output_fields=["id", "title", "year"],
limit=page_size,
offset=page_size # 跳过第一页
)
# 第 N 页
def query_page(collection, filter_expr, fields, page_num, page_size=20):
return client.query(
collection_name=collection,
filter=filter_expr,
output_fields=fields,
limit=page_size,
offset=(page_num - 1) * page_size
)
warning 深度分页的性能问题
offset 很大时(如 offset=10000),深度分页可能增加查询开销;具体性能变化取决于实现、数据规模和查询配置,本文没有提供线性下降的测量依据。如果需要遍历大量数据,优先考虑 迭代器(Iterator),并检查当前环境支持的分页范围。
Count 查询
# 统计满足条件的数据总量
result = client.query(
collection_name="movies",
filter='genre == "sci-fi"',
output_fields=["count(*)"] # 使用 count(*) 只返回数量,不返回数据
)
total_count = result[0]["count(*)"]
print(f"科幻片总数:{total_count}")
# 无条件统计 Collection 总数据量(等价于 get_collection_stats)
result = client.query(
collection_name="movies",
filter="", # 空字符串表示无条件
output_fields=["count(*)"]
)
total = result[0]["count(*)"]
Query vs Search 的选择决策
| 情况 | 用 Search | 用 Query |
|---|---|---|
| 有查询向量,要找语义相似的内容 | 是 | 否 |
| 只知道 ID,要取对应的数据 | 否 | 是(用 get 或 query) |
| 需要列举某个分类下的所有数据 | 否 | 是 |
| 需要统计数据量 | 否 | 是(count) |
| 先过滤、再按向量相似度排序 | 是(带 filter) | 否 |
| 验证某条数据是否存在 | 否 | 是 |
混合搜索(Hybrid Search)
混合搜索允许在一次请求中执行多路向量搜索,并将多路结果融合重排序。这是实现高质量语义检索的关键技术。
核心架构
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TB
Query["用户查询"] --> TextEmbed["文本 Embedding 模型<br/>→ text_vector"]
Query --> SparseEmbed["稀疏向量(BM25)<br/>→ sparse_vector"]
TextEmbed --> Req1["AnnSearchRequest<br/>字段:text_embedding<br/>Top-100 候选"]
SparseEmbed --> Req2["AnnSearchRequest<br/>字段:sparse_embedding<br/>Top-100 候选"]
Req1 --> Ranker["RRFRanker / WeightedRanker<br/>多路结果融合重排序"]
Req2 --> Ranker
Ranker --> Final["最终 Top-K 结果<br/>语义 + 关键词双重保障"]
style Query fill:#dbeafe,stroke:#3b82f6
style Ranker fill:#fef3c7,stroke:#f59e0b
style Final fill:#dcfce7,stroke:#16a34a
密集向量 + 稀疏向量混合检索(最经典的混合搜索)
这种模式同时利用语义相似性(密集向量)和关键词匹配(稀疏向量),在 RAG 场景中显著提升召回质量。
from pymilvus import MilvusClient, AnnSearchRequest, RRFRanker, WeightedRanker
client = MilvusClient(uri="http://localhost:19530")
# 假设 Collection 有两个向量字段:
# - text_embedding: FloatVector(1536),存储语义向量
# - sparse_embedding: SparseFloatVector,存储 BM25 权重
def get_dense_embedding(text: str) -> list[float]:
"""调用 OpenAI/Cohere 等模型获取密集向量"""
# ... 实现略
pass
def get_sparse_embedding(text: str) -> dict:
"""获取 BM25 稀疏向量(词索引 -> 权重)"""
# 使用 pymilvus 内置的 BM25 工具或自定义实现
# ... 实现略
pass
def hybrid_search(query: str, top_k: int = 10) -> list:
"""
混合检索:语义向量 + BM25 关键词,RRF 融合重排序
"""
dense_vec = get_dense_embedding(query)
sparse_vec = get_sparse_embedding(query)
# 密集向量搜索请求
dense_req = AnnSearchRequest(
data=[dense_vec],
anns_field="text_embedding",
param={
"metric_type": "COSINE",
"params": {"ef": top_k * 5} # HNSW 搜索参数 ef,不等同于返回给融合器的候选结果数
},
limit=top_k * 3 # 候选集放大,给重排序留余量
)
# 稀疏向量搜索请求
sparse_req = AnnSearchRequest(
data=[sparse_vec],
anns_field="sparse_embedding",
param={
"metric_type": "IP", # 稀疏向量用内积度量
"params": {"drop_ratio_search": 0.2} # 按较小权重丢弃查询稀疏向量中约 20% 的非零项,不是以 0.2 为绝对权重阈值
},
limit=top_k * 3
)
# RRF 融合:Reciprocal Rank Fusion,不需要关心各路分数的量纲
# k=60 是 RRF 的平滑参数,通常不需要调整
ranker = RRFRanker(k=60)
results = client.hybrid_search(
collection_name="documents",
reqs=[dense_req, sparse_req],
ranker=ranker,
limit=top_k,
output_fields=["doc_id", "content_preview", "source"]
)
return results[0] # 返回第一个查询的结果
# 使用示例
hits = hybrid_search("向量数据库的索引原理", top_k=5)
for hit in hits:
print(f"[{hit['distance']:.4f}] {hit['entity']['content_preview'][:80]}...")
多向量字段混合搜索(多模态场景)
from pymilvus import AnnSearchRequest, WeightedRanker
def multimodal_search(text_vec: list, image_vec: list,
text_weight: float = 0.6,
image_weight: float = 0.4,
top_k: int = 10) -> list:
"""
多模态混合搜索:文本语义 + 图像视觉特征加权融合
适用于:商品搜索(输入文字+图片,找视觉和语义都相似的商品)
"""
# 文本向量搜索
text_req = AnnSearchRequest(
data=[text_vec],
anns_field="text_embedding",
param={"metric_type": "COSINE", "params": {"ef": top_k * 5}},
limit=top_k * 2
)
# 图像向量搜索
image_req = AnnSearchRequest(
data=[image_vec],
anns_field="image_embedding",
param={"metric_type": "L2", "params": {"ef": top_k * 5}},
limit=top_k * 2
)
# WeightedRanker:对各路分数按度量类型归一化后进行加权融合
# COSINE 与 L2 的原始分数不可直接比较,但不要求各路使用相同 metric_type
# 权重应通过检索评测选择;不希望依赖分数尺度时可使用 RRFRanker
ranker = WeightedRanker(text_weight, image_weight)
results = client.hybrid_search(
collection_name="products",
reqs=[text_req, image_req],
ranker=ranker,
limit=top_k,
output_fields=["product_id", "name", "price", "category"]
)
return results[0]
# WeightedRanker vs RRFRanker 的选择
# WeightedRanker:明确知道每路搜索的权重时使用(如文本60%,图像40%)
# 内部按度量类型归一化分数,不要求各路使用相同 metric_type
# RRFRanker: 不确定各路权重,或者各路 metric_type 不同时使用
# 基于排名融合,对量纲不敏感,更稳健
混合搜索 + 过滤条件
# 混合搜索也支持附加 filter 条件(在每个 AnnSearchRequest 中指定)
dense_req = AnnSearchRequest(
data=[dense_vec],
anns_field="text_embedding",
param={"metric_type": "COSINE", "params": {"ef": 200}},
limit=50,
expr='category == "技术" and created_at > 1704067200' # filter 条件
)
sparse_req = AnnSearchRequest(
data=[sparse_vec],
anns_field="sparse_embedding",
param={"metric_type": "IP", "params": {"drop_ratio_search": 0.2}},
limit=50,
expr='category == "技术" and created_at > 1704067200' # 两路 filter 要一致
)
范围搜索(Range Search)
普通 ANN 搜索返回近似最相似的 Top-K 个向量;范围搜索在指定距离区间内检索结果,仍受 limit 和底层检索配置限制,不保证返回范围内所有向量。适合需要"相似度阈值"而不是"Top-K"的场景。
参数解释
results = client.search(
collection_name="images",
data=[query_vector],
anns_field="image_embedding",
limit=1000, # 范围内结果的上限(必须设置,防止返回过多数据)
search_params={
"metric_type": "L2",
"params": {
"radius": 1.0, # 外边界:平方欧氏距离 < radius 的向量才考虑
"range_filter": 0.1 # 内边界(可选):距离 >= range_filter 的才返回
# 即:range_filter <= distance <= radius
}
},
output_fields=["image_id", "description"]
)
tip radius 和 range_filter 的直觉理解
radius:最大可接受距离(圆的半径),超过这个距离的向量一律排除range_filter:最小距离(可选),用于排除过近的结果(比如排除完全相同的项)
搜索区间:range_filter <= distance <= radius
不同 Metric Type 下的参数含义
# L2 距离(越小越相似)
# radius 是最大距离,range_filter 是最小距离
search_params_l2 = {
"metric_type": "L2",
"params": {
"radius": 0.5, # 只考虑平方欧氏距离 < 0.5 的向量
"range_filter": 0.0 # 包含距离为 0 的结果;如需排除完全相同项,应另设条件或使用合适的正下界
}
}
# COSINE 相似度(越大越相似,范围 -1 到 1)
# 注意:range_filter > radius(因为越大越好)
search_params_cosine = {
"metric_type": "COSINE",
"params": {
"radius": 0.3, # 只考虑余弦相似度 > 0.3 的向量
"range_filter": 0.99 # 排除相似度 > 0.99 的结果;0.99 端点包含在区间内
}
}
# IP(内积,值越大越相似)
search_params_ip = {
"metric_type": "IP",
"params": {
"radius": 0.5, # 只考虑 IP > 0.5
"range_filter": 0.98 # 排除 IP > 0.98 的结果;0.98 端点包含在区间内
}
}
warning COSINE 相似度下 radius 和 range_filter 的顺序
L2 距离中,radius > range_filter(外大内小)。 但 COSINE 相似度中,值越大越相似,所以 range_filter > radius(内大外小)。 很多人在这里搞反,导致范围搜索返回空结果。
范围搜索的典型应用场景
# 场景一:去重检测——检索与新文档高度相似的候选,受 limit 和底层检索配置限制
def find_near_duplicates(new_doc_vec, threshold=0.95):
"""检索余弦相似度 > threshold 的疑似重复候选,不保证穷尽。"""
results = client.search(
collection_name="documents",
data=[new_doc_vec],
limit=100,
search_params={
"metric_type": "COSINE",
"params": {
"radius": threshold, # COSINE: 内边界
"range_filter": 1.0 # COSINE: 包含 1.0 的相似度上界;是否检出全部相同向量仍受 limit 和底层检索配置限制
}
}
)
return results[0]
# 场景二:相似用户推荐——检索与目标用户足够相似的候选,最多返回 max_return 条
def find_similar_users(user_vec, min_similarity=0.8, max_return=200):
results = client.search(
collection_name="user_vectors",
data=[user_vec],
limit=max_return,
search_params={
"metric_type": "COSINE",
"params": {
"radius": min_similarity, # 只考虑相似度 > min_similarity 的候选
"range_filter": 0.9999 # 排除自身(相似度接近1.0)
}
}
)
return results[0]
分组搜索(Grouping Search)
分组搜索解决了一个常见问题:搜索结果集中在同一个来源(同一文档/同一作者/同一类别)。通过按某个字段分组,保证结果多样性。
问题场景
不用分组的 RAG 搜索结果(问题):
1. [0.95] 文档A - 第1段
2. [0.93] 文档A - 第2段
3. [0.91] 文档A - 第3段
4. [0.89] 文档A - 第4段 ← 前4条都来自同一文档!
5. [0.87] 文档B - 第1段
...
用分组搜索后(改善):
1. [0.95] 文档A - 第1段 ← 文档A最相关的那段
2. [0.87] 文档B - 第1段 ← 文档B最相关的那段
3. [0.81] 文档C - 第2段 ← 文档C最相关的那段
...(每个文档只取最好的几段)
使用方式
results = client.search(
collection_name="documents",
data=[query_vector],
anns_field="embedding",
limit=10, # 最终返回 10 个分组
search_params={
"metric_type": "COSINE",
"params": {"ef": 200}
},
group_by_field="doc_id", # 按 doc_id 字段分组
group_size=2, # 每个分组最多返回 2 条(默认为 1)
strict_group_size=False, # False:不足 group_size 时也返回
output_fields=["doc_id", "chunk_index", "content_preview"]
)
# 结果:最多返回 10 个不同 doc_id,每个最多 2 条 chunk
for hit in results[0]:
print(f"文档: {hit['entity']['doc_id']}, "
f"段落: {hit['entity']['chunk_index']}, "
f"相似度: {hit['distance']:.4f}")
print(f"内容: {hit['entity']['content_preview'][:60]}...")
print()
note group_size 的含义
group_size=1:每个分组只取最相似的 1 条(默认值)group_size=2:每个分组取最相似的 2 条strict_group_size=True:如果某个分组没有足够的记录,该分组会被丢弃(保证每组都有 group_size 条)strict_group_size=False(推荐):即使不足 group_size 条,也保留该分组
分组搜索的典型场景
# RAG:每篇文档最多返回 2 段,确保知识来源多样性
rag_results = client.search(
collection_name="knowledge_base",
data=[query_embedding],
limit=5, # 最多 5 篇文档
search_params={"metric_type": "COSINE", "params": {"ef": 300}},
group_by_field="doc_id",
group_size=2, # 每篇文档最多取 2 段
output_fields=["doc_id", "content", "source"]
)
# 电商搜索:每个品牌最多展示 3 款商品,避免结果被某个品牌垄断
product_results = client.search(
collection_name="products",
data=[query_vec],
limit=20, # 最多 20 个品牌
search_params={"metric_type": "COSINE", "params": {"ef": 500}},
group_by_field="brand_id",
group_size=3, # 每个品牌最多展示 3 款
output_fields=["product_id", "name", "brand_id", "price"]
)
迭代器(Iterator)
当需要遍历大量数据时(如数据导出、全量重计算、离线评测),用 offset 分页会越来越慢,应该使用迭代器,它基于游标机制高效遍历数据。
search_iterator:遍历向量搜索结果
from pymilvus import MilvusClient
from pymilvus.orm.iterator import SearchIterator
client = MilvusClient(uri="http://localhost:19530")
# 场景:分批检索余弦相似度 > 0.7 的候选,总返回量不超过 10000,不保证穷尽
search_iterator = client.search_iterator(
collection_name="documents",
data=[query_vector],
anns_field="embedding",
batch_size=200, # 每批返回 200 条
limit=10000, # 总上限(防止无限遍历)
search_params={
"metric_type": "COSINE",
"params": {
"ef": 500,
"radius": 0.7, # 只考虑相似度 > 0.7 的候选
"range_filter": 1.0
}
},
output_fields=["doc_id", "content_preview"]
)
# 迭代器使用模式
all_results = []
try:
while True:
batch = search_iterator.next()
if not batch: # 没有更多数据时返回空列表
break
all_results.extend(batch)
print(f"已获取 {len(all_results)} 条相似文档...")
finally:
search_iterator.close() # 必须关闭,释放服务端资源
print(f"总计找到 {len(all_results)} 条相似度 >= 0.7 的文档")
query_iterator:遍历大量标量数据
from pymilvus.orm.iterator import QueryIterator
# 场景:导出所有科幻类型的电影数据
query_iterator = client.query_iterator(
collection_name="movies",
filter='genre == "sci-fi"',
output_fields=["id", "title", "year", "rating"],
batch_size=500 # 每批 500 条
)
exported_data = []
try:
while True:
batch = query_iterator.next()
if not batch:
break
exported_data.extend(batch)
# 实时处理每批数据(如写入文件)
for item in batch:
print(f"{item['id']},{item['title']},{item['year']},{item['rating']}")
finally:
query_iterator.close()
print(f"共导出 {len(exported_data)} 条科幻电影数据")
tip 什么时候用迭代器
- 需要遍历超过 10000 条数据
- 数据导出或离线批处理任务
- 不知道总数据量,需要"扫到没有为止"
什么时候用 offset 分页:
- 面向用户的翻页功能(页数有限,通常不超过 100 页)
- 需要随机跳转到某一页
搜索结果处理
结果格式解析
results = client.search(
collection_name="movies",
data=[query_vector],
limit=5,
output_fields=["title", "genre", "rating"]
)
# results 是一个列表,长度等于 data 中查询向量的数量
# results[i] 是第 i 个查询向量对应的 Top-K 结果
# results[i][j] 是第 i 个查询的第 j 名命中
for query_idx, hits in enumerate(results):
print(f"=== 第 {query_idx + 1} 个查询的结果 ===")
for rank, hit in enumerate(hits):
print(f" 排名 {rank + 1}:")
print(f" 主键 ID: {hit['id']}")
print(f" 相似度: {hit['distance']:.6f}")
print(f" 字段数据: {hit['entity']}")
# hit['entity'] 是一个字典,包含 output_fields 指定的字段
print(f" 标题: {hit['entity']['title']}")
print(f" 评分: {hit['entity']['rating']}")
相似度分数的含义与数值范围
warning distance 字段的含义取决于 metric_type!
Milvus 把分数统一放在 distance 字段,但不同 metric_type 的含义和方向完全相反:
| Metric Type | distance 的含义 |
数值范围 | 越接近哪个值越相似 |
|---|---|---|---|
L2 |
平方欧氏距离(越小越相似) | [0, +∞) | 0 |
COSINE |
余弦相似度(越大越相似) | [-1, 1] | 1 |
IP |
内积(越大越相似,向量未归一化时) | (-∞, +∞) | +∞ |
# 根据 metric_type 正确解读分数
def interpret_score(distance: float, metric_type: str) -> str:
"""以下阈值仅为示例,应针对实际模型、数据集和任务评测校准。"""
if metric_type == "COSINE":
if distance > 0.9:
return "高度相似"
elif distance > 0.7:
return "较为相似"
elif distance > 0.5:
return "有一定相似性"
else:
return "相似度较低"
elif metric_type == "L2":
if distance < 0.1:
return "高度相似"
elif distance < 0.5:
return "较为相似"
elif distance < 1.0:
return "有一定相似性"
else:
return "相似度较低"
else:
return f"原始分数:{distance:.4f}"
分数归一化
当需要将不同路搜索的分数对比,或需要将 L2 距离转换为相似度时:
import math
def l2_to_similarity(l2_distance: float) -> float:
"""将 L2 距离转换为 0-1 之间的相似度分数"""
return 1.0 / (1.0 + l2_distance)
def cosine_to_percentage(cosine_sim: float) -> float:
"""将余弦相似度 [-1,1] 转换为百分比 [0,100]"""
return (cosine_sim + 1) / 2 * 100
# 对多个结果的分数归一化(Min-Max)
def normalize_scores(hits: list, metric_type: str = "COSINE") -> list:
if not hits:
return []
distances = [h['distance'] for h in hits]
if metric_type in ["COSINE", "IP"]:
# 分数越大越好:最大值归一化为 1
max_d = max(distances)
min_d = min(distances)
denom = max_d - min_d if max_d != min_d else 1
return [(h, (h['distance'] - min_d) / denom) for h in hits]
else: # L2
# 距离越小越好:最小值归一化为 1
max_d = max(distances)
min_d = min(distances)
denom = max_d - min_d if max_d != min_d else 1
return [(h, 1 - (h['distance'] - min_d) / denom) for h in hits]
# 使用示例
hits = results[0]
normalized = normalize_scores(hits, metric_type="L2")
for hit, norm_score in normalized:
print(f"原始距离: {hit['distance']:.4f}, 归一化相似度: {norm_score:.4f}")
性能调优与最佳实践
搜索延迟优化清单
# 1. 确保数据已 load 到内存
client.load_collection("movies") # 第一次需要,之后自动保持
# 2. 只返回必要的 output_fields
# 错误:output_fields=["*"](返回所有字段,包含大向量)
# 正确:output_fields=["id", "title", "rating"]
# 3. 合理设置 search_params
# HNSW:ef = limit 的 2-5 倍就够(不要无限加大)
search_params = {"metric_type": "COSINE", "params": {"ef": 50}} # limit=10 时
# 4. 批量搜索代替循环
results = client.search(collection_name="movies", data=batch_queries, limit=10)
# 5. 为标量过滤字段建索引
index_params.add_index(field_name="genre", index_type="INVERTED")
index_params.add_index(field_name="year", index_type="STL_SORT")
召回率提升策略
# 策略一:加大 ef(HNSW)或 nprobe(IVF)
high_recall_params = {"metric_type": "COSINE", "params": {"ef": 500}}
# 策略二:使用混合搜索(语义 + 关键词双重保障)
# 参见 [[#混合搜索(Hybrid Search)]] 章节
# 策略三:有 filter 时加大候选集
# 是否增大 ef 应结合标准过滤或迭代过滤模式评测;过滤严格并不意味着 ef 必须单调增大
filtered_params = {
"metric_type": "COSINE",
"params": {"ef": 1000} # 严格 filter 场景,放大 ef
}
# 策略四:分组搜索保证结果多样性
# 参见 [[#分组搜索(Grouping Search)]] 章节
常见错误速查
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| 返回结果数量少于 limit | 满足条件的数据不足,或过滤模式与检索配置限制了命中 | 先检查符合条件的记录数,再评测 ef/nprobe;仅在业务允许时放宽 filter |
| 搜索延迟很高 | output_fields 包含向量字段;ef 设置过大 | 去掉向量字段;减小 ef |
| 范围搜索返回空结果 | COSINE 下 radius 和 range_filter 写反了 | 检查参数方向(见 范围搜索(Range Search)) |
| 混合搜索分数异常 | 度量配置、融合权重或对融合分数的解释不合适 | 检查各路索引与度量配置,评测 WeightedRanker 权重;需要排名融合时可使用 RRFRanker |
| 分页越翻越慢 | 大 offset 可能增加查询开销,具体取决于实现与配置 | 批量遍历优先考虑 迭代器(Iterator),并实测分页开销 |
| 结果高度重复(同文档) | 没有分组,多段来自同一文档 | 使用 group_by_field |
总结
Milvus 的查询体系可以用这张决策图来总结:
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TB
Start(["我需要检索数据"]) --> Q1{{"有查询向量?"}}
Q1 -->|"有"| Search["向量搜索路径"]
Q1 -->|"没有"| Query["标量查询路径"]
Search --> Q2{{"需要多路向量融合?"}}
Q2 -->|"是(多字段或密+稀混合)"| Hybrid["hybrid_search<br/>+ RRF/WeightedRanker"]
Q2 -->|"否"| Q3{{"有距离范围要求?"}}
Q3 -->|"是(阈值而非Top-K)"| Range["search + range_filter/radius"]
Q3 -->|"否"| Q4{{"结果需要去重/多样性"}}
Q4 -->|"是(避免同来源集中)"| Group["search + group_by_field"]
Q4 -->|"否"| Basic["基础 search<br/>+/- filter"]
Query --> Q5{{"数据量很大?"}}
Q5 -->|"是(>1万条)"| Iterator["query_iterator<br/>游标遍历"]
Q5 -->|"否"| Q6{{"需要翻页?"}}
Q6 -->|"是"| Paged["query + offset/limit"]
Q6 -->|"否"| SimpleQ["基础 query<br/>+/- filter + count(*)"]
style Hybrid fill:#fef3c7,stroke:#f59e0b
style Range fill:#e0e7ff,stroke:#6366f1
style Group fill:#fce7f3,stroke:#ec4899
style Basic fill:#dcfce7,stroke:#16a34a
style Iterator fill:#fee2e2,stroke:#ef4444
核心查询接口速查:
| 接口 | 用途 | 关键参数 |
|---|---|---|
search |
向量 ANN 搜索 | data, limit, search_params |
search + filter |
向量搜索 + 标量过滤 | 加 filter 表达式 |
search + range |
距离范围搜索 | radius, range_filter |
search + group_by_field |
分组搜索去重 | group_by_field, group_size |
hybrid_search |
多路向量混合搜索 | AnnSearchRequest, RRFRanker |
query |
标量条件查询 | filter, offset, limit |
get |
按 ID 精确查询 | ids |
search_iterator |
大量向量搜索遍历 | batch_size, limit |
query_iterator |
大量标量数据遍历 | batch_size |
下一步:
- 了解 Schema 设计如何影响查询性能 → 07-Schema设计与数据建模
- 学习索引类型选择如何改变 search_params 配置 → 06-索引类型与选型指南
- 把这些查询技巧应用到 RAG 系统 → 10-RAG集成实战
- 理解 Collection、Partition 等核心概念 → 03-核心概念详解
#Milvus #向量数据库 #向量搜索 #ANN搜索 #混合搜索 #RAG #技术/向量数据库