انتقل إلى المحتوى الرئيسي

التّجميعات: العدّ والتّجميع وتتبّع الزّمن على متن المدوَّنة

عرفت الوحدة 5 كيف نستخرج مقالات بعينها؛ هنا نطلب من المدوَّنة أرقامًا واتّجاهات. تُحضِّر Léa المراجعة الفصليّة لمنتج Veille: كم مقالًا في كلّ صنف؟ كيف يرتفع موضوع المناخ من سنة إلى أخرى؟ من يكتب الأكثر؟ تُجيب تجميعات Elasticsearch عن هذه الأسئلة في طلب واحد، دون أن تُعيد أيّ وثيقة.

عادتان تُغيّران كلّ شيء

size: 0 يخبر Elasticsearch ألّا يُعيد أيّ وثيقة: نُريد كتلة aggregations فحسب. نسيان size يُضيف 10 hits عديمة النّفع فوق الحساب.

تعيش التّجميعات تحت المفتاح aggs (المرادف aggregations). لكلّ تجميع اسم تختاره أنت، ونوع (terms أو date_histogram …)، ومعاملات، وربّما تجميعات فرعيّة داخل aggs الخاصّ به.

terms: العدّ حسب القيمة

يجمع terms الوثائق حسب القيمة المضبوطة لحقل keyword. وهو التّجميع الأكثر استعمالًا.

GET news/_search
{
"size": 0,
"aggs": {
"par_categorie": {
"terms": { "field": "category", "size": 5 }
}
}
}

النّتيجة المتوقَّعة، buckets في المقدّمة:

  • POLITICS — 32 739
  • WELLNESS — 17 827
  • ENTERTAINMENT — 16 058
  • TRAVEL — 9 887
  • STYLE & BEAUTY — 9 649

الحجم الإجماليّ للمدوَّنة 200 853 وثيقة؛ إذًا مجموع الـbuckets الخمسة الأولى يساوي 86 160، أي 43 % من المدوَّنة. يُعيد Elasticsearch كذلك doc_count_error_upper_bound (هامش خطأ محتمل مرتبط بالتّقسيم على shards؛ وهو صفر هنا لأنّ للفهرس shard واحدًا) وsum_other_doc_count (البقيّة).

على الكتّاب، نستهدف الحقل الفرعيّ keyword باسم authors.raw:

GET news/_search
{
"size": 0,
"aggs": {
"top_auteurs": {
"terms": { "field": "authors.raw", "size": 5 }
}
}
}

أوّل خمسة buckets:

  • Reuters — 4 954
  • Lee Moran — 2 433
  • Ron Dicker — 1 915
  • Ed Mazza — 1 328
  • Cole Delbyck — 1 145
terms على text

{"terms": {"field": "headline"}} يفشل أو يُكلِّف كثيرًا: headline مُحلَّل (titre_en) ولا يملك fielddata افتراضيًّا. نُجمِّع دومًا على keyword (هنا headline.raw وcategory وauthors.raw).

date_histogram: تتبّع الزّمن

يُقسِّم date_histogram محور الزّمن إلى فترات منتظمة. منذ Elasticsearch 8، نستعمل calendar_interval (المرتَكز على التّقويم: سنة، شهر، أسبوع، يوم) أو fixed_interval (مدّة ثابتة بالسّاعات أو الدّقائق). أُزيل interval القديم.

GET news/_search
{
"size": 0,
"aggs": {
"par_annee": {
"date_histogram": {
"field": "date",
"calendar_interval": "year",
"format": "yyyy"
}
}
}
}

تمتدّ المدوَّنة من 2012-01-28 إلى 2018-05-26، إذًا سبعة buckets (من 2012 إلى 2018). تُحذف الـbuckets الفارغة افتراضيًّا؛ أضِف "min_doc_count": 0 مع حدود extended_bounds للحصول على محور كامل.

للخطوة الشّهريّة:

GET news/_search
{
"size": 0,
"aggs": {
"par_mois": {
"date_histogram": {
"field": "date",
"calendar_interval": "month",
"format": "yyyy-MM"
}
}
}
}

(قد يختلف رقمك قليلًا من bucket لآخر حسب التّواريخ).

range: فترات على الطّلب

بينما يُقسِّم date_histogram بانتظام، يُعرِّف range شرائح مسمّاة.

GET news/_search
{
"size": 0,
"aggs": {
"avant_apres_2016": {
"range": {
"field": "date",
"ranges": [
{ "to": "2016-01-01", "key": "2012-2015" },
{ "from": "2016-01-01", "key": "2016-2018" }
]
}
}
}
}

cardinality: عدّ القيم المتمايزة

كم كاتبًا متمايزًا في المدوَّنة؟

GET news/_search
{
"size": 0,
"aggs": {
"nb_auteurs": {
"cardinality": { "field": "authors.raw" }
}
}
}

يستعمل cardinality خوارزميّة HyperLogLog++: سريعة وذات ذاكرة محدودة، لكنّها تقريبيّة. يضبط المعامل precision_threshold (افتراضيًّا 3 000، حدّه الأقصى 40 000) موازنة الدّقّة مقابل الذّاكرة — دون العتبة يكون العدّ دقيقًا شبه دائمًا؛ فوقها يبقى الخطأ النّسبيّ المتوسّط تحت 1 إلى 2 %.

أرقام مضبوطة على مجموعة صغيرة

على news، تدخل 41 صنفًا مضبوطًا بسهولة ضمن الدّقّة الافتراضيّة. للحصول على عدّ مضبوط تمامًا على مجموعة كبيرة جدًّا، يجب المرور بـcomposite (انظر أدناه) وعدّ الـbuckets في العميل.

avg وstats وmin وmax

تُطبَّق المقاييس الكلاسيكيّة على الحقول العدديّة وعلى التّواريخ. لا يملك news حقلًا رقميًّا في المجال، لكن يمكننا الاطّلاع على أقدم تاريخ وأحدثه:

GET news/_search
{
"size": 0,
"aggs": {
"bornes_dates": { "stats": { "field": "date" } }
}
}

تحوي الاستجابة min = 2012-01-28 وmax = 2018-05-26 (مع صيَغ min_as_string وmax_as_string قابلة للقراءة).

top_hits: عيّنة من كلّ bucket

top_hits تجميع فرعيّ يُعيد N وثيقة تمثيليّة لكلّ bucket أصل. مثاليّ لـ«أحدث عنوان في كلّ صنف».

GET news/_search
{
"size": 0,
"aggs": {
"par_categorie": {
"terms": { "field": "category", "size": 5 },
"aggs": {
"dernier_titre": {
"top_hits": {
"size": 1,
"sort": [ { "date": "desc" } ],
"_source": ["headline", "date"]
}
}
}
}
}
}

التّجميعات الفرعيّة: القوّة الحقيقيّة

التّداخل هو ما يُميّز التّجميعات عن GROUP BY بسيط. سؤال Léa: كم مقالًا لكلّ صنف، سنةً بسنة، لـPOLITICS وWELLNESS؟

GET news/_search
{
"size": 0,
"query": {
"terms": { "category": ["POLITICS", "WELLNESS"] }
},
"aggs": {
"par_categorie": {
"terms": { "field": "category", "size": 2 },
"aggs": {
"par_annee": {
"date_histogram": {
"field": "date",
"calendar_interval": "year",
"format": "yyyy"
}
}
}
}
}
}

كلّ bucket أصل (POLITICS وWELLNESS) يحمل par_annee الخاصّ به. مجموع par_categorie.POLITICS يساوي 32 739؛ ومجموع par_categorie.WELLNESS يساوي 17 827؛ وكلّ ذلك في طلب واحد.

filter وfilters في التّجميع

يُقيِّد filter (بالمفرد) الحساب على مجموعة فرعيّة دون تغيير الاستعلام العامّ.

GET news/_search
{
"size": 0,
"aggs": {
"recents": {
"filter": { "range": { "date": { "gte": "2017-01-01" } } },
"aggs": {
"par_categorie": { "terms": { "field": "category", "size": 5 } }
}
}
}
}

يُنتج filters (بالجمع) N buckets مسمّاة في مرور واحد، شبيهًا نوعًا ما بـrange لكن بمعايير حرّة.

GET news/_search
{
"size": 0,
"aggs": {
"sujets": {
"filters": {
"filters": {
"climat": { "match": { "headline": "climate change" } },
"election": { "match": { "headline": "election" } },
"sante": { "match": { "headline": "health" } }
}
}
}
}
}

يحوي bucket climat عددًا يبلغ 2 834 وثيقة (انظر الوحدة 5).

composite: ترقيم تجميع

terms بـsize: 10000 نمط مضادّ: ترتفع الذّاكرة ولا شيء يضمن استرداد كلّ المفاتيح. composite يُرقِّم بأسلوب نظيف، مع after_key نظير search_after.

GET news/_search
{
"size": 0,
"aggs": {
"toutes_categories": {
"composite": {
"size": 100,
"sources": [
{ "cat": { "terms": { "field": "category" } } }
]
}
}
}
}

تحوي الاستجابة after_key؛ نُعيد الاستدعاء بتمريره تحت "after": { "cat": "..." } حتّى النّفاد. على news، يكفي استدعاءان لتغطية 41 صنفًا مضبوطًا.

التّرتيب وsize: ثلاثة فخاخ

  • يُعيد terms افتراضيًّا الـbuckets مرتّبة بـdoc_count تنازليًّا. للتّرتيب بالمفتاح: "order": {"_key": "asc"}؛ بمقياس فرعيّ: "order": {"nom_metrique": "desc"}.
  • الحجم الافتراضيّ لـsize هو 10. يحتاج top 20 إلى "size": 20.
  • على عدّة shards، يطلب terms من كلّ shard shard_size مفتاحًا (افتراضيًّا size * 1.5 + 10) ثمّ يدمج. يقيس doc_count_error_upper_bound عدم اليقين. على news (shard واحد)، الهامش صفر.

قراءة استجابة تجميع

البنية منتظمة:

  • hits.total.value: عدد الوثائق المعنيّة بـquery.
  • aggregations.<nom>.buckets: قائمة الـbuckets.
  • كلّ bucket: key (القيمة) وdoc_count (العدد) والتّجميعات الفرعيّة تحت أسمائها.
  • في date_histogram: key (طابع epoch بالميليّة ثانية) وkey_as_string (السّلسلة المُنسَّقة).

كتابة فقرة صغيرة تُسمِّي كلّ حقل في اليوم الأوّل يجعل قراءة أيّ استجابة لاحقة أمرًا بلا جهد.

جرّب 1 — أعلى 3 أصناف في 2017

اكتب استعلامًا يُعيد الأصناف الثّلاثة الأكثر تواترًا للمقالات المنشورة في 2017 فقط. لا حاجة لأيّ بيانات من الوثيقة.

الحلّ
GET news/_search
{
"size": 0,
"query": {
"bool": {
"filter": [ { "range": { "date": { "gte": "2017-01-01", "lt": "2018-01-01" } } } ]
}
},
"aggs": {
"top3_2017": {
"terms": { "field": "category", "size": 3 }
}
}
}

يُقيِّد query نطاق التّحليل بسنة 2017 (كـfilter بلا score). ثمّ يعمل تجميع terms على هذه المدوَّنة الفرعيّة. قارن مع التّرتيب العامّ لترى إن كان POLITICS يبقى قبل WELLNESS في تلك السّنة (قد يختلف رقمك قليلًا).

جرّب 2 — عدد مقالات Lee Moran حسب الصّنف

يذكر الملخّص أنّ Lee Moran كتب 2 433 مقالًا إجمالًا. كم منها في COMEDY وWEIRD NEWS وENTERTAINMENT وPOLITICS؟

الحلّ
GET news/_search
{
"size": 0,
"query": {
"term": { "authors.raw": "Lee Moran" }
},
"aggs": {
"par_categorie": {
"terms": { "field": "category", "size": 5 }
}
}
}

الـbuckets المتوقَّعة: COMEDY 779 وWEIRD NEWS 391 وENTERTAINMENT 387 وPOLITICS 264 وSPORTS 101. المجموع (1 922) أقلّ من 2 433: تتوزّع البقيّة على الأصناف الـ36 الأخرى. مرِّر "size": 41 لترى كلّ شيء.

جرّب 3 — عدد الكتّاب المتمايزين حسب السّنة

هل المدوَّنة تصبح أكثر ثراءً بالمساهمين؟ أعِد سنة ← عدد الكتّاب المتمايزين (تقريبًا).

الحلّ
GET news/_search
{
"size": 0,
"aggs": {
"par_annee": {
"date_histogram": {
"field": "date",
"calendar_interval": "year",
"format": "yyyy"
},
"aggs": {
"auteurs_distincts": {
"cardinality": { "field": "authors.raw", "precision_threshold": 5000 }
}
}
}
}
}

كلّ bucket من par_annee يحمل تجميعًا فرعيًّا cardinality. يرتفع precision_threshold للحفاظ على دقّة جيّدة على 23 082 كاتبًا متمايزًا مُحصًى في المدوَّنة.

النقاط الأساسيّة

  • size: 0 يعزل التّجميعات؛ تختفي الـhits عديمة الفائدة.
  • يُجمِّع terms بـkeyword، ويُجمِّع date_histogram بفترة calendar_interval أو fixed_interval.
  • cardinality هو HyperLogLog++ تقريبيّ: اضبط precision_threshold وفق الدّقّة المطلوبة.
  • تُعيد التّجميعات الفرعيّة الحساب داخل كلّ bucket أصل: أصناف حسب السّنة، كتّاب حسب الصّنف.
  • يُقيِّد filter/filters تجميعًا دون المساس بـquery؛ ويُرقِّم composite بـafter_key.
  • اقرأ الاستجابة بالتّرتيب: buckets[].key وbuckets[].doc_count وbuckets[].<sous_agg>.

استكشاف الأخطاء

  • «Fielddata is disabled on text fields by default» على terms من headline → جمِّع على الحقل الفرعيّ keyword (headline.raw وauthors.raw)؛ لا تُعِد تفعيل fielddata.
  • doc_count_error_upper_bound غير صفريّ والقمّة تتحرّك → ارفع shard_size أو size؛ على news (shard واحد) الخطأ صفر.
  • يُعيد date_histogram صفر bucket → تأكّد من أنّ الحقل من نوع date بـ./lab.sh es news/_mapping?filter_path=**.date؛ وإلّا صحّح الـmapping (الوحدة 4).
  • «Trying to create too many buckets» → حدّ size أو انتقل إلى composite بـsize: 100 مع التّرقيم.

للاستزادة