Vector Databases

FAISS للاسترجاع متعدد الوسائط: تنفيذ بحث هجين للصور والنصوص باستخدام مقاييس مسافة مخصصة

بناء أنظمة بحث تفهم مظهر الصورة ومحتوى النص الخاص بها يمثل قفزة كبيرة إلى الأمام في تجربة المستخدم. بالنسبة للمطورين من المستوى المتوسط إلى المتقدم، غالبًا ما تكمن التحدي في دمج هذه المصادر البياناتية المختلفة في خط أنابيب استرجاع واحد متماسك. على الرغم من وجود نماذج تضمين متعددة الوسائط متخصصة، إلا أن هناك قيمة هائلة في فهم كيفية تنفيذ البحث الهجين باستخدام مكتبات منخفضة المستوى مثل FAISS (بحث Facebook AI عن التشابه). يستكشف هذا المنشور كيفية بناء نظام يسترجع النتائج بناءً على مزيج مرجح من التشابه البصري والنصي، مع مقاييس مسافة مخصصة لضبط الصلة بدقة.

لماذا البحث الهجين؟

يفتقد البحث النصي الخالص السياق البصري، بينما قد يفقد البحث الصوري الخالص (باستخدام التضمينات من نماذج مثل CLIP) الدلالات الدقيقة الموجودة في البيانات الوصفية أو التسميات التوضيحية. يعالج البحث الهجين هذه المشكلة من خلال حساب درجات تشابه منفصلة لمكونات النص والصورة في الاستعلام والمستند، ثم دمجها. في FAISS، يتطلب هذا إدارة فهرسين منفصلين - واحد لتضمينات النص وواحد لتضمينات الصورة - وتنفيذ طبقة منطقية تجمع نتائجهما.

إعداد خط أنابيب متعدد الوسائط

للبداية، نفترض أن لديك متجهات محسوبة مسبقًا لمجموعتك البياناتية. في هذا المثال، نستخدم تضمينات بأبعاد 512، وهي حجم مخرج شائع لنماذج مثل CLIP ViT-B/32. سنقوم بإنشاء فهارس FAISS مميزة:

  1. فهرس النص: باستخدام IndexFlatIP (الجداء الداخلي) للتشابه الكوسيني.
  2. فهرس الصورة: باستخدام IndexFlatIP أيضًا من أجل البساطة، على الرغم من أن IndexIVFFlat يُوصى به للمجموعات البياناتية الأكبر.

import faiss
import numpy as np

# تحديد أبعاد التضمين
dim = 512

# 1. إنشاء فهرس النص (الجداء الداخلي)
index_text = faiss.IndexFlatIP(dim)

# 2. إنشاء فهرس الصورة (الجداء الداخلي)
index_image = faiss.IndexFlatIP(dim)

# توليد بيانات تجريبية
num_docs = 1000
np.random.seed(42)
text_embeddings = np.random.rand(num_docs, dim).astype("float32")
image_embeddings = np.random.rand(num_docs, dim).astype("float32")

# تطبيع المتجهات للتشابه الكوسيني (مطلوب لكي يعمل الجداء الداخلي ككوسيني)
faiss.normalize_L2(text_embeddings)
faiss.normalize_L2(image_embeddings)

# إضافة المتجهات إلى الفهارس
index_text.add(text_embeddings)
index_image.add(image_embeddings)

تنفيذ مقاييس المسافة المخصصة والدمج

جوهر البحث الهجين هو استراتيجية الدمج. نهج شائع هو المزيج الخطي، حيث تكون الدرجة النهائية مجموعًا مرجحًا للدرجات الفردية. ومع ذلك، قد لا تكون الدرجات الخام من فهارس مختلفة على نفس المقياس. نحتاج إلى دالة مقياس مسافة مخصص تطبع هذه الدرجات قبل الدمج.

فيما يلي دالة Python تنفذ البحث الهجين. تقبل الأوزان alpha (للنص) و beta (للصورة)، حيث alpha + beta = 1.0.


def hybrid_search(index_t, index_i, query_text_vec, query_img_vec, k=10, alpha=0.5):
    """
    تنفذ البحث الهجين من خلال دمج درجات تشابه النص والصورة.
    
    :param index_t: فهرس FAISS لتضمينات النص
    :param index_i: فهرس FAISS لتضمينات الصورة
    :param query_text_vec: متجه استعلام نصي معياري (1, dim)
    :param query_img_vec: متجه استعلام صوري معياري (1, dim)
    :param k: عدد النتائج المراد إرجاعها
    :param alpha: وزن لتشابه النص (1-alpha هو وزن الصورة)
    :return: قائمة من tuples (الفهرس، الدرجة المجمعة)
    """
    # تطبيع متجهات الاستعلام إذا لم تكن كذلك بالفعل
    faiss.normalize_L2(query_text_vec)
    faiss.normalize_L2(query_img_vec)

    # تنفيذ البحث على كلا الفهرسين
    # ملاحظة: يعيد FAISS الفهارس والمسافات (الدرجات)
    D_text, I_text = index_t.search(query_text_vec, k)
    D_image, I_image = index_i.search(query_img_vec, k)

    # نحتاج إلى دمج النتائج. بما أن I_text و I_image قد يختلفان،
    # نسترجع أعلى k من كلاهما وندمجها.
    candidates = {}

    # جمع درجات النص
    for idx, score in zip(I_text[0], D_text[0]):
        if idx != -1:
            candidates[idx] = {'text_score': float(score), 'image_score': 0.0}

    # جمع درجات الصورة
    for idx, score in zip(I_image[0], D_image[0]):
        if idx != -1:
            if idx in candidates:
                candidates[idx]['image_score'] = float(score)
            else:
                candidates[idx] = {'text_score': 0.0, 'image_score': float(score)}

    # تطبيق المجموع المرجح المخصص (المزيج الخطي)
    # الدرجة النهائية = alpha * درجة النص + (1 - alpha) * درجة الصورة
    weighted_results = []
    for doc_id, scores in candidates.items():
        combined_score = (alpha * scores['text_score']) + ((1 - alpha) * scores['image_score'])
        weighted_results.append((doc_id, combined_score))

    # فرز حسب الدرجة المجمعة تنازليًا (يعيد FAISS درجات أعلى كجودة أفضل للـ IP)
    weighted_results.sort(key=lambda x: x[1], reverse=True)
    
    return weighted_results[:k]

# مثال على الاستخدام
query_text = np.random.rand(1, dim).astype("float32")
query_img = np.random.rand(1, dim).astype("float32")

results = hybrid_search(index_text, index_image, query_text, query_img, k=5, alpha=0.7)
for doc_id, score in results:
    print(f"Doc ID: {doc_id}, Combined Score: {score:.4f}")

التحسين والاستراتيجيات المتقدمة

المزيج الخطي أعلاه بسيط لكن يمكن تحسينه. لأنظمة الإنتاج، ضع في اعتبارك:

  • دمج الترتيب المتبادل (RRF): بدلاً من جمع الدرجات الخام، اجمع الترتيبات العكسية. هذا متين تجاه اختلافات المقياس بين الوسائط.
  • اختيار الفهرس: للمجموعات البياناتية التي تتجاوز 100 ألف متجه، استبدل IndexFlatIP بـ IndexIVFPQ (ملف معكوس مع التكميم المنتج) لتقليل بصمة الذاكرة وزيادة سرعة البحث. ستحتاج إلى إعادة تدريب هذه الفهارس على توزيع بياناتك المحدد.
  • الوزن الديناميكي: استخدم مصنفًا لتحديد ما إذا كان الاستعلام أكثر اعتمادًا على النص أم الصورة، وعدّل alpha ديناميكيًا.

الخلاصة

تنفيذ الاسترجاع متعدد الوسائط باستخدام FAISS يوفر تحكمًا دقيقًا في كيفية تفسير محرك البحث لديك لنوايا المستخدم. من خلال فصل فهارس النص والصورة وتطبيق منطق دمج مخصص، يمكنك إنشاء تجربة بحث غنية دلاليًا وبديهية بصريًا. ابدأ بالوزن الخطي، وقِس معدلات الإصابة لديك، وكرر نحو استراتيجيات دمج أكثر تعقيدًا مثل RRF أو نماذج التعلم للترتيب. بفضل سرعة FAISS، حتى البحث الهجين في الوقت الفعلي على مجموعات بيانات واسعة النطاق قابل للتحقيق.

Share: