14.09-Qdrant实践
要点
- Qdrant 是 Rust 写的开源向量数据库——性能好、过滤能力强
- 支持稠密向量 + 稀疏向量混合检索
- 丰富的过滤语法,适合复杂的多租户和权限控制场景
- 部署方式:Docker(开发)、Kubernetes(生产)、Qdrant Cloud(托管)
- TypeScript SDK 完善,API 设计直观
- 适合中等规模、需要复杂过滤和混合检索的场景
1. 部署
Docker(本地开发)
docker run -d -p 6333:6333 -p 6334:6334 \
-v $(pwd)/qdrant_storage:/qdrant/storage \
qdrant/qdrant- 6333:REST API
- 6334:gRPC API
Docker Compose
# docker-compose.yml
services:
qdrant:
image: qdrant/qdrant
ports:
- "6333:6333"
- "6334:6334"
volumes:
- ./qdrant_storage:/qdrant/storage
environment:
- QDRANT__SERVICE__GRPC_PORT=6334Qdrant Cloud(生产)
const client = new QdrantClient({
url: 'https://your-cluster.qdrant.io',
apiKey: process.env.QDRANT_API_KEY,
})2. 创建 Collection
import { QdrantClient } from '@qdrant/js-client-rest'
const client = new QdrantClient({
url: process.env.QDRANT_URL ?? 'http://localhost:6333',
apiKey: process.env.QDRANT_API_KEY,
})
// 创建 collection
await client.createCollection('documents', {
vectors: {
size: 768, // 向量维度,要和 embedding 模型匹配
distance: 'Cosine', // Cosine / Euclid / Dot
},
// 可选:稀疏向量(用于混合检索)
sparse_vectors: {
'text-sparse': {
index: { on_disk: true },
},
},
// 优化:payload 索引
optimizers_config: {
default_segment_number: 2,
memmap_threshold: 20000,
},
})
// 为常用的过滤字段创建索引(提升过滤查询性能)
await client.createPayloadIndex('documents', {
field_name: 'document_id',
field_schema: 'keyword',
})
await client.createPayloadIndex('documents', {
field_name: 'user_id',
field_schema: 'keyword',
})
await client.createPayloadIndex('documents', {
field_name: 'created_at',
field_schema: 'datetime',
})3. 写入向量
单条写入
await client.upsert('documents', {
wait: true,
points: [
{
id: 'doc-1-chunk-0',
vector: embedding,
payload: {
text: 'chunk 内容',
document_id: 'doc-1',
document_title: '文档标题',
chunk_index: 0,
user_id: 'user-123',
tenant_id: 'tenant-456',
created_at: new Date().toISOString(),
},
},
],
})批量写入
export async function upsertChunks(
collectionName: string,
chunks: Array<{
id: string
vector: number[]
payload: Record<string, unknown>
}>
): Promise<void> {
// Qdrant 单次批量写入限制约 100-1000 条(取决于 payload 大小)
const BATCH_SIZE = 100
for (let i = 0; i < chunks.length; i += BATCH_SIZE) {
const batch = chunks.slice(i, i + BATCH_SIZE)
await client.upsert(collectionName, {
wait: true,
points: batch.map((chunk) => ({
id: chunk.id,
vector: chunk.vector,
payload: chunk.payload,
})),
})
}
}带稀疏向量的写入(混合检索)
await client.upsert('documents', {
wait: true,
points: [
{
id: 'doc-1-chunk-0',
vector: {
dense: denseEmbedding, // 768 维稠密向量
'text-sparse': sparseVector, // 稀疏向量
},
payload: { text: '...', document_id: 'doc-1' },
},
],
})稀疏向量可以用 SPLADE、BM25 等方法生成。
4. 相似度查询
基础查询
const results = await client.query('documents', {
query: queryVector,
limit: 5,
with_payload: true,
})
// results 结构
// {
// points: [
// { id: '...', version: 1, score: 0.92, payload: {...} },
// ...
// ]
// }带过滤的查询
const results = await client.query('documents', {
query: queryVector,
limit: 5,
filter: {
must: [
// AND 条件
{ key: 'tenant_id', match: { value: 'tenant-456' } },
{ key: 'created_at', range: { gte: '2024-01-01' } },
],
should: [
// OR 条件
{ key: 'document_id', match: { value: 'doc-1' } },
{ key: 'document_id', match: { value: 'doc-2' } },
],
must_not: [
// NOT 条件
{ key: 'status', match: { value: 'archived' } },
],
},
})过滤语法
// 关键词匹配
{ key: 'status', match: { value: 'active' } }
// 多值匹配(IN)
{ key: 'category', match: { any: ['tech', 'news'] } }
// 数值范围
{ key: 'score', range: { gte: 0.5, lte: 1.0 } }
// 日期范围
{ key: 'created_at', range: { gte: '2024-01-01', lt: '2025-01-01' } }
// 嵌套字段
{ key: 'metadata.author', match: { value: 'alice' } }
// 全文匹配(需要对字段建立全文索引)
{ key: 'content', match: { text: '关键词' } }5. 混合检索
Qdrant 支持稠密 + 稀疏向量融合。
// 混合查询
const results = await client.query('documents', {
prefetch: [
// 稠密向量检索
{
query: denseQueryVector,
using: 'dense',
limit: 20,
},
// 稀疏向量检索
{
query: sparseQueryVector,
using: 'text-sparse',
limit: 20,
},
],
// 融合策略
query: {
fusion: 'rrf', // Reciprocal Rank Fusion
},
limit: 5,
})融合策略:
rrf(Reciprocal Rank Fusion):按排名融合,不需要分数归一化dbsf(Distributed Border Score Fusion):按分数融合,需要分数在同一尺度
6. Scroll 遍历
当需要遍历所有数据时(如重建索引),用 scroll。
async function* scrollAll(collectionName: string) {
let offset: string | undefined
while (true) {
const result = await client.scroll(collectionName, {
limit: 100,
offset,
with_payload: true,
with_vector: true,
})
for (const point of result.points) {
yield point
}
if (!result.next_page_offset) break
offset = result.next_page_offset
}
}
// 使用
for await (const point of scrollAll('documents')) {
console.log(point.id, point.score)
}7. 多租户隔离
方案一:Payload 过滤
所有租户的数据在同一个 collection 里,用 tenant_id 字段过滤。
const results = await client.query('documents', {
query: queryVector,
filter: {
must: [{ key: 'tenant_id', match: { value: user.tenantId } }],
},
})优点:管理简单。 缺点:依赖代码正确添加过滤条件。
方案二:Collection 隔离
每个租户一个独立的 collection。
const collectionName = `documents_${tenantId}`
// 首次使用时创建
const collections = await client.getCollections()
if (!collections.collections.find((c) => c.name === collectionName)) {
await client.createCollection(collectionName, {
vectors: { size: 768, distance: 'Cosine' },
})
}
const results = await client.query(collectionName, { query: queryVector })优点:物理隔离,更安全。 缺点:collection 数量多时管理复杂。
推荐
- 租户数量少(< 100)→ Collection 隔离
- 租户数量多 → Payload 过滤 + 严格权限检查
8. 监控
健康检查
// src/middleware/qdrant-health.ts
app.get('/health/qdrant', async (c) => {
try {
const result = await client.getCollections()
return c.json({ status: 'ok', collections: result.collections.length })
} catch (err) {
return c.json({ status: 'error', error: String(err) }, 503)
}
})性能监控
Qdrant 暴露 Prometheus metrics 在 http://localhost:6333/metrics。
关键指标:
qdrant_rest_responses_total:请求总数qdrant_rest_responses_duration_seconds:请求延迟qdrant_collections_total:collection 数量qdrant_points_count:向量总数
9. 备份和恢复
# 备份:Qdrant 提供 snapshot API
curl -X POST http://localhost:6333/collections/documents/snapshots
# 恢复
curl -X PUT http://localhost:6333/collections/documents/snapshots \
-H "Content-Type: application/json" \
-d '{"location": "file:///path/to/snapshot"}'或者直接用 Docker volume 备份:
# 停止服务
docker compose stop qdrant
# 备份存储目录
tar -czf qdrant-backup.tar.gz ./qdrant_storage
# 恢复
tar -xzf qdrant-backup.tar.gz -C ./qdrant_storage
# 启动
docker compose start qdrant10. 一个完整的 Qdrant Store 实现
// src/services/rag/qdrant-store.ts
import { QdrantClient } from '@qdrant/js-client-rest'
export class QdrantVectorStore {
private client: QdrantClient
constructor() {
this.client = new QdrantClient({
url: process.env.QDRANT_URL ?? 'http://localhost:6333',
apiKey: process.env.QDRANT_API_KEY,
})
}
async initCollection(name: string, dimensions: number): Promise<void> {
const collections = await this.client.getCollections()
if (!collections.collections.find((c) => c.name === name)) {
await this.client.createCollection(name, {
vectors: { size: dimensions, distance: 'Cosine' },
})
}
}
async upsertChunks(collectionName: string, chunks: ChunkWithEmbedding[]): Promise<void> {
const BATCH_SIZE = 100
for (let i = 0; i < chunks.length; i += BATCH_SIZE) {
const batch = chunks.slice(i, i + BATCH_SIZE)
await this.client.upsert(collectionName, {
wait: true,
points: batch.map((chunk) => ({
id: chunk.id,
vector: chunk.embedding,
payload: {
text: chunk.content,
...chunk.metadata,
},
})),
})
}
}
async search(
collectionName: string,
queryVector: number[],
options: {
topK?: number
tenantId?: string
userId?: string
} = {}
): Promise<SearchResult[]> {
const { topK = 5, tenantId, userId } = options
const filter: any = { must: [] }
if (tenantId) filter.must.push({ key: 'tenant_id', match: { value: tenantId } })
if (userId) filter.must.push({ key: 'user_id', match: { value: userId } })
const results = await this.client.query(collectionName, {
query: queryVector,
limit: topK,
filter: filter.must.length > 0 ? filter : undefined,
with_payload: true,
})
return results.points.map((p) => ({
id: String(p.id),
score: p.score,
content: p.payload?.text,
metadata: p.payload ?? {},
}))
}
async deleteDocument(collectionName: string, documentId: string): Promise<void> {
await this.client.delete(collectionName, {
filter: { must: [{ key: 'document_id', match: { value: documentId } }] },
})
}
}总结
回顾这一节的要点:
- Qdrant 是 Rust 写的开源向量数据库,性能和过滤能力领先
- 部署:Docker(开发)、K8s(生产)、Qdrant Cloud(托管)
- 丰富的过滤语法 + payload 索引提升过滤查询性能
- 支持稠密 + 稀疏向量混合检索
- 多租户隔离:payload 过滤(大规模)或 collection 隔离(小规模)
- 监控:Prometheus metrics + 健康检查
- 适合中等规模、需要复杂过滤的场景
下一篇讲 Milvus 实践——面向超大规模的向量数据库。