Recuperador de pesquisa híbrida Milvus
A pesquisa híbrida combina os pontos fortes de diferentes paradigmas de pesquisa para melhorar a precisão e a robustez da recuperação. Aproveita as capacidades da pesquisa de vectores densos e da pesquisa de vectores esparsos, bem como as combinações de várias estratégias de pesquisa de vectores densos, garantindo uma recuperação abrangente e precisa para diversas consultas.
Este diagrama ilustra o cenário de pesquisa híbrida mais comum, que é a pesquisa híbrida densa + esparsa. Neste caso, os candidatos são recuperados utilizando a semelhança de vectores semânticos e a correspondência precisa de palavras-chave. Os resultados destes métodos são fundidos, reavaliados e transmitidos a um LLM para gerar a resposta final. Esta abordagem equilibra precisão e compreensão semântica, tornando-a altamente eficaz para diversos cenários de consulta.
Para além da pesquisa híbrida densa + esparsa, as estratégias híbridas também podem combinar vários modelos de vectores densos. Por exemplo, um modelo de vectores densos pode especializar-se na captura de nuances semânticas, enquanto outro se concentra em incorporações contextuais ou representações específicas do domínio. Ao fundir os resultados destes modelos e ao reordená-los, este tipo de pesquisa híbrida assegura um processo de recuperação mais matizado e sensível ao contexto.
A integração do LangChain Milvus fornece uma forma flexível de implementar a pesquisa híbrida, suporta qualquer número de campos vectoriais e quaisquer modelos de incorporação densos ou esparsos personalizados, o que permite ao LangChain Milvus adaptar-se de forma flexível a vários cenários de utilização da pesquisa híbrida e, ao mesmo tempo, ser compatível com outras capacidades do LangChain.
Neste tutorial, começaremos com o caso mais comum denso + esparso e, em seguida, apresentaremos várias abordagens gerais de uso de pesquisa híbrida.
O MilvusCollectionHybridSearchRetriever, que é outra implementação de busca híbrida com Milvus e LangChain, está prestes a ser descontinuado. Por favor, use a abordagem deste documento para implementar a pesquisa híbrida, pois ela é mais flexível e compatível com LangChain.
Pré-requisitos
Antes de executar este notebook, certifique-se de ter as seguintes dependências instaladas:
$ pip install --upgrade --quiet langchain langchain-core langchain-community langchain-text-splitters langchain-milvus langchain-openai bs4 pymilvus[model] #langchain-voyageai
Se estiver a utilizar o Google Colab, para ativar as dependências que acabou de instalar, poderá ter de reiniciar o tempo de execução (clique no menu "Tempo de execução" na parte superior do ecrã e selecione "Reiniciar sessão" no menu pendente).
Vamos utilizar os modelos do OpenAI. Deve preparar as variáveis de ambiente OPENAI_API_KEY do OpenAI.
import os
os.environ["OPENAI_API_KEY"] = "sk-***********"
Especifique o seu servidor Milvus URI (e, opcionalmente, o TOKEN). Para saber como instalar e iniciar o servidor Milvus, siga este guia.
URI = "http://localhost:19530"
# TOKEN = ...
Prepare alguns documentos de exemplo, que são resumos de histórias de ficção categorizados por tema ou género.
from langchain_core.documents import Document
docs = [
Document(
page_content="In 'The Whispering Walls' by Ava Moreno, a young journalist named Sophia uncovers a decades-old conspiracy hidden within the crumbling walls of an ancient mansion, where the whispers of the past threaten to destroy her own sanity.",
metadata={"category": "Mystery"},
),
Document(
page_content="In 'The Last Refuge' by Ethan Blackwood, a group of survivors must band together to escape a post-apocalyptic wasteland, where the last remnants of humanity cling to life in a desperate bid for survival.",
metadata={"category": "Post-Apocalyptic"},
),
Document(
page_content="In 'The Memory Thief' by Lila Rose, a charismatic thief with the ability to steal and manipulate memories is hired by a mysterious client to pull off a daring heist, but soon finds themselves trapped in a web of deceit and betrayal.",
metadata={"category": "Heist/Thriller"},
),
Document(
page_content="In 'The City of Echoes' by Julian Saint Clair, a brilliant detective must navigate a labyrinthine metropolis where time is currency, and the rich can live forever, but at a terrible cost to the poor.",
metadata={"category": "Science Fiction"},
),
Document(
page_content="In 'The Starlight Serenade' by Ruby Flynn, a shy astronomer discovers a mysterious melody emanating from a distant star, which leads her on a journey to uncover the secrets of the universe and her own heart.",
metadata={"category": "Science Fiction/Romance"},
),
Document(
page_content="In 'The Shadow Weaver' by Piper Redding, a young orphan discovers she has the ability to weave powerful illusions, but soon finds herself at the center of a deadly game of cat and mouse between rival factions vying for control of the mystical arts.",
metadata={"category": "Fantasy"},
),
Document(
page_content="In 'The Lost Expedition' by Caspian Grey, a team of explorers ventures into the heart of the Amazon rainforest in search of a lost city, but soon finds themselves hunted by a ruthless treasure hunter and the treacherous jungle itself.",
metadata={"category": "Adventure"},
),
Document(
page_content="In 'The Clockwork Kingdom' by Augusta Wynter, a brilliant inventor discovers a hidden world of clockwork machines and ancient magic, where a rebellion is brewing against the tyrannical ruler of the land.",
metadata={"category": "Steampunk/Fantasy"},
),
Document(
page_content="In 'The Phantom Pilgrim' by Rowan Welles, a charismatic smuggler is hired by a mysterious organization to transport a valuable artifact across a war-torn continent, but soon finds themselves pursued by deadly assassins and rival factions.",
metadata={"category": "Adventure/Thriller"},
),
Document(
page_content="In 'The Dreamwalker's Journey' by Lyra Snow, a young dreamwalker discovers she has the ability to enter people's dreams, but soon finds herself trapped in a surreal world of nightmares and illusions, where the boundaries between reality and fantasy blur.",
metadata={"category": "Fantasy"},
),
]
Incorporação densa + incorporação esparsa
Opção 1 (recomendada): incorporação densa + função incorporada Milvus BM25
Utilizar a incorporação densa + a função incorporada Milvus BM25 para montar a instância de armazenamento do vetor de recuperação híbrido.
from langchain_milvus import Milvus, BM25BuiltInFunction
from langchain_openai import OpenAIEmbeddings
vectorstore = Milvus.from_documents(
documents=docs,
embedding=OpenAIEmbeddings(),
builtin_function=BM25BuiltInFunction(), # output_field_names="sparse"),
vector_field=["dense", "sparse"],
connection_args={
"uri": URI,
},
consistency_level="Bounded", # Supported values are (`"Strong"`, `"Session"`, `"Bounded"`, `"Eventually"`). See https://milvus.io/docs/tune_consistency.md#Consistency-Level for more details.
drop_old=False,
)
- Quando utilizar
BM25BuiltInFunction, tenha em atenção que a pesquisa de texto integral está disponível no Milvus Standalone e no Milvus Distributed, mas não no Milvus Lite, embora esteja prevista a sua inclusão no futuro. Também estará disponível no Zilliz Cloud (Milvus totalmente gerido) em breve. Entre em contacto com support@zilliz.com para obter mais informações.
No código acima, definimos uma instância de BM25BuiltInFunction e passamos para o objeto Milvus. BM25BuiltInFunction é uma classe de wrapper leve para Function em Milvus. Podemos utilizá-la com OpenAIEmbeddings para inicializar uma instância do armazenamento de vectores Milvus de pesquisa híbrida densa + esparsa.
BM25BuiltInFunction O Milvus não requer que o cliente passe corpus ou treino, todos são processados automaticamente no servidor Milvus, pelo que os utilizadores não precisam de se preocupar com qualquer vocabulário e corpus. Além disso, os utilizadores também podem personalizar o analisador para implementar o processamento de texto personalizado no BM25.
Para mais informações sobre BM25BuiltInFunction, consulte Full-Text-Search e Using Full-Text Search with LangChain and Milvus.
Opção 2: Utilizar a incorporação esparsa densa e personalizada da LangChain
É possível herdar a classe BaseSparseEmbedding de langchain_milvus.utils.sparse e implementar os métodos embed_query e embed_documents para personalizar o processo de incorporação esparsa. Isto permite-lhe personalizar qualquer método de incorporação esparsa com base em estatísticas de frequência de termos (por exemplo, BM25) ou redes neurais (por exemplo, SPADE).
Aqui está um exemplo:
from typing import Dict, List
from langchain_milvus.utils.sparse import BaseSparseEmbedding
class MyCustomEmbedding(BaseSparseEmbedding): # inherit from BaseSparseEmbedding
def __init__(self, model_path): ... # code to init or load model
def embed_query(self, query: str) -> Dict[int, float]:
... # code to embed query
return { # fake embedding result
1: 0.1,
2: 0.2,
3: 0.3,
# ...
}
def embed_documents(self, texts: List[str]) -> List[Dict[int, float]]:
... # code to embed documents
return [ # fake embedding results
{
1: 0.1,
2: 0.2,
3: 0.3,
# ...
}
] * len(texts)
Temos uma classe de demonstração BM25SparseEmbedding herdada de BaseSparseEmbedding em langchain_milvus.utils.sparse. Pode passá-la para a lista de incorporação de inicialização da instância de armazenamento de vectores Milvus, tal como outras classes de incorporação densa da langchain.
# BM25SparseEmbedding is inherited from BaseSparseEmbedding
from langchain_milvus.utils.sparse import BM25SparseEmbedding
embedding1 = OpenAIEmbeddings()
corpus = [doc.page_content for doc in docs]
embedding2 = BM25SparseEmbedding(
corpus=corpus
) # pass in corpus to initialize the statistics
vectorstore = Milvus.from_documents(
documents=docs,
embedding=[embedding1, embedding2],
vector_field=["dense", "sparse"],
connection_args={
"uri": URI,
},
consistency_level="Bounded", # Supported values are (`"Strong"`, `"Session"`, `"Bounded"`, `"Eventually"`). See https://milvus.io/docs/tune_consistency.md#Consistency-Level for more details.
drop_old=False,
)
Embora esta seja uma forma de utilizar o BM25, requer que os utilizadores gerem o corpus para obter estatísticas de frequência de termos. Em vez disso, recomendamos a utilização da função incorporada BM25 (Opção 1), uma vez que trata de tudo no lado do servidor Milvus. Isto elimina a necessidade de os utilizadores se preocuparem com a gestão do corpus ou com a formação de um vocabulário. Para mais informações, consulte a secção Utilizar a pesquisa de texto integral com LangChain e Milvus.
Definir vários campos vectoriais arbitrários
Ao inicializar o repositório de vectores do Milvus, pode passar a lista de embeddings (e também a lista de funções incorporadas no futuro) para implementar a recuperação multi-vias e, em seguida, classificar estes candidatos. Eis um exemplo:
# from langchain_voyageai import VoyageAIEmbeddings
embedding1 = OpenAIEmbeddings(model="text-embedding-ada-002")
embedding2 = OpenAIEmbeddings(model="text-embedding-3-large")
# embedding3 = 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], # embedding3],
builtin_function=BM25BuiltInFunction(output_field_names="sparse"),
# `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="Bounded", # Supported values are (`"Strong"`, `"Session"`, `"Bounded"`, `"Eventually"`). See https://milvus.io/docs/tune_consistency.md#Consistency-Level for more details.
drop_old=False,
)
vectorstore.vector_fields
['dense1', 'dense2', 'sparse']
Neste exemplo, temos três campos vectoriais. Entre eles, sparse é utilizado como campo de saída para BM25BuiltInFunction, enquanto os outros dois, dense1 e dense2, são automaticamente atribuídos como campos de saída para os dois modelos OpenAIEmbeddings (com base na ordem).
Especificar os parâmetros de índice para campos multi-vectoriais
Por predefinição, os tipos de índice de cada campo vetorial serão automaticamente determinados pelo tipo de incorporação ou função incorporada. No entanto, também pode especificar o tipo de índice para cada campo de vetor para otimizar o desempenho da pesquisa.
dense_index_param_1 = {
"metric_type": "COSINE",
"index_type": "HNSW",
}
dense_index_param_2 = {
"metric_type": "IP",
"index_type": "HNSW",
}
sparse_index_param = {
"metric_type": "BM25",
"index_type": "AUTOINDEX",
}
vectorstore = Milvus.from_documents(
documents=docs,
embedding=[embedding1, embedding2],
builtin_function=BM25BuiltInFunction(output_field_names="sparse"),
index_params=[dense_index_param_1, dense_index_param_2, sparse_index_param],
vector_field=["dense1", "dense2", "sparse"],
connection_args={
"uri": URI,
},
consistency_level="Bounded", # Supported values are (`"Strong"`, `"Session"`, `"Bounded"`, `"Eventually"`). See https://milvus.io/docs/tune_consistency.md#Consistency-Level for more details.
drop_old=False,
)
vectorstore.vector_fields
['dense1', 'dense2', 'sparse']
Mantenha a ordem da lista de parâmetros de índice consistente com a ordem de vectorstore.vector_fields para evitar confusões.
Classificar novamente os candidatos
Após a primeira fase de recuperação, é necessário classificar novamente os candidatos para obter um melhor resultado. Pode escolher WeightedRanker ou RRFRanker consoante os seus requisitos. Pode consultar a secção Reranking para obter mais informações.
Eis um exemplo de classificação ponderada:
vectorstore = Milvus.from_documents(
documents=docs,
embedding=OpenAIEmbeddings(),
builtin_function=BM25BuiltInFunction(),
vector_field=["dense", "sparse"],
connection_args={
"uri": URI,
},
consistency_level="Bounded", # Supported values are (`"Strong"`, `"Session"`, `"Bounded"`, `"Eventually"`). See https://milvus.io/docs/tune_consistency.md#Consistency-Level for more details.
drop_old=False,
)
query = "What are the novels Lila has written and what are their contents?"
vectorstore.similarity_search(
query, k=1, ranker_type="weighted", ranker_params={"weights": [0.6, 0.4]}
)
[Document(metadata={'pk': 454646931479252186, 'category': 'Heist/Thriller'}, page_content="In 'The Memory Thief' by Lila Rose, a charismatic thief with the ability to steal and manipulate memories is hired by a mysterious client to pull off a daring heist, but soon finds themselves trapped in a web of deceit and betrayal.")]
Aqui está um exemplo de classificação RRF:
vectorstore.similarity_search(query, k=1, ranker_type="rrf", ranker_params={"k": 100})
[Document(metadata={'category': 'Heist/Thriller', 'pk': 454646931479252186}, page_content="In 'The Memory Thief' by Lila Rose, a charismatic thief with the ability to steal and manipulate memories is hired by a mysterious client to pull off a daring heist, but soon finds themselves trapped in a web of deceit and betrayal.")]
Se não passar quaisquer parâmetros sobre a classificação, a estratégia de classificação média ponderada é utilizada por predefinição.
Utilizar a pesquisa híbrida e a reclassificação no RAG
No cenário do RAG, a abordagem mais prevalecente para a pesquisa híbrida é a recuperação densa + esparsa, seguida da reclassificação. O exemplo seguinte demonstra um código simples de ponta a ponta.
Preparar os dados
Utilizamos o Langchain WebBaseLoader para carregar documentos a partir de fontes Web e dividi-los em partes utilizando o 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#')
Carregar o documento no armazenamento vetorial Milvus
Tal como na introdução acima, inicializamos e carregamos os documentos preparados para o armazenamento vetorial Milvus, que contém dois campos vectoriais: dense é para a incorporação OpenAI e sparse é para a função BM25.
vectorstore = Milvus.from_documents(
documents=docs,
embedding=OpenAIEmbeddings(),
builtin_function=BM25BuiltInFunction(),
vector_field=["dense", "sparse"],
connection_args={
"uri": URI,
},
consistency_level="Bounded", # Supported values are (`"Strong"`, `"Session"`, `"Bounded"`, `"Eventually"`). See https://milvus.io/docs/tune_consistency.md#Consistency-Level for more details.
drop_old=False,
)
Construir a cadeia RAG
Preparamos a instância LLM e o prompt e, em seguida, combinamo-los num pipeline RAG utilizando a linguagem de expressão LangChain.
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)
Utilize a LCEL (Linguagem de Expressão LangChain) para construir uma cadeia 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()
Invocar a cadeia RAG com uma pergunta específica e obter a resposta
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 perform these tasks.'
Parabéns! Construiu uma cadeia RAG de pesquisa híbrida (vetor denso + função bm25 esparsa) alimentada por Milvus e LangChain.