基于PDF文档构建语义搜索引擎

1.主要内容

  • 文档和文档加载器
  • 文本分配
  • 向量模型(嵌入)
  • 向量存储和检索

2.文档和文档加载器

LangChain 实现了文件抽象,旨在表示文本单元及其相关的元数据。它有三个属性:

  • page_content:表示内容的字符串;
  • metadata:包含任意元数据的dict;
  • id:(可选)文档的字符串标识符。
    其中metadata可以捕捉关于文档来源、与其他文档的关系以及其他信息的信息
from langchain_core.documents import Document

documents = [
    Document(
        page_content="Dogs are great companions, known for their loyalty and friendliness.",
        metadata={"source": "mammal-pets-doc"},
    ),
    Document(
        page_content="Cats are independent pets that often enjoy their own space.",
        metadata={"source": "mammal-pets-doc"},
    ),
]
(1). 加载文档
我们可以将一个PDF作为Document对象,下面是一份自我认知的PDF,你也可以构建自己的文档输入到模型中:
from langchain_community.document_loaders import PyPDFLoader

file_path = r"D:\sources\自我认知.pdf"
loader = PyPDFLoader(file_path)

docs = loader.load()

print(len(docs))

输出PDF的页数:

3

PyPDFLoader是Document每个PDF页面的对象。对于每个页面,我们可以轻松访问:

  • 页面的字符串内容;
  • 包含文件名和页码的元数据。
print(f"{docs[0].page_content[:200]}\n")    #输出文档前200个字符
print(docs[0].metadata)    #输出文档基本信息
(2). 分块

无论是用于信息检索还是后续回答,如果直接采用页面中的表示可以会过于粗糙RAG原理。基于文档检索的目的是取回Document回答输入查询的对象,进一步的拆分PDF有助于确保文档相关部分的含义不会被周围文本洗白。分块就是为了避免这个问题,我们将把文档分割成1000个字符的块,块与块之间有200个字符的重叠部分,这种重叠有助于降低将某个陈述与其相关的重要背景信息割裂开来的可能性。 使用RecursiveCharacterTextSplitter它会用常见的分隔符递归拆分文档,比如不断增加线条,直到每个区块大小合适。

from langchain_text_splitters import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000, chunk_overlap=200, add_start_index=True
)
all_splits = text_splitter.split_documents(docs)

print(len(all_splits))

3.嵌入

向量搜索是一种常见的存储和搜索非结构化数据(如非结构化文本)的方法。其理念是存储与文本相关的数值向量。给定查询,我们可以嵌入它作为同维度的向量,并使用向量相似度度(如余弦相似度)来识别相关文本。
向量模型采用ollama平台,具体安装方式可以访问ollama官网

import requests

def embed_text(text: str):
    url = "http://localhost:11434/api/embed"
    payload = {
        "model": "nomic-embed-text:latest",
        "input": text
    }

    response = requests.post(url, json=payload)
    result = response.json()
    return result["embeddings"][0]   # 返回单条 embedding

vector_1 = embed_text(all_splits[0].page_content)
vector_2 = embed_text(all_splits[1].page_content)
assert len(vector_1) == len(vector_2)
# 打印向量信息
print("向量长度:", len(vector_1))
print("前10维:", vector_1[:10])
向量长度: 768
前10维: [0.03573038, 0.09579249, -0.08523589, -0.0144643625, 0.0054141716, 0.0043759444, 0.061593026, 0.0070784735, -0.014065722, -0.026565876]

有了生成文本嵌入的模型,我们可以将其存储在支持高效相似性搜索的特殊数据结构中。


4.向量存储

LangChain VectorStore 对象包含用于向存储中添加文本和文档对象以及使用各种相似度度量查询它们的方法。它们通常使用嵌入模型进行初始化,这些模型决定了如何将文本数据转换为数值向量。

from langchain.embeddings.base import Embeddings   #封装为类 因为我没有使用langchain封装的好的向量模型

class OllamaEmbeddingsWrapper(Embeddings):
    def embed_documents(self, texts):
        """返回二维列表,每条文本对应一个 embedding"""
        return [embed_text(t) for t in texts]

    def embed_query(self, text):
        """返回单条 embedding"""
        return embed_text(text)

import faiss       #Facebook 开源的高效向量搜索库,用于存储和检索 embedding 向量
from langchain_community.docstore.in_memory import InMemoryDocstore    #InMemoryDocstore:LangChain 提供的内存型文档存储,存储向量对应的原始文档内容
from langchain_community.vectorstores import FAISS   #FAISS:LangChain 封装的向量数据库接口,把 FAISS 与文档存储结合,方便向量检索

embedding_wrapper = OllamaEmbeddingsWrapper()   #这里 OllamaEmbeddingsWrapper 是你自定义的类(继承自 Embeddings),用来包装你的 embed_text 函数。

embedding_dim = len(embedding_wrapper.embed_query("hello world"))
#创建 FAISS 向量索引对象   IndexFlatL2 是最基础的 L2 距离索引(暴力搜索,精确度高,但大数据量慢)。
# FAISS 会用这个索引来存储向量,并计算向量间的 L2 距离(用于相似度搜索)
index = faiss.IndexFlatL2(embedding_dim)

