متى (لا) لاستخدام Vector DB


. إنها تحل مشكلة حقيقية، وفي كثير من الحالات، تكون الاختيار الصحيح لأنظمة RAG. ولكن هذا هو الأمر: مجرد استخدامك للتضمين لا يعني أنك تستخدمه يحتاج قاعدة بيانات المتجهات.

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

في Planck، نستخدم التضمينات لتحسين الأنظمة القائمة على LLM. ومع ذلك، في أحد تطبيقاتنا الواقعية، اخترنا ذلك يتجنب قاعدة بيانات متجهة واستخدمت بدلاً من ذلك ملف متجر بسيط ذو قيمة رئيسية، والذي تبين أنه مناسب بشكل أفضل بكثير.

قبل أن أتعمق في ذلك، دعونا نستكشف نسخة بسيطة ومعممة من السيناريو الخاص بنا لشرح السبب.

مثال فو

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

بمعنى آخر، البيانات هي سريع الزوال، سوف يسأل المستخدم فقط أ بعض الأسئلة، ونريد الرد عليهم في أسرع وقت ممكن.

توقف الآن للحظة واسأل نفسك:

أين يجب أن أخزن هذه التضمينات؟


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

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

لذا فإن السؤال الحقيقي ليس “هل يجب أن أستخدم قاعدة بيانات متجهة؟” لكن “هل عمل الفهرسة يستحق كل هذا العناء؟للإجابة على ذلك، يمكننا أن ننظر إلى معيار بسيط.

المقارنة المعيارية: الاسترجاع بدون فهرس مقابل الاسترجاع المفهرس

تصوير جوليا فياندر على Unsplash

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

نريد المقارنة بين نظامين:

  1. لا يوجد فهرسة على الإطلاق، ما عليك سوى الاحتفاظ بالتضمينات في الذاكرة ومسحها ضوئيًا مباشرةً.
  2. قاعدة بيانات المتجهات، حيث ندفع تكلفة الفهرسة مقدمًا لجعل كل استعلام أسرع.

أولاً، فكر في نهج “عدم وجود ناقل قاعدة بيانات”. عندما يأتي استعلام، فإننا نحسب أوجه التشابه بين تضمين الاستعلام وجميع عمليات التضمين المخزنة، ثم نحدد الجزء العلوي. هذا مجرد K-Nearest Neighbours بدون أي فهرس.

import numpy as np

def run_knn(embeddings: np.ndarray, query_embedding: np.ndarray, top_k: int) -> np.ndarray:
    sims = embeddings @ query_embedding
    return sims.argsort()[-top_k:][::-1]

يستخدم الكود المنتج النقطي كبديل لتشابه جيب التمام (بافتراض المتجهات المقيسة) ويقوم بفرز الدرجات للعثور على أفضل التطابقات. انها حرفيا فقط يقوم بمسح جميع المتجهات ويختار الأقرب منها.

الآن، دعونا نلقي نظرة على ما تفعله قاعدة بيانات المتجهات عادةً. تحت الغطاء، تعتمد معظم قواعد بيانات المتجهات على مؤشر أقرب جار تقريبي (ANN). تستبدل أساليب ANN القليل من الدقة من أجل زيادة كبيرة في سرعة البحث، وواحدة من الخوارزميات الأكثر استخدامًا لهذا الغرض هي HNSW. سنستخدم مكتبة hnswlib لمحاكاة سلوك الفهرس.

import numpy as np
import hnswlib

def create_hnsw_index(embeddings: np.ndarray, num_dims: int) -> hnswlib.Index:
    index = hnswlib.Index(space="cosine", dim=num_dims)
    index.init_index(max_elements=embeddings.shape[0])
    index.add_items(embeddings)
    return index

def query_hnsw(index: hnswlib.Index, query_embedding: np.ndarray, top_k: int) -> np.ndarray:
    labels, distances = index.knn_query(query_embedding, k=top_k)
    return labels[0]

لمعرفة أين تنتهي المقايضة، يمكننا إنشاء بعض التضمينات العشوائية، وتطبيعها، وقياس المدة التي تستغرقها كل خطوة:

import time
import numpy as np
import hnswlib
from tqdm import tqdm

def run_benchmark(num_embeddings: int, num_dims: int, top_k: int, num_iterations: int) -> None:
    print(f"Benchmarking with {num_embeddings} embeddings of dimension {num_dims}, retrieving top-{top_k} nearest neighbors.")

    knn_times: list[float] = []
    index_times: list[float] = []
    hnsw_query_times: list[float] = []

    for _ in tqdm(range(num_iterations), desc="Running benchmark"):
        embeddings = np.random.rand(num_embeddings, num_dims).astype('float32')
        embeddings = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True)
        query_embedding = np.random.rand(num_dims).astype('float32')
        query_embedding = query_embedding / np.linalg.norm(query_embedding)

        start_time = time.time()
        run_knn(embeddings, query_embedding, top_k)
        knn_times.append((time.time() - start_time) * 1e3)

        start_time = time.time()
        vector_db_index = create_hnsw_index(embeddings, num_dims)
        index_times.append((time.time() - start_time) * 1e3)

        start_time = time.time()
        query_hnsw(vector_db_index, query_embedding, top_k)
        hnsw_query_times.append((time.time() - start_time) * 1e3)

    print(f"BENCHMARK RESULTS (averaged over {num_iterations} iterations)")
    print(f"[Naive KNN] Average search time without indexing: {np.mean(knn_times):.2f} ms")
    print(f"[HNSW Index] Average index construction time: {np.mean(index_times):.2f} ms")
    print(f"[HNSW Index] Average query time with indexing: {np.mean(hnsw_query_times):.2f} ms")

