演示:实现混合搜索系统
通过动手示例构建一个完整的混合搜索系统。
你将学到什么
- 混合搜索分步实现
- RRF 算法的实践应用
- 性能优化技术
- 测试和评估方法
您将发现什么
在上一课中,您学习了混合搜索和通用查询 API 的理论。今天,您将使用真实数据集进行实践,比较密集向量搜索和稀疏向量搜索,并使用融合算法将它们结合起来。
您将学习如何
- 创建包含密集和稀疏命名向量的集合
- 在实际查询上比较密集搜索与稀疏搜索的行为
- 实现倒数排名融合 (RRF) 进行混合搜索
- 探索基于分布的分数融合 (DBSF) 作为替代方案
- 理解每种方法的局限性和优势
混合搜索的挑战
同时使用语义(密集)和词法(稀疏)搜索会带来有趣的挑战
- 不同的评分系统:密集搜索通常使用余弦相似度 ([-1, 1]),稀疏搜索使用 BM25(无界)
- 不同的结果集:同一个查询可能会返回完全不同的文档
- 词汇敏感性:如果关键词不匹配,稀疏搜索可能会返回较少或不返回任何结果
- 用户多样性:有些用户知道确切的术语,另一些则使用自然语言
步骤 1:环境设置
安装所需的库
!pip install -q qdrant-client[fastembed]
为什么选择 fastembed 额外安装? 这包括 FastEmbed,它提供内置模型,无需额外依赖即可生成密集和稀疏嵌入。您不需要为 OpenAI 或其他嵌入提供商安装单独的库。
连接到 Qdrant Cloud
Qdrant Cloud 为混合搜索实验提供所需的持久性和性能
from qdrant_client import QdrantClient
from google.colab import userdata
client = QdrantClient(
location="https://your-cluster-url.cloud.qdrant.io:6333",
api_key=userdata.get("api-key")
)
使用 Google Colab 密钥: userdata.get() 函数访问存储在您的 Colab 环境中的密钥,类似于环境变量。这可以确保您的 API 密钥安全,并将其从代码中隐藏。
步骤 2:使用命名向量创建集合
对于混合搜索,我们需要一个支持稀疏和密集向量的集合。Qdrant 允许每个点有多个命名向量
from qdrant_client import models
# Define the collection name
collection_name = "hybrid_search_demo"
# Create our collection with both sparse (bm25) and dense vectors
client.create_collection(
collection_name=collection_name,
vectors_config={
"dense": models.VectorParams(
distance=models.Distance.COSINE,
size=384,
),
},
sparse_vectors_config={
"sparse": models.SparseVectorParams(
modifier=models.Modifier.IDF
)
}
)
关键配置详情
- 命名向量:
"dense"和"sparse"标识每种向量类型 - 密集配置:384 维(与 sentence-transformers/all-MiniLM-L6-v2 匹配)
- 余弦距离:语义相似度的典型选择
- IDF 修饰符:用于 BM25 稀疏向量的逆文档频率加权
- 同一集合:两个向量存在于相同的点上,用于混合搜索
步骤 3:上传奶酪数据集
我们使用一个包含 10 个文档的小型数据集,描述了不同类型的奶酪和奶酪菜肴。这个简单的数据集使得观察不同搜索方法的行为变得容易
documents = [
"Aged Gouda develops a crystalline texture and nutty flavor profile after 18 months of maturation.",
"Mature Gouda cheese becomes grainy and develops a rich, buttery taste with extended aging.",
"Brie cheese features a soft, creamy interior surrounded by an edible white rind.",
"This French cheese has a flowing, buttery center encased in a bloomy white crust.",
"Fresh mozzarella pairs beautifully with ripe tomatoes and basil leaves.",
"Classic Margherita pizza topped with tomato sauce, mozzarella, and fresh basil.",
"Parmesan requires at least 12 months of cave aging to develop its signature sharp taste.",
"Parmigiano-Reggiano's distinctive piquant flavor comes from extended maturation in controlled environments.",
"Grilled cheese sandwiches are the ultimate American comfort food for cold winter days.",
"Croque Monsieur combines ham and Gruyère in France's answer to the toasted cheese sandwich.",
]
现在上传,包含密集和稀疏嵌入
import uuid
client.upsert(
collection_name=collection_name,
points=[
models.PointStruct(
id=uuid.uuid4().hex,
vector={
"dense": models.Document(
text=doc,
model="sentence-transformers/all-MiniLM-L6-v2",
),
"sparse": models.Document(
text=doc,
model="Qdrant/bm25",
),
},
payload={"text": doc},
)
for doc in documents
]
)
关于这种方法
- 文档模型:使用指定模型自动生成嵌入
- 双重嵌入:每个点都获得密集和稀疏表示
- 小型数据集:10 个文档非常适合观察搜索行为差异
- 无需批量处理:对于生产环境中的大型数据集,请实现批量处理和重试逻辑
步骤 4:比较密集搜索与稀疏搜索
让我们创建辅助函数来独立测试每种搜索方法。首先是密集搜索
def dense_search(query: str) -> list[models.ScoredPoint]:
response = client.query_points(
collection_name=collection_name,
query=models.Document(
text=query,
model="sentence-transformers/all-MiniLM-L6-v2",
),
using="dense",
limit=3,
)
return response.points
现在是稀疏搜索
def sparse_search(query: str) -> list[models.ScoredPoint]:
response = client.query_points(
collection_name=collection_name,
query=models.Document(
text=query,
model="Qdrant/bm25",
),
using="sparse",
limit=3,
)
return response.points
跨两种方法测试查询
现在让我们在不同查询类型上运行这两种方法
queries = [
"nutty aged cheese",
"soft French cheese",
"pizza ingredients",
"a good lunch",
]
for query in queries:
print("Query:", query)
dense_results = dense_search(query)
print("Dense Results:")
for result in dense_results:
print("\t-", result.payload["text"], result.score)
sparse_results = sparse_search(query)
print("Sparse Results:")
for result in sparse_results:
print("\t-", result.payload["text"], result.score)
print()
结果的关键观察
- 密集和稀疏产生不同的排名:对于“有坚果味的老化奶酪”,稀疏搜索正确地将精确匹配识别为第一名,而密集搜索将语义相似的文档排名更高
- 有时排名匹配:对于“软法式奶酪”,两种方法都同意排名前列的结果,但置信度得分不同
- 密集搜索始终返回预期结果:密集搜索将始终返回 3 个结果,因为任何两个向量都具有一定的相似性,即使非常低
- 稀疏搜索可以返回较少的结果:对于“披萨配料”,稀疏搜索只返回 1 个结果。对于“一顿美餐”,由于词汇不匹配,稀疏搜索返回 0 个结果
- 词汇不匹配问题:当查询词不出现在文档中时,稀疏搜索会完全失败,而密集搜索则理解语义意图
步骤 5:使用倒数排名融合进行混合搜索
现在让我们使用 RRF 结合这两种方法。这种融合算法不比较不兼容的得分——它只使用排名顺序
def rrf_search(query: str) -> list[models.ScoredPoint]:
response = client.query_points(
collection_name=collection_name,
prefetch=[
models.Prefetch(
query=models.Document(
text=query,
model="Qdrant/bm25",
),
using="sparse",
limit=3,
),
models.Prefetch(
query=models.Document(
text=query,
model="sentence-transformers/all-MiniLM-L6-v2",
),
using="dense",
limit=3,
)
],
query=models.FusionQuery(fusion=models.Fusion.RRF),
limit=3,
)
return response.points
RRF 在此代码中的工作方式
- 从两者中预取:从稀疏和密集搜索中检索前 3 个
- FusionQuery:应用 RRF 算法组合排名
- 单次 API 调用:整个混合管道在一次请求中执行
- 结果:在两种方法中表现良好的文档排名更高
RRF 结果分析
for query in queries:
print("Query:", query)
rrf_results = rrf_search(query)
print("RRF Results:")
for result in rrf_results:
print("\t-", result.payload["text"], result.score)
print()
注意事项
- 两全其美:结果包括来自密集和稀疏搜索的文档
- 排名保留:当两种方法都同意时(例如“软法式奶酪”),RRF 保持共识
- 处理稀疏空白:当稀疏搜索返回较少结果(或不返回任何结果)时,密集搜索填补空白
- 平衡评分:两种方法都排名靠前的文档在 RRF 分数中得到提升
步骤 6:基于分布的分数融合 (DBSF)
RRF 并不是唯一可用的融合方法。DBSF 对每个查询的分数进行归一化,并将它们跨不同的检索器求和
def dbsf_search(query: str) -> list[models.ScoredPoint]:
response = client.query_points(
collection_name=collection_name,
prefetch=[
models.Prefetch(
query=models.Document(
text=query,
model="Qdrant/bm25",
),
using="sparse",
limit=3,
),
models.Prefetch(
query=models.Document(
text=query,
model="sentence-transformers/all-MiniLM-L6-v2",
),
using="dense",
limit=3,
)
],
query=models.FusionQuery(fusion=models.Fusion.DBSF),
limit=3,
)
return response.points
DBSF 结果
for query in queries:
print("Query:", query)
dbsf_results = dbsf_search(query)
print("DBSF Results:")
for result in dbsf_results:
print("\t-", result.payload["text"], result.score)
print()
比较 DBSF 与 RRF
- 在这个简单的例子中,DBSF 和 RRF 为所有查询产生了相同的排名
- 这并非普遍规则——不同的融合方法可能产生不同的结果
- 随着数据集更大、查询更复杂,差异会变得更加明显
- DBSF 考虑分数分布,而 RRF 只使用排名位置
评估注意事项
融合是否提高了搜索质量? 在没有适当评估的情况下,我们无法明确说明。原因如下
搜索质量的挑战
- 主观相关性:“最佳结果”取决于未知的用户意图
- 无真实数据:我们没有定义预期输出的参考数据集
- 上下文很重要:不同的用户可能对同一个查询有不同的偏好结果
适当的评估需要
- 真实数据集:为每个查询定义预期结果
- 指标:使用准确率、召回率、NDCG 或其他相关性指标
- 用户反馈:收集真实用户满意度数据
- A/B 测试:在生产环境中比较不同的策略
对于本次演示: 我们“目测”结果以理解行为,但生产系统需要严格的评估框架。
总结与要点
您已经构建了: 一个完整的混合搜索管道,使用 Qdrant 的通用查询 API,通过单次调用将密集语义搜索与稀疏关键词搜索结合起来。
主要见解
- 密集搜索与稀疏搜索行为:密集搜索始终返回结果(语义),稀疏搜索可能不返回任何结果(关键词匹配)
- 融合解决不兼容性:RRF 和 DBSF 结合排名而不比较不兼容的得分
- 单次 API 调用:通用查询 API 使复杂管道变得简单
- 互补优势:密集搜索处理模糊查询,稀疏搜索处理精确匹配
- 评估很重要:适当的测试需要真实数据集和指标
生产建议
- 从 RRF 开始——它简单有效
- 如果您需要分数分布感知,请测试 DBSF
- 为您的特定领域构建评估数据集
- 监控用户满意度指标
- 考虑添加专门的重新排名器以获得更好的质量
后续步骤和资源
下一步
- 试验参数:调整预取限制,尝试不同的嵌入模型
- 添加重新排名:探索更复杂的模型进行最终重新排名阶段,例如交叉编码器
- 构建真实数据:为您的用例创建评估数据集
- 在您自己的数据上测试:将这些技术应用于特定领域的数据集
其他资源
- Qdrant 文档:混合搜索 - 完整的技术参考
- 通用查询 API 指南 - 高级用法模式
准备好迎接下一个挑战了吗? 您已经掌握了混合搜索的基础知识。这些相同的技术可以扩展到数百万文档,并为生产搜索系统提供支持!