Streaming-Ausdrücke in SolrJ
Hier bei Lucidworks hatten wir kürzlich ein „aufregendes“ Abenteuer. Ein Kunde wollte über SolrJ auf die Streaming Expression-Funktionalität zugreifen. Dies…
Hier bei Lucidworks hatten wir kürzlich ein „aufregendes“ Abenteuer. Ein Kunde wollte über SolrJ auf die Streaming Expression-Funktionalität zugreifen. Dies erwies sich als schwieriger, als wir erwartet hatten. Daher hielten wir es für sinnvoll, einen Blogbeitrag mit unseren Erkenntnissen/Empfehlungen zu verfassen.
Empfehlung
Sparen Sie sich Zeit und Mühe und verwenden Sie das unten beschriebene Muster. Versuchen Sie nicht, die Low-Level-Komponenten selbst zusammenzustellen, es sei denn, Sie möchten mehrere Tage damit verbringen, die Arbeit der Autoren des Streaming Expression-Codes zu duplizieren.
Ja, ich weiß. Echte Programmierer stellen Low-Level-Code selbst zusammen, weil sie, nun ja, echte Programmierer sind. Die traditionelle Begründung für die Verwendung von Low-Level-Konstrukten ist, dass sie effizienter sind. Das Schreiben auf einer höheren Ebene bedeutet, dass die einzige „Ineffizienz“ hier darin besteht, dass Solr den Ausdruck parsen muss. Ich behaupte, dass die Zeit, die für das Parsen der String-Version eines Streaming-Ausdrucks aufgewendet wird, im Vergleich zu der von der Abfrage geleisteten Arbeit so winzig ist, dass die „Effizienz“ der Verwendung eines Low-Level-Konstrukts völlig im Rauschen untergeht. Wenn Sie mehrere Tage (oder Wochen, wenn Sie bei einem Upgrade Ihren Code überarbeiten müssen) damit verbringen, 0,000000001% der Ausführungszeit zu gewinnen, wird das Ihren Chef nicht glücklich machen.
Glauben Sie mir das.
Außerdem steckt eine Menge Arbeit hinter dem Prozess, eingehende Anfragen in Ihrem Cluster auszugleichen. Übergeordnete Konstrukte übernehmen diese wichtige Aufgabe für Sie, und zwar mit beachtlicher Effizienz.
Die Analogie, die ich oft verwende, ist, dass Sie Solr anstelle von Lucene verwenden, um von der ganzen Arbeit zu profitieren, die die Solr-Leute geleistet haben. Sicherlich ist es möglich, Solr komplett zu umgehen, aber eine Reihe von sehr intelligenten Leuten hat hart daran gearbeitet, Ihnen zu ermöglichen, Ihr Problem auf einer höheren Ebene, schneller und mit weniger Arbeit zu lösen. Wenn Sie wirklich, wirklich, wirklich auf einer niedrigeren Ebene arbeiten müssen, ist das möglich. Wenn ich in diesem Fall Ihr Vorgesetzter wäre, würde ich Sie bitten, zu zeigen, warum es den Zeitaufwand für die Ingenieure wert ist, bevor ich den Aufwand genehmige.
Wenn Sie darauf bestehen, können Sie das gerne tun, aber betrachten Sie dies als faire Warnung, dass der Versuch, dies zu tun, ein Fehler ist:
- Ein guter Weg, um verrückt zu werden.
- Eine gute Möglichkeit, Code zu pflegen, den Sie nicht pflegen müssen.
- Eine gute Möglichkeit, den nächsten Sprint-Meilenstein zu verpassen.
Gehen wir sie der Reihe nach durch
Eine gute Art, verrückt zu werden
Die Implementierung von Solr Streaming Expression ist komplex. Sie ermöglicht leistungsstarke, wenn auch komplexe Funktionen. Es gibt vielleicht ein Dutzend Menschen auf der Welt, die das alles im Detail verstehen.
Wenn Sie diesen Blog lesen, um Tipps zu erhalten, gehören Sie wahrscheinlich nicht zu diesen Leuten.
Ein guter Weg, um Code pflegen zu müssen…
Die Streaming-Funktionalität entwickelt sich vom einfachen ‚/export‘-Handler über Streaming Expressions und ParallelSQL extrem schnell weiter. Der Versuch, mit dem Low-Level-Code zu arbeiten, bedeutet, dass Sie Ihren Code überarbeiten müssen, wenn sich der Low-Level-Code überhaupt ändert. Das ist keine sinnvolle Nutzung Ihrer Zeit.
Ein guter Weg, um die nächste Sprint-Deadline zu verpassen
Ich würde sogar sagen, dass es ein guter Weg ist, mehrere Sprinttermine zu verpassen. Sie werden gleich sehen, warum.
Hier ist der einfache Weg
Beginnen wir mit unserem empfohlenen Ansatz. Hier ist ein Streaming-Ausdruck (vereinfacht, viele Details wurden aus Gründen der Übersichtlichkeit weggelassen):
select(rollup(search search_parameters), rollup_parameters), select_parameters)
Angenommen, Sie haben die Benutzeroberfläche verwendet, um den Ausdruck zu erstellen und sind damit zufrieden. Um die einfache Version zu erstellen, sieht sie wie folgt aus:
String cexpr = "select(rollup(search search_parameters), rollup_parameters), select_parameters)";
ModifiableSolrParams paramsLoc = new ModifiableSolrParams();
paramsLoc.set("expr", cexpr);
paramsLoc.set("qt", "/stream");
// Note, the "/collection" below can be an alias.
String url = some_Solr_URL_in_your_cluster + "/collection" ;
TupleStream solrStream = new SolrStream(url, paramsLoc);
StreamContext context = new StreamContext();
solrStream.setStreamContext(context);
solrStream.open();
read_tuples_til_EOF;
solrStream.close(); // could be try-with-resources
Der obige ‚cexpr‘ kann auf jede beliebige Weise konstruiert werden, es ist schließlich nur ein String. Was auch immer mit curl oder dem Expressions-Teil der Admin-Benutzeroberfläche funktioniert.
Hier ist die harte Tour
Vergleichen Sie dies mit dem Code, den Sie schreiben (und pflegen!) müssten, wenn Sie versuchen würden, all dies selbst zu erstellen (und ich lasse einen großen Teil des Codes weg). Übrigens, nachdem wir auf mehrere Probleme gestoßen sind, haben wir beschlossen, es uns einfach zu machen [1]. Ich verwende die gleiche Kurzschrift wie oben…
StreamExpression expression;
StreamContext streamContext = new StreamContext();
SolrClientCache solrClientCache = new SolrClientCache();
// What's this? Why do I need it? If I don't set it I'll get an NPE
streamContext.setSolrClientCache(solrClientCache);
// what if I can't get to the ZK host since I can't tunnel to it if I need to?
StreamFactory factory = new StreamFactory().
withCollectionZkHost(collection_name, ZK_ensemble_string);
// Wait, is /export right? Maybe it's /stream? Well, yes it should be /stream.
// But what if it changes in the future?
// well, the below works, but is it correct? Hmmm, this is the search part,
// really the third level of select(rollup(search...)...)
expression = StreamExpressionParser.parse(search_expression);
CloudSolrStream searchStream = null;
RollupStream rollupStream = null;
SelectStream selectStream = null;
TupleStream stream = null;
// OK, let's set the innermost stream up.
try {
searchStream = new CloudSolrStream(expression, factory);
searchStream.setStreamContext(streamContext);
// What is this about? I have to change the metrics
// time I change the expressions I want to collect?
// this is an example of how to instantiate
// "rollup_parameters".
Bucket[] buckets = {new Bucket("something")};
Metric[] metrics = {
new SumMetric("some_field1"),
new MinMetric("some_field2"),
new CountMetric()};
// I need to revisit this if the expression changes?
rollupStream = new RollupStream(searchStream, buckets, metrics);
rollupStream.setStreamContext(streamContext);
// Set the rest of the parameters I need to set for rollupStream.
// What are they? How do I do that?
// Do I need to revisit this if the select changes?
// Now wrap the above in the outermost SelectStream
selectStream = new SelectStream(rollupStream, more_parameters_what_are_they);
// Is it OK to set the streamContext to the same underyling object for all
// three streams?
selectStream.setStreamContext(streamContext);
// set any additional necessaries.
stream = selectStream;
} catch (Exception e) {
// handle all of the bits I need to here. Closing the streams?
// Are they open? The usual issues with catch blocks.
}
// Oops, I caught the exception above, I guess I have to test here for
// whether the stream is null.
try {
stream.open();
read_tuples_til_EOF;
} catch (Exception e) {
//report an error
} finally {
// try/catch in a finally block just in case
// something goes wrong.
if (stream != null) {
try {
stream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
// Hmmm, what is this solrClientCache anyway?
// Oh, I set this cache for all the streams I created,
// do I have to close it three times (no)?
// All I know experimentally is that my SolrJ program will hang
// for 50 seconds if I don't do this when I try to exit.
// I guess I could look through all the code and understand this
// in more detail, but the boss wants results.
solrClientCache.close();
Ok, das ist ein wenig übertrieben, aber Sie verstehen, worum es geht. Damit der gesamte Code von Streaming Expressions die Wunder vollbringt, die er vollbringt, sind viele Überlegungen erforderlich. Und anstatt das alles für Sie zu erledigen, müssen Sie Ihren Code für jede Änderung in der Streaming Expression anpassen. Sagen wir, Sie möchten „max(some_field3“) hinzufügen oder einen generischen Builder schreiben. Und das, nachdem Sie den Ausdruck, den Sie haben möchten, bereits ausgearbeitet haben. Das einfache Muster oben hat das bereits für Sie erledigt.
Wenn ich meinen Griesgram-Hut aufsetzen würde, würde ich behaupten, dass es schön wäre, wenn diese Low-Level-Konstrukte vor SolrJ-Clients verborgen wären. SolrJ wird für die Kommunikation zwischen den Solr-Knoten verwendet, also muss diese Ebene offengelegt werden.
Wir haben den obigen Code nie vollständig zum Laufen gebracht. Schließlich stießen wir auf eine weitere Ebene von Schwierigkeiten. Ob es daran lag, dass der Code „qt=/export“ und nicht „qt=/stream“ verwendet hat oder ähnliches – ich weiß nicht, ob wir einen anderen Fehler hatten oder ob der Mond nicht voll war. Was ich weiß , ist, dass ich, als ich die empfohlene Lösung erhielt und über die Beibehaltung von „roll your own“ nachdachte, feststellte, dass die Vorteile der Verwendung des empfohlenen Musters unsere eigenen bei weitem übertrafen. Nicht nur unter dem Gesichtspunkt, dass wir etwas auf die Beine stellen müssen, sondern auch unter dem Gesichtspunkt der Wartungsfreundlichkeit.
Es ist unnötig, Ihre eigene Lösung zu entwickeln. Das empfohlene Muster umgeht die gesamte Komplexität und nutzt die Arbeit derjenigen, die die Grundlagen von Streaming Expressions wirklich verstehen. Ich empfehle Ihnen nachdrücklich, das empfohlene Muster zu verwenden und sich nur dann an Code-Muster auf niedrigerer Ebene zu wagen, wenn Sie einen zwingenden Bedarf haben. Und ehrlich gesagt fällt es mir schwer, mir einen zwingenden Bedarf auszumalen.
Andere Probleme
Und dann sind da noch die „zusätzlichen“ Probleme:
- Mit dem obigen Code erhalten Sie das ZK-Ensemble. Das scheint einfach genug zu sein. Aber was ist, wenn ich ein Deployment verwende, das mir keinen Zugriff auf das ZooKeeper-Ensemble erlaubt? Ich kann http-Anfragen über Tunneling viel einfacher verwenden
- Zugegebenermaßen sind die Strings von Streaming Expressions „interessant“ zu konstruieren. Wenn Sie das empfohlene Muster verwenden, können Sie beliebige Streaming Expressions verwenden und müssen nicht jedes Mal Ihren Code überarbeiten, wenn sich der Ausdruck ändert.
Fazit
Einige sehr kluge Leute haben viel Zeit damit verbracht, dies alles zum Laufen zu bringen, sich um die kleinen Details zu kümmern und dies auch in Zukunft zu tun. Profitieren Sie von deren harter Arbeit und verwenden Sie das empfohlene Muster!
[1] Besonders hervorheben möchte ich Joel Bernstein, einen der Hauptautoren des Streaming-Konzepts, der sich äußerst großzügig gezeigt hat, als es darum ging, meine Fragen und die Fragen der Benutzerliste zu beantworten. Sagen wir einfach, er lässt mich viel schlauer aussehen! Ich habe noch nicht ausgerechnet, wie hoch die Rechnung ist, die ich ihm schulde, aber sie ist beträchtlich.