Milvus Retriever híbrido de búsqueda
Visión general
La búsqueda híbrida combina los puntos fuertes de diferentes paradigmas de búsqueda para mejorar la precisión y la solidez de la recuperación. Aprovecha las capacidades tanto de la búsqueda de vectores densos como de la búsqueda de vectores dispersos, así como combinaciones de múltiples estrategias de búsqueda de vectores densos, garantizando una recuperación completa y precisa para diversas consultas.
Este diagrama ilustra el escenario de búsqueda híbrida más común, que es la búsqueda híbrida densa + dispersa. En este caso, los candidatos se recuperan utilizando tanto la similitud semántica vectorial como la concordancia precisa de palabras clave. Los resultados de estos métodos se fusionan, se vuelven a clasificar y se pasan a un LLM para generar la respuesta final. Este enfoque equilibra la precisión y la comprensión semántica, lo que lo hace muy eficaz para diversos escenarios de consulta.
Además de la búsqueda híbrida densa + dispersa, las estrategias híbridas también pueden combinar varios modelos de vectores densos. Por ejemplo, un modelo de vectores densos puede especializarse en captar matices semánticos, mientras que otro se centra en incrustaciones contextuales o representaciones específicas del dominio. Al fusionar los resultados de estos modelos y volver a clasificarlos, este tipo de búsqueda híbrida garantiza un proceso de recuperación más matizado y consciente del contexto.
La integración de LangChain Milvus proporciona una forma flexible de implementar la búsqueda híbrida, soporta cualquier número de campos vectoriales, y cualquier modelo personalizado de incrustación densa o dispersa, lo que permite a LangChain Milvus adaptarse de forma flexible a varios escenarios de uso de la búsqueda híbrida, y al mismo tiempo es compatible con otras capacidades de LangChain.
En este tutorial, comenzaremos con el caso más común denso + disperso, y luego introduciremos cualquier número de enfoques generales de uso de búsqueda híbrida.
El MilvusCollectionHybridSearchRetriever, que es otra implementación de búsqueda híbrida con Milvus y LangChain, está a punto de ser obsoleto. Por favor, utilice el enfoque de este documento para implementar la búsqueda híbrida porque es más flexible y compatible con LangChain.
Requisitos previos
Antes de ejecutar este cuaderno, asegúrate de tener instaladas las siguientes dependencias:
$ pip install --upgrade --quiet langchain langchain-core langchain-community langchain-text-splitters langchain-milvus langchain-openai bs4 pymilvus[model] #langchain-voyageai
Si estás utilizando Google Colab, para habilitar las dependencias recién instaladas, puede que necesites reiniciar el runtime (haz clic en el menú "Runtime" en la parte superior de la pantalla, y selecciona "Restart session" en el menú desplegable).
Utilizaremos los modelos de OpenAI. Deberá preparar las variables de entorno OPENAI_API_KEY
de OpenAI.
import os
os.environ["OPENAI_API_KEY"] = "sk-***********"
Especifique su servidor Milvus URI
(y opcionalmente el TOKEN
). Para saber cómo instalar e iniciar el servidor Milvus siga esta guía.
URI = "http://localhost:19530"
# TOKEN = ...
Prepare algunos documentos de ejemplo, que son resúmenes de historias ficticias clasificadas por tema o 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"},
),
]
Incrustación densa + Incrustación dispersa
Opción 1(Recomendada): incrustación densa + función incorporada Milvus BM25
Utilice la incrustación densa + la función incorporada Milvus BM25 para ensamblar la instancia del almacén de vectores de recuperación híbrida.
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="Strong",
drop_old=True,
)
- Cuando utilice
BM25BuiltInFunction
, tenga en cuenta que la búsqueda de texto completo está disponible en Milvus Standalone y Milvus Distributed, pero no en Milvus Lite, aunque está en la hoja de ruta para su futura inclusión. También estará disponible en Zilliz Cloud (Milvus totalmente gestionado) en breve. Póngase en contacto con support@zilliz.com para obtener más información.
En el código anterior, definimos una instancia de BM25BuiltInFunction
y la pasamos al objeto Milvus
. BM25BuiltInFunction
es una clase envolvente ligera para Function
en Milvus. Podemos usarla con OpenAIEmbeddings
para inicializar una instancia de almacén vectorial Milvus de búsqueda híbrida densa + dispersa.
BM25BuiltInFunction
no requiere que el cliente pase corpus o entrenamiento, todo se procesa automáticamente en el extremo del servidor Milvus, por lo que los usuarios no necesitan preocuparse de ningún vocabulario y corpus. Además, los usuarios también pueden personalizar el analizador para implementar el procesamiento de texto personalizado en el BM25.
Para obtener más información sobre BM25BuiltInFunction
, consulte Búsqueda de texto completo y Uso de la búsqueda de texto completo con LangChain y Milvus.
Opción 2: Utilizar la incrustación dispersa LangChain densa y personalizada
Puede heredar la clase BaseSparseEmbedding
de langchain_milvus.utils.sparse
, e implementar los métodos embed_query
y embed_documents
para personalizar el proceso de incrustación dispersa. Esto le permite personalizar cualquier método de incrustación dispersa basado en estadísticas de frecuencia de términos (por ejemplo, BM25) o en redes neuronales (por ejemplo, SPADE).
He aquí un ejemplo:
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)
Tenemos una clase de demostración BM25SparseEmbedding
heredada de BaseSparseEmbedding
en langchain_milvus.utils.sparse
. Puede pasarla a la lista de incrustación de inicialización de la instancia de Milvus vector store al igual que otras clases de incrustación densa 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="Strong",
drop_old=True,
)
Aunque esta es una forma de utilizar BM25, requiere que los usuarios gestionen el corpus para las estadísticas de frecuencia de términos. En su lugar, recomendamos utilizar la función integrada en BM25 (opción 1), ya que se encarga de todo en el lado del servidor Milvus. Esto elimina la necesidad de que los usuarios se preocupen de la gestión del corpus o de la formación de un vocabulario. Para obtener más información, consulte la sección Uso de la búsqueda de texto completo con LangChain y Milvus.
Definir múltiples campos vectoriales arbitrarios
Al inicializar el almacén de vectores de Milvus, puede pasar la lista de incrustaciones (y también la lista de funciones incorporadas en el futuro) para implementar la recuperación multidireccional, y luego volver a clasificar estos candidatos. He aquí un ejemplo:
# 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="Strong",
drop_old=True,
)
vectorstore.vector_fields
['dense1', 'dense2', 'sparse']
En este ejemplo, tenemos tres campos vectoriales. Entre ellos, sparse
se utiliza como campo de salida para BM25BuiltInFunction
, mientras que los otros dos, dense1
y dense2
, se asignan automáticamente como campos de salida para los dos modelos OpenAIEmbeddings
(en función del orden).
Especificar los parámetros de índice para campos multivectoriales
Por defecto, los tipos de índice de cada campo vectorial se determinarán automáticamente por el tipo de incrustación o función incorporada. Sin embargo, también puede especificar el tipo de índice de cada campo vectorial para optimizar el rendimiento de la búsqueda.
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="Strong",
drop_old=True,
)
vectorstore.vector_fields
['dense1', 'dense2', 'sparse']
Por favor, mantenga el orden de la lista de parámetros de índice coherente con el orden de vectorstore.vector_fields
para evitar confusiones.
Nueva clasificación de los candidatos
Después de la primera etapa de recuperación, necesitamos reordenar los candidatos para obtener un mejor resultado. Puede elegir WeightedRanker o RRFRanker en función de sus necesidades. Puede consultar el Reranking para obtener más información.
He aquí un ejemplo de reranking ponderado:
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,
)
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.")]
Aquí tiene un ejemplo de RRF reranking:
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.")]
Si no pasa ningún parámetro sobre el rerank, se utiliza por defecto la estrategia de rerank ponderado medio.
Uso de la búsqueda híbrida y la reordenación en RAG
En el escenario de la RAG, el enfoque más frecuente para la búsqueda híbrida es la recuperación densa + dispersa, seguida de la reordenación. El siguiente ejemplo muestra un código directo de principio a fin.
Preparación de los datos
Utilizamos Langchain WebBaseLoader para cargar documentos de fuentes web y dividirlos en trozos utilizando 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#')
Cargar el documento en el almacén vectorial Milvus
Como en la introducción anterior, inicializamos y cargamos los documentos preparados en el almacén vectorial de Milvus, que contiene dos campos vectoriales: dense
es para la incrustación OpenAI y sparse
es para la función 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,
)
Creación de la cadena RAG
Preparamos la instancia LLM y el prompt, y luego los combinamos en una cadena RAG utilizando el Lenguaje de Expresión 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)
Utiliza el LCEL (Lenguaje de Expresión LangChain) para construir una cadena 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()
Invoca la cadena RAG con una pregunta específica y recupera la respuesta
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.'
Enhorabuena. Ha construido una cadena RAG de búsqueda híbrida (vector denso + función bm25 dispersa) potenciada por Milvus y LangChain.