后端开发

向量数据库为什么火了?

2026年,如果你还没接触过向量数据库,那你一定错过了AI时代最重要的基础设施之一。从关键词搜索到语义搜索的范式转变,正在重塑整个搜索技术栈。

关键词搜索:用户搜"苹果",只能匹配包含"苹果"两个字的文档 语义搜索:用户搜"苹果",能理解你可能想找"iPhone"、"MacBook"或"水果营养"

这种能力的跃迁,核心在于Embedding(向量嵌入)——将文本转化为高维空间中的数值向量,语义相近的文本在向量空间中距离更近。


Embedding模型选择

选择合适的Embedding模型是语义搜索的基础。以下是2026年主流模型的对比:

模型 维度 最大Token 多语言 开源 MTEB排名 特点
OpenAI text-embedding-3-large 3072 8191 Top 5 通用性强,API调用简单
OpenAI text-embedding-3-small 1536 8191 Top 15 性价比高
BGE-M3 1024 8192 Top 10 多语言多粒度,支持稠密+稀疏+ColBERT
Cohere embed-v4 1024 128000 Top 8 超长文本,多模态
GTE-Qwen2 1536 32768 Top 3 阿里开源,中文表现优异
E5-Mistral-7B 4096 32768 Top 2 大模型嵌入,精度最高但推理慢

选择建议

追求精度 + 有GPU → GTE-Qwen2 / E5-Mistral-7B
追求性价比 + 快速上线 → OpenAI text-embedding-3-small
中文为主 + 需要开源 → BGE-M3 / GTE-Qwen2
超长文档 → Cohere embed-v4
多语言 + 混合检索 → BGE-M3(同时输出稠密+稀疏向量)

Embedding生成示例

@Service
public class EmbeddingService {

    private final RestTemplate restTemplate;

    @Value("${embedding.api.url}")
    private String apiUrl;

    @Value("${embedding.api.key}")
    private String apiKey;

    @Value("${embedding.model}")
    private String model;

    public float[] generateEmbedding(String text) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setBearerAuth(apiKey);

        Map<String, Object> body = Map.of(
            "model", model,
            "input", text
        );

        HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers);
        ResponseEntity<Map> response = restTemplate.postForEntity(
            apiUrl + "/embeddings", request, Map.class
        );

        List<Double> embedding = (List<Double>) ((Map) ((List) response.getBody().get("data")).get(0)).get("embedding");
        float[] result = new float[embedding.size()];
        for (int i = 0; i < embedding.size(); i++) {
            result[i] = embedding.get(i).floatValue();
        }
        return result;
    }
}

主流向量数据库对比

特性 Milvus pgvector Qdrant Weaviate Chroma
类型 专用向量数据库 PostgreSQL扩展 专用向量数据库 专用向量数据库 轻量级嵌入式
语言 Go/C++ C Rust Go Python
索引类型 HNSW, IVF_PQ, Flat, SCANN HNSW, IVFFlat HNSW HNSW HNSW
水平扩展 ✅ 原生支持 ❌ 需Citus ✅ 分片 ✅ 分片
混合搜索 ✅ (SQL)
过滤 ✅ 标量过滤 ✅ SQL WHERE ✅ Payload过滤 ✅ GraphQL ✅ 元数据过滤
事务 ✅ ACID
运维复杂度 极低
适用场景 大规模生产 已有PG的项目 中等规模 全功能搜索 原型/小规模

如何选择?

已有PostgreSQL基础设施 → pgvector,零额外运维成本 千万级以上向量 → Milvus,原生分布式 中等规模 + Rust性能 → Qdrant 快速原型验证 → Chroma 需要GraphQL + 多模态 → Weaviate


Spring Boot + pgvector实战

pgvector是PostgreSQL的扩展,让你在已有数据库中原生支持向量搜索,无需引入新的基础设施。

1. 环境准备

-- 安装pgvector扩展
CREATE EXTENSION IF NOT EXISTS vector;

-- 创建文档表
CREATE TABLE documents (
    id BIGSERIAL PRIMARY KEY,
    title VARCHAR(500) NOT NULL,
    content TEXT NOT NULL,
    source VARCHAR(200),
    embedding vector(1536),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 创建HNSW索引(推荐)
CREATE INDEX idx_documents_embedding ON documents
    USING hnsw (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 64);

2. Spring Boot配置

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: postgres
    password: secret
  jpa:
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect

3. 实体定义

@Entity
@Table(name = "documents")
public class Document {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 500)
    private String title;

    @Column(nullable = false, columnDefinition = "TEXT")
    private String content;

    private String source;

    @Column(columnDefinition = "vector(1536)")
    private float[] embedding;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    public Document() {}

    public Document(String title, String content, String source, float[] embedding) {
        this.title = title;
        this.content = content;
        this.source = source;
        this.embedding = embedding;
        this.createdAt = LocalDateTime.now();
    }
}

