深入浅出RAG——第3章:向量数据库入门

📖 本章学习目标

  • 解释为什么向量数据库是 RAG 系统的必要组件,而非"可有可无的优化"
  • 用自己的话描述 HNSW 算法如何实现快速的近似最近邻搜索
  • 使用 TypeScript 连接 Pinecone 或 Chroma,完成索引创建、向量插入和相似度查询
  • 根据部署模式、成本、扩展性等维度选型向量数据库

《第2章:文本嵌入 ------ 让计算机理解语义的桥梁》中,我们学会了如何用 Embedding 模型将文本转化为向量,并用余弦相似度衡量语义距离。但回到 RAG 的现实场景中,你的知识库可能有数千篇文档、几十万条文本片段,这些内容可能存储在数据库等介质里,如果需要对每条查询都遍历全库做余弦相似度计算(暴力搜索),那么延迟会高到不可接受。

向量数据库就是为解决这个规模问题而生的。它不仅仅是存储向量的容器,更是一个为高维向量检索专门优化的搜索引擎。

一、为什么需要向量数据库

1. 暴力搜索的困境

假设通过embedding后,你的知识库索引了 100 万条文档片段,每条是 1536 维的向量。那么一次查询需要做 100 万次余弦相似度计算,每次计算涉及 1536 次乘法和加法。这在诸如单线程的 Node.js 中,这个计算可能需要几十秒甚至更多。

我们可以写一个程序来做一个简单的性能估算:

typescript 复制代码
// 暴力搜索的性能测试
function bruteForceSearch(
  queryVector: number[],
  database: number[][],
  k: number
): Array<{ index: number; score: number }> {
  const scores = database.map((vec, index) => ({
    index,
    score: cosineSimilarity(queryVector, vec),
  }));
  
  return scores.sort((a, b) => b.score - a.score).slice(0, k);
}

// 模拟 100 万条向量数据
const databaseSize = 1_000_000;
const dimension = 1536;
const database: number[][] = [];

for (let i = 0; i < databaseSize; i++) {
  database.push(Array.from({ length: dimension }, () => Math.random()));
}

const queryVector = Array.from({ length: dimension }, () => Math.random());

console.time("暴力搜索");
const results = bruteForceSearch(queryVector, database, 10);
console.timeEnd("暴力搜索");
// 在我的机器上输出: 暴力搜索: 45234ms (约 45 秒)

在实际的应用中,RAG 查询通常被要求在 200ms 以内返回检索结果,用户才能获得流畅的问答体验。如果按照上面这种暴力搜索的方式就是差了两个数量级。即使你使用多线程或 GPU 加速,暴力搜索的线性复杂度 O(Nxd) 也决定了它无法应对百万级以上的数据规模。

2. 近似最近邻搜索(ANN)

为了实现在大规模高维向量数据集中高效的搜索,一种叫做 近似最近邻搜索(Approximate Nearest Neighbor,简称 ANN) 的算法策略被提出。它通过牺牲极小的精确度来换取巨大查询速度提升,其核心思想是不保证返回绝对的最优解,但能在可接受的误差范围内快速找到"足够近"的结果。

graph LR A[查询向量] --> B{向量数据库} B --> C[ANN 索引] C --> D[近似 Top-K<br/>精度损失 < 1%] D --> E[返回结果<br/>耗时 < 10ms] A --> F[暴力搜索] F --> G[精确 Top-K] G --> H[返回结果<br/>耗时 > 10000ms]

ANN 的本质上是是一种用空间换时间的策略,它预先构建一个索引数据结构,使得搜索时无需遍历所有向量,而是沿着索引快速定位到候选区域,大幅缩小搜索范围。

目前,主流的的 ANN 算法包括:

  • HNSW(Hierarchical Navigable Small World):目前最流行的算法,平衡了速度和精度
  • IVF(Inverted File Index):基于聚类的索引,适合超大规模数据
  • LSH(Locality-Sensitive Hashing):基于哈希的近似搜索,速度极快但精度较低
  • DiskANN:微软开发的磁盘友好算法,适合内存受限的场景

在这些算法中,HNSW 因其在速度和精度之间的优秀平衡,成为大多数向量数据库的默认选择。

3. 除搜索之外的额外能力

现代向量数据库不只是"存向量+搜向量",它们还提供 RAG 场景不可或缺的能力。

(1)Metadata Filtering(元数据过滤)

在实际应用中,你很少需要对整个数据库进行搜索。通常会有一些前置条件,比如:

  • 只搜索某个类别的文档(category = "technical"
  • 只搜索最近更新的文档(date > "2026-01-01"
  • 只搜索特定作者的文档(author = "张三"

如果先做向量搜索再过滤,可能召回的结果都被过滤掉了,导致空结果。正确的做法是先过滤再搜索,或者在搜索过程中同时考虑元数据条件。

typescript 复制代码
// 错误的做法:先搜索再过滤
const results = await collection.query({ queryEmbedding, nResults: 10 });
const filtered = results.filter(doc => doc.metadata.category === "technical");
// 可能 filtered 为空!

// 正确的做法:搜索时带上过滤条件
const results = await collection.query({
  queryEmbedding,
  nResults: 10,
  where: { category: "technical" }, // 在搜索时就应用过滤
});

(2)实时 CRUD(增删改查)

知识库不是一成不变的。新的文档会不断加入,旧的文档可能被修改或删除。向量数据库需要支持以下能力:

  • 增量插入:新文档加入时,只需为新文档生成向量并插入,无需重建整个索引
  • 按 ID 删除:删除过时或错误的文档
  • 部分更新:修改文档内容后,重新生成向量并更新

这些操作都需要保证索引的一致性,不能因为频繁更新导致搜索性能下降。

(3)水平扩展

当数据量从百万级增长到十亿级时,单机无法容纳所有数据。因此,向量数据库还需要支持:

  • 分片(Sharding):将数据分散到多个节点上
  • 副本(Replication):每个分片有多个副本,提高可用性和读取性能
  • 负载均衡:查询请求自动路由到合适的节点

这些分布式能力对于生产级 RAG 系统至关重要。

二、简单理解HNSW 算法

HNSW(Hierarchical Navigable Small World) 是目前最主流的 ANN 算法,Pinecone、Weaviate、Qdrant、Chroma 等都支持它。理解它的原理不需要数学推导,一个生活化的直觉就够了。

1. 跳表类比

想象一个跳表:你要在一栋 20 层楼里找一个人。你不会从 1 楼开始逐个房间敲门(暴力搜索)。你会先坐电梯到第 10 层(高层跳跃),发现不对;再到第 15 层(中层跳跃),还不对;最后从 14 层开始逐层搜索(低层精确查找)。

HNSW 就是把这个"多层跳跃"的思想应用在向量搜索上:

graph TD subgraph &#34;第 2 层(高层:长距离跳跃)&#34; A2[入口节点] --- B2[候选节点] end subgraph &#34;第 1 层(中层:中等跳跃)&#34; B1[候选节点] --- C1[候选节点] --- D1[候选节点] end subgraph &#34;第 0 层(底层:精确搜索)&#34; B0[候选节点] --- C0[候选节点] --- D0[候选节点] --- E0[结果] end A2 --> B2 B2 -.-> B1 B1 --> C1 --> D1 D1 -.-> B0 B0 --> C0 --> D0 --> E0

每一层都是一个图结构(节点=向量,边=近邻关系)。高层节点稀疏、边跨度大,用于快速跳跃;低层节点密集,用于精确搜索。搜索从最高层开始,每层找到最近节点后"下钻"到下一层,最终在底层还原出近似 Top-K。

2. HNSW 的关键参数

HNSW 的性能和精度由几个关键参数控制,分别是MefConstructionefSearch

(1)M(最大连接数)

每个节点在图中最多可以有多少条边。M 越大,图的连通性越好,搜索精度越高,但索引占用的内存也越多。典型值一般在16-64之间,M 翻倍,内存占用约增加 50%,搜索速度提升约 20%,使用时按照实际情况做权衡调整。需要注意的是M 是建图时确定的参数,一旦索引建好就无法修改,改 M 意味着重建整个索引。

(2)efConstruction(构建时的搜索范围)

构建索引时,每个新节点需要找到多少个近邻来建立连接。efConstruction 越大,索引质量越高,但构建时间越长。通常介于100-400之间,推荐起始值是200。大量的实践经验总结efConstruction ≥ M × 2较佳。同样,efConstruction 也是建图时确定的参数,修改需要重建索引。

(3)efSearch(搜索时的搜索范围)

查询时,算法维护的候选节点数量。efSearch 越大,搜索结果越精确,但查询延迟越高。通常为50-500。efSearch 控制的是查询时,这意味着你可以根据业务需求在运行时动态调整,无需重建索引。

3. HNSW 的优缺点

优点

  • 速度快:在百万级数据上,查询延迟通常在 1-10ms
  • 精度高:召回率通常能达到 95% 以上
  • 支持动态更新:可以随时插入新节点,无需重建索引
  • 实现成熟:有大量的开源实现和优化

缺点

  • 内存占用高:索引结构需要额外的内存,通常是原始数据的 2-4 倍
  • 构建时间长:对于千万级数据,索引构建可能需要数小时
  • 参数调优复杂:需要根据数据规模和业务需求调整多个参数

三、主流向量数据库对比和选型

目前市面上向量数据库有很多,比如MilvusQdrantWeaviatePineconeChroma 。还有一些数据库插件,比如PostgreSQL的扩展 pgvector。它们大致情况如下表所示。

特性 Chroma Pinecone Qdrant pgvector Weaviate
部署模式 自托管 / 嵌入式 仅 SaaS 托管 自托管 / 云托管 PostgreSQL 扩展 自托管 / 云托管
ANN 算法 HNSW 自研 HNSW IVFFlat / HNSW HNSW
Metadata 过滤 基础支持 丰富的元数据过滤 强大的 Payload 过滤 标准 SQL WHERE 复杂的过滤器语法
水平扩展 有限 自动扩展 支持分片和副本 依赖 PG 集群 支持分片
成本 免费开源 $0.058/小时起步 免费开源,云托管按需 免费开源(需 PG 实例) 免费开源,云托管按需
最佳场景 开发测试、小规模 POC 生产环境、零运维 需要混合过滤的企业场景 已使用 PG 的团队 知识图谱 + 向量混合检索

做向量数据库的选型时可以根据数据规模、运维能力和业务场景适合哪个产品进行衡量。对于大多数 RAG 应用和语义搜索场景,Qdrant 或 Milvus 是最稳妥的选择;如果你只是想快速验证想法,Chroma 或 pgvector 就足够了。以下是提供的可以参考的选型思路。

(1)开发阶段

推荐使用 Chroma。它的优势在于零配置启动,Docker 一行命令即可。Chroma的API 非常简洁,学习成本低,无需单独的服务端就可以在代码中嵌入使用。

(2)生产环境(预算充足)

如果成本预算充足,推荐使用 Pinecone。它的优势在于零运维,无需关心基础设施,性能稳定,SLA 有保障。同时,Pinecone生态完善,有丰富的集成和工具。

如果对成本较为敏感则推荐使用 Qdrantpgvector 。其中,Qdrant 的功能强大,支持复杂的过滤条件,社区活跃;而pgvector更适合如果你的团队已经在使用 PostgreSQL的情况,这是一个自然的延伸,无需引入新的技术栈。

(3)特殊场景

有时候你可能有特殊的场景,比如需要知识图谱 + 向量检索 ,这时可以选择 Weaviate ,因为它原生支持知识图谱和向量的混合检索;如果你的应用场景是超大规模数据(十亿级) ,那么可以选择 MilvusQdrant,它们在分布式架构上有更多优化。

⚠️ 注意

需要提醒的是不要在开发环境和生产环境用不同的向量数据库并期望一致的检索结果。不同数据库的 HNSW 实现参数(ef_search、ef_construction、M)默认值不同,产生的近似搜索结果会有细微差异。在生产环境上线前,务必用真实数据做充分的测试。

四、动手实践:Chroma

Chroma 是轻量级的开源向量数据库,适合本地开发和原型验证。如上文所述,其拥有零配置启动API 简洁功能完善等优点。它的API支持Python 和 JavaScript/TypeScript 客户端且非常易用,同时也支持 HNSW 索引、元数据过滤、批量操作等核心功能。在本地通过Docker启动即可直接使用。

下面我们通过一个例子展示Chroma完整的"建库→插入→查询"流程。

1. 环境准备

首先安装 Chroma 服务端和客户端(假设你已经安装并启动了Docker)。

bash 复制代码
# 使用 Docker 启动 Chroma 服务
docker run -d -p 8000:8000 chromadb/chroma

# 安装 TypeScript 客户端
npm install chromadb

2. 初始化与创建 Collection

typescript 复制代码
import { ChromaClient } from "chromadb";

// 连接到 Chroma 服务
const client = new ChromaClient({ 
  path: "http://localhost:8000" 
});

// 创建 Collection(类似传统数据库中的"表")
const collection = await client.createCollection({
  name: "rag_docs",
  metadata: { 
    description: "RAG 专栏文档库",
    embedding_model: "text-embedding-3-small",
  },
});

console.log(`Collection 已创建: ${collection.name}`);

使用Chroma首先需要创建Collection,Collection 是 Chroma 中的数据组织单元,类似于传统数据库中的"表"。一个 Collection 通常包含向量 (高维浮点数数组)、文档 (原始文本,可选,但建议存储以便溯源)、元数据 (键值对形式的附加信息,比如使用的embedding模型) 、ID(唯一标识符)。

就像传统数据库创建多张表一样,你可以在一个 Chroma 实例中创建多个 Collection,用于隔离不同业务的数据。例如:

  • product_docs:产品文档
  • api_docs:API 文档
  • faq_docs:常见问题解答

3. 插入向量

Chroma 支持两种方式:传入已生成的向量,或让它自动调用 Embedding 函数。

方式一:自行传入向量(推荐)

typescript 复制代码
import { getEmbedding } from "./embedding"; // 见第 2 章

// 准备数据
const documents = [
  "RAG 是检索增强生成技术",
  "Embedding 将文本映射为向量",
  "向量数据库用于高效相似度搜索",
];

// 批量生成向量
const embeddings = await Promise.all(
  documents.map(doc => getEmbedding(doc))
);

// 插入到 Chroma
await collection.add({
  ids: ["doc_1", "doc_2", "doc_3"],
  embeddings: embeddings,
  documents: documents,
  metadatas: [
    { source: "chapter1", date: "2026-05-27", category: "concept" },
    { source: "chapter2", date: "2026-05-27", category: "technique" },
    { source: "chapter3", date: "2026-05-27", category: "infrastructure" },
  ],
});

console.log("已插入 3 条文档");

方式二:让 Chroma 自动调用 Embedding 函数

typescript 复制代码
import { OpenAIEmbeddingFunction } from "chromadb";

// 配置自动 Embedding
const embedder = new OpenAIEmbeddingFunction({
  openai_api_key: process.env.OPENAI_API_KEY!,
  openai_model: "text-embedding-3-small",
});

const autoCollection = await client.createCollection({
  name: "auto_embed_docs",
  embeddingFunction: embedder,
});

// 直接插入文本,Chroma 会自动生成向量
await autoCollection.add({
  ids: ["doc_1", "doc_2"],
  documents: [
    "RAG 是检索增强生成技术",
    "Embedding 将文本映射为向量",
  ],
});

之所以推荐使用方式一,原因如下:

  • 一致性保证:检索时你需要手动为查询生成向量,插入时也手动生成,确保使用同一个 Embedding 模型
  • 错误处理:如果 Embedding API 调用失败,你可以在插入前捕获错误,而不是让 Chroma 静默失败
  • 灵活性:可以对向量进行预处理(如降维、归一化)后再存入

4. 执行相似度搜索

typescript 复制代码
// 查询时需要先对问题文本生成 Embedding
const queryText = "什么是 RAG 技术?";
const queryEmbedding = await getEmbedding(queryText);

// 执行相似度搜索
const results = await collection.query({
  queryEmbeddings: [queryEmbedding],
  nResults: 2,        // 返回 Top-2 结果
});

// 解析结果
console.log(`\n查询: "${queryText}"\n`);
for (let i = 0; i < results.ids[0].length; i++) {
  console.log(`[${i + 1}] ID: ${results.ids[0][i]}`);
  console.log(`    文档: ${results.documents[0][i]}`);
  console.log(`    距离: ${results.distances[0][i].toFixed(4)}`);
  console.log(`    元数据:`, results.metadatas[0][i]);
  console.log();
}

典型输出:

css 复制代码
查询: "什么是 RAG 技术?"

[1] ID: doc_1
    文档: RAG 是检索增强生成技术
    距离: 0.1234
    元数据: { source: 'chapter1', date: '2026-05-27', category: 'concept' }

[2] ID: doc_2
    文档: Embedding 将文本映射为向量
    距离: 0.3456
    元数据: { source: 'chapter2', date: '2026-05-27', category: 'technique' }

注意 :Chroma 返回的 distances距离而非相似度。对于余弦相似度,距离 = 1 - 相似度。所以距离越小,相似度越高。

5. 使用元数据过滤

通过元素据,可以增加过滤条件,在执行相似度搜索之前可以有效较少搜索的范围。

typescript 复制代码
// 只搜索 category 为 "concept" 的文档
const filteredResults = await collection.query({
  queryEmbeddings: [queryEmbedding],
  nResults: 5,
  where: { category: "concept" }, // 元数据过滤条件
});

console.log(`过滤后返回 ${filteredResults.ids[0].length} 条结果`);

Chroma 支持多种过滤操作符,如果你熟悉MongoDB等或者常见的Javascript ORM库,那么可以很轻松的掌握Chroma的操作符。

typescript 复制代码
// 等于
where: { category: "concept" }

// 不等于
where: { category: { $ne: "concept" } }

// 大于/小于(适用于数值型元数据)
where: { year: { $gt: 2020 } }

// 包含于列表
where: { tags: { $in: ["rag", "llm"] } }

// 多条件组合(AND)
where: { 
  category: "concept",
  year: { $gte: 2023 }
}

6. 删除和更新

Chroma的删除和更新API也非常简洁。

typescript 复制代码
// 按 ID 删除
await collection.delete({
  ids: ["doc_1"],
});

// 按元数据条件删除
await collection.delete({
  where: { category: "outdated" },
});

// 更新文档(需要先删除再插入,Chroma 不支持部分更新)
await collection.delete({ ids: ["doc_2"] });
await collection.add({
  ids: ["doc_2"],
  embeddings: [updatedEmbedding],
  documents: ["更新后的文档内容"],
  metadatas: [{ source: "chapter2", date: "2026-05-28", category: "technique" }],
});

五、动手实践:Pinecone

Pinecone 是托管的云端向量数据库,无需自己管理服务器。在生产环境中使用Pinecone,可以做到 零运维,即你无需关心服务器扩容、备份、监控等问题。Pinecone还针对大规模数据做了深度优化,有较高的性能。同时其还提供了 SDK、监控面板、日志等全套工具。它的缺点则是成本较高,且数据必须上传到云端(对于数据敏感的场景不适用)。

1. 环境准备

bash 复制代码
npm install @pinecone-database/pinecone

注册 Pinecone 账号并获取 API Key,然后在环境变量中配置:

bash 复制代码
export PINECONE_API_KEY="your-api-key"
export PINECONE_ENVIRONMENT="gcp-starter" # 或其他区域

2. 创建 Index

typescript 复制代码
import { Pinecone } from "@pinecone-database/pinecone";

const pc = new Pinecone({
  apiKey: process.env.PINECONE_API_KEY!,
});

// 检查 Index 是否已存在
const indexes = await pc.listIndexes();
if (!indexes.indexes?.some(idx => idx.name === "rag-index")) {
  // 创建新的 Index
  await pc.createIndex({
    name: "rag-index",
    dimension: 1536, // 必须与 Embedding 向量维度一致
    metric: "cosine", // 相似度度量方式:cosine / euclidean / dotproduct
    spec: {
      serverless: {
        cloud: "aws",
        region: "us-east-1",
      },
    },
  });
  
  console.log("Index 创建成功,等待初始化...");
  await sleep(10000); // 等待 Index 就绪
}

const index = pc.Index("rag-index");

可以看到,第一步是创建 Index(索引)。Index 是 Pinecone 中的数据组织单元,类似于 Chroma 的 Collection。但与 Chroma 不同的是,Pinecone 的 Index 需要在创建时指定维度、度量方式等参数,且创建后无法修改。

Pinecone 提供两种部署模式,Serverless 和Pod-based,区别如下:

  • Serverless:按需付费,自动扩缩容,适合中小规模应用
  • Pod-based:预留资源,性能更稳定,适合大规模生产环境

其实跟很多云服务产品都差不多。对于初学者,推荐从 Serverless 开始。

3. 插入向量

使用upsert API可以插入或更新向量。

typescript 复制代码
// Upsert 操作:如果 ID 已存在则更新,否则插入
await index.upsert([
  {
    id: "doc_1",
    values: [0.12, -0.34, 0.78 /* ... 1536 维 */],
    metadata: { 
      text: "RAG 是检索增强生成技术", 
      source: "chapter1",
      category: "concept",
    },
  },
  {
    id: "doc_2",
    values: [0.15, -0.31, 0.75 /* ... */],
    metadata: { 
      text: "Embedding 将文本映射为向量", 
      source: "chapter2",
      category: "technique",
    },
  },
]);

console.log("向量已插入");
  • Pinecone 的 upsert 是幂等操作,多次插入同一个 ID 不会报错
  • metadata 字段只能是字符串、数字、布尔值或这些类型的列表,不支持嵌套对象
  • 单次 upsert 最多可以插入 100 条向量

4. 执行相似度搜索

typescript 复制代码
// 生成查询向量
const queryEmbedding = await getEmbedding("什么是 RAG 技术?");

// 执行查询
const queryResult = await index.query({
  vector: queryEmbedding,
  topK: 5,
  includeMetadata: true,
  includeValues: false, // 不需要返回向量本身,节省带宽
});

// 解析结果
console.log(`\n查询: "什么是 RAG 技术?"\n`);
queryResult.matches?.forEach((match, index) => {
  console.log(`[${index + 1}] ID: ${match.id}`);
  console.log(`    分数: ${match.score?.toFixed(4)}`); // Pinecone 返回的是相似度分数
  console.log(`    元数据:`, match.metadata);
  console.log();
});

同样需要注意,Pinecone 返回的 score相似度分数而非距离。对于余弦相似度,分数越高表示越相似。这与 Chroma 的行为相反。

5. 使用命名空间(Namespace)

Pinecone 支持在一个 Index 中创建多个命名空间,用于逻辑隔离:

typescript 复制代码
// 在不同的命名空间中插入数据
await index.namespace("product_docs").upsert([...]);
await index.namespace("api_docs").upsert([...]);

// 查询时指定命名空间
const result = await index.namespace("product_docs").query({
  vector: queryEmbedding,
  topK: 5,
});

命名空间的作用类似于 Chroma 的 Collection,但更轻量。你可以为一个 Index 创建数百个命名空间,而无需担心资源开销。

6. 元数据过滤

Pinecone 的元数据过滤功能比 Chroma 更强大,除了支持更多的操作符外,还提供了过滤性能保障、Schema 预定义可索引字段、混合搜索(向量+全文+元数据)。

typescript 复制代码
const result = await index.query({
  vector: queryEmbedding,
  topK: 10,
  filter: {
    category: { $eq: "concept" },
    year: { $gte: 2023 },
    tags: { $in: ["rag", "llm"] },
  },
});

支持的过滤操作符包括:$eq$ne$gt$gte$lt$lte$in$nin$exists 等。

六、向量数据库的性能优化

1. 批量操作

无论是插入还是查询,批量操作都能显著减少网络往返开销:

typescript 复制代码
// 错误做法:逐条插入
for (const doc of documents) {
  await collection.add({ /* ... */ }); // 每次都是独立的 HTTP 请求
}

// 正确做法:批量插入
await collection.add({
  ids: documentIds,
  embeddings: allEmbeddings,
  documents: documents,
  metadatas: allMetadatas,
});

对于百万级数据的索引任务,批量操作可以将总耗时从几小时缩短到几分钟。

2. 索引参数调优

如果你使用的是自托管的向量数据库(如 Chroma、Qdrant),可以通过调整 HNSW 参数来平衡性能和精度:

typescript 复制代码
// Chroma 示例:创建 Collection 时指定 HNSW 参数
const collection = await client.createCollection({
  name: "optimized_docs",
  metadata: {
    "hnsw:space": "cosine",           // 相似度度量方式
    "hnsw:construction_ef": 200,      // efConstruction
    "hnsw:M": 16,                     // M(最大连接数)
  },
});

调优建议

  • 构建阶段 :增大 efConstructionM,提高索引质量
  • 查询阶段 :根据业务需求调整 efSearch,平衡速度和精度
  • 监控指标:关注召回率(Recall@K)和查询延迟,找到最优平衡点

3. 缓存策略

对于高频查询,可以引入缓存层:

typescript 复制代码
import NodeCache from "node-cache";

const queryCache = new NodeCache({ stdTTL: 3600 }); // 缓存 1 小时

async function cachedQuery(queryText: string) {
  // 检查缓存
  const cached = queryCache.get(queryText);
  if (cached) {
    return cached;
  }
  
  // 执行查询
  const queryEmbedding = await getEmbedding(queryText);
  const results = await collection.query({
    queryEmbeddings: [queryEmbedding],
    nResults: 5,
  });
  
  // 写入缓存
  queryCache.set(queryText, results);
  
  return results;
}

缓存能显著降低重复查询的延迟,但需要注意缓存失效策略(如文档更新后清除相关缓存)。

FAQ

Q1:能不能直接存在 PostgreSQL JSONB 里自己做余弦相似度?

小规模(< 1 万条)可以。超过这个量级,缺少索引的向量搜索会成为瓶颈。pgvector 扩展为 PostgreSQL 添加了 IVFFlat 和 HNSW 索引,是一个折中方案。

如果你的团队已经在使用 PostgreSQL,且数据量在百万级以下,pgvector 是一个不错的选择。它的优势在于:

  • 无需引入新的技术栈
  • 可以利用 PostgreSQL 的成熟生态(备份、监控、权限管理等)
  • 支持标准的 SQL 查询,便于与其他数据联合查询

但 pgvector 也有局限:

  • 分布式支持较弱,超大规模数据需要借助 Citus 等扩展
  • HNSW 实现不如专用向量数据库成熟

Q2:向量数据库需要多少内存?

一个 1536 维的 float32 向量占用 6KB。100 万条向量的原始数据约 6GB,加上 HNSW 索引的图结构开销,通常需要 8-12GB 内存。具体因数据库实现而异。

内存估算公式

diff 复制代码
总内存 ≈ 原始数据大小 × (2 ~ 4)

其中:
- 原始数据大小 = 向量数量 × 维度 × 4 bytes(float32)
- 系数 2-4 取决于 HNSW 参数(M 越大,系数越高)

例如:

  • 100 万条 1536 维向量:6GB × 3 ≈ 18GB
  • 1000 万条 768 维向量:30GB × 3 ≈ 90GB

如果你的服务器内存不足,可以考虑:

  • 使用 float16 或 int8 量化,减少向量大小(但会损失精度)
  • 使用 DiskANN 等磁盘友好的算法
  • 采用分布式架构,将数据分散到多个节点

Q3:删除文档后向量数据库会自动更新吗?

是的,向量数据库支持按 ID 删除。但需要注意的是如果你使用的是自托管方案(如 Chroma),删除后磁盘空间可能不会立即释放,这取决于底层存储引擎的回收策略。

对于 Chroma,删除操作只是标记删除,实际的空间回收需要等待 compaction 进程。如果需要立即释放空间,可以手动触发 compaction:

typescript 复制代码
await client.triggerCompaction();

Q4:向量数据库能否保证 100% 的召回率?

不能。ANN 的本质是"近似"搜索,为了速度牺牲了一定的精度。但在合理配置下,召回率可以达到 95%-99%,对于大多数应用场景已经足够。

如果对召回率有极致要求,可以:

  • 增大 efSearch 参数,提高搜索精度
  • 使用混合检索(向量搜索 + 关键词搜索),互补不足
  • 增大 k 值(召回更多候选),在后处理阶段过滤

Q5:如何选择相似度度量方式(cosine / euclidean / dotproduct)?

这取决于你的 Embedding 模型和业务场景:

  • Cosine(余弦相似度):最常用,只关心向量方向,不关心长度。适合语义相似度任务。
  • Euclidean(欧氏距离):关心向量的绝对位置。适合聚类分析等场景。
  • Dotproduct(点积):计算最快,但受向量长度影响。如果向量已归一化,点积等于余弦相似度。

推荐:除非有特殊需求,否则统一使用 Cosine。OpenAI 的 text-embedding-3 系列返回的是归一化向量,使用 Cosine 或 Dotproduct 效果相同。

练习

练习 1:搭建本地 Chroma 并创建索引

使用 Docker 启动 Chroma 服务,编写 TypeScript 脚本创建索引并插入 20 条测试数据(可以使用任意短文本)。

验证标准

  • collection.count() 返回 20
  • 能通过查询返回正确的 Top-3 结果
  • 查询结果的顺序符合语义相似度预期

提示:可以从维基百科摘录 20 段不同主题的文本作为测试数据。

练习 2:对比暴力搜索和 ANN 搜索

对 1000 条随机生成的向量,分别用暴力搜索(遍历计算余弦相似度)和 Chroma 查询做 Top-10 检索,对比延迟和结果的重叠率。

验证标准

  • ANN 结果与暴力搜索的重叠率 ≥ 95%(即 10 条结果中至少有 9 条相同)
  • ANN 搜索延迟降低至少一个数量级(如从 100ms 降到 10ms)

提示

typescript 复制代码
// 暴力搜索实现
function bruteForceSearch(
  queryVector: number[],
  database: Array<{ id: string; vector: number[] }>,
  k: number
) {
  const scores = database.map(item => ({
    id: item.id,
    score: cosineSimilarity(queryVector, item.vector),
  }));
  
  return scores.sort((a, b) => b.score - a.score).slice(0, k);
}

练习 3:使用 Metadata Filter 缩小搜索范围

给插入的文档添加 category 元数据字段(如 "technical"、"business"、"general"),查询时只搜索 category: "technical" 的文档,验证过滤结果。

验证标准

  • 不带过滤的查询返回所有类别的文档
  • 带过滤的查询只返回 "technical" 类别的文档
  • 过滤后的查询结果仍然保持语义相关性

练习 4:性能基准测试

编写一个性能测试脚本,测试 Chroma 在不同数据规模下的查询延迟:

  • 1,000 条向量
  • 10,000 条向量
  • 100,000 条向量

记录每种规模下的平均查询延迟(执行 100 次查询取平均值)。

验证标准

  • 1,000 条:延迟 < 5ms
  • 10,000 条:延迟 < 10ms
  • 100,000 条:延迟 < 20ms

如果延迟明显高于预期,检查 HNSW 参数配置是否合理。

📚 延伸阅读

  • Chroma Documentation --- Chroma 官方文档,包含详细的 API 参考和最佳实践
  • Pinecone Documentation --- Pinecone 官方文档,介绍了云端向量数据库的使用方法和架构设计
  • Qdrant Documentation --- Qdrant 官方文档,适合需要了解自托管方案的读者
  • pgvector GitHub --- pgvector 的项目主页,包含安装指南和使用示例
  • HNSW 论文 --- Malkov & Yashunin, "Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs",想深入了解算法原理的读者可以阅读
  • ANN Benchmarks --- 各种 ANN 算法的性能对比基准,帮助你了解不同算法的优劣
相关推荐
小柯南敲键盘1 小时前
AI批量翻译Temu商品标题的Python实践
开发语言·人工智能·python
动物园猫1 小时前
人群密度目标检测数据集:8,000张图像 | 目标检测
人工智能·目标检测·计算机视觉
蓝天星空1 小时前
一款APP打车软件设计参考
人工智能
微石科技1 小时前
慢病居家长期服务方案怎么选?宁波微石科技星梦云康打通居家慢病服务闭环
大数据·人工智能
酉鬼女又兒1 小时前
[特殊字符]零基础入门AI:归纳演绎、假设空间、归纳偏好、NFL、过拟合与欠拟合、模型评估选择、超参数、性能度量、混淆矩阵、P-R曲线和F1
人工智能·windows·python·深度学习·安全·机器学习·矩阵
cooldream20092 小时前
AI 时代,前端工程师的“新学习路线“—— 从“学框架“到“用 AI“
前端·人工智能·学习
艾斯特_2 小时前
Function Calling与工具调用:让模型从回答走向执行
人工智能·python·算法·ai