🚀 Prueba Zilliz Cloud, el Milvus completamente gestionado, gratis—¡experimenta un rendimiento 10 veces más rápido! Prueba Ahora>>

milvus-logo
LFAI
Home
  • Integraciones

Uso de la búsqueda de texto completo con LangChain y Milvus

Open In Colab GitHub Repository

Labúsqueda de texto completo es un método tradicional para recuperar documentos que contienen términos o frases específicos mediante la búsqueda directa de palabras clave en el texto. Clasifica los resultados en función de su relevancia, determinada normalmente por factores como la frecuencia y la proximidad de los términos. Mientras que la búsqueda semántica destaca en la comprensión de la intención y el contexto, la búsqueda de texto completo ofrece precisión en la concordancia exacta de palabras clave, lo que la convierte en una valiosa herramienta complementaria. El algoritmo BM25 es un popular método de clasificación para la búsqueda de texto completo, especialmente útil en la Generación Mejorada de Recuperación (RAG).

Desde Milvus 2.5, la búsqueda de texto completo se soporta de forma nativa a través del enfoque Sparse-BM25, representando el algoritmo BM25 como vectores dispersos. Milvus acepta texto en bruto como entrada y lo convierte automáticamente en vectores dispersos almacenados en un campo especificado, eliminando la necesidad de generar manualmente incrustaciones dispersas.

La integración de LangChain con Milvus también ha introducido esta función, simplificando el proceso de incorporación de la búsqueda de texto completo en las aplicaciones RAG. Al combinar la búsqueda de texto completo con la búsqueda semántica con vectores densos, se puede lograr un enfoque híbrido que aprovecha tanto el contexto semántico de las incrustaciones densas como la relevancia precisa de las palabras clave a partir de la concordancia de palabras. Esta integración mejora la precisión, la relevancia y la experiencia de usuario de los sistemas de búsqueda.

Este tutorial mostrará cómo utilizar LangChain y Milvus para implementar la búsqueda de texto completo en su aplicación.

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

  • Antes de continuar con este tutorial, asegúrese de que tiene una comprensión básica de la búsqueda de texto completo y el uso básico de la integración LangChain Milvus.

Requisitos previos

Antes de ejecutar este cuaderno, asegúrese de tener instaladas las siguientes dependencias:

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

Si utilizas Google Colab, para habilitar las dependencias que acabas de instalar, es posible que tengas que reiniciar el tiempo de ejecución (haz clic en el menú "Tiempo de ejecución" en la parte superior de la pantalla, y selecciona "Reiniciar sesión" 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:

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

Inicialización con la función BM25

Para la búsqueda de texto completo Milvus VectorStore acepta un parámetro builtin_function. A través de este parámetro, puede pasar una instancia de BM25BuiltInFunction. Esto es diferente de la búsqueda semántica, que normalmente pasa incrustaciones densas a VectorStore,

He aquí un ejemplo sencillo de búsqueda híbrida en Milvus con incrustación densa OpenAI para la búsqueda semántica y BM25 para la búsqueda de texto completo:

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

En el código anterior, definimos una instancia de BM25BuiltInFunction y la pasamos al objeto Milvus. BM25BuiltInFunction es una clase envoltorio ligera para Function en Milvus.

Puede especificar los campos de entrada y salida de esta función en los parámetros del objeto BM25BuiltInFunction:

  • input_field_names (str): El nombre del campo de entrada, por defecto es text. Indica qué campo lee esta función como entrada.
  • output_field_names (str): El nombre del campo de salida, por defecto es sparse. Indica en qué campo esta función emite el resultado calculado.

Tenga en cuenta que en los parámetros de inicialización de Milvus mencionados anteriormente, también especificamos vector_field=["dense", "sparse"]. Dado que el campo sparse se toma como el campo de salida definido por BM25BuiltInFunction, el otro campo dense se asignará automáticamente al campo de salida de OpenAIEmbeddings.

En la práctica, especialmente cuando se combinan varias incrustaciones o funciones, recomendamos especificar explícitamente los campos de entrada y salida de cada función para evitar ambigüedades.

En el siguiente ejemplo, especificamos explícitamente los campos de entrada y salida de BM25BuiltInFunction, dejando claro para qué campo es la función incorporada.

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

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

De esta forma, se pueden definir múltiples campos vectoriales y asignarles diferentes combinaciones de incrustaciones o funciones, para implementar la búsqueda híbrida.

Para realizar una búsqueda híbrida, basta con introducir el texto de la consulta y, opcionalmente, los parámetros topK y reranker. La instancia vectorstore gestionará automáticamente las incrustaciones vectoriales y las funciones incorporadas y, por último, utilizará un reranker para refinar los resultados. Los detalles de implementación subyacentes del proceso de búsqueda se ocultan al usuario.

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

Para más información sobre la búsqueda híbrida, puede consultar la introducción a la búsqueda híbrida y este tutorial sobre la búsqueda híbrida en LangChain Milvus.

Búsqueda BM25 sin incrustación

Si desea realizar únicamente una búsqueda de texto completo con la función BM25 sin utilizar ninguna búsqueda semántica basada en la incrustación, puede establecer el parámetro de incrustación en None y conservar únicamente el builtin_function especificado como instancia de la función BM25. El campo vectorial sólo tiene campo "disperso". Por ejemplo:

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

Personalizar analizador

Los analizadores son esenciales en la búsqueda de texto completo, ya que descomponen la frase en tokens y realizan análisis léxicos como el stemming y la eliminación de palabras vacías. Los analizadores suelen ser específicos de cada idioma. Puede consultar esta guía para saber más sobre los analizadores en Milvus.

Milvus admite dos tipos de analizadores: Analizadores incorporados y Analizadores personalizados. Por defecto, BM25BuiltInFunction utilizará el analizador incorporado estándar, que es el analizador más básico que tokeniza el texto con puntuación.

Si desea utilizar un analizador diferente o personalizar el analizador, puede pasar el parámetro analyzer_params en la inicialización 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
    ],
}


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

Podemos echar un vistazo al esquema de la colección Milvus y asegurarnos de que el analizador personalizado está configurado correctamente.

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

Para más detalles sobre los conceptos, por ejemplo, analyzer, tokenizer, filter, enable_match, analyzer_params, consulte la documentación del analizador.

Uso de la búsqueda híbrida y la reordenación en RAG

Hemos aprendido a utilizar la función básica BM25 incorporada en LangChain y Milvus. Vamos a introducir una implementación optimizada de RAG con búsqueda híbrida y reordenación.

Este diagrama muestra el proceso de recuperación y reordenación híbridas, que combina BM25 para la concordancia de palabras clave y la búsqueda vectorial para la recuperación semántica. Los resultados de ambos métodos se combinan, se reordenan y se pasan a un LLM para generar la respuesta final.

La búsqueda híbrida equilibra la precisión y la comprensión semántica, mejorando la exactitud y la solidez para diversas consultas. Recupera candidatos con la búsqueda de texto completo BM25 y la búsqueda vectorial, garantizando una recuperación semántica, precisa y consciente del contexto.

Empecemos con un ejemplo.

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 generate and execute these programming statements.'

Enhorabuena. Ha construido una cadena RAG de búsqueda híbrida (vector denso + función bm25 dispersa) potenciada por Milvus y LangChain.

Try Managed Milvus for Free

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

Get Started
Feedback

¿Fue útil esta página?