使用 LangChain 和 Milvus 進行全文檢索
全文檢索是一種傳統的方法,透過直接比對文字中的關鍵字,來檢索包含特定詞彙或短語的文件。它根據相關性對結果進行排序,通常由詞彙頻率和接近度等因素決定。語意搜尋擅長於理解意圖和上下文,而全文搜尋則提供精確的關鍵字匹配,使其成為有價值的補充工具。BM25 演算法是全文搜尋常用的排序方法,在檢索增強世代 (Retrieval-Augmented Generation, RAG) 中尤其有用。
自Milvus 2.5 以來,透過 Sparse-BM25 方法,將 BM25 演算法表示為稀疏向量,即可原生支援全文搜尋。Milvus 接受原始文字作為輸入,並自動將其轉換為儲存於指定欄位的稀疏向量,省去了手動產生稀疏嵌入的需求。
LangChain 與 Milvus 的整合也引進了這項功能,簡化了將全文檢索融入 RAG 應用程式的過程。透過結合全文檢索與密集向量的語意檢索,您可以達成一種混合方法,同時利用密集嵌入的語意上下文與字詞比對的精確關鍵字相關性。這種整合可以增強搜尋系統的精確度、相關性和使用者體驗。
本教學將介紹如何使用 LangChain 和 Milvus 在您的應用程式中實作全文搜尋。
全文檢索在 Milvus Standalone 和 Milvus Distributed 中可用,但在 Milvus Lite 中不可用,儘管它在未來加入的路線圖上。不久之後,Zilliz Cloud (完全管理的 Milvus) 也會提供此功能。如需詳細資訊,請聯絡support@zilliz.com。
先決條件
在執行本筆記本之前,請確認您已經安裝以下的相依性:
$ pip install --upgrade --quiet langchain langchain-core langchain-community langchain-text-splitters langchain-milvus langchain-openai bs4 #langchain-voyageai
如果您使用的是 Google Colab,為了啟用剛安裝的相依性,您可能需要重新啟動執行時(點選畫面上方的「Runtime」功能表,並從下拉式功能表中選擇「Restart session」)。
我們將使用 OpenAI 的模型。您應該準備OpenAI 的環境變數OPENAI_API_KEY
。
import os
os.environ["OPENAI_API_KEY"] = "sk-***********"
指定您的 Milvus 伺服器URI
(也可選擇TOKEN
)。關於如何安裝和啟動 Milvus 伺服器,請參考本指南。
URI = "http://localhost:19530"
# TOKEN = ...
準備一些範例文件:
from langchain_core.documents import Document
docs = [
Document(page_content="I like this apple", metadata={"category": "fruit"}),
Document(page_content="I like swimming", metadata={"category": "sport"}),
Document(page_content="I like dogs", metadata={"category": "pets"}),
]
使用 BM25 功能初始化
混合搜尋
對於全文檢索,Milvus VectorStore 接受一個builtin_function
參數。透過這個參數,您可以傳入BM25BuiltInFunction
的一個實例。這與語意搜尋不同,語意搜尋通常會傳入密集的嵌入到VectorStore
、
以下是在 Milvus 中使用 OpenAI dense embedding 進行語意搜尋,以及使用 BM25 進行全文搜尋的混合搜尋簡單範例:
from langchain_milvus import Milvus, BM25BuiltInFunction
from langchain_openai import OpenAIEmbeddings
vectorstore = Milvus.from_documents(
documents=docs,
embedding=OpenAIEmbeddings(),
builtin_function=BM25BuiltInFunction(),
# `dense` is for OpenAI embeddings, `sparse` is the output field of BM25 function
vector_field=["dense", "sparse"],
connection_args={
"uri": URI,
},
consistency_level="Strong",
drop_old=True,
)
在上面的程式碼中,我們定義了BM25BuiltInFunction
的一個實例,並將它傳給Milvus
物件。BM25BuiltInFunction
是一個輕量級的包裝類,用於 Function
的一個輕量級包裝類。
您可以在BM25BuiltInFunction
的參數中指定這個函式的輸入和輸出欄位:
input_field_names
(str):輸入欄位的名稱,預設值是 。它表示這個函式讀取哪一個欄位作為輸入。text
output_field_names
(str):輸出欄位的名稱,預設為 。它表示這個函式將計算結果輸出到哪一個欄位。sparse
請注意,在上述的 Milvus 初始化參數中,我們也指定了vector_field=["dense", "sparse"]
。由於sparse
欄位會被當作是由BM25BuiltInFunction
定義的輸出欄位,因此其他dense
欄位會被自動指定為 OpenAIEmbeddings 的輸出欄位。
實際上,尤其是結合多個嵌入式或函式時,我們建議明確指定每個函式的輸入和輸出欄位,以避免歧義。
在下面的範例中,我們明確指定BM25BuiltInFunction
的輸入和輸出欄位,讓內建函式的欄位一目了然。
# from langchain_voyageai import VoyageAIEmbeddings
embedding1 = OpenAIEmbeddings(model="text-embedding-ada-002")
embedding2 = OpenAIEmbeddings(model="text-embedding-3-large")
# embedding2 = VoyageAIEmbeddings(model="voyage-3") # You can also use embedding from other embedding model providers, e.g VoyageAIEmbeddings
vectorstore = Milvus.from_documents(
documents=docs,
embedding=[embedding1, embedding2],
builtin_function=BM25BuiltInFunction(
input_field_names="text", output_field_names="sparse"
),
text_field="text", # `text` is the input field name of BM25BuiltInFunction
# `sparse` is the output field name of BM25BuiltInFunction, and `dense1` and `dense2` are the output field names of embedding1 and embedding2
vector_field=["dense1", "dense2", "sparse"],
connection_args={
"uri": URI,
},
consistency_level="Strong",
drop_old=True,
)
vectorstore.vector_fields
['dense1', 'dense2', 'sparse']
在這個範例中,我們有三個向量欄位。其中,sparse
用作BM25BuiltInFunction
的輸出欄位,而另外兩個,dense1
和dense2
,則自動指定為兩個OpenAIEmbeddings
模型的輸出欄位(根據順序)。
如此一來,您就可以定義多個向量欄位,並為它們指定不同的嵌入或函數組合,以執行混合搜尋。
執行混合搜尋時,我們只需傳入查詢文字,並選擇性地設定 topK 和 reranker 參數。vectorstore
範例會自動處理向量嵌入和內建函數,最後再使用 reranker 來精煉結果。搜尋過程的底層實作細節對使用者是隱藏的。
vectorstore.similarity_search(
"Do I like apples?", k=1
) # , ranker_type="weighted", ranker_params={"weights":[0.3, 0.3, 0.4]})
[Document(metadata={'category': 'fruit', 'pk': 454646931479251897}, page_content='I like this apple')]
關於混合搜尋的更多資訊,您可以參考混合搜尋介紹和這篇LangChain Milvus 混合搜尋教學。
無嵌入的BM25搜尋
如果您只想使用 BM25 函式執行全文搜尋,而不使用任何基於 embedding 的語意搜尋,您可以設定 embedding 參數為None
,並只保留指定為 BM25 函式實例的builtin_function
。向量欄位只有「稀疏」欄位。舉例來說
vectorstore = Milvus.from_documents(
documents=docs,
embedding=None,
builtin_function=BM25BuiltInFunction(
output_field_names="sparse",
),
vector_field="sparse",
connection_args={
"uri": URI,
},
consistency_level="Strong",
drop_old=True,
)
vectorstore.vector_fields
['sparse']
自訂分析器
分析器是全文檢索中不可或缺的工具,它可將句子分割成字元,並執行詞彙分析,如詞幹分析和停止詞移除。分析器通常是特定語言的。您可以參考本指南以瞭解更多關於 Milvus 分析器的資訊。
Milvus 支援兩種類型的分析器:內建分析器和自訂分析器。在預設情況下,BM25BuiltInFunction
會使用標準的內建分析器,這是最基本的分析器,會用標點符號來標記文字。
如果您想使用不同的分析器或自訂分析器,可以在BM25BuiltInFunction
初始化時傳入analyzer_params
參數。
analyzer_params_custom = {
"tokenizer": "standard",
"filter": [
"lowercase", # Built-in filter
{"type": "length", "max": 40}, # Custom filter
{"type": "stop", "stop_words": ["of", "to"]}, # Custom filter
],
}
vectorstore = Milvus.from_documents(
documents=docs,
embedding=OpenAIEmbeddings(),
builtin_function=BM25BuiltInFunction(
output_field_names="sparse",
enable_match=True,
analyzer_params=analyzer_params_custom,
),
vector_field=["dense", "sparse"],
connection_args={
"uri": URI,
},
consistency_level="Strong",
drop_old=True,
)
我們可以看看 Milvus 套件的 schema,並確保自訂的分析器設定正確。
vectorstore.col.schema
{'auto_id': True, 'description': '', 'fields': [{'name': 'text', 'description': '', 'type': <DataType.VARCHAR: 21>, 'params': {'max_length': 65535, 'enable_match': True, 'enable_analyzer': True, 'analyzer_params': {'tokenizer': 'standard', 'filter': ['lowercase', {'type': 'length', 'max': 40}, {'type': 'stop', 'stop_words': ['of', 'to']}]}}}, {'name': 'pk', 'description': '', 'type': <DataType.INT64: 5>, 'is_primary': True, 'auto_id': True}, {'name': 'dense', 'description': '', 'type': <DataType.FLOAT_VECTOR: 101>, 'params': {'dim': 1536}}, {'name': 'sparse', 'description': '', 'type': <DataType.SPARSE_FLOAT_VECTOR: 104>, 'is_function_output': True}, {'name': 'category', 'description': '', 'type': <DataType.VARCHAR: 21>, 'params': {'max_length': 65535}}], 'enable_dynamic_field': False, 'functions': [{'name': 'bm25_function_de368e79', 'description': '', 'type': <FunctionType.BM25: 1>, 'input_field_names': ['text'], 'output_field_names': ['sparse'], 'params': {}}]}
更多的概念細節,例如analyzer
,tokenizer
,filter
,enable_match
,analyzer_params
,請參考分析器文件。
在 RAG 中使用混合搜索和重排
我們已經了解如何在 LangChain 和 Milvus 中使用基本的 BM25 內建函式。讓我們介紹一個使用混合搜尋和重新排列的最佳化 RAG 實作。
此圖顯示混合檢索與重新排序的流程,結合了用於關鍵字比對的 BM25 和用於語意檢索的向量搜尋。來自這兩種方法的結果會合併、重新排序,並傳送到 LLM 以產生最終答案。
混合搜尋平衡了精確度與語意理解,針對不同的查詢提高了精確度與穩健性。它利用 BM25 全文檢索和向量檢索來擷取候選項目,確保能同時進行語意、上下文感知和精確的檢索。
讓我們從一個範例開始。
準備資料
我們使用 Langchain WebBaseLoader 從網路來源載入文件,並使用 RecursiveCharacterTextSplitter 將文件分割成小塊。
import bs4
from langchain_community.document_loaders import WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Create a WebBaseLoader instance to load documents from web sources
loader = WebBaseLoader(
web_paths=(
"https://lilianweng.github.io/posts/2023-06-23-agent/",
"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/",
),
bs_kwargs=dict(
parse_only=bs4.SoupStrainer(
class_=("post-content", "post-title", "post-header")
)
),
)
# Load documents from web sources using the loader
documents = loader.load()
# Initialize a RecursiveCharacterTextSplitter for splitting text into chunks
text_splitter = RecursiveCharacterTextSplitter(chunk_size=2000, chunk_overlap=200)
# Split the documents into chunks using the text_splitter
docs = text_splitter.split_documents(documents)
# Let's take a look at the first document
docs[1]
Document(metadata={'source': 'https://lilianweng.github.io/posts/2023-06-23-agent/'}, page_content='Fig. 1. Overview of a LLM-powered autonomous agent system.\nComponent One: Planning#\nA complicated task usually involves many steps. An agent needs to know what they are and plan ahead.\nTask Decomposition#\nChain of thought (CoT; Wei et al. 2022) has become a standard prompting technique for enhancing model performance on complex tasks. The model is instructed to “think step by step” to utilize more test-time computation to decompose hard tasks into smaller and simpler steps. CoT transforms big tasks into multiple manageable tasks and shed lights into an interpretation of the model’s thinking process.\nTree of Thoughts (Yao et al. 2023) extends CoT by exploring multiple reasoning possibilities at each step. It first decomposes the problem into multiple thought steps and generates multiple thoughts per step, creating a tree structure. The search process can be BFS (breadth-first search) or DFS (depth-first search) with each state evaluated by a classifier (via a prompt) or majority vote.\nTask decomposition can be done (1) by LLM with simple prompting like "Steps for XYZ.\\n1.", "What are the subgoals for achieving XYZ?", (2) by using task-specific instructions; e.g. "Write a story outline." for writing a novel, or (3) with human inputs.\nAnother quite distinct approach, LLM+P (Liu et al. 2023), involves relying on an external classical planner to do long-horizon planning. This approach utilizes the Planning Domain Definition Language (PDDL) as an intermediate interface to describe the planning problem. In this process, LLM (1) translates the problem into “Problem PDDL”, then (2) requests a classical planner to generate a PDDL plan based on an existing “Domain PDDL”, and finally (3) translates the PDDL plan back into natural language. Essentially, the planning step is outsourced to an external tool, assuming the availability of domain-specific PDDL and a suitable planner which is common in certain robotic setups but not in many other domains.\nSelf-Reflection#')
將文件載入 Milvus 向量儲存庫
如上文的介紹,我們將準備好的文件初始化並載入 Milvus 向量存儲,其中包含兩個向量領域:dense
是 OpenAI 嵌入,sparse
是 BM25 函數。
vectorstore = Milvus.from_documents(
documents=docs,
embedding=OpenAIEmbeddings(),
builtin_function=BM25BuiltInFunction(),
vector_field=["dense", "sparse"],
connection_args={
"uri": URI,
},
consistency_level="Strong",
drop_old=True,
)
建立 RAG 鏈
我們準備好 LLM 實例和提示,然後用 LangChain Expression Language 將它們結合為 RAG 管道。
from langchain_core.runnables import RunnablePassthrough
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
# Initialize the OpenAI language model for response generation
llm = ChatOpenAI(model_name="gpt-4o", temperature=0)
# Define the prompt template for generating AI responses
PROMPT_TEMPLATE = """
Human: You are an AI assistant, and provides answers to questions by using fact based and statistical information when possible.
Use the following pieces of information to provide a concise answer to the question enclosed in <question> tags.
If you don't know the answer, just say that you don't know, don't try to make up an answer.
<context>
{context}
</context>
<question>
{question}
</question>
The response should be specific and use statistics or numbers when possible.
Assistant:"""
# Create a PromptTemplate instance with the defined template and input variables
prompt = PromptTemplate(
template=PROMPT_TEMPLATE, input_variables=["context", "question"]
)
# Convert the vector store to a retriever
retriever = vectorstore.as_retriever()
# Define a function to format the retrieved documents
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
使用 LCEL(LangChain Expression Language) 建立 RAG 鏈。
# Define the RAG (Retrieval-Augmented Generation) chain for AI response generation
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# rag_chain.get_graph().print_ascii()
以特定的問題來啟動 RAG 鏈,並擷取回應
query = "What is PAL and PoT?"
res = rag_chain.invoke(query)
res
'PAL (Program-aided Language models) and PoT (Program of Thoughts prompting) are approaches that involve using language models to generate programming language statements to solve natural language reasoning problems. This method offloads the solution step to a runtime, such as a Python interpreter, allowing for complex computation and reasoning to be handled externally. PAL and PoT rely on language models with strong coding skills to effectively generate and execute these programming statements.'
恭喜您!您已經建立了一個由 Milvus 和 LangChain 支援的混合(密集向量 + 稀疏 bm25 函數)搜尋 RAG 鍊。