🚀 Попробуйте Zilliz Cloud, полностью управляемый Milvus, бесплатно — ощутите 10-кратное увеличение производительности! Попробовать сейчас>

milvus-logo
LFAI
Главная
  • Интеграции
  • Home
  • Docs
  • Интеграции

  • Оркестровка

  • LangChain

  • Полнотекстовый поиск

Использование полнотекстового поиска с помощью LangChain и Milvus

Open In Colab GitHub Repository

Полнотекстовый поиск - это традиционный метод поиска документов, содержащих определенные термины или фразы, путем прямого сопоставления ключевых слов в тексте. Он ранжирует результаты на основе релевантности, которая обычно определяется такими факторами, как частота и близость терминов. В то время как семантический поиск позволяет лучше понять замысел и контекст, полнотекстовый поиск обеспечивает точность точного подбора ключевых слов, что делает его ценным дополнительным инструментом. Алгоритм BM25 - это популярный метод ранжирования для полнотекстового поиска, особенно полезный в Retrieval-Augmented Generation (RAG).

Начиная с Milvus 2.5, полнотекстовый поиск поддерживается с помощью подхода Sparse-BM25, представляющего алгоритм BM25 в виде разреженных векторов. Milvus принимает на вход необработанный текст и автоматически преобразует его в разреженные векторы, хранящиеся в указанном поле, устраняя необходимость в ручной генерации разреженных вкраплений.

Интеграция LangChain с Milvus также добавила эту функцию, упростив процесс внедрения полнотекстового поиска в приложения RAG. Комбинируя полнотекстовый поиск с семантическим поиском с помощью плотных векторов, вы можете получить гибридный подход, который использует как семантический контекст из плотных вкраплений, так и точную релевантность ключевых слов из сопоставления слов. Такая интеграция повышает точность, релевантность и удобство работы с поисковыми системами.

В этом руководстве мы покажем, как использовать LangChain и Milvus для реализации полнотекстового поиска в вашем приложении.

  • Полнотекстовый поиск доступен в Milvus Standalone и Milvus Distributed, но не в Milvus Lite, хотя он включен в дорожную карту для будущего включения. В ближайшее время он также будет доступен в Zilliz Cloud (полностью управляемый Milvus). За дополнительной информацией обращайтесь на support@zilliz.com.

  • Прежде чем приступить к этому руководству, убедитесь, что вы имеете базовое представление о полнотекстовом поиске и базовом использовании интеграции LangChain Milvus.

Предварительные условия

Перед запуском этого блокнота убедитесь, что у вас установлены следующие зависимости:

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

Если вы используете Google Colab, то для включения только что установленных зависимостей вам может потребоваться перезапустить среду выполнения (нажмите на меню "Runtime" в верхней части экрана и выберите "Restart session" из выпадающего меню).

Мы будем использовать модели из OpenAI. Вам необходимо подготовить переменные окружения OPENAI_API_KEY из OpenAI.

import os

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

Укажите ваш сервер Milvus URI (и, по желанию, TOKEN). О том, как установить и запустить сервер Milvus, читайте в этом руководстве.

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

Подготовьте несколько примеров документов:

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

Инициализация с помощью функции BM25

Для полнотекстового поиска Milvus VectorStore принимает параметр builtin_function. Через этот параметр вы можете передать экземпляр BM25BuiltInFunction. Это отличается от семантического поиска, который обычно передает плотные вкрапления в VectorStore,

Вот простой пример гибридного поиска в Milvus с использованием плотных вкраплений OpenAI для семантического поиска и BM25 для полнотекстового поиска:

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

В приведенном выше коде мы определяем экземпляр BM25BuiltInFunction и передаем его объекту Milvus. BM25BuiltInFunction - это облегченный класс-обертка для Function в Milvus.

Вы можете указать поля ввода и вывода для этой функции в параметрах BM25BuiltInFunction:

  • input_field_names (str): Имя поля ввода, по умолчанию text. Оно указывает, какое поле эта функция считывает в качестве входного.
  • output_field_names (str): Имя выходного поля, по умолчанию sparse. Указывает, в какое поле эта функция выводит результат вычислений.

Обратите внимание, что в параметрах инициализации Milvus, упомянутых выше, мы также указываем vector_field=["dense", "sparse"]. Поскольку поле sparse принимается за выходное поле, определяемое полем BM25BuiltInFunction, другое поле dense будет автоматически назначено выходным полем OpenAIEmbeddings.

