🚀 Prova Zilliz Cloud, la versione completamente gestita di Milvus, gratuitamente—sperimenta prestazioni 10 volte più veloci! Prova Ora>>

milvus-logo
LFAI
Casa
  • Integrazioni

Uso della ricerca full-text con LangChain e Milvus

Open In Colab GitHub Repository

Laricerca full-text è un metodo tradizionale per recuperare i documenti che contengono termini o frasi specifiche, confrontando direttamente le parole chiave all'interno del testo. I risultati vengono classificati in base alla rilevanza, tipicamente determinata da fattori quali la frequenza e la prossimità dei termini. Mentre la ricerca semantica eccelle nella comprensione dell'intento e del contesto, la ricerca full-text offre una precisione nella corrispondenza esatta delle parole chiave, rendendola un prezioso strumento complementare. L'algoritmo BM25 è un metodo di classificazione popolare per la ricerca full-text, particolarmente utile nella Retrieval-Augmented Generation (RAG).

Da Milvus 2.5, la ricerca full-text è supportata in modo nativo dall'approccio Sparse-BM25, che rappresenta l'algoritmo BM25 come vettori sparsi. Milvus accetta il testo grezzo come input e lo converte automaticamente in vettori sparsi memorizzati in un campo specificato, eliminando la necessità di generare manualmente l'incorporazione sparsa.

L'integrazione di LangChain con Milvus ha introdotto anche questa funzione, semplificando il processo di incorporazione della ricerca full-text nelle applicazioni RAG. Combinando la ricerca full-text con la ricerca semantica con vettori densi, è possibile ottenere un approccio ibrido che sfrutta sia il contesto semantico dagli embedding densi sia la precisa rilevanza delle parole chiave dalla corrispondenza delle parole. Questa integrazione migliora l'accuratezza, la pertinenza e l'esperienza utente dei sistemi di ricerca.

Questo tutorial mostra come utilizzare LangChain e Milvus per implementare la ricerca full-text nella vostra applicazione.

  • La ricerca full-text è disponibile in Milvus Standalone e Milvus Distributed, ma non in Milvus Lite, anche se è in programma per il futuro. Sarà presto disponibile anche in Zilliz Cloud (Milvus a gestione completa). Per ulteriori informazioni, contattare support@zilliz.com.

  • Prima di procedere con questo tutorial, assicuratevi di avere una conoscenza di base della ricerca full-text e dell'utilizzo di base dell'integrazione LangChain Milvus.

Prerequisiti

Prima di eseguire questo notebook, assicuratevi di avere installato le seguenti dipendenze:

$ pip install --upgrade --quiet  langchain langchain-core langchain-community langchain-text-splitters langchain-milvus langchain-openai bs4 #langchain-voyageai

Se si utilizza Google Colab, per abilitare le dipendenze appena installate potrebbe essere necessario riavviare il runtime (fare clic sul menu "Runtime" nella parte superiore dello schermo e selezionare "Restart session" dal menu a discesa).

Utilizzeremo i modelli di OpenAI. È necessario preparare le variabili d'ambiente OPENAI_API_KEY da OpenAI.

import os

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

Specificare il server Milvus URI (e facoltativamente TOKEN). Per sapere come installare e avviare il server Milvus, seguire questa guida.

URI = "http://localhost:19530"
# TOKEN = ...

Preparare alcuni documenti di esempio:

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"}),
]

Inizializzazione con la funzione BM25

Per la ricerca full-text Milvus VectorStore accetta un parametro builtin_function. Attraverso questo parametro, è possibile passare un'istanza di BM25BuiltInFunction. Questo è diverso dalla ricerca semantica, che di solito passa embeddings densi a VectorStore,

Ecco un semplice esempio di ricerca ibrida in Milvus con OpenAI dense embedding per la ricerca semantica e BM25 per la ricerca full-text:

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,
)

Nel codice qui sopra, definiamo un'istanza di BM25BuiltInFunction e la passiamo all'oggetto Milvus. BM25BuiltInFunction è una classe leggera per il wrapper di Function in Milvus.

È possibile specificare i campi di ingresso e di uscita di questa funzione nei parametri dell'oggetto BM25BuiltInFunction:

  • input_field_names (str): Il nome del campo di input, predefinito è text. Indica quale campo la funzione legge come input.
  • output_field_names (str): Il nome del campo di uscita, predefinito è sparse. Indica il campo in cui la funzione invia il risultato calcolato.

Si noti che nei parametri di inizializzazione di Milvus menzionati sopra, si specifica anche vector_field=["dense", "sparse"]. Poiché il campo sparse viene preso come campo di uscita definito da BM25BuiltInFunction, l'altro campo dense verrà automaticamente assegnato al campo di uscita di OpenAIEmbeddings.

