Dynamische Filter mit ElasticSearch

ElasticSearch Filter Query

Einleitung

Der Suchindex ElasticSearch ist bei den meisten unserer Projekte ein fester und gern verwendeter Teil des Technologiestacks. Der Einsatz hat viele Vorteile: beispielsweise wird bei Frontend-Requests die SQL Datenbank nicht unnötig belastet, die Implementierung von Features wie Fuzzy Search geht - dank direkter Unterstützung von ElasticSearch - einfacher von der Hand und das Filtern von Produkten nach unterschiedlichen Kriterien ist intuitiv möglich.

In diesem Blogpost soll eine Lösung vorgestellt werden, wie mit verschiedenen Abfrage-Techniken voll dynamische Filter umgesetzt werden können. Hierzu werden mehrere unterschiedliche Lösungen vorgestellt, um den Weg zum finalen Ansatz nachvollziehbar zu halten.

Vorteil

Die Vorteile von dynamischen Filtern hinsichtlich der Nutzererfahrung liegen auf der Hand: der Nutzer erhält neben der Aktualisierung der Produktübersicht auch immer ein Feedback, welche weiteren Filter noch zur Auswahl stehen. Zusätzlich kann er keine Filter setzen, die in einem ergebnislosen Zustand enden. Das bedeutet, dass jede einzelne Aktion immer eine valide Eingabe darstellt. Hierzu ist es allerdings notwendig beim Setzen eines Filters die anderen zur Verfügung stehenden Filter ständig zu aktualisieren.

Umsetzung

Dynamische Filter lassen sich relativ einfach umsetzen: es wird eine Anfrage an ElasticSearch geschickt, um die Produkte zu laden die angezeigt werden sollen und über Aggregations werden die einzelnen Produktattribute zusammen gefasst, die als Filter zur Verfügung stehen sollen. Sobald einer der Filter gesetzt wird, wird neben der Produktliste aber auch das Ergebnis der Aggregation aktualisiert. Während das für alle anderen Filter durchaus in Ordnung ist, gehen an dieser Stelle alle Optionen für den ausgewählten Filter verloren. Beispielhaft kann das wie folgt aussehen:

Query:

GET /test_index/product/_search
{
    "aggs": {
        "colour": {
            "terms": {
                "field": "colour"
            }
        },
        "category": {
            "terms": {
                "field": "category"
            }
        },
        "size": {
            "terms": {
                "field": "size"
            }
        }
    }
}

Result:

{
   "took": 151,
   "timed_out": false,
   "_shards": {
      "total": 5,
      "successful": 5,
      "skipped": 0,
      "failed": 0
   },
   "hits": {
      "total": 31,
      "max_score": 1,
      "hits": [
         {
            "_index": "test_index",
            "_type": "product",
            "_id": "1234-MA",
            "_score": 1,
            "_source": {
               "category": "Trousers",
               "colour": "red",
               "sku": "1234-MA",
               "size": "M"
            }
         },
         {...},
         {...},
         {...},
         {...},
         {...},
         {...},
         {...},
         {...},
         {...}
      ]
   },
   "aggregations": {
      "colour": {
         "doc_count_error_upper_bound": 0,
         "sum_other_doc_count": 0,
         "buckets": [
            {
               "key": "blue",
               "doc_count": 12
            },
            {
               "key": "green",
               "doc_count": 12
            },
            {
               "key": "red",
               "doc_count": 7
            }
         ]
      },
      "size": {
         "doc_count_error_upper_bound": 0,
         "sum_other_doc_count": 0,
         "buckets": [
            {
               "key": "L",
               "doc_count": 8
            },
            {
               "key": "M",
               "doc_count": 8
            },
            {
               "key": "XL",
               "doc_count": 8
            },
            {
               "key": "S",
               "doc_count": 7
            }
         ]
      },
      "category": {
         "doc_count_error_upper_bound": 0,
         "sum_other_doc_count": 0,
         "buckets": [
            {
               "key": "Trousers",
               "doc_count": 12
            },
            {
               "key": "T-Shirt",
               "doc_count": 11
            },
            {
               "key": "Shoes",
               "doc_count": 8
            }
         ]
      }
   }
}

Query: Red Only

GET /test_index/product/_search
{
   "query": {
      "bool": {
         "must": [
            {
               "term": {
                  "colour": {
                     "value": "red"
                  }
               }
            }
         ]
      }
   },
   "aggs": {
      "colour": {
         "terms": {
            "field": "colour"
         }
      },
      "category": {
         "terms": {
            "field": "category"
         }
      },
      "size": {
         "terms": {
            "field": "size"
         }
      }
   }
}

Result:

{
   "took": 21,
   "timed_out": false,
   "_shards": {
      "total": 5,
      "successful": 5,
      "skipped": 0,
      "failed": 0
   },
   "hits": {
      "total": 7,
      "max_score": 1.7917595,
      "hits": [
         {
            "_index": "test_index",
            "_type": "product",
            "_id": "1234-A",
            "_score": 1.7917595,
            "_source": {
               "category": "Trousers",
               "colour": "red",
               "sku": "1234-A",
               "size": "S"
            }
         },
         {...},
         {...},
         {...},
         {...},
         {...},
         {...}
      ]
   },
   "aggregations": {
      "colour": {
         "doc_count_error_upper_bound": 0,
         "sum_other_doc_count": 0,
         "buckets": [
            {
               "key": "red",
               "doc_count": 7
            }
         ]
      },
      "size": {
         "doc_count_error_upper_bound": 0,
         "sum_other_doc_count": 0,
         "buckets": [
            {
               "key": "L",
               "doc_count": 2
            },
            {
               "key": "M",
               "doc_count": 2
            },
            {
               "key": "XL",
               "doc_count": 2
            },
            {
               "key": "S",
               "doc_count": 1
            }
         ]
      },
      "category": {
         "doc_count_error_upper_bound": 0,
         "sum_other_doc_count": 0,
         "buckets": [
            {
               "key": "Trousers",
               "doc_count": 4
            },
            {
               "key": "T-Shirt",
               "doc_count": 3
            }
         ]
      }
   }
}

Um die im Beispiel verlorenen Optionen für die Filtergrupe "Farbe" zu behalten, benötigt man in diesem Fall also entweder Logik im Frontend / Backend, damit die Informationen wieder indexiert werden können oder aber eine zusätzliche Anfrage an ElasticSearch, um die Optionen für die zuletzt gesetzte Filtergruppe zurück zu erhalten. Offensichtlich ist diese Lösung nicht sehr elegant und verlässt sich auf zusätzliche Logik, bei der eine Menge schief gehen kann. Beispielweise ist es hier eben möglich Filter zu setzen, die kein Ergebnis mehr erzielen. Diesen Zustand kann der Nutzer in der Regel ohne ein komplettes Zurücksetzen aller Filter auch nicht rückgängig machen, das heißt er verliert ein Stückweit die Möglichkeit selbst einen Fehler zu korrigieren. Noch dazu ist das Verhalten für den Besucher schlecht nachvollziehbar: Wieso können Filter konstruiert werden, die in eine Sackgasse führen?

Ein erster Schritt zur Lösung sind die sogenannten Post Filter von ElasticSearch. Das Verwenden von Post Filtern führt dazu, dass alle Aggregationen noch auf allen Dokumenten ausgeführt werden und die Ergebnismenge erst danach entsprechend den Filterkriterien eingeschränkt wird. Für die Aggregationen selbst bedeutet das offensichtlich, dass sie auch unnötige Informationen beinhalten können, wie zum Beispiel Farben die es in der über den Post Filter gewählten Kategorie nicht gibt. Umgedreht bleibt allerdings auch die aktuell gesetzte Filtergruppe mit allen Optionen vorhanden, was es erlaubt, die dafür benötigte Logik aus dem Programmcode zu entfernen.

Query:

GET /test_index/product/_search
{
   "post_filter": {
      "bool": {
         "must": [
            {
               "term": {
                  "colour": {
                     "value": "red"
                  }
               }
            }
         ]
      }
   },
   "aggs": {
      "colour": {
         "terms": {
            "field": "colour"
         }
      },
      "category": {
         "terms": {
            "field": "category"
         }
      },
      "size": {
         "terms": {
            "field": "size"
         }
      }
   }
}

Result:

{
   "took": 16,
   "timed_out": false,
   "_shards": {
      "total": 5,
      "successful": 5,
      "skipped": 0,
      "failed": 0
   },
   "hits": {
      "total": 7,
      "max_score": 1,
      "hits": [
         {
            "_index": "test_index",
            "_type": "product",
            "_id": "1234-MA",
            "_score": 1,
            "_source": {
               "category": "Trousers",
               "colour": "red",
               "sku": "1234-MA",
               "size": "M"
            }
         },
         {...},
         {...},
         {...},
         {...},
         {...},
         {...}
      ]
   },
   "aggregations": {
      "colour": {
         "doc_count_error_upper_bound": 0,
         "sum_other_doc_count": 0,
         "buckets": [
            {
               "key": "blue",
               "doc_count": 12
            },
            {
               "key": "green",
               "doc_count": 12
            },
            {
               "key": "red",
               "doc_count": 7
            }
         ]
      },
      "size": {
         "doc_count_error_upper_bound": 0,
         "sum_other_doc_count": 0,
         "buckets": [
            {
               "key": "L",
               "doc_count": 8
            },
            {
               "key": "M",
               "doc_count": 8
            },
            {
               "key": "XL",
               "doc_count": 8
            },
            {
               "key": "S",
               "doc_count": 7
            }
         ]
      },
      "category": {
         "doc_count_error_upper_bound": 0,
         "sum_other_doc_count": 0,
         "buckets": [
            {
               "key": "Trousers",
               "doc_count": 12
            },
            {
               "key": "T-Shirt",
               "doc_count": 11
            },
            {
               "key": "Shoes",
               "doc_count": 8
            }
         ]
      }
   }
}

Im Vergleich zur Ausgangslage besteht das Ergebnis der Aggregationen nun aber aus zu vielen Informationen - im Test-Datensatz gibt es keine roten Schuhe. Es wurde bisher lediglich das Problem behoben, dass ein gesetzter Filter dazu führt alle Optionen der entsprechenden Gruppe zu verlieren. Um die Ergebnismenge der Aggregationen wieder einzuschränken existiert das Konzept der Filtered Aggregations. Hiermit können analog zu einer normalen Suchanfrage die Dokumente, die für eine Aggregation in Betracht gezogen werden, beliebig eingeschränkt werden. Für die Betrachtung in diesem Artikel bedeutet das, dass alle Aggregationen mit (fast) denselben Argumenten nachgefiltert werden, die auch schon für den Post Filter verwendet werden.

Query:

GET /test_index/product/_search
{
   "post_filter": {
      "bool": {
         "must": [
            {
               "term": {
                  "colour": {
                     "value": "red"
                  }
               }
            }
         ]
      }
   },
   "aggs": {
      "colour": {
         "filter": {
            "bool": {
               "must": [
                  {
                     "term": {
                        "colour": "red"
                     }
                  }
               ]
            }
         },
         "aggs": {
            "colour": {
               "terms": {
                  "field": "colour"
               }
            }
         }
      },
      "category": {
         "filter": {
            "bool": {
               "must": [
                  {
                     "term": {
                        "colour": "red"
                     }
                  }
               ]
            }
         },
         "aggs": {
            "category": {
               "terms": {
                  "field": "category"
               }
            }
         }
      },
      "size": {
         "filter": {
            "bool": {
               "must": [
                  {
                     "term": {
                        "colour": "red"
                     }
                  }
               ]
            }
         },
         "aggs": {
            "size": {
               "terms": {
                  "field": "size"
               }
            }
         }
      }
   }
}

Result:

{
   "took": 7,
   "timed_out": false,
   "_shards": {
      "total": 5,
      "successful": 5,
      "skipped": 0,
      "failed": 0
   },
   "hits": {
      "total": 7,
      "max_score": 1,
      "hits": [
         {
            "_index": "test_index",
            "_type": "product",
            "_id": "1234-MA",
            "_score": 1,
            "_source": {
               "category": "Trousers",
               "colour": "red",
               "sku": "1234-MA",
               "size": "M"
            }
         },
         {...},
         {...},
         {...},
         {...},
         {...},
         {...}
      ]
   },
   "aggregations": {
      "colour": {
         "doc_count": 7,
         "colour": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
               {
                  "key": "red",
                  "doc_count": 7
               }
            ]
         }
      },
      "size": {
         "doc_count": 7,
         "size": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
               {
                  "key": "L",
                  "doc_count": 2
               },
               {
                  "key": "M",
                  "doc_count": 2
               },
               {
                  "key": "XL",
                  "doc_count": 2
               },
               {
                  "key": "S",
                  "doc_count": 1
               }
            ]
         }
      },
      "category": {
         "doc_count": 7,
         "category": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
               {
                  "key": "Trousers",
                  "doc_count": 4
               },
               {
                  "key": "T-Shirt",
                  "doc_count": 3
               }
            ]
         }
      }
   }
}

