在信息检索领域,文档的拆分与组合一直是个难题。我们希望文档足够小,以便其嵌入能精确地反映其含义;然而,如果过长,嵌入可能会失去实际意义。同时,也希望文档足够长,以保留每个片段的上下文。当文档过长时,检索的准确度可能降低。
为了在这之间取得平衡,Parent Document Retriever 分析并存储了小块数据,并在检索时首先获取小块,再查找这些块的父IDs,从而返回较大的文档。
核心原理解析
ParentDocumentRetriever
的工作流程是先将文档拆分成小块,并存储这些小块。在检索时,它会先找出与查询最相似的小块,然后通过这些小块获取相应的父文档,最终返回更大的原始文档。
代码实现演示
下面,我们将通过Python代码演示如何使用 Parent Document Retriever 来实现不同粒度的文档检索。
from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore
from langchain_chroma import Chroma
from langchain_community.document_loaders import TextLoader
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 加载文档
loaders = [
TextLoader("paul_graham_essay.txt"),
TextLoader("state_of_the_union.txt"),
]
docs = []
for loader in loaders:
docs.extend(loader.load())
# 使用 RecursiveCharacterTextSplitter 创建子文档
child_splitter = RecursiveCharacterTextSplitter(chunk_size=400)
# 使用 Chroma 创建向量存储,以存储子文档嵌入
vectorstore = Chroma(
collection_name="full_documents", embedding_function=OpenAIEmbeddings()
)
# 使用 InMemoryStore 存储父文档
store = InMemoryStore()
# 初始化 ParentDocumentRetriever
retriever = ParentDocumentRetriever(
vectorstore=vectorstore,
docstore=store,
child_splitter=child_splitter,
)
# 添加文档
retriever.add_documents(docs, ids=None)
# 检索示例
sub_docs = vectorstore.similarity_search("justice breyer")
print(sub_docs[0].page_content) # 输出小块内容
retrieved_docs = retriever.invoke("justice breyer")
print(len(retrieved_docs[0].page_content)) # 输出大块长度
应用场景分析
这种方法适用于需要精细颗粒化查询的场景,例如文档分析、法律文献回顾、学术研究等。在这些场景中,检索系统需要在文档的整体理解和具体细节之间找到一个平衡点。
实践建议
- 选择适当的拆分策略:根据具体需求选择合适的
chunk_size
,以平衡检索的精确度和效率。 - 优化嵌入模型:使用高效的嵌入模型以获得更准确的相似度匹配。
- 探索存储和检索参数:不断调整存储和检索参数以应对不同大小和类型的文档。
如果遇到问题欢迎在评论区交流。
—END—