In pratica, soprattutto quando si combinano più embeddings o funzioni, si consiglia di specificare esplicitamente i campi di ingresso e di uscita per ogni funzione, per evitare ambiguità.

Nell'esempio seguente, specifichiamo esplicitamente i campi di ingresso e di uscita di BM25BuiltInFunction, rendendo chiaro a quale campo si riferisce la funzione incorporata.

# 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']

In questo esempio, abbiamo tre campi vettoriali. Tra questi, sparse è usato come campo di uscita per BM25BuiltInFunction, mentre gli altri due, dense1 e dense2, sono assegnati automaticamente come campi di uscita per i due modelli OpenAIEmbeddings (in base all'ordine).

In questo modo, è possibile definire più campi vettoriali e assegnare loro diverse combinazioni di incorporazioni o funzioni, per implementare la ricerca ibrida.

Per eseguire una ricerca ibrida, è sufficiente inserire il testo della query e impostare facoltativamente i parametri topK e reranker. L'istanza di vectorstore gestirà automaticamente gli embeddings vettoriali e le funzioni integrate e infine utilizzerà un reranker per raffinare i risultati. I dettagli dell'implementazione del processo di ricerca sono nascosti all'utente.

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')]

Per ulteriori informazioni sulla ricerca ibrida, è possibile consultare l'introduzione alla ricerca ibrida e questo tutorial sulla ricerca ibrida di LangChain Milvus.

Ricerca BM25 senza incorporazione

Se si desidera eseguire solo la ricerca full-text con la funzione BM25 senza utilizzare alcuna ricerca semantica basata sull'embedding, è possibile impostare il parametro embedding su None e mantenere solo il builtin_function specificato come istanza della funzione BM25. Il campo vettoriale ha solo un campo "sparse". Ad esempio:

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']

Personalizzare l'analizzatore

Gli analizzatori sono essenziali nella ricerca full-text, in quanto suddividono la frase in token ed eseguono l'analisi lessicale, come lo stemming e la rimozione delle stop word. Gli analizzatori sono solitamente specifici per la lingua. Per saperne di più sugli analizzatori in Milvus, si può consultare questa guida.

Milvus supporta due tipi di analizzatori: Analizzatori integrati e Analizzatori personalizzati. Per impostazione predefinita, BM25BuiltInFunction utilizzerà l'analizzatore standard incorporato, che è l'analizzatore più elementare e che tokenizza il testo con la punteggiatura.

Se si vuole usare un analizzatore diverso o personalizzare l'analizzatore, si può passare il parametro analyzer_params nell'inizializzazione di 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
    ],
}


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,
)

Si può dare un'occhiata allo schema della collezione Milvus e verificare che l'analizzatore personalizzato sia impostato correttamente.

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': {}}]}

Per ulteriori dettagli sui concetti, ad esempio analyzer, tokenizer, filter, enable_match, analyzer_params, consultare la documentazione dell'analizzatore.

Utilizzo della ricerca ibrida e del reranking in RAG

Abbiamo imparato a usare la funzione di base di BM25 in LangChain e Milvus. Introduciamo un'implementazione ottimizzata di RAG con ricerca ibrida e reranking.

Questo diagramma mostra il processo di Hybrid Retrieve & Reranking, che combina BM25 per la corrispondenza delle parole chiave e la ricerca vettoriale per il recupero semantico. I risultati di entrambi i metodi vengono uniti, riclassificati e passati a un LLM per generare la risposta finale.

La ricerca ibrida bilancia la precisione e la comprensione semantica, migliorando l'accuratezza e la robustezza per query diverse. Recupera i candidati con la ricerca full-text e la ricerca vettoriale di BM25, garantendo un recupero semantico, consapevole del contesto e accurato.

Cominciamo con un esempio.

Preparare i dati

Utilizziamo Langchain WebBaseLoader per caricare i documenti da fonti web e dividerli in parti utilizzando 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#')

Caricare il documento nell'archivio vettoriale Milvus

Come nell'introduzione precedente, inizializziamo e carichiamo i documenti preparati nell'archivio vettoriale Milvus, che contiene due campi vettoriali: dense è per l'incorporamento OpenAI e sparse è per la funzione 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,
)

Costruire la catena RAG

Prepariamo l'istanza LLM e il prompt, quindi li combiniamo in una pipeline RAG usando il 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)

Utilizzare LCEL (LangChain Expression Language) per costruire una catena 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()

Invochiamo la catena RAG con una domanda specifica e recuperiamo la risposta.

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.'

Congratulazioni! Avete costruito una catena RAG ibrida (vettore denso + funzione rada bm25) con Milvus e LangChain.

Try Managed Milvus for Free

Zilliz Cloud is hassle-free, powered by Milvus and 10x faster.

Get Started
Feedback

Questa pagina è stata utile?