将云推理与 Qdrant 结合用于向量搜索
在本教程中,我们将逐步构建一个混合语义搜索引擎,使用 Qdrant Cloud 的内置推理功能。您将学习如何
- 使用云推理自动嵌入数据,而无需运行本地模型,
- 将密集语义嵌入与稀疏 BM25 关键词结合,以及
- 使用倒数排名融合 (RRF) 执行混合搜索以检索最相关的结果。
初始化客户端
创建Qdrant Cloud 账户和专用的付费集群后,初始化 Qdrant 客户端。将cloud_inference设置为True以启用云推理。
from qdrant_client import QdrantClient
qdrant_client = QdrantClient(
"xyz-example.cloud-region.cloud-provider.cloud.qdrant.io",
api_key="<paste-your-api-key-here>",
cloud_inference=True,
timeout=30.0
)
import {QdrantClient} from "@qdrant/js-client-rest";
const client = new QdrantClient({
url: 'https://xyz-example.qdrant.io:6333',
apiKey: '<paste-your-api-key-here>',
});
use qdrant_client::Qdrant;
use qdrant_client::qdrant::{Document};
use qdrant_client::qdrant::{PointStruct, UpsertPointsBuilder};
let client = Qdrant::from_url("https://xyz-example.qdrant.io:6334")
.api_key("<paste-your-api-key-here>")
.build()
.unwrap();
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
QdrantClient client =
new QdrantClient(
QdrantGrpcClient.newBuilder("xyz-example.qdrant.io", 6334, true)
.withApiKey("<paste-your-api-key-here>")
.build());
using Qdrant.Client;
var client = new QdrantClient(
host: "xyz-example.cloud-region.cloud-provider.cloud.qdrant.io",
https: true,
apiKey: "<paste-your-api-key-here>"
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "xyz-example.cloud-region.cloud-provider.cloud.qdrant.io",
Port: 6334,
APIKey: "<paste-your-api-key-here>",
UseTLS: true,
})
创建集合
Qdrant 将向量和相关元数据存储在集合中。集合在创建时需要设置向量参数。在本例中,我们使用BM25用于稀疏向量,并使用all-minilm-l6-v2用于密集向量来设置集合。BM25 使用逆文档频率来降低出现在许多文档中的常见术语的权重,同时提升对于检索更具区分度的稀有术语的重要性。如果我们在bm25_sparse_vector命名稀疏向量的配置中启用,Qdrant 将处理 IDF 项的计算。
from qdrant_client import models
client.create_collection(
collection_name="{collection_name}",
vectors_config={
"dense_vector": models.VectorParams(
size=384,
distance=models.Distance.COSINE
)
},
sparse_vectors_config={
"bm25_sparse_vector": models.SparseVectorParams(
modifier=models.Modifier.IDF # Enable Inverse Document Frequency
)
}
)
client.createCollection("{collection_name}", {
vectors: {
dense_vector: { size: 384, distance: "Cosine" },
},
sparse_vectors: {
bm25_sparse_vector: {
modifier: "idf" // Enable Inverse Document Frequency
}
}
});
use qdrant_client::qdrant::{
CreateCollectionBuilder, Distance, Modifier, SparseVectorParamsBuilder,
SparseVectorsConfigBuilder, VectorParamsBuilder, VectorsConfigBuilder,
};
let mut vector_config = VectorsConfigBuilder::default();
vector_config.add_named_vector_params(
"dense_vector",
VectorParamsBuilder::new(384, Distance::Cosine),
);
let mut sparse_vectors_config = SparseVectorsConfigBuilder::default();
sparse_vectors_config.add_named_vector_params(
"bm25_sparse_vector",
SparseVectorParamsBuilder::default().modifier(Modifier::Idf), // Enable Inverse Document Frequency
);
client
.create_collection(
CreateCollectionBuilder::new("{collection_name}")
.vectors_config(vector_config)
.sparse_vectors_config(sparse_vectors_config),
)
.await?;
import io.qdrant.client.grpc.Collections.CreateCollection;
import io.qdrant.client.grpc.Collections.Distance;
import io.qdrant.client.grpc.Collections.Modifier;
import io.qdrant.client.grpc.Collections.SparseVectorConfig;
import io.qdrant.client.grpc.Collections.SparseVectorParams;
import io.qdrant.client.grpc.Collections.VectorParams;
import io.qdrant.client.grpc.Collections.VectorParamsMap;
import io.qdrant.client.grpc.Collections.VectorsConfig;
client
.createCollectionAsync(
CreateCollection.newBuilder()
.setCollectionName("{collection_name}")
.setVectorsConfig(
VectorsConfig.newBuilder()
.setParamsMap(
VectorParamsMap.newBuilder()
.putAllMap(
Map.of(
"dense_vector",
VectorParams.newBuilder()
.setSize(384)
.setDistance(Distance.Cosine)
.build())))
.setSparseVectorsConfig(
SparseVectorConfig.newBuilder()
.putMap(
"bm25_sparse_vector",
SparseVectorParams.newBuilder()
.setModifier(Modifier.Idf)
.build())))
.build())
.get();
await client.CreateCollectionAsync(
collectionName: "{collection_name}",
vectorsConfig: new VectorParamsMap
{
Map = {
["dense_vector"] = new VectorParams {
Size = 384, Distance = Distance.Cosine
},
}
},
sparseVectorsConfig: new SparseVectorConfig
{
Map = {
["bm25_sparse_vector"] = new() {
Modifier = Modifier.Idf, // Enable Inverse Document Frequency
}
}
}
);
client.CreateCollection(context.Background(), &qdrant.CreateCollection{
CollectionName: "{collection_name}",
VectorsConfig: qdrant.NewVectorsConfigMap(
map[string]*qdrant.VectorParams{
"dense_vector": {
Size: 384,
Distance: qdrant.Distance_Cosine,
},
}),
SparseVectorsConfig: qdrant.NewSparseVectorsConfig(
map[string]*qdrant.SparseVectorParams{
"bm25_sparse_vector": {
Modifier: qdrant.Modifier_Idf.Enum(),
},
},
),
})
添加数据
现在您可以添加示例文档、其相关元数据以及每个文档的点 ID。以下是miriad/miriad-4.4M数据集的示例
| qa_id | paper_id | question | year | venue | specialty | passage_text |
|---|---|---|---|---|---|---|
| 38_77498699_0_1 | 77498699 | 复发性多软骨炎的临床特征是什么? | 2006 | 耳鼻喉科互联网杂志 | 风湿病学 | 一位 45 岁男性因疼痛性肿胀就诊…… |
| 38_77498699_0_2 | 77498699 | 复发性多软骨炎有哪些治疗方法? | 2006 | 耳鼻喉科互联网杂志 | 风湿病学 | 患者在治疗后病情有所改善…… |
| 38_88124321_0_3 | 88124321 | 大动脉炎如何诊断? | 2015 | 自身免疫性疾病杂志 | 风湿病学 | 一位 32 岁女性,伴有疲劳和肢体疼痛…… |
我们不会摄取数据集中的所有条目,但为了演示目的,只取前一百个
from qdrant_client.http.models import PointStruct, Document
from datasets import load_dataset
import uuid
dense_model = "sentence-transformers/all-minilm-l6-v2"
bm25_model = "qdrant/bm25"
ds = load_dataset("miriad/miriad-4.4M", split="train[0:100]")
points = []
for idx, item in enumerate(ds):
passage = item["passage_text"]
point = PointStruct(
id=uuid.uuid4().hex, # use unique string ID
payload=item,
vector={
"dense_vector": Document(
text=passage,
model=dense_model
),
"bm25_sparse_vector": Document(
text=passage,
model=bm25_model
)
}
)
points.append(point)
client.upload_points(
collection_name="{collection_name}",
points=points,
batch_size=8
)
import { randomUUID } from "crypto";
const denseModel = "sentence-transformers/all-minilm-l6-v2";
const bm25Model = "qdrant/bm25";
// NOTE: loadDataset is a user-defined function.
// Implement it to handle dataset loading as needed.
const dataset = loadDataset("miriad/miriad-4.4M", "train[0:100]");
const points = dataset.map((item) => {
const passage = item.passage_text;
return {
id: randomUUID().toString(),
vector: {
dense_vector: {
text: passage,
model: denseModel,
},
bm25_sparse_vector: {
text: passage,
model: bm25Model,
},
},
};
});
await client.upsert("{collection_name}", { points });
use qdrant_client::qdrant::{
Document, NamedVectors, PointStruct, UpsertPointsBuilder,
};
use qdrant_client::Payload;
let dense_model = "sentence-transformers/all-minilm-l6-v2";
let bm25_model = "qdrant/bm25";
// NOTE: load_dataset is a user-defined function.
// Implement it to handle dataset loading as needed.
let dataset: Vec<_> = load_dataset("miriad/miriad-4.4M", "train[0:100]");
let points: Vec<PointStruct> = dataset
.iter()
.map(|item| {
let passage = item["passage_text"].as_str().unwrap();
let vectors = NamedVectors::default()
.add_vector(
"dense_vector",
Document::new(passage, dense_model),
)
.add_vector(
"bm25_sparse_vector",
Document::new(passage, bm25_model),
);
let payload = Payload::try_from(item.clone()).unwrap();
PointStruct::new(Uuid::new_v4().to_string(), vectors, payload)
})
.collect();
client
.upsert_points(UpsertPointsBuilder::new(collection_name, points))
.await?;
import static io.qdrant.client.PointIdFactory.id;
import static io.qdrant.client.VectorsFactory.namedVectors;
import static io.qdrant.client.VectorsFactory.vectors;
import io.qdrant.client.grpc.Points.Document;
import io.qdrant.client.grpc.Points.PointStruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
String denseModel = "sentence-transformers/all-minilm-l6-v2";
String bm25Model = "qdrant/bm25";
// NOTE: loadDataset is a user-defined function.
// Implement it to handle dataset loading as needed.
List<Map<String, String>> dataset = loadDataset("miriad/miriad-4.4M", "train[0:100]");
List<PointStruct> points = new ArrayList<>();
for (Map<String, String> item : dataset) {
String passage = item.get("passage_text");
PointStruct point =
PointStruct.newBuilder()
.setId(id(UUID.randomUUID()))
.setVectors(
namedVectors(
Map.of(
"dense_vector",
vectors(
Document.newBuilder().setText(passage).setModel(denseModel).build()),
"bm25_sparse_vector",
vectors(
Document.newBuilder().setText(passage).setModel(bm25Model).build()))))
.build();
points.add(point);
}
client.upsertAsync("{collection_name}", points).get();
var denseModel = "sentence-transformers/all-minilm-l6-v2";
var bm25Model = "qdrant/bm25";
// NOTE: LoadDataset is a user-defined function.
// Implement it to handle dataset loading as needed.
var dataset = LoadDataset("miriad/miriad-4.4M", "train[0:100]");
var points = new List<PointStruct>();
foreach (var item in dataset)
{
var passage = item["passage_text"].ToString();
var point = new PointStruct
{
Id = Guid.NewGuid(),
Vectors = new Dictionary<string, Vector>
{
["dense_vector"] = new Document
{
Text = passage,
Model = denseModel
},
["bm25_sparse_vector"] = new Document
{
Text = passage,
Model = bm25Model
}
},
};
points.Add(point);
}
await client.UpsertAsync(
collectionName: "{collectionName}",
points: points
);
denseModel := "sentence-transformers/all-minilm-l6-v2"
bm25Model := "qdrant/bm25"
// NOTE: loadDataset is a user-defined function.
// Implement it to handle dataset loading as needed.
dataset := loadDataset("miriad/miriad-4.4M", "train[0:100]")
points := make([]*qdrant.PointStruct, 0, 100)
for _, item := range dataset {
passage := item["passage_text"]
point := &qdrant.PointStruct{
Id: qdrant.NewID(uuid.New().String()),
Vectors: qdrant.NewVectorsMap(map[string]*qdrant.Vector{
"dense_vector": qdrant.NewVectorDocument(&qdrant.Document{
Text: passage,
Model: denseModel,
}),
"bm25_sparse_vector": qdrant.NewVectorDocument(&qdrant.Document{
Text: passage,
Model: bm25Model,
}),
}),
}
points = append(points, point)
}
_, err = client.Upsert(ctx, &qdrant.UpsertPoints{
CollectionName: "{collection_name},
Points: points,
})
设置输入查询
创建示例查询
query_text = "What is relapsing polychondritis?"
let query_text = "What is relapsing polychondritis?";
let query_text = "What is relapsing polychondritis?";
String queryText = "What is relapsing polychondritis?";
var queryText = "What is relapsing polychondritis?"
queryText := "What is relapsing polychondritis?"
运行向量搜索
在这里,您将提出一个问题,以便检索语义相关的结果。最终结果通过使用倒数排名融合进行重新排名获得。
results = client.query_points(
collection_name="{collection_name}",
prefetch=[
models.Prefetch(
query=Document(
text=query_text,
model=dense_model
),
using="dense_vector",
limit=5
),
models.Prefetch(
query=Document(
text=query_text,
model=bm25_model
),
using="bm25_sparse_vector",
limit=5
)
],
query=models.FusionQuery(fusion=models.Fusion.RRF),
limit=5,
with_payload=True
)
print(results.points)
const results = await client.query(collectionName, {
prefetch: [
{
query: {
text: queryText,
model: denseModel,
},
using: "dense_vector",
},
{
query: {
text: queryText,
model: bm25Model,
},
using: "bm25_sparse_vector",
},
],
query: {
fusion: "rrf",
},
});
use qdrant_client::qdrant::{Document, Fusion, PrefetchQueryBuilder, Query, QueryPointsBuilder};
let dense_prefetch = PrefetchQueryBuilder::default()
.query(Query::new_nearest(Document::new(query_text, dense_model)))
.using("dense_vector")
.build();
let bm25_prefetch = PrefetchQueryBuilder::default()
.query(Query::new_nearest(Document::new(query_text, bm25_model)))
.using("bm25_sparse_vector")
.build();
let query_request = QueryPointsBuilder::new(collection_name)
.add_prefetch(dense_prefetch)
.add_prefetch(bm25_prefetch)
.query(Query::new_fusion(Fusion::Rrf))
.with_payload(true)
.build();
let results = client.query(query_request).await?;
import static io.qdrant.client.QueryFactory.fusion;
import static io.qdrant.client.QueryFactory.nearest;
import io.qdrant.client.grpc.Points.Document;
import io.qdrant.client.grpc.Points.Fusion;
import io.qdrant.client.grpc.Points.PrefetchQuery;
import io.qdrant.client.grpc.Points.QueryPoints;
PrefetchQuery densePrefetch =
PrefetchQuery.newBuilder()
.setQuery(
nearest(Document.newBuilder().setText(queryText).setModel(denseModel).build()))
.setUsing("dense_vector")
.build();
PrefetchQuery bm25Prefetch =
PrefetchQuery.newBuilder()
.setQuery(nearest(Document.newBuilder().setText(queryText).setModel(bm25Model).build()))
.setUsing("bm25_sparse_vector")
.build();
QueryPoints request =
QueryPoints.newBuilder()
.setCollectionName("{collection_name}")
.addPrefetch(densePrefetch)
.addPrefetch(bm25Prefetch)
.setQuery(fusion(Fusion.RRF))
.build();
client.queryAsync(request).get();
await client.QueryAsync(
collectionName: "{collection_name}", prefetch: new List <PrefetchQuery> {
new() {
Query = new Document {
Text = queryText,
Model = bm25Model
},
Using = "bm25_sparse_vector",
Limit = 5
},
new() {
Query = new Document {
Text = queryText,
Model = denseModel
},
Using = "dense_vector",
Limit = 5
}
},
query: Fusion.Rrf,
limit: 5
);
prefetch := []*qdrant.PrefetchQuery{
{
Query: qdrant.NewQueryDocument(&qdrant.Document{
Text: queryText,
Model: bm25Model,
}),
Using: qdrant.PtrOf("bm25_sparse_vector"),
},
{
Query: qdrant.NewQueryDocument(&qdrant.Document{
Text: queryText,
Model: denseModel,
}),
Using: qdrant.PtrOf("dense_vector"),
},
}
client.Query(ctx, &qdrant.QueryPoints{
CollectionName: "{collection_name}",
Prefetch: prefetch,
Query: qdrant.NewQueryFusion(qdrant.Fusion_RRF),
})
语义搜索引擎将按照相关性顺序检索最相似的结果。
[ScoredPoint(id='9968a760-fbb5-4d91-8549-ffbaeb3ebdba',
version=0, score=14.545895,
payload={'text': "Relapsing Polychondritis is a rare..."},
vector=None, shard_key=None, order_value=None)]