构建一个语义搜索引擎
本教程将向您介绍 LangChain 的 文档加载器、嵌入 和 向量存储 抽象。这些抽象设计用于从(向量)数据库和其他来源检索数据,以集成到 LLM 工作流中。对于在模型推理过程中通过检索数据进行推理的应用程序(例如检索增强生成 RAG,请参阅我们的 RAG 教程 此处)而言,这些功能非常重要。
在这里,我们将基于 PDF 文档构建一个搜索引擎。这将允许我们检索与输入查询相似的 PDF 中的段落。
概念
本指南专注于文本数据的检索。我们将涵盖以下概念:
- 文档和文档加载器;
- 文本分割器;
- 嵌入;
- 向量存储和检索器。
准备工作
Jupyter Notebook
本指南及其他教程可能最适合在 Jupyter Notebook 中运行。安装说明请参见 此处。
安装
本指南需要 @langchain/community 和 pdf-parse:
- npm
- yarn
- pnpm
npm i @langchain/community pdf-parse
yarn add @langchain/community pdf-parse
pnpm add @langchain/community pdf-parse
更多细节,请参阅我们的 安装指南。
LangSmith
您使用 LangChain 构建的许多应用程序将包含多个步骤,涉及多次调用 LLM。 随着这些应用程序变得越来越复杂,能够检查您的链或代理内部的具体情况变得至关重要。 实现此目的的最佳方法是使用 LangSmith。
在上面的链接注册后,请确保设置您的环境变量以开始记录跟踪:
export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="..."
# 如果您不在无服务器环境中,可以减少跟踪延迟
# export LANGCHAIN_CALLBACKS_BACKGROUND=true
文档和文档加载器
LangChain 实现了一个 Document 抽象,旨在表示一个文本单元及其相关的元数据。它有三个属性:
pageContent:表示内容的字符串;metadata:任意元数据的记录;id:(可选)文档的字符串标识符。
属性 metadata
可以捕获文档来源、与其他文档的关系以及其他信息。请注意,单个 Document
对象通常代表较大文档的一部分。
我们可以在需要时生成示例文档:
import { Document } from "@langchain/core/documents";
const documents = [
new Document({
pageContent: "狗是很好的伴侣,以其忠诚和友好著称。",
metadata: { source: "mammal-pets-doc" },
}),
new Document({
pageContent: "猫是独立的宠物,通常喜欢自己的空间。",
metadata: { source: "mammal-pets-doc" },
}),
];
然而,LangChain 生态系统实现了 文档加载器,这些加载器 与数百种常见来源集成。这使得将这些来源的数据集成到您的 AI 应用程序中变得更加容易。
加载文档
让我们将 PDF 加载为一系列 Document 对象。LangChain 仓库中有一个示例
PDF,这里
是耐克 2023 年的 10-K 文件。LangChain 实现了
PDFLoader,我们可以使用它来解析
PDF:
import { PDFLoader } from "@langchain/community/document_loaders/fs/pdf";
const loader = new PDFLoader("../../data/nke-10k-2023.pdf");
const docs = await loader.load();
console.log(docs.length);
107
有关 PDF 文档加载器的更多详细信息,请参阅 本指南。
PDFLoader 会为每个 PDF 页面加载一个 Document
对象。对于每个文档,我们可以轻松访问:
- 页面的字符串内容;
- 包含文件名和页码的元数据。
docs[0].pageContent.slice(0, 200);
目录
美国
证券交易委员会
华盛顿特区 20549
表格 10-K
(勾选一项)
☑ 根据 1934 年证券交易法第 13 条或第 15(d) 条提交的年度报告
FO
docs[0].metadata;
{
source: '../../data/nke-10k-2023.pdf',
pdf: {
version: '1.10.100',
info: {
PDFFormatVersion: '1.4',
IsAcroFormPresent: false,
IsXFAPresent: false,
Title: '0000320187-23-000039',
Author: 'EDGAR Online, a division of Donnelley Financial Solutions',
Subject: 'Form 10-K filed on 2023-07-20 for the period ending 2023-05-31',
Keywords: '0000320187-23-000039; ; 10-K',
Creator: 'EDGAR Filing HTML Converter',
Producer: 'EDGRpdf Service w/ EO.Pdf 22.0.40.0',
CreationDate: "D:20230720162200-04'00'",
ModDate: "D:20230720162208-04'00'"
},
metadata: null,
totalPages: 107
},
loc: { pageNumber: 1 }
}
分割
为了信息检索和后续问答的目的,一个页面可能过于粗略。我们的最终目标是检索回答输入查询的
Document 对象,进一步分割我们的 PDF
将有助于确保文档中相关部分的含义不会因周围文本而“被冲淡”。
我们可以使用 文本分割器 来实现此目的。在这里,我们将使用一个基于字符的简单文本分割器。我们将文档分割为 1000 个字符的块, 并在块之间保留 200 个字符的重叠。重叠有助于减少将陈述与其相关上下文分离的可能性。我们使用 RecursiveCharacterTextSplitter, 它将递归地使用常见分隔符(如换行符)分割文档,直到每个块的大小合适为止。这是推荐用于通用文本用例的文本分割器。
我们设置 add_start_index=True,以便在初始文档中每个分割的 Document
开始的字符索引作为元数据属性“start_index”保留。
有关处理 PDF 的更多详细信息,请参阅 本指南。
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 200,
});
const allSplits = await textSplitter.splitDocuments(docs);
allSplits.length;
513
嵌入
向量搜索是存储和搜索非结构化数据(如非结构化文本)的常见方式。其思路是存储与文本关联的数值向量。给定一个查询,我们可以将其 嵌入 为相同维度的向量,并使用向量相似度度量(如余弦相似度)来识别相关文本。
LangChain 支持来自 数十个提供商 的嵌入。这些模型指定了如何将文本转换为数值向量。让我们选择一个模型。
Pick your embedding model:
- OpenAI
- Azure
- AWS
- VertexAI
- MistralAI
- Cohere
Install dependencies
- npm
- yarn
- pnpm
npm i @langchain/openai
yarn add @langchain/openai
pnpm add @langchain/openai
OPENAI_API_KEY=your-api-key
import { OpenAIEmbeddings } from "@langchain/openai";
const embeddings = new OpenAIEmbeddings({
model: "text-embedding-3-large"
});
Install dependencies
- npm
- yarn
- pnpm
npm i @langchain/openai
yarn add @langchain/openai
pnpm add @langchain/openai
AZURE_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"
});
Install dependencies
- npm
- yarn
- pnpm
npm i @langchain/aws
yarn add @langchain/aws
pnpm add @langchain/aws
BEDROCK_AWS_REGION=your-region
import { BedrockEmbeddings } from "@langchain/aws";
const embeddings = new BedrockEmbeddings({
model: "amazon.titan-embed-text-v1"
});
Install dependencies
- npm
- yarn
- pnpm
npm i @langchain/google-vertexai
yarn add @langchain/google-vertexai
pnpm add @langchain/google-vertexai
GOOGLE_APPLICATION_CREDENTIALS=credentials.json
import { VertexAIEmbeddings } from "@langchain/google-vertexai";
const embeddings = new VertexAIEmbeddings({
model: "text-embedding-004"
});
Install dependencies
- npm
- yarn
- pnpm
npm i @langchain/mistralai
yarn add @langchain/mistralai
pnpm add @langchain/mistralai
MISTRAL_API_KEY=your-api-key
import { MistralAIEmbeddings } from "@langchain/mistralai";
const embeddings = new MistralAIEmbeddings({
model: "mistral-embed"
});
Install dependencies
- npm
- yarn
- pnpm
npm i @langchain/cohere
yarn add @langchain/cohere
pnpm add @langchain/cohere
COHERE_API_KEY=your-api-key
import { CohereEmbeddings } from "@langchain/cohere";
const embeddings = new CohereEmbeddings({
model: "embed-english-v3.0"
});
const vector1 = await embeddings.embedQuery(allSplits[0].pageContent);
const vector2 = await embeddings.embedQuery(allSplits[1].pageContent);
console.assert(vector1.length === vector2.length);
console.log(`生成长度为 ${vector1.length}\n`);
console.log(vector1.slice(0, 10));
生成长度为 3072 的向量
[
0.014310152,
-0.01681044,
-0.0011537228,
0.010546423,
0.022808468,
-0.028327717,
-0.00058849837,
0.0419197,
-0.0012900416,
0.0661778
]
有了生成文本嵌入的模型后,我们可以将其存储在一个特殊的数据结构中,该结构支持高效的相似性搜索。
向量存储
LangChain 的
VectorStore
对象包含将文本和 Document
对象添加到存储的方法,以及使用各种相似性度量进行查询的方法。它们通常使用
嵌入
模型初始化,这些模型决定如何将文本数据转换为数值向量。
LangChain 包括与不同向量存储技术的 集成。一些向量存储由提供商托管(例如,各种云提供商),需要特定的凭据才能使用;一些(例如 Postgres)可以在本地运行或通过第三方运行;还有一些可以在内存中运行以处理轻量级工作负载。
Pick your vector store:
- Memory
- Chroma
- FAISS
- MongoDB
- PGVector
- Pinecone
- Qdrant
Install dependencies
- npm
- yarn
- pnpm
npm i langchain
yarn add langchain
pnpm add langchain
import { MemoryVectorStore } from "langchain/vectorstores/memory";
const vectorStore = new MemoryVectorStore(embeddings);
Install dependencies
- npm
- yarn
- pnpm
npm i @langchain/community
yarn add @langchain/community
pnpm add @langchain/community
import { Chroma } from "@langchain/community/vectorstores/chroma";
const vectorStore = new Chroma(embeddings, {
collectionName: "a-test-collection",
});
Install dependencies
- npm
- yarn
- pnpm
npm i @langchain/community
yarn add @langchain/community
pnpm add @langchain/community
import { FaissStore } from "@langchain/community/vectorstores/faiss";
const vectorStore = new FaissStore(embeddings, {});
Install dependencies
- npm
- yarn
- pnpm
npm i @langchain/mongodb
yarn add @langchain/mongodb
pnpm add @langchain/mongodb
import { 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",
});
Install dependencies
- npm
- yarn
- pnpm
npm i @langchain/community
yarn add @langchain/community
pnpm add @langchain/community
import { PGVectorStore } from "@langchain/community/vectorstores/pgvector";
const vectorStore = await PGVectorStore.initialize(embeddings, {})
Install dependencies
- npm
- yarn
- pnpm
npm i @langchain/pinecone
yarn add @langchain/pinecone
pnpm add @langchain/pinecone
import { PineconeStore } from "@langchain/pinecone";
import { Pinecone as PineconeClient } from "@pinecone-database/pinecone";
const pinecone = new PineconeClient();
const vectorStore = new PineconeStore(embeddings, {
pineconeIndex,
maxConcurrency: 5,
});
Install dependencies
- npm
- yarn
- pnpm
npm i @langchain/qdrant
yarn add @langchain/qdrant
pnpm add @langchain/qdrant
import { QdrantVectorStore } from "@langchain/qdrant";
const vectorStore = await QdrantVectorStore.fromExistingCollection(embeddings, {
url: process.env.QDRANT_URL,
collectionName: "langchainjs-testing",
});
实例化我们的向量存储后,现在可以对文档进行索引。
await vectorStore.addDocuments(allSplits);
请注意,大多数向量存储实现都允许您连接到现有的向量存储——例如,通过提供客户端、索引名称或其他信息。有关特定 集成 的详细信息,请参阅文档。
一旦我们实例化了一个包含文档的
VectorStore,我们就可以对其进行查询。VectorStore
包括以下查询方法: - 同步和异步; - 按字符串查询和按向量查询; -
返回和不返回相似度分数; - 按相似度和
最大边缘相关性(在检索结果中平衡与查询的相似度和多样性)。
这些方法的输出通常包括一个 Document 对象列表。
使用
嵌入通常将文本表示为“密集”向量,使得含义相似的文本在几何上接近。这使我们能够通过传递问题来检索相关信息,而无需知道文档中使用的任何特定关键词。
根据与字符串查询的相似性返回文档:
const results1 = await vectorStore.similaritySearch("耐克是什么时候成立的?");
results1[0];
Document {
pageContent: '目录\n' +
'第一部分\n' +
'第1项。业务\n' +
'一般情况\n' +
'NIKE, Inc. 于1967年根据俄勒冈州法律注册成立。在本年度报告表10-K(本“年度报告”)中使用的术语“我们”、“我们”、“我们的”,\n' +
'“NIKE”和“公司”是指NIKE, Inc.及其前身、子公司和关联公司,统称,除非上下文另有说明。\n' +
'我们主要的业务活动是设计、开发和全球营销及销售运动鞋、服装、设备、配件和服务。NIKE是\n' +
'世界上最大的运动鞋和服装销售商。我们通过NIKE直营业务进行销售,这些业务包括NIKE拥有零售店\n' +
'以及通过我们的数字平台(也称为“NIKE品牌数字”)销售给零售客户和各种独立分销商、被许可方和销售',
metadata: {
source: '../../data/nke-10k-2023.pdf',
pdf: {
version: '1.10.100',
info: [Object],
metadata: null,
totalPages: 107
},
loc: { pageNumber: 4, lines: [Object] }
},
id: undefined
}
返回分数:
const results2 = await vectorStore.similaritySearchWithScore(
"耐克2023年的收入是多少?"
);
results2[0];
[
Document {
pageContent: '目录\n' +
'2023财年NIKE品牌收入亮点\n' +
'下表列出了按可报告运营部门、分销渠道和主要产品线划分的NIKE品牌收入:\n' +
'2023财年与2022财年比较\n' +
'•NIKE, Inc. 2023财年的收入为512亿美元,较2022财年按报告基础和货币中性基础分别增长了10%和16%。\n' +
'增长的原因是北美、欧洲、中东和非洲(“EMEA”)、APLA和大中华区的收入增加,分别对NIKE, Inc. 收入贡献了约7、6、\n' +
'2和1个百分点。\n' +
'•NIKE品牌收入占NIKE, Inc. 收入的90%以上,按报告基础和货币中性基础分别增长了10%和16%。\n' +
"这一增长主要是由于男士、乔丹品牌、女士和儿童的增长,分别在批发\n" +
'等效基础上增长了17%、35%、11%和10%。',
metadata: {
source: '../../data/nke-10k-2023.pdf',
pdf: [Object],
loc: [Object]
},
id: undefined
},
0.6992287611800424
]
根据嵌入查询的相似性返回文档:
const embedding = await embeddings.embedQuery(
"2023年耐克的利润率受到了怎样的影响?"
);
const results3 = await vectorStore.similaritySearchVectorWithScore(
embedding,
1
);
results3[0];
[
Document {
pageContent: '目录\n' +
'毛利率\n' +
'2023财年与2022财年比较\n' +
'2023财年,我们的合并毛利增长4%至222.92亿美元,而2022财年为214.79亿美元。毛利率下降250个基点至\n' +
'2023财年的43.5%,而2022财年为46.0%,原因如下:\n' +
'*批发等效\n' +
'2023财年毛利率下降的主要原因是:\n' +
'•NIKE品牌产品成本增加,按批发等效基础计算,主要是由于原材料成本增加、入境运输和物流成本上升以及\n' +
'产品组合;\n' +
'•我们在NIKE直营业务中的毛利率较低,这是由于当前期间为了清理库存而进行的促销活动较多,而前一期间由于库存供应不足而促销活动较少;\n' +
'•净外汇汇率的不利变化,包括对冲;以及\n' +
'•按批发等效基础计算,特卖毛利率较低。\n' +
'这被部分抵消了:',
metadata: {
source: '../../data/nke-10k-2023.pdf',
pdf: [Object],
loc: [Object]
},
id: undefined
},
0.7368815472158006
]
了解更多:
检索器
LangChain 的 VectorStore 对象不继承
Runnable。LangChain
的
检索器
是可运行的,因此它们实现了一组标准方法(例如同步和异步的 invoke 和
batch
操作)。尽管我们可以从向量存储构建检索器,但检索器还可以与非向量存储的数据源交互(例如外部
API)。
向量存储实现了一个 as
retriever
方法,该方法将生成一个检索器,特别是
VectorStoreRetriever。这些检索器包括特定的
search_type 和 search_kwargs
属性,这些属性标识要调用的底层向量存储的方法及其参数化方式。
const retriever = vectorStore.asRetriever({
searchType: "mmr",
searchKwargs: {
fetchK: 1,
},
});
await retriever.batch(["耐克是什么时候成立的?", "耐克2023年的收入是多少?"]);
[
[
Document {
pageContent: '目录\n' +
'第一部分\n' +
'第1项。业务\n' +
'一般情况\n' +
'NIKE, Inc. 于1967年根据俄勒冈州法律注册成立。在本年度报告表10-K(本“年度报告”)中使用的术语“我们”、“我们”、“我们的”,\n' +
'“NIKE”和“公司”是指NIKE, Inc.及其前身、子公司和关联公司,统称,除非上下文另有说明。\n' +
'我们主要的业务活动是设计、开发和全球营销及销售运动鞋、服装、设备、配件和服务。NIKE是\n' +
'世界上最大的运动鞋和服装销售商。我们通过NIKE直营业务进行销售,这些业务包括NIKE拥有零售店\n' +
'以及通过我们的数字平台(也称为“NIKE品牌数字”)销售给零售客户和各种独立分销商、被许可方和销售',
metadata: [Object],
id: undefined
}
],
[
Document {
pageContent: '目录\n' +
'2023财年NIKE品牌收入亮点\n' +
'下表列出了按可报告运营部门、分销渠道和主要产品线划分的NIKE品牌收入:\n' +
'2023财年与2022财年比较\n' +
'•NIKE, Inc. 2023财年的收入为512亿美元,较2022财年按报告基础和货币中性基础分别增长了10%和16%。\n' +
'增长的原因是北美、欧洲、中东和非洲(“EMEA”)、APLA和大中华区的收入增加,分别对NIKE, Inc. 收入贡献了约7、6、\n' +
'2和1个百分点。\n' +
'•NIKE品牌收入占NIKE, Inc. 收入的90%以上,按报告基础和货币中性基础分别增长了10%和16%。\n' +
"这一增长主要是由于男士、乔丹品牌、女士和儿童的增长,分别在批发\n" +
'等效基础上增长了17%、35%、11%和10%。',
metadata: [Object],
id: undefined
}
]
]
VectorStoreRetriever 支持 "similarity"(默认)和
"mmr"(最大边缘相关性,如上所述)的搜索类型。
检索器可以轻松集成到更复杂的应用程序中,例如 检索增强生成 (RAG) 应用程序,这些应用程序将给定问题与检索到的上下文结合,生成一个用于 LLM 的提示。要了解如何构建此类应用程序,请查看 RAG 教程。
了解更多:
检索策略可以丰富而复杂。例如:
- 我们可以从查询中 推断硬规则和过滤器(例如,“使用 2020 年以后发布的文档”);
- 我们可以 返回与检索到的上下文相关联的文档(例如,通过某种文档分类法);
- 我们可以为每个上下文单元生成 多个嵌入;
- 我们可以 集成多个检索器的结果;
- 我们可以为文档分配权重,例如,为 最近的文档 赋予更高的权重。
如何指南中的 检索器 部分涵盖了这些及其他内置检索策略。
扩展 BaseRetriever 类以实现自定义检索器也很简单。请参阅我们的如何指南 此处。
下一步
您现在已经了解了如何构建一个基于 PDF 文档的语义搜索引擎。
有关文档加载器的更多信息:
有关嵌入的更多信息:
有关向量存储的更多信息:
有关 RAG 的更多信息,请参阅: