كان البحث عن ‘الشبيه’ في بياناتنا مثل البحث عن إبرة في الكون: كيف أنقذتنا ‘التجزئة الحساسة للموقع’ (LSH) من جحيم المقارنات الكاملة؟

ليلة لا تُنسى مع “مكتبتنا” وقاعدة البيانات التي كادت أن تستسلم

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

المشكلة الحقيقية ظهرت عندما أردنا إضافة ميزة “البحث عن المقالات المشابهة” أو منع إضافة المقالات المكررة. في البداية، كانت الفكرة بسيطة وساذجة: عندما يأتي مقال جديد، نقارنه مع كل المقالات الموجودة في قاعدة البيانات. في الأيام الأولى، ومع بضعة آلاف من المقالات، كان الأمر يعمل “ماشي حاله”. لكن عندما وصلنا إلى مئات الآلاف، ثم الملايين، بدأت الكارثة. أصبحت عملية إضافة مقال واحد تستغرق دقائق، وسيرفر قاعدة البيانات يئن تحت وطأة الاستعلامات التي لا تنتهي. كنا حرفيًا في جحيم المقارنات الكاملة (Brute-force comparison).

في ليلة من تلك الليالي، وأنا جالس في مكتبي محاطًا بأكواب الميرمية الفارغة، أنظر إلى شاشة مراقبة الأداء وهي تصرخ باللون الأحمر، شعرت أننا وصلنا إلى حائط مسدود. قلت في نفسي: “يا أبو عمر، لازم يكون فيه طريقة أذكى من هيك. مش معقول شركات زي جوجل وفيسبوك بتقارن كل بوست جديد مع كل البوستات القديمة!”. وهنا، ومثل “كف” صحوة على الرقبة، تذكرت مفهومًا كنت قد قرأته في إحدى الأوراق البحثية: Locality-Sensitive Hashing (LSH). كانت تلك اللحظة هي بداية النهاية لكابوسنا.

جحيم المقارنات الكاملة: لما كل مقارنة جديدة كانت مسمارًا في نعش الأداء

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

الآن، ماذا لو أردت أن تجد كل الأزواج المتشابهة داخل المليون مقال نفسهم؟ هنا تبدأ الحسابات الفلكية. عدد المقارنات التي تحتاجها يتناسب مع مربع عدد العناصر (n)، وهو ما نطلق عليه في علم الحاسوب تعقيدًا من الدرجة الثانية أو O(n²).

لو كان لدينا مليون مقال (n = 1,000,000)، فإن عدد المقارنات سيكون تقريبًا (n * (n-1)) / 2، وهو رقم يقترب من 500 مليار مقارنة! هذا ليس بطئًا، هذا مستحيل عمليًا.

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

بصيص النور: التجزئة الحساسة للموقع (LSH) وفلسفتها العبقرية

هنا يأتي دور الـ LSH. الفكرة وراءها بسيطة وعبقرية في آن واحد، وهي عكس كل ما تعلمناه عن دوال التجزئة (Hashing Functions) التقليدية مثل SHA-256 أو MD5.

عكس كل ما تعلمناه عن دوال التجزئة!

دوال التجزئة التقليدية (Cryptographic Hashing) مصممة بحيث لو غيرت حرفًا واحدًا فقط في الإدخال، فإن الإخراج (الـ hash) يتغير بشكل كامل وعشوائي. هذا ممتاز للأمان والتحقق من سلامة البيانات، ولكنه كارثي للبحث عن التشابه.

أما التجزئة الحساسة للموقع (LSH) فتقوم بالعكس تمامًا. فلسفتها تقول:

“إذا كان عنصران (مثل مقالين) متشابهين، فيجب أن يكون لهما نفس الـ hash (أو hash متشابه) باحتمالية عالية. وإذا كانا مختلفين، فاحتمالية أن يكون لهما نفس الـ hash منخفضة جدًا.”

بكلمات بسيطة، LSH هي طريقة “ذكية” لوضع العناصر المتشابهة في نفس “الصندوق” (Bucket) دون الحاجة لمقارنتها مع كل العناصر الأخرى. بعد ذلك، كل ما علينا فعله هو مقارنة العناصر الموجودة داخل نفس الصندوق فقط، وهو عدد أقل بكثير من إجمالي العناصر.

كيف تعمل الـ LSH بالزبط؟ خلينا نفصّصها حبة حبة

لتبسيط الأمور، سأشرح واحدة من أشهر تقنيات LSH المستخدمة مع البيانات النصية، وهي التي تعتمد على ما يسمى MinHashing مع Jaccard Similarity. لا تخف من الأسماء، فالمفهوم أبسط مما يبدو.

الخطوة الأولى: تحويل النص إلى “مجموعات” (Sets)

الحواسيب لا تفهم النصوص كما نفهمها. لذا، أول خطوة هي تحويل كل مقال إلى شيء يمكن قياسه رياضيًا. الطريقة الشائعة هي تحويل النص إلى مجموعة من “القطع” الصغيرة المتتابعة التي تسمى Shingles أو k-grams.

مثلاً، لو كانت لدينا الجملة “البحث عن العناصر المتشابهة”، وقمنا بتقسيمها إلى shingles من 3 كلمات:

  • “البحث عن العناصر”
  • “عن العناصر المتشابهة”

وهكذا، يصبح كل مقال عبارة عن مجموعة (Set) فريدة من هذه الـ shingles.

الخطوة الثانية: صناعة “البصمة” أو التوقيع (MinHashing)

الآن بعد أن أصبح كل مقال عبارة عن مجموعة ضخمة من الـ shingles، لا يزال من المكلف مقارنة هذه المجموعات مباشرة. هنا يأتي دور الـ MinHash.

تخيل أن لدينا 100 دالة تجزئة (hash functions) مختلفة (h1, h2, …, h100). لكل مقال، سنقوم بالتالي:

  1. نمرر كل shingle في مجموعة المقال عبر كل دالة من دوال التجزئة المئة.
  2. لكل دالة تجزئة (مثلاً h1)، نحتفظ فقط بأصغر قيمة hash نتجت عن كل الـ shingles في المقال.

النتيجة؟ لكل مقال، سنحصل على “بصمة” أو “توقيع” (Signature) مكون من 100 رقم (قيمة الـ MinHash لكل دالة من الدوال المئة). هذا التوقيع هو تمثيل مضغوط جدًا للمقال الأصلي.

الجميل في الأمر أن التشابه بين توقيعي مقالين (نسبة الأرقام المتطابقة في التوقيعين) هو تقريب ممتاز للتشابه الفعلي بين المقالين الأصليين (ما يسمى Jaccard Similarity)!

الخطوة الثالثة (السحر الحقيقي): تقنية الأشرطة (Banding Technique)

لدينا الآن توقيعات صغيرة لكل المقالات، ولكننا ما زلنا بحاجة لمقارنة كل توقيع مع الآخرين، وهذا يعيدنا إلى مشكلة O(n²). السحر الحقيقي لـ LSH يكمن في الخطوة الأخيرة: تقنية الأشرطة (Banding).

الفكرة كالتالي:

  1. نأخذ التوقيع المكون من 100 رقم ونقسمه إلى “أشرطة” (bands). مثلاً، 20 شريطًا، كل شريط يحتوي على 5 أرقام. (20 * 5 = 100).
  2. لكل مقال، نأخذ كل شريط من أشرطته العشرين ونقوم بعمل hash لهذا الشريط كوحدة واحدة. هذا الـ hash يحدد “الصندوق” (Bucket) الذي سيقع فيه هذا الشريط.
  3. الآن، نعتبر أي مقالين “مرشحين للمقارنة” (Candidate Pair) إذا تطابقا في شريط واحد على الأقل من الأشرطة العشرين. أي إذا وقعا في نفس “الصندوق” لأي من الأشرطة.

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

يلا نكتب كود: مثال عملي باستخدام Python

