1. 向量的核心概念

向量:先把它理解成“坐标”

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

在向量数据库里,向量最重要的意义不是“存了一串数字”,而是:它表示一个对象在高维空间中的位置。文本、图片、音频,都会被映射成这个空间里的一个点。

这里有 3 个容易忽略但非常关键的点:

  1. 向量是有序的,[0.1, 0.2][0.2, 0.1] 不是同一个向量。
  2. 只有维度相同的向量才能比较,比如 1536 维和 3072 维向量不能直接算相似度。
  3. 对 Embedding 来说,单个维度通常没有明确的人类语义,真正有意义的是整组坐标共同形成的位置关系

所以在语义搜索里,我们真正关心的不是“第 517 维代表什么”,而是:两个对象在空间里离得近不近、方向像不像

维度是什么:不是字段数,而是表征容量

向量的维度(Dimension)就是它包含多少个数。3 维向量是三维空间里的点,1536 维向量可以理解成 1536 维空间里的点。

初学者很容易把“维度”理解成业务字段,比如“标题一维、作者一维、日期一维”。这在 Embedding 里通常是不对的。Embedding 的维度不是手工设计出来的字段,而是模型训练后形成的潜在特征(latent features)

可以把它类比成这样:

  • 传统结构化数据:每一列的含义很清楚,比如 priceagecity
  • 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]
  • ab 方向完全一样,只是 b 更长
  • ac 的方向差异很大

如果用欧氏距离,ab 会显得“有点远”;但如果用余弦相似度,ab 的相似度是 1,因为它们方向完全一致。

这很像文本语义中的情况:

  • “我喜欢猫”
  • “我非常非常喜欢猫”

这两句话长度和强度不同,但核心语义方向很接近。余弦相似度更容易抓住这种“意思相近但表达强弱不同”的关系。

实战里要特别注意:有些系统返回的是“相似度”,有些返回的是“距离”。
相似度通常是越大越像,距离通常是越小越像。
例如很多库内部会把余弦相似度转换成余弦距离:distance = 1 - cosine_similarity

归一化:为什么很多系统更关心方向而不是长度

在很多 Embedding 场景里,我们会先把向量做归一化(Normalization),也就是把所有向量缩放到相近甚至相同的长度。

这样做的好处是:

  • 减少“文本长短”对结果的干扰
  • 让比较更聚焦在方向差异上
  • 在某些实现里,归一化后的点积可以直接近似余弦相似度

你不一定每次都需要手动处理归一化,但理解这个概念会帮助你读懂很多文档里关于 cosineipl2 的差异。

向量数据库概述

传统数据库擅长精确匹配,比如:

WHERE name = '猫'

而向量数据库做的是近邻检索(Nearest Neighbor Search):找到和“猫”这段查询在语义上最接近的内容。

一条向量记录通常不只有向量本身,还会包含:

  • id:唯一标识
  • embedding:高维向量
  • document:原始文本或原始对象引用
  • metadata:标签、分类、时间等结构化字段

这也是为什么向量数据库不只是“数据库里加一列数组”那么简单。它还要解决两个问题:

  1. 如何高效存储和组织海量高维向量
  2. 如何在查询时快速找到 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(复数),不是 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 的回答基于你的私有数据,而不是仅靠训练时的知识。

其他场景

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

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"] });

image-20260513213423016


核心要点总结

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

前端小白