我只是想确保我正确遵循混合搜索的想法,从而遵循 qdrant 实现。
矢量化数据库上的混合搜索:对密集嵌入执行搜索和关键字匹配搜索,最后一项可以在稀疏向量上完成。
完成简单的定义后,我们可以在 Qdrant 中创建一个向量化数据库,并且由于 qdrant 不支持子字符串搜索,或者没有对此进行优化,因此我们创建一个包含密集和稀疏向量的集合,例如:
baseURL = "http://localhost:6333"
client = QdrantClient(url=baseURL)
collection_name = "example_collection"
client.create_collection(
collection_name=collection_name,
vectors_config={
"text-dense": models.VectorParams(
size=1536, # OpenAI Embeddings
distance=models.Distance.COSINE,
)
},
sparse_vectors_config={
"text-sparse": models.SparseVectorParams(
index=models.SparseIndexParams(
on_disk=False,
)
)
},
)
因此,假设我们用以下内容更新了一些点:
points = [
PointStruct(
id=1,
vector={
"text-dense": [0.1] * 1536,
"text-sparse": models.SparseVector(indices=[6, 7], values=[1.0, 2.0]),
},
payload={"name": f"Example_{1}"},
)
]
client.upsert(
collection_name=collection_name,
wait=True,
points=points,
)
我们现在可以执行混合搜索,这可以通过一种非常基本的方式来实现,例如对数据库进行两个独立的查询,一个用于密集嵌入,另一个用于稀疏嵌入,例如:
# Query example once treated with specific models.
user_query_dense_vector = [0.099] * 1536
user_query_sparse_vector = {6: 2.0, 7: 3.0}
# Dense search
dense_results = client.search(
collection_name=collection_name,
query_vector=("text-dense", user_query_dense_vector),
with_vectors=True,
)
# Sparse search
sparse_results = client.search(
collection_name=collection_name,
query_vector=models.NamedSparseVector(
name="text-sparse",
vector=models.SparseVector(
indices=list(user_query_sparse_vector.keys()),
values=list(user_query_sparse_vector.values()),
),
),
with_vectors=True,
)
假设 user_query_dense_vector 和 user_query_sparse_vector 是用户查询经过模型处理后的结果向量,我们可以执行融合方法,例如倒数排名融合(RRF):1 / (k +rank)。我们基本上迭代密集和稀疏的结果,并计算检索到的每个块的分数。
combined_scores = {}
for rank, point in enumerate(dense_results, start=1):
if point.id not in combined_scores:
combined_scores[point.id] = 0
combined_scores[point.id] += 1 / (k + rank)
for rank, point in enumerate(sparse_results, start=1):
if point.id not in combined_scores:
combined_scores[point.id] = 0
combined_scores[point.id] += 1 / (k + rank)
fused_results = sorted(combined_scores.items(), key=lambda x: x[1], reverse=True)
结果在哪里:
[(1, 0.03278688524590164)]
这基本上只返回第一个点,因为只有一个点。
现在的问题是,如果我们遵循 Qdrant 文档,他们使用预取方法来实现混合搜索,如果我们省略 Matryoshka 分支、第一个整数搜索(为了更快的检索)和最后一个后期交互重新排名,我们应该基本上得到与上面代码相同的结果,我们分别搜索然后融合它们。
sparse_dense_rrf_prefetch = models.Prefetch(
prefetch=[
models.Prefetch(
query=user_query_dense_vector,
using="text-dense",
limit=25,
),
models.Prefetch(
query=models.SparseVector(
indices=list(user_query_sparse_vector.keys()),
values=list(user_query_sparse_vector.values()),
),
using="text-sparse",
limit=25,
),
],
# RRF fusion
query=models.FusionQuery(
fusion=models.Fusion.RRF,
),
)
client.query_points(
collection_name=collection_name,
prefetch=[sparse_dense_rrf_prefetch],
query=user_query_dense_vector,
using="text-dense",
with_payload=True,
limit=10,
)
显然,它返回数据库中的唯一点。
QueryResponse(points=[ScoredPoint(id=1, version=0, score=0.9999997, payload={'name': 'point_1'}, vector=None, shard_key=None, order_value=None)])
我想我期待的分数与上面从 RRF 实现的分数类似,而不是查询的稀疏向量与稀疏向量之间的余弦相似度,这正是我通过稀疏搜索实现的结果 (sparse_results) .
不知道为什么,但我觉得有点不对劲。是否有人实现了类似的东西并且可以证实预取确实正确实现了?
提前谢谢您!
您提供的代码有两个问题。
首先,应该改变k值。您没有共享定义 k 的代码部分,但您似乎正在使用 k = 60。应改为 k = 2。
其次,您正在将 rank 初始化为 1,而它应该在 0 初始化。
要进行这些更改,请使用以下代码:
combined_scores = {}
k = ?????
for rank, point in enumerate(dense_results, start=1):
if point.id not in combined_scores:
combined_scores[point.id] = 0
combined_scores[point.id] += 1 / (k + rank)
for rank, point in enumerate(sparse_results, start=1):
if point.id not in combined_scores:
combined_scores[point.id] = 0
combined_scores[point.id] += 1 / (k + rank)
fused_results = sorted(combined_scores.items(), key=lambda x: x[1], reverse=True)
并将其更改为:
combined_scores = {}
k = 2
for rank, point in enumerate(dense_results):
if point.id not in combined_scores:
combined_scores[point.id] = 0
combined_scores[point.id] += 1 / (k + rank)
for rank, point in enumerate(sparse_results):
if point.id not in combined_scores:
combined_scores[point.id] = 0
combined_scores[point.id] += 1 / (k + rank)