الحكي النظري جميل، ولكن “زي ما بنحكي”، الإيد اللي في المي مش زي الإيد اللي في النار. دعونا نرى كيف يبدو هذا عمليًا باستخدام لغة Python ومكتبة رائعة اسمها datasketch تبسط العملية كلها.


# أولاً، قم بتثبيت المكتبة
# pip install datasketch

import numpy as np
from datasketch import MinHash, MinHashLSH

# بياناتنا (مثلاً، مقالات مبسطة)
data = {
    1: "الذكاء الاصطناعي هو مستقبل التكنولوجيا ويتطور بسرعة",
    2: "الذكاء الاصطناعي يعتبر مستقبل التكنولوجيا ويتطور بسرعة كبيرة", # شبيه جداً بـ 1
    3: "تعلم الآلة هو فرع من فروع الذكاء الاصطناعي",
    4: "الطقس اليوم مشمس وجميل في مدينة رام الله", # مختلف تمامًا
    5: "يعتبر تعلم الآلة فرعًا أساسيًا من الذكاء الاصطناعي" # شبيه بـ 3
}

# --- الخطوة 1 و 2: إنشاء MinHash لكل مستند ---

# عدد دوال التجزئة التي سنستخدمها (جزء من التوقيع)
num_perm = 128
minhashes = {}

print("1. Creating MinHashes for each document...")
for doc_id, text in data.items():
    # تحويل النص إلى مجموعة من الكلمات (يمكن استخدام shingles لنتائج أدق)
    shingles = set(text.split())
    
    # إنشاء كائن MinHash
    m = MinHash(num_perm=num_perm)
    
    # تحديث الـ MinHash بكل كلمة في المستند
    for shingle in shingles:
        m.update(shingle.encode('utf8'))
        
    minhashes[doc_id] = m
    print(f"  - Created MinHash for doc {doc_id}")

# --- الخطوة 3: إعداد LSH والفهرسة ---

# إعداد LSH. العتبة (threshold) تحدد مدى التشابه المطلوب
# عدد الأشرطة (bands) يتم حسابه بناءً على العتبة و num_perm
# threshold=0.5 يعني أننا نبحث عن المستندات المتشابهة بنسبة 50% على الأقل
lsh = MinHashLSH(threshold=0.5, num_perm=num_perm)

print("n2. Indexing documents in LSH...")
for doc_id, m in minhashes.items():
    # إضافة كل MinHash إلى فهرس LSH مع معرّف المستند
    lsh.insert(doc_id, m)
    print(f"  - Indexed doc {doc_id}")

# --- البحث عن المتشابهات ---

print("n3. Querying for similar documents...")
# لنبحث عن المستندات المشابهة للمستند رقم 1
query_id = 1
query_minhash = minhashes[query_id]
result = lsh.query(query_minhash)

print(f"nDocuments similar to doc {query_id}: {result}")

# لنبحث عن المستندات المشابهة للمستند رقم 3
query_id = 3
query_minhash = minhashes[query_id]
result = lsh.query(query_minhash)

print(f"Documents similar to doc {query_id}: {result}")

عند تشغيل هذا الكود، ستلاحظ أن الاستعلام عن المستند 1 سيعيد المستند 2 كشبيه، والاستعلام عن المستند 3 سيعيد المستند 5. بينما لن يظهر المستند 4 في أي من النتائج لأنه مختلف تمامًا. كل هذا تم بكفاءة عالية دون مقارنات كاملة!

من “صندوق عدة” أبو عمر: نصائح ذهبية لتطبيق LSH

  • الموازنة هي المفتاح: اختيار عدد دوال التجزئة (num_perm) والعتبة (threshold) هو فن وعلم. زيادة num_perm تحسن الدقة ولكن تزيد التكلفة. العتبة المنخفضة جدًا ستعطيك الكثير من النتائج الخاطئة (False Positives)، والعتبة المرتفعة جدًا قد تفوتك بعض المتشابهات (False Negatives). ابدأ بقيم تقديرية ثم قم بالتجربة والتعديل بناءً على بياناتك.
  • ليست للبيانات الصغيرة: إذا كان لديك بضعة آلاف من السجلات، قد تكون المقارنة المباشرة أسرع وأبسط. LSH تظهر قوتها الحقيقية مع مئات الآلاف والملايين من السجلات.
  • اختر عائلة LSH المناسبة: ما شرحناه (MinHash) هو الأفضل لـ Jaccard Similarity (تشابه المجموعات). هناك عائلات LSH أخرى مثل Random Projection لـ Cosine Similarity (المستخدمة مع متجهات الكلمات مثل Word2Vec) و LSH للـ Hamming Distance. اختر الأداة المناسبة للمهمة.
  • فكر في التخزين: فهرس LSH يمكن أن يصبح كبيرًا. ستحتاج إلى استراتيجية لتخزينه بكفاءة، ربما في قاعدة بيانات NoSQL مثل Redis أو في الذاكرة إذا كانت الموارد تسمح.

الخلاصة: متى تطلق العنان لـ LSH؟

خوارزمية LSH ليست حلاً لكل المشاكل، لكنها أداة خارقة عندما يتعلق الأمر بالبحث التقريبي عن الجيران الأقرب (Approximate Nearest Neighbor) في مجموعات البيانات الضخمة. إنها العمود الفقري للعديد من الأنظمة التي تتعامل مع:

  • اكتشاف المحتوى المكرر أو المسروق (مقالات، أكواد برمجية).
  • تجميع الأخبار المتشابهة من مصادر مختلفة.
  • اقتراح المنتجات المشابهة في المتاجر الإلكترونية.
  • البحث عن الصور المتشابهة بصريًا.

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

نصيحة أخيرة: ابدأ صغيرًا. جرب LSH على مجموعة فرعية من بياناتك، افهم كيف تتأثر النتائج بتغيير المعلمات، وعندما تشعر بالثقة، أطلق العنان لقوتها على بياناتك الكاملة. بالتوفيق يا جماعة!

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

ذكاء اصطناعي

نماذجنا اللغوية كانت تهذي: كيف أنقذ “التوليد المعزز بالاسترجاع” (RAG) مشاريعنا من جحيم الهلوسة؟

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

22 مايو، 2026 قراءة المزيد
تجربة المستخدم والابداع البصري

كانت واجهاتنا تتحدث بلغة الروبوتات: كيف أنقذنا ‘فن الكتابة لتجربة المستخدم’ (UX Writing) من جحيم حيرة المستخدم؟

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

22 مايو، 2026 قراءة المزيد
التوظيف وبناء الهوية التقنية

كان ملفي على GitHub مقبرة للمشاريع المنسية: كيف أنقذني ‘التنظيم القصصي’ من جحيم الانطباع الأول السيء؟

ملف GitHub الفوضوي كاد أن يكلّفني وظيفة أحلامي. في هذه المقالة، أشارككم كـ"أبو عمر" كيف حوّلت 'مقبرة المشاريع' هذه إلى قصة احترافية تروي مسيرتي التقنية...

22 مايو، 2026 قراءة المزيد
التوسع والأداء العالي والأحمال

كانت قاعدة بياناتنا تختنق: كيف أنقذتنا “النسخ المتماثلة للقراءة” (Read Replicas) من جحيم بطء الاستعلامات؟

أشارككم قصة حقيقية من قلب المعركة، عندما كاد تطبيقنا أن ينهار تحت ضغط المستخدمين. سأشرح لكم كيف كانت "النسخ المتماثلة للقراءة" (Read Replicas) طوق النجاة...

21 مايو، 2026 قراءة المزيد
التكنلوجيا المالية Fintech

من كابوس التحقق اليدوي إلى onboarding بدقائق: كيف أنقذت eKYC شركات التكنولوجيا المالية

كان التحقق من هوية العميل كابوساً يدوياً يهدد بفشل المشاريع الناشئة. في هذه المقالة، أشارككم قصة حقيقية من قلب المعركة، وكيف أنقذتنا تقنية "اعرف عميلك...

21 مايو، 2026 قراءة المزيد
البودكاست