Open In Colab GitHub Repository

Recherche en texte intégral avec Milvus et Haystack

Larecherche en texte intégral est une méthode traditionnelle qui permet de récupérer des documents en faisant correspondre des mots-clés ou des phrases spécifiques dans le texte. Elle classe les résultats sur la base de scores de pertinence calculés à partir de facteurs tels que la fréquence des termes. Alors que la recherche sémantique permet de mieux comprendre le sens et le contexte, la recherche en texte intégral excelle dans la correspondance précise des mots-clés, ce qui en fait un complément utile à la recherche sémantique. L'algorithme BM25 est largement utilisé pour le classement dans la recherche plein texte et joue un rôle clé dans la génération améliorée par la recherche (RAG).

Milvus 2.5 introduit des capacités natives de recherche en texte intégral à l'aide de l'algorithme BM25. Cette approche convertit le texte en vecteurs épars qui représentent les scores BM25. Vous pouvez simplement saisir du texte brut et Milvus générera et stockera automatiquement les vecteurs épars, sans qu'aucune génération manuelle d'intégration éparse ne soit nécessaire.

Haystack prend désormais en charge cette fonctionnalité de Milvus, ce qui facilite l'ajout de la recherche en texte intégral aux applications RAG. Vous pouvez combiner la recherche en texte intégral avec la recherche sémantique à vecteurs denses pour une approche hybride qui bénéficie à la fois de la compréhension sémantique et de la précision de l'appariement des mots clés. Cette combinaison améliore la précision de la recherche et fournit de meilleurs résultats aux utilisateurs.

Ce tutoriel montre comment mettre en œuvre la recherche en texte intégral et la recherche hybride dans votre application à l'aide de Haystack et Milvus.

Pour utiliser le magasin de vecteurs Milvus, indiquez votre serveur Milvus URI (et éventuellement TOKEN). Pour démarrer un serveur Milvus, vous pouvez configurer un serveur Milvus en suivant le guide d'installation Milvus ou simplement essayer Zilliz Cloud(Milvus entièrement géré) gratuitement.

  • La recherche en texte intégral est actuellement disponible dans Milvus Standalone, Milvus Distributed et Zilliz Cloud, bien qu'elle ne soit pas encore prise en charge dans Milvus Lite (qui prévoit cette fonctionnalité pour une mise en œuvre future). Contactez support@zilliz.com pour plus d'informations.
  • Avant de poursuivre ce tutoriel, assurez-vous d'avoir une compréhension de base de la recherche en texte intégral et de l'utilisation de base de l'intégration Haystack Milvus.

Conditions préalables

Avant d'exécuter ce bloc-notes, assurez-vous que les dépendances suivantes sont installées :

$ pip install --upgrade --quiet pymilvus milvus-haystack

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 la clé api OPENAI_API_KEY comme variable d'environnement.

import os

os.environ["OPENAI_API_KEY"] = "sk-***********"

Préparer les données

Importez les paquets nécessaires dans ce carnet. Préparez ensuite quelques exemples de documents.

from haystack import Pipeline
from haystack.components.embedders import OpenAIDocumentEmbedder, OpenAITextEmbedder
from haystack.components.writers import DocumentWriter
from haystack.utils import Secret
from milvus_haystack import MilvusDocumentStore, MilvusSparseEmbeddingRetriever
from haystack.document_stores.types import DuplicatePolicy
from milvus_haystack.function import BM25BuiltInFunction
from milvus_haystack import MilvusDocumentStore
from milvus_haystack.milvus_embedding_retriever import MilvusHybridRetriever

from haystack.utils import Secret
from haystack.components.builders import PromptBuilder
from haystack.components.generators import OpenAIGenerator
from haystack import Document

documents = [
    Document(content="Alice likes this apple", meta={"category": "fruit"}),
    Document(content="Bob likes swimming", meta={"category": "sport"}),
    Document(content="Charlie likes white dogs", meta={"category": "pets"}),
]

L'intégration de la recherche en texte intégral dans un système RAG permet d'équilibrer la recherche sémantique et la recherche précise et prévisible par mot-clé. Vous pouvez également choisir de n'utiliser que la recherche en texte intégral, bien qu'il soit recommandé de combiner la recherche en texte intégral avec la recherche sémantique pour obtenir de meilleurs résultats de recherche. Ici, à des fins de démonstration, nous montrerons la recherche en texte intégral seule et la recherche hybride.

Recherche BM25 sans intégration

Créer le pipeline d'indexation

Pour la recherche plein texte, Milvus MilvusDocumentStore accepte un paramètre builtin_function. Ce paramètre permet de transmettre une instance de BM25BuiltInFunction, qui met en œuvre l'algorithme BM25 du côté du serveur Milvus. Définissez le builtin_function spécifié comme instance de la fonction BM25. Par exemple :

connection_args = {"uri": "http://localhost:19530"}
# connection_args = {"uri": YOUR_ZILLIZ_CLOUD_URI, "token": Secret.from_env_var("ZILLIZ_CLOUD_API_KEY")}

document_store = MilvusDocumentStore(
    connection_args=connection_args,
    sparse_vector_field="sparse_vector",  # The sparse vector field.
    text_field="text",
    builtin_function=[
        BM25BuiltInFunction(  # The BM25 function converts the text into a sparse vector.
            input_field_names="text",
            output_field_names="sparse_vector",
        )
    ],
    consistency_level="Bounded",  # Supported values are (`"Strong"`, `"Session"`, `"Bounded"`, `"Eventually"`).
    drop_old=True,  # Drop the old collection if it exists and recreate it.
)

Pour les connection_args :

  • Vous pouvez configurer un serveur Milvus plus performant sur docker ou kubernetes. Dans cette configuration, veuillez utiliser l'adresse du serveur, par exemplehttp://localhost:19530, comme votre uri.
  • Si vous souhaitez utiliser Zilliz Cloud, le service cloud entièrement géré pour Milvus, ajustez les adresses uri et token, qui correspondent au point de terminaison public et à la clé Api dans Zilliz Cloud.

Créer un pipeline d'indexation pour écrire des documents dans le magasin de documents Milvus.

writer = DocumentWriter(document_store=document_store, policy=DuplicatePolicy.NONE)

indexing_pipeline = Pipeline()
indexing_pipeline.add_component("writer", writer)
indexing_pipeline.run({"writer": {"documents": documents}})
{'writer': {'documents_written': 3}}

Créer le pipeline de récupération

Créer un pipeline de récupération qui récupère les documents du magasin de documents Milvus à l'aide de MilvusSparseEmbeddingRetriever, qui est un wrapper autour de document_store.

retrieval_pipeline = Pipeline()
retrieval_pipeline.add_component(
    "retriever", MilvusSparseEmbeddingRetriever(document_store=document_store)
)

question = "Who likes swimming?"

retrieval_results = retrieval_pipeline.run({"retriever": {"query_text": question}})

retrieval_results["retriever"]["documents"][0]
Document(id=bd334348dd2087c785e99b5a0009f33d9b8b8198736f6415df5d92602d81fd3e, content: 'Bob likes swimming', meta: {'category': 'sport'}, score: 1.2039074897766113)

Créer le pipeline d'indexation

Dans la recherche hybride, nous utilisons la fonction BM25 pour effectuer une recherche en texte intégral et nous spécifions le champ vectoriel dense vector, pour effectuer une recherche sémantique.

document_store = MilvusDocumentStore(
    connection_args=connection_args,
    vector_field="vector",  # The dense vector field.
    sparse_vector_field="sparse_vector",  # The sparse vector field.
    text_field="text",
    builtin_function=[
        BM25BuiltInFunction(  # The BM25 function converts the text into a sparse vector.
            input_field_names="text",
            output_field_names="sparse_vector",
        )
    ],
    consistency_level="Bounded",  # Supported values are (`"Strong"`, `"Session"`, `"Bounded"`, `"Eventually"`).
    drop_old=True,  # Drop the old collection and recreate it.
)

Créer un pipeline d'indexation qui convertit les documents en embeddings. Les documents sont ensuite écrits dans le magasin de documents Milvus.

writer = DocumentWriter(document_store=document_store, policy=DuplicatePolicy.NONE)

indexing_pipeline = Pipeline()
indexing_pipeline.add_component("dense_doc_embedder", OpenAIDocumentEmbedder())
indexing_pipeline.add_component("writer", writer)
indexing_pipeline.connect("dense_doc_embedder", "writer")
indexing_pipeline.run({"dense_doc_embedder": {"documents": documents}})

print("Number of documents:", document_store.count_documents())
Calculating embeddings: 100%|██████████| 1/1 [00:01<00:00,  1.15s/it]


Number of documents: 3

Créer le pipeline d'extraction

Créer un pipeline d'extraction qui extrait les documents du magasin de documents Milvus à l'aide de MilvusHybridRetriever, qui contient document_store et reçoit les paramètres relatifs à la recherche hybride.

# from pymilvus import WeightedRanker
retrieval_pipeline = Pipeline()
retrieval_pipeline.add_component("dense_text_embedder", OpenAITextEmbedder())
retrieval_pipeline.add_component(
    "retriever",
    MilvusHybridRetriever(
        document_store=document_store,
        # top_k=3,
        # reranker=WeightedRanker(0.5, 0.5),  # Default is RRFRanker()
    ),
)

retrieval_pipeline.connect("dense_text_embedder.embedding", "retriever.query_embedding")
<haystack.core.pipeline.pipeline.Pipeline object at 0x3383ad990>
🚅 Components
  - dense_text_embedder: OpenAITextEmbedder
  - retriever: MilvusHybridRetriever
🛤️ Connections
  - dense_text_embedder.embedding -> retriever.query_embedding (List[float])

Lors de l'exécution de la recherche hybride à l'aide de MilvusHybridRetriever, nous pouvons définir les paramètres topK et reranker. Le système traitera automatiquement les intégrations vectorielles et les fonctions intégrées et utilisera finalement un reranker pour affiner les résultats. Les détails de l'implémentation sous-jacente du processus de recherche sont cachés à l'utilisateur.

Pour plus d'informations sur la recherche hybride, vous pouvez consulter l'introduction à la recherche hybride.

question = "Who likes swimming?"

retrieval_results = retrieval_pipeline.run(
    {
        "dense_text_embedder": {"text": question},
        "retriever": {"query_text": question},
    }
)

retrieval_results["retriever"]["documents"][0]
Document(id=bd334348dd2087c785e99b5a0009f33d9b8b8198736f6415df5d92602d81fd3e, content: 'Bob likes swimming', meta: {'category': 'sport'}, score: 0.032786883413791656, embedding: vector of size 1536)

Personnaliser l'analyseur

Les analyseurs sont essentiels dans la recherche en texte intégral car ils décomposent la phrase en tokens et effectuent des analyses lexicales telles que le stemming et la suppression des mots vides. Les analyseurs sont généralement spécifiques à une langue. Vous pouvez consulter ce guide pour en savoir plus sur les analyseurs dans Milvus.

Milvus prend en charge deux types d'analyseurs : Les analyseurs intégrés et les analyseurs personnalisés. Par défaut, le site BM25BuiltInFunction utilise l'analyseur intégré standard, qui est l'analyseur le plus basique et qui symbolise le texte avec la ponctuation.

Si vous souhaitez utiliser un analyseur différent ou personnaliser l'analyseur, vous pouvez passer le paramètre analyzer_params dans l'initialisation de BM25BuiltInFunction.

analyzer_params_custom = {
    "tokenizer": "standard",
    "filter": [
        "lowercase",  # Built-in filter
        {"type": "length", "max": 40},  # Custom filter
        {"type": "stop", "stop_words": ["of", "to"]},  # Custom filter
    ],
}

document_store = MilvusDocumentStore(
    connection_args=connection_args,
    vector_field="vector",
    sparse_vector_field="sparse_vector",
    text_field="text",
    builtin_function=[
        BM25BuiltInFunction(
            input_field_names="text",
            output_field_names="sparse_vector",
            analyzer_params=analyzer_params_custom,  # Custom analyzer parameters.
            enable_match=True,  # Whether to enable match.
        )
    ],
    consistency_level="Bounded",
    drop_old=True,
)

