赞
踩
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
包下还有很多检索增强的类。
不支持聚合和排序,不能在嵌套字段中,否则无法被索引。
{
"mappings": {
"properties": {
"my_vector": {
"type": "dense_vector",
"dims": 1023,
"index": true,
"similarity": "dot_product"
}
}
}
}
支持的属性
element_type
dims,必填字段,向量维数,不能超过2048。
index,默认为fasle,设置为true支持kNN搜索。
similarity,相似度度量算法,如果index为true,该字段必须设置。
建议归一化向量,选择dot_product方式,提高检索效率。
index_options,可选字段
通过相似性度量搜索k个最邻近向量,es比较新的版本已经自带模型,不需要在应用程序中编码文本字段和查询语句,elastic云支持自己上传模型,但是似乎这个功能不免费?
消耗资源少,响应快,牺牲精确度
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检索需要的数据文件后缀
降低向量维度
向量维数越大计算越耗资源,有的模型可以选择不同的编码维度,也可以使用降维方法减少维度,在准确度和检索速度之间做取舍。
不要返回向量字段
加载向量数据返回耗费时间,可以使用_source
从返回结果中排除这个字段,关于如何排除字段以及性能影响,可以查看ElasticSearch中_source、store_fields、doc_values性能比较,es官方文档。
还有几个点涉及到es的底层数据结构,需要一定的调优能力,可以查看官方文档
field
,必填,向量字段名
filter
,可选,query dsl的filter,返回向量检索和filter过滤条件都满足的文档。
k
,必填,返回的邻近向量数,必须小于num-candidates
。
num-candidates
,相当于每个分片上的k
,es从每个分片上检索num_candidates
个向量结果,再根据评分汇总返回k
个最终结果。增大该值可以提高检索结果的准确度。
query_vector
,可选,要检索的向量,维度必须和创建mapping时一致。
query_vector_builder
,可选,指定模型的相关信息,将编码文本为向量的任务交给es。query_vector
和query_vector_builder
,必须填且只能填一个。
similarity
,可选,float类型,判定检索命中的一个阈值,与所选的距离度量方式有关,不是文档分数_score,通过该值对文档进行评分,并应用boost
(如果有)。
如果是l2_norm
,距离需要小于等于similarity
如果是cosine
或者dot_product
,相似度需要大于等于similarity
。
boost
,计算评分时的系数,knn可以和query一起使用,两者的结果合并再计算评分,boost*评分再求和。
查询所有文档计算相似度以保证结果的准确度,可以先使用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_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(d∈D)=r∈R∑k+r(d)1
文档 | BM25相关性排名 | 密集向量相关性排名 | BM25 rrf_score | 密集向量rrf_score | 按rff_score总分排名 |
---|---|---|---|---|---|
A | 1 | 3 | 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=131 | 1 |
B | - | 2 | - | 1 10 + 2 = 1 12 \frac{1}{10+2}=\frac{1}{12} 10+21=121 | 3 |
C | 3 | 1 | 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=111 | 1 |
D | 2 | 4 | 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=141 | 2 |
在示例中,文档A和C最后总分相同,原始的rrf_score计算没有考虑不同检索计算得分时的权重,假定我们认为密集向量排名比BM25排名更准确,那么可以将密集向量的权重调大一些,那么示例数据rrf排序后第一名的文档就是C了。
在lanchain
的EnsembleRetriever
中,有添加了权重计算的完整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
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自动推断类型,如果需要使用全文检索,需要创建索引时指定分词器,同时检索时只能检索一个向量字段和一个文本字段,部分参数无法灵活定义,并不是太好用。建议自己手搓添加文档和搜索文档的过程。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。