На практике, особенно при объединении нескольких вкраплений или функций, мы рекомендуем явно указывать поля ввода и вывода для каждой функции, чтобы избежать двусмысленности.

В следующем примере мы явно указываем поля ввода и вывода BM25BuiltInFunction, чтобы было понятно, для какого поля предназначена встроенная функция.

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

В этом примере у нас есть три векторных поля. Из них sparse используется как выходное поле для BM25BuiltInFunction, а два других, dense1 и dense2, автоматически назначаются выходными полями для двух моделей OpenAIEmbeddings (в соответствии с порядком).

Таким образом, вы можете определить несколько векторных полей и назначить им различные комбинации вкраплений или функций, чтобы реализовать гибридный поиск.

При выполнении гибридного поиска нам нужно только передать текст запроса и опционально задать параметры topK и reranker. Экземпляр vectorstore автоматически обработает векторные вкрапления и встроенные функции и, наконец, использует реранкер для уточнения результатов. Основные детали реализации процесса поиска скрыты от пользователя.

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

Для получения дополнительной информации о гибридном поиске вы можете обратиться к введению в гибридный поиск и этому учебнику по гибридному поиску LangChain Milvus.

Поиск BM25 без встраивания

Если вы хотите выполнять только полнотекстовый поиск с помощью функции BM25 без использования семантического поиска на основе встраивания, вы можете установить параметр встраивания на None и сохранить только builtin_function, указанный в качестве экземпляра функции BM25. Векторное поле имеет только "разреженное" поле. Например:

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

Настроить анализатор

Анализаторы играют важную роль в полнотекстовом поиске, разбивая предложение на лексемы и выполняя лексический анализ, например, стемминг и удаление стоп-слов. Анализаторы обычно зависят от конкретного языка. Вы можете обратиться к этому руководству, чтобы узнать больше об анализаторах в Milvus.

Milvus поддерживает два типа анализаторов: Встроенные анализаторы и Пользовательские анализаторы. По умолчанию на сайте BM25BuiltInFunction будет использоваться стандартный встроенный анализатор, который является самым базовым анализатором и выполняет токенизацию текста с пунктуацией.

Если вы хотите использовать другой анализатор или настроить его под себя, вы можете передать параметр analyzer_params в инициализации 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,
)

Мы можем взглянуть на схему коллекции Milvus и убедиться, что настроенный анализатор настроен правильно.

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

Более подробную информацию о концептах, например, analyzer, tokenizer, filter, enable_match, analyzer_params, можно найти в документации к анализатору.

Использование гибридного поиска и реранжирования в RAG

Мы узнали, как использовать базовую встроенную функцию BM25 в LangChain и Milvus. Давайте представим оптимизированную реализацию RAG с гибридным поиском и реранжированием.

На этой диаграмме показан процесс гибридного поиска и реранжирования, сочетающий BM25 для подбора ключевых слов и векторный поиск для семантического поиска. Результаты, полученные обоими методами, объединяются, ранжируются и передаются в LLM для генерации окончательного ответа.

Гибридный поиск балансирует между точностью и семантическим пониманием, повышая точность и устойчивость к различным запросам. Он извлекает кандидатов с помощью полнотекстового поиска BM25 и векторного поиска, обеспечивая семантический, контекстный и точный поиск.

Давайте начнем с примера.

Подготовка данных

Мы используем Langchain WebBaseLoader для загрузки документов из веб-источников и разбиваем их на фрагменты с помощью 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#')

Загрузка документа в векторное хранилище Milvus

Как и в предыдущем вступлении, мы инициализируем и загружаем подготовленные документы в векторное хранилище Milvus, которое содержит два векторных поля: dense - для вставки OpenAI и sparse - для функции 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,
)

Построение цепочки RAG

Мы подготавливаем экземпляр LLM и подсказку, а затем объединяем их в RAG-конвейер с помощью языка выражений 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)

Используйте язык LCEL (LangChain Expression Language) для построения 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()

Вызовите цепочку RAG с определенным вопросом и получите ответ

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

Поздравляем! Вы построили гибридную (плотный вектор + разреженная функция bm25) поисковую RAG-цепочку на базе Milvus и LangChain.

Попробуйте Managed Milvus бесплатно

Zilliz Cloud работает без проблем, поддерживается Milvus и в 10 раз быстрее.

Начать
Обратная связь

Была ли эта страница полезной?