Wie am Beispiel ersichtlich darf ein gesetzter Filter nicht auf die dazugehörige Aggregation angewendet werden. Während alle anderen Filter nun die korrekten Optionen unter dem Kriterium "Farbe: Rot" anzeigen würden (der Kategorie-Filter blendet die Kategorie "Schuhe" nun korrekterweise aus), hat der Farbfilter wieder nur eine Option. Um hier wieder die korrekten Informationen zu erhalten wird schlicht die Einschränkung hinsichtlich der Farbe aus der entsprechenden Aggregation entfernt. Kurz gesagt hängen die Filter und die Aggregationen also sehr eng zusammen und können vereinfacht als eine Einheit betrachtet werden, die sich selbst nicht beeinflussen darf.

Query:

GET /test_index/product/_search
{
   "post_filter": {
      "bool": {
         "must": [
            {
               "term": {
                  "colour": {
                     "value": "red"
                  }
               }
            }
         ]
      }
   },
   "aggs": {
      "colour": {
         "filter": {
         },
         "aggs": {
            "colour": {
               "terms": {
                  "field": "colour"
               }
            }
         }
      },
      "category": {
         "filter": {
            "bool": {
               "must": [
                  {
                     "term": {
                        "colour": "red"
                     }
                  }
               ]
            }
         },
         "aggs": {
            "category": {
               "terms": {
                  "field": "category"
               }
            }
         }
      },
      "size": {
         "filter": {
            "bool": {
               "must": [
                  {
                     "term": {
                        "colour": "red"
                     }
                  }
               ]
            }
         },
         "aggs": {
            "size": {
               "terms": {
                  "field": "size"
               }
            }
         }
      }
   }
}

Result:

{
   "took": 6,
   "timed_out": false,
   "_shards": {
      "total": 5,
      "successful": 5,
      "skipped": 0,
      "failed": 0
   },
   "hits": {
      "total": 7,
      "max_score": 1,
      "hits": [
         {
            "_index": "test_index",
            "_type": "product",
            "_id": "1234-MA",
            "_score": 1,
            "_source": {
               "category": "Trousers",
               "colour": "red",
               "sku": "1234-MA",
               "size": "M"
            }
         },
         {...},
         {...},
         {...},
         {...},
         {...},
         {...}
      ]
   },
   "aggregations": {
      "colour": {
         "doc_count": 31,
         "colour": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
               {
                  "key": "blue",
                  "doc_count": 12
               },
               {
                  "key": "green",
                  "doc_count": 12
               },
               {
                  "key": "red",
                  "doc_count": 7
               }
            ]
         }
      },
      "size": {
         "doc_count": 7,
         "size": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
               {
                  "key": "L",
                  "doc_count": 2
               },
               {
                  "key": "M",
                  "doc_count": 2
               },
               {
                  "key": "XL",
                  "doc_count": 2
               },
               {
                  "key": "S",
                  "doc_count": 1
               }
            ]
         }
      },
      "category": {
         "doc_count": 7,
         "category": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
               {
                  "key": "Trousers",
                  "doc_count": 4
               },
               {
                  "key": "T-Shirt",
                  "doc_count": 3
               }
            ]
         }
      }
   }
}

Diese Einschränkung führt leider noch nicht zur finalen Lösungen. Bis hierher ist es zwar möglich, dynamische Filter zu erzeugen, die sich gegenseitig nachfiltern, ohne die Informationen der eigenen Filtergruppe zu verlieren. Dadurch wurde aber ein Seiteneffekt geschaffen: es ist - bereits mit wenig Aufwand - möglich eine Filterkombination zu setzen, bei der ein ursprünglich gesetzter Filter (bspw. Farbe: Rot) nicht mehr Teil der Ergebnisse der dazugehörigen Aggregation ist.

Query:

GET /test_index/product/_search
{
   "post_filter": {
      "bool": {
         "must": [
            {
               "terms": {
                  "colour": [
                     "red",
                     "green"
                  ]
               }
            },
            {
               "term": {
                  "category": "Shoes"
               }
            }
         ]
      }
   },
   "aggs": {
      "colour": {
         "filter": {
            "bool": {
               "must": [
                  {
                     "term": {
                        "category": "Shoes"
                     }
                  }
               ]
            }
         },
         "aggs": {
            "colour": {
               "terms": {
                  "field": "colour"
               }
            }
         }
      },
      "category": {
         "filter": {
            "bool": {
               "must": [
                  {
                     "terms": {
                        "colour": [
                           "red",
                           "green"
                        ]
                     }
                  }
               ]
            }
         },
         "aggs": {
            "category": {
               "terms": {
                  "field": "category"
               }
            }
         }
      },
      "size": {
         "filter": {
            "bool": {
               "must": [
                  {
                     "terms": {
                        "colour": [
                           "red",
                           "green"
                        ]
                     }
                  },
                  {
                     "term": {
                        "category": "Shoes"
                     }
                  }
               ]
            }
         },
         "aggs": {
            "size": {
               "terms": {
                  "field": "size"
               }
            }
         }
      }
   }
}

Result:

{
   "took": 19,
   "timed_out": false,
   "_shards": {
      "total": 5,
      "successful": 5,
      "skipped": 0,
      "failed": 0
   },
   "hits": {
      "total": 4,
      "max_score": 1,
      "hits": [
         {
            "_index": "test_index",
            "_type": "product",
            "_id": "1234-MC-S",
            "_score": 1,
            "_source": {
               "category": "Shoes",
               "colour": "green",
               "sku": "1234-MC-S",
               "size": "M"
            }
         },
         {...},
         {...},
         {...}
      ]
   },
   "aggregations": {
      "colour": {
         "doc_count": 8,
         "colour": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
               {
                  "key": "blue",
                  "doc_count": 4
               },
               {
                  "key": "green",
                  "doc_count": 4
               }
            ]
         }
      },
      "size": {
         "doc_count": 4,
         "size": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
               {
                  "key": "L",
                  "doc_count": 1
               },
               {
                  "key": "M",
                  "doc_count": 1
               },
               {
                  "key": "S",
                  "doc_count": 1
               },
               {
                  "key": "XL",
                  "doc_count": 1
               }
            ]
         }
      },
      "category": {
         "doc_count": 19,
         "category": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
               {
                  "key": "Trousers",
                  "doc_count": 8
               },
               {
                  "key": "T-Shirt",
                  "doc_count": 7
               },
               {
                  "key": "Shoes",
                  "doc_count": 4
               }
            ]
         }
      }
   }
}

Je nachdem wie das Frontend zur Anzeige der Filter umgesetzt ist kann dieses Verhalten wieder zu einer Situation führen, die entweder die Filter unbenutzbar macht (komplettes neu Laden der Seite erforderlich) oder aus der sich ein Nutzer nicht mehr ohne komplettes Zurücksetzen seiner Filter befreien kann.

Um dem entgegen zu wirken wird eine zusätzliche Einschränkung auf der Filter-Aggregations-Einheit getroffen. Während der Filter die Aggregation nicht nachfiltern darf, muss gleichzeitig dafür gesorgt werden, dass bereits gesetzte Optionen weiterhin Teil der Aggregation bleiben. Dieser Vorgang hat keinen Einfluss auf die gefilterten Produkte, da nur der Filter innerhalb einer Aggregation betroffen ist, aber alle relevanten Werte sind nun Teil der Aggregation-Ergebnisse. Hierfür braucht es abseits der Filter-Implementierung selbst keine weitere Logik im Frontend oder Backend. Die Einschränkung selbst geschieht über ein verschachteltes should -> must / should Konstrukt, wobei das innere must Statement als Gate für die ansonsten gesetzten Filter dient und das should Konstrukt den Zweck hat die ausgewählte Option der eigenen Filtergruppe nicht zu verlieren.

Query:

GET /test_index/product/_search
{
   "post_filter": {
      "bool": {
         "must": [
            {
               "terms": {
                  "colour": [
                     "red",
                     "green"
                  ]
               }
            },
            {
               "term": {
                  "category": "Shoes"
               }
            }
         ]
      }
   },
   "aggs": {
      "colour": {
         "filter": {
            "bool": {
               "should": [
                  {
                     "bool": {
                        "must": [
                           {
                              "term": {
                                 "category": "Shoes"
                              }
                           }
                        ]
                     }
                  },
                  {
                     "bool": {
                        "should": [
                           {
                              "terms": {
                                 "colour": [
                                    "red",
                                    "green"
                                 ]
                              }
                           }
                        ]
                     }
                  }
               ]
            }
         },
         "aggs": {
            "colour": {
               "terms": {
                  "field": "colour"
               }
            }
         }
      },
      "category": {
         "filter": {
            "bool": {
               "should": [
                  {
                     "bool": {
                        "must": [
                           {
                              "terms": {
                                 "colour": [
                                    "red",
                                    "green"
                                 ]
                              }
                           }
                        ]
                     }
                  },
                  {
                     "bool": {
                        "should": {
                           "term": {
                              "category": "Shoes"
                           }
                        }
                     }
                  }
               ]
            }
         },
         "aggs": {
            "category": {
               "terms": {
                  "field": "category"
               }
            }
         }
      },
      "size": {
         "filter": {
            "bool": {
               "must": [
                  {
                     "terms": {
                        "colour": [
                           "red",
                           "green"
                        ]
                     }
                  },
                  {
                     "term": {
                        "category": "Shoes"
                     }
                  }
               ]
            }
         },
         "aggs": {
            "size": {
               "terms": {
                  "field": "size"
               }
            }
         }
      }
   }
}

