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(复数),不是id和document。
查询与搜索
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"] });

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

