1. 向量的核心概念
向量:先把它理解成“坐标”
标量(Scalar)是一个单独的数,比如 42。向量(Vector)是一组有序的数,比如 [0.2, -0.5, 0.8]。
在向量数据库里,向量最重要的意义不是“存了一串数字”,而是:它表示一个对象在高维空间中的位置。文本、图片、音频,都会被映射成这个空间里的一个点。
这里有 3 个容易忽略但非常关键的点:
- 向量是有序的,
[0.1, 0.2]和[0.2, 0.1]不是同一个向量。 - 只有维度相同的向量才能比较,比如 1536 维和 3072 维向量不能直接算相似度。
- 对 Embedding 来说,单个维度通常没有明确的人类语义,真正有意义的是整组坐标共同形成的位置关系。
所以在语义搜索里,我们真正关心的不是“第 517 维代表什么”,而是:两个对象在空间里离得近不近、方向像不像。
维度是什么:不是字段数,而是表征容量
向量的维度(Dimension)就是它包含多少个数。3 维向量是三维空间里的点,1536 维向量可以理解成 1536 维空间里的点。
初学者很容易把“维度”理解成业务字段,比如“标题一维、作者一维、日期一维”。这在 Embedding 里通常是不对的。Embedding 的维度不是手工设计出来的字段,而是模型训练后形成的潜在特征(latent features)。
可以把它类比成这样:
- 传统结构化数据:每一列的含义很清楚,比如
price、age、city - Embedding 向量:每一维通常都说不清具体含义,但整体组合可以稳定表达语义
维度更高通常意味着模型有更强的表达能力,但维度高不等于一定更好。实际效果还取决于模型训练质量、训练数据、是否适配你的任务,以及最重要的一点:存储和查询必须使用同一个向量空间。
Embedding 到底做了什么
Embedding 模型做的事,本质上是把非结构化内容压缩成一个稠密向量:
"今天天气不错" → [0.012, -0.034, 0.056, ..., 0.078]
这个过程不是简单的“字符串转数字”,而是在做语义表征。模型会尽量让语义接近的内容,在向量空间里也彼此接近。
比如下面这两句话,字面上不完全相同,但很可能会靠得比较近:
- “iPhone 怎么截图?”
- “苹果手机截屏怎么操作?”
而下面这两句虽然都出现了“苹果”,向量却未必接近:
- “苹果手机截屏怎么操作?”
- “苹果很好吃,适合做派。”
这就是 Embedding 的价值:它不只看关键词是否重合,还试图捕捉主题、意图、上下文。
向量空间:真正有意义的是相对位置
Embedding 生成后,每一段文本都会落在同一个高维空间里。这个空间最重要的不是绝对坐标,而是相对几何关系:
- 距离近:通常表示语义更接近
- 方向相似:通常表示语义模式更像
- 聚成一团:通常表示属于相近主题
- 离群很远:可能是罕见内容,也可能是噪声
这里有个非常实用的结论:向量只能在同一个空间里比较。
如果你的数据存储时用模型 A 编码,查询时却用模型 B 编码,即使两个模型都输出 1536 维向量,结果也可能完全不靠谱。因为它们虽然维度一样,但坐标系不一样,就像一个用经纬度、一个用平面坐标,数值看起来都像“坐标”,却不能直接混算。
距离和相似度:为什么“近”就代表“像”
向量数据库的检索,本质上是在问一句话:
“给定查询向量,数据库里哪些向量离它最近?”
这里的“最近”可以有不同定义,常见的有 3 种:
| 方法 | 关注点 | 结果怎么判断 | 典型用途 |
|---|---|---|---|
| 欧氏距离(Euclidean) | 绝对空间距离 | 值越小越相似 | 关注位置差异 |
| 余弦相似度(Cosine) | 方向是否一致 | 值越大越相似(-1 到 1) | 语义搜索最常见 |
| 点积(Dot Product) | 方向 + 向量长度 | 值越大通常越相似 | 某些推荐/排序场景常用 |
为什么语义搜索常用余弦相似度?看一个很小的例子就够了:
a = [1, 1]
b = [10, 10]
c = [1, -1]
a和b方向完全一样,只是b更长a和c的方向差异很大
如果用欧氏距离,a 和 b 会显得“有点远”;但如果用余弦相似度,a 和 b 的相似度是 1,因为它们方向完全一致。
这很像文本语义中的情况:
- “我喜欢猫”
- “我非常非常喜欢猫”
这两句话长度和强度不同,但核心语义方向很接近。余弦相似度更容易抓住这种“意思相近但表达强弱不同”的关系。
实战里要特别注意:有些系统返回的是“相似度”,有些返回的是“距离”。
相似度通常是越大越像,距离通常是越小越像。
例如很多库内部会把余弦相似度转换成余弦距离:distance = 1 - cosine_similarity。
归一化:为什么很多系统更关心方向而不是长度
在很多 Embedding 场景里,我们会先把向量做归一化(Normalization),也就是把所有向量缩放到相近甚至相同的长度。
这样做的好处是:
- 减少“文本长短”对结果的干扰
- 让比较更聚焦在方向差异上
- 在某些实现里,归一化后的点积可以直接近似余弦相似度
你不一定每次都需要手动处理归一化,但理解这个概念会帮助你读懂很多文档里关于 cosine、ip、l2 的差异。
向量数据库概述
传统数据库擅长精确匹配,比如:
WHERE name = '猫'
而向量数据库做的是近邻检索(Nearest Neighbor Search):找到和“猫”这段查询在语义上最接近的内容。
一条向量记录通常不只有向量本身,还会包含:
id:唯一标识embedding:高维向量document:原始文本或原始对象引用metadata:标签、分类、时间等结构化字段
这也是为什么向量数据库不只是“数据库里加一列数组”那么简单。它还要解决两个问题:
- 如何高效存储和组织海量高维向量
- 如何在查询时快速找到 Top K 个最近邻
如果每次查询都把问题向量和库里所有向量逐个比较,数据一大就会非常慢。因此向量数据库通常会构建近似最近邻索引(ANN, Approximate Nearest Neighbor),比如 HNSW,用少量精度换取非常大的检索速度提升。
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 的回答基于你的私有数据,而不是仅靠训练时的知识。
其他场景
| 场景 | 说明 |
|---|---|
| 语义搜索 | 搜索”如何部署”能匹配”上线流程” |
| 推荐系统 | 根据用户偏好向量找相似内容 |
| 重复检测 | 找到语义重复的文档 |
| 分类聚类 | 按语义相似度自动分组 |
3. 完整实战代码
一个从创建到查询的完整示例:
踩坑记录:出现
[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,不是单数形式