Result:

{
   "took": 20,
   "timed_out": false,
   "_shards": {
      "total": 5,
      "successful": 5,
      "skipped": 0,
      "failed": 0
   },
   "hits": {
      "total": 4,
      "max_score": 1,
      "hits": [
         {
            "_index": "test_index",
            "_type": "product",
            "_id": "1234-MC-S",
            "_score": 1,
            "_source": {
               "category": "Shoes",
               "colour": "green",
               "sku": "1234-MC-S",
               "size": "M"
            }
         },
         {...},
         {...},
         {...}
      ]
   },
   "aggregations": {
      "colour": {
         "doc_count": 23,
         "colour": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
               {
                  "key": "green",
                  "doc_count": 12
               },
               {
                  "key": "red",
                  "doc_count": 7
               },
               {
                  "key": "blue",
                  "doc_count": 4
               }
            ]
         }
      },
      "size": {
         "doc_count": 4,
         "size": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
               {
                  "key": "L",
                  "doc_count": 1
               },
               {
                  "key": "M",
                  "doc_count": 1
               },
               {
                  "key": "S",
                  "doc_count": 1
               },
               {
                  "key": "XL",
                  "doc_count": 1
               }
            ]
         }
      },
      "category": {
         "doc_count": 23,
         "category": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
               {
                  "key": "Shoes",
                  "doc_count": 8
               },
               {
                  "key": "Trousers",
                  "doc_count": 8
               },
               {
                  "key": "T-Shirt",
                  "doc_count": 7
               }
            ]
         }
      }
   }
}

Vor allem am letzten Beispiel ist auch gut ersichtlich, wie wichtig eine gute und saubere Architektur für den Aufbau der Suchanfrage im Backend ist. Mit einfachen Query-Templates lässt sich die benötigte Dynamik nicht oder nur sehr kompliziert umsetzen - vor allem, wenn mehr als drei Filter und damit auch mehr als drei Aggregationen im Spiel sind.

Fazit

Der hier vorgestellte Ansatz aus der Kombination von Post Filtern und Filtered Aggregations hat für die Implementierung dynamischer Filter einige entscheidende Vorteile. Wie bereits zu Beginn angesprochen kann Logik aus dem Frontend / Backend entfernt werden, die nur dazu dient, die Filteroptionen konsistent zu halten. Noch dazu kann die Aktualisierung der Ergebnisse und der Filteroptionen in nur einer Anfrage an ElasticSearch komplett abgedeckt werden. In Kombination führt das auch zu einer weniger fehleranfälligen Implementierung, da so viel Logik wie möglich über ElasticSearch abgerollt wird, statt sie selbst zu implementieren. In der von uns genutzten Implementierung bestehen Filter aus einem Anfrage und einem Aggregation Teil, über den sie sich selbst repräsentieren und der die oben genannte enge Verbindung zwischen diesen beiden Aspekten abbildet. Hierbei verwenden die meisten der aktuell bei uns genutzten Filter eine generische Implementierung, womit Filter einfach über eine Konfigurationsdatei oder das Magento Backend hinzugefügt werden können. Lediglich für etwas komplexere Fälle muss ein neuer Filter explizit implementiert werden. Mit Hilfe einer Abstraktionsschicht können so alle Anfragen aus den momentan benötigten Filtern automatisch zusammen gebaut werden. Aus Nutzersicht wird das Ziel erreicht, dass jede Änderung einzeln durchgeführt und auch wieder rückgängig gemacht werden kann, es wird ein immer valider Zustand erreicht.

Du willst bei uns mitmachen?
Zu den Stellen