# write documents to the document store
writer = DocumentWriter(document_store=document_store, policy=DuplicatePolicy.NONE)
indexing_pipeline = Pipeline()
indexing_pipeline.add_component("dense_doc_embedder", OpenAIDocumentEmbedder())
indexing_pipeline.add_component("writer", writer)
indexing_pipeline.connect("dense_doc_embedder", "writer")
indexing_pipeline.run({"dense_doc_embedder": {"documents": documents}})
Calculating embeddings: 100%|██████████| 1/1 [00:00<00:00,  1.39it/s]





{'dense_doc_embedder': {'meta': {'model': 'text-embedding-ada-002-v2',
   'usage': {'prompt_tokens': 11, 'total_tokens': 11}}},
 'writer': {'documents_written': 3}}

Nous pouvons examiner le schéma de la collection Milvus et nous assurer que l'analyseur personnalisé est correctement configuré.

document_store.col.schema
{'auto_id': False, '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': 'id', 'description': '', 'type': <DataType.VARCHAR: 21>, 'params': {'max_length': 65535}, 'is_primary': True, 'auto_id': False}, {'name': 'vector', 'description': '', 'type': <DataType.FLOAT_VECTOR: 101>, 'params': {'dim': 1536}}, {'name': 'sparse_vector', 'description': '', 'type': <DataType.SPARSE_FLOAT_VECTOR: 104>, 'is_function_output': True}], 'enable_dynamic_field': True, 'functions': [{'name': 'bm25_function_7b6e15a4', 'description': '', 'type': <FunctionType.BM25: 1>, 'input_field_names': ['text'], 'output_field_names': ['sparse_vector'], 'params': {}}]}

Pour plus de détails sur les concepts, par exemple analyzer, tokenizer, filter, enable_match, analyzer_params, veuillez vous référer à la documentation de l'analyseur.

Utilisation de la recherche hybride dans le pipeline RAG

Nous avons appris à utiliser la fonction intégrée de base BM25 dans Haystack et Milvus et avons préparé un fichier chargé document_store. Nous allons maintenant présenter une implémentation optimisée de RAG avec la recherche hybride.

Ce diagramme illustre le processus de récupération et de reclassement hybride, qui combine le BM25 pour la correspondance des mots clés et la recherche vectorielle dense pour la récupération sémantique. Les résultats des deux méthodes sont fusionnés, reclassés et transmis à un LLM pour générer la réponse finale.

La recherche hybride équilibre la précision et la compréhension sémantique, améliorant la précision et la robustesse pour diverses requêtes. Elle récupère les candidats à l'aide de la recherche plein texte BM25 et de la recherche vectorielle, garantissant ainsi une récupération sémantique, contextuelle et précise.

Essayons une implémentation optimisée de RAG avec la recherche hybride.

prompt_template = """Answer the following query based on the provided context. If the context does
                     not include an answer, reply with 'I don't know'.\n
                     Query: {{query}}
                     Documents:
                     {% for doc in documents %}
                        {{ doc.content }}
                     {% endfor %}
                     Answer:
                  """

rag_pipeline = Pipeline()
rag_pipeline.add_component("text_embedder", OpenAITextEmbedder())
rag_pipeline.add_component(
    "retriever", MilvusHybridRetriever(document_store=document_store, top_k=1)
)
rag_pipeline.add_component("prompt_builder", PromptBuilder(template=prompt_template))
rag_pipeline.add_component(
    "generator",
    OpenAIGenerator(
        api_key=Secret.from_token(os.getenv("OPENAI_API_KEY")),
        generation_kwargs={"temperature": 0},
    ),
)
rag_pipeline.connect("text_embedder.embedding", "retriever.query_embedding")
rag_pipeline.connect("retriever.documents", "prompt_builder.documents")
rag_pipeline.connect("prompt_builder", "generator")

results = rag_pipeline.run(
    {
        "text_embedder": {"text": question},
        "retriever": {"query_text": question},
        "prompt_builder": {"query": question},
    }
)
print("RAG answer:", results["generator"]["replies"][0])
RAG answer: Bob likes swimming.

Pour plus d'informations sur l'utilisation de milvus-haystack, veuillez vous référer au dépôt officiel de milvus-haystack.