الرقم اللي جنب البراند مش كويري تانية بسذاجة
اليوزر بحث عن laptop، وفلتر رام ١٦. جنب البراندات عايز يشوف: Lenovo (٤٠)، Apple (١٢)، Dell (٩). الأرقام دي faceted search، وبتطلع من aggregations: عدد، نطاقات سعر، أصغر وأكبر سعر، متوسط. مش بتحسب score. بتلخّص المستندات اللي عدّت الشروط.
حقل brand لازم يكون keyword. الـ text المتقطع مش بينفع لعدّ القيم زي ما اليوزر شايفها.
الفخ: اخترت Apple فباقي البراندات اختفت
لو حطيت brand = Apple جوه نفس الفلاتر اللي الـ aggregation بتشوفها، العدّ هيطلّع Apple بس. اليوزر مش هيقدر يبدّل لـ Lenovo من غير ما يمسح الفلتر الأول.
القاعدة: عدّ البراند بكل الفلاتر ما عدا اختيار البراند نفسه. نفس الفكرة على نطاق السعر: عدّاد السعر متأثرش باختيار السعر الحالي، بس يتأثر بالبراند والرام.
function asFilter(clauses) { const list = clauses.filter(Boolean); if (list.length === 0) return { match_all: {} }; if (list.length === 1) return list[0]; return { bool: { filter: list } };}export function facetAggs({ price, rating, stock, brand, ram }) { return { brands: { filter: asFilter([price, rating, stock, ram]), aggs: { values: { terms: { field: "brand", size: 20 } } }, }, rams: { filter: asFilter([price, rating, stock, brand]), aggs: { values: { terms: { field: "ram_gb", size: 10 } } }, }, };}النتيجة في aggregations.brands.values.buckets: كل bucket فيه key و doc_count. إجمالي الصفحة نفسه ممكن يتقص عند ١٠٠٠٠. لو محتاج الرقم بالظبط ابعت track_total_hits: true، واعرف إن ده أغلى. كتير من المتاجر بتكتب “أكثر من ١٠٠٠٠” لما relation تبقى gte.
نطاقات السعر نفس الأسلوب، بـ range aggregation على حقل price، جوه filter فيه كل الشروط ما عدا السعر.
الاقتراح مش نسخة مصغّرة من بحث الصفحة
أول ما اليوزر يكتب iph عايز قائمة: iPhone 15، iPhone 15 Pro. كل حرف ممكن يبعت طلب جديد، فلو الطلب تقيل أو من غير انتظار قصير هتضرب Elasticsearch على الفاضي.
اختيار كويس لأسماء المنتجات. الحقل بيعمل لواحق جاهزة، والكويري bool_prefix. من غير fuzzy هنا.
تحكم أكتر في طول البادئة، والإندكس بيكبر. مناسب لو محتاج قواعد خاصة.
فيه كمان completion suggester: سريع أوي لأنه مبني في الذاكرة، وأقل مرونة لو الاقتراح لازم يحترم فلاتر كتير. اختار على حسب الشاشة، متفتحش التلاتة مع بعض من غير سبب.
export async function suggestNames(es, prefix) { const q = prefix.trim(); if (q.length < 2) return []; const result = await es.search({ index: "products", size: 8, _source: ["name"], query: { multi_match: { query: q, type: "bool_prefix", fields: ["name.suggest", "name.suggest._2gram", "name.suggest._3gram"], }, }, }); return result.hits.hits.map((hit) => hit._source.name);}الحقل name.suggest من نوع search_as_you_type يتضاف في الـ mapping جنب name، مش بدلّه. بحث الصفحة يفضل على name و description. في الواجهة استنى حوالي ٢٠٠ ملي ثانية بعد آخر حرف قبل الطلب، ومتبعتش طلب لكل حرف لو اليوزر لسه بيكتب بسرعة.
التظليل: مقطع، مش الوصف كله
لو الكلمة جوه وصف طويل، نتيجة البحث تعرض الحتة اللي حوالي الماتش. ده highlighting.
export const highlight = { fields: { description: { fragment_size: 140, number_of_fragments: 1 }, }, pre_tags: ["<mark>"], post_tags: ["</mark>"],};الرد بيرجع المقطع جاهز فيه العلامات، منفصل عن _source. اعرض المقطع كنص فيه تظليل، ومتحطّش HTML جاي من وصف التاجر من غير ما تنضّفه. العلامة اللي طلبتها أنت بس هي اللي مسموح بيها.
صفحة النتائج متبعتش description كامل لعشرين منتج. الاسم، السعر، الصورة، والمقطع. التفاصيل تتجاب لما يفتح المنتج، ومن SQL أو من مستند البحث حسب إيه اللي محتاجه الصفحة.
🧪 Lab
ابحث عن laptop من غير فلتر براند، واتأكد إن buckets البراند أكتر من واحد. بعدين ثبّت brand=Apple في كويري النتايج بس، وسيب aggregation البراند من غير الفلتر ده. Apple تفضل متعلّمة في الصفحة، و Lenovo لسه ظاهر بعدها رقم. لو Lenovo اختفت، فلتر البراند داخل على العدّ بالغلط.