前言(未完成)
数据检索的挑战 —— 从结构化时代到非结构化时代。传统数据库存储的是标量数据,标量原来是物理上的概念,指只有大小而没有方向的物理量,在数据库这里用标量来表示一类数据类型,例如数字型、字符型、日期型、布尔型,针对该类数据类型可以使用精确匹配的方式进行查询,例如传统关系数据库的 SQL,该检索方式称为标量查询。全文检索是指在非结构化的文本数据中基于特定单词或者文本在全文范围内进行检索。常见的搜索引擎就是对全文检索技术的实现,例如 Lucene、Solr、ElasticSearch 等。标量查询和全文检索本质上是关于标量数据或关键字的精确匹配,其查找结果与输入目标之间是完全相同的关系。
全球数据量急剧增长,其中超过80%的数据都会是处理难度较大的非结构化数据,如文档、文本、图形、图像、音频、视频等,非结构化数据在大数据时代的重要地位已成为共识。现代 AI/ML 技术的发展提供了一种从非结构数据中提取语义信息的方式 —— embedding。区别于前面提到的标量类型,embedding 是一种向量类型 —— 由多个数值组成的数组,因此 embedding 又被称为向量或者矢量。
向量模型是指通过特定的嵌入(embedding)技术,将原始的非结构化数据转换成一个数值型的向量,在数学表示上,向量是一个由浮点数或者二值型数据组成的 n 维数组。embedding model 本质上是一种数据压缩技术,通过 AI/ML 技术对原始数据进行编码,使用比原始数据更少的比特位来表示数据。压缩后的数据就是原始数据的“隐空间表示”,压缩过程中,外部特征和非重要特性被去除,最重要的特征被保存下来,随着数据维度的降低,本质上越相似的原始数据,其隐空间表示也越相近。因此,隐空间是一个抽象的多维空间,外部对象被编码为该空间的内部表示,被编码对象在外部世界越相似,在隐空间中就越靠近彼此。基于以上理论基础,我们可以通过 embedding 之间的距离来衡量其相似程度。
技术
向量检索是一种基于距离函数的相似度检索,由于向量间的距离反映了向量的相似度,因此通过距离排序可以查找最相似的若干个向量。向量检索算法有 kNN 和 ANN 两种。
- kNN(k-Nearest Neighbors)是一种蛮力检索方式,当给定目标向量时,计算该向量与候选向量集中所有向量的相似度,并返回最相似的 K 条。当向量库中数据量很大时 kNN 会消耗很多计算资源,耗时也不理想。
- ANN ( Approximate Nearest Neighbor)是一种更为高效的检索方式,其基本思想是预先计算向量间的距离,并将距离相近的向量存储在一起,从而在检索时可以更高效。预先计算就是构建向量索引的过程,向量索引是一种将向量数据组织为能够高效检索的结构。向量索引大幅提升了检索速度,但返回的是近似结果,因此 ANN 检索会有少量的精度牺牲。常见的 ANN 索引类型有 IVF、HNSW、PQ、IVFPQ
向量的相似性度量基于距离函数,常见的距离函数有欧式距离、余弦距离、点积距离,实际应用中选择何种距离函数取决于具体的应用场景。
- 欧式距离衡量两个向量在空间中的直线距离。欧式距离存在尺度敏感性的局限性,通过归一化等技术可以有效降低尺度敏感性对相似度的干扰。欧式距离适用于低维向量,在高维空间下会逐渐失效。
- 余弦距离衡量两个向量之间夹角的余弦值。余弦距离存在数值敏感性的局限性,因为其只考虑了向量的方向,而没有考虑向量的长度。余弦距离适用于高维向量或者稀疏向量。
- 点积距离通过将两个向量的对应分量相乘后再全部求和而进行相似度衡量,点积距离同时考虑了向量的长度和方向。点积距离存在尺度敏感性和零向量的局限性。
混合检索
传统检索技术善于精确查询,但缺乏语义理解。而向量检索技术能够很好的识别用户意图,但在精确检索方面召回率大概率不如传统检索技术,两种技术都不完美。对于特定的检索场景,两者结合能够提供更准确的检索结果。但混合检索提出了对多个结果集重新排序的难题。全文检索返回的结果集基于TF-IDF、BM25等文档相关性评分排序,向量检索返回的结果集基于距离函数的相似性评分排序,应用程序需要对两者的结果进行重新排序(Re-ranking)。重新排序指将来自多种检索技术的有序结果集进行规范化合并,形成同一标准的单一有序结果集。单一有序结果集能够更好的供下游系统处理和分析。常见的重新排序算法有 RRF、RankNet、LambdaRank、LambdaMART 等。
产品
面向 RAG 应用开发者的实用指南和建议在 RAG 应用生产环境中有效部署向量数据库的关键技巧:
- 设计一个有效的 Schema:仔细考虑数据结构及其查询方式,创建一个可优化性能和提供可扩展性的 Schema。
- 动态 Schema vs. 固定 Schema。。动态 Schema 提供了灵活性,简化了数据插入和检索流程,无需进行大量的数据对齐或 ETL 过程。这种方法非常适合需要更改数据结构的应用。另一方面,固定 Schema 也十分重要,因为它们有着紧凑的存储格式,在性能效率和节约内存方面表现出色。
- 设置主键和 Partition key
- 选择 Embedding 向量类型。稠密向量 (Dense Embedding);稀疏向量(Sparse Embedding);二进制向量(Binary Embedding)
- 考虑可扩展性:考虑未来的数据规模增长,并充分设计架构以适应不断增长的数据量和用户流量。
- 选择最佳索引并优化性能:可以针对向量数据构建高效的索引结构,如倒排索引、树形结构(如 KD 树、Ball Tree)或近似最近邻搜索算法(如FAISS、HNSW),加速检索过程。
milvus
使用
当我们把通过模型或者 AI 应用处理好的数据喂给它之后(“一堆特征向量”),它会根据一些固定的套路,例如像传统数据库进行查询优化加速那样,为这些数据建立索引。避免我们进行数据查询的时候,需要笨拙的在海量数据中进行。
本地
faiss 原生使用
# 准备数据
model = SentenceTransformer('uer/sbert-base-chinese-nli')
sentences = ["住在四号普里怀特街的杜斯利先生及夫人非常骄傲地宣称自己是十分正常的人",
"杜斯利先生是一家叫作格朗宁斯的钻机工厂的老板", "哈利看着她茫然地低下头摸了摸额头上闪电形的伤疤",
"十九年来哈利的伤疤再也没有疼过"]
sentence_embeddings = model.encode(sentences)
# 建立索引
dimension = sentence_embeddings.shape[1]
index = faiss.IndexFlatL2(dimension)
index.add(sentence_embeddings)
# 检索
topK = 2
search = model.encode(["哈利波特猛然睡醒"]) # 将要搜索的内容“哈利波特猛然睡醒”编码为向量
D, I = index.search(search, topK) # D指的是“数据置信度/可信度” I 指的是我们之前数据准备时灌入的文本数据的具体行数。
print(I)
print([x for x in sentences if sentences.index(x) in I[0]])
faiss 与LangChain 集合,主要是与 LangChain 的 document和 Embeddings 结合。 faiss 本身只存储 文本向量化后的向量(index.faiss文件),但是vector db对外使用,一定是文本查文本,所以要记录 文本块与向量关系(index.pkl文件)。此外,需支持新增和删除文件(包含多个文本块),所以也要支持按文件删除 文本块对应的向量。
from langchain.document_loaders import TextLoader
# 录入documents 到faiss
loader = TextLoader("xx.txt") # 加载文件夹中的所有txt类型的文件
documents = loader.load() # 将数据转成 document 对象,每个文件会作为一个 document
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
docs = text_splitter.split_documents(documents) # 切割加载的 document
embeddings = OpenAIEmbeddings() # 初始化 openai 的 embeddings 对象
db = FAISS.from_documents(docs, embeddings) # 将 document 通过 openai 的 embeddings 对象计算 embedding 向量信息并临时存入 faiss 向量数据库,用于后续匹配查询
query = "What did the president say about Ketanji Brown Jackson"
docs = db.similarity_search(query)
print(docs[0].page_content)
简单的源码分析
# 根据文档内容构建 langchain.vectorstores.Faiss
vectorstore.base.from_documents(cls: Type[VST],documents: List[Document], embedding: Embeddings, **kwargs: Any,) -> VST:
"""Return VectorStore initialized from documents and embeddings."""
texts = [d.page_content for d in documents]
metadatas = [d.metadata for d in documents]
return cls.from_texts(texts, embedding, metadatas=metadatas, **kwargs)
# Embeds documents.
embeddings = embedding.embed_documents(texts)
cls.__from(texts,embeddings,embedding, metadatas=metadatas,ids=ids,**kwargs,)
# Initializes the FAISS database
faiss = dependable_faiss_import()
index = faiss.IndexFlatL2(len(embeddings[0]))
vector = np.array(embeddings, dtype=np.float32)
index.add(vector)
# 建立id 与text 的关联
documents = []
if ids is None:
ids = [str(uuid.uuid4()) for _ in texts]
for i, text in enumerate(texts):
metadata = metadatas[i] if metadatas else {}
documents.append(Document(page_content=text, metadata=metadata))
index_to_id = dict(enumerate(ids))
# Creates an in memory docstore
docstore = InMemoryDocstore(dict(zip(index_to_id.values(), documents)))
return cls(embedding.embed_query,index,docstore,index_to_id,normalize_L2=normalize_L2,**kwargs,)
save_local:
faiss = dependable_faiss_import()
faiss.write_index(self.index, str(path / "{index_name}.faiss".format(index_name=index_name)))
with open(path / "{index_name}.pkl".format(index_name=index_name), "wb") as f:
pickle.dump((self.docstore, self.index_to_docstore_id), f)
在线
Pinecone 是一个在线的向量数据库。所以,我可以第一步依旧是注册,然后拿到对应的 api key。
from langchain.vectorstores import Pinecone
# 从远程服务加载数据
docsearch = Pinecone.from_existing_index(index_name, embeddings)
# 录入documents 持久化数据到pinecone
# 初始化 pinecone
pinecone.init(api_key="你的api key",environment="你的Environment")
loader = DirectoryLoader('/content/sample_data/data/', glob='**/*.txt')
documents = loader.load() # 将数据转成 document 对象,每个文件会作为一个 document
text_splitter = CharacterTextSplitter(chunk_size=500, chunk_overlap=0)
split_docs = text_splitter.split_documents(documents) # 切割加载的 document
docsearch = Pinecone.from_texts([t.page_content for t in split_docs], embeddings, index_name=index_name) # 持久化数据到pinecone