استخدام البحث عن النص الكامل مع LangChain وMilvus
البحث في النص الكامل هو طريقة تقليدية لاسترجاع المستندات التي تحتوي على مصطلحات أو عبارات محددة عن طريق المطابقة المباشرة للكلمات الرئيسية داخل النص. ويقوم بترتيب النتائج بناءً على مدى ملاءمتها، وعادةً ما يتم تحديدها من خلال عوامل مثل تكرار المصطلح والقرب. في حين أن البحث الدلالي يتفوق في فهم القصد والسياق، فإن البحث في النص الكامل يوفر دقة مطابقة الكلمات المفتاحية بدقة، مما يجعله أداة تكميلية قيّمة. تُعد خوارزمية BM25 طريقة ترتيب شائعة للبحث في النص الكامل، وهي مفيدة بشكل خاص في التوليد المعزز للاسترجاع (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، لتمكين التبعيات المثبتة للتو، قد تحتاج إلى إعادة تشغيل وقت التشغيل (انقر على قائمة "وقت التشغيل" في أعلى الشاشة، وحدد "إعادة تشغيل الجلسة" من القائمة المنسدلة).
سنستخدم النماذج من OpenAI. يجب عليك إعداد متغيرات البيئة OPENAI_API_KEY
من OpenAI.
import os
os.environ["OPENAI_API_KEY"] = "sk-***********"
حدد خادم Milvus URI
(واختيارياً TOKEN
). لمعرفة كيفية تثبيت خادم ميلفوس وبدء تشغيله باتباع هذا الدليل.
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
البحث الهجين
للبحث في النص الكامل يقبل ميلفوس فيكتور ستور معلمة 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
في ميلفوس.
يمكنك تحديد حقول الإدخال والإخراج لهذه الدالة في معلمات BM25BuiltInFunction
:
input_field_names
(str): اسم حقل الإدخال، الافتراضي هوtext
. يشير إلى الحقل الذي تقرأه هذه الدالة كمدخل.output_field_names
(str): اسم حقل الإخراج، الافتراضي هوsparse
. يشير إلى الحقل الذي تخرج هذه الدالة النتيجة المحسوبة إليه.
لاحظ أنه في معلمات تهيئة ميلفوس المذكورة أعلاه، نحدد أيضًا 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 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']
تخصيص المحلل
تُعد المحللات ضرورية في البحث في النص الكامل من خلال تقسيم الجملة إلى رموز وإجراء تحليل معجمي مثل الجذعية وإزالة كلمات التوقف. عادةً ما تكون المحللات خاصة باللغة. يمكنك الرجوع إلى هذا الدليل لمعرفة المزيد عن أدوات التحليل في ملفوس.
يدعم ميلفوس نوعين من المحللات: المحللات المدمجة والمحللات المخصصة. بشكل افتراضي، سيستخدم 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,
)
يمكننا إلقاء نظرة على مخطط مجموعة ميلفوس والتأكد من إعداد المحلل المخصص بشكل صحيح.
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 والبحث المتجه، مما يضمن استرجاعًا دلاليًا ودقيقًا ومدركًا للسياق.
لنبدأ بمثال.
إعداد البيانات
نستخدم أداة تحميل الويب 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 vector، والذي يحتوي على حقلي متجهين: 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 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)
استخدم LCEL (لغة تعبير سلسلة اللغات) لبناء سلسلة 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.'
تهانينا! لقد قمتَ ببناء سلسلة RAG بحث هجينة (متجه كثيف + دالة bm25 متناثرة) مدعومة من Milvus وLangChain.