חיפוש פקטות ב-Elasticsearch עם אגרגציות: מוני דיוק

קשה למצוא את המוצר הנכון בקטלוג כשהפילטרים מציגים מונים לא מדויקים ומאטים את החיפוש. אנו מגדירים את Elasticsearch לחיפוש פנים (faceted search) עם אגרגציות נכונות, כך שלקוחות מוצאים מיד את מה שהם צריכים. הצוות שלנו מספק את הפרויקט במפתח מלא—מביקורת ועד הטמעה ותמיכה מתמשכת.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
חיפוש פקטות ב-Elasticsearch עם אגרגציות: מוני דיוק
מורכב
~3-5 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1365
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1330
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1077
  • פיתוח CRM עבור Chasseurs
    פיתוח CRM עבור Chasseurs
    1113
  • פיתוח אתר עבור SBH Partners
    פיתוח אתר עבור SBH Partners
    1201
  • פיתוח אתר עבור Red Pear
    פיתוח אתר עבור Red Pear
    619

חיפוש פנים (Faceted Search) עם Elasticsearch ואגרגציות: מוני ספירה מדויקים

דמיינו קטלוג עם מיליון מוצרים. ללא חיפוש פנים, המשתמש דפדף ב-200 עמודים. עם זה, הוא מוצא את המותג וטווח המחירים הרצויים תוך שניות. אבל המונים משקרים אם לא מגדירים את האגרגציות POST /products/_search { "query": { "bool": { "must": [ { "match": { "title": "ноутбук" } } ], "filter": [ { "term": { "is_active": true } }, { "range": { "price": { "gte": 20000, "lte": 80000 } } } ] } }, "aggs": { "by_brand": { "terms": { "field": "brand", "size": 20, "order": { "_count": "desc" } } }, "price_ranges": { "range": { "field": "price", "ranges": [ { "key": "до 30 000", "to": 30000 }, { "key": "30–60 000", "from": 30000, "to": 60000 }, { "key": "60–100 000", "from": 60000, "to": 100000 }, { "key": "от 100 000", "from": 100000 } ] } }, "price_stats": { "stats": { "field": "price" } }, "by_category": { "terms": { "field": "category", "size": 10 } }, "by_rating": { "histogram": { "field": "rating", "interval": 1, "min_doc_count": 1 } } }, "size": 20, "from": 0 } ו-filter. מאמר זה מכסה כיצד לבנות מוני פנים נכונים על Elasticsearch, תוך הימנעות מטעויות אופייניות.

אנו משלבים את Elasticsearch עם חיפוש פנים בקטלוגים ובחנויות מסחר אלקטרוני. אלה מוני סינון: "מותג: Samsung (143), Apple (89)", "מחיר: עד 30,000 (234), 30–60,000 (145)". כאשר נבחר מסנן, רשימת המוצרים מצטמצמת, והמונים של ערכים אחרים מתעדכנים. במסדי נתונים רלציוניים פעולות כאלה יקרות — GROUP BY עם COUNT(*) על קבוצה מסוננת. ב-Elasticsearch, המשימה נפתרת עם אגרגציות, לעיתים קרובות בבקשה אחת. אנו נעריך את הפרויקט שלך וניישם אותו במפתח מלא תוך 2–4 ימי עבודה.

כיצד ליישם מוני פנים נכונים

בקשה אופיינית לקטלוג: שאילתת חיפוש + מסננים פעילים + אגרגציות לספירת פנים. דוגמה לבקשה בסיסית:

POST /products/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "title": "ноутбук"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "is_active": true
          }
        },
        {
          "range": {
            "price": {
              "gte": 20000,
              "lte": 80000
            }
          }
        }
      ]
    }
  },
  "aggs": {
    "by_brand": {
      "terms": {
        "field": "brand",
        "size": 20,
        "order": {
          "_count": "desc"
        }
      }
    },
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          {
            "key": "до 30 000",
            "to": 30000
          },
          {
            "key": "30–60 000",
            "from": 30000,
            "to": 60000
          },
          {
            "key": "60–100 000",
            "from": 60000,
            "to": 100000
          },
          {
            "key": "от 100 000",
            "from": 100000
          }
        ]
      }
    },
    "price_stats": {
      "stats": {
        "field": "price"
      }
    },
    "by_category": {
      "terms": {
        "field": "category",
        "size": 10
      }
    },
    "by_rating": {
      "histogram": {
        "field": "rating",
        "interval": 1,
        "min_doc_count": 1
      }
    }
  },
  "size": 20,
  "from": 0
}

הבעיה של מונים נעלמים בעת בחירת פן

אם המשתמש בוחר מותג "Samsung" ומוסיף אותו ל-by_brand, האגרגציה POST /products/_search { "query": { "bool": { "must": [ { "match": { "title": "ноутбук" } } ], "filter": [ { "term": { "is_active": true } } ] } }, "post_filter": { "bool": { "filter": [ { "term": { "brand": "Samsung" } } ] } }, "aggs": { "all_brands": { "global": {}, "aggs": { "filtered_brands": { "filter": { "bool": { "must": [ { "match": { "title": "ноутбук" } } ], "filter": [ { "term": { "is_active": true } } ] } }, "aggs": { "brands": { "terms": { "field": "brand", "size": 20 } } } } } }, "price_ranges": { "range": { "field": "price", "ranges": [ { "key": "до 30 000", "to": 30000 }, { "key": "30–60 000", "from": 30000, "to": 60000 }, { "key": "от 60 000", "from": 60000 } ] } } }, "size": 20 } סופרת רק בתוך הקבוצה המסוננת. המונים של מותגים אחרים הופכים לאפס — המשתמש לא יכול לראות כמה מוצרים יש ממותגים אחרים. הפתרון הוא Post Filter בשילוב עם Global Aggregation:

POST /products/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "title": "ноутбук"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "is_active": true
          }
        }
      ]
    }
  },
  "post_filter": {
    "bool": {
      "filter": [
        {
          "term": {
            "brand": "Samsung"
          }
        }
      ]
    }
  },
  "aggs": {
    "all_brands": {
      "global": {},
      "aggs": {
        "filtered_brands": {
          "filter": {
            "bool": {
              "must": [
                {
                  "match": {
                    "title": "ноутбук"
                  }
                }
              ],
              "filter": [
                {
                  "term": {
                    "is_active": true
                  }
                }
              ]
            }
          },
          "aggs": {
            "brands": {
              "terms": {
                "field": "brand",
                "size": 20
              }
            }
          }
        }
      }
    },
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          {
            "key": "до 30 000",
            "to": 30000
          },
          {
            "key": "30–60 000",
            "from": 30000,
            "to": 60000
          },
          {
            "key": "от 60 000",
            "from": 60000
          }
        ]
      }
    }
  },
  "size": 20
}

לפי תיעוד Elasticsearch, post_filter מוחל על התוצאות לאחר האגרגציה. האגרגציה global חורגת מההקשר של השאילתה — היא מאפשרת לספור אגרגטים על פני כל המסמכים עם מסננים נוספים.

כיצד לעבוד עם תכונות מקוננות?

אם תכונות (גודל, צבע, חומר) מאוחסנות כאובייקטים מסוג nested:

"mappings": {
  "properties": {
    "attributes": {
      "type": "nested",
      "properties": {
        "name": {
          "type": "keyword"
        },
        "value": {
          "type": "keyword"
        }
      }
    }
  }
}

אגרגציה על תכונות מקוננות:

"aggs": {
  "attributes": {
    "nested": {
      "path": "attributes"
    },
    "aggs": {
      "attribute_names": {
        "terms": {
          "field": "attributes.name",
          "size": 50
        },
        "aggs": {
          "attribute_values": {
            "terms": {
              "field": "attributes.value",
              "size": 20
            }
          }
        }
      }
    }
  }
}

זוהי אגרגציה מקוננת דו-רמתית: תחילה קיבוץ לפי שם התכונה ("צבע", "גודל"), בתוך כל קבוצה — ערכים ("שחור", "לבן").

Cardinality — ספירת ערכים ייחודיים

כדי להציג "נמצאו 1,247 מוצרים מ-89 מותגים", השתמשו באגרגציית cardinality עם השדה "mappings": { "properties": { "attributes": { "type": "nested", "properties": { "name": { "type": "keyword" }, "value": { "type": "keyword" } } } } } ו-"aggs": { "attributes": { "nested": { "path": "attributes" }, "aggs": { "attribute_names": { "terms": { "field": "attributes.name", "size": 50 }, "aggs": { "attribute_values": { "terms": { "field": "attributes.value", "size": 20 } } } } } } } 100. זה מבטיח דיוק עד 100 ערכים ייחודיים; מעל זה, מרווח השגיאה הוא 1–6%.

הגדרת חיפוש פנים ב-5 שלבים

  1. זיהוי שדות לסינון: מותג, מחיר, קטגוריה, דירוג, תכונות. קבוצה אופיינית היא 5–10 שדות.
  2. הגדרת mapping: השתמשו בסוג brand לשדות מפתח, precision_threshold למובנים. ודאו ששדות לאגרגציות אינם מנותחים.
  3. בניית שאילתה עם אגרגציות: כללו את כל האגרגציות הנדרשות (terms, range, histogram, nested). למונים נכונים, השתמשו באגרגציות keyword ו-nested.
  4. החלת post_filter: הפרידו מסננים מהשאילתה הראשית. זה שומר על מוני האגרגציה.
  5. אופטימיזציית ביצועים: השתמשו ב-post_filter לשדות עם קרדינליות נמוכה, caching לאגרגציות ללא שאילתת חיפוש, הגבילו גודל ל-20.

אופטימיזציית אגרגציות

אגרגציית global עם execution_hint: map גדול היא פעולה יקרה: כל shard מחזיר ערכי top-N, והצומת המרכזי ממזג תוצאות. עבור שדות עם קרדינליות גבוהה (אלפי ערכים ייחודיים), הביצועים מתדרדרים. שיטות אופטימיזציה:

טבלת שיטות אופטימיזציה
שיטה תיאור מתי ליישם
execution_hint: map לקרדינליות נמוכה (<1000) עובד מהר יותר מ-ordinals שדות סטטוס, קטגוריה
אגרגציה caching אגרגציות ללא שאילתת חיפוש נשמרות ב-shard request cache עמודי קטלוג ללא חיפוש
הגבלת גודל לפנים, ערכי top-20 מספיקים; אין צורך בניווט עמוק תמיד

השוואת סוגי אגרגציות

סוג דוגמת שימוש ביצועים
Terms קיבוץ לפי מותג מהיר כאשר קרדינליות <10k
Range טווחי מחירים מהיר מאוד
Histogram דירוג עם צעד 1 מהיר
Nested תכונות מקוננות בינוני (תלוי במספר האובייקטים)
Cardinality מותגים ייחודיים מהיר (אלגוריתם HyperLogLog)

יישום בצד השרת

מחלקת PHP לבניית שאילתות פנים:

class FacetedSearchService {
    public function search(array $params): array {
        $query = $this->buildQuery($params);
        $response = $this->client->search([
            'index' => 'products',
            'body' => $query,
        ]);

        return [
            'hits' => $response['hits']['hits'],
            'total' => $response['hits']['total']['value'],
            'facets' => $this->extractFacets($response['aggregations']),
        ];
    }

    private function extractFacets(array $aggs): array {
        $facets = [];

        if (isset($aggs['by_brand'])) {
            $facets['brand'] = array_map(fn($b) => [
                'value' => $b['key'],
                'count' => $b['doc_count'],
            ], $aggs['by_brand']['buckets']);
        }

        if (isset($aggs['price_ranges'])) {
            $facets['price'] = array_map(fn($r) => [
                'label' => $r['key'],
                'count' => $r['doc_count'],
            ], $aggs['price_ranges']['buckets']);
        }

        return $facets;
    }
}

מה כלול בהגדרת חיפוש פנים במפתח מלא

  • עיצוב סכמות mapping ואינדקסים.
  • הגדרת כל סוגי האגרגציות (terms, range, nested, cardinality).
  • יישום לוגיקת post_filter + global aggregations למונים נכונים.
  • אופטימיזציית ביצועים (execution_hint, caching).
  • כתיבת קוד בצד השרת לבניית שאילתות ועיבוד תשובות.
  • תיעוד, הדרכת צוות, תמיכה לאחר היישום.

לוחות זמנים

  • יישום בסיסי (3–5 סוגי אגרגציות) — 2 ימי עבודה.
  • תרחיש מורכב (post_filter, global, nested) — 3–4 ימים.
  • אופטימיזציה על נפחים אמיתיים (>5 מיליון מסמכים) — יום נוסף.

לצוות שלנו ניסיון של 6+ שנים עם Elasticsearch ומהנדסים מוסמכים. השלמנו מעל 50 פרויקטים של חיפוש פנים, ומבטיחים דיוק מונים וביצועים. צרו קשר לייעוץ — נעזור לכם להימנע מטעויות יקרות. העלות מחושבת באופן אישי, והחיסכון מהיישום יכול להיות משמעותי. קבלו פתרון מלא עם תיעוד מלא ותמיכה.