假期快乐,朋友们要多聚会:
5.7 索引(Indexing)
在LangChain中,索引(Indexing)是一种将文档同步到向量存储(Vector Store)的机制,能够将从任何来源的数据同步到向量存储中并保持同步。
5.7.1 索引(Indexing)介绍
在LangChain中,langchain.indexes模块提供了一系列的工具和类,用于管理和优化数据的索引过程,特别是在向量存储(vectorstore)中。langchain.indexes模块的主要目的是避免在向量存储中写入重复内容,以及在内容未发生变化时避免重写内容。此外,langchain.indexes还支持从LangChain数据加载器到向量存储的索引工作流程,并能够在内容通过一系列转换从源内容派生时继续工作,例如,通过分块处理父文档来索引子文档。
1. 记录管理器RecordManager
RecordManager是一个抽象基类,定义了记录管理器的接口。RecordManager用于跟踪和协调文档的索引状态,确保内容的唯一性和最新性。RecordManager通常使用哈希值和时间戳来识别和处理重复或过时的内容。
(1)成员方法
- __init__(namespace):用于初始化记录管理器,接收一个 namespace 参数,该参数定义了记录管理器的命名空间。
- acreate_schema():异步方法,用于创建记录管理器的数据库架构。
- adelete_keys(keys):异步方法,用于从数据库中删除指定的记录。
- aexists(keys):异步方法,用于检查提供的键是否存在于数据库中。
- aget_time():异步方法,用于获取当前服务器时间作为高精度时间戳。
- alist_keys(*, before=None, after=None, group_ids=None, limit=None):异步方法,用于基于提供的过滤器列出数据库中的记录。
- aupdate(keys, *, group_ids=None, time_at_least=None):异步方法,用于将记录更新(或插入,如果不存在)到数据库。
(2)参数说明
- namespace (str):命名空间,用于区分不同的记录管理器实例。
- keys (Sequence[str]):键的序列,用于指定要操作的记录。
- group_ids (Optional[Sequence[str]]):组ID的序列,用于指定记录所属的组。
- limit (Optional[int]):可选的记录返回数量限制。
- time_at_least (Optional[float]):如果提供,只有在 updated_at 字段至少是这个时间时,才执行更新操作。
(3)注意事项
- 方法名称以 a 开头的表示它们是异步方法,需要在异步环境中调用。
- 方法get_time()强调使用服务器时间来确保单增时钟,这对于清理旧文档时避免数据丢失很重要。
- 方法update()和方法aupdate()在执行更新操作时,如果提供了参数time_at_least,只有在记录的 updated_at 字段值至少等于这个时间戳时,才会更新记录。
类RecordManager是索引模块langchain.indexes模块的核心,它通过维护一个记录数据库来确保向量存储中的文档是最新的,并且没有重复。这在处理大规模数据集和保持索引一致性方面非常有用。实际的记录管理器实现需要继承这个基类并实现所有的抽象方法。
2. GraphIndexCreator
类GraphIndexCreator提供了创建图索引的功能。图索引是一种特殊的索引结构,它将数据表示为节点和边的集合,这对于表示和查询复杂的关系和网络结构非常有用。
(1)属性和参数
- graph_type:图类型,默认为 NetworkxEntityGraph 类型,这是一个用于表示实体图的类。
- llm:一个可选的 BaseLanguageModel 实例,用于提供语言模型功能。
(2)方法
- afrom_text:异步方法,用于从文本中创建图索引。
- construct:类方法,用于从信任或预先验证的数据创建新模型。
- copy:实例方法,用于复制模型,可以选择包含、排除字段或更改值。
- dict:实例方法,用于生成模型的字典表示。
- from_orm:类方法,用于从 ORM(对象关系映射)对象创建模型。
- from_text:同步方法,功能与 afrom_text 相同,但不是异步的。
- json:实例方法,用于生成模型的 JSON 表示。
- parse_file:类方法,用于从文件中解析模型。
- parse_obj:类方法,用于从对象中解析模型。
- parse_raw:类方法,用于从原始数据中解析模型。
- schema:类方法,用于获取模型的 JSON Schema。
- schema_json:类方法,用于获取模型的 JSON Schema 的 JSON 字符串表示。
- update_forward_refs:类方法,用于更新模型字段的前向引用。
- validate:类方法,用于验证传入的值并创建模型。
3. VectorStoreIndexWrapper
VectorStoreIndexWrapper是一个包装类,提供了便捷访问向量存储的功能。主要包括如下所示的成员方法。
(1)aquery:异步方法,用于向向量存储查询问题。
- 参数 question 是要查询的问题字符串。
- 参数 llm 是一个可选的 BaseLanguageModel 实例,用于提供语言模型功能。
- 参数 retriever_kwargs 是一个可选的字典,包含检索器的关键字参数。
- 参数 kwargs 是其他任意关键字参数。
- 返回值:是查询结果的字符串。
(2)aquery_with_sources:异步方法,与 aquery 类似,但返回包含来源信息的字典。
(3)construct:类方法,用于从信任或预先验证的数据创建新模型。
(4)copy:实例方法,用于复制模型,可以选择包含、排除字段或更改值。
(5)dict:实例方法,用于生成模型的字典表示。
(6)from_orm:类方法,用于从 ORM(对象关系映射)对象创建模型。
(7)json:实例方法,用于生成模型的 JSON 表示。
(8)parse_file:类方法,用于从文件中解析模型。
(9)parse_obj:类方法,用于从对象中解析模型。
(10)parse_raw:类方法,用于从原始数据中解析模型。
(11)query:同步方法,功能与 aquery 相同,但不是异步的。
(12)query_with_sources:同步方法,功能与 aquery_with_sources 相同,但不是异步的。
(13)schema:类方法,用于获取模型的 JSON Schema。
(14)schema_json:类方法,用于获取模型的 JSON Schema 的 JSON 字符串表示。
(15)update_forward_refs:类方法,用于更新模型字段的前向引用。
(16)validate:类方法,用于验证传入的值并创建模型。
4. VectorStoreIndexCreator
类VectorStoreIndexCreator用于创建向量存储索引,它基于 BaseModel,是一个模型类,提供了从文档或加载器创建向量存储索引的逻辑。
(1)属性和参数
- embedding:一个可选的 Embeddings 实例,用于将文本转换为向量表示。
- text_splitter:一个可选的 TextSplitter 实例,用于将文本分割成更小的块或段落。
- vectorstore_cls:向量存储的类,默认为 InMemoryVectorStore,一个在内存中存储向量的类。
- vectorstore_kwargs:一个可选的字典,包含创建向量存储实例时的关键字参数。
(2)方法
- afrom_documents:异步方法,用于从文档列表创建向量存储索引。参数 documents 是一个 Document 对象列表,返回值是 VectorStoreIndexWrapper 的一个实例,它包装了创建的向量存储。
- afrom_loaders:异步方法,用于从加载器列表创建向量存储索引。参数 loaders 是 BaseLoader 对象的列表,返回值同样是 VectorStoreIndexWrapper 的一个实例。
- construct:类方法,用于从信任或预先验证的数据创建新模型。
- copy:实例方法,用于复制模型,可以选择包含、排除字段或更改值。
- dict:实例方法,用于生成模型的字典表示。
- from_documents:同步方法,功能与 afrom_documents 相同,但不是异步的。
- from_loaders:同步方法,功能与 afrom_loaders 相同,但不是异步的。
- from_orm:类方法,用于从 ORM(对象关系映射)对象创建模型。
- json:实例方法,用于生成模型的 JSON 表示。
- parse_file:类方法,用于从文件中解析模型。
- parse_obj:类方法,用于从对象中解析模型。
- parse_raw:类方法,用于从原始数据中解析模型。
- schema:类方法,用于获取模型的 JSON Schema。
- schema_json:类方法,用于获取模型的 JSON Schema 的 JSON 字符串表示。
- update_forward_refs:类方法,用于更新模型字段的前向引用。
- validate:类方法,用于验证传入的值并创建模型。
请看下面的例子,演示了使用langchain.indexes实现索引文档的过程,包括如何处理文档的去重、更新和删除。另外,本实例还展示了如何使用自定义加载器来索引文档,以及如何执行向量存储的相似性搜索的用法。在实际使用中,开发者需要根据自己的环境配置相应的参数,例如 Elasticsearch 的 URL 和索引名称。
实例5-15:使用langchain.indexes实现索引文档(源码路径:codes\5\jian15.py)
实例文件jian15.py的具体实现代码如下所示。
# 初始化向量存储和嵌入模型
collection_name = "test_index"
embedding = OpenAIEmbeddings()
vectorstore = ElasticsearchStore(
es_url="http://localhost:9200",
index_name="test_index",
embedding=embedding
)
# 初始化记录管理器
namespace = f"elasticsearch/{collection_name}"
record_manager = SQLRecordManager(namespace, db_url="sqlite:///record_manager_cache.sql")
# 创建记录管理器的模式
record_manager.create_schema()
# 定义清除内容的辅助方法
def _clear():
index([], record_manager, vectorstore, cleanup="full", source_id_key="source")
# 创建测试文档
doc1 = Document(page_content="kitty", metadata={"source": "kitty.txt"})
doc2 = Document(page_content="doggy", metadata={"source": "doggy.txt"})
# 索引空向量存储中的内容(无删除模式)
_clear()
index(
[doc1, doc1, doc1, doc1, doc1],
record_manager,
vectorstore,
cleanup=None,
source_id_key="source",
)
# 索引向量存储中的内容(无删除模式)
_clear()
index([doc1, doc2], record_manager, vectorstore, cleanup=None, source_id_key="source")
# 再次索引相同的内容将被跳过
index([doc1, doc2], record_manager, vectorstore, cleanup=None, source_id_key="source")
# "增量"删除模式
_clear()
index(
[doc1, doc2],
record_manager,
vectorstore,
cleanup="incremental",
source_id_key="source",
)
# 再次索引相同的内容将被跳过,包括嵌入操作
index(
[doc1, doc2],
record_manager,
vectorstore,
cleanup="incremental",
source_id_key="source",
)
# 如果没有提供文档,增量索引模式下什么也不会改变
index([], record_manager, vectorstore, cleanup="incremental", source_id_key="source")
# 如果我们更改文档,新版本将被写入,并且所有旧版本将被删除
changed_doc_2 = Document(page_content="puppy", metadata={"source": "doggy.txt"})
index(
[changed_doc_2],
record_manager,
vectorstore,
cleanup="incremental",
source_id_key="source",
)
# "完整"删除模式
_clear()
all_docs = [doc1, doc2]
index(all_docs, record_manager, vectorstore, cleanup="full", source_id_key="source")
# 假设有人删除了第一个文档
del all_docs[0]
all_docs
# 使用完整模式将清理已删除的内容
index(all_docs, record_manager, vectorstore, cleanup="full", source_id_key="source")
# 使用 CharacterTextSplitter 分割文档
doc1 = Document(
page_content="kitty kitty kitty kitty kitty", metadata={"source": "kitty.txt"}
)
doc2 = Document(page_content="doggy doggy the doggy", metadata={"source": "doggy.txt"})
new_docs = CharacterTextSplitter(
separator="t", keep_separator=True, chunk_size=12, chunk_overlap=2
).split_documents([doc1, doc2])
# 索引分割后的文档
_clear()
index(
new_docs,
record_manager,
vectorstore,
cleanup="incremental",
source_id_key="source",
)
# 更改文档内容并重新索引
changed_doggy_docs = [
Document(page_content="woof woof", metadata={"source": "doggy.txt"}),
Document(page_content="woof woof woof", metadata={"source": "doggy.txt"}),
]
index(
changed_doggy_docs,
record_manager,
vectorstore,
cleanup="incremental",
source_id_key="source",
)
# 执行相似性搜索
results = vectorstore.similarity_search("dog", k=30)
for doc in results:
print(doc)
# 使用自定义加载器
class MyCustomLoader(BaseLoader):
def lazy_load(self):
text_splitter = CharacterTextSplitter(
separator="t", keep_separator=True, chunk_size=12, chunk_overlap=2
)
docs = [
Document(page_content="woof woof", metadata={"source": "doggy.txt"}),
Document(page_content="woof woof woof", metadata={"source": "doggy.txt"}),
]
yield from text_splitter.split_documents(docs)
def load(self):
return list(self.lazy_load())
# 清除内容并使用自定义加载器索引
_clear()
loader = MyCustomLoader()
docs = loader.load()
index(
docs,
record_manager,
vectorstore,
cleanup="full",
source_id_key="source",
)
# 执行相似性搜索
results = vectorstore.similarity_search("dog", k=30)
for doc in results:
print(doc)
上述代码用于将文档索引到向量存储中,并支持后续的内容检索。具体实现流程如下所示:
(1)初始化向量存储和嵌入模型:创建一个 ElasticsearchStore 实例,它是一个向量存储,用于存储文档的嵌入向量。然后,使用 OpenAIEmbeddings 作为文档内容的嵌入模型。
(2)初始化记录管理器:使用 SQLRecordManager 来跟踪文档的索引状态。定义一个命名空间,通常与向量存储和集合名称相关。
(3)创建记录管理器的模式:调用 create_schema 方法来初始化记录管理器所需的数据库结构。
(4)创建测试文档:创建两个 Document 实例,包含页面内容和元数据。
(5)索引文档到向量存储
- 定义一个辅助函数 _clear 来清除向量存储中的内容。
- 使用不同的删除模式(cleanup 参数)来索引文档:
- None:不自动清理旧版本,但会进行内容去重。
- incremental:增量模式,仅索引自上次索引以来发生变化的文档。
- full:完全模式,重新索引所有文档,并删除未包含在索引中的文档。
(6)处理文档变更:如果文档内容发生变化,则使用 incremental 模式来更新文档。
(7)使用分割器处理文档:使用 CharacterTextSplitter 对长文档进行分割,以便将它们作为单独的文档索引。
(8)索引分割后的文档:使用 incremental 模式索引分割后的文档。
(9)使用自定义加载器:创建一个自定义的 MyCustomLoader 类,它使用 CharacterTextSplitter 来分割文档,并生成可由索引器处理的文档流。
(10)执行相似性搜索:使用向量存储的 similarity_search 方法来执行基于内容的搜索。
上述实现流程的核心是 index 函数,它负责将文档添加到向量存储中,并根据指定的删除模式处理文档的更新和删除。通过这种方式,可以确保向量存储中的内容始终是最新的,并且没有重复。此外,通过使用 SQLRecordManager,可以跟踪每个文档的索引状态,包括它们的哈希值和最后更新时间,这有助于高效地管理和维护索引。执行后会输出:
# 索引空向量存储中的内容(无删除模式)
{'num_added': 1, 'num_updated': 0, 'num_skipped': 0, 'num_deleted': 0}
# 索引向量存储中的内容(无删除模式)
{'num_added': 2, 'num_updated': 0, 'num_skipped': 0, 'num_deleted': 0}
# 再次索引相同的内容将被跳过
{'num_added': 0, 'num_updated': 0, 'num_skipped': 2, 'num_deleted': 0}
# 增量删除模式下索引文档
{'num_added': 2, 'num_updated': 0, 'num_skipped': 0, 'num_deleted': 0}
# 再次索引相同的文档将被跳过
{'num_added': 0, 'num_updated': 0, 'num_skipped': 2, 'num_deleted': 0}
# 没有提供文档,增量索引模式下什么也不会改变
{'num_added': 0, 'num_updated': 0, 'num_skipped': 0, 'num_deleted': 0}
# 增量模式下,文档变更后新版本被写入,旧版本被删除
{'num_added': 1, 'num_updated': 0, 'num_skipped': 0, 'num_deleted': 1}
# 完整删除模式下,除了提供的文档外,其他所有文档都被删除
{'num_added': 2, 'num_updated': 0, 'num_skipped': 0, 'num_deleted': 0}
# 删除特定文档后,使用完整模式清理内容
{'num_added': 0, 'num_updated': 0, 'num_skipped': 1, 'num_deleted': 1}
另外,当执行 vectorstore.similarity_search("dog", k=30) 时,会返回与查询 "dog" 语义相似的文档列表,参数k用于指定返回的结果数量。执行后将显示一系列 Document 对象,其中包含了与搜索查询 "dog" 相关的页面内容和元数据。例如下面的输出结果:
[Document(page_content='kitty kit', metadata={'source': 'kitty.txt'}),
Document(page_content='tty kitty ki', metadata={'source': 'kitty.txt'}),
Document(page_content='tty kitty', metadata={'source': 'kitty.txt'}),
Document(page_content='doggy doggy', metadata={'source': 'doggy.txt'}),
Document(page_content='the doggy', metadata={'source': 'doggy.txt'})]
5.7.2 基于Apify爬虫的向量索引查询系统
Apify是一个用于网络爬取和数据提取的云平台,提供了一个包含一千多个现成应用(称为 Actors)的生态系统,这些应用用于各种网络爬取、爬行和数据提取用例。例如,可以使用它来提取 Google 搜索结果、Instagram 和 Facebook 个人资料、亚马逊或 Shopify 上的产品、Google Maps 评论等。在下面的例子中,将使用 Website Content Crawler Actor(可以深入爬取网站信息,如文档、知识库、帮助中心或博客)从网页中爬取文本内容,然后将文档输入到向量索引中,并从中回答问题。
实例5-16:基于Apify爬虫的向量索引查询系统(源码路径:codes\5\jian16.py)
实例文件jian16.py的具体实现代码如下所示。
import os
from langchain.indexes.vectorstore import VectorstoreIndexCreator
from langchain_community.utilities import ApifyWrapper
from langchain_core.documents import Document
# 设置环境变量,存储您的 OpenAI 和 Apify API 密钥
os.environ["OPENAI_API_KEY"] = "Your_OpenAI_API_Key"
os.environ["APIFY_API_TOKEN"] = "Your_Apify_API_Token"
# 初始化 ApifyWrapper
apify = ApifyWrapper()
# 调用 Apify 的 Website Content Crawler Actor 并获取结果
loader = apify.call_actor(
actor_id="apify/website-content-crawler",
run_input={"startUrls": [{"url": "https://python.langchain.com/en/latest/"}]},
# 将 Apify 数据集记录转换为 LangChain Document 对象
dataset_mapping_function=lambda item: Document(
page_content=item["text"] or "", metadata={"source": item["url"]}
),
)
# 使用爬取的文档初始化向量索引
index = VectorstoreIndexCreator().from_loaders([loader])
# 使用向量索引查询信息
query = "What is LangChain?"
result = index.query_with_sources(query)
# 打印查询结果和来源
print(result["answer"])
print(result["sources"])
上述代码的实现流程如下所示:
(1)爬取网站内容:使用 Apify 的 website-content-crawler Actor 从指定的 URL 爬取网站内容。
(2)创建文档加载器:通过 call_actor 方法获取爬取结果,并使用 dataset_mapping_function 将 Apify 数据集记录转换为 LangChain 能够理解的 Document 对象。
(3)初始化向量索引:使用 VectorstoreIndexCreator 类的 from_loaders 方法,从文档加载器中创建一个向量索引。这个索引是基于文档内容和元数据构建的,可以用于后续的查询操作。
(4)查询向量索引:通过 query_with_sources 方法向向量索引发出查询,获取问题的答案和来源信息。
在运行本实例之前,请确保已经将Your_OpenAI_API_Key和Your_Apify_API_Token替换为自己的API密钥。执行后会输出:
LangChain is a standard interface through which you can interact with a variety of large language models (LLMs). It provides modules that can be used to build language model applications, and it also provides chains and agents with memory capabilities.
https://python.langchain.com/en/latest/modules/models/llms.html, https://python.langchain.com/en/latest/getting_started/getting_started.html