es混合检索与langchain检索增强

检索增强全流程实践

Langchain Retriever

  • MultiQueryRetriever,利用llm为问题生成3个意思接近的问题,根据3个问题检索相关文档并全部返回。

  • MultiVectorRetriever,当同一个文档在向量库中因存储不同向量而存在多条记录时,通过id进行去重。代码实现非常简单,不知道有什么用,为什么不存储为多个向量字段而不是多个文档,可能是因为langchain的vectorstore只支持检索一个向量字段。

    class MultiVectorRetriever(BaseRetriever):
        """Retrieve from a set of multiple embeddings for the same document."""
    
        vectorstore: VectorStore
        """The underlying vectorstore to use to store small chunks
        and their embedding vectors"""
        docstore: BaseStore[str, Document]
        """The storage layer for the parent documents"""
        id_key: str = "doc_id"
        search_kwargs: dict = Field(default_factory=dict)
        """Keyword arguments to pass to the search function."""
    
        def _get_relevant_documents(
            self, query: str, *, run_manager: CallbackManagerForRetrieverRun
        ) -> List[Document]:
            """Get documents relevant to a query.
            Args:
                query: String to find relevant documents for
                run_manager: The callbacks handler to use
            Returns:
                List of relevant documents
            """
            sub_docs = self.vectorstore.similarity_search(query, **self.search_kwargs)
            # We do this to maintain the order of the ids that are returned
            ids = []
            for d in sub_docs:
                if d.metadata[self.id_key] not in ids:
                    ids.append(d.metadata[self.id_key])
            docs = self.docstore.mget(ids)
            return [d for d in docs if d is not None]
    
  • Contextual compression,检索出来的文档可能包含很多无用的上下文信息,直接扔给llm会造成干扰并且增加响应时间,使用上下文压缩的方式提高上下文和问题的相关性。这种思路的关键在于如何压缩上下文,langchain提供了几种实现。

    • DocumentCompressorPipeline,流水线,需要提供一系列BaseDocumentTransformer或者BaseDocumentCompressor

    • LLMChainExtractor,利用llm提取有效上下文信息。

    • LLMChainFilter,利用llm去除无关上下文信息。

    • CohereRerank,调用Cohere Rerank API重排评分。

    • EmbeddingsFilter,又来一遍向量相似度度量?
      在这里插入图片描述

  • Ensemble Retriever,整合一系列retrieve的结果,再进行rrf,常见的就是全文检索+向量检索+rrf倒数排序融合,es混合搜索就是这个流程,但是rrf需要许可证。

  • Parent Document Retriever,通常切分文档时,我们既希望文档短一点,这样可以全文检索和向量检索的准确度,但是文档太短包含的信息可能太狭隘,对于关联多条文档的问题无法提供信息充分的上下文。该Retriever将文档拆分为较小的块,同时每块关联其父文档的id,小块用于提高检索准确度,大块父文档用于返回上下文,再考虑上文提到的上下文压缩,或许是一个提高检索精度的好办法。

  • SelfQueryRetriever,由LLM将自然语言转化成查询语句。
    在这里插入图片描述

  • TimeWeightedVectorStoreRetriever,记录上一次访问文档的时间,越久越少访问的文档评分越低。

    semantic_similarity + (1.0 - decay_rate) ^ hours_passed
    
  • WebResearchRetriever,从网络检索内容以提供上下文。

    langchain.retrievers包下还有很多检索增强的类。

Elasticsearch向量检索

dense_vector类型

不支持聚合和排序,不能在嵌套字段中,否则无法被索引。

{
  "mappings": {
    "properties": {
      "my_vector": {
        "type": "dense_vector",
        "dims": 1023,
        "index": true,
        "similarity": "dot_product" 
      }
    }
  }
}

支持的属性

  • element_type

    • float,默认,4字节浮点数。
    • byte,1字节整数,-218~127。
  • dims,必填字段,向量维数,不能超过2048。

  • index,默认为fasle,设置为true支持kNN搜索。

  • similarity,相似度度量算法,如果index为true,该字段必须设置。

    • l2_norm,欧式距离
    • dot_product,点积
    • cosine,余弦相似度

    建议归一化向量,选择dot_product方式,提高检索效率。

  • index_options,可选字段

    • type,必填字段,kNN算法,目前只支持hnsw。
    • m,必填字段,hnsw中每个节点的邻节点数,默认值16。
    • es_construction,必填字段,汇聚每个新节点的邻接点时,跟踪的节点数量,默认是100。

kNN检索

通过相似性度量搜索k个最邻近向量,es比较新的版本已经自带模型,不需要在应用程序中编码文本字段和查询语句,elastic云支持自己上传模型,但是似乎这个功能不免费?

近似kNN

消耗资源少,响应快,牺牲精确度

注意事项
  • dot_product还是cosine

    建议归一化向量,选择dot_product方式,提高检索效率;cosine无需归一化,可直接计算。

  • 足够的内存

    Elasticsearch使用HNSW算法进行近似KNN搜索。HNSW是一种基于图的算法,向量保存在内存中才能有效工作。所以需要保证数据节点有足够的内存保存向量数据和索引结构。要查看向量数据的大小,es提供了API分析索引磁盘使用情况。从经验来说(使用默认的HNSW配置),使用float类型,占用字节近似num_vectors * 4 *(num_dimensions + 12)。当使用byte类型,所需的空间近似num_vector *(num_dimensions + 12)。这里所指的空间是文件系统缓存,而不是Java堆。

  • 预热文件系统缓存

    当es启动时,文件系统缓存为空,开始的检索可能会比较慢,可以预加载索引数据来建立缓存,但是如果加载太多数据到文件系统缓存,可能减慢检索速度。

    近似kNN检索需要的数据文件后缀

    • vec,向量值
    • vex,HNSW图
    • vem,元数据
  • 降低向量维度

    向量维数越大计算越耗资源,有的模型可以选择不同的编码维度,也可以使用降维方法减少维度,在准确度和检索速度之间做取舍。

  • 不要返回向量字段

    加载向量数据返回耗费时间,可以使用_source从返回结果中排除这个字段,关于如何排除字段以及性能影响,可以查看ElasticSearch中_source、store_fields、doc_values性能比较es官方文档

  • 还有几个点涉及到es的底层数据结构,需要一定的调优能力,可以查看官方文档

kNN选项
  • field,必填,向量字段名

  • filter,可选,query dsl的filter,返回向量检索和filter过滤条件都满足的文档。

  • k,必填,返回的邻近向量数,必须小于num-candidates

  • num-candidates,相当于每个分片上的k,es从每个分片上检索num_candidates个向量结果,再根据评分汇总返回k个最终结果。增大该值可以提高检索结果的准确度。

  • query_vector,可选,要检索的向量,维度必须和创建mapping时一致。

  • query_vector_builder,可选,指定模型的相关信息,将编码文本为向量的任务交给es。query_vectorquery_vector_builder,必须填且只能填一个。

  • similarity,可选,float类型,判定检索命中的一个阈值,与所选的距离度量方式有关,不是文档分数_score,通过该值对文档进行评分,并应用boost(如果有)。

    如果是l2_norm,距离需要小于等于similarity

    如果是cosine或者dot_product,相似度需要大于等于similarity

  • boost,计算评分时的系数,knn可以和query一起使用,两者的结果合并再计算评分,boost*评分再求和。

精确kNN

查询所有文档计算相似度以保证结果的准确度,可以先使用query过滤一部分文档,再进行精确kNN提高检索速度。

如果确定字段不需要进行近似kNN,可以将字段的index属性设置为false,可以提升索引速度。

精确kNN使用script_score查询

{
  "query": {
    "script_score": {
      "query" : {
        "bool" : {
          "filter" : {
            "range" : {
              "price" : {
                "gte": 1000
              }
            }
          }
        }
      },
      "script": {
        "source": "cosineSimilarity(params.queryVector, 'product-vector') + 1.0",
        "params": {
          "queryVector": [-0.5, 90.0, -10, 14.8, -156.0]
        }
      }
    }
  }
}

语义检索

es所谓的语义检索即是自带的模型以及向量检索,es提供了一些NLP模型,包括密集向量和稀疏向量的,如果进行中文搜索,需要自己上传配置模型。提高语义检索的通常步骤是选择一个效果较好的通用模型,积累语料,对模型进行训练,优化效果。但训练的成本并不低,为了提供一个通用简便的使用,es提供了一种稀疏向量编码器ELSER,开箱即用,尽量减少微调,目前仅适用于英语。

简单来说,语义检索就是将模型编码的工作也交给了es,不需要我们提前编码好再发送给es进行距离计算。包括部署模型、创建向量字段、生成嵌入向量、检索数据四个步骤。这个功能不免费,具体可以查看官方文档

倒数融合排序(RRF)

rrf用于将多个检索结果集合并为一个按照rrf_score排序的结果集。通常情况下组合多种排名方法比单个排名具有更好的效果,例如全文检索BM25排名 和密集向量相似度排名。 本质上就是将多个有序结果集组合成一个单一的有序结果集。 理论上可以将每个结果集的分数归一化(因为原始分数在完全不同的范围内),然后进行线性组合,根据每个排名的分数加权和排序最终结果集,这种方法需要提供正确的权重,了解每种方法得分的统计分布,并能根据实际情况优化权重,这并不简单。

