LanceDB 基础使用:用 TypeScript 完成第一次向量检索

LanceDB 基础使用:用 TypeScript 完成第一次向量检索

本文是「从零搭建私人 RAG 知识库」专栏的基础篇。上一篇介绍了 Vector Store 的概念和常见产品,这一篇将使用 CorpRAG 采用的 LanceDB,完成文档写入、持久化和语义检索。

前言

我们准备三句话:

text 复制代码
登录状态保存在 Redis 中。
订单支付成功后会发送通知。
系统使用 Docker 容器部署。

然后搜索:

text 复制代码
用户会话存在哪里?

最终希望得到:

text 复制代码
内容:登录状态保存在 Redis 中。
来源:auth.md

为了完成这个最小示例,我们会使用:

  • TypeScript:编写程序;
  • Ollama:在本地运行模型;
  • nomic-embed-text:把文本转换成向量;
  • LanceDB:保存向量并执行相似度检索;
  • LangChain.js:连接文档、Embedding 和 LanceDB。

完整流程如下:

text 复制代码
准备文档
   ↓
Embedding 生成文档向量
   ↓
写入 LanceDB
   ↓
Embedding 生成查询向量
   ↓
LanceDB 返回最相似的文档

一、为什么使用 LanceDB

LanceDB 是一个面向向量检索的数据库。它可以把数据直接保存在本地目录中,不要求我们先启动一个独立的数据库服务。

对于个人知识库和学习项目,它有几个比较直观的优点:

  • 可以嵌入应用运行;
  • 本地开发环境搭建简单;
  • 同时保存向量、文本和结构化字段;
  • 提供 TypeScript SDK;
  • 可以通过 LangChain 的 Vector Store 封装使用。

需要注意,LanceDB 和 Ollama 的职责不同:

text 复制代码
Ollama + Embedding 模型:把文本转换成向量
LanceDB:保存向量并查找相近向量

LanceDB 不会自己理解自然语言,也不会生成最终答案。


二、准备运行环境

1. 安装依赖

创建一个 Node.js 项目后,安装以下依赖:

bash 复制代码
npm install @lancedb/lancedb \
  @langchain/community \
  @langchain/core \
  @langchain/ollama \
  @langchain/textsplitters

npm install -D typescript tsx @types/node

本文使用 ESM。可以在 package.json 中加入:

json 复制代码
{
  "type": "module"
}

2. 准备 Ollama

确保本地已经安装并启动 Ollama,然后下载 Embedding 模型:

bash 复制代码
ollama pull nomic-embed-text

可以通过下面的命令查看本地模型:

bash 复制代码
ollama list

这里使用的 nomic-embed-text 只负责生成向量,不负责生成聊天回答。


三、运行第一个 LanceDB 示例

创建 lancedb-demo.ts

ts 复制代码
import { Document } from "@langchain/core/documents";
import { LanceDB } from "@langchain/community/vectorstores/lancedb";
import { OllamaEmbeddings } from "@langchain/ollama";

const documents = [
  new Document({
    pageContent: "登录状态保存在 Redis 中。",
    metadata: { source: "auth.md" },
  }),
  new Document({
    pageContent: "订单支付成功后会发送通知。",
    metadata: { source: "order.md" },
  }),
  new Document({
    pageContent: "系统使用 Docker 容器部署。",
    metadata: { source: "deploy.md" },
  }),
];

const embeddings = new OllamaEmbeddings({
  model: "nomic-embed-text",
  baseUrl: "http://localhost:11434",
});

const store = await LanceDB.fromDocuments(documents, embeddings, {
  uri: "./vector-data",
  tableName: "documents",
  mode: "overwrite",
});

const results = await store.similaritySearch(
  "用户会话存在哪里?",
  1,
);

for (const doc of results) {
  console.log("内容:", doc.pageContent);
  console.log("来源:", doc.metadata.source);
}

运行程序:

bash 复制代码
npx tsx lancedb-demo.ts

预期得到类似结果:

text 复制代码
内容:登录状态保存在 Redis 中。
来源:auth.md

实际结果由模型版本和输入内容决定。如果最相关结果不同,可以增加示例文本,或调整问题后再次观察。

程序运行后,当前目录还会出现:

text 复制代码
vector-data/

这就是 LanceDB 保存本地数据的目录。


四、代码逐步解释

1. 用 Document 表示文档

LangChain 使用 Document 同时保存正文和元数据:

ts 复制代码
new Document({
  pageContent: "登录状态保存在 Redis 中。",
  metadata: {
    source: "auth.md",
  },
});

其中:

  • pageContent 是参与 Embedding 和检索的正文;
  • metadata 是来源、分类等附加信息。

元数据不会替代正文,但可以随检索结果一起返回,帮助我们展示来源:

text 复制代码
来源:auth.md

真实项目还可以保存:

ts 复制代码
metadata: {
  source: "auth.md",
  system: "ext",
  docType: "FADR",
}

同一批数据中的字段类型应尽量保持一致。例如,不要让某些记录中的 createdAt 是字符串,另一些记录中却是对象。

2. 创建 Embedding 模型

ts 复制代码
const embeddings = new OllamaEmbeddings({
  model: "nomic-embed-text",
  baseUrl: "http://localhost:11434",
});

OllamaEmbeddings 是 LangChain 对 Ollama Embedding API 的封装。

写入文档时,它会调用:

ts 复制代码
embeddings.embedDocuments(texts);

查询时,它会调用:

ts 复制代码
embeddings.embedQuery(question);

二者都使用同一个模型,生成的向量才能在同一个语义空间中进行比较。

3. 创建表并写入文档

ts 复制代码
const store = await LanceDB.fromDocuments(documents, embeddings, {
  uri: "./vector-data",
  tableName: "documents",
  mode: "overwrite",
});

这段代码完成了四件事:

text 复制代码
读取 pageContent
   ↓
调用 Embedding 模型生成向量
   ↓
创建 documents 表
   ↓
写入文本、向量和元数据

三个配置项的含义是:

  • uri:LanceDB 数据目录;
  • tableName:表名;
  • mode:创建表时如何处理同名表。

本文使用 overwrite,每次运行都会重建同名表。这适合可以反复执行的教学示例,但不适合直接用于需要保留历史数据的写入流程。

4. 执行相似度检索

ts 复制代码
const results = await store.similaritySearch(
  "用户会话存在哪里?",
  1,
);

第二个参数 1 表示返回最相关的一条结果,也就是 Top-1。

内部过程可以概括为:

text 复制代码
问题文本
   ↓
生成查询向量
   ↓
在 documents 表中搜索最近向量
   ↓
返回对应的 Document

查询返回的是 Document[],因此可以继续读取正文和元数据:

ts 复制代码
for (const doc of results) {
  console.log(doc.pageContent);
  console.log(doc.metadata);
}

五、调整 Top-K

如果希望一次返回三条结果,可以把第二个参数改成 3

ts 复制代码
const results = await store.similaritySearch(
  "系统运行相关信息",
  3,
);

输出每条结果:

ts 复制代码
results.forEach((doc, index) => {
  console.log(`第 ${index + 1} 条:${doc.pageContent}`);
  console.log(`来源:${doc.metadata.source}`);
});

K 值需要根据实际场景调整:

  • K 太小,可能漏掉有用片段;
  • K 太大,可能返回更多无关内容;
  • 在 RAG 中,结果过多还会占用大模型上下文。

学习阶段可以从 Top-3 开始,再根据实际查询效果进行调整。


六、重新打开已经存在的表

前面的示例在同一个进程中完成写入和查询。真实项目通常会把索引和查询分开:

text 复制代码
索引程序:读取文档并建立向量表
查询程序:打开已有向量表并执行检索

可以使用 LanceDB SDK 打开已有表,再交给 LangChain 的 LanceDB 封装:

ts 复制代码
import { connect } from "@lancedb/lancedb";
import { LanceDB } from "@langchain/community/vectorstores/lancedb";
import { OllamaEmbeddings } from "@langchain/ollama";

const embeddings = new OllamaEmbeddings({
  model: "nomic-embed-text",
  baseUrl: "http://localhost:11434",
});

const connection = await connect("./vector-data");
const table = await connection.openTable("documents");

const store = new LanceDB(embeddings, { table });

const results = await store.similaritySearch(
  "应用是如何部署的?",
  1,
);

console.log(results[0].pageContent);
console.log(results[0].metadata.source);

预期更容易命中:

text 复制代码
系统使用 Docker 容器部署。

这里仍然要使用建立索引时的 Embedding 模型。更换模型后,即使成功打开旧表,旧向量与新查询向量也可能无法正确比较。

先检查表是否存在

如果表名来自用户配置或不同业务模块,可以先读取表名:

ts 复制代码
const connection = await connect("./vector-data");
const tableNames = await connection.tableNames();

if (!tableNames.includes("documents")) {
  throw new Error("向量表不存在,请先建立索引");
}

const table = await connection.openTable("documents");

这样可以把数据库底层错误转换成更容易理解的业务提示。


七、增加文档并观察结果

在示例中增加一条安全相关文档:

ts 复制代码
new Document({
  pageContent: "用户密码经过哈希处理后保存。",
  metadata: { source: "security.md" },
});

然后分别搜索:

text 复制代码
密码是怎样存储的?
登录状态保存在哪里?

两个问题都和用户系统有关,但关注点不同。可以观察 Embedding 模型是否能分别找到密码和登录状态对应的内容。

还可以尝试:

  1. 用不同表达方式提出同一个问题;
  2. 把 Top-K 从 1 改为 3;
  3. 增加语义相近的干扰文档;
  4. 修改元数据并观察返回结果;
  5. 比较短句与长段落的检索效果。

这些实验可以帮助我们理解:向量数据库只负责搜索,最终效果还取决于文档内容和 Embedding 模型。


八、从短句走向真实文档

真实知识库中的 Markdown 往往很长,不适合直接把整篇文档作为一条记录写入。

如果一篇文档同时包含认证、数据库和部署方案,整篇文档生成的单个向量可能无法准确表达每个局部主题。因此,写入向量库之前通常需要先分块:

text 复制代码
Markdown 文档
   ↓
读取正文
   ↓
按一定大小切分为 Chunk
   ↓
每个 Chunk 分别生成向量
   ↓
写入 LanceDB

使用 LangChain 文本分割器时,代码类似:

ts 复制代码
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";

const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 500,
  chunkOverlap: 50,
});

const chunks = await splitter.splitDocuments(documents);

其中:

  • chunkSize 控制每个片段的目标大小;
  • chunkOverlap 让相邻片段保留一部分重复内容,减少语义在边界处被完全切断的情况。

然后把 chunks 写入 LanceDB:

ts 复制代码
const store = await LanceDB.fromDocuments(chunks, embeddings, {
  uri: "./vector-data",
  tableName: "documents",
  mode: "overwrite",
});

50050 只是一个起点,并不是适合所有文档的标准答案。后续需要使用真实问题评估检索结果。


九、常见问题

1. 无法连接 Ollama

如果出现连接失败,先确认 Ollama 服务已经启动,并检查地址:

ts 复制代码
baseUrl: "http://localhost:11434"

还要确认模型已经下载:

bash 复制代码
ollama list

2. 每次运行后旧数据都消失

示例使用了:

ts 复制代码
mode: "overwrite"

它会重建同名表。正式项目应明确区分"全量重建索引"和"向已有表增加数据",不要在普通查询流程中执行覆盖写入。

3. 打开表后无法检索

创建 LangChain LanceDB 实例时,需要把打开的 table 传进去:

ts 复制代码
const store = new LanceDB(embeddings, { table });

只调用 openTable() 并不会自动得到 LangChain Vector Store。

4. 更换模型后结果异常

文档向量和查询向量必须来自同一个 Embedding 模型及兼容配置。更换模型后通常需要重建索引。

5. 最相似结果不符合预期

可以从以下方面排查:

  • 文档是否包含足够明确的信息;
  • 文本分块是否过大或过小;
  • Embedding 模型是否适合当前语言和领域;
  • 问题表达是否过于模糊;
  • Top-K 是否太小;
  • 是否存在大量语义相近的干扰内容。

向量检索返回的是"模型认为最接近的内容",并不保证结果一定正确。


十、在 RAG 中如何使用

目前我们完成的是检索部分:

text 复制代码
问题
  ↓
LanceDB
  ↓
相关文档

完整 RAG 还会把检索结果交给大模型:

text 复制代码
用户问题
   ↓
LanceDB 检索相关片段
   ↓
构造"问题 + 相关片段"
   ↓
大模型根据片段组织答案

LanceDB 的职责到"返回相关片段"为止。它不会自行判断最终答案,也不会代替大模型生成自然语言回复。

在 CorpRAG 中,这些能力会进一步拆分为:

text 复制代码
文档加载 → 文本分块 → Embedding → LanceDB → Retriever → MCP 工具

掌握本篇的最小示例后,再阅读项目代码时,就能更容易理解每一层的职责。


总结

本文完成了 LanceDB 最基础的使用流程:

text 复制代码
准备 Document
   ↓
通过 Ollama 生成向量
   ↓
使用 LanceDB.fromDocuments() 建立向量表
   ↓
使用 similaritySearch() 执行语义检索
   ↓
返回正文和来源

需要记住的核心结论有:

  1. LanceDB 负责保存和检索向量,不负责生成向量
  2. LangChain 的 Document 同时保存正文和元数据
  3. fromDocuments() 会生成向量并创建向量表
  4. similaritySearch() 会把问题向量化并返回 Top-K 个结果
  5. 索引和查询必须使用同一个 Embedding 模型
  6. overwrite 适合重建索引,不适合无条件用于日常增量写入
  7. 真实长文档通常需要先分块,再写入 LanceDB

最后用一句话概括:

Ollama 和 Embedding 模型负责生成语义坐标,LanceDB 负责保存坐标,并找出与问题最接近的文档。

相关推荐
xyphf_和派孔明1 小时前
企业级微前端项目完整创建步骤,第二步:配置主应用
前端
武子康1 小时前
低延迟不是更快地猜:EOU / Barge-in / Turn Protocol 必须统一(4 种结束 + Generation Fencing + 9 类可复现场景)
人工智能·后端·llm
默_笙1 小时前
🔑 让 AI 学会"读"网页:我用 Cheerio 爬了一篇掘金文章,然后切成小块存进了向量库
前端·javascript
颜进强1 小时前
Ollama 从入门到实践:本地模型运行、API 调用
前端·后端·ai编程
颜进强1 小时前
Embedding 基础使用:用 Ollama 和 LangChain.js 生成文本向量
前端·后端·ai编程
颜进强1 小时前
Vector Store 入门:什么是向量数据库,主流产品如何选择
前端·后端·ai编程
AI编程实验室1 小时前
Agent Skills 实战第三课:从规格到工单,别再按前后端拆任务
ai编程
AI大模型-小雄1 小时前
ChatGPT Plus 够不够用?从5种开发场景判断是否需要 Pro
chatgpt·ai编程·开发工具·codex·chatgpt plus·chatgpt pro
PH = 71 小时前
SpringBoot使用自动装配编写Start包
java·spring boot·后端