Seitenanfang

ElasticSearch Query

Um ElasticSearch mit den beiden Datenbanken vergleichen zu können, müssen alle drei natürlich die gleiche Aufgabe lösen. Dazu muss eine bestehende SQL-Abfrage in die Query-Sprachen von ElasticSearch und MongoDB übersetzt werden.

elasticsearch_query.jpgIn dem Projekt, dass mir als Praxisbeispiel dient, werden die Abfragen letztendlich vom Nutzer über ein Webinterface zusammengestellt. Verschiedene Filterkriterien können dabei verknüpft werden und die Software macht daraus ein SQL-Query. Für meinen Test werde ich natürlich nicht die ganze Dynamik abbilden, aber möglichst repräsentative Abfragen aus dem Livesystem verwenden.

Eines der einfachsten Beispiele:

SELECT SQL_NO_CACHE DISTINCT SQL_BUFFER_RESULT id, url
FROM MAIN_TABLE WHERE ( (bool1 = 1 AND bool2 = 1) AND state IN (42,69) ) AND 1 AND country = "de"
AND ( url NOT LIKE "%sky.heaven%" )
AND ( url NOT LIKE "%anything.else%" ) AND ( url NOT LIKE "%geocities.com%" ) AND
ORDER BY id DESC

Das Beispiel zeigt schon, dass die freie Konfiguration via Web durchaus ihre Spuren hinterlässt: Zu viele Klammern und unschöne Konstrukte wie "AND 1" sind nur Kosmetik, aber die URL-Matches stellen die schlimmstmögliche Form von LIKE-Abfragen dar. In der Realität kommen häufig zehn und mehr NOT LIKE Abfragen nach obigem Schema vor. Theoretisch ließen sich diese optimieren, in der Praxis und sind solche Änderungen allerdings ziemlich schwierig umzusetzen.

SQL_NO_CACHE habe ich für den Test hinzugefügt, weil die Tabelle unter realen Bedingungen so oft geschrieben wird, dass der Query-Cache dort nie genutzt werden kann.

MongoDB

Ich arbeite seit Jahren mit MongoDB, die Abfrage in ein MongoDB-Query zu übersetzen, ist relativ leicht:

{
bool1 => "1",
bool2 => "1",
'$or' => [
{ state => "42" },
{ state => "69" },
],
country => "de",
domain => { '$not' => qr/(sky\.heaven|anything\.else|geocities\.com)/ },
}

MongoDB arbeitet im Gegensatz zu Perl typisiert. 1, NumberLong(1) und "1" sind drei verschiedene Werte. Beim Import habe ich darauf (natürlich) nicht geachtet und die bool1-, bool2- und state- Spalten wurde als String ("1") importiert. Diesen "Fehler" muss ich jetzt natürlich auch bei der Abfrage einhalten, sonst findet MongoDB keine Treffer.

ElasticSearch

Installation und Datenimport waren bei ElasticSearch sehr einfach und problemlos. Die Abfrage gestaltete sich allerdings wesentlich schwieriger.

Bei einem Fehler in der Abfrage gibt ElasticSearch einen unformartierten JSON-ähnlichen Block zurück, der wenig aussagekräftige Fehlermeldungen von jedem einzelnen Shard enthält. Die Rückgabe erinnert stark an JSON, ist aber nicht wirklich kompatibel, so dass auch der JSON-Formatter nicht sehr viel erreicht (ich habe mehrere ausprobiert).

Meine neue Lieblingsfehlermeldung ist (Auszug):

QueryParsingException[[instance.main] [_na] filter malformed, no field after start_object]

Diese Meldung scheint immer dann zu kommen, wenn beliebige Fehler im JSON-Query vorliegen - aber sie sagt nichts Näheres darüber, in welchem Query-Teil oder bei welchem Schlüsselwort der Parser nicht mehr weiter wusste.

Nach einigen Stunden mit vielen Experimenten und Google-Abfragen hatte ich schließlich folgende Abfrage zusammen:

$dbh_es->search(
index => instance().'.main',
type => 'main',
body => {
query => { filtered => { filter => { bool => {
must => [
{
term => {
bool1 => 1,
},
},{
term => {
bool2 => 1,
},
},{
'or' => [
{ term => { state => 42 }, },
{ term => { state => 69 }, },
],
}
], # Ende von "must"
must_not => {
regexp => {
url => '(sky\.heaven|anything\.else|geocities\.com)',
},
},
} } } }, # Ende von bool, filter, filtered, query
fields => [ "id", "url" ],
'sort' => [ { id => "desc" } ],
}, # Ende von body
);

Von allen drei Abfragen ist die für ElasticSearch auf jeden Fall die komplizierteste. Für die meisten Einzelabfragen habe ich mindesten zwei oder drei Alternativen gesehen und ich hoffe einfach mal dass dieses Query wenigstens halbwegs performant ist. Wenigstens ist es lauffähig und deswegen werde ich es vorerst verwenden.

Im Einzelnen:

  • Zunächst werden der zu verwendende Index (SQL-Logik: "Datenbank") und Type (SQL-Logik: "Tabelle") angegeben. Beide sind optional, denn ElasticSearch kann auch den ganzen Cluster mit allen Daten abfragen oder alle Dokumente in einem Index ohne Beachtung ihres Types. Ich weiß aber ganz genau, welche Dokumente überhaupt nützlich für mich sind, deswegen gebe ich index und type an.
  • Der body enthält das eigentliche Query sowie weitere Parameter.
  • Eigentlich nutzt diese Abfrage gar kein Query, sondern Filter. Ein ElasticSearch-Query berechnet für jeden gefundenen Datensatz eine "Score", also eine Punktewertung. Diese dient am Ende als Sortierkriterium. Bei einer Web-Suchfunktion ist das durchaus sinnvoll (relevanteste Treffer zuerst), aber für meinen Anwendungsfall unnötiger Overhead. Ein Filter trifft nur eine "ja"/"nein" Entscheidung, wie ein mySQL- oder MongoDB-Query und eignet sich besser für die in diesem Projekt notwendigen Suchanfragen.
  • Die Abfrage enthält zwei Arten von Bedingungen: Einige müssen erfüllt sein und einige dürfen nicht erfüllt sein. Mittels bool und must/must_not lässt sich genau dieser Fall abbilden.
  • Die bool-Spalten werden einzeln als term abgefragt.
  • Abgefragt werden nur die tatsächlich benötigten Felder.

Allerdings liefert diese Abfrage nicht das gewünschte Ergebnis: Statt der von mySQL und MongoDB gefundenen 152.050 Datensätze, findet ElasticSearch 183.342 Dokumente. Außerdem werden nur die ersten 10 Ergebnisse tatsächlich zurückgegeben. Für eine Web-Suche ist das durchaus praktikabel, für eine Applikation, die alle verfügbaren Datensätze benötigt, allerdings leider nicht.

$dbh_es->search(
index => instance().'.main',
type => 'main',
size => 1000000,
body => {
query => { filtered => { filter => { bool => {
must => [
{
term => {
bool1 => 1,
},
},{
term => {
bool2 => 1,
},
},{
'or' => [
{ term => { state => 42 }, },
{ term => { state => 69 }, },
],
}
], # Ende von "must"
must_not => {
regexp => {
url => '.*(sky\.heaven|anything\.else|geocities\.com).*',
},
},
} } } }, # Ende von bool, filter, filtered, query
fields => [ "id", "url" ],
'sort' => [ { id => "desc" } ],
}, # Ende von body
);

Mit dieser Version werden beide Probleme beseitigt: ElasticSearch-RegExps matchen immer das komplette Feld, ohne das ".*" am Anfang und Ende wurde einfach kein Datensatz ausgeschlossen. Mit size wird festgelegt, wie viele Datensätze zurückgegeben werden sollen (ähnlich mySQL LIMIT). Bei 300.000 Datensätzen in der Testdatenbank ist eine Million einfach ein Wert für "alles".

Dadurch können allerdings auch Probleme entstehen, weil das komplette Ergebnis im RAM gehalten und verarbeitet werden muss. mySQL hat das gleiche Problem, dieses lässt sich allerdings durch SQL_BUFFER_RESULT abschwächen. Bei MongoDB bin ich nicht sicher, ob der Cursor tatsächlich alle Ergebnisse sofort speichert oder bei Bedarf vom Server nachläd.

Die ersten Ergebnisse sind überraschend: ElasticSearch braucht etwa 4,7 Sekunden für eine einfache Abfrage, mySQL 3,2 Sekunden und MongoDB lässt sich ganze 16,7 Sekunden Zeit. In einem realistischen Testumfeld erwartet ich allerdings eine deutliche Verschiebung, insbesondere weil sich mySQL in Clusterkonfiguration gerne einen ganzen Slave für ein solches Query reserviert während MongoDB parallel arbeitet, wie hoffentlich auch ElasticSearch.

 

Noch keine Kommentare. Schreib was dazu

Schreib was dazu

Die folgenden HTML-Tags sind erlaubt:<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>