4. 向量搜索Repository

@Repository
public class DocumentVectorRepository {

    private final JdbcTemplate jdbcTemplate;

    public DocumentVectorRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public List<DocumentSearchResult> searchSimilar(float[] queryEmbedding, int limit) {
        String vectorStr = Arrays.stream(queryEmbedding)
            .mapToObj(f -> String.format("%.6f", f))
            .collect(Collectors.joining(",", "[", "]"));

        String sql = """
            SELECT id, title, content, source,
                   1 - (embedding <=> ?::vector) AS similarity
            FROM documents
            WHERE embedding IS NOT NULL
            ORDER BY embedding <=> ?::vector
            LIMIT ?
            """;

        return jdbcTemplate.query(sql,
            (rs, rowNum) -> new DocumentSearchResult(
                rs.getLong("id"),
                rs.getString("title"),
                rs.getString("content"),
                rs.getString("source"),
                rs.getDouble("similarity")
            ),
            vectorStr, vectorStr, limit
        );
    }

    public List<DocumentSearchResult> searchWithFilter(
            float[] queryEmbedding, String category, int limit) {
        String vectorStr = Arrays.stream(queryEmbedding)
            .mapToObj(f -> String.format("%.6f", f))
            .collect(Collectors.joining(",", "[", "]"));

        String sql = """
            SELECT id, title, content, source,
                   1 - (embedding <=> ?::vector) AS similarity
            FROM documents
            WHERE embedding IS NOT NULL
              AND source = ?
            ORDER BY embedding <=> ?::vector
            LIMIT ?
            """;

        return jdbcTemplate.query(sql,
            (rs, rowNum) -> new DocumentSearchResult(
                rs.getLong("id"),
                rs.getString("title"),
                rs.getString("content"),
                rs.getString("source"),
                rs.getDouble("similarity")
            ),
            vectorStr, category, vectorStr, limit
        );
    }
}

5. 搜索服务

@Service
public class SemanticSearchService {

    private final EmbeddingService embeddingService;
    private final DocumentVectorRepository vectorRepository;

    public List<DocumentSearchResult> search(String query, int limit) {
        float[] queryEmbedding = embeddingService.generateEmbedding(query);
        return vectorRepository.searchSimilar(queryEmbedding, limit);
    }

    public List<DocumentSearchResult> searchByCategory(String query, String category, int limit) {
        float[] queryEmbedding = embeddingService.generateEmbedding(query);
        return vectorRepository.searchWithFilter(queryEmbedding, category, limit);
    }
}

索引类型:HNSW、IVF_PQ、Flat

索引类型 原理 查询速度 召回率 内存占用 构建速度 适用场景
Flat 暴力搜索 100% 极快 <10万条,精度优先
HNSW 层级导航小世界图 高(>95%) 10万-千万级,通用首选
IVF_PQ 倒排+乘积量化 中(85-95%) 亿级,内存受限场景
SCANN 各向异性量化 最快 Milvus专属,大规模

HNSW参数调优

-- m: 每个节点的邻居数,越大召回越高但内存越大
-- ef_construction: 构建时的搜索宽度,越大索引质量越高但构建越慢
CREATE INDEX idx_documents_embedding ON documents
    USING hnsw (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 64);

-- 查询时调整ef_search(pgvector 0.7+)
SET hnsw.ef_search = 100;  -- 默认40,增大可提高召回

经验法则m=16, ef_construction=64是大多数场景的甜蜜点。ef_search设为预期返回条数的2-5倍。


混合搜索:向量搜索 + BM25关键词搜索

纯粹的向量搜索在精确匹配场景下可能失效(如搜索特定ID、产品编号)。混合搜索将语义搜索与关键词搜索融合,取长补短。

混合搜索架构

用户查询
    ├──→ Embedding → 向量搜索 → 语义结果(权重α)
    ├──→ 分词 → BM25搜索 → 关键词结果(权重β)
    └──→ RRF融合 → 最终排序

pgvector + 全文检索实现

-- 创建全文检索索引
ALTER TABLE documents ADD COLUMN tsv tsvector
    GENERATED ALWAYS AS (to_tsvector('simple', coalesce(title, '') || ' ' || coalesce(content, ''))) STORED;

CREATE INDEX idx_documents_tsv ON documents USING GIN (tsv);

-- 混合搜索:向量相似度 + 全文检索
WITH vector_results AS (
    SELECT id, 1 - (embedding <=> '[0.1, 0.2, ...]'::vector) AS vector_score
    FROM documents
    ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
    LIMIT 50
),
text_results AS (
    SELECT id, ts_rank(tsv, plainto_tsquery('simple', '搜索关键词')) AS text_score
    FROM documents
    WHERE tsv @@ plainto_tsquery('simple', '搜索关键词')
    LIMIT 50
)
SELECT COALESCE(v.id, t.id) AS id,
       COALESCE(v.vector_score, 0) * 0.7 + COALESCE(t.text_score, 0) * 0.3 AS hybrid_score
FROM vector_results v
FULL OUTER JOIN text_results t ON v.id = t.id
ORDER BY hybrid_score DESC
LIMIT 20;

RRF(Reciprocal Rank Fusion)

public List<DocumentSearchResult> hybridSearch(String query, int limit) {
    float[] embedding = embeddingService.generateEmbedding(query);

    List<DocumentSearchResult> vectorResults = vectorRepository.searchSimilar(embedding, 50);
    List<DocumentSearchResult> keywordResults = fulltextRepository.search(query, 50);

    Map<Long, Double> rrfScores = new HashMap<>();
    int k = 60;

    for (int i = 0; i < vectorResults.size(); i++) {
        Long id = vectorResults.get(i).getId();
        rrfScores.merge(id, 1.0 / (k + i + 1), Double::sum);
    }

    for (int i = 0; i < keywordResults.size(); i++) {
        Long id = keywordResults.get(i).getId();
        rrfScores.merge(id, 1.0 / (k + i + 1), Double::sum);
    }

    return rrfScores.entrySet().stream()
        .sorted(Map.Entry.<Long, Double>comparingByValue().reversed())
        .limit(limit)
        .map(entry -> findDocumentById(entry.getKey(), vectorResults, keywordResults))
        .collect(Collectors.toList());
}

Rerank重排序

向量搜索的初步结果可能不够精准,Rerank模型对候选集进行精细排序,显著提升精度。

Rerank模型 延迟 精度提升 开源 特点
Cohere Rerank ~100ms +15-25% API调用,多语言
bge-reranker-v2-m3 ~50ms +10-20% 中文表现优异
Jina Reranker ~80ms +10-15% 长文档友好
cross-encoder/ms-marco ~60ms +10-15% 经典方案

典型流程:向量搜索取Top 50 → Rerank重排 → 返回Top 10。精度提升15-25%,延迟增加50-100ms,通常值得。


生产调优

维度选择

维度 模型示例 存储成本 精度 适用场景
384 MiniLM 对精度要求不高
768 BGE-base 通用场景
1024 BGE-M3 多语言
1536 OpenAI v3 很高 通用高质量
3072 OpenAI v3-large 很高 最高 精度优先

量化压缩

// Matryoshka维度截断:高维向量截断为低维
// 例如1536维 → 768维,存储减半,精度损失<3%
public float[] truncateEmbedding(float[] embedding, int targetDim) {
    if (targetDim >= embedding.length) return embedding;
    float[] truncated = new float[targetDim];
    System.arraycopy(embedding, 0, truncated, 0, targetDim);
    return truncated;
}

// 8位量化:float32 → int8,存储减少75%
public byte[] quantizeToInt8(float[] embedding) {
    float maxAbs = 0;
    for (float f : embedding) maxAbs = Math.max(maxAbs, Math.abs(f));

    byte[] quantized = new byte[embedding.length];
    for (int i = 0; i < embedding.length; i++) {
        quantized[i] = (byte) (embedding[i] / maxAbs * 127);
    }
    return quantized;
}

分片策略

数据规模 分片策略 说明
<100万 单节点 无需分片
100万-1000万 按业务分表 如按分类、按时间
1000万-1亿 Hash分片 均匀分布到多个分片
>1亿 Milvus分布式 原生支持PB级

成本分析:自建 vs 云服务

维度 自建Milvus Zilliz Cloud Pinecone pgvector自建
100万条1536维 ~$50/月 ~$65/月 ~$70/月 ~$30/月
1000万条1536维 ~$200/月 ~$300/月 ~$350/月 ~$150/月
运维成本
扩展性 需手动 自动 自动 需手动
数据安全 完全可控 云端 云端 完全可控
学习曲线 陡峭 极低

建议:起步阶段用pgvector(已有PG的情况下),日增向量超过10万条时考虑Milvus或Zilliz Cloud。Pinecone适合不想碰基础设施的团队,但成本偏高。


总结

向量数据库和语义搜索不是银弹,但在以下场景中价值巨大:

  1. 知识库/RAG:让AI基于企业私有数据回答问题
  2. 推荐系统:基于内容相似度推荐
  3. 语义去重:识别语义相同但表述不同的内容
  4. 智能客服:理解用户意图而非关键词匹配

从关键词到语义的范式转变,本质是从"匹配字面"到"理解意图"的升级。 选择合适的向量数据库和Embedding模型,结合混合搜索和Rerank,才能构建真正好用的语义搜索系统。

本站提供浏览器本地工具,免注册即可试用 →

#向量数据库#语义搜索#Embedding#Milvus#pgvector