另一种方法是rrf算法,相比优化每种排序方法的权重,rrf相对简单粗暴,不利用相关分数,而仅靠排名计算,绕开了不同方法得分统计分布的影响。rrf_score的计算公式如下
R R F s c o r e ( d ∈ D ) = ∑ r ∈ R 1 k + r ( d ) RRFscore(d \in D) = \sum_{r \in R} \frac{1}{k+r(d)} RRFscore(dD)=rRk+r(d)1

  • D,查询的文档结果集,例如BM25排序后的结果集,向量检索后的结果集。
  • R,查询的文档结果集的排序序列, 1 , 2 , 3... N 1,2,3...N 1,2,3...N r ( d ) r(d) r(d)表示文档在结果集中的排名。
  • k,每个查询的单个结果集中的文档对最终排名结果集的影响程度。 较高的值表示排名较低的文档具有更大的影响力。 此值必须大于或等于 1。默认为 60。
    计算过程就是对每组结果集的每个文档,为计算rrf_score并累加,最后按rrf_score排序文档。
    假定k=10,以下是一个排序示例。
文档BM25相关性排名密集向量相关性排名BM25 rrf_score密集向量rrf_score按rff_score总分排名
A13 1 1 + 10 = 1 11 \frac{1}{1+10}=\frac{1}{11} 1+101=111 1 3 + 10 = 1 13 \frac{1}{3+10}=\frac{1}{13} 3+101=1311
B-2- 1 10 + 2 = 1 12 \frac{1}{10+2}=\frac{1}{12} 10+21=1213
C31 1 3 + 10 = 1 13 \frac{1}{3+10}=\frac{1}{13} 3+101=131 1 1 + 10 = 1 11 \frac{1}{1+10}=\frac{1}{11} 1+101=1111
D24 1 2 + 10 = 1 12 \frac{1}{2+10}=\frac{1}{12} 2+101=121 1 4 + 10 = 1 14 \frac{1}{4+10}=\frac{1}{14} 4+101=1412

在示例中,文档A和C最后总分相同,原始的rrf_score计算没有考虑不同检索计算得分时的权重,假定我们认为密集向量排名比BM25排名更准确,那么可以将密集向量的权重调大一些,那么示例数据rrf排序后第一名的文档就是C了。

lanchainEnsembleRetriever中,有添加了权重计算的完整rrf代码实现,

"""
Perform weighted Reciprocal Rank Fusion on multiple rank lists.
You can find more details about RRF here:
https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf

Args:
    doc_lists: A list of rank lists, where each rank list contains unique items.

Returns:
    list: The final aggregated list of items sorted by their weighted RRF
            scores in descending order.
"""
if len(doc_lists) != len(self.weights):
    raise ValueError(
        "Number of rank lists must be equal to the number of weights."
    )

# Create a union of all unique documents in the input doc_lists
all_documents = set()
for doc_list in doc_lists:
    for doc in doc_list:
        all_documents.add(doc.page_content)

# Initialize the RRF score dictionary for each document
rrf_score_dic = {doc: 0.0 for doc in all_documents}

# Calculate RRF scores for each document
for doc_list, weight in zip(doc_lists, self.weights):
    for rank, doc in enumerate(doc_list, start=1):
        rrf_score = weight * (1 / (rank + self.c))
        # 以文档id做key会更好,langchain的Document只有metadata字典和page_content
        rrf_score_dic[doc.page_content] += rrf_score

# Sort documents by their RRF scores in descending order
sorted_documents = sorted(
    rrf_score_dic.keys(), key=lambda x: rrf_score_dic[x], reverse=True
)

# Map the sorted page_content back to the original document objects
page_content_to_doc_map = {
    doc.page_content: doc for doc_list in doc_lists for doc in doc_list
}
sorted_docs = [
    page_content_to_doc_map[page_content] for page_content in sorted_documents
]

return sorted_docs

Langchain整合Elasticsearch

self._embeddings = HuggingFaceBgeEmbeddings(model_name=Configuration.EMBEDDING_MODEL,
                                                    model_kwargs={'device': Configuration.DEVICE},
                                                    encode_kwargs={'normalize_embeddings': True})
self._es_client = Elasticsearch(hosts=f'http://{Configuration.ES_HOST}:{Configuration.ES_PORT}',
                                    basic_auth=(Configuration.ES_USER, Configuration.ES_PASSWORD))

self._es_vector_store = ElasticsearchStore(index_name=Configuration.INDEX_NAME, embedding=self._embeddings,
                                                   es_connection=self._es_client,
                                                   distance_strategy=DistanceStrategy.DOT_PRODUCT,
                                                   strategy=ApproxRetrievalStrategy(hybrid=True, rrf=True)) # 混合检索,rrf重排

如果没有提前建立索引,es会自动创建索引,增加向量字段,其他的字段均由es自动推断类型,如果需要使用全文检索,需要创建索引时指定分词器,同时检索时只能检索一个向量字段和一个文本字段,部分参数无法灵活定义,并不是太好用。建议自己手搓添加文档和搜索文档的过程。

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值