從零到一帶你實(shí)戰(zhàn)RAG混合檢索 原創(chuàng)
在之前的文章中,我們探討了混合檢索的概念以及其后續(xù)的重新排序(rerank)和重組(reorder)操作。今天,我們將從實(shí)踐角度解析如何執(zhí)行混合檢索。下圖是混合檢索的流程:
BM25
眾所周知,混合檢索主要通過(guò)關(guān)鍵詞匹配來(lái)確定可能的答案,接著結(jié)合語(yǔ)義匹配以進(jìn)一步提升答案的精確度。BM25就是其中一種常見(jiàn)的關(guān)鍵詞搜索技術(shù)。
BM25就像一個(gè)智能的匹配工具,在我們使用搜索系統(tǒng)時(shí),它能幫助我們找到最相關(guān)的信息。BM25如何做到這一點(diǎn)呢?它主要看兩個(gè)方面:首先,它會(huì)檢查我們的查詢(xún)?cè)~在某份文檔中出現(xiàn)了多少次;其次,它還會(huì)看這個(gè)詞在所有文檔中出現(xiàn)的頻率。如果一個(gè)詞在特定文檔中經(jīng)常出現(xiàn),但在其他地方很少見(jiàn),那么這個(gè)詞對(duì)這篇文檔來(lái)說(shuō)就非常重要,BM25會(huì)認(rèn)為這篇文檔與我們的查詢(xún)非常匹配。此外,BM25模型對(duì)于長(zhǎng)文檔和短文檔有一個(gè)平衡處理,防止因文檔長(zhǎng)度不同,而導(dǎo)致的詞頻偏差。
讓我們通過(guò)一個(gè)搜索引擎的簡(jiǎn)單例子來(lái)揭示BM25的運(yùn)作原理。假設(shè)你在一個(gè)搜索引擎中輸入了"香蕉面包",并且有三篇文章與這個(gè)查詢(xún)相關(guān):
- 文章A標(biāo)題為 "如何制作香蕉面包",全文1000字,并且"香蕉面包"一詞在其中出現(xiàn)10次。
- 文章B標(biāo)題為 "面包的種類(lèi)",全文500字,在其中"香蕉面包"一詞出現(xiàn)5次。
- 文章C標(biāo)題為 "水果和烘焙",全文2000字,其中"香蕉面包"一詞出現(xiàn)10次。
首先,BM25會(huì)考慮查詢(xún)?cè)~(即"香蕉面包")在每篇文章中出現(xiàn)的頻率。在文章A和C中,"香蕉面包"都出現(xiàn)了10次,而在文章B中出現(xiàn)了5次。因此,就頻率而言,文章A和C可能比文章B更相關(guān)。
然后,BM25會(huì)考慮文檔的長(zhǎng)度。盡管文章A和C中"香蕉面包"的出現(xiàn)次數(shù)相同,但文章A只有1000字,而文章C有2000字。這意味著,在文章A中,"香蕉面包"占據(jù)了更大比例,因此可能更相關(guān)。
因此,綜合考慮以上因素,搜索引擎可能會(huì)認(rèn)為文章A是對(duì)"香蕉面包"這個(gè)查詢(xún)最相關(guān)的結(jié)果,其次是文章B,再次是文章C。
langchain中封裝了BM25對(duì)應(yīng)的retriever的實(shí)現(xiàn),我們來(lái)看下:
from langchain.retrievers.bm25 import BM25Retriever
doc_list_1 = [
"I like apples",
"I like oranges",
"Apples and oranges are fruits",
]
# initialize the bm25 retriever and chroma retriever
bm25_retriever = BM25Retriever.from_texts(
doc_list_1, metadatas=[{"source": 1}] * len(doc_list_1)
)
bm25_retriever.k = 2
# [Document(page_cnotallow='I like apples', metadata={'source': 1}), Document(page_cnotallow='Apples and oranges are fruits', metadata={'source': 1})]
print(bm25_retriever.invoke("apples"))
# [Document(page_cnotallow='Apples and oranges are fruits', metadata={'source': 1}), Document(page_cnotallow='I like oranges', metadata={'source': 1})]
print(bm25_retriever.invoke("apple"))
上述代碼演示了我們?cè)诓樵?xún)"apples"時(shí),BM25根據(jù)詞頻能成功返回包含"apples"的兩句話。但是,當(dāng)我們將查詢(xún)改為"apple"時(shí),意外地得到了'I like oranges'這一結(jié)果??雌饋?lái)BM25對(duì)單詞的單復(fù)數(shù)形式十分"敏感",這也暴露出BM25的一個(gè)限制。而同樣是基于詞頻的elasticsearch卻能有效改善這一問(wèn)題。在使用elasticsearch替換bm25之前,我們先湊合著用bm25看下在langchain中如何做混合檢索。
from langchain.retrievers import EnsembleRetriever
from langchain.retrievers.bm25 import BM25Retriever
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
doc_list_1 = [
"I like apples",
"I like oranges",
"Apples and oranges are fruits",
]
# initialize the bm25 retriever and chroma retriever
bm25_retriever = BM25Retriever.from_texts(
doc_list_1, metadatas=[{"source": 1}] * len(doc_list_1)
)
bm25_retriever.k = 2
embedding = OpenAIEmbeddings()
chroma_vectorstore = Chroma.from_texts(
doc_list_1, embedding, metadatas=[{"source": 2}] * len(doc_list_1)
)
chroma_retriever = chroma_vectorstore.as_retriever(search_kwargs={"k": 2})
# initialize the ensemble retriever
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, chroma_retriever], weights=[0.5, 0.5]
)
在上面的代碼中,我們使用了基于關(guān)鍵詞搜索的BM25 retriever和基于向量檢索的chroma retriever, 然后使用了一個(gè)EnsembleRetriever對(duì)這兩個(gè)retriever搜索到的結(jié)果使用RFF算法進(jìn)行整合,整合后我們先來(lái)看看查詢(xún)"apples"的結(jié)果:
# [Document(page_cnotallow='I like apples', metadata={'source': 1}), Document(page_cnotallow='Apples and oranges are fruits', metadata={'source': 1})]
print(ensemble_retriever.invoke("apples"))
我們?cè)賮?lái)搜索一下"apple", 結(jié)果如下:
# [Document(page_cnotallow='Apples and oranges are fruits', metadata={'source': 1}), Document(page_cnotallow='I like oranges', metadata={'source': 1})]
print(bm25_retriever.invoke("apple"))
# [Document(page_cnotallow='I like apples', metadata={'source': 2}), Document(page_cnotallow='Apples and oranges are fruits', metadata={'source': 2})]
print(chroma_retriever.invoke("apple"))
# [Document(page_cnotallow='Apples and oranges are fruits', metadata={'source': 1}), Document(page_cnotallow='I like apples', metadata={'source': 2}), Document(page_cnotallow='I like oranges', metadata={'source': 1})]
print(ensemble_retriever.invoke("apple"))
可以看出,基于語(yǔ)義的搜索對(duì)單復(fù)數(shù)形式并不敏感,'I like apples'仍然被檢索到。整合后的EnsembleRetriever返回了更優(yōu)質(zhì)的結(jié)果,前兩個(gè)都是與"蘋(píng)果"相關(guān)的文檔。這也證明了,我們通過(guò)使用向量檢索成功地彌補(bǔ)了BM25這種關(guān)鍵詞搜索可能存在的短板。
RRF算法
RFF 是一種數(shù)據(jù)融合方法,常用于元搜索。元搜索是將來(lái)自多個(gè)不同源或檢索系統(tǒng)的搜索結(jié)果進(jìn)行融合的過(guò)程。RRF通過(guò)考慮每個(gè)列表中項(xiàng)目的排名和各個(gè)列表本身的可靠性進(jìn)行結(jié)果合并。
RRF算法的公式如下:
RRF(S) = Σ(1 / (60 + rank))
其中,S是待融合的集合,rank表示每一個(gè)條目在其列表中的排名。
以一個(gè)具體例子說(shuō)明:
假設(shè)我們有三個(gè)搜索系統(tǒng)A、B、C,它們分別產(chǎn)生以下排名:
- 系統(tǒng)A:[物品1, 物品2, 物品3]
- 系統(tǒng)B:[物品2, 物品1, 物品3]
- 系統(tǒng)C:[物品3, 物品1, 物品2]
針對(duì)物品1,它在系統(tǒng)A、B、C中的排名分別為1、2、2。其在RRF算法中的得分為:
1/(60+1) + 1/(60+2) + 1/(60+2) = 0.016129 + 0.016393 + 0.016393 = 0.048915
同樣地,我們可以計(jì)算物品2和物品3的RRF分?jǐn)?shù)。然后比較這些得分,得分最高的物品就被看作是綜合最優(yōu)的選擇。
下面是EnsembleRetriever中對(duì)數(shù)據(jù)融合的RRF算法核心實(shí)現(xiàn):
rrf_score: Dict[str, float] = defaultdict(float)
for doc_list, weight in zip(doc_lists, self.weights):
for rank, doc in enumerate(doc_list, start=1):
# self.c = 60
rrf_score[doc.page_content] += weight / (rank + self.c)
使用elasticsearch替換BM25模型
我們先通過(guò)下面的docker-compose.yml安裝elasticsearch
version: "2.3"
services:
elasticsearch:
image: elasticsearch:8.9.0
container_name: elasticsearch
environment:
- discovery.type=single-node
- ES_JAVA_OPTS=-Xms1g -Xmx1g
- xpack.security.enabled=false
volumes:
- ./es_data:/usr/share/elasticsearch/data
- ./plugins:/usr/share/elasticsearch/plugins
ports:
- 9200:9200
networks:
- elastic
kibana:
image: kibana:8.6.0
container_name: kibana
ports:
- 5601:5601
depends_on:
- elasticsearch
networks:
- elastic
networks:
elastic:
name: elastic
driver: bridge
這里我們只是做演示示例,所以安裝比較簡(jiǎn)單,沒(méi)有安裝證書(shū)和映射分詞器等等,elasticsearch安裝完之后,我們就可以往里面插入數(shù)據(jù)并進(jìn)行查詢(xún)了。在插入數(shù)據(jù)之前,我們先建好index, 這個(gè)index可以理解成向量庫(kù)的collection、mysql的table:
curl -X PUT "localhost:9200/my_index" -H 'Content-Type: application/json' -d'
{
"settings": {
"analysis": {
"analyzer": {
"english": {
"tokenizer": "standard",
"filter": ["lowercase", "english_stemmer"]
}
},
"filter": {
"english_stemmer": {
"type": "stemmer",
"language": "english"
}
}
}
},
"mappings": {
"properties": {
"text": {
"type": "text",
"analyzer": "english"
}
}
}
}
'
索引創(chuàng)建好之后,我們可以開(kāi)始插入數(shù)據(jù)。下面是一個(gè) Python 示例,展示了如何使用 ??langchain_elasticsearch?
? 模塊向 Elasticsearch 插入數(shù)據(jù),并進(jìn)行查詢(xún):
from langchain_elasticsearch import ElasticsearchStore, BM25Strategy
doc_list_1 = [
"I like apples",
"You like Apples",
"I like oranges",
"Apples and oranges are fruits",
]
db = ElasticsearchStore(
es_url="http://localhost:9200",
index_name="my_index",
strategy=BM25Strategy(),
)
# initialize the bm25 retriever and chroma retriever
db.add_texts(doc_list_1, metadatas=[{"source": 1}] * len(doc_list_1))
elasticsearch_retriever = db.as_retriever(search_kwargs={"k": 4})
# [Document(metadata={'source': 1}, page_cnotallow='I like apples'), Document(metadata={'source': 1}, page_cnotallow='You like Apples'), Document(metadata={'source': 1}, page_cnotallow='Apples and oranges are fruits')]
print(elasticsearch_retriever.invoke("apple"))
你會(huì)看到,當(dāng)我們查詢(xún) "apple" 時(shí),Elasticsearch 能夠返回所有包含 "apple" 的文檔。這展示了 Elasticsearch 相比單獨(dú)使用 BM25 算法的優(yōu)勢(shì)。此外,elasticsearch還支持模糊匹配和自動(dòng)補(bǔ)全功能,即使用戶(hù)輸入有誤,也能提供相關(guān)的搜索結(jié)果,感興趣的可以自行去研究,這里我們不做重點(diǎn)講解。
現(xiàn)在我們可以使用elasticsearch和chromadb來(lái)做混合檢索了,具體代碼如下:
from langchain_community.vectorstores import Chroma
from langchain.retrievers import EnsembleRetriever
from langchain_openai import OpenAIEmbeddings
from langchain_elasticsearch import ElasticsearchStore, BM25Strategy
doc_list_1 = [
"I like apples",
"You like Apples",
"I like oranges",
"Apples and oranges are fruits",
]
db = ElasticsearchStore(
es_url="http://localhost:9200",
index_name="my_index",
strategy=BM25Strategy(),
)
db.add_texts(doc_list_1, metadatas=[{"source": 1}] * len(doc_list_1))
elasticsearch_retriever = db.as_retriever(search_kwargs={"k": 4})
# elasticsearch_retriever: [Document(page_cnotallow='I like apples', metadata={'source': 1}), Document(page_cnotallow='You like Apples', metadata={'source': 1}), Document(page_cnotallow='Apples and oranges are fruits', metadata={'source': 1})]
print("elasticsearch_retriever: ", elasticsearch_retriever.invoke("apple"))
# initialize the bm25 retriever and chroma retriever
embedding = OpenAIEmbeddings()
chroma_vectorstore = Chroma.from_texts(
doc_list_1, embedding, metadatas=[{"source": 2}] * len(doc_list_1)
)
chroma_retriever = chroma_vectorstore.as_retriever(search_kwargs={"k": 4})
# chroma_retriever: [Document(page_cnotallow='I like apples', metadata={'source': 2}), Document(page_cnotallow='You like Apples', metadata={'source': 2}), Document(page_cnotallow='Apples and oranges are fruits', metadata={'source': 2}), Document(page_cnotallow='I like oranges', metadata={'source': 2})]
print("chroma_retriever: ", chroma_retriever.invoke("apple"))
# initialize the ensemble retriever
ensemble_retriever = EnsembleRetriever(
retrievers=[elasticsearch_retriever, chroma_retriever], weights=[0.5, 0.5]
)
# ensemble_retriever: [Document(page_cnotallow='I like apples', metadata={'source': 1}), Document(page_cnotallow='You like Apples', metadata={'source': 1}), Document(page_cnotallow='Apples and oranges are fruits', metadata={'source': 1}), Document(page_cnotallow='I like oranges', metadata={'source': 2})]
print("ensemble_retriever: ", ensemble_retriever.invoke("apple"))
實(shí)際上,Elasticsearch已經(jīng)提供了混合檢索功能。它允許在存儲(chǔ)文檔時(shí)同時(shí)存儲(chǔ)對(duì)應(yīng)的文本向量,在進(jìn)行搜索時(shí)只需指定為混合搜索模式即可啟用這項(xiàng)功能。但需注意,該功能屬于付費(fèi)服務(wù)。以下是相關(guān)示例代碼:
from langchain_openai import OpenAIEmbeddings
from langchain_elasticsearch import ElasticsearchStore, BM25Strategy, DenseVectorStrategy
doc_list_1 = [
"I like apples",
"You like Apples",
"I like oranges",
"Apples and oranges are fruits",
]
db = ElasticsearchStore(
es_url="http://localhost:9200",
index_name="my_index_1",
embedding = OpenAIEmbeddings(),
strategy=DenseVectorStrategy(hybrid=True),
)
db.add_texts(doc_list_1, metadatas=[{"source": 1}] * len(doc_list_1))
elasticsearch_retriever = db.as_retriever(search_kwargs={"k": 4})
print("elasticsearch_retriever: ", elasticsearch_retriever.invoke("apple"))
在這個(gè)例子中,我們?cè)O(shè)置了DenseVectorStrategy策略并開(kāi)啟了混合檢索模式(hybrid=True)。然而,此時(shí)我們遇到了一個(gè)報(bào)錯(cuò):
elasticsearch.AuthorizationException: AuthorizationException(403, 'security_exception', 'current license is non-compliant for [Reciprocal Rank Fusion (RRF)]')
這個(gè)錯(cuò)誤表示當(dāng)前的許可證并不包含“Reciprocal Rank Fusion (RRF)”功能,即Elasticsearch的混合檢索功能。如果我們只設(shè)置hybrid=False,那么就會(huì)使用Elasticsearch的基礎(chǔ)向量檢索功能,而不涉及任何付費(fèi)服務(wù)。
本文轉(zhuǎn)載自公眾號(hào)AI 博物院 作者:longyunfeigu
