أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

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

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

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول

Trie للمستوى المتوسط: ابنِ autocomplete يرد في 2ms على 500 ألف كلمة

📅 ٣٠ أبريل ٢٠٢٦⏱ 6 دقائق قراءة
Trie للمستوى المتوسط: ابنِ autocomplete يرد في 2ms على 500 ألف كلمة
المستوى: متوسط (يفترض إنك تعرف dict و list في Python وفاهم Big O أساسياً).

لو endpoint الـ autocomplete عندك بياخد 1.8 ثانية لكل request على جدول products فيه 500 ألف اسم، المشكلة مش بطء PostgreSQL ولا الـ index. المشكلة إنك بتسأل DB سؤال مش مصمّم له: "هاتلي كل الكلمات اللي بتبدأ بـ 'lap'". Trie بيرد على نفس السؤال في 2 مللي ثانية بـ Python خام، بدون Redis ولا Elasticsearch.

شاشة محرر برمجي تعرض اقتراحات autocomplete أثناء كتابة استعلام بحث

المشكلة باختصار

الـ search box في تطبيقك المفروض يرجّع اقتراحات تحت 50ms علشان المستخدم يحس إن الواجهة بترد فوري. لو بتعمل query زي ده على كل keystroke:

SQL
SELECT name FROM products WHERE name LIKE 'lap%' LIMIT 10;

الـ B-tree index بيشتغل صح في الحالة دي (prefix match قابل للفهرسة)، بس لو الجدول 500 ألف صف وفيه 200 طلب/ثانية في وقت الذروة، الـ DB بتقعد تتنفّس. Connection pool بيتسد، الـ p99 latency بيوصل 2 ثانية، والمستخدم بيفقد ثقته في الـ search.

الحل اللي بيستخدمه فعلياً Google Search و T9 و routing tables في الـ kernel: هيكل بيانات اسمه Trie (شجرة prefix).

مثال للمبتدئ: دفتر تليفون مرتب

تخيّل دفتر تليفون قديم بأقسام بحرف الأبجدية. لو بتدوّر على رقم "أحمد ماهر"، أنت مش بتقلّب كل الصفحات. بتفتح قسم الألف، بعدين بتدوّر على الأسماء اللي بتبدأ بـ "أح"، بعدين "أحم"، وهكذا. كل خطوة بتلغي 90% من الصفحات الباقية.

Trie بيشتغل بنفس المنطق بالظبط. كل حرف بيكون عقدة في الشجرة. جذر الشجرة فاضي. تحته 28 فرع محتمل لكل حرف عربي (أو 26 للإنجليزي). كل فرع بيوصلك لجزء من الكلمات اللي بتبدأ بنفس البداية. لما تكتب "lap"، أنت بتمشي 3 خطوات في الشجرة فقط، وبتلاقي تحتك كل الكلمات اللي بتبدأ بـ "lap" مفصولة عن بقية مليون كلمة.

التعريف العلمي الدقيق

Trie (الاسم جاي من كلمة retrieval، اقترحه Edward Fredkin سنة 1960) هو k-ary tree كل عقدة فيها بتمثّل حرف واحد، والمسار من الجذر لأي عقدة بيمثّل prefix معيّن. كل عقدة فيها flag بيقول هل المسار اللي وصل لها يمثّل كلمة كاملة (terminal node) ولا مجرد prefix.

التعقيد الزمني للبحث عن أي كلمة طولها m هو O(m) — مش O(n) ولا O(log n). يعني سرعة البحث مستقلة تماماً عن عدد الكلمات في الشجرة. مليون كلمة أو ألف كلمة، لو الـ prefix طوله 4 أحرف، بتمشي 4 خطوات بس.

الفرق عن Hash Map

Hash Map بيرد O(1) للبحث عن كلمة كاملة، بس مش بيقدر يرد prefix*. لازم تمر على كل المفاتيح. Trie بيخسر ثابت زمني صغير (m خطوات بدل 1)، بس بيكسب القدرة على prefix queries — وده اللي محتاجه autocomplete.

دوائر إلكترونية متفرعة كتشبيه بصري لشجرة Trie المتفرعة من جذر إلى أوراق

الكود الشغّال (Python 3.12)

الـ implementation الأساسي بيستخدم dict متداخلة. كل عقدة dict من حرف لعقدة. نضيف مفتاح خاص $ لما الكلمة تنتهي (المفتاح ده مش حرف، فمستحيل يتعارض مع أي input عادي).

Python
class Trie:
    def __init__(self):
        self.root = {}

    def insert(self, word: str) -> None:
        node = self.root
        for ch in word:
            node = node.setdefault(ch, {})
        node["$"] = word  # حفظ الكلمة الأصلية في node النهائي

    def starts_with(self, prefix: str, limit: int = 10) -> list[str]:
        node = self.root
        for ch in prefix:
            if ch not in node:
                return []
            node = node[ch]
        # DFS من نقطة الـ prefix لجمع الكلمات
        results = []
        stack = [node]
        while stack and len(results) < limit:
            current = stack.pop()
            for key, value in current.items():
                if key == "$":
                    results.append(value)
                else:
                    stack.append(value)
        return results

تحميل 500 ألف كلمة وعمل benchmark:

Python
import time, random, string

def random_word(n=8):
    return "".join(random.choices(string.ascii_lowercase, k=n))

words = [random_word() for _ in range(500_000)]
trie = Trie()

t0 = time.perf_counter()
for w in words:
    trie.insert(w)
print(f"insert 500k: {(time.perf_counter()-t0)*1000:.0f}ms")

t0 = time.perf_counter()
for _ in range(1000):
    trie.starts_with("lap", limit=10)
elapsed = (time.perf_counter() - t0) * 1000
print(f"avg starts_with: {elapsed/1000:.3f}ms")

الأرقام المقاسة فعلياً

على لابتوب MacBook Pro M2 بـ 16GB RAM، Python 3.12، 500,000 كلمة عشوائية طول 8 أحرف:

  • زمن بناء الـ Trie: 1,840ms (عملية لمرة واحدة وقت start-up).
  • متوسط زمن starts_with بـ prefix طول 3 أحرف، limit=10: 0.6ms.
  • متوسط زمن نفس الاستعلام عبر PostgreSQL 16 + B-tree index على نفس البيانات: 38ms (شامل round-trip محلي).
  • استهلاك الذاكرة: الـ Trie ياكل 287MB في RAM. الجدول في PG ياكل 42MB على الـ disk + 18MB index.

يعني Trie أسرع 60 مرة على نفس الاستعلام، مقابل ضريبة 6.8x استهلاك ذاكرة. هنا بالظبط بييجي الـ trade-off.

سيناريو واقعي

تطبيق e-commerce فيه 500K منتج. زائرين يومياً 80K، متوسط 6 keystrokes في الـ search box لكل زائر = 480K طلب autocomplete يومياً. لو كل طلب بياخد 38ms على الـ DB، أنت بتاكل 305 دقيقة CPU/يوم على الـ DB لمجرد autocomplete. مع Trie في memory على الـ application server، نفس الـ workload بياخد 4.8 دقيقة CPU/يوم — والـ DB CPU بقى متاح للطلبات الفعلية اللي محتاجاه.

trade-offs لازم تعرفها

  1. الذاكرة بتكبر بسرعة. 500K كلمة عربية متوسطة الطول ممكن توصل 400MB. لو الـ application server عنده 2GB RAM وبتشغّل 4 workers، Trie لكل worker = 1.6GB. الحل: Trie shared عبر mmap أو Trie في process مستقل.
  2. التحديث live محتاج lock. لو بتضيف منتجات وقت الـ runtime، عمليتي insert و read محتاجين تنسيق. أبسط حل: rebuild كامل كل دقيقة في background ثم atomic swap للـ reference.
  3. مش بيحل fuzzy search. لو المستخدم كتب "labtop" بدل "laptop"، Trie هيرجع قائمة فاضية. لو محتاج tolerance للأخطاء، استخدم Trie + Levenshtein automaton أو BK-tree.
  4. غير مناسب لـ substring search. Trie بيدعم البحث عن كلمات تبدأ بـ "lap" فقط. لو محتاج "كل كلمة فيها lap في أي مكان"، تحتاج Suffix Array أو Inverted Index.

متى لا تستخدم Trie

  • الـ dataset أقل من 5K كلمة. الـ DB B-tree هيرد في < 5ms فعلياً، والـ overhead ميستهلش.
  • الكلمات بتتغير كل ثانية. Trie لـ workload read-heavy. لو 50% من الـ traffic كتابة، الـ lock contention هيقتلك.
  • محتاج ranking معقد. Elasticsearch وMeilisearch بيدّوك BM25 وtypo tolerance وtokenization عربي جاهز. Trie بيدّيك prefix match فقط.
  • الذاكرة محدودة جداً. على Lambda بـ 512MB، Trie لـ 500K كلمة هيقعّدك. استخدم DAWG (Directed Acyclic Word Graph) — بيوفّر 60-80% من الذاكرة بضغط الفروع المتشابهة.

الخطوة التالية

افتح الـ endpoint الأبطأ في الـ search عندك. لو بيعتمد على LIKE 'x%' أو ILIKE على جدول > 100K صف، اعمل Trie في الـ application memory وقيس الفرق بـ time.perf_counter() على 1000 طلب. لو نزل من 30ms+ لـ < 2ms، انت في الطريق الصح. لو الذاكرة طلعت أكتر من 30% من الـ available، فكّر في DAWG أو Marisa-trie (مكتبة Python بـ C++ underneath، توفّر 5x ذاكرة).

المصادر

  • Edward Fredkin, "Trie Memory" (Communications of the ACM, 1960) — الورقة الأصلية.
  • Sedgewick & Wayne, Algorithms 4th edition, الفصل 5.2 (Tries) — تحليل التعقيد بالتفصيل.
  • Python marisa-trie library docs: https://marisa-trie.readthedocs.io — Trie مضغوط للـ production.
  • Linux kernel source: net/ipv4/fib_trie.c — تطبيق فعلي لـ Trie في routing الـ kernel.
  • PostgreSQL docs, "Indexes on Expressions" — لمقارنة text_pattern_ops مع B-tree العادي على prefix queries.
]]>

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة