使用 LangChain 构建语义搜索引擎 (Build a semantic search engine)
概述 (Overview)
使用 LangChain 嵌入模型 (Embeddings) 与向量数据库 (Vector stores) 针对 PDF 构建语义搜索引擎。你可以用它检索与用户查询语义相似的文档段落,随后将检索器接入检索增强生成(RAG)或其他大语言模型工作流中。
本教程涵盖:
- 从 PDF 创建
Document文档对象。 - 生成文本嵌入向量(Embeddings)。
- 加载并切分 PDF。
- 将文本分块索引到向量库中并执行相似度查询。
- 将向量库包装为规范检索器(Retriever)。
本指南还在搜索引擎的基础之上提供了最小化 RAG 实现。
核心概念 (Concepts)
本教程聚焦于文本检索,涵盖以下核心概念:
环境准备 (Setup)
安装依赖 (Install dependencies)
本教程使用 pdf-parse 库读取 PDF:
npm i pdf-parseyarn add pdf-parsepnpm add pdf-parseFor more details, see the Installation guide.
Configure LangSmith
Many of the applications you build with LangChain will contain multiple steps with multiple invocations of LLM calls. As these applications get more and more complex, it becomes crucial to be able to inspect what exactly is going on inside your chain or agent. The best way to do this is with LangSmith.
After you sign up at the link above, make sure to set your environment variables to start logging traces:
export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="..."创建文档对象 (Create documents)
LangChain 提供了 Document 抽象来表示一段文本单元及其关联元数据。它具有三个属性:
pageContent:表示文档正文内容的字符串。metadata:包含自定义元数据键值对的对象。id:(可选)文档的字符串唯一标识符。
metadata 可以记录文档来源、与其他文档的关联关系等信息。单个 Document 通常代表一个完整长文档拆分后的文本块。
以下代码创建示例文档:
import { Document } from "@langchain/core/documents";
const documents = [
new Document({
pageContent:
"Dogs are great companions, known for their loyalty and friendliness.",
metadata: { source: "mammal-pets-doc" },
}),
new Document({
pageContent: "Cats are independent pets that often enjoy their own space.",
metadata: { source: "mammal-pets-doc" },
}),
];生成嵌入向量 (Generate embeddings)
向量搜索存储与文本相关联的数值向量。将用户查询映射为相同维度的向量,随后通过相似度度量(如余弦相似度)检索相关文本。
LangChain 支持来自多家提供商的嵌入模型。选择一个模型指定如何将文本转化为高维向量:
OpenAI
npm i @langchain/openaiyarn add @langchain/openaipnpm add @langchain/openaiimport { OpenAIEmbeddings } from "@langchain/openai";
const embeddings = new OpenAIEmbeddings({
model: "text-embedding-3-large"
});Azure
npm i @langchain/openaiyarn add @langchain/openaipnpm add @langchain/openaiAZURE_OPENAI_API_INSTANCE_NAME=<YOUR_INSTANCE_NAME>
AZURE_OPENAI_API_KEY=<YOUR_KEY>
AZURE_OPENAI_API_VERSION="2024-02-01"import { AzureOpenAIEmbeddings } from "@langchain/openai";
const embeddings = new AzureOpenAIEmbeddings({
azureOpenAIApiEmbeddingsDeploymentName: "text-embedding-ada-002"
});AWS
npm i @langchain/awsyarn add @langchain/awspnpm add @langchain/awsBEDROCK_AWS_REGION=your-regionimport { BedrockEmbeddings } from "@langchain/aws";
const embeddings = new BedrockEmbeddings({
model: "amazon.titan-embed-text-v1"
});Gemini Enterprise Agent Platform
npm i @langchain/google-vertexaiyarn add @langchain/google-vertexaipnpm add @langchain/google-vertexaiGOOGLE_APPLICATION_CREDENTIALS=credentials.jsonimport { VertexAIEmbeddings } from "@langchain/google-vertexai";
const embeddings = new VertexAIEmbeddings({
model: "gemini-embedding-001"
});MistralAI
npm i @langchain/mistralaiyarn add @langchain/mistralaipnpm add @langchain/mistralaiMISTRAL_API_KEY=your-api-keyimport { MistralAIEmbeddings } from "@langchain/mistralai";
const embeddings = new MistralAIEmbeddings({
model: "mistral-embed"
});Cohere
npm i @langchain/cohereyarn add @langchain/coherepnpm add @langchain/cohereCOHERE_API_KEY=your-api-keyimport { CohereEmbeddings } from "@langchain/cohere";
const embeddings = new CohereEmbeddings({
model: "embed-english-v3.0"
});const vector1 = await embeddings.embedQuery(documents[0].pageContent);
const vector2 = await embeddings.embedQuery(documents[1].pageContent);
assert vector1.length === vector2.length;
console.log(`Generated vectors of length ${vector1.length}\n`);
console.log(vector1.slice(0, 10));Generated vectors of length 1536
[-0.008586574345827103, -0.03341241180896759, -0.008936782367527485, -0.0036674530711025, 0.010564599186182022, 0.009598285891115665, -0.028587326407432556, -0.015824200585484505, 0.0030416189692914486, -0.012899317778646946]接下来,将嵌入向量存入支持高效相似度检索的向量数据库中。
选择向量数据库 (Select a vector store)
LangChain 的 VectorStore 对象负责将文本和 Document 存入数据库,并通过相似度度量指标进行检索。它们通常使用嵌入模型进行初始化,以将文本转换为向量。
LangChain 集成了多种向量库技术。有些是云端托管服务需要配置凭证,有些运行在独立基础架构上(本地或第三方),还有一些是纯内存型向量库以支持轻量级工作负载。选择你的向量库:
Memory
npm i @langchain/classicyarn add @langchain/classicpnpm add @langchain/classicimport { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";
const vectorStore = new MemoryVectorStore(embeddings);MongoDB
npm i @langchain/mongodbyarn add @langchain/mongodbpnpm add @langchain/mongodbimport { MongoDBAtlasVectorSearch } from "@langchain/mongodb"
import { MongoClient } from "mongodb";
const client = new MongoClient(process.env.MONGODB_ATLAS_URI || "");
const collection = client
.db(process.env.MONGODB_ATLAS_DB_NAME)
.collection(process.env.MONGODB_ATLAS_COLLECTION_NAME);
const vectorStore = new MongoDBAtlasVectorSearch(embeddings, {
collection: collection,
indexName: "vector_index",
textKey: "text",
embeddingKey: "embedding",
});Pinecone
npm i @langchain/pineconeyarn add @langchain/pineconepnpm add @langchain/pineconeimport { PineconeStore } from "@langchain/pinecone";
import { Pinecone as PineconeClient } from "@pinecone-database/pinecone";
const pinecone = new PineconeClient({
apiKey: process.env.PINECONE_API_KEY,
});
const pineconeIndex = pinecone.Index("your-index-name");
const vectorStore = new PineconeStore(embeddings, {
pineconeIndex,
maxConcurrency: 5,
});Qdrant
npm i @langchain/qdrantyarn add @langchain/qdrantpnpm add @langchain/qdrantimport { QdrantVectorStore } from "@langchain/qdrant";
const vectorStore = await QdrantVectorStore.fromExistingCollection(embeddings, {
url: process.env.QDRANT_URL,
collectionName: "langchainjs-testing",
});Redis
npm i @langchain/redisyarn add @langchain/redispnpm add @langchain/redisimport { RedisVectorStore } from "@langchain/redis";
const vectorStore = new RedisVectorStore(embeddings, {
redisClient: client,
indexName: "langchainjs-testing",
});加载并切分 PDF (Load and split a PDF)
从 PDF 文件中加载内容,并在建立索引之前将其切分为较小的文本块。本示例使用 Nike 2023 财年 10-K 财报 PDF 样本。
import { readFileSync } from "node:fs";
import { Document } from "@langchain/core/documents";
import { PDFParse } from "pdf-parse";
// Below is a minimal helper for demonstration purposes.
async function loadPdfPages(filePath: string): Promise<Document[]> {
const parser = new PDFParse({
data: new Uint8Array(readFileSync(filePath)),
});
try {
const { pages } = await parser.getText();
return pages.map(
(page) =>
new Document({
pageContent: page.text,
metadata: { source: filePath, page: page.num - 1 },
})
);
} finally {
await parser.destroy();
}
}
const filePath = "../../data/nke-10k-2023.pdf";
const docs = await loadPdfPages(filePath);
console.log(docs.length);107单页内容通常颗粒度太大,不利于精准检索。进一步切分页面可以避免关键段落被周围无关文字稀释。RecursiveCharacterTextSplitter 会按照常用分隔符(例如换行符)递归切分,直到每个分块达到目标大小。这是处理通用文本场景的首选切分器。
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 200,
});
const allSplits = await textSplitter.splitDocuments(docs);
console.log(allSplits.length);516索引文档 (Index documents)
将文本分块批量存入向量数据库:
await vectorStore.addDocuments(allSplits);大多数向量数据库集成还支持直接连接到已有库(例如通过客户端或已有索引名称)。详情请参阅具体向量库集成文档。
查询向量数据库 (Query the vector store)
将文档添加到 VectorStore 后,你可以通过多种方式查询:
- 同步或异步查询
- 通过自然语言文本或向量查询
- 附带或不附带相似度分数
- 按照余弦相似度或最大边际相关性(MMR)(平衡相似度与结果多样性)
这些查询方法通常返回一组 Document 对象。
通过文本字符串检索 (Search by string)
嵌入模型将文本映射到稠密向量空间中,使得语义相近的内容在几何距离上彼此靠近。这意味着你可以直接传入自然语言问题来检索相关段落:
const results1 = await vectorStore.similaritySearch(
"When was Nike incorporated?"
);
console.log(results1[0]);Document {
pageContent: 'direct to consumer operations sell products...',
metadata: {'page': 4, 'source': '../example_data/nke-10k-2023.pdf', 'start_index': 3125}
}返回相似度分数 (Return scores)
你可以在返回文档的同时获取相似度得分。不同向量库的分数定义有所不同,此处的分数代表距离指标(数值越小表示相似度越高):
const results2 = await vectorStore.similaritySearchWithScore(
"What was Nike's revenue in 2023?"
);
console.log(results2[0]);Score: 0.23699893057346344
Document {
pageContent: 'Table of Contents...',
metadata: {'page': 35, 'source': '../example_data/nke-10k-2023.pdf', 'start_index': 0}
}通过向量检索 (Search by vector)
自行将查询向量化,然后直接使用生成的数值向量进行检索:
const embedding = await embeddings.embedQuery(
"How were Nike's margins impacted in 2023?"
);
const results3 = await vectorStore.similaritySearchVectorWithScore(
embedding,
1
);
console.log(results3[0]);Document {
pageContent: 'FISCAL 2023 COMPARED TO FISCAL 2022...',
metadata: {
'page': 36,
'source': '../example_data/nke-10k-2023.pdf',
'start_index': 0
}
}Learn more:
使用检索器 (Use retrievers)
LangChain 的 VectorStore 对象并未直接继承 Runnable。而检索器 (Retrievers) 是标准的 Runnable,因此支持同步/异步的 invoke 与 batch 等标准调用方法。
你可以从向量库生成检索器,检索器也可以封装非向量数据源(例如外部 REST API)。
向量库实现了 asRetriever 方法,返回一个 VectorStoreRetriever。这些检索器暴露了 searchType 和 searchKwargs 参数,用于配置底层的检索算法:
const retriever = vectorStore.asRetriever({
searchType: "mmr",
searchKwargs: {
fetchK: 1,
},
});
await retriever.batch([
"When was Nike incorporated?",
"What was Nike's revenue in 2023?",
]);[
[Document {
metadata: {'page': 4, 'source': '../example_data/nke-10k-2023.pdf', 'start_index': 3125},
pageContent: 'direct to consumer operations sell products...',
}],
[Document {
metadata: {'page': 3, 'source': '../example_data/nke-10k-2023.pdf', 'start_index': 0},
pageContent: 'Table of Contents...',
}],
]你可以将检索器用于构建更复杂的应用,如检索增强生成(RAG)——在提示词中将用户问题与检索到的上下文融合传递给大模型。深入了解如何构建此类应用,请参阅 Deep Agents RAG 教程。
后续进阶 (Next steps)
你已经掌握了如何基于 PDF 文档构建语义搜索引擎。
欲了解更多信息,请参考:
关于 RAG 的更多内容:
