Faiss

简单记录一下对faiss的体验。这里使用两种方式体验:基于 langchain、不基于 langchain

GitHub官网:GitHub - facebookresearch/faiss: A library for efficient similarity search and clustering of dense vectors.

官网:Welcome to Faiss Documentation — Faiss documentation

参考 Langchain-Chatchat

怎么知道的faiss呢!源于Langchain-Chatchat这个项目,查看他是怎样实现基于知识库的问答的,下面的代码主要摘自或者参考该项目。

当时使用的Langchain-Chatchat版本为:v0.2.5 、 v0.2.6

知识库问答逻辑

Langchain-Chatchat中知识库问答的调用逻辑大体如下:

  1. server\api.py > api: /chat/knowledge_base_chat ->
  2. server\chat\knowledge_base_chat.py > def knowledge_base_chat ->
  3. server\knowledge_base\kb_doc_api.py > def search_docs ->
  4. server\knowledge_base\kb_service\base.py > def search_docs(调用各个向量库对应的实现)->
  5. server\knowledge_base\kb_service\faiss_kb_service.py > def do_search

在最后一步中你会看到vs.similarity_search_with_score(query, k=top_k, score_threshold=score_threshold),这就是Langchain封装Faiss暴露的查询方法了;至于他是怎么得到的 vs (也就是 FAISS),文件路径为server\knowledge_base\kb_cache\faiss_cache.py,过程如下图:

Langchain-Chatchat 项目中 load_vector_store

这里参考的是上图中的这条路线:本地如果没有,则新建一个;如果存在,则读取本地的。上图中如何读取本地的已经很明确,至于如果新创建一个还要看下图:

Langchain-Chatchat 项目中,本地没有,新建一个 vector_store

知识库新建和添加文档逻辑

上面的章节简单介绍了一下根据知识库问答的逻辑,那么知识库是怎么创建的,上传的文档又是怎么添加到知识库的?这个章节将介绍一下。

Langchain-Chatchat中创建知识库的调用逻辑大体如下:

  1. server\api.py > api: /knowledge_base/create_knowledge_base ->
  2. server\knowledge_base\kb_api.py > def create_kb ->
  3. server\knowledge_base\kb_service\base.py > def create_kb(调用各个向量库对应的实现)->
  4. server\knowledge_base\kb_service\faiss_kb_service.py > def do_create_kb > def load_vector_store ->
  5. server\knowledge_base\kb_cache\faiss_cache.py > class KBFaissPool > def load_vector_store

这里的最后一步又回到了第一张图中的获取vector_store

Langchain-Chatchat中上传文档并添加到向量库的调用逻辑大体如下:

  1. server\api.py > api: /knowledge_base/upload_docs ->
  2. server\knowledge_base\kb_doc_api.py > def upload_docs > def update_docs ->
  3. server\knowledge_base\kb_service\base.py > def update_doc ->
  4. server\knowledge_base\kb_service\base.py > def add_doc(调用各个向量库对应的实现)->

    方法中在保存到向量库之前还需要拆分文档,如下:

    1. docs = kb_file.file2text()(位于 add_doc 方法中)->
    2. server\knowledge_base\utils.py > def file2text
      1. docs = self.file2docs()(文档对应的Loader读取文档)
      2. self.splited_docs = self.docs2texts()(拆分)
  5. server\knowledge_base\kb_service\faiss_kb_service.py > def do_add_doc
    1. ids = vs.add_documents(docs)
    2. vs.save_local(self.vs_path)

过程就这样吧!是不是过几天再看就直接😵😵😵

自己练练

点击查看知识库中的文档

说明:开始使用的txt文档格式、UTF-8 编码,浏览器打开时乱码,所以改成了 json

第一版

Langchain-Chatchat,在自己动手之前先膜拜一下。代码如下:

上面代码想要运行起来还需要安装这些依赖(2024-03-25): pip install langchain-community langchain_text_splitters faiss-cpu sentence-transformers chardet。最初的时候还没有langchain-community、langchain-core等,只有langchain。安装后依赖之后就可以执行了,运行结果如下:

参考 Langchain-Chatchat 项目,相似度搜索结果

不同格式的文档对应不同的loader,到底有哪些,你可以查看langchain_community\document_loaders\__init__.py

方法 getSplitDocs 的返回值格式:[Document(page_content='文档拆分之后的块内容', metadata={'source': 'D:/llm/0-my/xiaodu114-1.txt', 'start_index': 0})]

vector_store.similarity_search_with_score 参数介绍,k : 返回的文档个数;score_threshold : 阈值。返回值得分:分数越小,相似度越高。这里将阈值控制在了0.5,搜索“你知道 xiaodu114 吗”时,如果不控制,那么吴京和关羽的内容也会返回回来

指定文档搜索

对于本地知识库,我们应该会有N多文档,那么如果我们只想根据其中的一个或者几个文档搜索,该如何处理?查看similarity_search_with_score方法的源码,发现有filter参数,支持传入方法或者字典

指定一个文档
#   下面是新增加的代码
similar_docs_2 = vector_store.similarity_search_with_score(query, k=10, score_threshold = 0.5,
                                                filter = {"source": 'D:/llm/0-my/xiaodu114-1.txt'})
print("相似度查询之指定一个文档搜索结束。结果如下:\n"+str(similar_docs_2))
            

参考 Langchain 项目,相似度查询之指定一个文档搜索

指定多个文档
#   下面是新增加的代码
def filter1(metadata):
    if metadata["source"] in ["D:/llm/0-my/xiaodu114-3.txt","D:/llm/0-my/xiaodu114-2.txt"]: return True
similar_docs3 = vector_store.similarity_search_with_score(query, k=10, score_threshold = 0.5,filter=filter1)
print("相似度查询之指定多个文档搜索结束。结果如下:\n"+str(similar_docs3))
            

参考 Langchain 项目,相似度查询之指定多个文档搜索

小结

从查看langchain的源码发现FAISS应该并不支持指定文档查询(或者说传入filter),是搜索到结果之后再次过滤来实现的,如下图:

Langchain 项目,相似度查询实现

批量搜索

langchain封装的FAISS的查询接口是单个的,是不是我没有找到批量的?网上查了一下,FAISS是支持批量查询的。这里首先在langchain基础上扩展方法,来支持批量查询。新建一个langchain_faiss_extend.py的文件,代码如下:

点击查看代码

下面看一下如何使用

#   引入本地依赖使用,这里指的是 langchain_faiss_extend.py
import sys
sys.path.append(".")

#   删掉上面示例代码中的这个
from langchain_community.vectorstores.faiss import FAISS
#   使用引入已经添加批量搜索的 FAISS
from langchain_faiss_extend import FAISS

#   调用批量查询
query1 = "介绍一下 吴京"
query2 = "你知道 xiaodu114 吗"
similar_docs = vector_store.batch_similarity_search_with_score([query1, query2], k=10, score_threshold = 0.5)
print("【批量】相似度查询结束。结果如下:\n"+str(similar_docs))
            

下面在看一下测试截图:

Langchain 项目添加扩展方法,相似度查询批量实现

另起炉灶

这里不再依赖langchain,开始单干了,当然该参考的时候还得参考啊。

第一版

第一次脱离langchain的怀抱,尚有些羞涩……后面在慢慢完善。

点击查看代码

不依赖 Langchain 项目,相似度查询批量结果

图中可以看到“xiaodu114”相关的几个文档的得分和上面使用langchain的得分基本相等

上面示例代码中创建索引还可以使用faiss.index_factory来替代,例如:

#   这是上面示例代码中的代码
index = faiss.IndexFlatL2(len(init_embedding))

#   你还可以这样
index = faiss.index_factory(len(init_embedding), 'Flat' ,faiss.METRIC_L2)
                

暂时还没有找到不同的索引和faiss.index_factory方法参数的对应关系