Solr Filter-Abfrage: Cache vs. Post-Filter
Werden die Filterabfragen unabhängig voneinander auf den gesamten Datensatz oder nacheinander auf eine schrumpfende Ergebnismenge angewendet? Das kommt darauf an. Lesen Sie weiter, um herauszufinden, warum.
Vor einiger Zeit bin ich dem IRC-Kanal#solr
beigetreten, als ich mitten in einer Unterhaltung über Solrs queryResultCache & filterCache war. Die erste Nachricht, die ich sah, war…
< victori:#solr> Wie auch immer, werden Filterabfragen unabhängig voneinander auf den gesamten Datensatz oder nacheinander auf eine schrumpfende Ergebnismenge angewendet?
Wie bei vielen Dingen im Leben lautet die Antwort „Es kommt darauf an“.
Die Antwort auf diese Frage hängt vor allem davon ab:
- Wenn eine/beide
queryResultCache
/filterCache
für die Erfassung aktiviert sind(das sind sie in den Standardkonfigurationen) - Wenn die einzelnen „Query“-Objekte (
q
& null oder mehrfq
parms) die Zwischenspeicherung aktiviert haben(das ist standardmäßig der Fall)
… aber je nach dem kommen weitere Nuancen ins Spiel:
- Der effektive
cost
Parameter, der für jede fq angegeben wird (Standardwert ist ‚0‘ für die meisten Abfragen) - Der Typ des zugrundeliegenden Abfrageobjekts, das für jedes fq erstellt wird: Ist die
PostFilter
API implementiert?
Als ich einige dieser Nuancen im IRC erklärte (und was sie an dem Verhalten ändern), wurden mir 2 Dinge klar:
- Das wäre ein wirklich toller Blogbeitrag!
- Ich wünschte, es gäbe eine Möglichkeit, zu demonstrieren, wie all dies geschieht, anstatt es nur zu beschreiben.
Das brachte mich zu der Frage, ob es möglich wäre, einen „Tracing Wrapper Query Parser“ zu erstellen, mit dem man Protokollmeldungen abrufen kann, die zeigen, wann genau eine bestimmte Abfrage (oder genauer gesagt der „Scorer“ für diese Abfrage) aufgefordert wurde, jedes Dokument zu bewerten. Mit so etwas könnte man (bei kleinen Datensätzen) mit verschiedenen q
& fq
Parametern und verschiedenen cache
und cost
lokalen Parametern experimentieren und sehen, wie sich die Ausführung verändert. Ich habe einen kurzen Versuch unternommen, diese Art von QParser-Wrapper zu bauen, und habe mir schnell eine Menge Kopfzerbrechen darüber gemacht, wie komplex die allgemeinen Abfrage-, Gewichtungs- und Scorer-APIs auf der unteren Ebene sein können.
Auf der anderen Seite ist die ValueSource (aka Function) API viel einfacher und erleichtert das Zusammenstellen von Funktionen um andere Funktionen herum. Solr macht es außerdem bereits einfach, jede ValueSource als Abfrage über den {!frange}
QParser zu verwenden, der zufälligerweise auch die PostFilter-API unterstützt!
Ein paar Stunden später waren die„TraceValueSource“ und die trace()
Funktionssyntax geboren, und jetzt kann ich sie verwenden, um Sie durch die verschiedenen Nuancen zu führen, wie Solr verschiedene Abfragen und Filter ausführt.
WICHTIGER HINWEIS:
In diesem Artikel gehen wir davon aus, dass die zugrunde liegende Logik, die Lucene zur Ausführung einer einfachen Abfrage verwendet, im Wesentlichen wie folgt aussieht: Schleife über alle docIds im Index (beginnend bei 0), um jedes Dokument mit der Abfrage zu vergleichen. Wenn ein Dokument übereinstimmt, speichern Sie dessen Ergebnis und fahren mit der nächsten docId im Index fort.
Ebenso nehmen wir an, dass Lucene bei der Berechnung der Konjunktion (X ^ Y) von zwei Abfragen im Wesentlichen die gleiche Logik anwendet:
- Schleife über alle docIds im Index (beginnend bei 0), um jedes Dokument mit X zu vergleichen, bis wir ein übereinstimmendes Dokument finden
- wenn dieses Dokument ebenfalls mit Y übereinstimmt, notieren Sie die Punktzahl und fahren mit der nächsten docId fort
- Wenn das Dokument nicht mit Y übereinstimmt, vertauschen Sie X und Y und beginnen den Prozess mit der nächsten docId von vorne
Dies sind beides extreme Vereinfachungen der Art und Weise, wie die meisten Abfragen tatsächlich ausgeführt werden. Viele Abfragen, die auf Term & Points basieren, sind viel stärker darauf optimiert, in der Liste der Dokumente auf der Grundlage der Term/Points-Metadaten „weiterzuspringen“.
{!frange}
Abfragen und die Funktion trace()
Beginnen wir mit einer ganz einfachen Aufwärmübung, um Sie mit dem {!frange}
QParser und der trace()
Funktion, die ich hinzugefügt habe, beginnend mit einigen trivialen Beispieldaten…
$ bin/solr -e schemaless -noprompt ... curl -H 'Content-Type: application/json' 'http://localhost:8983/solr/gettingstarted/update?commit=true' --data-binary ' [{"id": "A", "foo_i": 42, "bar_i": 99}, {"id": "B", "foo_i": -42, "bar_i": 75}, {"id": "C", "foo_i": -7, "bar_i": 1000}, {"id": "D", "foo_i": 7, "bar_i": 50}]' ... tail -f example/schemaless/logs/solr.log ...
Den größten Teil dieses Blogs werde ich Abfragen gegen diese 4 Dokumente ausführen und Ihnen dabei zeigen:
- Die vollständige URL der Anfrage
- Schlüssel für url-dekodierte Request-Parameter in der Anfrage zum leichteren Lesen
- Alle Protokollmeldungen, die als Ergebnis der Anfrage an
solr.log
geschrieben werden
Mit dem Parser {!frange}
kann der Benutzer eine beliebige Funktion (auch bekannt als ValueSource) angeben, die in eine Abfrage verpackt wird, die nur dann mit Dokumenten übereinstimmt, wenn die Ergebnisse dieser Funktion in einen bestimmten Bereich fallen. Ein Beispiel: Bei den 4 Beispieldokumenten, die wir oben indiziert haben, stimmt die folgende Abfrage nicht mit dem Dokument ‚A‘ oder ‚C‘ überein, weil die Summe der Felder foo_i + bar_i
(42 + 100 = 142
bzw. -7 + 1000 = 993
) nicht zwischen den unteren und oberen Bereichsgrenzen der Abfrage (0 <= sum(foo_i,bar_i) <= 100
) liegt…
http://localhost:8983/solr/gettingstarted/select?omitHeader=true&fl=id&q={!frange%20l=0%20u=100}sum%28foo_i,bar_i%29 // q = {!frange l=0 u=100}sum(foo_i,bar_i) { "response":{"numFound":2,"start":0,"docs":[ { "id":"B"}, { "id":"D"}] }} INFO - 2017-11-14 20:27:06.897; [ x:gettingstarted] org.apache.solr.core.SolrCore; [gettingstarted] webapp=/solr path=/select params={q={!frange+l%3D0+u%3D100}sum(foo_i,bar_i)&omitHeader=true&fl=id} hits=2 status=0 QTime=29
Unter der Haube macht der Scorer für die FunctionRangeQuery, die von diesem Parser erzeugt wird, eine Schleife über jedes Dokument im Index und fragt die ValueSource, ob sie für dieses Dokument „existiert“ (d.h. ob die zugrundeliegenden Felder existieren) und wenn ja, fragt er nach dem berechneten Wert für dieses Dokument.
Im Allgemeinen implementiert die Funktion trace()
, die wir verwenden werden, die ValueSource-API so, dass sie jedes Mal, wenn sie nach dem „Wert“ eines Dokuments gefragt wird, an eine andere ValueSource delegiert und eine Meldung über die Eingabe (Dokument-ID) und das Ergebnis protokolliert – zusammen mit einer konfigurierbaren Bezeichnung.
Wenn wir die Funktion, die wir in unserer vorherigen Abfrage verwendet haben, in trace(simple_sum,sum(foo_i,bar_i))
ändern und erneut ausführen, können wir die einzelnen Methoden sehen, die in diesem Prozess für die ValueSource „sum“ aufgerufen werden (zusammen mit der internen ID + uniqueKey des Dokuments und dem von uns gewählten Label „simple_sum“) sowie das Ergebnis der umschlossenen Funktion …
http://localhost:8983/solr/gettingstarted/select?omitHeader=true&fl=id&q={!frange%20l=0%20u=100}trace%28simple_sum,sum%28foo_i,bar_i%29%29 // q = {!frange l=0 u=100}trace(simple_sum,sum(foo_i,bar_i)) TraceValueSource$TracerValues; simple_sum: exists(#0: "A") -> true TraceValueSource$TracerValues; simple_sum: floatVal(#0: "A") -> 141.0 TraceValueSource$TracerValues; simple_sum: exists(#1: "B") -> true TraceValueSource$TracerValues; simple_sum: floatVal(#1: "B") -> 33.0 TraceValueSource$TracerValues; simple_sum: exists(#2: "C") -> true TraceValueSource$TracerValues; simple_sum: floatVal(#2: "C") -> 993.0 TraceValueSource$TracerValues; simple_sum: exists(#3: "D") -> true TraceValueSource$TracerValues; simple_sum: floatVal(#3: "D") -> 57.0 SolrCore; [gettingstarted] webapp=/solr path=/select params={q={!frange+l%3D0+u%3D100}trace(simple_sum,sum(foo_i,bar_i))&omitHeader=true&fl=id} hits=2 status=0 QTime=6
Da wir die _default
Solr-Konfigurationen verwenden, wurde diese Abfrage im queryResultCache
zwischengespeichert. Wenn wir sie erneut ausführen, werden keine neuen „Tracing“-Informationen protokolliert, da Solr die ValueSource nicht gegen jedes der Dokumente im Index auswerten muss, um die Anfrage zu beantworten…
http://localhost:8983/solr/gettingstarted/select?omitHeader=true&fl=id&q={!frange%20l=0%20u=100}trace%28simple_sum,sum%28foo_i,bar_i%29%29 // q = {!frange l=0 u=100}trace(simple_sum,sum(foo_i,bar_i)) SolrCore; [gettingstarted] webapp=/solr path=/select params={q={!frange+l%3D0+u%3D100}trace(simple_sum,sum(foo_i,bar_i))&omitHeader=true&fl=id} hits=2 status=0 QTime=0
Normale fq
Verarbeitung
Lassen Sie uns nun mehrere {!frange}
& trace()
Kombinationen, um zu sehen, was passiert, wenn wir einige Filterabfragen in unserer Anfrage haben…
http://localhost:8983/solr/gettingstarted/select?omitHeader=true&fl=id&q={!frange%20l=0%20u=100}trace%28simple_sum,sum%28foo_i,bar_i%29%29&fq={!frange%20l=0}trace%28pos_foo,foo_i%29&fq={!frange%20u=90}trace%28low_bar,bar_i%29 // q = {!frange l=0 u=100}trace(simple_sum,sum(foo_i,bar_i)) // fq = {!frange l=0}trace(pos_foo,foo_i) // fq = {!frange u=90}trace(low_bar,bar_i) TraceValueSource$TracerValues; pos_foo: exists(#0: "A") -> true TraceValueSource$TracerValues; pos_foo: floatVal(#0: "A") -> 42.0 TraceValueSource$TracerValues; pos_foo: exists(#1: "B") -> true TraceValueSource$TracerValues; pos_foo: floatVal(#1: "B") -> -42.0 TraceValueSource$TracerValues; pos_foo: exists(#2: "C") -> true TraceValueSource$TracerValues; pos_foo: floatVal(#2: "C") -> -7.0 TraceValueSource$TracerValues; pos_foo: exists(#3: "D") -> true TraceValueSource$TracerValues; pos_foo: floatVal(#3: "D") -> 7.0 TraceValueSource$TracerValues; low_bar: exists(#0: "A") -> true TraceValueSource$TracerValues; low_bar: floatVal(#0: "A") -> 99.0 TraceValueSource$TracerValues; low_bar: exists(#1: "B") -> true TraceValueSource$TracerValues; low_bar: floatVal(#1: "B") -> 75.0 TraceValueSource$TracerValues; low_bar: exists(#2: "C") -> true TraceValueSource$TracerValues; low_bar: floatVal(#2: "C") -> 1000.0 TraceValueSource$TracerValues; low_bar: exists(#3: "D") -> true TraceValueSource$TracerValues; low_bar: floatVal(#3: "D") -> 50.0 TraceValueSource$TracerValues; simple_sum: exists(#3: "D") -> true TraceValueSource$TracerValues; simple_sum: floatVal(#3: "D") -> 57.0 SolrCore; [gettingstarted] webapp=/solr path=/select params={q={!frange+l%3D0+u%3D100}trace(simple_sum,sum(foo_i,bar_i))&omitHeader=true&fl=id&fq={!frange+l%3D0}trace(pos_foo,foo_i)&fq={!frange+u%3D90}trace(low_bar,bar_i)} hits=1 status=0 QTime=23
Es gibt hier eine Menge Informationen zu beachten, also lassen Sie uns diese in der Reihenfolge der Protokollmeldungen besprechen…
- Um die einzelnen
fq
Abfragen für eine größtmögliche Wiederverwendung zwischenzuspeichern, führt Solr jedefq
Abfrage unabhängig gegen den gesamten Index aus:- Zunächst wird die Funktion „pos_foo“ auf alle 4 Dokumente angewendet, um festzustellen, ob
0 <= foo_i
- wird dieses resultierende DocSet in die
filterCache
für diesefq
- wird dieses resultierende DocSet in die
- dann wird die Funktion „low_bar“ auf alle 4 Dokumente angewendet, um zu sehen, ob
bar_i <= 90
- wird dieses resultierende DocSet in die
filterCache
für diesefq
- wird dieses resultierende DocSet in die
- Zunächst wird die Funktion „pos_foo“ auf alle 4 Dokumente angewendet, um festzustellen, ob
- Die Hauptabfrage (simple_sum) ist nun bereit, ausgeführt zu werden:
- Anstatt die Hauptabfrage gegen alle Dokumente im Index auszuführen, muss sie nur gegen die Schnittmenge der DocSets aus jedem der einzelnen (zwischengespeicherten) Filter ausgeführt werden
- Da das Dokument ‚A‘ nicht mit der „low_bar“
fq
übereinstimmt, wird die Funktion „simple_sum“ nie aufgefordert, es als mögliche Übereinstimmung für die Gesamtanfrage zu bewerten - Gleichermaßen: da ‚B‘ nicht mit „pos_foo“
fq
übereinstimmt, wird es auch nicht berücksichtigt. - Gleichermaßen: Da ‚C‘ nicht mit der „low_bar“
fq
übereinstimmt, wird es ebenfalls nicht berücksichtigt. - Nur das Dokument „D“ entsprach beiden
fq
Filtern, also wird es mit der Hauptabfrage abgeglichen – und es ist ein Treffer, also haben wirhits=1
Bei zukünftigen Anfragen können die zwischengespeicherten Filterabfragen immer noch wieder verwendet werden, um die Menge der Dokumente einzuschränken, die die Hauptabfrage prüfen muss, selbst wenn sich der Haupt-Q-Parameter ändert und möglicherweise mit einer anderen Gruppe von Werten/Dokumenten übereinstimmt… wie wir in dieser nächsten Anfrage mit denselben fq-Parametern sehen können.
http://localhost:8983/solr/gettingstarted/select?omitHeader=true&fl=id&q={!frange%20u=999}trace%28max_foo,foo_i%29&fq={!frange%20l=0}trace%28pos_foo,foo_i%29&fq={!frange%20u=90}trace%28low_bar,bar_i%29 // q = {!frange u=999}trace(max_foo,foo_i) // fq = {!frange l=0}trace(pos_foo,foo_i) // fq = {!frange u=90}trace(low_bar,bar_i) TraceValueSource$TracerValues; max_foo: exists(#3: "D") -> true TraceValueSource$TracerValues; max_foo: floatVal(#3: "D") -> 7.0 SolrCore; [gettingstarted] webapp=/solr path=/select params={q={!frange+u%3D999}trace(max_foo,foo_i)&omitHeader=true&fl=id&fq={!frange+l%3D0}trace(pos_foo,foo_i)&fq={!frange+u%3D90}trace(low_bar,bar_i)} hits=1 status=0 QTime=1
Nicht gecachte Filter
Lassen Sie uns nun überlegen, was passiert, wenn wir 2 optionale lokale Parameter zu unseren Filterabfragen hinzufügen:
cache=false - Tells Solr that we don't need/want this filter to be cached independently for re-use.
- Dadurch kann Solr diese Filter gleichzeitig mit der Verarbeitung der Hauptabfrage auswerten
cost=X
– Gibt einen ganzzahligen „Hinweis“ für Solr an, wie teuer die Ausführung dieses Filters ist.- Solr bietet eine Sonderbehandlung für einige Arten von Filtern, wenn
100 <= cost
(mehr dazu später) - Standardmäßig geht Solr davon aus, dass die meisten Filter den Standardwert
cost=0
haben (aber ab Solr 7.2 werden{!frange}
Abfragen standardmäßig aufcost=100
gesetzt). - Für diese Beispiele geben wir explizit einen Preis für jedes
fq
an, so dass: 0 < kosten < 100.
- Solr bietet eine Sonderbehandlung für einige Arten von Filtern, wenn
http://localhost:8983/solr/gettingstarted/select?omitHeader=true&fl=id&q={!frange%20l=0%20u=100}trace%28simple_sum,sum%28foo_i,bar_i%29%29&fq={!frange%20cache=false%20cost=50%20l=0}trace%28pos_foo_nocache_50,foo_i%29&fq={!frange%20cache=false%20cost=25%20u=100}trace%28low_bar_nocache_25,bar_i%29 // q = {!frange l=0 u=100}trace(simple_sum,sum(foo_i,bar_i)) // fq = {!frange cache=false cost=50 l=0}trace(pos_foo_nocache_50,foo_i) // fq = {!frange cache=false cost=25 u=100}trace(low_bar_nocache_25,bar_i) TraceValueSource$TracerValues; low_bar_nocache_25: exists(#0: "A") -> true TraceValueSource$TracerValues; low_bar_nocache_25: floatVal(#0: "A") -> 99.0 TraceValueSource$TracerValues; pos_foo_nocache_50: exists(#0: "A") -> true TraceValueSource$TracerValues; pos_foo_nocache_50: floatVal(#0: "A") -> 42.0 TraceValueSource$TracerValues; simple_sum: exists(#0: "A") -> true TraceValueSource$TracerValues; simple_sum: floatVal(#0: "A") -> 141.0 TraceValueSource$TracerValues; low_bar_nocache_25: exists(#1: "B") -> true TraceValueSource$TracerValues; low_bar_nocache_25: floatVal(#1: "B") -> 75.0 TraceValueSource$TracerValues; pos_foo_nocache_50: exists(#1: "B") -> true TraceValueSource$TracerValues; pos_foo_nocache_50: floatVal(#1: "B") -> -42.0 TraceValueSource$TracerValues; pos_foo_nocache_50: exists(#2: "C") -> true TraceValueSource$TracerValues; pos_foo_nocache_50: floatVal(#2: "C") -> -7.0 TraceValueSource$TracerValues; pos_foo_nocache_50: exists(#3: "D") -> true TraceValueSource$TracerValues; pos_foo_nocache_50: floatVal(#3: "D") -> 7.0 TraceValueSource$TracerValues; low_bar_nocache_25: exists(#3: "D") -> true TraceValueSource$TracerValues; low_bar_nocache_25: floatVal(#3: "D") -> 50.0 TraceValueSource$TracerValues; simple_sum: exists(#3: "D") -> true TraceValueSource$TracerValues; simple_sum: floatVal(#3: "D") -> 57.0 SolrCore; [gettingstarted] webapp=/solr path=/select params={q={!frange+l%3D0+u%3D100}trace(simple_sum,sum(foo_i,bar_i))&omitHeader=true&fl=id&fq={!frange+cache%3Dfalse+cost%3D50+l%3D0}trace(pos_foo_nocache_50,foo_i)&fq={!frange+cache%3Dfalse+cost%3D25+u%3D100}trace(low_bar_nocache_25,bar_i)} hits=1 status=0 QTime=8
Lassen Sie uns dies noch einmal der Reihe nach durchgehen und darüber sprechen, was an jedem Punkt passiert:
- Da die Filter nicht zwischengespeichert werden, kann Solr sie mit der Hauptabfrage
q
kombinieren und alle drei in einem Durchgang über den Index ausführen - Die Filter werden nach ihrer
cost
sortiert und der Filter mit den niedrigsten Kosten (low_bar_nocache_25) wird aufgefordert, das „erste“ Dokument zu finden, das er findet:- Dokument „A“ ist ein Treffer für low_bar_nocache_25 (bar_i <= 100), also wird der nächste Filter konsultiert…
- Dokument „A“ ist auch ein Treffer für pos_foo_nocache_50 (0 <= foo_i), so dass alle Filter übereinstimmen – die Hauptabfrage kann konsultiert werden…
- Dokument „A“ ist keine Übereinstimmung mit der Hauptabfrage (simple_sum)
- Die Filter werden dann aufgefordert, ihre „nächste“ Übereinstimmung nach „A“ zu finden, beginnend mit dem Filter mit den niedrigsten Kosten: low_bar_nocache_25
- Dokument „B“ ist ein Treffer für ‚low_bar_nocache_25‘, also wird der nächste Filter konsultiert…
- Dokument „B“ ist keine Übereinstimmung für den Filter ‚pos_foo_nocache_50‘, so dass dieser Filter so lange prüft, bis er die „nächste“ Übereinstimmung (nach „B“) findet.
- Das Dokument „C“ ist kein Treffer für den Filter ‚pos_foo_nocache_50‘, so dass dieser Filter weiter prüft, bis er den „nächsten“ Treffer findet (nach „C“)
- Dokument „D“ ist die „nächste“ Übereinstimmung für den Filter ‚pos_foo_nocache_50‘, so dass die übrigen Filter für dieses Dokument konsultiert werden…
- Das Dokument „D“ ist auch ein Treffer für den Filter ‚low_bar_nocache_25‘, so dass alle Filter übereinstimmen – die Hauptabfrage kann erneut konsultiert werden.
- Dokument „D“ ist ein Treffer für die Hauptabfrage (simple_sum), und wir haben unseren ersten (und einzigen) Treffer für die Anfrage
Hier gibt es zwei sehr wichtige Dinge zu beachten, die vielleicht nicht sofort offensichtlich sind:
- Nur weil die einzelnen
fq
Parameter aufcache=false
verweisen, heißt das nicht, dass nichts von ihren Ergebnissen zwischengespeichert wird. Die Ergebnisse der Hauptabfrageq
in Verbindung mit den (nicht zwischengespeicherten) Filtern können immer noch in derqueryResultCache
landen, wie Sie sehen können, wenn die gleiche Abfrage erneut ausgeführt wird…http://localhost:8983/solr/gettingstarted/select?omitHeader=true&fl=id&q={!frange%20l=0%20u=100}trace%28simple_sum,sum%28foo_i,bar_i%29%29&fq={!frange%20cache=false%20cost=50%20l=0}trace%28pos_foo_nocache_50,foo_i%29&fq={!frange%20cache=false%20cost=25%20u=100}trace%28low_bar_nocache_25,bar_i%29 // q = {!frange l=0 u=100}trace(simple_sum,sum(foo_i,bar_i)) // fq = {!frange cache=false cost=50 l=0}trace(pos_foo_nocache_50,foo_i) // fq = {!frange cache=false cost=25 u=100}trace(low_bar_nocache_25,bar_i) SolrCore; [gettingstarted] webapp=/solr path=/select params={q={!frange+l%3D0+u%3D100}trace(simple_sum,sum(foo_i,bar_i))&omitHeader=true&fl=id&fq={!frange+cache%3Dfalse+cost%3D50+l%3D0}trace(pos_foo_nocache_50,foo_i)&fq={!frange+cache%3Dfalse+cost%3D25+u%3D100}trace(low_bar_nocache_25,bar_i)} hits=1 status=0 QTime=1
…erhalten wir keine
trace()
Meldungen, da die gesamte „q + fqs + sort + pagination“ Kombination in derqueryResultCache
stand.(HINWEIS: Genau wie bei der Verwendung von
cache=false
in den lokalen Parametern derfq
Parameter verhindern, dass sie in derfilterCache
, mit der Angabecache=false
auf derq
Parameter kann auch verhindern, dass ein Eintrag für diese Abfrage in derqueryResultCache
falls gewünscht) - Der relative
cost
Wert eines jeden Filters bestimmt nicht die Reihenfolge, in der er für jedes Dokument ausgewertet wird.- Im obigen Beispiel stellte die höhere
cost=50
für den Filter ‚pos_foo_nocache_50‘ nicht sicher, dass er gegen weniger Dokumente ausgeführt wurde als der kostengünstigere Filter ‚low_bar_nocache_25‘.- Dokument „C“ wurde mit dem (teureren) Filter ‚pos_foo_nocache_50‘ geprüft (und ausgeschlossen), ohne dass das Dokument mit dem kostengünstigeren Filter ‚low_bar_nocache_25‘ geprüft wurde.
- Die
cost
gibt nur an, in welcher Reihenfolge die einzelnen Filter konsultiert werden sollten, um das „nächste“ passende Dokument nach jeder zuvor gefundenen Übereinstimmung mit der gesamten Anfrage zu finden.- Relative Kostenwerte stellen sicher, dass ein Filter mit höheren Kosten nicht aufgefordert wird, die „nächste“ Übereinstimmung mit einem Dokument zu suchen, das ein Filter mit niedrigeren Kosten bereits definitiv als Nicht-Übereinstimmung ausgeschlossen hat.
Vergleichen Sie die obigen Ergebnisse mit dem folgenden Beispiel, bei dem die gleichen Funktionen neue ‚Kosten‘-Werte verwenden:
http://localhost:8983/solr/gettingstarted/select?omitHeader=true&fl=id&q={!frange%20l=0%20u=100}trace%28simple_sum,sum%28foo_i,bar_i%29%29&fq={!frange%20cache=false%20cost=10%20l=0}trace%28pos_foo_nocache_10,foo_i%29&fq={!frange%20cache=false%20cost=80%20u=100}trace%28low_bar_nocache_80,bar_i%29 // q = {!frange l=0 u=100}trace(simple_sum,sum(foo_i,bar_i)) // fq = {!frange cache=false cost=10 l=0}trace(pos_foo_nocache_10,foo_i) // fq = {!frange cache=false cost=80 u=100}trace(low_bar_nocache_80,bar_i) TraceValueSource$TracerValues; pos_foo_nocache_10: exists(#0: "A") -> true TraceValueSource$TracerValues; pos_foo_nocache_10: floatVal(#0: "A") -> 42.0 TraceValueSource$TracerValues; low_bar_nocache_80: exists(#0: "A") -> true TraceValueSource$TracerValues; low_bar_nocache_80: floatVal(#0: "A") -> 99.0 TraceValueSource$TracerValues; simple_sum: exists(#0: "A") -> true TraceValueSource$TracerValues; simple_sum: floatVal(#0: "A") -> 141.0 TraceValueSource$TracerValues; pos_foo_nocache_10: exists(#1: "B") -> true TraceValueSource$TracerValues; pos_foo_nocache_10: floatVal(#1: "B") -> -42.0 TraceValueSource$TracerValues; pos_foo_nocache_10: exists(#2: "C") -> true TraceValueSource$TracerValues; pos_foo_nocache_10: floatVal(#2: "C") -> -7.0 TraceValueSource$TracerValues; pos_foo_nocache_10: exists(#3: "D") -> true TraceValueSource$TracerValues; pos_foo_nocache_10: floatVal(#3: "D") -> 7.0 TraceValueSource$TracerValues; low_bar_nocache_80: exists(#3: "D") -> true TraceValueSource$TracerValues; low_bar_nocache_80: floatVal(#3: "D") -> 50.0 TraceValueSource$TracerValues; simple_sum: exists(#3: "D") -> true TraceValueSource$TracerValues; simple_sum: floatVal(#3: "D") -> 57.0 SolrCore; [gettingstarted] webapp=/solr path=/select params={q={!frange+l%3D0+u%3D100}trace(simple_sum,sum(foo_i,bar_i))&omitHeader=true&fl=id&fq={!frange+cache%3Dfalse+cost%3D10+l%3D0}trace(pos_foo_nocache_10,foo_i)&fq={!frange+cache%3Dfalse+cost%3D80+u%3D100}trace(low_bar_nocache_80,bar_i)} hits=1 status=0 QTime=3
Der Gesamtablauf ist dem des letzten Beispiels ziemlich ähnlich:
- Da die Filter nicht zwischengespeichert werden, kann Solr sie mit der Hauptabfrage kombinieren und alle drei in einem Durchgang über den Index ausführen
- Die Filter werden nach ihrem
cost
sortiert und der Filter mit den niedrigsten Kosten (pos_foo_nocache_10) wird aufgefordert, das „erste“ Dokument zu finden, das er findet:- Dokument „A“ ist ein Treffer für pos_foo_nocache_10 (0 <= foo) – also wird der nächste Filter konsultiert…
- Dokument „A“ ist ein Treffer für low_bar_nocache_80 (bar <= 100) – also stimmen alle Filter überein, und die Hauptabfrage kann aufgerufen werden…
- Dokument „A“ ist keine Übereinstimmung mit der Hauptabfrage (simple_sum)
- Die Filter werden dann aufgefordert, ihre „nächste“ Übereinstimmung nach „A“ zu finden, beginnend mit dem Filter mit den niedrigsten Kosten: (pos_foo_nocache_10)
- Dokument „B“ ist keine Übereinstimmung für den Filter ‚pos_foo_nocache_10‘, so dass dieser Filter so lange prüft, bis er die „nächste“ Übereinstimmung (nach „B“) findet.
- Das Dokument „C“ ist kein Treffer für den Filter ‚pos_foo_nocache_10‘, so dass dieser Filter weiter prüft, bis er den „nächsten“ Treffer findet (nach „C“)
- Dokument „D“ ist die „nächste“ Übereinstimmung für den Filter ‚pos_foo_nocache_10‘, so dass die übrigen Filter für dieses Dokument konsultiert werden…
- Das Dokument „D“ ist auch ein Treffer für den Filter ‚low_bar_nocache_80‘, so dass alle Filter übereinstimmen – die Hauptabfrage kann erneut konsultiert werden.
- Dokument „D“ ist ein Treffer für die Hauptabfrage, und wir haben unseren ersten (und einzigen) Treffer für die Anfrage
- Im obigen Beispiel stellte die höhere
Das Wichtigste an diesen Beispielen ist, dass, obwohl wir Solr einen „Hinweis“ auf die relativen Kosten dieser Filter gegeben haben, die zugrunde liegenden Scoring-APIs in Lucene davon abhängen, dass jede Abfrage in der Lage ist, die „nächste Übereinstimmung nach doc#X“ zu finden. Sobald ein „kostengünstiger“ Filter mit dieser Aufgabe betraut wurde, wird das von ihm identifizierte Dokument als Eingabe verwendet, wenn ein „kostspieligerer“ Filter aufgefordert wird, die „nächste Übereinstimmung“ zu finden. Wenn der kostspieligere Filter nur sehr wenige Dokumente findet, muss er möglicherweise insgesamt mehr Dokumente im Segment „durchsuchen“ als der kostengünstigere Filter.
Post-Filterung
Es gibt eine kleine Handvoll von Abfragen in Solr (vor allem {!frange}
und {!collapse}
), die – zusätzlich zur Unterstützung der normalen Lucene-APIs für iteratives Scoring – auch eine spezielle „PostFilter“-API implementieren.
Wenn eine Solr-Anfrage einen Filter enthält, der cache=false
ist und eine cost >= 100
hat, prüft Solr, ob die zugrunde liegende Query-Implementierung die PostFilter-API unterstützt. Wenn dies der Fall ist, verwendet Solr automatisch diese API, um sicherzustellen, dass diese Post-Filter erst nach einem potenziell passenden Dokument konsultiert werden:
- Es wurde bereits bestätigt, dass es mit allen regulären (nicht-post)
fq
Filtern übereinstimmt. - Es wurde bereits bestätigt, dass es sich um eine Übereinstimmung mit der Hauptabfrage
q
handelt. - Es wurde bereits bestätigt, dass er mit allen preiswerteren Nachfiltern mithalten kann.
(Diese allgemeine Benutzererfahrung (und die besondere Behandlung von cost >= 100
, und nicht irgendeine Art von spezieller postFilter=true
Syntax) konzentriert sich darauf, dass die Benutzer angeben können, wie „teuer“ sie die verschiedenen Filter erwarten, während Solr sich darum kümmert, wie diese verschiedenen teuren Filter am besten behandelt werden, je nachdem, wie sie intern implementiert sind, ohne dass der Benutzer im Voraus wissen muss: „Unterstützt diese Abfrage Post-Filterung?“)
Für fortgeschrittene Solr-Benutzer, die benutzerdefinierte Filter-Plugins schreiben möchten (insbesondere sicherheitsrelevante Filter, die möglicherweise externe Datenquellen konsultieren oder komplexe Regeln erzwingen müssen), kann die PostFilter-API eine großartige Möglichkeit sein, um sicherzustellen, dass teure Operationen nur dann ausgeführt werden, wenn sie absolut notwendig sind.
Betrachten wir noch einmal unser früheres Beispiel für nicht zwischengespeicherte Filterabfragen, aber dieses Mal verwenden wir cost=200
für die Filterbedingung bar < 100
, so dass sie als Postfilter verwendet wird…
http://localhost:8983/solr/gettingstarted/select?omitHeader=true&fl=id&q={!frange%20l=0%20u=100}trace%28simple_sum,sum%28foo_i,bar_i%29%29&fq={!frange%20cache=false%20cost=50%20l=0}trace%28pos_foo_nocache_50,foo_i%29&fq={!frange%20cache=false%20cost=200%20u=100}trace%28low_bar_postfq_200,bar_i%29 // q = {!frange l=0 u=100}trace(simple_sum,sum(foo_i,bar_i)) // fq = {!frange cache=false cost=50 l=0}trace(pos_foo_nocache_50,foo_i) // fq = {!frange cache=false cost=200 u=100}trace(low_bar_postfq_200,bar_i) TraceValueSource$TracerValues; pos_foo_nocache_50: exists(#0: "A") -> true TraceValueSource$TracerValues; pos_foo_nocache_50: floatVal(#0: "A") -> 42.0 TraceValueSource$TracerValues; simple_sum: exists(#0: "A") -> true TraceValueSource$TracerValues; simple_sum: floatVal(#0: "A") -> 141.0 TraceValueSource$TracerValues; pos_foo_nocache_50: exists(#1: "B") -> true TraceValueSource$TracerValues; pos_foo_nocache_50: floatVal(#1: "B") -> -42.0 TraceValueSource$TracerValues; pos_foo_nocache_50: exists(#2: "C") -> true TraceValueSource$TracerValues; pos_foo_nocache_50: floatVal(#2: "C") -> -7.0 TraceValueSource$TracerValues; pos_foo_nocache_50: exists(#3: "D") -> true TraceValueSource$TracerValues; pos_foo_nocache_50: floatVal(#3: "D") -> 7.0 TraceValueSource$TracerValues; simple_sum: exists(#3: "D") -> true TraceValueSource$TracerValues; simple_sum: floatVal(#3: "D") -> 57.0 TraceValueSource$TracerValues; low_bar_postfq_200: exists(#3: "D") -> true TraceValueSource$TracerValues; low_bar_postfq_200: floatVal(#3: "D") -> 50.0 SolrCore; [gettingstarted] webapp=/solr path=/select params={q={!frange+l%3D0+u%3D100}trace(simple_sum,sum(foo_i,bar_i))&omitHeader=true&fl=id&fq={!frange+cache%3Dfalse+cost%3D50+l%3D0}trace(pos_foo_nocache_50,foo_i)&fq={!frange+cache%3Dfalse+cost%3D200+u%3D100}trace(low_bar_postfq_200,bar_i)} hits=1 status=0 QTime=4
Hier sehen wir einen ganz anderen Ausführungsablauf als in den vorherigen Beispielen:
- Der einzige nicht zwischengespeicherte (nicht gepostete) Filter (pos_foo_nocache_50) wird zunächst konsultiert, um das „erste“ Dokument zu finden, auf das er passt
- Dokument „A“ ist ein Treffer für pos_foo_nocache_50 (0 <= foo) – alle „regulären“ Filter stimmen also überein, und die Hauptabfrage kann konsultiert werden…
- Dokument „A“ ist keine Übereinstimmung mit der Hauptabfrage (simple_sum), also wird „A“ nicht mehr berücksichtigt.
- Der Post-Filter (low_bar_postfq_200) wird bei „A“ nie konsultiert.
- Der einsame Nicht-Post-Filter wird erneut aufgefordert, die „nächste“ Übereinstimmung nach „A“ zu finden.
- Dokument „B“ ist keine Übereinstimmung für den Filter ‚pos_foo_nocache_50‘, so dass dieser Filter so lange prüft, bis er die „nächste“ Übereinstimmung (nach „B“) findet.
- Das Dokument „C“ ist kein Treffer für den Filter ‚pos_foo_nocache_50‘, so dass dieser Filter weiter prüft, bis er den „nächsten“ Treffer findet (nach „C“)
- Dokument „D“ ist die „nächste“ Übereinstimmung für den Filter ‚pos_foo_nocache_50‘ – da es keine anderen „regulären“ Filter gibt, wird die Hauptabfrage erneut konsultiert
- Dokument „D“ ist auch ein Treffer für die Hauptabfrage
- Nachdem alle anderen Bedingungen erfüllt sind, wird das Dokument „D“ mit dem Post-Filter (low_bar_postfq_200) abgeglichen – da es übereinstimmt, haben wir unseren ersten (und einzigen) Treffer für die Anfrage
In diesen Beispielen waren die Funktionen, die wir in unseren Filtern verwendet haben, relativ einfach. Wenn Sie jedoch nach mehreren komplexen mathematischen Funktionen über viele Felder hinweg filtern wollten, können Sie sehen, wie die Angabe von „Kosten“ im Verhältnis zur Komplexität der Funktion von Vorteil sein könnte, um sicherzustellen, dass die „einfacheren“ Funktionen zuerst geprüft werden.
Fazit…
Ich hoffe, diese Beispiele sind hilfreich für alle, die sich ein Bild davon machen wollen, wie/warum sich Filterabfragen in verschiedenen Situationen verhalten, und insbesondere wie {!frange}
Abfragen funktionieren, so dass Sie einige der Nachteile der Optimierung der Einstellungen berücksichtigen können. cache
und cost
Parameter Ihrer verschiedenen Filter.
Selbst für mich mit ~12 Jahren Solr-Erfahrung wurde mir beim Durchgehen dieser Beispiele klar, dass ich eine falsche Vorstellung davon hatte, wie/wann FunctionRangeQuery
optimiert werden könnte (was letztendlich zu SOLR-11641 führte, das {!frange cache=false ...}
in zukünftigen Solr-Versionen standardmäßig viel schneller machen sollte).