Qdrant 向量数据库使用文档

0 阅读14分钟

Qdrant 向量数据库使用文档

Qdrant ['kwɒdrənt] 是一款由 Rust 编写的高性能、可扩展的向量相似性搜索数据库,专注于存储、索引和检索高维向量。

目录


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

端口说明

端口协议用途
6333HTTP/gRPCREST API + Web Dashboard
6334gRPCgRPC 高性能通信

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

  1. 点击 Create Collection alt text转存失败,建议直接上传图片文件
  2. 填写配置:
参数说明示例值
Collection Name集合名称text_collection
Vector Size向量维度384 (MiniLM) / 1536 (OpenAI)
Distance距离度量Cosine
  1. 点击 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-0021536OpenAI 通用OpenAI
text-embedding-3-small512/1536OpenAI 高效OpenAI
all-MiniLM-L6-v2384本地快速HuggingFace
bge-large-zh-v1.51024中文语义HuggingFace
m3e-large1024中文通用HuggingFace
bce-embedding-base-v11024中文通用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     # 使用内存映射的阈值
  }
}

调优指南

  • 召回率优先:增大 mef_construct
  • 速度优先:使用 gRPC + 增大 ef 搜索参数
  • 内存优化:开启 on_disk: true 存储向量到磁盘

参考资源

资源链接
Qdrant 官方文档qdrant.tech/documentati…
Qdrant GitHubgithub.com/qdrant/qdra…
Spring AI 文档spring.io/projects/sp…
Qdrant Python 客户端github.com/qdrant/qdra…