MongoDB: $facet + $bucket + البحث النصي/الجغرافي المكاني
آخر تحديث: 2026-08-26
ميزات التجميع المتقدمة — إتقان استخدام $facet و$bucket والبحث النصي والاستعلامات الجغرافية المكانية لبناء قدرات شاملة لتحليل البيانات.
المواضيع الأربعة الرئيسية لهذه الدورة: يتناول $facet (البحث المتوازي متعدد القنوات) التحدي المتمثل في «الحصول على نتائج متعددة الأبعاد من استعلام واحد»؛ ويتناول $bucket (تصنيف البيانات إلى فئات) التحدي المتمثل في «إحصاءات الفواصل الزمنية»؛ ويتناول البحث النصي التحدي المتمثل في «البحث عن الكلمات المفتاحية»؛ بينما تتناول الاستعلامات الجغرافية المكانية التحدي المتمثل في «المسافة والنطاق». ورغم أن هذه المواضيع الأربعة قد تبدو مستقلة عن بعضها، إلا أنها تشكل مجتمعةً القدرات الكاملة لنظام البحث في التجارة الإلكترونية — نتائج البحث + الإحصاءات المتعددة الأوجه + تصنيف الأسعار + المتاجر القريبة.
1. ما ستتعلمه
- $facet الحساب المتوازي متعدد القنوات
- $bucket / $bucketAuto: تجميع البيانات في مجموعات
- فهرس النص + $text/$search
- مؤشر 2dsphere الجغرافي المكاني + $geoNear / $geoWithin
القيمة التعليمية للبحث المتقدم: تشكل $facet + $bucket + $text + $geoNear الإمكانات الكاملة للبحث في MongoDB — حيث يُعد $facet العمود الفقري لهيكلية البحث (المعالجة المتوازية متعددة الأبعاد)، و$bucket أداة لتحليل توزيع البيانات (الرسوم البيانية التراكمية)، و$text محركًا لاسترجاع الكلمات المفتاحية، و$geoNear جوهر خدمات تحديد الموقع. إن إتقان هذه العوامل الأربعة يعني أنه يمكنك بناء نظام بحث لا يعتمد على Elasticsearch — مما يوفر مبلغًا كبيرًا من تكاليف البنية التحتية للمشاريع الصغيرة والمتوسطة الحجم.
تطور أنظمة البحث: تطور متطلبات البحث في المشروع — المرحلة 1 (المنتج القابل للتسويق: MVP): $match + $sort بسيطان (التصفية والفرز حسب الفئة/السعر)؛ المرحلة 2 (البحث المتعدد الأوجه): + $facet + $bucket (إحصائيات متعددة الأبعاد على صفحة البحث)؛ المرحلة 3 (البحث عن النص الكامل): + فهرسة النص + $text (البحث عن الكلمات المفتاحية)؛ المرحلة 4 (LBS): + 2dsphere + $geoNear (البحث عن المواقع القريبة)؛ المرحلة 5 (البحث المتخصص): الترحيل إلى Elasticsearch (تقسيم الكلمات الصينية، والمرادفات، ونظام بينيين). تظل معظم المشاريع في المرحلتين 3 و4 ولا تنتقل إلى المرحلة 5 إلا عندما يتجاوز حجم البيانات أو تعقيد البحث قدرات MongoDB.
اعتبارات التشغيل لـ $facet: هناك ثلاثة قيود رئيسية على $facet في بيئة التشغيل: 1. حد الذاكرة: يتشارك كل خط أنابيب فرعي حدًا للذاكرة يبلغ 100 ميغابايت؛ ولا يجوز أن يتجاوز إجمالي مخرجات جميع خطوط الأنابيب الفرعية هذا الحد. عند التعامل مع مجموعات البيانات الكبيرة، يجب استخدام $match لتصفية البيانات قبل تطبيق $facet؛ 2. لا تستخدم $collation (قواعد الفرز) في خطوط الأنابيب الفرعية؛ إذا كان الفرز باللغة الصينية مطلوبًا، فقم بمعالجته خارج $facet؛ 3. تجنب استخدام $lookup في خطوط الأنابيب الفرعية (قد يؤدي استخدام $lookup داخل $facet إلى مشكلات كارثية في الأداء — حيث تنفذ كل خط أنابيب فرعي استعلام ربط مرة واحدة). أفضل الممارسات: استخدم $match أو $project قبل $facet لتقليل حجم المدخلات؛ قصر خطوط الأنابيب الفرعية على الإحصائيات الخفيفة ($group، $bucket، $count)، ووضع العمليات الثقيلة ($lookup، $sort) خارج $facet.
graph TB
Input[Input Set<br/>1000 Document] --> Facet[$facet<br/>Parallel Execution]
Facet --> B1[Branch 1<br/>topExpensive<br/>Sort by price (ascending) 10]
Facet --> B2[Branch 2<br/>byCategory<br/>Categorical Statistics]
Facet --> B3[Branch 3<br/>priceRanges<br/>Prices by Bin]
Facet --> B4[Branch 4<br/>totalStats<br/>Overview Statistics]
B1 --> Output[Multidimensional Results<br/>Returns one result]
B2 --> Output
B3 --> Output
B4 --> Output
style Facet fill:#d4edda
style Output fill:#cce5ff
نموذج التنفيذ المتوازي في $facet: لا يُعد التوازي في $facet «تسريعًا متعدد الخيوط» — حيث تنفذ MongoDB كل خط أنابيب فرعي بالتسلسل ضمن خيط واحد، لكنها تتشارك نفس اللقطة الداخلة، لذا فهي متوازية من الناحية المنطقية. ولا تكمن الميزة الحقيقية للأداء في التنفيذ المتوازي، بل في «تقليل عدد رحلات الشبكة ذهابًا وإيابًا» — حيث تتطلب الأبعاد الإحصائية الأربعة استعلامًا واحدًا فقط بدلاً من أربعة. ويُحسب استهلاك الذاكرة في $facet بمجموع استهلاك جميع الأنابيب الفرعية — فإذا تم إدخال 1,000 مستند، وقامت كل من الأنابيب الفرعية الأربعة بمعالجة 1,000 مستند، فإن إجمالي استهلاك الذاكرة يعادل تقريبًا استهلاك 4,000 مستند.
تصميم بنية مخرجات $facet: مخرجات $facet عبارة عن مستند تكون فيه المفاتيح أسماء خطوط الأنابيب الفرعية، والقيم مصفوفات لنتائج خطوط الأنابيب الفرعية. عند تصميم خطوط الأنابيب الفرعية، ضع في اعتبارك ما يلي: 1. كل خط أنابيب فرعي مستقل — ولا يمكنه الرجوع إلى نتائج خطوط الأنابيب الفرعية الأخرى؛ 2. لا يؤثر ترتيب خطوط الأنابيب الفرعية على النتائج (فهي تعمل بشكل متوازٍ من الناحية المنطقية)؛ 3. يمكن استخدام أي مرحلة تجميع ($match، $group، $sort، $lookup، إلخ) داخل خط أنابيب فرعي؛ 4. النتيجة الفارغة تُرجع مصفوفة فارغة [] بدلاً من القيمة null. عند تحليل نتائج $facet في الواجهة الأمامية، استخدم Object.keys() للتكرار عبر كل بُعد.
تكلفة $facet والبدائل: تكلفة $facet = حجم البيانات المدخلة × عدد خطوط الأنابيب الفرعية. عندما يكون حجم البيانات المدخلة كبيرًا (>100,000 سجل) ويوجد عدد كبير من خطوط الأنابيب الفرعية (>5)، قد يتجاوز استهلاك الذاكرة الحدود المسموح بها. البدائل: 1. تقسيم عمليات $facet الكبيرة إلى عدة استعلامات تجميع مستقلة — مع التضحية بكفاءة الشبكة لصالح أمان الذاكرة؛ 2. إضافة $match/$project قبل $facet لتقليل حجم المدخلات بشكل كبير؛ 3. استخدام العروض المادية ($merge/$out) لحساب النتائج الإحصائية مسبقًا. معايير الاختيار: استخدم $facet لحجم البيانات الصغير (<10,000 سجل) والعروض المادية لحجم البيانات الكبير.
2. المعالجة المتوازية متعددة القنوات باستخدام $facet
وصف المفهوم: يتيح $facet تشغيل عدة مسارات فرعية مستقلة بالتوازي على نفس مجموعة المستندات المدخلة. ينتج كل مسار فرعي نتائجه الخاصة، والتي يتم دمجها في النهاية في مستند واحد. وتعد هذه الأداة مثالية لإنشاء صفحات نتائج البحث (قائمة + إحصائيات مصنفة + تجميع + مجاميع) — حيث يحل استعلام واحد محل عدة عمليات ذهاب وإياب.
كيفية العمل: يقبل $facet كائنًا يكون مفتاحه هو اسم خط الأنابيب الفرعي وقيمته عبارة عن مصفوفة من المراحل. يقوم MongoDB بتنفيذ جميع خطوط الأنابيب الفرعية بالتوازي على نفس المدخلات، حيث تعالج كل منها البيانات بشكل مستقل. ويكون الناتج مستندًا يتوافق فيه كل مفتاح مع مصفوفة نتائج أحد خطوط الأنابيب الفرعية. ملاحظة: تشترك الأنابيب الفرعية في $facet في المدخلات ولكن لا يمكنها الرجوع إلى نتائج بعضها البعض، ويكون إجمالي استهلاك الذاكرة هو مجموع الذاكرة المستخدمة من قبل جميع الأنابيب الفرعية.
sequenceDiagram
participant Input as Input: 1000 Products
participant F as $facet
participant P1 as Child pipeline1: topExpensive
participant P2 as Child pipeline2: byCategory
participant P3 as Child pipeline3: priceRanges
participant P4 as Child pipeline4: stats
Input->>F: Incoming Document Stream
par Parallel Execution
F->>P1: 1000 Documents → $sort + $limit → 10 items
F->>P2: 1000 Documents → $group → 8 groups
F->>P3: 1000 Documents → $bucket → 5 buckets
F->>P4: 1000 Document → $group → 1 Overview of Articles
end
P1-->>F: topExpensive: [...]
P2-->>F: byCategory: [...]
P3-->>F: priceRanges: [...]
P4-->>F: stats: [...]
F-->>Input: {topExpensive, byCategory, priceRanges, stats}
| خط الأنابيب الفرعي $facet | الاستخدامات النموذجية | تركيبات المراحل الشائعة |
|---|---|---|
| قائمة النتائج | ترقيم الصفحات | $sort + $skip + $limit + $project |
| إحصائيات إجمالية | معلومات عن ترقيم الصفحات | $count |
| إحصائيات الفئة | التصفية حسب المعيار | $group + $sort |
| الترتيب حسب السعر | التصفية حسب النطاق السعري | $bucket / $bucketAuto |
| إحصائيات عامة | المقاييس المجمعة | $group({ _id: null }) |
مبادئ بنية التحليل متعدد الأبعاد: تتمثل القيمة الأساسية لـ $facet في «استعلام واحد، نتائج متعددة الأبعاد» — حيث يتعين على صفحات البحث في مواقع التجارة الإلكترونية عرض العناصر التالية في آن واحد: قوائم المنتجات (مقسمة إلى صفحات)، العدد الإجمالي (معلومات ترقيم الصفحات)، توزيع الأسعار (عوامل التصفية)، وإحصائيات الفئات (الشريط الجانبي). بدون $facet، سيتطلب ذلك أربعة استعلامات منفصلة (أربع دورات ذهاب وإياب عبر الشبكة)؛ أما مع $facet، فيُرجع استعلام واحد جميع الأبعاد، مما يؤدي إلى زيادة سرعة العرض في الواجهة الأمامية بأربعة أضعاف.
قيود الذاكرة الخاصة بـ $facet: التنفيذ المتوازي لـ $facet ليس متوازياً بالفعل — حيث تنفذ MongoDB خطوط الأنابيب الفرعية بالترتيب الذي تم تعريفها به، لكنها تشترك في نفس المدخلات. ويُحسب استهلاك الذاكرة بمجموع الذاكرة التي يستخدمها كل خط أنابيب فرعي: 1,000 مستند × 4 خطوط أنابيب فرعية = الذاكرة المطلوبة لمعالجة 4,000 مستند. استراتيجيات التخفيف: 1. استخدم $match قبل $facet لتقليل حجم المدخلات؛ 2. استخدم $project في الأنابيب الفرعية لتبسيط الحقول؛ 3. حدد عدد الأنابيب الفرعية (يُنصح بـ 3–5)؛ 4. بالنسبة لمجموعات البيانات الكبيرة جدًّا، قم بتعيين allowDiskUse: true.
عزل خطوط الأنابيب الفرعية لـ $facet: كل خط أنابيب فرعي لـ $facet معزول تمامًا — فلا يمكنه الرجوع إلى نتائج خطوط الأنابيب الفرعية الأخرى، ولا يمكنه مشاركة الحسابات الوسيطة. إذا تطلبت عدة خطوط أنابيب فرعية نفس المعالجة المسبقة (مثل استخراج الحقول بواسطة $project)، فيجب على كل خط أنابيب فرعي تنفيذها بشكل متكرر. وتتمثل ميزة العزل في التوازي الخالي من التبعية؛ أما العيب فهو الحساب الزائد عن الحاجة. إن العبء الناتج عن القليل من الحسابات الزائدة أقل بكثير من وقت الانتظار المرتبط بالتنفيذ التسلسلي — وهذه هي المفاضلة الأساسية في تصميم $facet.
نمط تصميم الأنابيب الفرعية لـ $facet: يتبع تصميم الأنابيب الفرعية لـ $facet مبدأ «بعد واحد لكل أنبوب»— 1. أنبوب قائمة النتائج: $sort + $skip + $limit + $project (للعرض المقسم إلى صفحات؛ يجب استخدام $limit لتحديد عدد النتائج)؛ 2. خط أنابيب العدد الإجمالي: $count (يعرض رقمًا فقط؛ وهو خط الأنابيب الأقل وزنًا)؛ 3. خط أنابيب إحصائيات الفئات: $group + $sort + $limit (يحسب العدد مجمّعًا حسب حقل الفئة؛ ويعرض $limit أفضل N)؛ 4. خط أنابيب تجميع الأسعار: $bucket/$bucketAuto (يقوم بتجميع البيانات حسب النطاق؛ ويُخرج بيانات الرسم البياني التراكمي)؛ 5. خط أنابيب النظرة العامة: $group({_id: null, avg: {$avg}, sum: {$sum}}) (إحصائيات عامة؛ يُرجع مستندًا واحدًا فقط). تغطي خطوط الأنابيب الخمسة هذه 90% من متطلبات البيانات لصفحات البحث.
تفاصيل نموذج تنفيذ $facet: على الرغم من أن $facet يبدو متوازياً، إلا أن MongoDB تنفذ كل خط أنابيب فرعي في مؤشر ترابط واحد — ولا يحدث التوازي الحقيقي إلا في المجموعات المقسمة (حيث يعالج كل جزء بياناته بشكل مستقل، ويقوم mongos بدمج النتائج). في بيئة المثيل الفردي أو مجموعة النسخ المتماثلة، يتم تنفيذ الأنابيب الفرعية بالتسلسل حسب ترتيب تعريفها. ومع ذلك، لا يزال $facet أسرع من الاستعلامات المستقلة المتعددة — 1. فهو يقوم بمسح البيانات المدخلة مرة واحدة فقط (تشترك كل أنبوب فرعي في قراءة نفس البيانات)؛ 2. لا يتطلب سوى رحلة شبكة واحدة ذهابًا وإيابًا (العميل → MongoDB → إرجاع النتائج الكاملة)؛ 3. يتجنب العبء الإضافي لعمليات $match المتكررة (يتم تنفيذ $match الذي يسبق $facet مرة واحدة فقط).
سيناريوهات الأعمال للتحليل متعدد الأبعاد: السيناريو الأكثر شيوعًا لـ $facet هو صفحة البحث في التجارة الإلكترونية — فعندما يبحث المستخدم عن «الهواتف المحمولة»، يجب أن تعرض الصفحة العناصر التالية في آن واحد: 1. قائمة بالمنتجات (مقسمة إلى صفحات، ومصنفة، مع الصور والأسعار)؛ 2. العدد الإجمالي لنتائج البحث («تم العثور على 256 منتجًا»); 3. مخطط شريطي يوضح توزيع الأسعار (ثلاثة نطاقات: 0–1,000، و1,000–3,000، و3,000+); 4. توزيع العلامات التجارية (هواوي 30٪، آبل 25٪، شاومي 20٪، إلخ); 5. توزيع أحجام الشاشات، وما إلى ذلك. كانت الطريقة التقليدية تتطلب 5 استعلامات × 50 مللي ثانية = 250 مللي ثانية، في حين أن استعلام $facet واحدًا يستغرق حوالي 80 مللي ثانية، مما يحول تجربة المستخدم من «تأخير ملحوظ» إلى «استجابة فورية».
$facet مقابل الاستعلامات المتعددة: مفاضلة: تُرجع $facet نتائج متعددة الأبعاد في استعلام واحد، لكنها تنطوي أيضًا على قيود — 1. يجب أن تكون جميع خطوط الأنابيب الفرعية لـ $facet ضمن نفس استدعاء التجميع ولا يمكن تحميلها عند الطلب (على سبيل المثال، إذا قام المستخدم بعرض قائمة المنتجات فقط دون الإحصائيات، فإن خط أنابيب الإحصائيات يُنفَّذ مع ذلك)؛ 2. بنية نتائج $facet ثابتة، لذا لا يمكن إضافة الأبعاد ديناميكيًا (على سبيل المثال، تتطلب إضافة بُعد «توزيع الألوان» تعديل خط أنابيب التجميع وإعادة النشر)؛ 3. معالجة الأخطاء صعبة — إذا فشل أي خط أنابيب فرعي في $facet، يفشل التجميع بأكمله (لا يمكن الحصول على نتائج جزئية). النهج البديل: بالنسبة للسيناريوهات التي قد تتغير فيها الأبعاد ديناميكيًا، ضع الأبعاد عالية التكرار داخل $facet (قائمة المنتجات + العدد الإجمالي) والأبعاد منخفضة التكرار في واجهات برمجة تطبيقات (APIs) منفصلة (توزيع العلامات التجارية/الرسم البياني السعري عبر نقاط نهاية منفصلة يتم استدعاؤها عند الطلب). يضمن ذلك أوقات استجابة سريعة للاستعلامات عالية التكرار مع منع الاستعلامات منخفضة التكرار من إهدار موارد $facet.
// === $facet Executing Multiple Independent Pipelines in Parallel ===
db.products.aggregate([
{
$facet: {
// Branch 1:Top results sorted by price 10
topExpensive: [
{ $sort: { price: -1 } },
{ $limit: 10 }
],
// Branch 2:Categorical Statistics
byCategory: [
{ $group: { _id: '$category', count: { $sum: 1 } } }
],
// Branch 3:Price Range Distribution
priceRanges: [
{
$bucket: {
groupBy: '$price',
boundaries: [0, 100, 500, 1000, 5000],
default: '5000+',
output: { count: { $sum: 1 } }
}
}
],
// Branch 4:Overview Statistics
stats: [
{ $group: { _id: null, total: { $sum: 1 }, avgPrice: { $avg: '$price' } } }
]
}
}
]);
// A single query returns results for all dimensions
المبادئ الرياضية لـ $bucket: يُعرِّف المصفوفة boundaries من $bucket N-1 خانات، كل منها عبارة عن فترة مغلقة من اليسار ومفتوحة من اليمين [boundaries[i], boundaries[i+1]). ويتم وضع المستندات في الخانة المقابلة بناءً على قيمتها groupBy. على سبيل المثال، يحدد boundaries=[0, 100, 500, 1000] 3 فئات: [0,100)، [100,500)، [500,1000). ويمثل _id لـ $bucket قيمة الحد الأيسر للفئة. تقوم $bucketAuto تلقائيًا باختيار حدود المجموعات بناءً على توزيع البيانات لضمان احتواء كل مجموعة على نفس العدد تقريبًا من المستندات — مما يجعلها مناسبة للتحليل الاستكشافي للبيانات (عندما يكون توزيع البيانات غير معروف).
سيناريوهات العمل الخاصة بـ $bucket: سيناريوهات العمل النموذجية لـ $bucket — 1. إحصائيات النطاق السعري: تحسب منصات التجارة الإلكترونية عدد المنتجات حسب النطاق السعري (0–100، 100–500، 500–1,000) لاستخدامها في عوامل تصفية الأسعار على صفحات البحث؛ 2. توزيع التقييمات: تقوم أنظمة التقييم بتجميع التقييمات في فئات (1 نجمة/2 نجمة/3 نجوم/4 نجوم/5 نجوم) لعرض الرسوم البيانية الشريطية للتقييمات؛ 3. التجميع حسب العمر: تقوم ملامح المستخدم بتجميع المستخدمين حسب الفئة العمرية (18–25، 25–35، 35–50، 50+)، وتُستخدم في التسويق الموجه؛ 4. التجميع الزمني: تُجمَّع السجلات حسب الساعة أو اليوم أو الشهر، وتُستخدم في مخططات الاتجاهات. يمثل $bucket بشكل أساسي تحويلًا من «القيم المستمرة → الفترات المنفصلة».
استراتيجية التعامل مع الفئات الفارغة: تعرض $bucket الفئات الفارغة (العدد = 0)، بينما لا تعرض $bucketAuto ذلك. ولهذا الأمر آثار على الأعمال — حيث يجب أن يعرض مرشح السعر في صفحة البحث جميع نطاقات الأسعار (بما في ذلك الفئات الفارغة)، وإلا فلن يعرف المستخدمون النطاقات المتاحة. كما يجب أن تعرض مخططات توزيع التقييمات أشرطة لجميع تقييمات النجوم (بما في ذلك تلك التي تحتوي على 0 تقييم). لذلك، استخدم $bucket (النطاقات المعروفة) لسيناريوهات تصفية التقييمات/الأسعار، واستخدم $bucketAuto (الذي يكتشف أنماط توزيع البيانات تلقائيًا) للتحليل الاستكشافي.
توسيع نطاق إخراج $bucket: يمكن لمخرجات $bucket القيام بأكثر من مجرد العد باستخدام $sum: 1؛ فهي يمكنها أيضًا إجراء عمليات تجميع أخرى — مثل متوسط التقييم لكل نطاق سعري (avgRating: {$avg: '$rating'})، وأعلى سعر (maxPrice: {$max: '$price'})، وقائمة بالمنتجات المشمولة (products: {$push: '$title'}). يؤدي توسيع المخرجات إلى تحويل $bucket من أداة عد بسيطة إلى أداة تجميع وإحصاءات متعددة الأبعاد — حيث تُرجع مقاييس إحصائية متعددة لكل فترة زمنية في استعلام واحد.
بنية صفحة البحث $facet + $bucket: البنية الكلاسيكية لصفحات البحث في التجارة الإلكترونية — حيث يُرجع استعلام $facet واحد ما يلي: 1. قائمة بنتائج البحث ($sort + $skip + $limit)؛ 2. العدد الإجمالي ($count)؛ 3. توزيع الأسعار ($bucket، مجمّع حسب النطاق السعري)؛ 4. توزيع الفئات ($group، مجمّع حسب الفئة)؛ 5. توزيع التقييمات ($bucket، مجمّع حسب التقييم). تسترد الواجهة الأمامية جميع البيانات في طلب واحد وتعرض صفحة البحث الكاملة — بما في ذلك قائمة المنتجات، وفلتر الأسعار، والشريط الجانبي للفئات، ومخطط شريطي للتقييمات.
اعتبارات الأداء لاستعلامات $facet: تعمل المسارات الفرعية لـ $facet بالتوازي ولكنها تتشارك بيانات الإدخال — 1. الإدخال المشترك: يتم تنفيذ مرحلة $match التي تسبق $facet مرة واحدة فقط، لذا تعالج جميع المسارات الفرعية نفس مجموعة بيانات الإدخال؛ 2. استخدام الذاكرة: يحتفظ كل مسار فرعي بحالة الذاكرة الخاصة به؛ N مسارًا فرعيًا = N أضعاف استخدام الذاكرة (إذا كان الحد الأقصى للمرحلة الواحدة هو 100 ميجابايت، فقد تستهلك 4 مسارات فرعية 400 ميجابايت)؛ 3. استراتيجية التحسين: تجنب عمليات $match الزائدة عن الحاجة داخل المسارات الفرعية (نظرًا لأن التصفية قد تم تنفيذها بالفعل قبل $facet)؛ وقم بتنفيذ عمليات $project/$group الضرورية فقط؛ 4. النهج البديل: إذا تجاوزت عملية $facet حد الذاكرة، فيمكن تقسيمها إلى عدة استعلامات تجميع مستقلة (على حساب الترابطية وزيادة عدد جولات الشبكة ذهابًا وإيابًا). في بيئات الإنتاج، تُعد عملية $facet مناسبة لمجموعات البيانات الصغيرة إلى المتوسطة الحجم (< 100,000 مستند مطابق)؛ أما بالنسبة لمجموعات البيانات الكبيرة، فيجب تقييم استخدام الذاكرة.
3. $bucket: تقسيم البيانات إلى مجموعات
المبدأ الإحصائي وراء عملية التقسيم إلى شرائح: يُعد $bucket في جوهره بنية بيانات من نوع الرسم البياني التكراري — فهو يقسم نطاقًا مستمرًا من القيم إلى فترات منفصلة ويحسب عدد الوثائق في كل فترة. ويُعد الرسم البياني التكراري الخطوة الأولى في استكشاف البيانات: فمن خلال ملاحظة توزيع القيم، يمكنك تحديد ما إذا كانت البيانات موزعة توزيعًا عاديًا، وما إذا كانت هناك قيم متطرفة، وما إذا كانت هناك حاجة إلى التوحيد. يُعد $bucket مناسبًا للفترات المعروفة (مثل نطاقات الأسعار 0–100، و100–500، و500–1000)، بينما يُعد $bucketAuto مناسبًا للتحليل الاستكشافي (حيث يتم اختيار المجموعات المتجانسة تلقائيًا).
$bucket مقابل $switch: مقارنة بين طرق التجميع: يمكن استخدام كل من $bucket و$switch لتصنيف النطاقات، لكن لهما أهداف تصميمية مختلفة — فـ$bucket هي أداة إحصائية (تُنتج أعدادًا وقيمًا مجمعة لكل مجموعة)، بينما $switch هي أداة تصنيف (تُنتج تسميات تصنيفية لكل مستند). الاختيار: إذا كنت بحاجة إلى رسم بياني أو إحصائيات → استخدم $bucket؛ إذا كنت بحاجة إلى تعيين تسمية لكل مستند → استخدم $switch + $addFields.
خوارزمية التجميع $bucketAuto: تستخدم $bucketAuto خوارزمية تجميع تعتمد على العمق المتساوي تقريبًا — والهدف منها هو أن يحتوي كل مجموعة على نفس العدد تقريبًا من المستندات، بدلاً من التجميع على أساس العرض المتساوي (حيث يكون لكل مجموعة نفس نطاق القيم). على سبيل المثال، إذا كانت أسعار المنتجات مركزة في النطاق 50–200، فستقوم $bucketAuto بإنشاء المزيد من المجموعات في النطاق 50–200 (حيث تكون كثافة البيانات عالية) وعدد أقل من المجموعات في النطاق 500–5000 (حيث تكون البيانات متفرقة). وهذا يوفر معلومات أكثر من التقسيم إلى مجموعات ذات عرض متساوٍ — الذي ينشئ مجموعات فارغة في المناطق المتناثرة من البيانات ويحشر كميات كبيرة من البيانات في مجموعة واحدة في المناطق الكثيفة. يتحكم الخيار granularities في $bucketAuto في عدد المجموعات (على سبيل المثال، 5 أو 10 أو 20 مجموعة).
graph LR
A["price: 599"] --> B{Which bucket??}
C["price: 29"] --> B
D["price: 1299"] --> B
E["price: 7500"] --> B
B --> F["[0, 100): price=29 ✅"]
B --> G["[100, 500): None"]
B --> H["[500, 1000): price=599 ✅"]
B --> I["[1000, 5000): price=1299 ✅"]
B --> J["default 'Other': price=7500 ✅"]
style F fill:#d4edda
style H fill:#d4edda
style I fill:#d4edda
style J fill:#fff3cd
| بُعد المقارنة | $bucket | $bucketAuto |
|---|---|---|
| حدود الدلو | محددة يدويًّا | محسوبة تلقائيًّا |
| عدد المجموعات | يتحدد بناءً على عدد الحدود | محدد buckets: N |
| حجم الدلو | قد يختلف | متجانس قدر الإمكان |
| حالات الاستخدام | الفترات المعروفة (نطاقات الأسعار) | استكشاف توزيع البيانات |
| التعامل مع البراميل الفارغة | عرض البراميل الفارغة | عدم عرض البراميل الفارغة |
امتدادات إخراج $bucket: تتيح لك المعلمة output الخاصة بـ $bucket إجراء حسابات التجميع داخل كل مجموعة — ولا يقتصر ذلك على مجرد حساب $sum: 1 البسيط، بل يشمل أيضًا $avg (متوسط السعر)، و$push (جمع حقول محددة من المستندات الموجودة في المجموعة)، و$max/$min (القيم القصوى والدنيا في المجموعة). الاستخدام النموذجي: $bucket + output: {count: {$sum: 1}, avgPrice: {$avg: '$price'}, topProduct: {$max: '$price'}} تُرجع العدد، ومتوسط السعر، والعنصر الأغلى داخل دلو واحد. وهذا يجعل $bucket ليس مجرد أداة «العد حسب الدلو»، بل أيضًا أداة «التحليل الإحصائي حسب الدلو».
خوارزمية $bucketAuto وقيودها: تستخدم $bucketAuto خوارزمية تقسيم إلى مجموعات ذات عمق متساوٍ تقريبيًّا — وتهدف إلى ضمان احتواء كل مجموعة على نفس عدد الوثائق، بدلاً من فترات زمنية ذات عرض متساوٍ. وهذا يعني أن النطاق السعري 0–100 قد يحتوي على 500 مستند (نظرًا لوجود عدد أكبر من العناصر الرخيصة)، في حين أن النطاق 5,000–10,000 قد يحتوي على 50 مستندًا فقط (نظرًا لوجود عدد أقل من العناصر باهظة الثمن). القيود: 1. حدود المجموعات غير قابلة للتحكم (يتم حسابها تلقائيًا؛ ولا يمكن تحديد نطاقات خاصة بالأعمال مثل «0–100» أو «100–500»)؛ 2. ضعف الأداء في حالة انحراف البيانات (على سبيل المثال، 99% من المنتجات تتراوح أسعارها بين 0 و1,000، في حين أن 1% منها تزيد أسعارها عن 10,000؛ سيؤدي التقسيم التلقائي إلى إنشاء العديد من المجموعات الصغيرة ضمن النطاق 0–1,000)؛ 3. غير مناسب للعرض على مستخدمي الأعمال (حدود الفئات هي قيم تعسفية مثل 47.5–89.3، بدلاً من نطاقات بديهية مثل «0–100» أو «100–500»). يُعد $bucketAuto مناسبًا لتحليل البيانات الاستكشافي، بينما يُعد $bucket مناسبًا لعرض نطاقات ثابتة على مستخدمي الأعمال.
// === $bucket Custom Bucketing ===
db.products.aggregate([
{
$bucket: {
groupBy: '$price',
boundaries: [0, 100, 500, 1000, 5000, 10000], // Barrel Boundary
default: 'Other', // Out of range
output: {
count: { $sum: 1 },
products: { $push: { sku: '$sku', title: '$title', price: '$price' } }
}
}
}
]);
// [
// { _id: 0, count: 120, products: [...] },
// { _id: 100, count: 80, products: [...] },
// ...
// ]
// === $bucketAuto Automatic Bin Sorting ===
db.products.aggregate([
{
$bucketAuto: {
groupBy: '$price',
buckets: 5 // Automatic Sorting 5 a bucket
}
}
]);
اختيار بنية البحث: يُعد الاختيار بين البحث النصي في MongoDB وElasticsearch قرارًا هندسيًّا كلاسيكيًّا. مزايا البحث النصي في MongoDB: 1. لا حاجة إلى بنية تحتية إضافية (البيانات موجودة بالفعل في MongoDB)؛ 2. التوافق مع عمليات CRUD (نفس لغة الاستعلام)؛ 3. أداء كافٍ لمجموعات البيانات الصغيرة (<1 مليون سجل). مزايا Elasticsearch: 1. تجزئة الكلمات الصينية (باستخدام أدوات التجزئة مثل IK Analyzer)؛ 2. المطابقة التقريبية والمرادفات؛ 3. التمييز باللون؛ 4. دعم أحجام البيانات التي تصل إلى عشرات المليارات. مبدأ الاختيار: استخدم MongoDB للعمليات البحثية البسيطة وElasticsearch للعمليات البحثية المعقدة. يمكنك البدء بالبحث النصي في MongoDB والانتقال إلى Elasticsearch بمجرد أن تصبح متطلبات البحث أكثر تعقيدًا.
عملية إنشاء فهرس معكوس: عند إنشاء فهرس نصي، تقوم MongoDB بإجراء تحليل لغوي على الحقول النصية لكل مستند — 1. التقطيع إلى رموز (التقسيم حسب المسافات/علامات الترقيم)؛ 2. استخراج الجذور (على سبيل المثال، «running» → «run»، الجمع → المفرد)؛ 3. إزالة الكلمات الممنوعة (الكلمات عالية التكرار التي لا معنى لها مثل «the»، «a»، «an»، إلخ)؛ 4. إنشاء فهرس معكوس (الكلمة → قائمة بمعرّفات المستندات + تكرار المصطلح). أثناء الاستعلام، تخضع المصطلحات الموجودة في جملة $search أيضًا للتقطيع إلى رموز واستخلاص الجذور، ثم يتم استرداد المستندات المطابقة من الفهرس المعكوس. ملاحظة: يتم تقسيم النص CJK (الصينية/اليابانية/الكورية) حسب الأحرف الفردية بدلاً من الكلمات (لا يتم التقطيع بناءً على المسافات)، مما يؤدي إلى نتائج رديئة — على سبيل المثال، يتم تقسيم كلمة CJK متعددة الأحرف إلى إدخالات فردية مكونة من حرف واحد بدلاً من التعرف عليها كوحدة دلالية واحدة.
درجة الصلة BM25: يستخدم البحث النصي في MongoDB خوارزمية BM25 لحساب درجات الصلة، مع أخذ ثلاثة عوامل في الاعتبار: 1. تكرار المصطلح (TF): كلما زاد عدد مرات ظهور المصطلح في المستند، ارتفعت الدرجة؛ 2. تكرار المستند العكسي (IDF): كلما قل تكرار ظهور المصطلح في جميع الوثائق (كلما كان نادرًا)، ارتفعت الدرجة؛ 3. تطبيع طول الوثيقة: تُعتبر المطابقة في وثيقة قصيرة أكثر قيمة من المطابقة في وثيقة طويلة. تؤثر المعلمة weights في الفهرسة النصية المرجحة على حساب BM25 — حيث تساهم الحقول ذات الأوزان الأعلى بشكل أكبر في الدرجة عند العثور على مطابقة.
4. البحث عن النص
مبادئ نظام البحث: يعتمد البحث النصي في MongoDB على الفهارس المقلوبة — أثناء إنشاء الفهرس، يتم تقطيع النص إلى رموز (tokenization) واستخلاص جذوره (stemming) لإنشاء جدول تخطيط «كلمة → مستند». وأثناء إجراء الاستعلام، يُستخدم عامل $search لتحديد الكلمات المفتاحية، ويتم البحث عن المستندات المطابقة في الفهرس المقلوب، مع حساب درجات الصلة باستخدام خوارزمية TF-IDF. القيود: 1. لا يدعم تقطيع النص الصيني إلى رموز (يتم التقسيم حسب الأحرف بدلاً من التقطيع الدلالي)؛ 2. لا يدعم المطابقة التقريبية (يتطلب استخدام الاستعلام التقريبي في Elasticsearch)؛ 3. لا يدعم توسيع المرادفات؛ 4. فهرس نصي واحد فقط لكل مجموعة.
متى تستخدم البحث النصي في MongoDB مقابل Elasticsearch: يُعد البحث النصي في MongoDB مناسبًا للسيناريوهات البسيطة — مثل البحث عن الكلمات أو العبارات بالضبط في المحتوى باللغة الإنجليزية، والبحث عن النص الكامل في مجموعات البيانات الصغيرة. أما Elasticsearch فهو مناسب للسيناريوهات المعقدة — مثل تجزئة الكلمات الصينية، والبحث التقريبي، والمرادفات، وتمييز النص، والبحث باستخدام نظام بينيين، وتوزيع الأوزان على حقول متعددة. معايير اتخاذ القرار: 1. حجم البيانات < 100,000 ولا يُطلب سوى البحث باللغة الإنجليزية → يكفي استخدام MongoDB؛ 2. يُطلب تجزئة الكلمات الصينية أو البحث غير الدقيق → يُعد استخدام Elasticsearch إلزاميًا؛ 3. البحث ميزة أساسية → Elasticsearch؛ 4. البحث ميزة ثانوية → يوفر MongoDB تنفيذًا بسيطًا.
الخيار الثالث لـ MongoDB Atlas Search: Atlas Search هو محرك البحث المدمج في خدمة MongoDB Atlas السحابية — والذي يعتمد على Apache Lucene (نفس المحرك الأساسي المستخدم في Elasticsearch) — ويوفر ميزات متقدمة مثل تجزئة الكلمات الصينية، والبحث التقريبي، والمرادفات، وتمييز النص، ويتكامل بسلاسة مع MongoDB (دون الحاجة إلى مزامنة البيانات مع مجموعة خارجية). المزايا: 1. عدم وجود أي تأخير في مزامنة البيانات (تتم مزامنة فهارس Lucene مع مجموعات MongoDB في الوقت الفعلي)؛ 2. الاستخدام المباشر في مرحلة $search من مسارات التجميع (لا حاجة لتعلم صيغة استعلام جديدة)؛ 3. عدم وجود أي عبء تشغيلي (يتم استضافته بواسطة Atlas، ولا حاجة لإدارة مجموعة Elasticsearch). القيود: 1. متاح فقط على خدمة Atlas السحابية (غير مدعوم على MongoDB المُستضاف ذاتيًا)؛ 2. تُفرض تكاليف إضافية؛ 3. الميزات ليست شاملة بقدر Elasticsearch (على سبيل المثال، لا توجد مجموعات التجميع أو الأنواع المتداخلة). بالنسبة للمشاريع الصغيرة والمتوسطة الحجم، يوفر Atlas Search التوازن الأمثل — فهو يلغي الحاجة إلى صيانة مجموعة Elasticsearch مع توفير إمكانيات بحث احترافية.
ملخص قرارات اختيار نظام البحث: الاختيار من بين ثلاثة حلول للبحث — 1. ميزة $text الأصلية في MongoDB: تكلفة صفرية، صيانة صفرية، وظائف محدودة (البحث باللغة الإنجليزية، بدون تجزئة الكلمات، بدون المطابقة التقريبية)، مناسبة للمشاريع الصغيرة التي يُعد فيها البحث ميزة ثانوية؛ 2. Atlas Search: صيانة منخفضة، تكلفة معتدلة، وظائف قوية (تقسيم الكلمات الصينية، المطابقة التقريبية، التمييز باللون)، مناسب للمشاريع متوسطة الحجم لمستخدمي Atlas؛ 3. Elasticsearch: تكاليف تشغيلية عالية، تكلفة مرتفعة، ميزات قوية للغاية (جميع قدرات البحث + تحليلات Kibana)، مناسب للمشاريع واسعة النطاق حيث يُعد البحث ميزة أساسية. معايير الاختيار: حجم البيانات (صغير → MongoDB، كبير → ES)، اللغة (اللغة الإنجليزية فقط → MongoDB، اللغة الصينية → ES/Atlas)، أهمية البحث (ثانوية → MongoDB، أساسية → ES)، القدرات التشغيلية (محدودة → Atlas، قوية → ES).
graph TD
A[Document: title='Smartphone X 5G'<br/>description='Latest 5G phone'] --> B[Text Index<br/>Lexical Analysis]
B --> C[Inverted Index<br/>smartphone → doc1<br/>phone → doc1<br/>5g → doc1<br/>latest → doc1]
D["$search: 'phone'"] --> E[Searching the Inverted Index<br/>phone → doc1]
E --> F[✅ Hit]
G["$search: 'phone -cheap'"] --> H[phone → doc1<br/>Including the exclusion of cheap]
H --> F
style C fill:#d4edda
style F fill:#d4edda
| نوع البحث | الصيغة | الوصف |
|---|---|---|
| البحث على مستوى الكلمة | $search: 'phone smartphone' |
مطابقة أي كلمة (أو) |
| البحث عن عبارة | $search: '"smartphone 5g"' |
يجب أن تكون العبارة كاملة |
| استبعاد البحث | $search: 'phone -cheap' |
تضمين كلمة "هاتف" مع استبعاد كلمة "رخيص" |
| الترتيب حسب الصلة | score: { $meta: 'textScore' } |
الترتيب حسب الصلة |
(1) إنشاء فهرس نصي
استراتيجية تصميم الفهرس النصي: يمكن أن تحتوي كل مجموعة على فهرس نصي واحد كحد أقصى، لكنه يمكن أن يشمل حقولًا متعددة ويحدد أوزانًا لها — حيث تحصل الحقول ذات الأوزان الأعلى على قيمة textScore أعلى عند مطابقتها، مما يؤدي إلى ترتيب أكثر منطقية لنتائج البحث. اعتبارات التصميم: 1. يجب إعطاء العنوان وزنًا أعلى من الوصف (تعتبر مطابقات العنوان أكثر أهمية للمستخدمين أثناء عمليات البحث)؛ 2. لا تنشئ فهارس نصية للحقول ذات المحتوى المعلوماتي المنخفض (مثل status، category)؛ 3. تستهلك الفهارس النصية مساحة تخزين كبيرة (حوالي 2–3% من حجم البيانات)، لذا استخدمها بحذر مع المجموعات الكبيرة؛ 4. لا يمكن أن تتواجد الفهارس المركبة والفهارس النصية معًا في نفس الاستعلام.
// === Create a Text Index(Single field)===
db.products.createIndex({ title: 'text' });
// === Multi-field text index ===
db.products.createIndex({
title: 'text',
description: 'text',
tags: 'text'
});
// === Weighted Text Index ===
db.products.createIndex(
{
title: 'text',
description: 'text'
},
{
weights: {
title: 10, // title High weighting
description: 1
},
name: 'TextIndex'
}
);
(2) البحث عن النص $text
أربعة أوضاع للبحث في $text: يدعم $text أربعة أنماط بحث — 1. البحث على مستوى الكلمة (المعنى الافتراضي "أو"): "phone smartphone" يطابق المستندات التي تحتوي على أي من الكلمتين؛ 2. البحث عن العبارة (علامات الاقتباس المزدوجة): "smartphone 5g" يجب أن تحتوي على العبارة بالضبط؛ 3. البحث بالاستبعاد (علامة الطرح): "phone -cheap" تحتوي على "phone" ولكن لا تحتوي على "cheap"؛ 4. الفرز حسب الصلة: استخدم $meta: "textScore" لاسترداد الدرجات وفرز النتائج. ملاحظة: لا يدعم البحث في $text أحرف البدل (*)، أو المطابقة التقريبية (~)، أو التعبيرات النمطية.
تكاليف إنشاء الفهارس النصية وصيانتها: تكلفة إنشاء الفهارس النصية أعلى من تكلفة الفهارس العادية — 1. حجم الفهرس: تُنشئ الفهارس النصية فهارس معكوسة بعد تقطيع كل حقل إلى رموز؛ وقد يصل حجم الفهرس إلى 30–50% من حجم البيانات الأصلية (مقارنةً بحوالي 10% للفهارس العادية)؛ 2. وقت الإنشاء: قد يستغرق إنشاء فهرس نصي لمجموعات البيانات الكبيرة (> 1 مليون مستند) ما بين عدة دقائق إلى عدة ساعات، ويجب إجراؤه خلال ساعات خارج أوقات الذروة؛ 3. أداء الكتابة: في كل مرة يتم فيها إدراج مستند يحتوي على حقول نصية أو تحديثه، يجب تحديث الفهرس المعكوس، مما يزيد من زمن استجابة الكتابة بنسبة 10–30٪؛ 4. القيود: لا يمكن أن تحتوي كل مجموعة إلا على فهرس نصي واحد (على الرغم من أنه يمكن أن يغطي حقولًا متعددة)؛ ولا تدعم الفهارس النصية عمليات البحث النصي في استعلامات $or. تعني هذه التكاليف أنه يجب عليك إنشاء الفهارس النصية فقط للحقول التي تتطلب بالفعل البحث عن النص الكامل، وتجنب إنشائها لجميع حقول السلاسل.
// === Basic Search ===
db.products.find({ $text: { $search: 'phone smartphone' } });
// Match documents where title or description contains "phone" or "smartphone"
// === Phrase Search ===
db.products.find({ $text: { $search: '"smartphone 5g"' } });
// Must include the complete phrase
// === Exclude from Search ===
db.products.find({ $text: { $search: 'phone -cheap' } });
// Includes phone But does not include cheap
// === Sort by Relevance ===
db.products.find(
{ $text: { $search: 'phone' } },
{ score: { $meta: 'textScore' } }
).sort({ score: { $meta: 'textScore' } });
تحسين أداء الاستعلامات الجغرافية المكانية: تُعد فهارس 2dsphere عنصراً أساسياً لأداء الاستعلامات الجغرافية المكانية — فبدونها، ستقوم $geoNear و$nearSphere بإجراء مسح كامل للمجموعة. نصائح للتحسين: 1. يجب أن تكون $geoNear هي المرحلة الأولى في مسار المعالجة (وإلا فلن يمكن استخدام الفهرس)؛ 2. كلما قلت قيمة maxDistance، قل نطاق المسح، وتحسّن الأداء؛ 3. تدعم الفهارس المركبة {location: '2dsphere', category: 1} الاستعلامات عن «المتاجر من فئة معينة في الجوار»؛ 4. $geoWithin أسرع من $geoNear (بدون فرز) ويجب إعطاؤها الأولوية في الاستعلامات التي تعتمد على النطاق فقط.
بدائل للبحث باللغة الصينية: توفر فهرسة النصوص في MongoDB دعمًا محدودًا للغة الصينية — فهي تقسم النص حسب الأحرف بدلاً من الكلمات، مما يؤدي إلى تقسيم «الكلمة المكونة من عدة أحرف من اللغات الصينية واليابانية والكورية» إلى أربعة إدخالات مكونة من حرف واحد، وبالتالي فإن البحث عن مصطلح مكون من عدة أحرف لن يطابق الكلمة المركبة بالكامل. ثلاثة بدائل: 1. البحث باستخدام التعبيرات العادية $regex: /term/ — بسيط ولكنه لا يمكنه فرز النتائج ولا يستخدم الفهرس؛ 2. تجزئة الكلمات على مستوى التطبيق — معالجة المستندات مسبقًا باستخدام أداة تجزئة مثل jieba أثناء الإدراج، وتخزين النتائج المجزأة في حقل صفيف، والاستعلام باستخدام فهارس متعددة المفاتيح؛ 3. Elasticsearch — محرك بحث احترافي مزود بأداة تجزئة مدمجة للغة الصينية (IK Analyzer) تدعم البحث باستخدام نظام بينيين وتوسيع المرادفات. الخيار 3 هو الخيار المفضل لبيئات الإنتاج.
مناهج عملية لتجزئة الكلمات الصينية: يكمن التحدي الأساسي في البحث باللغة الصينية في أن «الكلمات» تفتقر إلى فواصل طبيعية—1. نهج تجزئة jieba: استخدم nodejieba في Node.js للتجزئة عند إدراج المستندات، وقم بتخزين النتائج في حقل segmentedTitle: ['term1', 'term2', 'compound']، وأنشئ فهرسًا متعدد المفاتيح، وقم بإجراء الاستعلامات عن طريق تجزئة مصطلحات البحث واستخدام $all للمطابقة؛ 2. نهج N-gram: استخدام $regex مع مطابقة n-gram (على سبيل المثال، /term1.{0,2}term2/). يدعم هذا النهج المطابقة التقريبية ولكنه يعاني من ضعف الأداء (مسح المجموعة الكاملة)؛ 3. البحث بالبينيين: تخزين حقل pinyinTitle إضافي (يتم تحويله باستخدام مكتبة البينيين) لدعم البحث عن المنتجات الصينية عبر الإدخال بالبينيين؛ 4. حل Elasticsearch: إنشاء مسار تزامن بين MongoDB و Elasticsearch (باستخدام Change Stream أو MongoSync). يتم تنفيذ الاستعلامات عبر ES، بينما تظل البيانات مخزنة في MongoDB. الحل 1 مناسب للمشاريع الصغيرة والمتوسطة الحجم (بدون تكلفة)، بينما الحل 4 مناسب للمشاريع الكبيرة (تجربة بحث احترافية).
البنية الداخلية لمؤشر 2dsphere: يقوم مؤشر 2dsphere بترميز الإحداثيات الجغرافية على شكل «جيوهاش» (Geohash) — وهو تقسيم شبكي متكرر لسطح الأرض، حيث يتم تمثيل كل خلية شبكية بسلسلة من الأحرف. أثناء إجراء الاستعلام، يقوم $geoNear أولاً بتحديد الخلايا الشبكية المجاورة استنادًا إلى «جيوهاش» نقطة المركز، ثم يحسب المسافة الكروية بدقة. تسمح طبيعة Geohash القائمة على مطابقة البادئات باستغلال الفهرس بكفاءة في استعلامات النطاق — فعند الاستعلام عن «في نطاق 5 كم»، لا يلزم سوى مسح عدد قليل من الخلايا المجاورة، بدلاً من مسح مجموعة البيانات بأكملها.
دقة حسابات المسافة: تستخدم MongoDB الهندسة الكروية (الشكل الإهليلجي WGS84) لحساب المسافات — وتكون قيمة distanceField التي تُرجعها $geoNear بالمتر، بدقة تبلغ حوالي 0.5 متر. يُقاس نصف القطر في $geoWithin و$centerSphere بالراديان — لتحويل الكيلومترات إلى راديان، استخدم km / 6378.1. ملاحظة: 6378.1 هو نصف قطر خط الاستواء للأرض (كم). قد يؤدي استخدام هذا التحويل في المناطق ذات خطوط العرض العالية إلى حدوث خطأ طفيف (نصف القطر القطبي يبلغ 6356.8 كم)، لكن تأثير ذلك على تطبيقات LBS ضئيل للغاية.
الهندسة المتكاملة للبحث + خدمات تحديد الموقع الجغرافي (LBS): غالبًا ما تحتاج أنظمة البحث في التجارة الإلكترونية إلى دعم كل من «البحث عن الكلمات المفتاحية» و«البحث عن المواقع القريبة» في آن واحد — مثل «هواتف 5G في نطاق 3 كيلومترات». طرق التكامل بين هذين النوعين من البحث: 1. إجراء بحث $text أولاً، ثم التصفية حسب المسافة باستخدام $geoNear (مناسب للحالات التي تحتوي على عدد قليل من نتائج البحث حيث تكون عملية التصفية حسب القرب سريعة)؛ 2. إجراء بحث القرب باستخدام $geoNear أولاً، يليه بحث الكلمات المفتاحية باستخدام $match (مناسب للسيناريوهات التي تحتوي على عدد قليل من النتائج القريبة حيث يكون التصفية حسب الكلمات المفتاحية سريعًا)؛ 3. تشغيل كلا البحثين بالتوازي باستخدام $facet ثم دمج النتائج (الطريقة الأكثر مرونة ولكنها تستهلك الكثير من الذاكرة). الطريقة الأولى هي الأكثر شيوعًا — حيث يرى المستخدمون أولاً المنتجات ذات الصلة ثم يقومون بفرزها حسب المسافة.
5. الاستعلامات الجغرافية المكانية
مبادئ الفهرسة الجغرافية المكانية: يقوم فهرس 2dsphere بترميز الإحداثيات الكروية (خط الطول/خط العرض) على شكل «جيوهاش» (Geohashes) — وهو نظام يقسم سطح الأرض بشكل متكرر إلى شبكات، يتم ترميز كل منها بسلسلة من الأحرف. بالنسبة لاستعلامات القرب، يقوم النظام أولاً بمطابقة الشبكات التي تحمل نفس بادئة الجيوهاش (التصفية الأولية)، ثم يحسب بدقة المسافة الكروية (التصفية الدقيقة). تقلل هذه الاستراتيجية ذات المرحلتين من تعقيد استعلامات $geoNear و$nearSphere من O(N) إلى O(log N).
اختيار عوامل الاستعلام الجغرافية المكانية: لكل عامل من عوامل الاستعلام الثلاثة حالة استخدام خاصة به — يُستخدم $geoNear خلال مرحلة التجميع، ويُنتج حقول المسافة، ويدعم الفرز والتصفية؛ وهو الأنسب لسيناريوهات «البحث عن الأماكن القريبة + فرز النتائج»؛ أما $nearSphere فهو عامل استعلام ذو صيغة بسيطة لكنه لا يُنتج قيم المسافة؛ وهو مناسب لتحديد ما إذا كانت النتيجة «داخل نطاق» ما؛ أما $geoWithin فلا يقوم بفرز النتائج، وهو مناسب للاستعلامات المجمعة لاسترجاع «جميع النتائج داخل منطقة ما» (مثل مناطق التوصيل).
| المشغل | مسافة الإخراج | الفرز | التوافق مع التجميع | حالات الاستخدام |
|---|---|---|---|---|
| $geoNear | ✅ | ✅ | نعم | البحث القائم على الموقع |
| $nearSphere | ❌ | ✅ | لا | التحقق من المسافة |
| $geoWithin | ❌ | ❌ | نعم | تجميع حسب المنطقة |
قيود فريدة على $geoNear: يجب أن تكون $geoNear هي المرحلة الأولى في مسار التجميع — وهذا قيد صارم لأن $geoNear تحتاج إلى تصفح فهرس 2dsphere بدءًا من الجذر. إذا كنت بحاجة إلى تطبيق تصفية $match أولاً، فيجب عليك تحديد ذلك في خيار الاستعلام الخاص بـ $geoNear، بدلاً من وضع مرحلة $match قبل $geoNear. تُخرج distanceField في $geoNear المسافة الكروية (بالمتر)، ويمكن استخدام distanceMultiplier لتحويل الوحدات (على سبيل المثال، × 0.001 للتحويل إلى كيلومترات). تُخرج includeLocs: true نقاط الإحداثيات المطابقة (مفيدة للوثائق ذات المواقع المتعددة — مثل شركة لديها عدة فروع).
النموذج المعماري لنظام LBS: يتألف نظام LBS (الخدمات القائمة على الموقع) الكامل من ثلاث طبقات: 1. طبقة البيانات: فهرس 2dsphere + تخزين الإحداثيات الجغرافية؛ 2. طبقة الاستعلام: $geoNear/$geoWithin للبحث عن الأماكن القريبة والاستعلامات عن المناطق؛ 3. طبقة التطبيق: الفرز حسب المسافة + ترقيم الصفحات + تخزين النتائج مؤقتًا. تدفق العمل النموذجي: يفتح المستخدم «المقاهي القريبة» → استرداد موقع المستخدم → بحث $geoNear في نطاق 3 كيلومترات → الفرز حسب المسافة → عرض أفضل 20 نتيجة. عوامل الأداء الرئيسية: فهرس 2dsphere + حد maxDistance + التحكم في عدد النتائج المعروضة.
استراتيجية التخزين المؤقت لخدمات تحديد المواقع (LBS): يُعد التخزين المؤقت لاستعلامات خدمات تحديد المواقع (LBS) أكثر تعقيدًا من الاستعلامات العادية — نظرًا لأن المواقع متصلة، وتتداخل نتائج البحث عن المواقع المتجاورة بشكل كبير. استراتيجية التخزين المؤقت: 1. التخزين المؤقت باستخدام الشبكة الجغرافية (تقسيم الخريطة إلى شبكات مقاس 1 كم × 1 كم، وتخزين نتائج البحث لكل شبكة مؤقتًا، وإرجاع النتائج المخزنة مؤقتًا للاستعلامات داخل نفس الشبكة)؛ 2. التخزين المؤقت القائم على المستخدم (تخزين النتائج المؤقتة لـ «المواقع التي بحث عنها المستخدم مؤخرًا»، مع مدة صلاحية (TTL) مدتها 5 دقائق)؛ 3. التخزين المؤقت للمناطق الشائعة (حساب النتائج مسبقًا للمناطق التي يكثر البحث عنها مثل مراكز المدن، مع إجراء استعلامات في الوقت الفعلي للمناطق الأقل شهرة). ملاحظة: انتهاء صلاحية ذاكرة التخزين المؤقت — عندما تتغير المعلومات المخزنة، يجب مسح ذاكرة التخزين المؤقت الخاصة بالشبكة المتأثرة.
الاختيار بين الفهارس ثنائية الأبعاد (2d) و2dsphere: تقدم MongoDB نوعين من الفهارس الجغرافية المكانية — 2d (إحداثيات مستوية، مناسبة للخرائط المسطحة ومشاهد الألعاب، يتم تخزينها باستخدام أزواج الإحداثيات القديمة [x, y]) و2dsphere (إحداثيات كروية، مناسبة لإحداثيات الأرض في العالم الحقيقي، يتم تخزينها باستخدام GeoJSON Point {type: 'Point', coordinates: [lng, lat]}). يجب استخدام 2dsphere في 99% من سيناريوهات LBS— 1. يدعم حسابات المسافة الكروية الحقيقية (يستخدم 2d المسافة الأوقليدية، مما يؤدي إلى أخطاء كبيرة عند خطوط العرض العالية)؛ 2. يدعم جميع العوامل الجغرافية المكانية مثل $geoNear و$geoWithin و$near (يدعم 2d وظائف جزئية فقط لـ $near)؛ 3. يدعم أشكال GeoJSON متعددة (Point/LineString/Polygon). يجب استخدام 2d فقط في السيناريوهات المستوية بحتة مثل خرائط الألعاب.
(1) إنشاء فهرس 2dsphere
// === Add a Location Field ===
db.stores.insertOne({
name: 'Tokyo Store',
location: {
type: 'Point',
coordinates: [139.6917, 35.6895] // [longitude, latitude]
}
});
// === Create 2dsphere Index ===
db.stores.createIndex({ location: '2dsphere' });
(2) الاستعلامات الجغرافية
النقاط الرئيسية حول تنسيق إحداثيات GeoJSON: تستخدم الاستعلامات الجغرافية المكانية في MongoDB تنسيق GeoJSON — النوع: 'Point' + الإحداثيات: [lng, lat]. هناك خطأان شائعان: 1. عكس ترتيب خطوط الطول والعرض (تستخدم خرائط Google الترتيب lat/lng، بينما تستخدم MongoDB الترتيب lng/lat؛ تأكد من مراعاة ذلك)؛ 2. قيم الإحداثيات خارج النطاق (خط الطول من -180 إلى 180، وخط العرض من -90 إلى 90). وحدة المسافة الافتراضية لـ $geoNear هي المتر (عندما تكون spherical: true)؛ بينما تشير maxDistance: 5000 إلى نطاق يبلغ 5 كيلومترات.
تحويل $centerSphere إلى راديان: وحدات قياس نصف القطر في $geoWithin و$centerSphere هي الراديان — 1 راديان ≈ 6,378.1 كيلومتر (نصف قطر خط الاستواء للأرض). صيغة التحويل: راديان = كيلومتر / 6378.1. مثال: 5 كيلومترات = 5/6378.1 ≈ 0.000784 راديان. هذا التحويل عرضة للأخطاء، لذا يُنصح بتغليفه كدالة مساعدة: kmToRadians(km) { return km / 6378.1; }.
// === $geoNear Find Nearby ===
db.stores.aggregate([
{
$geoNear: {
near: { type: 'Point', coordinates: [139.6917, 35.6895] }, // Tokyo Coordinates
distanceField: 'distance', // Output Field
maxDistance: 5000, // Maximum Distance 5km (meters)
spherical: true
}
}
]);
// === $geoWithin + $centerSphere Range Query ===
db.stores.find({
location: {
$geoWithin: {
$centerSphere: [
[139.6917, 35.6895], // Center Point
5 / 6378.1 // 5km Radius (radians)
]
}
}
});
// === $nearSphere Simple Query ===
db.stores.find({
location: {
$nearSphere: {
$geometry: { type: 'Point', coordinates: [139.6917, 35.6895] },
$maxDistance: 5000
}
}
});
خصائص أداء $geoWithin: لا تقوم $geoWithin بالفرز أو حساب المسافات — بل تقتصر على تحديد ما إذا كانت نقطة ما تقع داخل منطقة ما أم لا، لذا فهي أسرع بكثير من $geoNear. تعد $geoWithin مناسبة للاستعلامات الإحصائية التي تسأل «كم عدد العناصر الموجودة داخل منطقة ما» (على سبيل المثال، «المتاجر الموجودة داخل منطقة التوصيل»)، بينما تعد $geoNear مناسبة للاستعلامات المصنفة التي تسأل عن «أقرب العناصر» (على سبيل المثال، «أقرب 5 مقاهي»). تدعم $geoWithin ثلاثة أنواع من أشكال المناطق: $centerSphere (دائرة)، و$polygon (مضلع)، و$box (مستطيل).
الاختيار بين $geoWithin و$geoNear: يعتمد اختيار العامل على متطلبات العمل — 1. الحاجة إلى «أقرب N نتيجة» → $geoNear (مرتبة حسب المسافة + الحد الأقصى)؛ 2. الحاجة إلى «جميع النتائج داخل منطقة ما» → $geoWithin (بدون ترتيب، أداء أفضل)؛ 3. الحاجة إلى قيم المسافة → $geoNear (تُخرج حقل المسافة)؛ 4. الحاجة إلى الدمج مع $facet → $geoWithin (لا يمكن استخدام $geoNear داخل $facet لأنه يجب أن يكون المرحلة الأولى في مسار المعالجة)؛ 5. استعلامات مساحة المضلعات → $geoWithin + $polygon (يدعم $geoNear المناطق الدائرية فقط). استخدم $geoWithin لمناطق التوصيل (المضلعات غير المنتظمة) و$geoNear للبحث عن الأماكن القريبة (المناطق الدائرية).
استراتيجية التخزين المؤقت لنظام LBS: يمكن تخزين نتائج استعلامات LBS مؤقتًا — حيث تظل نتائج البحث الخاصة بمستخدم في نفس الموقع دون تغيير لفترة قصيرة — 1. تصميم مفتاح التخزين المؤقت: lbs:{lat}:{lng}:{radius}:{category}:{keyword}، ترميز جميع معلمات الاستعلام في مفتاح التخزين المؤقت؛ 2. دقة التخزين المؤقت: يتم الاحتفاظ بخطوط العرض والطول حتى 3 أرقام عشرية (بدقة تبلغ حوالي 110 مترًا)؛ ويتشارك المستخدمون داخل نفس الشبكة في التخزين المؤقت؛ 3. إعداد مدة الصلاحية (TTL): 3–5 دقائق (تظل مواقع المتاجر دون تغيير على المدى القصير، ولكن يجب عرض المتاجر التي تم افتتاحها حديثًا على الفور)؛ 4. التحميل المسبق للذاكرة المؤقتة: يتم تخزين نتائج البحث الخاصة بالمناطق التجارية الشهيرة (مثل سانليتون في بكين وشارع نانجينغ في شنغهاي) مسبقًا في الذاكرة المؤقتة؛ 5. استراتيجية انتهاء الصلاحية: عند بدء تشغيل متاجر جديدة، يتم مسح الذاكرة المؤقتة للمنطقة المقابلة. يمكن لتخزين LBS المؤقت أن يقلل من استعلامات قاعدة البيانات بأكثر من 80٪، وهو الطريقة الأساسية لتحسين أداء نظام LBS.
مرجع سريع لتحويل الراديان: تستخدم معلمات المسافة في $centerSphere و$nearSphere وحدة الراديان — تحويل الكيلومتر إلى الراديان = الكيلومتر / 6378.1؛ وتحويل الميل إلى الراديان = الميل / 3963.2. التحويلات الشائعة: 1 كم ≈ 0.00015696 راديان، 5 كم ≈ 0.0007848 راديان، 10 كم ≈ 0.00157 راديان. يستخدم المعامل maxDistance الخاص بـ $geoNear وحدة المتر (لا حاجة للتحويل)، وهو أحد الأسباب التي تجعل $geoNear أسهل في الاستخدام من $nearSphere.
دقة حسابات المسافة في أنظمة LBS: تستخدم الاستعلامات الجغرافية المكانية في MongoDB الهندسة الكروية (المقربة بواسطة الإهليلج WGS84) بشكل افتراضي — وتبلغ الدقة أقصى مستوياتها بالقرب من خط الاستواء (الخطأ < 0.5%)، ورغم انخفاضها قليلاً عند خطوط العرض العالية، فإنها تظل أعلى بكثير من الحسابات التي تستخدم الهندسة المستوية. وحدة قياس المسافة لمؤشرات 2dsphere هي المتر، كما أن ناتج distanceField لـ $geoNear يُقاس بالمتر أيضًا. اعتبارات الدقة الشائعة: 1. الدقة كافية للمسافات القصيرة (< 100 م) (الخطأ < 1 م)؛ 2. بالنسبة للمسافات الطويلة (> 1,000 كم)، قد يصل الخطأ إلى عشرات الأمتار (بسبب خطأ التقريب الكروي)؛ 3. تكون الدقة في أدنى مستوياتها في الاستعلامات عبر القطبين (على الرغم من أن مثل هذه السيناريوهات نادرة للغاية). بالنسبة لـ 99% من تطبيقات LBS (البحث عن المطاعم أو المتاجر القريبة)، تلبي دقة المسافة في MongoDB المتطلبات تمامًا.
6. التدريب العملي الشامل
نظرة عامة على المفهوم: يجمع هذا التمرين العملي الشامل بين $facet و$bucket والبحث النصي والاستعلامات الجغرافية المكانية لإنشاء نظام بحث متكامل للتجارة الإلكترونية. يعرض استعلام $facet واحد قائمة نتائج البحث، وإجمالي العدد، وفئات الأسعار، وإحصائيات الفئات في آن واحد، مما يتيح للواجهة الأمامية عرض صفحة البحث مباشرةً. وتوفر الاستعلامات الجغرافية المكانية الدعم لسيناريوهات الخدمات القائمة على الموقع (LBS).
كيفية العمل: يستقبل استعلام $facet في واجهة برمجة تطبيقات البحث (Search API) $match (البحث النصي + تصفية الفئات) كمدخل مشترك، وتقوم أربعة مسارات فرعية بمعالجته بشكل متوازٍ: العناصر (قائمة مقسمة إلى صفحات)، الإجمالي (العدد الإجمالي)، المعايير (شرائح الأسعار)، والفئات (إحصائيات الفئات). أما الاستعلامات الجغرافية المكانية، فيتم تنفيذها بشكل مستقل وتُرجع نتائج مرتبة حسب المسافة.
النقاط الرئيسية لتصميم بنية واجهة برمجة تطبيقات البحث: يتمثل التحدي الأساسي لواجهة برمجة تطبيقات البحث الشاملة في «إرجاع البيانات عبر جميع الأبعاد في طلب واحد» — حيث تتطلب الواجهة الأمامية قائمة (لعرض نتائج البحث)، وعددًا إجماليًّا (لتقسيم الصفحات)، وتصنيفًا إلى فئات (لفلترات السعر/التقييم)، وتصنيفًا حسب الفئات (للتنقل عبر الشريط الجانبي). تتيح $facet حساب هذه الأبعاد الأربعة بالتوازي بناءً على مدخل واحد، مما يغني عن إجراء أربعة استعلامات منفصلة. ومع ذلك، لاحظ أن $match التي تسبق $facet يجب أن تستوفي شروط كل من البحث النصي وفلاتر الأعمال (مثل الفئات ونطاقات الأسعار)؛ وإلا فإن المدخلات التي تراها كل خط أنابيب فرعي ستكون غير متسقة.
استراتيجيات فرز نتائج البحث: يؤثر فرز نتائج البحث بشكل مباشر على تجربة المستخدم — وينبغي اختيار استراتيجيات الفرز بناءً على السياق: 1. الفرز حسب الصلة ($meta: 'textScore'): عندما يبحث المستخدمون عن كلمات مفتاحية، يتوقعون ظهور النتائج الأكثر صلة في الأعلى؛ 2. الفرز حسب المسافة ($geoNear): عند البحث عن متاجر قريبة، يتوقع المستخدمون ظهور أقربها في الأعلى؛ 3. الترتيب حسب السعر: عند مقارنة الأسعار، يتوقع المستخدمون ظهور الأسعار الأقل أو الأعلى في الأعلى؛ 4. الترتيب حسب حجم المبيعات: عند إجراء عملية شراء، يتوقع المستخدمون ظهور العناصر الأكثر رواجًا في الأعلى؛ 5. الترتيب الشامل (صيغة مرجحة): تقوم $addFields بحساب درجة مركبة = 0.4 × الصلة + 0.3 × حجم المبيعات + 0.2 × التقييم + 0.1 × حداثة المعلومات. الترتيب الشامل هو الحل الأمثل للبحث في التجارة الإلكترونية.
تصميم تجربة المستخدم لأنظمة البحث: البحث أكثر من مجرد «إدخال كلمات مفتاحية وعرض النتائج» — فالتجربة الكاملة للبحث تشمل: 1. اقتراحات البحث (الإكمال التلقائي أثناء الكتابة)؛ 2. نتائج البحث (قائمة + فرز)؛ 3. التصفية حسب المعايير (مرشحات الفئات/السعر/العلامة التجارية)؛ 4. إحصائيات النتائج (العدد الإجمالي/التوزيع حسب الفئة)؛ 5. التعامل مع النتائج الفارغة (توصية بالعناصر الشائعة أو اقتراح تعديلات على الكلمات المفتاحية). يمكن لـ $facet من MongoDB توفير البيانات الخاصة بالبنود 2 و3 و4 في استعلام واحد، بينما تتطلب اقتراحات البحث والتعامل مع النتائج الفارغة استعلامات إضافية ومنطقًا تجاريًا.
graph TB
A[Search Request<br/>q=5G phone<br/>category=Electronics] --> B[$match<br/>$text + category]
B --> C[$facet]
C --> D[items<br/>$sort+skip+limit<br/>Paginated List]
C --> E[total<br/>$count<br/>Total]
C --> F[facets<br/>$bucket<br/>Prices by Bin]
C --> G[categories<br/>$group<br/>Categorical Statistics]
D --> H[Search Results Page<br/>Returns one result]
E --> H
F --> H
G --> H
style C fill:#d4edda
style H fill:#cce5ff
(1) البحث عن المنتجات + الإحصاءات المصنفة
القيمة التجارية للبحث المُصنَّف: يُعد البحث المُصنَّف نموذج التفاعل الأساسي للبحث في مجال التجارة الإلكترونية — فبعد أن يدخل المستخدم كلمة بحث، تعرض الصفحة في آن واحد نتائج البحث ومعايير التصفية عبر أبعاد مختلفة (النطاق السعري، الفئة، العلامة التجارية، التقييم). وعندما ينقر المستخدم على أي معيار تصفية، يتم تضييق نطاق نتائج البحث على الفور، ويتم تحديث أعداد المعايير في الوقت الفعلي. وتعد هذه التجربة التكرارية المتمثلة في «البحث → التصفية → البحث مرة أخرى» أكثر كفاءة بعشر مرات من النهج التقليدي المتمثل في «البحث → تقسيم الصفحات → البحث مرة أخرى». ويُعد $facet أفضل أداة لتنفيذ البحث المتعدد الأبعاد — فهو يعرض إحصائيات عبر جميع الأبعاد في استعلام واحد، مما يلغي الحاجة إلى إرسال طلبات متعددة.
تصميم معلمات واجهة برمجة تطبيقات البحث: يجب أن تغطي معلمات واجهة برمجة تطبيقات البحث جميع أبعاد التصفية — 1. الكلمة المفتاحية (q): $text — استعلام البحث؛ 2. الفئة (category): مطابقة تامة لحقل «category»؛ 3. النطاق السعري (minPrice/maxPrice): $gte/$lte لتصفية حقل price؛ 4. العلامة التجارية (brand): المطابقة التامة أو $in للاختيارات المتعددة؛ 5. التقييم (minRating): $gte لتصفية حقل rating؛ 6. ترقيم الصفحات (page/limit): $skip/$limit للتحكم في عدد النتائج؛ 7. الفرز (sort): الفرز حسب الصلة بالموضوع، أو السعر، أو التقييم، أو حجم المبيعات. مبادئ تصميم المعلمات: جميع معلمات التصفية اختيارية (تُرجع جميع النتائج عند عدم توفير أي معلمات)؛ معلمات ترقيم الصفحات لها قيم افتراضية (page=1، limit=20)؛ الترتيب له خيار افتراضي (sort=relevance).
// === Product Search + Multiple statistical dimensions ===
app.get('/api/products/search', async (req, res) => {
const { q, category } = req.query;
const match = {};
if (q) match.$text = { $search: q };
if (category) match.category = category;
const result = await Product.aggregate([
{ $match: match },
{
$facet: {
items: [
{ $sort: { score: { $meta: 'textScore' } } },
{ $skip: 0 },
{ $limit: 20 },
{ $project: { sku: 1, title: 1, price: 1, thumbnail: 1 } }
],
total: [{ $count: 'count' }],
facets: [
{
$bucket: {
groupBy: '$price',
boundaries: [0, 100, 500, 1000, 5000],
default: '5000+',
output: { count: { $sum: 1 } }
}
}
],
categories: [
{
$group: {
_id: '$category',
count: { $sum: 1 }
}
}
]
}
}
]);
res.json(result[0]);
});
(2) البحث عن المتاجر القريبة
// === Find Stores Near You ===
app.get('/api/stores/nearby', async (req, res) => {
const { lng, lat, maxDistance = 5000 } = req.query;
const stores = await Store.find({
location: {
$nearSphere: {
$geometry: { type: 'Point', coordinates: [parseFloat(lng), parseFloat(lat)] },
$maxDistance: parseInt(maxDistance)
}
}
}).limit(20);
res.json(stores);
});
▶ المثال 1: التطبيق العملي للبحث متعدد الأبعاد باستخدام $facet + المتاجر القريبة حسب الموقع
// Scene 1:Multi-dimensional Product Search(Text + Statistics by Category)
db.products.insertMany([
{ sku: 'PHONE-001', title: 'Smartphone X 5G', description: 'Latest 5G phone', price: 599, category: 'Electronics', location: { type: 'Point', coordinates: [139.6917, 35.6895] } },
{ sku: 'PHONE-002', title: 'Smartphone Pro 5G', description: 'Pro 5G phone', price: 899, category: 'Electronics', location: { type: 'Point', coordinates: [139.7017, 35.6995] } },
{ sku: 'LAPTOP-001', title: 'Laptop Pro', description: 'Powerful laptop', price: 1299, category: 'Computers', location: { type: 'Point', coordinates: [139.6817, 35.6795] } }
]);
db.products.createIndex({ title: 'text', description: 'text' });
db.products.createIndex({ location: '2dsphere' });
// Text Search + Statistics by Category + Prices by Bin
db.products.aggregate([
{ $match: { $text: { $search: '5G phone' } } },
{
$facet: {
// List of Results
items: [
{ $sort: { score: { $meta: 'textScore' } } },
{ $limit: 10 },
{ $project: { sku: 1, title: 1, price: 1, score: { $meta: 'textScore' } } }
],
// Total
total: [{ $count: 'count' }],
// Prices by Bin
priceBuckets: [
{
$bucket: {
groupBy: '$price',
boundaries: [0, 500, 1000, 2000],
default: '2000+',
output: { count: { $sum: 1 }, avgPrice: { $avg: '$price' } }
}
}
],
// Categorical Statistics
categories: [
{ $group: { _id: '$category', count: { $sum: 1 } } }
]
}
}
]);
// Scene 2:Find Stores Near You(5km Electronic products inside)
db.products.aggregate([
{
$geoNear: {
near: { type: 'Point', coordinates: [139.6917, 35.6895] }, // Tokyo Station
distanceField: 'distance',
maxDistance: 5000, // 5km
spherical: true,
query: { category: 'Electronics' }
}
},
{ $project: { sku: 1, title: 1, price: 1, distance: { $round: ['$distance', 0] } } }
]);
// Output: Distance to Each Item (meters), Sort by Distance
الإخراج:
TEXT 📖 للعرض فقط[ { sku: 'PHONE-001', title: 'Smartphone X 5G', price: 599, distance: 0 }, { sku: 'PHONE-002', title: 'Smartphone Pro 5G', price: 899, distance: 1456 } ]
النتيجة: السيناريو 1 يُرجع نتائج متعددة الأبعاد (قائمة + العدد الإجمالي + فئات الأسعار + الفئات)؛ أما السيناريو 2 فيُرجع قائمة بالمنتجات مرتبة حسب المسافة.
▶ المثال 2: واجهة برمجة تطبيقات (API) البحث المتعدد الأبعاد عن المنتجات من ShopHub
// Scene:ShopHub E-commerce Search Page,A single query returns a list+Separate into buckets+Categories+Total
db.products.insertMany([
{ sku: 'PHONE-001', title: 'Smartphone X 5G', description: 'Latest 5G phone with amazing camera', price: 599, category: 'Electronics', brand: 'TechCorp' },
{ sku: 'PHONE-002', title: 'Budget Phone 5G', description: 'Affordable 5G phone', price: 299, category: 'Electronics', brand: 'DataFlow' },
{ sku: 'TABLET-001', title: 'Tablet Pro 5G', description: '5G tablet for professionals', price: 899, category: 'Electronics', brand: 'TechCorp' },
{ sku: 'BOOK-001', title: '5G Technology Guide', description: 'Understanding 5G networks', price: 39, category: 'Books', brand: 'AppVenture' },
{ sku: 'WATCH-001', title: 'Smart Watch', description: 'Fitness tracker watch', price: 199, category: 'Wearables', brand: 'DataFlow' }
]);
db.products.createIndex({ title: 'text', description: 'text' });
// Multi-dimensional Search:Text Search + Separate into buckets + Categories + Total
const results = db.products.aggregate([
{ $match: { $text: { $search: '5G' } } },
{
$facet: {
items: [
{ $sort: { score: { $meta: 'textScore' } } },
{ $limit: 10 },
{ $project: { sku: 1, title: 1, price: 1, category: 1, score: { $meta: 'textScore' } } }
],
total: [{ $count: 'count' }],
priceBuckets: [
{
$bucket: {
groupBy: '$price',
boundaries: [0, 100, 300, 600, 1000],
default: '1000+',
output: { count: { $sum: 1 } }
}
}
],
categories: [
{ $group: { _id: '$category', count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]
}
}
]);
// Output Structure:
// {
// items: [Smartphone X 5G(score:2.5), Tablet Pro 5G(score:2.0), Budget Phone 5G(score:1.8), 5G Technology Guide(score:1.2)],
// total: [{count: 4}],
// priceBuckets: [{_id:0,count:1},{_id:100,count:1},{_id:300,count:1},{_id:600,count:1}],
// categories: [{_id:'Electronics',count:3},{_id:'Books',count:1}]
// }
النتيجة: يُرجع استعلام $facet واحد قائمة نتائج البحث (مرتبة حسب الصلة)، والعدد الإجمالي، وتوزيع الأسعار حسب الفئة، وإحصائيات الفئة، مما يتيح للواجهة الأمامية عرض صفحة البحث مباشرةً.
▶ المثال 3:نظام بحث متكامل للتجارة الإلكترونية(الصعوبة ⭐⭐⭐)
// Scene:ShopHub E-commerce product search with geo-location and faceted navigation
db.stores.insertMany([
{ _id: 'store_001', name: 'متجر الرياض', location: { type: 'Point', coordinates: [46.6753, 24.7136] }, address: 'شارع الملك فهد' },
{ _id: 'store_002', name: 'متهر جدة', location: { type: 'Point', coordinates: [39.1735, 21.4858] }, address: 'شارع الكورنيش' }
]);
db.products.insertMany([
{ sku: 'PHONE-001', title: 'هاتف ذكي X 5G', description: 'أحدث هاتف ذكي بتقنية 5G مع كاميرا رائعة', price: 599, category: 'Electronics', brand: 'TechCorp', storeId: 'store_001', stock: 50, rating: 4.5 },
{ sku: 'PHONE-002', title: 'هاتف ميزانية 5G', description: 'هاتف 5G اقتصادي', price: 299, category: 'Electronics', brand: 'DataFlow', storeId: 'store_001', stock: 100, rating: 4.0 },
{ sku: 'TABLET-001', title: 'تابلت برو 5G', description: 'تابلت 5G للمحترفين', price: 899, category: 'Electronics', brand: 'TechCorp', storeId: 'store_002', stock: 30, rating: 4.8 },
{ sku: 'WATCH-001', title: 'ساعة ذكية', description: 'ساعة رياضية ذكية', price: 199, category: 'Wearables', brand: 'DataFlow', storeId: 'store_002', stock: 75, rating: 4.2 }
]);
db.products.createIndex({ title: 'text', description: 'text' });
db.stores.createIndex({ location: '2dsphere' });
// بحث متكامل: نص + فئات الأسعار + الفئات + المتاجر القريبة
const searchResults = db.products.aggregate([
// 1. البحث النصي
{ $match: { $text: { $search: '5G ذكي' } } },
{
$facet: {
// 2. قائمة المنتجات
products: [
{ $sort: { score: { $meta: 'textScore' } } },
{ $limit: 10 },
{
$lookup: {
from: 'stores',
localField: 'storeId',
foreignField: '_id',
as: 'store'
}
},
{ $unwind: '$store' },
{
$project: {
sku: 1, title: 1, price: 1, category: 1, brand: 1, rating: 1,
store: '$store.name',
score: { $meta: 'textScore' }
}
}
],
// 3. إحصائيات الأسعار
priceBuckets: [
{
$bucket: {
groupBy: '$price',
boundaries: [0, 200, 500, 1000, 2000],
default: '2000+',
output: { count: { $sum: 1 }, avgRating: { $avg: '$rating' } }
}
}
],
// 4. إحصائيات الفئات
categoryStats: [
{ $group: { _id: '$category', count: { $sum: 1 }, avgPrice: { $avg: '$price' } } },
{ $sort: { count: -1 } }
],
// 5. العدد الإجمالي
totalCount: [{ $count: 'count' }]
}
}
]);
// البحث عن المتاجر القريبة
const nearbyStores = db.stores.aggregate([
{
$geoNear: {
near: { type: 'Point', coordinates: [46.6753, 24.7136] },
distanceField: 'distance',
maxDistance: 500000, // 500 كم
spherical: true
}
},
{
$lookup: {
from: 'products',
localField: '_id',
foreignField: 'storeId',
as: 'products'
}
},
{
$project: {
name: 1, address: 1,
distance: { $round: ['$distance', 0] },
productCount: { $size: '$products' }
}
}
]);
الإخراج:
TEXT 📖 للعرض فقط{ items: [ { sku: 'PHONE-001', title: 'Smartphone X 5G', price: 599, category: 'Electronics', score: 2.5 }, { sku: 'PHONE-002', title: 'Budget Phone 5G', price: 299, category: 'Electronics', score: 1.8 } ], total: [{ count: 4 }], priceBuckets: [{ _id: 0, count: 1 }, { _id: 100, count: 1 }, { _id: 300, count: 1 }, { _id: 600, count: 1 }], categories: [{ _id: 'Electronics', count: 3 }, { _id: 'Books', count: 1 }] }
الإخراج:
TEXT 📖 للعرض فقطsearchResults: { products: [{sku: 'PHONE-001', title: 'هاتف ذكي X 5G', price: 599, store: 'متجر الرياض', score: 2.5}, ...], priceBuckets: [{_id: 0, count: 1, avgRating: 4.2}, {_id: 200, count: 1, avgRating: 4.0}, ...], categoryStats: [{_id: 'Electronics', count: 3, avgPrice: 599}, ...], totalCount: [{count: 4}] } nearbyStores: [{name: 'متجر الرياض', address: 'شارع الملك فهد', distance: 0, productCount: 2}, ...]
❓ أسئلة شائعة
شرح الأسئلة الشائعة: تتناول هذه الأسئلة الأربعة القيود الأساسية في أربعة مجالات رئيسية — حد سعة الذاكرة في $facet، وحد اللغة في البحث النصي، وقواعد الإحداثيات في الاستعلامات الجغرافية المكانية، والحد الخوارزمي في $bucketAuto. لكل قيد أسبابه التقنية الخاصة به والاستراتيجيات المقابلة له. إن فهم هذه القيود أهم من مجرد حفظ الإجابات — على سبيل المثال، معرفة أن البحث النصي في MongoDB لا يدعم تجزئة الكلمات الصينية يتيح لك دمج Elasticsearch في تصميم البنية الخاصة بك منذ البداية.
[longitude, latitude] (ملاحظة: خط الطول يأتي أولاً).📖 ملخص
- يقوم $facet بتنفيذ عدة مسارات معالجة مستقلة بشكل متوازٍ ويعرض نتائج متعددة الأبعاد
- $bucket: تخصيص حاوية مخصصة؛ $bucketAuto: تخصيص حاوية تلقائي
- فهرس النص: نص + مرجح + بحث عن النص
- البيانات الجغرافية المكانية: 2dsphere + $geoNear + $geoWithin + $nearSphere
تكامل الموضوعات الأربعة الرئيسية: تعمل ميزتا $facet و$bucket على توسيع قدرات التجميع — مما يتيح لصفحات نتائج البحث عرض البيانات عبر جميع الأبعاد في آن واحد؛ كما يعمل البحث النصي والاستعلامات الجغرافية المكانية على توسيع قدرات الاستعلام — بدءًا من التطابقات الدقيقة وصولًا إلى البحث التقريبي، ومن الاستعلامات القائمة على السمات وصولًا إلى الاستعلامات المكانية. وتشكل هذه القدرات الأربع مجتمعة نظام بحث متكامل للتجارة الإلكترونية — البحث عن الكلمات المفتاحية + التصفية حسب الأوجه + تصنيف الأسعار حسب الفئات + المتاجر القريبة — تغطي 90% من سيناريوهات البحث.
قرارات الاختيار التقنية لأنظمة البحث: الاختيار بين البحث الأصلي في MongoDB وElasticsearch — 1. يُعد MongoDB مناسبًا لـ: المشاريع الصغيرة (أقل من 100,000 مستند)، ومنطق البحث البسيط (الكلمات المفتاحية + الفئة + تصفية الأسعار)، والحالات التي لا ترغب فيها في إدخال بنية تحتية جديدة؛ 2. يُعدّ Elasticsearch مناسبًا لـ: المشاريع الكبيرة (أكثر من مليون مستند)، والسيناريوهات التي تتطلب تجزئة الكلمات الصينية، والبحث باستخدام نظام بينيين، وتوسيع المرادفات، وخوارزميات الترتيب المعقدة (BM25 + التعزيزات المخصصة)، والفهرسة شبه الفورية (تحديثات بمستوى الميلي ثانية)؛ 3. النهج الهجين: تُخزَّن البيانات في MongoDB وتُزامن مع Elasticsearch عبر Change Stream، مع تشغيل الاستعلامات على Elasticsearch. يجمع هذا النهج الهجين بين أداء الكتابة في MongoDB وقدرات البحث في Elasticsearch — وهو البنية الأكثر شيوعًا في بيئات الإنتاج.
أنماط التصميم لخطوط الأنابيب الفرعية $facet: هناك ثلاثة أنماط تصميم لخطوط الأنابيب الفرعية $facet — 1. خطوط الأنابيب الفرعية المستقلة (الأكثر شيوعًا): تعالج كل خط أنابيب فرعي مجموعة البيانات بأكملها بشكل مستقل، دون أي تبعيات بينها (على سبيل المثال، يعرض خط الأنابيب الفرعي items قائمة، ويعرض خط الأنابيب الفرعي total عددًا، ويعرض خط الأنابيب الفرعي facets مجموعات)؛ 2. $match المشترك: تشترك جميع الأنابيب الفرعية في نتائج التصفية من مرحلة $match التي تسبق $facet، مما يتجنب التصفية المكررة؛ 3. $facet متداخلة (غير موصى بها): $facet متداخلة داخل $facet، مما يؤدي إلى منطق معقد ومضاعفة استهلاك الذاكرة. النمط 2 هو أفضل الممارسات — قم بتطبيق التصفية المشتركة مسبقًا (مثلًا حسب الفئة أو الكلمة المفتاحية أو النطاق السعري)، واجعل الأنابيب الفرعية تؤدي فقط المنطق الإحصائي الخاص بها.
📝 تمارين
- السؤال الأساسي (⭐): قم بإنشاء فهرس نصي (العنوان + الوصف) لمجموعة
productsوقم بإجراء بحث نصي. - المشكلة الأساسية (⭐): استخدم المتغير $bucket لحساب عدد العناصر حسب النطاق السعري.
- مشكلة متقدمة (⭐⭐): قم بتنفيذ واجهة برمجة تطبيقات (API) للبحث الكامل (البحث النصي + الإحصائيات القائمة على المجموعات + الإحصائيات القائمة على الفئات).
- تمرين متقدم (⭐⭐): قم بتنفيذ عملية بحث عن متجر قريب (2dsphere + $nearSphere).
- سؤال التحدي (⭐⭐⭐): نظام البحث المتكامل (النص + الجغرافيا + الإحصاءات ذات الأوجه).
توصيات لتنفيذ التحدي: يُعد نظام البحث الشامل التحدي الأكثر تعقيدًا في هذه الدورة التدريبية — فهو يتطلب الجمع بين أربع قدرات: $text (البحث النصي)، و$facet (الإحصاءات متعددة الأبعاد)، و$bucket (تصنيف الأسعار حسب الفئات)، و$geoNear (الفرز حسب المسافة). نوصي بالتنفيذ خطوة بخطوة: 1. أولاً، قم بتنفيذ البحث $text وقائمة النتائج؛ 2. ثم أضف $facet للإحصاءات متعددة الأبعاد؛ 3. بعد ذلك، أضف $bucket لتصنيف الأسعار حسب الفئات؛ 4. وأخيرًا، أضف البحث عن القرب $geoNear. لا تنتقل إلى الخطوة التالية إلا بعد التحقق من صحة كل خطوة. لاحظ أنه لا يمكن استخدام $text و$geoNear ضمن نفس $match — يجب أن يكون $text الشرط الأول في $match، ويجب أن يكون $geoNear المرحلة الأولى في مسار المعالجة. الحل: استخدم $facet لتشغيل عمليتي البحث بالتوازي، أو قم بتنفيذ $text أولاً، ثم $geoNear.
النقاط الرئيسية لنشر نظام بحث شامل: لا تزال النسخة التعليمية من التحدي على بعد بضع خطوات من مرحلة الإنتاج — 1. التحقق من صحة المدخلات: يجب التحقق من صحة جميع معلمات الاستعلام (طول q من 1 إلى 200، وقيم تعداد الفئات، ونطاق السعر من 0 إلى 999999، والتحقق من صحة تنسيق الإحداثيات)؛ 2. الترتيب الافتراضي: الترتيب حسب حجم المبيعات أو التقييم (وليس ترتيبًا عشوائيًا) في حالة عدم توفير كلمات رئيسية؛ والترتيب حسب الصلة عند توفير كلمات رئيسية؛ 3. تخزين النتائج مؤقتًا: تخزين النتائج مؤقتًا لمعلمات الاستعلام نفسها لمدة 5 دقائق (المفتاح = hash(params)) لتقليل الحمل على قاعدة البيانات؛ 4. الحماية من انتهاء المهلة: maxTimeMS: 5000 يحدد مدة تنفيذ التجميع؛ في حالة انتهاء المهلة، يتم إرجاع نتائج جزئية مصحوبة برسالة تنبه تفيد بأن «النتائج قد تكون غير كاملة»؛ 5. مراقبة الاستعلامات البطيئة: يتم تسجيل استعلامات البحث التي تتجاوز مدة تنفيذها ثانية واحدة من أجل تحسين الفهارس ومسارات المعالجة. تمثل هذه النقاط الفجوات الرئيسية بين انتقال نظام البحث من مرحلة العرض التوضيحي إلى مرحلة الإنتاج.