run_benchmark(num_embeddings=50000, num_dims=1536, top_k=5, num_iterations=20)

نتائج

في هذا المثال، نستخدم 50000 تضمينًا بـ 1536 بُعدًا (مطابقة أبعاد OpenAI text-embedding-3-small) واسترداد أفضل 5 جيران. ستختلف النتائج الدقيقة باختلاف التكوينات، لكن النمط الذي نهتم به هو نفسه.

أنا أشجعك على تشغيل المعيار باستخدام أرقامك الخاصة، فهي أفضل طريقة لمعرفة كيفية تنفيذ المقايضات في حالة الاستخدام المحددة الخاصة بك.

في المتوسط، يستغرق بحث KNN الساذج 24.54 مللي ثانية لكل استعلام. يستغرق إنشاء مؤشر HNSW لنفس التضمينات حوالي 277 ثانية. بمجرد إنشاء الفهرس، يستغرق كل استعلام حوالي 0.47 مللي ثانية.

ومن هنا يمكننا تقدير نقطة التعادل. الفرق بين KNN الساذج والاستعلامات المفهرسة هو 24.07 مللي ثانية لكل استعلام. وهذا يعني أنك بحاجة 11.510 استعلامًا قبل الوقت الموفر في كل استعلام يعوض الوقت المستغرق في إنشاء الفهرس.

تم إنشاؤه باستخدام الكود المعياري: رسم بياني يقارن KNN الساذج وكفاءة البحث المفهرسة

علاوة على ذلك، حتى مع وجود قيم مختلفة لعدد التضمينات وtop-k، تظل نقطة التعادل في آلاف الاستعلامات وتظل ضمن نطاق ضيق إلى حد ما. لا تحصل على سيناريو حيث تبدأ الفهرسة في تحقيق النتائج بعد بضع عشرات من الاستعلامات فقط.

تم إنشاؤه باستخدام الكود المعياري: رسم بياني يوضح نقاط التعادل لأعداد التضمين المختلفة وإعدادات أعلى k (الصورة بواسطة المؤلف)

الآن قارن ذلك بمثال Foo. يقوم المستخدم بتحميل مجموعة صغيرة من الملفات ويطرح بعض الأسئلة، وليس الآلاف. لا يصل النظام أبدًا إلى النقطة التي يؤتي فيها المؤشر ثماره. وبدلاً من ذلك، تقوم خطوة الفهرسة ببساطة بتأخير اللحظة التي يتمكن فيها النظام من الإجابة على السؤال الأول وتضيف تعقيدًا تشغيليًا.

بالنسبة لهذا النوع من السياق قصير العمر لكل مستخدم، فإن أسلوب KNN البسيط في الذاكرة ليس فقط أسهل في التنفيذ والتشغيل، ولكنه أيضًا أسرع من البداية إلى النهاية.

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

وهذا يمنحنا حلاً خفيف الوزن ومنخفض التعقيد ومناسبًا تمامًا لحالة الاستخدام الخاصة بنا سياقات قصيرة العمر ومنخفضة الاستعلام.

مثال من العالم الحقيقي: لماذا اخترنا متجرًا ذا قيمة أساسية

تصوير جافين ألانوود على Unsplash

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

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

إذا عدت إلى المعيار السابق، فمن المفترض أن يؤدي هذا النمط بالفعل إلى تشغيل مستشعر “هذه ليست حالة استخدام لقاعدة بيانات متجهة”.

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

لهذا السبب اخترنا ريديس بدلاً من قاعدة بيانات المتجهات. على الرغم من أن قواعد بيانات المتجهات ممتازة في التعامل مع كميات كبيرة من عمليات التضمين ودعم الاستعلامات السريعة للجيران الأقرب، إلا أنها تقدم فهرسة النفقات العامة، وفي حالتنا، فإن هذه النفقات العامة لا تستحق العناء.

ختاماً

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

ولكن فقط لأنك تستخدم التضمين أو لا يعني إنشاء نظام RAG أنه يجب عليك استخدام قاعدة بيانات متجهة بشكل افتراضي.

كل تقنية قاعدة بيانات لها نقاط القوة والمقايضات الخاصة بها. يبدأ الاختيار الأفضل بالفهم العميق لبياناتك وحالة الاستخدام، بدلاً من اتباع هذا الاتجاه بلا وعي.

لذا، في المرة القادمة التي تحتاج فيها إلى اختيار قاعدة بيانات، توقف للحظة واسأل: هل أختار القاعدة الصحيحة بناءً على المقايضات الموضوعية، أم أنني أختار فقط الخيار الأكثر عصرية والأكثر لمعانًا؟

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

زر الذهاب إلى الأعلى