Récupérateur de recherche hybride Milvus
La recherche hybride combine les forces de différents paradigmes de recherche pour améliorer la précision et la robustesse de la recherche. Elle exploite les capacités de la recherche vectorielle dense et de la recherche vectorielle éparse, ainsi que les combinaisons de plusieurs stratégies de recherche vectorielle dense, garantissant une recherche complète et précise pour diverses requêtes.
Ce diagramme illustre le scénario de recherche hybride le plus courant, à savoir la recherche hybride dense + sparse. Dans ce cas, les candidats sont recherchés en utilisant à la fois la similarité des vecteurs sémantiques et la correspondance précise des mots-clés. Les résultats de ces méthodes sont fusionnés, reclassés et transmis à un LLM pour générer la réponse finale. Cette approche équilibre la précision et la compréhension sémantique, ce qui la rend très efficace pour divers scénarios d'interrogation.
Outre la recherche hybride dense + clairsemée, les stratégies hybrides peuvent également combiner plusieurs modèles de vecteurs denses. Par exemple, un modèle de vecteur dense peut se spécialiser dans la capture des nuances sémantiques, tandis qu'un autre se concentre sur les enchâssements contextuels ou les représentations spécifiques à un domaine. En fusionnant les résultats de ces modèles et en les reclassant, ce type de recherche hybride garantit un processus de recherche plus nuancé et plus sensible au contexte.
L'intégration de LangChain Milvus fournit un moyen flexible d'implémenter la recherche hybride, elle supporte n'importe quel nombre de champs vectoriels, et n'importe quel modèle d'intégration dense ou éparse personnalisé, ce qui permet à LangChain Milvus de s'adapter de manière flexible à divers scénarios d'utilisation de la recherche hybride, tout en étant compatible avec d'autres capacités de LangChain.
Dans ce tutoriel, nous commencerons par le cas le plus courant dense + sparse, puis nous présenterons un certain nombre d'approches générales d'utilisation de la recherche hybride.
Le MilvusCollectionHybridSearchRetriever, qui est une autre implémentation de la recherche hybride avec Milvus et LangChain, est sur le point d'être obsolète. Veuillez utiliser l'approche décrite dans ce document pour mettre en œuvre la recherche hybride, car elle est plus flexible et compatible avec LangChain.
Conditions préalables
Avant d'exécuter ce notebook, assurez-vous que les dépendances suivantes sont installées :
$ pip install --upgrade --quiet langchain langchain-core langchain-community langchain-text-splitters langchain-milvus langchain-openai bs4 pymilvus[model] #langchain-voyageai
Si vous utilisez Google Colab, pour activer les dépendances qui viennent d'être installées, vous devrez peut-être redémarrer le runtime (cliquez sur le menu "Runtime" en haut de l'écran, et sélectionnez "Restart session" dans le menu déroulant).
Nous utiliserons les modèles d'OpenAI. Vous devez préparer les variables d'environnement OPENAI_API_KEY à partir d'OpenAI.
import os
os.environ["OPENAI_API_KEY"] = "sk-***********"
Spécifiez votre serveur Milvus URI (et éventuellement TOKEN). Pour savoir comment installer et démarrer le serveur Milvus, suivez ce guide.
URI = "http://localhost:19530"
# TOKEN = ...
Préparez quelques documents d'exemple, qui sont des résumés d'histoires fictives classées par thème ou par genre.
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"},
),
]
Encastrement dense + Encastrement clairsemé
Option 1 (recommandée) : incorporation dense + fonction intégrée Milvus BM25
Utilisez l'intégration dense + la fonction intégrée Milvus BM25 pour assembler l'instance de magasin de vecteurs de recherche hybride.
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,
)
- Si vous utilisez
BM25BuiltInFunction, veuillez noter que la recherche en texte intégral est disponible dans Milvus Standalone et Milvus Distributed, mais pas dans Milvus Lite, bien qu'elle figure sur la feuille de route en vue d'une inclusion future. Elle sera également bientôt disponible dans Zilliz Cloud (Milvus entièrement géré). Veuillez contacter support@zilliz.com pour plus d'informations.
Dans le code ci-dessus, nous définissons une instance de BM25BuiltInFunction et la passons à l'objet Milvus. BM25BuiltInFunction est une classe enveloppante légère pour Function dans Milvus. Nous pouvons l'utiliser avec OpenAIEmbeddings pour initialiser une instance de magasin de vecteurs Milvus à recherche hybride dense + éparse.
BM25BuiltInFunction Milvus n'exige pas du client qu'il transmette un corpus ou une formation, tout est traité automatiquement au niveau du serveur Milvus, de sorte que les utilisateurs n'ont pas besoin de se préoccuper d'un vocabulaire ou d'un corpus. En outre, les utilisateurs peuvent également personnaliser l'analyseur pour mettre en œuvre le traitement de texte personnalisé dans la BM25.
Pour plus d'informations sur BM25BuiltInFunction, veuillez vous référer à Full-Text-Search et Using Full-Text Search with LangChain and Milvus.
Option 2 : Utiliser l'intégration dense et personnalisée de LangChain (sparse embedding)
Vous pouvez hériter de la classe BaseSparseEmbedding à partir de langchain_milvus.utils.sparse et mettre en œuvre les méthodes embed_query et embed_documents pour personnaliser le processus d'incorporation éparse. Cela vous permet de personnaliser n'importe quelle méthode d'incorporation éparse basée sur les statistiques de fréquence des termes (par exemple BM25) ou les réseaux neuronaux (par exemple SPADE).
Voici un exemple :
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)
Nous avons une classe de démonstration BM25SparseEmbedding héritée de BaseSparseEmbedding dans langchain_milvus.utils.sparse. Vous pouvez la passer dans la liste d'intégration d'initialisation de l'instance du magasin de vecteurs Milvus, tout comme d'autres classes d'intégration dense 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,
)
Bien qu'il s'agisse d'une façon d'utiliser BM25, elle exige que les utilisateurs gèrent le corpus pour obtenir des statistiques sur la fréquence des termes. Nous recommandons d'utiliser plutôt la fonction intégrée de BM25 (Option 1), car elle gère tout du côté du serveur Milvus. Les utilisateurs n'ont donc pas à se préoccuper de la gestion du corpus ou de la formation d'un vocabulaire. Pour plus d'informations, veuillez vous référer à la section Utilisation de la recherche plein texte avec LangChain et Milvus.
Définir plusieurs champs vectoriels arbitraires
Lors de l'initialisation du magasin de vecteurs Milvus, vous pouvez transmettre la liste des embeddings (et, à l'avenir, la liste des fonctions intégrées) pour mettre en œuvre la recherche multi-voies, puis classer ces candidats. Voici un exemple :
# 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']
Dans cet exemple, nous avons trois champs de vecteurs. Parmi eux, sparse est utilisé comme champ de sortie pour BM25BuiltInFunction, tandis que les deux autres, dense1 et dense2, sont automatiquement assignés comme champs de sortie pour les deux modèles OpenAIEmbeddings (en fonction de l'ordre).
Spécifier les paramètres d'index pour les champs multi-vecteurs
Par défaut, les types d'index de chaque champ vectoriel sont automatiquement déterminés par le type d'intégration ou de fonction intégrée. Cependant, vous pouvez également spécifier le type d'index pour chaque champ vectoriel afin d'optimiser les performances de la recherche.
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']
Veillez à ce que l'ordre de la liste des paramètres d'index soit cohérent avec l'ordre de vectorstore.vector_fields afin d'éviter toute confusion.
Reclasser les candidats
Après la première étape de la recherche, nous devons reclasser les candidats pour obtenir un meilleur résultat. Vous pouvez choisir WeightedRanker ou RRFRanker en fonction de vos besoins. Vous pouvez vous référer au Reranking pour plus d'informations.
Voici un exemple de reclassement pondéré :
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.")]
Voici un exemple de reclassement 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.")]
Si vous ne fournissez aucun paramètre concernant le reranking, la stratégie de reranking pondéré moyen est utilisée par défaut.
Utilisation de la recherche hybride et du reclassement dans RAG
Dans le scénario de RAG, l'approche la plus répandue pour la recherche hybride est la recherche dense + sparse, suivie du reranking. L'exemple suivant montre un code simple de bout en bout.
Préparation des données
Nous utilisons le Langchain WebBaseLoader pour charger les documents à partir de sources Web et les diviser en morceaux à l'aide du 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#')
Chargement du document dans le magasin vectoriel Milvus
Comme dans l'introduction ci-dessus, nous initialisons et chargeons les documents préparés dans le magasin de vecteurs Milvus, qui contient deux champs de vecteurs : dense est pour l'intégration OpenAI et sparse est pour la fonction 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,
)
Construction de la chaîne RAG
Nous préparons l'instance LLM et l'invite, puis nous les combinons dans un pipeline RAG à l'aide du LangChain Expression Language.
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)
Utilisez le LCEL (LangChain Expression Language) pour construire une chaîne 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()
Invoquer la chaîne RAG avec une question spécifique et récupérer la réponse.
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.'
Nous vous félicitons ! Vous avez construit une chaîne RAG hybride (vecteur dense + fonction bm25 clairsemée) alimentée par Milvus et LangChain.