#将 FAISS 与 LangChain 封装起来,形成完整的向量存储
vector_store = FAISS(
    embedding_function=embedding_wrapper,   #提供 embedding 生成方法,FAISS 在插入/检索时会调用它。
    index=index,             #使用前面创建的 FAISS 索引对象存储向量。
    docstore=InMemoryDocstore(),    #用于存储原始文档内容,向量索引只是存向量。
    index_to_docstore_id={},     #FAISS 索引向量的 id → 文档存储 id 的映射表。
)

向量存储实例化完成后,我们现在可以对文档进行索引了。

ids = vector_store.add_documents(documents=all_splits)

一旦我们实例化了一个包含文档的 VectorStore,就可以对其进行查询。VectorStore 包含以下查询方法:

  • 同步和异步;
  • 按字符串查询和按向量查询;
  • 是否返回相似度得分;
  • 通过相似性和最大边际相关性(以平衡检索结果中查询的相似性和多样性)。

这些方法通常会在其输出中包含一个 Document 对象列表。词嵌入通常将文本表示为一个“稠密”向量,使得含义相似的文本在几何上彼此接近。这让我们只需输入一个问题,无需了解文档中使用的任何特定关键词,即可检索相关信息。 比如根据与字符串查询的相似度返回文档:

results = vector_store.similarity_search(
    "小魏多大?"
)

print(results[0])
page_content='是她勇敢追梦最坚强的后盾。 
第四章 小小种子与远大苍穹 
20岁的小魏,如同一张洁净的宣纸,正在被来自家庭、地域与历史的浓厚墨彩,细细
描绘。她的生命故事才刚刚写下序章,但我们已经可以看到其中交织的丰富脉络:豫
章古城的文脉赋予她灵秀底蕴,江右先贤的故事塑造她品格雏形,传统养生智慧启迪
她强健体魄,民间艺术基因点燃她梦想火花,而诗礼传家的温暖,则为她提供了永不
枯竭的爱与力量。 
她的梦想——成为一名杰出的舞蹈家——因此拥有了超越个体的文化分量。这不仅仅
是一个孩童的职业向往,更可能成为一个文化传承与创新的生动隐喻。她未来要跳
的,或许不仅是芭蕾的足尖或现代舞的线条,更是能舞出“滕王高阁临江渚”的意境,转' metadata={'producer': 'Microsoft® Word 2024', 'creator': 'Microsoft® Word 2024', 'creationdate': '2025-12-23T16:05:36+08:00', 'moddate': '2025-12-23T16:05:36+08:00', 'source': 'D:\\sources\\自我认知.pdf', 'total_pages': 3, 'page': 1, 'page_label': '2', 'start_index': 807}

根据分数回归查询:

results = vector_store.similarity_search_with_score("小妍多大?")
doc, score = results[0]
print(f"Score: {score}\n")
print(doc)
Score: 0.7209560871124268

page_content='是她勇敢追梦最坚强的后盾。 
第四章 小小种子与远大苍穹 
20岁的小魏,如同一张洁净的宣纸,正在被来自家庭、地域与历史的浓厚墨彩,细细
描绘。她的生命故事才刚刚写下序章,但我们已经可以看到其中交织的丰富脉络:豫
章古城的文脉赋予她灵秀底蕴,江右先贤的故事塑造她品格雏形,传统养生智慧启迪
她强健体魄,民间艺术基因点燃她梦想火花,而诗礼传家的温暖,则为她提供了永不
枯竭的爱与力量。 
她的梦想——成为一名杰出的舞蹈家——因此拥有了超越个体的文化分量。这不仅仅
是一个孩童的职业向往,更可能成为一个文化传承与创新的生动隐喻。她未来要跳
的,或许不仅是芭蕾的足尖或现代舞的线条,更是能舞出“滕王高阁临江渚”的意境,转' metadata={'producer': 'Microsoft® Word 2024', 'creator': 'Microsoft® Word 2024', 'creationdate': '2025-12-23T16:05:36+08:00', 'moddate': '2025-12-23T16:05:36+08:00', 'source': 'D:\\sources\\自我认知.pdf', 'total_pages': 3, 'page': 1, 'page_label': '2', 'start_index': 807}

根据与嵌入式查询的相似度返回文档:

embedding = embedding_wrapper.embed_query("小妍多大?")

results = vector_store.similarity_search_by_vector(embedding)
print(results[0])
page_content='是她勇敢追梦最坚强的后盾。 
第四章 小小种子与远大苍穹 
20岁的小魏,如同一张洁净的宣纸,正在被来自家庭、地域与历史的浓厚墨彩,细细
描绘。她的生命故事才刚刚写下序章,但我们已经可以看到其中交织的丰富脉络:豫
章古城的文脉赋予她灵秀底蕴,江右先贤的故事塑造她品格雏形,传统养生智慧启迪
她强健体魄,民间艺术基因点燃她梦想火花,而诗礼传家的温暖,则为她提供了永不
枯竭的爱与力量。 
她的梦想——成为一名杰出的舞蹈家——因此拥有了超越个体的文化分量。这不仅仅
是一个孩童的职业向往,更可能成为一个文化传承与创新的生动隐喻。她未来要跳
的,或许不仅是芭蕾的足尖或现代舞的线条,更是能舞出“滕王高阁临江渚”的意境,转' metadata={'producer': 'Microsoft® Word 2024', 'creator': 'Microsoft® Word 2024', 'creationdate': '2025-12-23T16:05:36+08:00', 'moddate': '2025-12-23T16:05:36+08:00', 'source': 'D:\\sources\\自我认知.pdf', 'total_pages': 3, 'page': 1, 'page_label': '2', 'start_index': 807}

