搭建并使用向量数据库
前序基础知识参考链接介绍:大模型入门到精通——使用Embedding API及搭建本地知识库(一)
搭建并使用向量数据库,因此读取数据后我们省去数据处理的环节直入主题
基于 LangChain 实现 README.md
相关文档的向数据库搭建,可以按照以下步骤进行:
1. 环境准备
确保你的Python环境已经安装了LangChain和其他必要的库。可以通过以下命令安装:
pip install langchain faiss-cpu
pip install chromadb
如果你打算使用GPU加速Faiss,可以安装faiss-gpu
。
2. 读取和预处理文档
- 遍历一个指定的文件夹,从中加载PDF和Markdown文档
- 对这些文档的文本内容进行预处理,并保留处理后的文档对象
- 最后打印出其中一个文档的类型、元数据和内容。验证结果;
1. 导入所需的库
import os
from langchain.document_loaders.pdf import PyMuPDFLoader
from langchain.document_loaders.markdown import UnstructuredMarkdownLoader
-
os
: 用于操作系统交互,主要用于遍历目录和处理文件路径。 -
PyMuPDFLoader
和UnstructuredMarkdownLoader
: 分别用于加载PDF和Markdown文件的文档加载器。这些加载器是langchain
库的一部分,能够将文档内容加载为Document
对象。
2. 初始化文件路径
file_paths = []
folder_path = './data/'
for root, dirs, files in os.walk(folder_path):
for file in files:
file_path = os.path.join(root, file)
file_paths.append(file_path)
-
file_paths
: 用于存储所有找到的文件的完整路径。 -
folder_path
: 指定要遍历的文件夹路径。在这里是相对路径./data/
。 -
os.walk(folder_path)
: 递归遍历指定目录中的所有子目录和文件,返回每个目录的路径(root
)、子目录(dirs
)和文件列表(files
)。 -
file_paths.append(file_path)
: 将每个文件的完整路径添加到file_paths
列表中。
3. 初始化加载器
loaders = []
for file_path in file_paths:
file_type = file_path.split('.')[-1]
if file_type == 'pdf':
loaders.append(PyMuPDFLoader(file_path))
elif file_type == 'md':
loaders.append(UnstructuredMarkdownLoader(file_path))
-
loaders
: 用于存储不同类型文件的加载器实例。 -
file_type = file_path.split('.')[-1]
: 通过文件扩展名来判断文件类型(如pdf
或md
)。 - 根据文件类型,实例化对应的加载器(
PyMuPDFLoader
或UnstructuredMarkdownLoader
),并将实例添加到loaders
列表中。
4. 定义预处理函数
def preprocess_text(text):
# 转换为小写
text = text.lower()
# 移除非字母数字字符(标点符号等)
text = ''.join(char for char in text if char.isalnum() or char.isspace())
# 其他预处理操作,如去除停用词、分词等,可以在这里添加
return text
preprocess_text(text)
: 这是一个用于文本预处理的函数。
-
text.lower()
: 将文本中的所有字符转换为小写。 -
''.join(char for char in text if char.isalnum() or char.isspace())
: 移除所有非字母数字字符(例如标点符号),仅保留字母、数字和空格。
5. 加载文件并进行预处理
documents = []
for loader in loaders:
raw_documents = loader.load() # 加载文档对象
for document in raw_documents:
# 对文本内容进行预处理
document.page_content = preprocess_text(document.page_content)
# 保留预处理后的Document对象
documents.append(document)
-
documents
: 用于存储加载并预处理后的Document
对象。 -
raw_documents = loader.load()
: 使用加载器加载文件,得到一组Document
对象。 -
document.page_content = preprocess_text(document.page_content)
: 对每个Document
对象的文本内容(page_content
属性)进行预处理,并将处理后的文本重新赋值给page_content
属性。 -
documents.append(document)
: 将预处理后的Document
对象存储到documents
列表中。
6. 访问并打印文档信息
text = documents[1]
print(f"每一个元素的类型:{type(text)}.",
f"该文档的描述性数据:{text.metadata}",
f"查看该文档的内容:\n{text.page_content[0:]}",
sep="\n------\n")
-
text = documents[1]
: 获取documents
列表中的第二个文档对象。 -
type(text)
: 打印text
变量的类型。因为text
是一个Document
对象,所以类型应该是Document
。 -
text.metadata
: 打印文档的元数据,这可能包括文档的标题、作者、创建日期等信息。 text.page_content[0:]
: 打印文档的文本内容。由于内容可能很长,这里只是示例地显示从头开始的部分内容。
3. 文本分割
1. 导入 CharacterTextSplitter
from langchain.text_splitter import CharacterTextSplitter
-
CharacterTextSplitter
是langchain
库中的一个文本分割器,它能够将长文本按字符数分割成较小的块。这在处理长文档时特别有用,尤其是在需要对文本进行进一步处理或分析时。
2. 定义 split_text
函数
def split_text(content, chunk_size=500, overlap=50):
splitter = CharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=overlap)
return splitter.split_text(content)
-
split_text
是一个用于分割文本的函数。
参数说明:
-
content
: 需要分割的文本内容,类型通常是字符串。 -
chunk_size=500
: 每个文本块的最大字符数。默认为500个字符。这个参数决定了分割后的每个块的大小。 -
overlap=50
: 相邻块之间的重叠字符数。默认为50个字符。这个参数可以确保在处理分块文本时,相邻块之间有一定的上下文关联,避免内容断裂带来的理解困难。
函数过程:
- 实例化
CharacterTextSplitter
:
-
splitter = CharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=overlap)
:根据指定的块大小和重叠长度,创建一个CharacterTextSplitter
对象。
- 分割文本:
-
return splitter.split_text(content)
:使用split_text
方法将输入的content
按指定的块大小和重叠长度进行分割,并返回分割后的文本块列表。
应用场景
- 大文档处理:当你需要处理非常大的文档时,将其分割成较小的块可以使得后续的处理(如语义分析、摘要生成等)更为高效。
- 上下文相关处理:通过设定适当的重叠,可以在块之间保留部分上下文,避免因块之间的断裂导致信息丢失或理解困难。
3. 示例使用
content = "这是一个非常长的文本,需要分割成较小的块。每个块的大小为500个字符,并且相邻块之间有50个字符的重叠。"
chunks = split_text(content)
for i, chunk in enumerate(chunks):
print(f"块 {i+1}:\n{chunk}\n")
- 这段代码将示例文本
content
按照chunk_size=500
和overlap=50
的设置进行分割,并打印出每个分割块。
4. langchain
分割工具介绍
langchain
提供了多种文本分割工具,以适应不同的文本处理需求。
1. CharacterTextSplitter
- 功能: 按照字符数对文本进行分割。
- 参数:
chunk_size
(块大小),chunk_overlap
(块之间的重叠字符数)。 - 适用场景: 需要基于字符数对文本进行分割的场景。
2. RecursiveCharacterTextSplitter
- 功能: 递归地按字符数进行分割,先尝试按较大块分割,若失败则逐步缩小分割块的大小。
- 参数:
chunk_size
,chunk_overlap
,separators
(分割符列表)。 - 适用场景: 处理复杂文本,确保尽可能保留语义完整性。
3. TokenTextSplitter
- 功能: 按照令牌(token)数对文本进行分割,常用于需要处理基于令牌的语言模型。
- 参数:
chunk_size
,chunk_overlap
,model_name
(指定使用的模型)。 - 适用场景: 处理需要与语言模型(如 GPT-3、GPT-4)进行交互的文本。
4. SentenceTextSplitter
- 功能: 按句子进行分割,确保每个分割块是完整的句子。
- 参数:
chunk_size
,chunk_overlap
。 - 适用场景: 需要在句子层面进行处理的文本,如摘要生成或情感分析。
5. MarkdownHeaderTextSplitter
- 功能: 根据 Markdown 文档中的标题进行分割,每个分割块对应一个标题及其内容。
- 参数:
headers_to_split_on
(标题层级列表,如["#", "##", "###"]
)。 - 适用场景: 在不同的文本结构级别上进行分割,如处理 Markdown 格式文档时。
6. ListSplitter
- 功能: 将文本列表中的每个元素视为一个分割块。
- 适用场景: 需要直接处理文本列表时的场景,如输入已分割的句子列表。
7. ParagraphTextSplitter
- 功能: 按段落进行文本分割。
- 适用场景: 适用于需要按段落级别处理的任务,如文本摘要或分类。
8. NltkTextSplitter
- 功能: 使用 NLTK 库的分词器来进行文本分割,如按句子或单词进行分割。
- 适用场景: 需要利用 NLTK 的自然语言处理功能进行文本分割的场景。
9. SpacyTextSplitter
- 功能: 使用 SpaCy 库进行文本分割,通常按句子或词语进行分割。
- 适用场景: 需要利用 SpaCy 的高级语言处理功能来进行分割时,特别是处理多语言文本的任务。
10. RegexTextSplitter
- 功能: 使用正则表达式进行文本分割。
- 参数:
pattern
(正则表达式模式)。 - 适用场景: 在需要根据特定模式或结构进行分割的文本时非常有用。
11. LanguageSplitter
- 功能: 根据不同语言的格式和特点进行分割,支持多语言文本分割。
- 参数:
language
,chunk_size
,chunk_overlap
。 - 适用场景: 在处理多语言文本时,根据语言特点进行更好的分割和处理。
4. 文本向量化类定义及介绍
1. ZhipuAIEmbedding
自定义
from __future__ import annotations
import logging
from typing import Dict, List, Any
from langchain.embeddings.base import Embeddings
from langchain.pydantic_v1 import BaseModel, root_validator
logger = logging.getLogger(__name__)
class ZhipuAIEmbeddings(BaseModel, Embeddings):
"""`Zhipuai Embeddings` embedding models."""
client: Any
"""`zhipuai.ZhipuAI"""
@root_validator()
def validate_environment(cls, values: Dict) -> Dict:
"""
实例化ZhipuAI为values["client"]
Args:
values (Dict): 包含配置信息的字典,必须包含 client 的字段.
Returns:
values (Dict): 包含配置信息的字典。如果环境中有zhipuai库,则将返回实例化的ZhipuAI类;否则将报错 'ModuleNotFoundError: No module named 'zhipuai''.
"""
from zhipuai import ZhipuAI
_ = load_dotenv(find_dotenv())
api_key = os.environ["API_key"]
values["client"] = ZhipuAI(api_key=api_key)
return values
def embed_query(self, text: str) -> List[float]:
"""
生成输入文本的 embedding.
Args:
texts (str): 要生成 embedding 的文本.
Return:
embeddings (List[float]): 输入文本的 embedding,一个浮点数值列表.
"""
embeddings = self.client.embeddings.create(
model="embedding-2",
input=text
)
return embeddings.data[0].embedding
def embed_documents(self, texts: List[str]) -> List[List[float]]:
"""
生成输入文本列表的 embedding.
Args:
texts (List[str]): 要生成 embedding 的文本列表.
Returns:
List[List[float]]: 输入列表中每个文档的 embedding 列表。每个 embedding 都表示为一个浮点值列表。
"""
return [self.embed_query(text) for text in texts]
async def aembed_documents(self, texts: List[str]) -> List[List[float]]:
"""Asynchronous Embed search docs."""
raise NotImplementedError("Please use `embed_documents`. Official does not support asynchronous requests")
async def aembed_query(self, text: str) -> List[float]:
"""Asynchronous Embed query text."""
raise NotImplementedError("Please use `aembed_query`. Official does not support asynchronous requests")
def vectorize_text(embeddings_model,chunks):
return embeddings_model.embed_documents(chunks)
1. ZhipuAIEmbeddings
类
- 该类继承了
BaseModel
和Embeddings
,表示这是一个用于生成文本嵌入的模型。 -
client: Any
: 用于存储ZhipuAI
的实例,这个实例是用来与 ZhipuAI 的 API 进行交互的。
2. validate_environment
方法
- 这是一个
root_validator
方法,用于在实例化类时对环境进行检查。 - 通过加载
.env
文件中的 API 密钥,实例化ZhipuAI
客户端,并将其存储在values["client"]
中。如果zhipuai
模块不存在,则抛出错误。
3. embed_query
方法
- 功能: 生成输入文本的嵌入向量。
- 输入:
text
,一个字符串。 - 返回: 生成的嵌入向量,一个浮点数列表。该方法调用
client.embeddings.create()
,使用指定的模型生成文本的嵌入。
4. embed_documents
方法
- 功能: 生成多个文本的嵌入向量。
- 输入:
texts
,一个字符串列表。 - 返回: 返回每个输入文本的嵌入向量,嵌入向量是浮点数列表的列表。方法内部循环调用
embed_query
方法来生成每个文本的嵌入。
5. 异步方法 (aembed_documents
和 aembed_query
)
- 这些方法被设计为异步版本,但目前没有实现。它们抛出
NotImplementedError
,提示官方不支持异步请求。
6. vectorize_text
函数
- 功能: 使用
ZhipuAIEmbeddings
类的实例生成输入文本块的嵌入向量。 - 输入:
embeddings_model
是ZhipuAIEmbeddings
的实例,chunks
是待处理的文本块。 - 返回: 通过调用
embed_documents
方法,返回每个文本块的嵌入向量列表。
2. 词向量生成及存储
# 使用 OpenAI Embedding
# from langchain.embeddings.openai import OpenAIEmbeddings
# 使用百度千帆 Embedding
# from langchain.embeddings.baidu_qianfan_endpoint import QianfanEmbeddingsEndpoint
# 使用封装的智谱 Embedding,需要将封装代码下载到本地使用
# 定义 Embeddings
# embedding = OpenAIEmbeddings()
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv())
embedding = ZhipuAIEmbeddings()
# embedding = QianfanEmbeddingsEndpoint()
# 定义持久化路径
persist_directory = './data_base/vector_db/chroma'
os.makedirs(persist_directory,exist_ok=True)
1. 加载环境变量
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv())
- 功能: 使用
dotenv
库加载.env
文件中的环境变量。这通常用于获取 API 密钥等配置信息。 -
find_dotenv()
会在项目目录中查找.env
文件,而load_dotenv()
则会将文件中的内容加载到环境变量中。
2. 定义 Embedding 模型
embedding = ZhipuAIEmbeddings()
- 这行代码初始化了一个
ZhipuAIEmbeddings
实例,用于生成文本的嵌入向量。可以根据需要切换到其他嵌入模型,如OpenAIEmbeddings
或QianfanEmbeddingsEndpoint
。 - ZhipuAIEmbeddings 是一个自定义的嵌入模型类,使用智谱AI的服务来生成文本嵌入。
3. 定义持久化路径
from langchain.vectorstores.chroma import Chroma
persist_directory = './data_base/vector_db/chroma'
os.makedirs(persist_directory, exist_ok=True)
# import faiss
vectordb = Chroma.from_documents(
documents=split_docs[:20], # 为了速度,只选择前 20 个切分的 doc 进行生成;使用千帆时因QPS限制,建议选择前 5 个doc
embedding=embedding,
persist_directory=persist_directory # 允许我们将persist_directory目录保存到磁盘上
)
print(f"向量库中存储的数量:{vectordb._collection.count()}")
- 功能: 定义了一个用于持久化存储的目录路径。这个路径通常用于保存生成的向量数据库。
-
os.makedirs(persist_directory, exist_ok=True)
会在指定路径上创建目录,如果目录已经存在,exist_ok=True
选项会防止抛出错误。
3. langchain.embeddings
相关介绍
langchain.embeddings
模块提供了多种嵌入模型的接口,这些模型用于将文本转换为向量表示,这在自然语言处理任务中非常常见。是一些常用的嵌入模型及其简介:
1. OpenAIEmbeddings
- 简介: 这是一个基于 OpenAI 提供的嵌入服务的模型。它通常用于生成高质量的文本嵌入,适用于需要处理复杂自然语言任务的场景。
- 适用场景: 文本分类、语义搜索、问答系统等。
- 示例代码:
from langchain.embeddings.openai import OpenAIEmbeddings
embedding = OpenAIEmbeddings()
2. HuggingFaceEmbeddings
- 简介: 基于 Hugging Face 提供的预训练模型,可以选择多种模型(如 BERT、RoBERTa)来生成嵌入。这些模型广泛应用于各类 NLP 任务。
- 适用场景: 句子相似度、情感分析、文本摘要等。
- 示例代码:
from langchain.embeddings.huggingface import HuggingFaceEmbeddings
embedding = HuggingFaceEmbeddings(model_name="distilbert-base-uncased")
3. SentenceTransformersEmbeddings
- 简介: 使用
SentenceTransformers
库中的模型生成文本嵌入,这些模型经过特定任务的微调,如语义相似度和聚类。 - 适用场景: 文本聚类、信息检索、语义搜索等。
- 示例代码:
from langchain.embeddings.sentence_transformers import SentenceTransformersEmbeddings
embedding = SentenceTransformersEmbeddings(model_name="all-MiniLM-L6-v2")
4. BaiduQianfanEmbeddings
- 简介: 这是一个基于百度千帆平台的嵌入模型,主要面向中文 NLP 任务,支持生成高质量的中文文本嵌入。
- 适用场景: 中文文本处理、中文语义搜索、中文文本分类等。
- 示例代码:
from langchain.embeddings.baidu_qianfan_endpoint import QianfanEmbeddingsEndpoint
embedding = QianfanEmbeddingsEndpoint(endpoint_url="your_endpoint_url", api_key="your_api_key")
5. ZhipuAIEmbeddings
- 简介: 基于智谱 AI 提供的嵌入服务,主要用于生成中文和英文文本的嵌入表示,适合多语言环境。
- 适用场景: 多语言文本处理、跨语言信息检索等。
- 示例代码:
embedding = ZhipuAIEmbeddings()
6. CohereEmbeddings
- 简介: 使用 Cohere 提供的嵌入模型,该模型支持多种语言,适用于需要大规模处理的任务。
- 适用场景: 语义搜索、问答系统、多语言文本处理等。
- 示例代码:
from langchain.embeddings.cohere import CohereEmbeddings
embedding = CohereEmbeddings(api_key="your_api_key")
7. AzureEmbeddings
- 简介: 基于微软 Azure 提供的认知服务的嵌入模型,集成了 Azure 的各种 NLP 服务,适合企业级应用。
- 适用场景: 企业级应用、定制化 NLP 解决方案等。
示例代码:
from langchain.embeddings.azure_openai import AzureEmbeddings
embedding = AzureEmbeddings(deployment_name="your_deployment_name", api_key="your_api_key")
5.内容检索
5.1 相似度检索
相似度检索是通过计算查询与文档之间的相似度来找到相关内容的过程。余弦相似度是常用的相似度度量方法,通过点积和范数计算两个向量之间的夹角余弦值,值越接近 1,表示文档与查询越相关。
1. 相似度检索的原理
相似度检索是一种用于从大量文档中找到与查询最相关内容的技术。其核心思想是将文档和查询都转换为向量,然后计算这些向量之间的相似度。相似度越高,文档与查询越相关。
2. 文本向量化
首先,需要将文本转换为向量。文本向量化可以通过多种方法实现,比如词袋模型 (Bag of Words)、TF-IDF、Word2Vec、BERT 等嵌入模型。在这个过程中,每个文档和查询都会被表示为一个高维向量。
3. 余弦相似度
Chroma的相似度搜索使用的是余弦距离, 即:
其中 、 分别是向量 、 的分量。
当你需要数据库返回严谨的按余弦相似度排序的结果时可以使用 similarity_search 函数。
- 向量化:将查询和文档集合中的每个文档都转换为向量。
- 计算相似度:使用余弦相似度计算查询向量与每个文档向量之间的相似度。
- 排序:将文档按照相似度得分从高到低排序,返回前 (k) 个与查询最相关的文档。
5. 代码示例解释
question="什么是大语言模型"
sim_docs = vectordb.similarity_search(question,k=3)
-
question
:用户的查询,例如“什么是大语言模型”。 -
similarity_search
:根据相似度检索相关文档的方法。 -
k=3
:表示返回最相关的 3 个文档。
在检索过程中,代码将查询转换为向量,与向量数据库中的所有文档进行相似度计算,并返回相似度最高的 3 个文档。
1. 最大边际相关性 (MMR) 检索的原理
最大边际相关性 (MMR, Maximum Marginal Relevance) 是一种在信息检索中常用的方法,用来平衡文档的相关性和多样性。传统的相似度检索通常会返回内容相似性最高的几个文档,但这些文档可能在信息上有较大重叠,导致结果的单一性。MMR 通过引入多样性来改善这一点。MMR 检索通过在相关性和多样性之间找到平衡,能够返回更为丰富和全面的文档集,避免了单一化的问题。这在需要覆盖多个信息面向的复杂查询中尤其有用。
2. MMR 的核心思想
MMR 的核心在于在每一步选择下一个文档时,不仅考虑该文档与查询的相关性,还要考虑该文档与已经选择的文档集合的相似性。选择与已有文档相似性低的文档,以增加结果的多样性。MMR 通过以下公式来计算得分:
其中:
- 是文档 与查询
- 是文档 与已选文档集合 ` 中某个文档
3. 选择过程
- 初始化:首先选取与查询 (Q) 相关性最高的文档作为初始集合。
- 迭代选择:在每一步中,从剩余文档中选择一个能够最大化 MMR 公式的文档,并将其加入已选择的文档集合。
- 停止条件:重复上述步骤,直到选取了 (k) 个文档。
4. 代码示例解释
mmr_docs = vectordb.max_marginal_relevance_search(question, k=3)
-
max_marginal_relevance_search
: 使用 MMR 算法来进行检索的方法。 -
k=3
: 表示希望返回 3 个结果。 -
question
: 用户的查询,如“什么是大语言模型”。
在这个过程中,代码首先根据相关性选择一个文档,然后逐步添加其他与已选文档多样性更高的文档,从而避免了内容单一的问题。
5. MMR 的优势
- 丰富性:通过增加检索结果的多样性,避免了结果集中内容的重复。
- 信息覆盖:能够更全面地覆盖查询相关的不同方面的信息。
- 定制化调节:通过调整 (\lambda) 参数,可以定制相关性和多样性之间的平衡,适应不同应用场景的需求。
6. 结果示例
print("-"*100)
for i, sim_doc in enumerate(mmr_docs):
print(f"MMR 检索到的第{i}个内容: \n{sim_doc.page_content[:200]}", end="\n--------------\n")
-
sim_doc.page_content[:200]
: 打印每个检索到的文档的前 200 个字符,以展示 MMR 选择的文档内容。 - 结果输出:MMR 检索到的内容会比单纯的相似度检索结果更加多样化,涵盖了查询问题的多个角度。
6. 结果示例
print("-"*100)
for i, sim_doc in enumerate(mmr_docs):
print(f"MMR 检索到的第{i}个内容: \n{sim_doc.page_content[:200]}", end="\n--------------\n")
-
sim_doc.page_content[:200]
: 打印每个检索到的文档的前 200 个字符,以展示 MMR 选择的文档内容。 - 结果输出:MMR 检索到的内容会比单纯的相似度检索结果更加多样化,涵盖了查询问题的多个角度。
参考
https://datawhalechina.github.io/llm-universe/#/C3/4.搭建并使用向量数据库