第二章 RAG第一部分:索引您的数据
在前一章中,您了解了使用LangChain创建大型语言模型应用程序的重要构建块。您还构建了一个简单的AI聊天机器人,由发送给模型的提示和模型生成的输出组成。但是这个简单的聊天机器人有很大的局限性。
如果您的用例需要模型没有训练的知识,该怎么办?例如,假设您想使用AI询问有关公司的问题,但信息包含在私有PDF或其他类型的文档中。虽然我们已经看到模型提供者丰富了他们的训练数据集,以包括越来越多的世界公共信息(无论它以何种格式存储),但大型语言模型的知识语料库仍然存在两个主要限制:
-
私人数据
根据定义,不公开的信息不包括在大型语言模型的训练数据中。
-
时事
训练大型语言模型是一个昂贵且耗时的过程,可能会持续数年,而数据收集只是第一步。这导致了所谓的知识截止日期,即大型语言模型对现实世界事件一无所知的日期;通常这是训练集完成的日期。这可以是过去几个月到几年的任何时间,取决于所讨论的模型。
在任何一种情况下,模型都很可能产生幻觉(发现误导或错误的信息),并以不准确的信息作出反应。调整提示也不能解决问题,因为它依赖于模型当前的知识。
目标:为大型语言模型选择相关背景
如果大型语言模型用例所需的唯一私有/当前数据是一到两页的文本,那么本章将会短得多:要使大型语言模型获得该信息,您所需要的就是在发送给模型的每个提示中包含整个文本。
向大型语言模型提供数据的挑战首先是数量问题。你发送给大型语言模型的每一个提示都包含了太多的信息。每次调用模型时,您将包含大量文本集合中的哪个小子集?或者换句话说,您如何(在模型的帮助下)选择与每个问题最相关的文本?
在本章和下一章中,你将学习如何通过两个步骤来克服这个挑战:
-
索引您的文档,也就是说,以一种您的应用程序可以轻松地为每个问题找到最相关的方式对它们进行预处理
-
从索引中检索这些外部数据,并将其用作大型语言模型的上下文,以便根据您的数据生成准确的输出
这一章的重点是索引,这是第一步,它涉及到将你的文档预处理成一种可以被大型语言模型理解和搜索的格式。这种技术称为检索增强生成(RAG)。但在我们开始之前,让我们讨论一下为什么您的文档需要预处理。
假设您想使用大型语言模型来分析特斯拉2022年年报中的财务业绩和风险,该报告以PDF格式存储。你的目标是能够提出这样的问题:“特斯拉在2022年面临哪些主要风险?”,并根据文档的风险因素部分的上下文获得类似人类的回应。
为了实现这一目标,你需要采取四个关键步骤(如图2-1所示):
-
从文档中提取文本。
-
将文本分成易于管理的小块。
-
将文本转换成计算机能理解的数字。
-
将文本的这些数字表示形式存储在某个地方,以便轻松快速地检索文档的相关部分以回答给定的问题。
图2-1预处理文档以供大型语言模型使用的四个关键步骤
图2-1说明了文档的预处理和转换流程,这个过程称为摄取。摄取就是将文档转换为计算机可以理解和分析的数字,并将其存储在特殊类型的数据库中以便有效检索的过程。这些数字在形式上被称为嵌入,这种特殊类型的数据库被称为向量存储。让我们从一些比llm驱动的嵌入更简单的东西开始,更仔细地看看什么是嵌入以及它们为什么重要。
嵌入:将文本转换为数字
嵌入指的是将文本表示为一个(长)数字序列。这是一种有损表示——也就是说,您无法从这些数字序列中恢复原始文本,因此通常要同时存储原始文本和这种数字表示。
那么,为什么要麻烦呢?因为你获得了与数字打交道所带来的灵活性和力量:你可以用单词做数学!让我们看看为什么这是令人兴奋的。
在大型语言模型之前的嵌入
早在大型语言模型之前,计算机科学家就在使用嵌入——例如,在网站上启用全文搜索功能,或将电子邮件分类为垃圾邮件。让我们来看一个例子:
-
以这三个句子为例:
-
多么晴朗的一天。
-
今天的天空真明亮。
-
我已经好几个星期没见过晴天了。
-
-
列出其中所有独特的单词:what, a, sunny, day, such, bright等等。
-
对于每个句子,一个单词一个单词地进行分析,如果没有出现,则将数字赋值为0,如果在句子中出现一次,则赋值为1,如果出现两次,则赋值为2,以此类推。
结果如表2-1所示。
表2-1 三个句子的词嵌入
在这个模型中,我几周没有看到一个晴天的嵌入是数字序列0 1 1 1 1 0 0 0 0 1 1 1 1 1 1。这被称为词袋模型,这些嵌入也被称为稀疏嵌入(或稀疏向量-向量是数字序列的另一个词),因为很多数字将为0。大多数英语句子只使用现有英语单词的一小部分。
您可以成功地将此模型用于:
-
关键词搜索
您可以找到哪些文档包含一个或多个给定的单词
-
文件分类
您可以计算先前标记为电子邮件垃圾邮件或非垃圾邮件的示例集合的嵌入,将它们取平均值,并获得每个类(垃圾邮件或非垃圾邮件)的平均单词频率。然后,将每个新文档与这些平均值进行比较,并进行相应的分类。
这里的限制是,模型没有意义意识,只有实际使用的单词。例如,sunny day和bright skies的嵌入看起来非常不同。事实上,它们没有共同的词汇,尽管我们知道它们有相似的意思。或者,在电子邮件分类问题中,潜在的垃圾邮件发送者可以通过用同义词替换常见的“垃圾邮件词”来欺骗过滤器。
在下一节中,我们将看到语义嵌入如何通过使用数字来表示文本的含义而不是文本中找到的确切单词来解决这一限制。
基于大模型的嵌入
我们将跳过其间的所有机器学习开发,直接跳到基于大模型的嵌入。要知道,从上一节中概述的简单方法到本文中描述的复杂方法是一个逐渐演变的过程。
您可以将嵌入模型视为大型语言模型训练过程的一个分支。如果您还记得前言中的内容,大型语言模型训练过程(从大量书面文本中学习)使大型语言模型能够以最合适的延续(输出)完成提示(或输入)。这种能力源于对周围文本上下文中单词和句子含义的理解,并从训练文本中如何将单词一起使用中学习。这种对提示符含义(或语义)的理解可以提取为输入文本的数字表示(或嵌入),也可以直接用于一些非常有趣的用例。
在实践中,大多数嵌入模型都是为此目的单独训练的,遵循与大型语言模型类似的体系结构和训练过程,因为这样更有效,并产生更高质量的嵌入。
因此,嵌入模型是一种算法,它接受一段文本并输出其含义的数字表示形式——从技术上讲,是一长串浮点(十进制)数,通常在100到2000个数字或维度之间。这些也被称为密集嵌入,与前一节的稀疏嵌入相反,因为这里的所有维度通常都不等于0。
提示
不同的模型产生不同数量和大小的列表。所有这些都是特定于每个模型的;也就是说,即使列表的大小匹配,您也不能比较来自不同模型的嵌入。应该始终避免组合来自不同模型的嵌入。
解释语义嵌入
想想这三个词:狮子、宠物和狗。直观地看,哪一对单词乍一看具有相似的特征?最明显的答案是宠物和狗。但计算机没有能力利用这种直觉或对英语语言的细微理解。为了让计算机区分狮子、宠物或狗,你需要能够将它们翻译成计算机的语言,也就是数字。
图2-2说明了将每个单词转换为保留其含义的假设数字表示。
图2-2 词的语义表示
图2-2显示了每个单词及其相应的语义嵌入。请注意,数字本身没有特别的含义,但是两个含义相近的单词(或句子)的数字序列应该比不相关的单词的数字序列更接近。正如您所看到的,每个数字都是一个浮点值,每个数字都代表一个语义维度。让我们看看我们说的更近是什么意思:
如果我们在三维空间中绘制这些向量,它看起来像图2-3。
图2-3 多维空间中的词向量图
如图2-3所示,宠物和狗的媒介距离比狮子的媒介距离更近。我们还可以观察到,每个情节之间的角度取决于它们的相似程度。例如,单词pet和lion之间的角度比pet和dog之间的角度更大,这表明后两个单词对有更多的相似之处。两个向量之间的角度越窄或距离越短,它们的相似性就越近。
计算多维空间中两个向量之间相似度的一种有效方法称为余弦相似度。余弦相似度计算向量的点积,并将其除以它们的大小的乘积,输出一个介于-1和1之间的数字,其中0表示向量没有相关性,-1表示它们绝对不相似,1表示它们绝对相似。所以,以我们这里的三个单词为例,宠物和狗之间的余弦相似度可能是0.75,但宠物和狮子之间的余弦相似度可能是0.1。
将句子转换为捕获语义的嵌入,然后执行计算以找到不同句子之间的语义相似性的能力使我们能够获得大型语言模型学位,以找到最相关的文档来回答有关大型文本(如Tesla PDF文档)的问题。既然您已经了解了总体情况,那么让我们回顾一下预处理文档的第一步(索引)。
嵌入的其他用途
这些数字和向量序列有一些有趣的性质:
-
正如你之前学到的,如果你把一个向量看作是描述高维空间中的一个点,那么距离越近的点就越有相似的含义,所以距离函数可以用来度量相似性。
-
紧密相连的点群可以说是相关的;因此,可以使用聚类算法来识别主题(或点簇),并将新输入分类到其中一个主题中。
-
如果取多个嵌入的平均值,则平均嵌入可以说代表了该组的总体含义;也就是说,你可以通过以下方式嵌入一个很长的文档(例如,这本书):
-
分别嵌入每个页面
-
取所有页面嵌入量的平均值作为图书嵌入量
-
-
您可以通过使用加法和减法的基本数学运算来“穿越”“意义”空间:例如,操作king - man + woman = queen。如果你取king(国王)的意思(或语义嵌入),减去man(男人)的意思,大概你就得到了更抽象的monarch(君主)的意思,在这一点上,如果你加上woman(女人)的意思,你就得到了queen(女王)的意思(或嵌入)。
-
除了文本之外,还有一些模型可以为非文本内容生成嵌入,例如图像、视频和声音。例如,它可以找到与给定句子最相似或最相关的图像。
我们不会在本书中探讨所有这些属性,但了解它们可以用于许多应用程序是有用的,例如:
-
搜索
为新查询查找最相关的文档
-
聚类
给定一组文档,将它们分成组(例如,主题)
-
分类
将新文档分配给先前标识的组或标签(例如,主题)
-
推荐
给定一个文档,显示类似的文档
-
异常检测
识别与以前见过的非常不同的文件
我们希望这给您留下一些直观的印象,即嵌入是非常通用的,并且可以在您未来的项目中得到很好的使用。
将文档转换为文本
正如本章开头所提到的,预处理文档的第一步是将其转换为文本。为了实现这一点,您需要构建逻辑,以最小的质量损失来解析和提取文档。幸运的是,LangChain提供了处理解析逻辑的文档加载器,使您能够将来自不同来源的数据“加载”到由文本和相关元数据组成的document类中。例如,考虑一个简单的.txt文件。你可以简单地导入一个LangChain TextLoader类来提取文本,如下所示:
# 使用langchain提供的文本提取器
# 相较于直接导入文本增加了许多文档的细节
from langchain_community.document_loaders import TextLoader
# 换成自己文件地址
loader = TextLoader('./prompt/summarize.txt', encoding="utf-8")
docs = loader.load()
print(docs)
前面的代码块假设在当前目录中有一个名为test.txt的文件。所有LangChain文档加载器的使用都遵循类似的模式:
-
首先,从一长串集成列表中为您的文档类型选择加载器。
-
创建所讨论的加载器的实例,以及配置它的任何参数,包括文档的位置(通常是文件系统路径或web地址)。
-
通过调用Load()来加载文档,该方法返回准备传递到下一阶段的文档列表(稍后将详细介绍)。
除了.txt文件,LangChain还为其他流行的文件类型提供文档加载器,包括.csv、.json和Markdown,以及与Slack和Notion等流行平台的集成。
例如,您可以使用WebBaseLoader从web url加载HTML并将其解析为文本。
安装beautifulsoup4包:
# pip install beautifulsoup4
# 利用爬虫爬取网页地址 然后使用langchain带的解析器 对其进行解析
from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader('https://www.langchain.com/')
docs = loader.load()
print(docs)
在我们的Tesla PDF用例中,我们可以使用LangChain的PDFLoader从PDF文档中提取文本:
# 从pdf中提取文件
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader('./test/test.pdf')
pages = loader.load()
print(pages)
文本已经从PDF文档中提取出来并存储在document类中。但是有一个问题。加载的文档长度超过100,000个字符,因此它不适合绝大多数大型语言模型或嵌入模型的上下文窗口。为了克服这个限制,我们需要将Document拆分为可管理的文本块,这些文本块稍后可以转换为嵌入和语义搜索,从而进入第二步(检索)。
提示
大型语言模型和嵌入模型在设计时对它们可以处理的输入和输出令牌的大小有严格的限制。这个限制通常被称为上下文窗口,通常适用于输入和输出的组合;也就是说,如果上下文窗口是100(我们将在一秒钟内讨论单位),并且您的输入度量为90,则输出长度最多为10。上下文窗口通常以令牌的数量来衡量,例如8,192个令牌。正如前言中提到的,符号是文本的数字表示,每个符号通常覆盖英语文本的三到四个字符。
把你的文本分成小块
乍一看,将大量文本分成几个块似乎很简单,但是将语义相关(通过含义相关)的文本块保持在一起是一个复杂的过程。为了更容易地将大文档分割成小的,但仍然有意义的文本块,LangChain提供了RecursiveCharacterTextSplitter,它做以下工作:
-
按重要性排列一个分隔符列表。默认情况下,它们是:
a.段落分离器:\n\n
b.行分离器:\n
c.字分离器:空格字符
-
为了遵循给定的块大小,例如1000个字符,首先拆分段落。
-
对于任何超过所需块大小的段落,用下一个分离器分隔:lines。继续执行,直到所有块都小于所需长度,或者没有其他分隔符可以尝试。
-
将每个块作为Document发出,并传入原始文档的元数据和关于原始文档中位置的附加信息。
让我们来看一个例子:
# 对文件进行分块
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader
loader = TextLoader('./prompt/summarize.txt', encoding="utf-8")
docs = loader.load()
# chunk_size表示允许的最大的分块长度
# chunk_overlep分块之中的重叠部分
splitter = RecursiveCharacterTextSplitter(chunk_size=50, chunk_overlap=20)
splitted_docs = splitter.split_documents(docs)
print(splitted_docs)
在前面的代码中,文档加载器创建的文档被分成每个1,000个字符的块,每个200个字符的块之间有一些重叠,以保持一些上下文。结果也是一个文档列表,其中每个文档的长度最多为1000个字符,按照书面文本的自然划分进行分割——段落、新行,最后是单词。它使用文本的结构来保持每个块都是一致的、可读的文本片段。
RecursiveCharacterTextSplitter还可以用于将代码语言和Markdown拆分为语义块。这是通过使用特定于每种语言的关键字作为分隔符来实现的,这可以确保,例如,每个函数体都保持在同一个块中,而不是在几个块之间分割。通常,由于编程语言比书面文本具有更多的结构,因此不太需要使用块之间的重叠。LangChain包含了许多流行语言的分隔符,比如Python、JS、Markdown、HTML等等。这里有一个例子:
# 对源代码进行拆分
from langchain_text_splitters import (
Language,
RecursiveCharacterTextSplitter,
)
PYTHON_CODE = """ def hello_world(): print("Hello, World!") # Call the function hello_world() """
python_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.PYTHON, chunk_size=50, chunk_overlap=0
)
python_docs = python_splitter.create_documents([PYTHON_CODE])
print(python_docs)
注意,我们仍然像以前一样使用RecursiveCharacterTextSplitter,但现在我们使用from_language方法为特定语言创建了它的实例。它接受语言的名称,以及块大小的常用参数,等等。还请注意,我们现在正在使用create_documents方法,该方法接受字符串列表,而不是我们之前使用的文档列表。当要分割的文本不是来自文档加载器,因此只有原始文本字符串时,此方法非常有用。
您还可以使用create_documents的第二个可选参数,以便传递与每个文本字符串相关联的元数据列表。这个元数据列表应该具有与字符串列表相同的长度,并将用于填充返回的每个Document的元数据字段。
让我们看一个Markdown文本的例子,也使用元数据参数:
# 对markdown进行拆分
from langchain_text_splitters import (
Language,
RecursiveCharacterTextSplitter,
)
markdown_text = """ # 🦜🔗 LangChain ⚡ Building applications with LLMs through composability ⚡ ## Quick Install ```bash pip install langchain ``` As an open source project in a rapidly developing field, we are extremely open to contributions. """
# 对markdown和源代码进行拆分后返回的是一个特殊对象,所以要用到create_documents再进行处理
# 特殊对象<langchain_text_splitters.character.RecursiveCharacterTextSplitter object at 0x000001B3A1E3E120>
md_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.MARKDOWN, chunk_size=60, chunk_overlap=0
)
# create_documents方法
# 第一个参数是要进行切分的数据
# 第二个参数是要传入的元数据 类似于来源、文件名之列的数据
md_docs = md_splitter.create_documents(
[markdown_text], [{"source": "https://www.langchain.com"}])
print(md_docs)
注意两件事:
-
文本沿着Markdown文档中的自然停止点拆分;例如,标题放在一个块中,标题下的文本行放在另一个块中,依此类推。
-
我们在第二个参数中传递的元数据附加到每个结果文档,这允许您跟踪,例如,文档来自何处以及您可以到哪里查看原始文档。
生成文本嵌入
LangChain还有一个Embeddings类,用于与文本嵌入模型(包括OpenAI、Cohere和hug face)交互,并生成文本的矢量表示。该类提供了两个方法:一个用于嵌入文档,另一个用于嵌入查询。前者接受文本字符串列表作为输入,而后者接受单个文本字符串。
# 翻译过程中我使用了开源的嵌入工具 如果你需要使用API 请自行替换嵌入模型
from langchain_openai import OpenAIEmbeddings
model = OpenAIEmbeddings()
embeddings = model.embed_documents([
"Hi there!",
"Oh, hello!",
"What's your name?",
"My friends call me World",
"Hello World!"
])
注意,您可以同时嵌入多个文档;比起一次嵌入一个模型,您应该更喜欢这样做,因为这样会更有效(由于这些模型的构造方式)。您将得到一个包含多个数字列表的列表—每个内部列表都是一个向量或嵌入,如前一节所述。
# 向量嵌入工具的使用
from langchain_huggingface import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2" # 高效的语义模型
)
# 输入文本
texts = ["你好,世界。", "LangChain 是一个强大的工具。"]
# 生成嵌入向量
vectors = embeddings.embed_documents(texts)
# 打印结果
for idx, vector in enumerate(vectors):
print(f"文本 {idx + 1}: {texts[idx]}")
print(f"嵌入向量: {vector[:5]}... (维度: {len(vector)})\n")
现在让我们看一个端到端示例,使用到目前为止我们看到的三个功能:
-
文档加载器,将任何文档转换为纯文本
-
文本分割器,将每个大文档分割成许多小文档
-
嵌入模型,以创建每个分割含义的数字表示
代码如下:
# 批量进行嵌入操作
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_ollama import OllamaLLM
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
deepseek = OllamaLLM(
model="deepseek-r1:32b",
)
loader = TextLoader("./test/deeplearning.txt", encoding="utf-8")
doc = loader.load()
# Split the document
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
chunks = splitter.split_documents(doc)
# Generate embeddings
embeddings_model = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2" # 高效的语义模型
)
embeddings = embeddings_model.embed_documents(
[chunk.page_content for chunk in chunks]
)
print(embeddings)
从文档中生成嵌入后,下一步是将它们存储在称为矢量存储的特殊数据库中。
在矢量存储中存储嵌入
在本章的前面,我们讨论了余弦相似度计算来测量向量空间中向量之间的相似度。向量存储是一个数据库,用于存储向量和执行复杂的计算,如余弦相似度,高效和快速。
与专门存储结构化数据(如JSON文档或符合关系数据库模式的数据)的传统数据库不同,矢量存储处理非结构化数据,包括文本和图像。与传统数据库一样,矢量存储能够执行创建、读取、更新、删除(CRUD)和搜索操作。
向量存储解锁了各种各样的用例,包括利用AI来回答有关大型文档的问题的可扩展应用程序,如图2-4所示。
图2-4 加载,嵌入,存储,并从矢量存储检索相关文档
图2-4说明了文档嵌入是如何插入到向量存储中,以及之后,当发送查询时,如何从向量存储中检索类似的嵌入。
目前,有大量的矢量存储提供商可供选择,每个提供商都专注于不同的功能。您的选择应取决于应用程序的关键需求,包括多租户、元数据过滤功能、性能、成本和可伸缩性。
虽然矢量存储是为管理矢量数据而构建的利基数据库,但使用它们存在一些缺点:
-
大多数矢量存储相对较新,可能经不起时间的考验。
-
管理和优化向量存储可能呈现相对陡峭的学习曲线。
-
管理单独的数据库会增加应用程序的复杂性,并可能消耗宝贵的资源。
幸运的是,向量存储功能最近已经通过pgvector扩展扩展到PostgreSQL(一个流行的开源关系数据库)。这使您能够使用您已经熟悉的相同数据库,并为事务表(例如用户表)和矢量搜索表提供支持。
设置PGVector
要使用Postgres和PGVector,你需要遵循几个设置步骤:
-
确保您的计算机上安装了Docker,按照操作系统的说明进行操作。
-
在终端运行以下命令;它将启动一个Postgres实例在您的计算机上运行端口6024:
docker run \
--name pgvector-container \
-e POSTGRES_USER=langchain \
-e POSTGRES_PASSWORD=langchain \
-e POSTGRES_DB=langchain \
-p 6024:5432 \
-d pgvector/pgvector:pg16
打开docker仪表板容器,您应该看到pgvector-container旁边有一个绿色的运行状态。
-
保存要在代码中使用的连接字符串;我们稍后会用到它。
postgresql+psycopg://langchain:langchain@localhost:6024/langchain
使用Vector Stores
继续上一节关于嵌入的内容,现在让我们来看一个在PGVector中加载、分割、嵌入和存储文档的例子:
请注意,我们是如何重用前几节中的代码,首先使用加载器加载文档,然后将它们分成更小的块。然后,我们实例化我们想要使用的嵌入模型——在本例中是OpenAI的。请注意,您可以在这里使用LangChain支持的任何其他嵌入模型。
接下来,我们有了新的一行代码,它创建给定文档、嵌入模型和连接字符串的向量存储。这将做几件事:
-
建立一个连接到运行在你电脑上的Postgres实例(参见“使用PGVector进行设置”)。
-
如果这是您第一次运行它,则运行任何必要的设置,例如创建保存文档和向量的表。
-
使用您选择的模型为传入的每个文档创建嵌入。
-
将嵌入、文档的元数据和文档的文本内容存储在Postgres中,以便进行搜索。
db.similarity_search("query", k=4)
这个方法将找到最相关的文档(你之前索引的),通过以下过程:
-
搜索查询(在本例中是单词查询)将被发送到嵌入模型以检索其嵌入。
-
然后,它将在Postgres上运行一个查询,以找到N个(在本例中为4个)先前存储的与您的查询最相似的嵌入。
-
最后,它将获取与每个嵌入相关的文本内容和元数据。
-
模型现在可以返回一个文档列表,按照它们与查询的相似程度排序——最相似的在前,第二相似的在后,依此类推。
我们在这里使用的add_documents方法将遵循与fromDocuments类似的过程:
-
使用您选择的模型为传入的每个文档创建嵌入。
-
将嵌入、文档的元数据和文档的文本内容存储在Postgres中,以便进行搜索。
在本例中,我们使用可选的ids参数为每个文档分配标识符,这允许我们稍后更新或删除它们。
db.delete(ids=[1])
这将删除使用其通用唯一标识符(UUID)插入的第二个文档。现在让我们看看如何用更系统的方法来做。
"""
1. Ensure docker is installed and running (https://docs.docker.com/get-docker/)
2. pip install -qU langchain_postgres
3. Run the following command to start the postgres container:
docker run \
--name pgvector-container \
-e POSTGRES_USER=langchain \
-e POSTGRES_PASSWORD=langchain \
-e POSTGRES_DB=langchain \
-p 6024:5432 \
-d pgvector/pgvector:pg16
4. Use the connection string below for the postgres container
"""
from langchain_community.document_loaders import TextLoader
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_postgres.vectorstores import PGVector
from langchain_core.documents import Document
import uuid
# 请参阅上面的docker命令以启动启用了pgvector的PostgreSQL实例。
# 用于连接启用了pgvector的PostgreSQL数据库的连接字符串
connection = "postgresql+psycopg://langchain:langchain@localhost:6024/langchain"
# 加载文档并将其拆分为多个小块
# 从指定路径以UTF-8编码加载原始文档
raw_documents = TextLoader('./test/deeplearning.txt', encoding="utf-8").load()
# 初始化文本拆分器以将文档拆分成较小的块
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # 每个块的最大大小
chunk_overlap=200 # 连续块之间的重叠部分
)
# 将原始文档拆分成较小的块
documents = text_splitter.split_documents(raw_documents)
# 为文档创建嵌入
# 使用预训练的HuggingFace模型初始化用于生成嵌入的模型
embeddings_model = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2" # 高效的语义模型
)
# 从文档列表生成一个向量存储。此存储将文档的嵌入向量保存在 PostgreSQL 数据库中,以便后续进行相似性搜索等操作。
db = PGVector.from_documents(
documents, # 要存储的文档
embeddings_model, # 用于生成嵌入的模型
connection=connection # PostgreSQL数据库的连接字符串
)
# 在向量存储(db)中找到最相似的文档
results = db.similarity_search("query", k=4) # 搜索与查询最相似的前4个文档
# 向 向量存储中生成新文档
# 为新文档生成唯一ID
ids = [str(uuid.uuid4()), str(uuid.uuid4())]
# 使用生成的ID向向量存储中添加新文档
db.add_documents(
[
Document(
page_content="池塘里有猫", # 文档的内容
metadata={"location": "池塘", "topic": "动物"}, # 与文档相关的元数据
),
Document(
page_content="池塘里也有鸭子", # 文档的内容
metadata={"location": "池塘", "topic": "动物"}, # 与文档相关的元数据
),
],
ids=ids, # 新文档的ID
)
# 打印文档添加的确认信息
print("文档添加成功。\n获取的文档数量:",
len(db.get_by_ids(ids))) # 获取并打印添加的文档数量
# 从向量存储中删除文档
print("删除ID为", ids[1], "的文档")
db.delete({"ids": ids}) # 删除具有指定ID的文档
# 打印文档删除的确认信息
print("文档删除成功。\n获取的文档数量:",
len(db.get_by_ids(ids))) # 获取并打印剩余的文档数量
PGVector 类提供了以下五个主要方法:
from_documents:从文档和嵌入模型创建一个向量存储实例。
similarity_search:在向量存储中执行相似性搜索,返回与查询最相似的前 k 个文档。
add_documents:向向量存储中添加新文档。
delete:从向量存储中删除文档。
get_by_ids:根据文档 ID 获取文档。
跟踪文档的更改
使用矢量存储的主要挑战之一是处理定期更改的数据,因为更改意味着重新索引。而且重新索引可能会导致嵌入和重复先前存在的内容的昂贵的重新计算。
幸运的是,LangChain提供了一个索引API,使您可以轻松地将文档与矢量存储保持同步。API使用一个类(RecordManager)来跟踪向矢量存储写入的文档。当索引内容时,计算每个文档的哈希值,并将以下信息存储在RecordManager中:
-
文档散列(页面内容和元数据的散列)
-
写入时间
-
源ID(每个文档应该在其元数据中包含信息,以确定该文档的最终来源)。
此外,索引API还提供清理模式,帮助您决定如何删除矢量存储中的现有文档。例如,如果在插入之前更改了文档的处理方式,或者源文档发生了更改,则可能需要删除与正在索引的新文档来自同一源的所有现有文档。如果删除了一些源文档,则需要删除矢量存储中的所有现有文档,并用重新索引的文档替换它们。
模式有:
-
None模式不做任何自动清理,允许用户手动清理旧内容。
-
如果源文档或派生文档的内容发生了更改,增量模式和完整模式将删除以前版本的内容。
-
完整模式还将删除当前正在索引的文档中不包含的任何文档。
下面是一个使用索引API与Postgres数据库设置为记录管理器的例子:
# 向数据库中添加更多文档
from langchain.indexes import SQLRecordManager, index
from langchain_postgres.vectorstores import PGVector
from langchain_huggingface import HuggingFaceEmbeddings
from langchain.docstore.document import Document
# PostgreSQL数据库的连接字符串
connection = "postgresql+psycopg://langchain:langchain@localhost:6024/langchain"
collection_name = "my_docs"
embeddings_model = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2" # 高效的语义模型
)
namespace = "my_docs_namespace"
# 初始化向量存储
vectorstore = PGVector(
embeddings=embeddings_model,
collection_name=collection_name,
connection=connection,
use_jsonb=True,
)
# 初始化SQL记录管理器
record_manager = SQLRecordManager(
namespace,
db_url="postgresql+psycopg://langchain:langchain@localhost:6024/langchain",
)
# 如果模式不存在则创建模式
record_manager.create_schema()
# 创建文档
docs = [
Document(page_content='池塘里有猫', metadata={
"id": 1, "source": "cats.txt"}),
Document(page_content='池塘里也有鸭子', metadata={
"id": 2, "source": "ducks.txt"}),
]
# 索引文档
index_1 = index(
docs,
record_manager,
vectorstore,
# 使用增量模式
cleanup="incremental", # 防止重复文档
source_id_key="source", # 使用source字段作为source_id
)
print("索引尝试1:", index_1)
# 再次尝试索引时,不会再次添加文档
index_2 = index(
docs,
record_manager,
vectorstore,
cleanup="incremental",
source_id_key="source",
)
print("索引尝试2:", index_2)
# 如果我们修改文档,新版本将被写入,所有共享相同source的旧版本将被删除。
docs[0].page_content = "我刚刚修改了这个文档!"
index_3 = index(
docs,
record_manager,
vectorstore,
cleanup="incremental",
source_id_key="source",
)
print("索引尝试3:", index_3)
首先,创建一个记录管理器,它跟踪哪些文档以前被索引过。然后使用索引函数将矢量存储与新的文档列表同步。在本例中,我们使用增量模式,因此与以前的文档具有相同ID的任何文档都将被新版本替换。