5.检索(Retriever)

LangChain VectorStore 对象并非 Runnable 的子类。LangChain Retriever 是 Runnable,因此它们实现了一组标准方法(例如,同步和异步调用以及批量操作)。虽然我们可以从 VectorStore 构建 Retriever,但 Retriever 也可以与非 VectorStore 数据源(例如外部 API)进行交互。
我们可以自己创建一个简单的版本,而无需继承 Retriever 类。如果我们选择用于检索文档的方法,就可以轻松创建一个可运行的程序。下面我们将围绕 similarity_search 方法构建一个示例:

from typing import List

from langchain_core.documents import Document
from langchain_core.runnables import chain


@chain
def retriever(query: str) -> List[Document]:
    return vector_store.similarity_search(query, k=1)


retriever.batch(
    [
        "小魏多大?",
        "小魏的梦想是什么?",
    ],
)
[[Document(id='30fb9038-610f-4e1c-9656-9ed07eb79500', metadata={'producer': 'Microsoft® Word 2024', 'creator': 'Microsoft® Word 2024', 'creationdate': '2025-12-23T16:05:36+08:00', 'moddate': '2025-12-23T16:05:36+08:00', 'source': 'D:\\sources\\自我认知.pdf', 'total_pages': 3, 'page': 1, 'page_label': '2', 'start_index': 807}, page_content='是她勇敢追梦最坚强的后盾。 \n第四章 小小种子与远大苍穹 \n20岁的小魏,如同一张洁净的宣纸,正在被来自家庭、地域与历史的浓厚墨彩,细细\n描绘。她的生命故事才刚刚写下序章,但我们已经可以看到其中交织的丰富脉络:豫\n章古城的文脉赋予她灵秀底蕴,江右先贤的故事塑造她品格雏形,传统养生智慧启迪\n她强健体魄,民间艺术基因点燃她梦想火花,而诗礼传家的温暖,则为她提供了永不\n枯竭的爱与力量。 \n她的梦想——成为一名杰出的舞蹈家——因此拥有了超越个体的文化分量。这不仅仅\n是一个孩童的职业向往,更可能成为一个文化传承与创新的生动隐喻。她未来要跳\n的,或许不仅是芭蕾的足尖或现代舞的线条,更是能舞出“滕王高阁临江渚”的意境,转')],
 [Document(id='30fb9038-610f-4e1c-9656-9ed07eb79500', metadata={'producer': 'Microsoft® Word 2024', 'creator': 'Microsoft® Word 2024', 'creationdate': '2025-12-23T16:05:36+08:00', 'moddate': '2025-12-23T16:05:36+08:00', 'source': 'D:\\sources\\自我认知.pdf', 'total_pages': 3, 'page': 1, 'page_label': '2', 'start_index': 807}, page_content='是她勇敢追梦最坚强的后盾。 \n第四章 小小种子与远大苍穹 \n20岁的小魏,如同一张洁净的宣纸,正在被来自家庭、地域与历史的浓厚墨彩,细细\n描绘。她的生命故事才刚刚写下序章,但我们已经可以看到其中交织的丰富脉络:豫\n章古城的文脉赋予她灵秀底蕴,江右先贤的故事塑造她品格雏形,传统养生智慧启迪\n她强健体魄,民间艺术基因点燃她梦想火花,而诗礼传家的温暖,则为她提供了永不\n枯竭的爱与力量。 \n她的梦想——成为一名杰出的舞蹈家——因此拥有了超越个体的文化分量。这不仅仅\n是一个孩童的职业向往,更可能成为一个文化传承与创新的生动隐喻。她未来要跳\n的,或许不仅是芭蕾的足尖或现代舞的线条,更是能舞出“滕王高阁临江渚”的意境,转')]]

Vectorstore 实现了一个 as_retriever 方法,该方法会生成一个 Retriever 对象,具体来说是 VectorStoreRetriever 对象。这些 Retriever 对象包含特定的 search_type 和 search_kwargs 属性,用于标识要调用底层 Vectorstore 的哪些方法以及如何对这些方法进行参数化。例如,我们可以使用以下代码重现上述操作:

retriever = vector_store.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 1},
)

retriever.batch(
    [
        "小魏多大?",
        "小魏的梦想是什么?",
    ],
)

检索器(retriever )可以轻松集成到更复杂的应用程序中,例如检索增强生成 (RAG) 应用程序,这类应用程序将给定的问题与检索到的上下文结合起来,生成 LLM 的提示(prompt)。

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