1. 向量的核心概念

向量:不只是数字

标量(Scalar)是一个单独的数,比如 42。向量(Vector)是一组有序的数,比如 [0.2, -0.5, 0.8]

向量的关键特性:维度灵活。一个向量可以有 3 维、128 维、甚至 1536 维。维度越多,能表达的信息越丰富。

在语义搜索的语境下,向量的核心直觉是:语义相似的文本 → 向量在空间中距离接近

比如 “猫” 和 “狗” 的向量会比 “猫” 和 “汽车” 更近,因为前者语义更相似。

向量空间与距离

向量之间的”相似”需要度量。两种核心方式:

度量方式 衡量什么 适用场景
欧氏距离(Euclidean) 绝对距离 空间位置差异
余弦相似度(Cosine) 方向一致性 语义搜索

关键洞察:语义搜索中,方向比大小更重要。”我喜欢猫” 和 “我非常喜欢猫” 语义相近但强度不同,余弦相似度能捕捉这种方向一致性,而不被幅度差异干扰。

向量嵌入(Embedding)

Embedding 模型将文本转换为高维向量。例如 OpenAI 的 text-embedding-ada-002 输出 1536 维向量。

"今天天气不错" → [0.012, -0.034, 0.056, ..., 0.078]  # 1536 个浮点数

维度 = 辨别力:维度越高,模型能区分的语义粒度越细。3 维向量只能粗略分类,1536 维向量能捕捉细微的语义差异。

相似度计算

三种主流计算方式:

方法 公式本质 结果含义
欧氏距离 空间直线距离 值越小越相似
余弦相似度 向量夹角的余弦 值越大越相似(-1 到 1)
点积(Dot Product) 余弦 × 幅度 兼顾方向与强度

语义搜索最常用余弦相似度,因为它只关注方向、忽略幅度。

向量数据库概述

传统数据库做精确匹配:WHERE name = '猫'。向量数据库做语义搜索:找到和”猫”语义最接近的内容。

flowchart LR
    A[原始文本] --> B[Embedding 模型]
    B --> C[高维向量]
    C --> D[存入向量数据库]
    D --> E[查询时:查询文本也转为向量]
    E --> F[计算相似度]
    F --> G[返回最相似的结果]

核心流程:存的时候向量化,查的时候也向量化,然后用相似度匹配

2. Chroma

Chroma 是一个轻量级开源向量数据库,核心概念:

概念 说明
Collection 类似数据库的表,存储一组相关向量
Document 原始文本内容
Embedding 文本对应的向量
ID 每条记录的唯一标识
Metadata 结构化元数据,用于精确过滤

元数据的威力:Metadata 让向量数据库不仅能做语义搜索,还能结合精确过滤——比如”找和’猫’语义相似且 category=’动物’ 的文档”。

JS/TS SDK 安装与配置

Chroma 采用 Client-Server 架构:

flowchart LR
    A[JS/TS 应用] --> B[ChromaClient]
    B -->|HTTP API| C[Chroma Server]
    C --> D[向量存储 + 检索]

安装与连接:

import { ChromaClient } from "chromadb";

const client = new ChromaClient({
  ssl: false,
  host: 'localhost',
  port: 8000
});

Collection 操作

Collection 是 Chroma 的核心组织单元:

// 创建 Collection,指定距离算法
const collection = await client.createCollection({
  name: "my_docs",
  metadata: {
    "hnsw:space": "cosine",  // 可选: cosine | l2 | ip
  },
});

// 获取已有 Collection
const existing = await client.getCollection({ name: "my_docs" });

// 创建或获取(幂等操作)
const col = await client.getOrCreateCollection({
  name: "my_docs",
  metadata: { "hnsw:space": "cosine" },
});

// 删除
await client.deleteCollection({ name: "my_docs" });

hnsw:space 的三种选项对应三种相似度算法:

参数值 对应算法
cosine 余弦相似度(最常用)
l2 欧氏距离
ip 内积(点积)

添加与更新数据

// 添加数据(自动 Embedding)
await collection.add({
  ids: ["doc1", "doc2"],          // 注意是 ids 不是 id
  documents: ["这是第一段文本", "这是第二段文本"],
  metadatas: [{ category: "tech" }, { category: "life" }],
});

// 更新数据(必须已存在)
await collection.update({
  ids: ["doc1"],
  documents: ["更新后的文本"],
});

// Upsert:存在则更新,不存在则添加
await collection.upsert({
  ids: ["doc1", "doc3"],
  documents: ["更新的文本", "新增的文本"],
});

踩坑提醒:add 的参数是 ids(复数)、documents(复数),不是 iddocument

查询与搜索

const results = await collection.query({
  queryTexts: ["搜索关键词"],
  nResults: 5,
});

// 返回结构:每个查询一组结果(嵌套数组)
// results.ids[0]       → ["doc1", "doc3", ...]
// results.distances[0] → [0.12, 0.34, ...]  值越小越相似
// results.documents[0] → ["匹配的文本1", "匹配的文本2", ...]

注意返回结构的嵌套queryTexts 可以传多个查询文本,所以结果是”每个查询一个数组”的嵌套结构。即使只查一个,也要用 results.ids[0] 来访问。

Embedding Function 配置

Chroma 默认使用内置 Embedding 模型,但你可以自定义:

import { OpenAIEmbeddingFunction } from "chromadb";

const embedder = new OpenAIEmbeddingFunction({
  openai_api_key: "sk-...",
  openai_model: "text-embedding-ada-002",
});

// 创建时指定
const collection = await client.createCollection({
  name: "my_docs",
  embeddingFunction: embedder,
});

最大的坑:获取已有 Collection 时,必须重新传入 embeddingFunction

// 错误:会用默认模型,导致向量不匹配
const col = await client.getCollection({ name: "my_docs" });

// 正确:重新传入 embeddingFunction
const col = await client.getCollection({
  name: "my_docs",
  embeddingFunction: embedder,
});

如果不传,Chroma 不会报错,而是静默使用默认模型——存的时候用模型 A 编码,查的时候用模型 B 编码,结果完全错误但程序不会崩溃。这是最危险的 bug 类型:静默失败

flowchart TD
    A[创建 Collection 时指定 Embedding 模型 A] --> B[数据用模型 A 编码存入]
    B --> C[查询时用模型 A 编码查询文本]
    C --> D[向量空间一致 ✅ 结果正确]

    A --> E[查询时忘记传入模型 → 默认模型 B]
    E --> F[查询文本用模型 B 编码]
    F --> G[向量空间不一致 ❌ 结果错误但无报错]
    style G fill:#ff6b6b,color:#fff
    style D fill:#51cf66,color:#fff

元数据过滤

元数据过滤让向量搜索从”纯语义”升级为”语义 + 精确”:

// 简单过滤
const results = await collection.query({
  queryTexts: ["机器学习"],
  nResults: 5,
  where: { category: "tech" },
});

// 复合过滤
const results = await collection.query({
  queryTexts: ["机器学习"],
  nResults: 5,
  where: {
    $and: [
      { category: { $eq: "tech" } },
      { year: { $gte: 2023 } },
    ],
  },
});

支持的过滤操作符:$eq, $ne, $gt, $gte, $lt, $lte, $and, $or

精确 + 模糊是最强组合:先用元数据精确过滤缩小范围,再做语义搜索,既准又快。

实际应用场景

RAG(检索增强生成)

RAG 是向量数据库最热门的应用场景:

flowchart LR
    A[用户提问] --> B[查询文本 → Embedding]
    B --> C[向量数据库检索相关文档]
    C --> D[检索结果 + 原始问题 → LLM]
    D --> E[LLM 基于检索内容生成回答]
    E --> F[返回答案]

RAG 的核心价值:让 LLM 的回答基于你的私有数据,而不是仅靠训练时的知识。

其他场景

场景 说明
语义搜索 搜索”如何部署”能匹配”上线流程”
推荐系统 根据用户偏好向量找相似内容
重复检测 找到语义重复的文档
分类聚类 按语义相似度自动分组

完整实战代码

一个从创建到查询的完整示例:

踩坑记录:出现 [cause]: AggregateError [ETIMEDOUT]:报错时,可能是Node版本不兼容导致的,可以使用Node21或者使用Bun运行

import { ChromaClient, OpenAIEmbeddingFunction } from "chromadb";

// 1. 连接 + 配置 Embedding
const client = new ChromaClient({
  ssl: false,
  host: 'localhost',
  port: 8000
});
const embedder = new OpenAIEmbeddingFunction({
  openai_api_key: process.env.OPENAI_API_KEY!,
});

// 2. 创建 Collection
const collection = await client.getOrCreateCollection({
  name: "knowledge_base",
  metadata: { "hnsw:space": "cosine" },
  embeddingFunction: embedder,
});

// 3. 添加数据
await collection.add({
  ids: ["1", "2", "3"],
  documents: [
    "React 是一个用于构建用户界面的 JavaScript 库",
    "Vue 是一个渐进式 JavaScript 框架",
    "Python 是一种通用编程语言",
  ],
  metadatas: [
    { type: "frontend", framework: "react" },
    { type: "frontend", framework: "vue" },
    { type: "language", framework: "none" },
  ],
});

// 4. 语义搜索 + 元数据过滤
const results = await collection.query({
  queryTexts: ["渐进式前端框架"],
  nResults: 2,
  where: { type: { $eq: "frontend" } },
});

console.log(results.documents[0]);
// → ["React 是一个用于构建用户界面的 JavaScript 库", "Vue 是一个渐进式 JavaScript 框架"]

// 5. 更新与删除
await collection.upsert({
  ids: ["1"],
  documents: ["React 18 引入了并发渲染特性"],
  metadatas: [{ type: "frontend", framework: "react", version: 18 }],
});

await collection.delete({ ids: ["3"] });

image-20260513213423016


核心要点总结

  1. 方向比距离重要 —— 余弦相似度关注方向,适合语义搜索
  2. 维度 = 辨别力 —— 高维向量能捕捉更细微的语义差异
  3. 向量搜索 ≠ 关键词搜索 —— “部署”能搜到”上线”,传统搜索做不到
  4. 精确 + 模糊是最强组合 —— 元数据过滤 + 语义搜索双管齐下
  5. 模型一致性是生死线 —— 存和查必须用同一个 Embedding 模型,否则静默出错
  6. API 命名注意复数 —— idsdocumentsmetadatas,不是单数形式

前端小白