🚀 완전 관리형 Milvus인 Zilliz Cloud를 무료로 체험해보세요—10배 더 빠른 성능을 경험하세요! 지금 체험하기>>

milvus-logo
LFAI
홈페이지
  • 통합

밀버스 하이브리드 서치 리트리버

개요

하이브리드 검색은 서로 다른 검색 패러다임의 강점을 결합하여 검색 정확도와 견고성을 향상시킵니다. 밀도 벡터 검색과 희소 벡터 검색의 기능은 물론, 여러 밀도 벡터 검색 전략의 조합을 활용하여 다양한 쿼리에 대해 포괄적이고 정확한 검색을 보장합니다.

이 다이어그램은 가장 일반적인 하이브리드 검색 시나리오인 고밀도+스파스 하이브리드 검색을 보여줍니다. 이 경우, 시맨틱 벡터 유사도와 정확한 키워드 매칭을 모두 사용하여 후보를 검색합니다. 이러한 방법의 결과는 병합되고 순위가 재조정된 후 LLM으로 전달되어 최종 답변을 생성합니다. 이 접근 방식은 정확도와 의미론적 이해의 균형을 맞추기 때문에 다양한 쿼리 시나리오에 매우 효과적입니다.

고밀도 + 스파스 하이브리드 검색 외에도 하이브리드 전략은 여러 개의 고밀도 벡터 모델을 결합할 수도 있습니다. 예를 들어, 하나의 고밀도 벡터 모델은 의미론적 뉘앙스를 포착하는 데 특화되어 있고, 다른 모델은 문맥 내포 또는 도메인별 표현에 초점을 맞출 수 있습니다. 이러한 모델의 결과를 병합하고 순위를 재조정함으로써, 이러한 유형의 하이브리드 검색은 보다 미묘한 맥락을 인식하는 검색 프로세스를 보장합니다.

LangChain Milvus 통합은 하이브리드 검색을 구현하는 유연한 방법을 제공하며, 임의의 수의 벡터 필드와 사용자 정의 밀도 또는 희소 임베딩 모델을 지원하여 다양한 하이브리드 검색 사용 시나리오에 유연하게 적응할 수 있으며 동시에 LangChain의 다른 기능과 호환됩니다.

이 튜토리얼에서는 가장 일반적인 밀도 + 스파스 사례부터 시작하여 여러 가지 일반적인 하이브리드 검색 사용 방식을 소개합니다.

Milvus와 LangChain을 사용한 하이브리드 검색의 또 다른 구현인 MilvusCollectionHybridSearchRetriever는 더 이상 사용되지 않을 예정입니다. 이 문서의 접근 방식이 더 유연하고 LangChain과 호환되므로 하이브리드 검색을 구현할 때 이 접근 방식을 사용하시기 바랍니다.

전제 조건

이 노트북을 실행하기 전에 다음 종속성이 설치되어 있는지 확인하세요:

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

Google Colab을 사용하는 경우, 방금 설치한 종속성을 활성화하려면 런타임을 다시 시작해야 할 수 있습니다(화면 상단의 "런타임" 메뉴를 클릭하고 드롭다운 메뉴에서 "세션 다시 시작"을 선택).

OpenAI의 모델을 사용하겠습니다. OpenAI에서 환경 변수 OPENAI_API_KEY 를 준비해야 합니다.

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

밀도 높은 임베딩 + 스파스 임베딩

옵션 1(권장): 밀도 높은 임베딩 + Milvus BM25 내장 기능 사용

밀도 임베딩 + Milvus BM25 내장 함수를 사용하여 하이브리드 검색 벡터 저장소 인스턴스를 조립합니다.

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="Strong",
    drop_old=True,
)
  • BM25BuiltInFunction 을 사용할 경우, 전체 텍스트 검색은 Milvus Standalone 및 Milvus Distributed에서는 사용할 수 있지만, 향후 로드맵에 포함될 예정이지만 Milvus Lite에서는 사용할 수 없다는 점에 유의하세요. 이 기능은 조만간 Zilliz Cloud(완전 관리형 Milvus)에서도 제공될 예정입니다. 자세한 내용은 support@zilliz.com 으로 문의하세요.

위 코드에서는 BM25BuiltInFunction 인스턴스를 정의하고 Milvus 객체에 전달합니다. BM25BuiltInFunction 는 Milvus의 경량 래퍼 클래스입니다. Function 의 경량 래퍼 클래스입니다. OpenAIEmbeddings 와 함께 사용하여 밀도 + 스파스 하이브리드 검색 Milvus 벡터 저장소 인스턴스를 초기화할 수 있습니다.

BM25BuiltInFunction 클라이언트가 말뭉치나 학습을 전달할 필요가 없으며, 모두 Milvus 서버 측에서 자동으로 처리되므로 사용자는 어휘와 말뭉치에 대해 신경 쓸 필요가 없습니다. 또한 사용자는 BM25에서 사용자 지정 텍스트 처리를 구현하도록 분석기를 커스터마이징할 수도 있습니다.

자세한 내용은 BM25BuiltInFunction 에서 전체 텍스트 검색과 LangChain 및 Milvus로 전체 텍스트 검색 사용하기를 참조하세요.

옵션 2: 조밀하고 사용자 정의된 LangChain 스파스 임베딩 사용

langchain_milvus.utils.sparse 에서 BaseSparseEmbedding 클래스를 상속하고 embed_queryembed_documents 메서드를 구현하여 스파스 임베딩 프로세스를 사용자 정의할 수 있습니다. 이를 통해 용어 빈도 통계(예: BM25) 또는 신경망(예: SPADE)에 기반한 모든 희소 임베딩 방법을 사용자 정의할 수 있습니다.

다음은 예시입니다:

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)

langchain_milvus.utils.sparseBaseSparseEmbedding 에서 상속된 데모 클래스 BM25SparseEmbedding 가 있습니다. 다른 랭체인 고밀도 임베딩 클래스와 마찬가지로 Milvus 벡터 저장소 인스턴스의 초기화 임베딩 목록에 전달할 수 있습니다.

# 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="Strong",
    drop_old=True,
)

이 방법은 BM25를 사용하는 방법이지만, 사용자가 용어 빈도 통계를 위해 말뭉치를 관리해야 합니다. 대신 Milvus 서버 측에서 모든 것을 처리하는 BM25 내장 함수(옵션 1)를 사용하는 것을 권장합니다. 이렇게 하면 사용자가 말뭉치 관리나 어휘 훈련에 신경 쓸 필요가 없습니다. 자세한 내용은 LangChain 및 Milvus에서 전체 텍스트 검색 사용하기를 참조하세요.

여러 개의 임의의 벡터 필드 정의하기

Milvus 벡터 저장소를 초기화할 때 임베딩 목록(향후 내장 함수 목록도 포함될 예정)을 전달하여 다방향 재검색을 구현한 다음 이러한 후보의 순위를 재조정할 수 있습니다. 다음은 예시입니다:

# 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="Strong",
    drop_old=True,
)

vectorstore.vector_fields
['dense1', 'dense2', 'sparse']

이 예제에는 세 개의 벡터 필드가 있습니다. 이 중 sparseBM25BuiltInFunction 의 출력 필드로 사용되며, 나머지 두 개의 dense1dense2 은 두 개의 OpenAIEmbeddings 모델의 출력 필드로 자동 할당됩니다(순서에 따라).

멀티벡터 필드에 대한 인덱스 매개변수 지정하기

기본적으로 각 벡터 필드의 인덱스 유형은 임베딩 또는 내장 함수 유형에 따라 자동으로 결정됩니다. 그러나 검색 성능을 최적화하기 위해 각 벡터 필드에 대한 인덱스 유형을 지정할 수도 있습니다.

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="Strong",
    drop_old=True,
)

vectorstore.vector_fields
['dense1', 'dense2', 'sparse']

혼동을 피하기 위해 인덱스 매개변수 목록의 순서를 vectorstore.vector_fields 의 순서와 일관되게 유지하세요.

후보 순위 재조정

검색의 첫 번째 단계가 끝나면 더 나은 결과를 얻기 위해 후보의 순위를 다시 매겨야 합니다. 요구 사항에 따라 가중 순위 또는 RRFRanker를 선택할 수 있습니다. 자세한 내용은 재랭킹을 참조하세요.

다음은 가중치 재랭크의 예입니다:

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

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

다음은 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.")]

재랭크에 대한 매개변수를 전달하지 않으면 기본적으로 평균 가중치 재랭크 전략이 사용됩니다.

RAG에서 하이브리드 검색 및 재랭크 사용

RAG 시나리오에서 가장 널리 사용되는 하이브리드 검색 방식은 밀도 검색 + 희소 검색이며, 그 다음이 재랭크입니다. 다음 예제에서는 간단한 엔드투엔드 코드를 보여줍니다.

데이터 준비

웹 소스에서 문서를 로드하고 RecursiveCharacterTextSplitter를 사용해 청크로 분할하기 위해 Langchain WebBaseLoader를 사용합니다.

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 벡터 스토어에 로드합니다. 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 인스턴스와 프롬프트를 준비한 다음 LangChain 표현 언어를 사용하여 RAG 파이프라인으로 결합합니다.

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 표현식 언어)을 사용하여 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 perform these tasks.'

축하합니다! Milvus와 LangChain으로 구동되는 하이브리드(밀도 벡터 + 스파스 bm25 함수) 검색 RAG 체인을 구축하셨습니다.

Try Managed Milvus for Free

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

Get Started
피드백

이 페이지가 도움이 되었나요?