Qdrant 向量数据库使用文档
Qdrant ['kwɒdrənt] 是一款由 Rust 编写的高性能、可扩展的向量相似性搜索数据库,专注于存储、索引和检索高维向量。
目录
- 1. 核心概念
- 2. Docker 安装
- 3. Web UI 使用
- 4. REST API
- 5. Spring Boot 整合
- 6. Spring AI 整合
- 7. 应用场景示例
- 8. 进阶主题
- 9. 运维指南
1. 核心概念
1.1 向量数据库简介
向量数据库(Vector Databases)用于高效地存储、索引和检索高维向量,特别适合处理非结构化数据(文本、图像、音频)的相似性搜索。
1.2 Qdrant 核心概念图
graph TB
subgraph Qdrant架构
A[Client 客户端] --> B[REST API / gRPC]
B --> C[Collection 集合]
C --> D[Point 点]
D --> E[Vector 向量]
D --> F[Payload 元数据]
end
subgraph 索引类型
G[HNSW 图索引]
H[IVF 倒排索引]
end
subgraph 距离度量
I[Cosine 余弦]
J[Euclidean 欧氏]
K[Dot Product 点积]
end
C --> G
C --> H
D --> I
D --> J
D --> K
1.3 核心概念说明
| 概念 | 说明 | 类比 |
|---|---|---|
| Collection | 存储一组相同维度向量的集合 | 关系型数据库的表 |
| Point | 集合中的实体,包含 ID、Vector、Payload | 表中的一行记录 |
| Vector | 表示数据特征的数值数组 | 文本的词嵌入、图像特征 |
| Payload | 附加的 JSON 元数据 | 用于过滤的标签、分类等 |
| HNSW | 分层可导航小世界图索引 | 高质量 ANN 搜索 |
| Distance | 向量相似度度量方式 | Cosine/Euclidean/Dot |
1.4 距离度量对比
graph LR
A[向量 A] -->|Cosine| B[衡量方向相似性<br/>适合文本语义搜索]
A -->|Euclidean| C[衡量空间直线距离<br/>适合图像/特征向量]
A -->|Dot Product| D[衡量向量投影相似度<br/>适合推荐系统]
| 度量方式 | 适用场景 | 特点 |
|---|---|---|
| Cosine 余弦相似度 | 文本语义搜索 | 只关心方向,不关心长度 |
| Euclidean 欧氏距离 | 图像/视频搜索 | 考虑向量绝对距离 |
| Dot Product 点积 | 推荐系统、评分预测 | 考虑向量长度和方向 |
2. Docker 安装
2.1 快速启动
# 拉取镜像
docker pull qdrant/qdrant
# 运行容器
docker run -d \
--name qdrant \
-p 6333:6333 \
-p 6334:6334 \
qdrant/qdrant
端口说明:
| 端口 | 协议 | 用途 |
|---|---|---|
| 6333 | HTTP/gRPC | REST API + Web Dashboard |
| 6334 | gRPC | gRPC 高性能通信 |
2.2 数据持久化
docker run -d \
--name qdrant \
-p 6333:6333 \
-p 6334:6334 \
-v /your/path/qdrant_storage:/qdrant/storage \
qdrant/qdrant
2.3 内存模式(临时测试用)
docker run --rm \
-p 6333:6333 \
-p 6334:6334 \
qdrant/qdrant --path ""
2.4 指定版本启动
docker run -d \
--name qdrant-1.4 \
-p 6333:6333 \
-p 6334:6334 \
-v /your/path/qdrant_data:/qdrant/storage \
qdrant/qdrant:v1.4.0
2.5 Docker Compose 部署
# docker-compose.yml
version: '3.8'
services:
qdrant:
image: qdrant/qdrant:latest
ports:
- "6333:6333" # REST API + Web Dashboard
- "6334:6334" # gRPC
volumes:
- ./qdrant_data:/qdrant/storage
2.6 生产环境配置
version: '3.8'
services:
qdrant:
image: qdrant/qdrant:latest
ports:
- "6333:6333" # REST API + Web Dashboard
- "6334:6334" # gRPC
volumes:
- ./qdrant_data:/qdrant/storage
environment:
- QDRANT__SERVICE__API_KEY=your_secret_key # 启用 API 认证
deploy:
resources:
limits:
memory: 4G # 限制内存
reservations:
memory: 2G
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:6333/readyz"]
interval: 30s
timeout: 10s
retries: 3
生产环境要点:
- 建议内存至少 4GB,大规模数据需要更多
- 生产环境务必启用 API Key 认证
- gRPC 端口性能比 REST 高 10 倍,推荐使用
2.7 配置文件详解
Qdrant 支持通过配置文件进行高级配置。配置文件默认位于 /qdrant/config/config.yaml:
# qdrant.yaml
service:
host: 0.0.0.0
http_port: 6333 # REST API 端口
grpc_port: 6334 # gRPC 端口
api_key: your_api_key # API 认证密钥
max_request_size_mb: 32 # 最大请求大小 (MB)
max_batch_size: 100 # 最大批量操作数
storage:
storage_path: /qdrant/storage # 数据存储路径
snapshots_path: /qdrant/snapshots # 快照存储路径
temp_path: /tmp/qdrant # 临时文件路径
on_disk_payload: false # Payload 是否存储到磁盘
cluster:
enabled: true # 启用集群模式
p2p_port: 6335 # 节点间通信端口
telemetry_disabled: false # 是否禁用遥测数据上报
使用自定义配置文件
# 通过命令行参数指定配置文件
docker run -d \
--name qdrant \
-p 6333:6333 \
-p 6334:6334 \
-v /your/path/qdrant.yaml:/qdrant/config/config.yaml \
-v /your/path/qdrant_storage:/qdrant/storage \
qdrant/qdrant
2.8 日志配置
Qdrant 本身会生成运行日志,日志文件存储在 /qdrant/logs/ 目录。
日志目录挂载
docker run -d \
--name qdrant \
-p 6333:6333 \
-p 6334:6334 \
-v /your/path/qdrant_storage:/qdrant/storage \
-v /your/path/qdrant_logs:/qdrant/logs \
qdrant/qdrant
日志级别配置
通过 RUST_LOG 环境变量配置 Qdrant 应用日志级别:
# docker-compose.yml
services:
qdrant:
image: qdrant/qdrant:latest
environment:
- RUST_LOG=info # 日志级别
日志级别说明:
| 级别 | 说明 | 使用场景 |
|---|---|---|
error | 只记录错误 | 生产环境最小化日志 |
warn | 警告和错误 | 生产环境推荐 |
info | 一般信息 | 调试时使用 |
debug | 详细调试信息 | 问题排查 |
trace | 最详细日志 | 开发调试 |
日志文件结构
Qdrant 生成的日志文件位于 /qdrant/logs/ 目录:
qdrant_logs/
├── qdrant.log # 主日志文件(包含所有运行日志)
└── qdrant.err.log # 错误日志(仅错误级别)
查看日志文件:
# 在容器内查看日志文件
docker exec qdrant cat /qdrant/logs/qdrant.log
# 实时跟踪日志
docker exec -it qdrant tail -f /qdrant/logs/qdrant.log
# 复制日志到宿主机
docker cp qdrant:/qdrant/logs/qdrant.log ./qdrant.log
2.9 数据持久化
Qdrant 的所有数据都存储在 /qdrant/storage 目录下。
数据目录结构
qdrant_storage/
├── collections/ # Collection 数据
│ └── {collection_name}/
│ ├── index/ # HNSW 索引文件
│ ├── payload_index/ # Payload 索引
│ ├── payload/ # Payload 数据
│ └── vectors/ # 向量数据
│
├── snapshots/ # 快照存储
│ └── {collection_name}/
│ └── {snapshot_file}.snapshot
│
├── raft_state/ # 集群状态(集群模式)
│
└── tmp/ # 临时文件(可忽略)
各文件说明:
| 目录/文件 | 说明 | 备份建议 |
|---|---|---|
collections/ | 所有 Collection 的向量和索引数据 | 必须备份 |
snapshots/ | 手动创建的快照 | 按需备份 |
raft_state/ | 集群元数据 | 集群模式必须备份 |
tmp/ | 临时文件 | 无需备份 |
2.10 完整部署示例
# docker-compose.yml - 完整配置示例
version: '3.8'
services:
qdrant:
image: qdrant/qdrant:latest
container_name: qdrant
restart: unless-stopped
ports:
- "6333:6333" # REST API + Web Dashboard
- "6334:6334" # gRPC
volumes:
- ./qdrant_data:/qdrant/storage # 数据持久化目录
- ./qdrant_logs:/qdrant/logs # 日志文件目录
environment:
- QDRANT__SERVICE__API_KEY=${QDRANT_API_KEY} # API 认证
- QDRANT__SERVICE__MAX_REQUEST_SIZE_MB=32 # 最大请求大小
- RUST_LOG=info # 日志级别
deploy:
resources:
limits:
memory: 8G
reservations:
memory: 4G
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:6333/readyz"]
interval: 30s
timeout: 10s
retries: 3
docker-compose up -d
持久化总结:
| 挂载路径 | Qdrant 内部路径 | 说明 |
|---|---|---|
./qdrant_data | /qdrant/storage | 向量数据、索引、快照等 |
./qdrant_logs | /qdrant/logs | 日志文件(qdrant.log, qdrant.err.log) |
3. Web UI 使用
访问 http://localhost:6333/dashboard
3.1 创建 Collection
- 点击 Create Collection

- 填写配置:
| 参数 | 说明 | 示例值 |
|---|---|---|
| Collection Name | 集合名称 | text_collection |
| Vector Size | 向量维度 | 384 (MiniLM) / 1536 (OpenAI) |
| Distance | 距离度量 | Cosine |
- 点击 Create
3.2 添加 Points
以 JSON 格式添加数据:
[
{
"id": 1,
"vector": [0.1, 0.2, 0.3, ...],
"payload": {
"text": "这是一段示例文本",
"category": "技术"
}
},
{
"id": 2,
"vector": [0.4, 0.5, 0.6, ...],
"payload": {
"text": "另一段文本内容",
"category": "技术"
}
}
]
3.3 执行搜索
{
"vector": [0.15, 0.25, ...],
"limit": 10,
"with_payload": true,
"filter": {
"must": [
{
"key": "category",
"match": {"value": "技术"}
}
]
}
}
4. REST API
API 基础地址:http://localhost:6333
💡 性能提示:gRPC 接口比 REST 快 10 倍,大规模应用建议使用 gRPC。gRPC 地址:
grpc://localhost:6334
4.1 API 认证
# 带 API Key 的请求
curl -X PUT "http://localhost:6333/collections/my_collection" \
-H "Content-Type: application/json" \
-H "api-key: your_api_key" \
-d '{
"vectors_config": {
"size": 384,
"distance": "Cosine"
}
}'
4.1 Collection 管理
创建 Collection
curl -X PUT "http://localhost:6333/collections/my_collection" \
-H "Content-Type: application/json" \
-d '{
"vectors_config": {
"size": 384,
"distance": "Cosine"
}
}'
查看所有 Collection
curl http://localhost:6333/collections
查看 Collection 详情
curl http://localhost:6333/collections/my_collection
删除 Collection
curl -X DELETE "http://localhost:6333/collections/my_collection"
4.2 Point 管理
Upsert(插入或更新)
⚠️ 注意:向量维度必须与 Collection 创建时指定的
size一致。
curl -X PUT "http://localhost:6333/collections/my_collection/points" \
-H "Content-Type: application/json" \
-d '{
"points": [
{
"id": 1,
"vector": [0.05, 0.12, 0.07, 0.11, 0.23, 0.18, ...], // 必须与 Collection 的 size 一致
"payload": {"text": "Hello Qdrant", "tag": "greeting"}
},
{
"id": 2,
"vector": [0.21, 0.15, 0.09, 0.31, 0.08, 0.27, ...],
"payload": {"text": "Vector search demo", "tag": "demo"}
}
]
}'
💡 向量维度说明:示例中的
...表示省略。实际使用时,向量维度必须完全匹配(384 维就用 384 个数值)。可以使用 Embedding 模型生成正确维度的向量。
Retrieve(按 ID 查询)
curl -X POST "http://localhost:6333/collections/my_collection/points/retrieve" \
-H "Content-Type: application/json" \
-d '{"ids": [1, 2, 3]}'
Delete(删除)
curl -X POST "http://localhost:6333/collections/my_collection/points/delete" \
-H "Content-Type: application/json" \
-d '{"points": [1, 2]}'
4.3 搜索 API
基础搜索
⚠️ 注意:查询向量维度必须与 Collection 的向量维度一致。
curl -X POST "http://localhost:6333/collections/my_collection/points/search" \
-H "Content-Type: application/json" \
-d '{
"vector": [0.15, 0.25, 0.07, 0.11, 0.23, 0.18, ...], // 必须与 Collection 的 size 一致
"limit": 10,
"with_payload": true
}'
带过滤条件的搜索
{
"vector": [0.15, 0.25, ...],
"limit": 10,
"with_payload": true,
"filter": {
"must": [ // 必须满足
{"key": "category", "match": {"value": "技术"}}
],
"must_not": [ // 排除条件
{"key": "status", "match": {"value": "deleted"}}
],
"should": [ // 满足任一即可
{"key": "tag", "match": {"value": "重要"}},
{"key": "tag", "match": {"value": "紧急"}}
]
}
}
范围过滤
{
"vector": [0.15, 0.25, ...],
"limit": 10,
"filter": {
"must": [
{"key": "price", "range": {"gte": 100, "lte": 500}},
{"key": "timestamp", "range": {"gte": 1678886400}}
]
}
}
4.4 Scroll API(滚动查询/分页)
Scroll API 用于分页获取 Collection 中的 Points,不进行向量相似度搜索。
基础滚动查询
curl -X POST "http://localhost:6333/collections/my_collection/points/scroll" \
-H "Content-Type: application/json" \
-d '{
"limit": 10,
"with_payload": true,
"with_vector": false
}'
响应示例:
{
"points": [
{
"id": 1,
"vector": [0.1, 0.2, 0.3],
"payload": {"text": "文档1", "city": "北京"}
}
],
"next_page_offset": "10"
}
带过滤条件的滚动查询
curl -X POST "http://localhost:6333/collections/my_collection/points/scroll" \
-H "Content-Type: application/json" \
-d '{
"limit": 10,
"filter": {
"must": [
{
"key": "city",
"match": {
"any": ["北京", "上海", "深圳"]
}
}
]
}
}'
分页滚动查询
# 第一页
curl -X POST "http://localhost:6333/collections/my_collection/points/scroll" \
-H "Content-Type: application/json" \
-d '{"limit": 10, "with_payload": true}'
# 下一页(使用上一页返回的 offset)
curl -X POST "http://localhost:6333/collections/my_collection/points/scroll" \
-H "Content-Type: application/json" \
-d '{"limit": 10, "offset": "10", "with_payload": true}'
4.5 过滤条件完整指南
| 匹配类型 | 说明 | 示例 |
|---|---|---|
match.value | 精确匹配单个值 | {"key": "city", "match": {"value": "北京"}} |
match.any | 匹配数组中任一值 | {"key": "city", "match": {"any": ["北京", "上海"]}} |
match.text | 字符串包含匹配 | {"key": "title", "match": {"text": "教程"}} |
range | 数值/日期范围 | {"key": "price", "range": {"gte": 100, "lte": 500}} |
datetime_range | 日期时间范围 | {"key": "date", "datetime_range": {"gte": "2024-01-01T00:00:00Z"}} |
nested | 嵌套对象查询 | {"key": "author", "nested": {"key": "name", "match": {"value": "张三"}}} |
is_empty | 字段为空检查 | {"key": "tags", "is_empty": {"is_empty": true}} |
is_null | 字段为 null 检查 | {"key": "deleted_at", "is_null": true} |
has_id | 指定 ID 列表 | {"has_id": [1, 2, 3]} |
has_vector | 是否包含向量 | {"has_vector": true} |
复合过滤示例:
curl -X POST "http://localhost:6333/collections/my_collection/points/search" \
-H "Content-Type: application/json" \
-d '{
"vector": [0.15, 0.25, ...],
"limit": 10,
"filter": {
"must": [
{"key": "status", "match": {"value": "active"}},
{"key": "price", "range": {"gte": 100, "lte": 500}}
],
"must_not": [
{"key": "category", "match": {"value": "已删除"}},
{"key": "city", "match": {"any": ["东京", "伦敦"]}}
],
"should": [
{"key": "is_featured", "match": {"value": true}},
{"key": "rating", "range": {"gte": 4.5}}
]
}
}'
4.6 API 流程图
sequenceDiagram
participant Client
participant Qdrant as Qdrant Server
Client->>Qdrant: PUT /collections 创建集合
Qdrant-->>Client: 200 OK
Client->>Qdrant: PUT /points 插入向量
Qdrant-->>Client: 200 OK
Client->>Qdrant: POST /search 搜索向量
Qdrant->>Qdrant: HNSW 图索引查询
Qdrant->>Qdrant: Payload 条件过滤
Qdrant-->>Client: 返回 Top-K 结果
5. Spring Boot 整合
5.1 添加依赖
<dependency>
<groupId>io.qdrant</groupId>
<artifactId>qdrant-client</artifactId>
<version>1.8.0</version>
</dependency>
5.2 配置文件
# application.yml
qdrant:
host: localhost
port: 6333
api-key: your_api_key # 如需认证
timeout: 30
5.3 配置类
@Configuration
public class QdrantConfig {
@Value("${qdrant.host:localhost}")
private String host;
@Value("${qdrant.port:6333}")
private int port;
@Value("${qdrant.api-key:}")
private String apiKey;
@Bean
public QdrantClient qdrantClient() {
QdrantClientConfig config = new QdrantClientConfig(host, port);
if (apiKey != null && !apiKey.isEmpty()) {
config.withApiKey(apiKey);
}
return new QdrantClient(config);
}
}
5.4 Service 层使用
@Service
@RequiredArgsConstructor
public class QdrantService {
private final QdrantClient qdrantClient;
private static final String COLLECTION = "documents";
private static final int VECTOR_DIM = 384;
/**
* 初始化集合
*/
public void initCollection() throws ExecutionException, InterruptedException {
boolean exists = qdrantClient.getCollections().get()
.getCollectionsList()
.stream()
.anyMatch(c -> c.getName().equals(COLLECTION));
if (!exists) {
qdrantClient.createCollectionAsync(COLLECTION,
VectorParams.newBuilder()
.setSize(VECTOR_DIM)
.setDistance(Distance.COSINE)
.build()
).get();
}
}
/**
* 添加文档
*/
public void addDocument(String id, float[] vector, Map<String, Object> payload)
throws ExecutionException, InterruptedException {
PointStruct point = PointStruct.newBuilder()
.setId(id)
.addAllVector(toList(vector))
.putAllPayload(payload)
.build();
qdrantClient.upsertAsync(COLLECTION, List.of(point), null).get();
}
/**
* 搜索相似文档
*/
public List<ScoredPoint> searchSimilar(float[] queryVector, int topK)
throws ExecutionException, InterruptedException {
SearchParams params = SearchParams.newBuilder()
.setHnswEf(128)
.setExact(false)
.build();
return qdrantClient.searchAsync(COLLECTION, queryVector, params, topK)
.get()
.getResultList();
}
/**
* 带过滤的搜索
*/
public List<ScoredPoint> searchWithFilter(float[] queryVector, int topK,
Filter filter) throws ExecutionException, InterruptedException {
SearchParams params = SearchParams.newBuilder()
.setHnswEf(128)
.build();
return qdrantClient.searchAsync(COLLECTION, queryVector, filter,
params, topK).get().getResultList();
}
private List<Float> toList(float[] array) {
return Arrays.stream(array).boxed().toList();
}
}
5.5 生成向量的辅助类
@Service
public class EmbeddingService {
// 方式一:使用 OpenAI Embedding
@Autowired
private EmbeddingClient embeddingClient;
public float[] generateEmbedding(String text) {
EmbeddingResponse response = embeddingClient.embedFor(List.of(text));
return response.getResult().get(0).getEmbedding();
}
// 方式二:使用本地模型(如 Sentence-Transformers)
// 需要引入 sentence-transformers 依赖
public float[] generateEmbeddingLocal(String text) {
// HuggingFaceEmbeddings huggingFace = new HuggingFaceEmbeddings();
// return huggingFace.embedQuery(text);
throw new UnsupportedOperationException("请实现本地模型嵌入");
}
}
5.6 Controller 示例
@RestController
@RequestMapping("/api/vector")
@RequiredArgsConstructor
public class VectorController {
private final QdrantService qdrantService;
private final EmbeddingService embeddingService;
@PostMapping("/search")
public List<Map<String, Object>> search(@RequestBody SearchRequest request) {
try {
float[] queryVector = embeddingService.generateEmbedding(request.getQuery());
List<ScoredPoint> results = qdrantService.searchSimilar(queryVector, request.getLimit());
return results.stream()
.map(point -> Map.<String, Object>of(
"id", point.getId(),
"score", point.getScore(),
"payload", point.getPayloadMap()
))
.toList();
} catch (Exception e) {
throw new RuntimeException("搜索失败", e);
}
}
}
6. Spring AI 整合
Spring AI 提供了统一的 VectorStore 接口,简化向量数据库集成。
💡 核心概念区分:
- VectorStore(Qdrant):负责存储向量数据
- EmbeddingModel:负责生成向量(将文本转为数值数组)
- 两者是独立的,不需要将 EmbeddingModel 配置到 Qdrant
6.1 添加依赖
<!-- Qdrant 存储 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-qdrant-vectorstore-spring-boot-starter</artifactId>
</dependency>
<!-- Embedding 模型(选择其一) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!-- 或使用本地模型 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-huggingface-spring-boot-starter</artifactId>
</dependency>
6.2 配置文件
spring:
ai:
# Qdrant 存储配置
vectorstore:
qdrant:
host: localhost
port: 6333
collection-name: spring_ai_docs
# vector-dimensions: 1536 # 向量维度,默认根据 embedding 模型自动推断
# Embedding 模型配置
embedding:
openai:
api-key: ${OPENAI_API_KEY}
# model: text-embedding-3-small # 默认 1536 维
# model: text-embedding-3-large # 256/1024/3072 维
# 或者使用 HuggingFace 本地模型
# embedding:
# huggingface:
# api-key: ${HF_API_KEY}
6.3 使用 VectorStore
@Service
@RequiredArgsConstructor
public class SpringAIVectorService {
private final VectorStore vectorStore;
private final EmbeddingModel embeddingModel;
/**
* 添加文档(自动生成向量)
*/
public void addDocument(String content, Map<String, Object> metadata) {
Document document = new Document(content, metadata);
vectorStore.add(List.of(document));
}
/**
* 批量添加文档
*/
public void addDocuments(List<Document> documents) {
vectorStore.add(documents);
}
/**
* 相似性搜索
*/
public List<Document> search(String query, int topK) {
SearchRequest request = SearchRequest.builder()
.query(query)
.topK(topK)
.similarityThreshold(0.7f)
.build();
return vectorStore.similaritySearch(request);
}
/**
* 带过滤的搜索
*/
public List<Document> searchWithFilter(String query, String category) {
Filter filter = Filter.builder()
.EQ("category", category)
.build();
SearchRequest request = SearchRequest.builder()
.query(query)
.topK(10)
.filterFunction(filter)
.build();
return vectorStore.similaritySearch(request);
}
/**
* 删除文档
*/
public void deleteDocument(String id) {
vectorStore.delete(List.of(id));
}
}
6.4 Spring AI 架构图
graph TB
subgraph 应用层
A[Spring Boot Application]
B[Document]
end
subgraph Spring AI
C[EmbeddingModel]
D[VectorStore]
end
subgraph Qdrant
E[Collection]
F[Point]
end
A --> B
B --> C
C -->|向量| D
D -->|REST/gRPC| E
E --> F
B -->|自动转换| F
7. 应用场景示例
7.1 语义搜索
场景:基于语义而非关键词的文档搜索
@Service
@RequiredArgsConstructor
public class SemanticSearchService {
private final QdrantService qdrantService;
private final EmbeddingService embeddingService;
public List<Map<String, Object>> search(String query, int limit) {
try {
// 1. 将查询文本转为向量
float[] queryVector = embeddingService.generateEmbedding(query);
// 2. 执行向量搜索
List<ScoredPoint> results = qdrantService.searchSimilar(queryVector, limit);
// 3. 转换结果
return results.stream()
.map(this::toResult)
.toList();
} catch (Exception e) {
throw new RuntimeException("搜索失败", e);
}
}
private Map<String, Object> toResult(ScoredPoint point) {
return Map.of(
"id", point.getId().getUuid(),
"score", point.getScore(),
"content", point.getPayloadMap().get("content"),
"title", point.getPayloadMap().get("title")
);
}
}
7.2 推荐系统
场景:基于用户兴趣推荐相似商品
@Service
@RequiredArgsConstructor
public class RecommendationService {
private final QdrantService qdrantService;
private final EmbeddingService embeddingService;
/**
* 商品相似推荐
*/
public List<Long> recommendProducts(long productId, int limit) {
// 1. 获取目标商品的向量
// 2. 搜索相似商品
// 3. 排除自身
// ...
return List.of();
}
/**
* 用户兴趣推荐
*/
public List<Long> recommendForUser(long userId, int limit) {
// 1. 获取用户历史行为(浏览/购买/收藏)
List<float[]> userInterestVectors = getUserInterestVectors(userId);
// 2. 计算用户兴趣向量(取平均或加权)
float[] userProfile = calculateUserProfile(userInterestVectors);
// 3. 搜索相似商品
return qdrantService.searchSimilar(userProfile, limit + 10)
.stream()
.filter(p -> !isPurchased(userId, p.getId())) // 排除已购买的
.limit(limit)
.map(p -> Long.parseLong(p.getId().getUuid()))
.toList();
}
private float[] calculateUserProfile(List<float[]> vectors) {
int dim = vectors.get(0).length;
float[] profile = new float[dim];
for (float[] v : vectors) {
for (int i = 0; i < dim; i++) {
profile[i] += v[i];
}
}
int count = vectors.size();
for (int i = 0; i < dim; i++) {
profile[i] /= count;
}
return profile;
}
}
7.3 RAG 应用(检索增强生成)
@Service
@RequiredArgsConstructor
public class RAGService {
private final QdrantService qdrantService;
private final EmbeddingService embeddingService;
private final ChatClient chatClient;
/**
* RAG 检索 + 生成
*/
public String ragQuery(String question) {
try {
// 1. 检索相关文档
float[] queryVector = embeddingService.generateEmbedding(question);
List<ScoredPoint> docs = qdrantService.searchSimilar(queryVector, 5);
// 2. 构建上下文
String context = docs.stream()
.map(d -> d.getPayloadMap().get("content").toString())
.collect(Collectors.joining("\n\n"));
// 3. 构建 prompt 并调用 LLM
String prompt = String.format("""
基于以下上下文回答问题。如果上下文中没有相关信息,请说明不知道。
上下文:
%s
问题:%s
""", context, question);
return chatClient.call(prompt);
} catch (Exception e) {
throw new RuntimeException("RAG 查询失败", e);
}
}
}
7.4 混合搜索(Hybrid Search)
混合搜索结合关键词搜索(BM25)和向量搜索(Semantic),兼顾精确匹配和语义理解。
7.4.1 概念说明
| 搜索类型 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 关键词搜索 | TF-IDF / BM25 评分 | 精确匹配关键词 | 无法理解语义 |
| 向量搜索 | 语义相似度 | 理解同义词、上下文 | 可能遗漏精确词 |
| 混合搜索 | 两者结合 | 兼顾精确与语义 | 实现复杂 |
7.4.2 Qdrant 实现方案
Qdrant 通过 Sparse Vector + Dense Vector 实现混合搜索:
用户查询
├── Sparse Vector(稀疏向量)→ BM25 关键词匹配
└── Dense Vector(密集向量)→ 语义向量搜索
↓
分数融合(RRF / Rounded Robin)
↓
最终排序结果
7.4.3 配置 Collection
{
"vectors_config": {
"params": {
"dense": {
"size": 768,
"distance": "Cosine"
}
},
"sparse_vectors": {
"sparse-dynamic": {}
}
},
"sparse_vectors_config": {
"sparse-dynamic": {
"modifier": "idf",
"ignore_error": false
}
}
}
7.4.4 Java 实现
@Service
@RequiredArgsConstructor
public class HybridSearchService {
private final QdrantClient qdrantClient;
private final EmbeddingModel embeddingModel;
private static final String COLLECTION = "hybrid_collection";
/**
* 混合搜索
*/
public List<HybridSearchResult> hybridSearch(String query, int limit)
throws ExecutionException, InterruptedException {
// 1. 生成密集向量(语义搜索)
float[] denseVector = embeddingModel.embed(query);
// 2. 生成稀疏向量(关键词搜索)- 使用词频分析
Map<String, Float> sparseVector = generateSparseVector(query);
// 3. 并行执行两种搜索
SearchParams params = SearchParams.newBuilder()
.setHnswEf(128)
.build();
// 密集向量搜索
List<ScoredPoint> denseResults = qdrantClient.searchAsync(
COLLECTION, "dense", denseVector, params, limit * 2
).get().getResultList();
// 稀疏向量搜索
List<ScoredPoint> sparseResults = qdrantClient.searchAsync(
COLLECTION, "sparse-dynamic", sparseVector, params, limit * 2
).get().getResultList();
// 4. RRF 分数融合
return fusionResults(denseResults, sparseResults, limit, 60);
}
/**
* 生成稀疏向量(基于词频)
*/
private Map<String, Float> generateSparseVector(String text) {
Map<String, Float> sparse = new HashMap<>();
String[] words = text.toLowerCase().split("\\s+");
// 简单的 TF 计算,实际可用 BM25
for (String word : words) {
if (word.length() > 2) {
sparse.put(word, 1.0f);
}
}
return sparse;
}
/**
* RRF(Reciprocal Rank Fusion)分数融合
* @param k RRF 参数,通常设为 60
*/
private List<HybridSearchResult> fusionResults(
List<ScoredPoint> dense,
List<ScoredPoint> sparse,
int limit,
int k) {
Map<String, Double> scores = new HashMap<>();
// Dense 结果评分
for (int i = 0; i < dense.size(); i++) {
String id = dense.get(i).getId().toString();
double rrf = 1.0 / (k + i + 1);
scores.merge(id, rrf, Double::sum);
}
// Sparse 结果评分
for (int i = 0; i < sparse.size(); i++) {
String id = sparse.get(i).getId().toString();
double rrf = 1.0 / (k + i + 1);
scores.merge(id, rrf, Double::sum);
}
// 按分数排序
return scores.entrySet().stream()
.sorted((a, b) -> Double.compare(b.getValue(), a.getValue()))
.limit(limit)
.map(e -> new HybridSearchResult(e.getKey(), e.getValue()))
.toList();
}
}
7.4.5 混合搜索流程图
graph TB
A[用户查询] --> B[文本处理]
B --> C[Sparse Vector]
B --> D[Dense Vector]
C --> E[BM25 搜索]
D --> F[Embedding 模型]
F --> G[向量相似度搜索]
E --> H[RRF 分数融合]
G --> H
H --> I[综合排序]
I --> J[返回 Top-K 结果]
8. 进阶主题
8.1 索引配置
{
"vectors_config": {
"size": 384,
"distance": "Cosine"
},
"hnsw_config": {
"m": 16,
"ef_construct": 100
}
}
| 参数 | 说明 | 推荐值 |
|---|---|---|
m | 每个节点的连接数 | 8-64 |
ef_construct | 构建时的搜索宽度 | 100-500 |
8.2 Payload 索引
# 为 category 字段创建索引
curl -X PUT "http://localhost:6333/collections/my_collection/index" \
-H "Content-Type: application/json" \
-d '{
"field_name": "category",
"field_schema": "keyword"
}'
8.3 Named Vectors(多向量/多模态)
支持同一个 Point 存储多个向量,适合多模态场景(如同时存储文本和图像向量):
{
"vectors_config": {
"params": {
"text_vector": {
"size": 384,
"distance": "Cosine"
},
"image_vector": {
"size": 512,
"distance": "Dot"
}
},
"on_disk": false
}
}
Java 客户端使用:
// 插入多向量
Map<String, List<Float>> vectors = new HashMap<>();
vectors.put("text_vector", toList(textEmbedding));
vectors.put("image_vector", toList(imageEmbedding));
PointStruct point = PointStruct.newBuilder()
.setId(id)
.putAllVectors(vectors)
.putAllPayload(payload)
.build();
// 指定向量名搜索
qdrantClient.searchAsync(
COLLECTION,
"text_vector", // 指定向量名称
queryVector,
params,
topK
).get();
适用场景:
| 场景 | 说明 |
|---|---|
| 多模态搜索 | 同时支持文本和图像检索 |
| 跨模型对比 | 不同 Embedding 模型的结果对比 |
| 混合检索 | 文本向量 + 图像向量融合搜索 |
8.4 性能优化建议
| 优化方向 | 具体措施 |
|---|---|
| 内存 | 确保有足够的内存缓存活跃数据 |
| 批量操作 | 使用批量 upsert 减少网络开销 |
| 异步操作 | 使用 *Async 方法 |
| 索引调优 | 根据数据量调整 HNSW 参数 |
| 分片 | 大规模数据使用分片部署 |
8.5 常用向量模型
| 模型 | 向量维度 | 适用场景 | 来源 |
|---|---|---|---|
| text-embedding-ada-002 | 1536 | OpenAI 通用 | OpenAI |
| text-embedding-3-small | 512/1536 | OpenAI 高效 | OpenAI |
| all-MiniLM-L6-v2 | 384 | 本地快速 | HuggingFace |
| bge-large-zh-v1.5 | 1024 | 中文语义 | HuggingFace |
| m3e-large | 1024 | 中文通用 | HuggingFace |
| bce-embedding-base-v1 | 1024 | 中文通用 | HuggingFace |
9. 运维指南
9.1 监控指标
Qdrant 提供健康检查和监控端点:
# 健康检查
curl http://localhost:6333/readyz
# 集群状态(集群模式)
curl http://localhost:6333/cluster
# 详细指标
curl http://localhost:6333/metrics
关键监控指标:
| 指标 | 说明 | 告警阈值 |
|---|---|---|
collections_total | 集合数量 | - |
points_total | 向量总数 | - |
memory_allocated_bytes | 内存占用 | > 80% 限制 |
requests_total | 请求总数 | - |
requests_failures_total | 失败请求数 | > 1% |
9.2 备份与恢复
# 创建快照
curl -X POST "http://localhost:6333/collections/my_collection/snapshots" \
-H "Content-Type: application/json"
# 列出快照
curl "http://localhost:6333/collections/my_collection/snapshots"
# 下载快照
curl "http://localhost:6333/collections/my_collection/snapshots/{snapshot_name}" \
-o snapshot.snapshot
# 从快照恢复
curl -X PUT "http://localhost:6333/collections/my_collection/snapshots/{snapshot_name}/recovery" \
-H "Content-Type: application/json" \
-d '{"location": "/path/to/snapshot.snapshot"}'
9.3 常见问题(FAQ)
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 搜索结果为空 | 向量维度不匹配 | 确认 Embedding 模型维度与 Collection 一致 |
| 搜索很慢 | HNSW 参数过低 | 增大 ef 参数或使用 gRPC |
| 内存占用过高 | 数据量超出内存 | 增加内存或使用分片部署 |
| 插入失败 | Collection 不存在 | 先创建 Collection 或开启 auto-create |
| 相似度分数异常 | 未归一化向量 | 使用 Cosine 距离或先归一化向量 |
| API 认证失败 | API Key 错误 | 检查环境变量配置和请求头 |
9.4 性能调优建议
{
"hnsw_config": {
"m": 16, # 连接数,越大越精确但越慢
"ef_construct": 200, # 构建索引时的搜索宽度
"full_scan_threshold": 10000 # 小于此数据量使用全表扫描
},
"optimizers_config": {
"indexing_threshold": 20000, # 触发索引的向量数
"memmap_threshold": 50000 # 使用内存映射的阈值
}
}
调优指南:
- 召回率优先:增大
m和ef_construct - 速度优先:使用 gRPC + 增大
ef搜索参数 - 内存优化:开启
on_disk: true存储向量到磁盘
参考资源
| 资源 | 链接 |
|---|---|
| Qdrant 官方文档 | qdrant.tech/documentati… |
| Qdrant GitHub | github.com/qdrant/qdra… |
| Spring AI 文档 | spring.io/projects/sp… |
| Qdrant Python 客户端 | github.com/qdrant/qdra… |