Suche verstehen
Wir helfen Ihnen dabei, die Such­anfragen Ihrer Kund*innen und Mit­ar­bei­­ter*innen zu ver­stehen und schnell re­le­vante Ergebnisse zu liefern.

Eine Solr SearchComponent entwickeln

Apache Solr verwendet SearchComponents, um verschiedene Aspekte der Suche abzubilden und modular zusammenstellen zu können. So gibt es eigenständige SearchComponents für die eigentliche Suche, das Highlighting, die Facettierung uvm.

Dieser Ansatz ermöglicht es ebenfalls, eigene SearchComponents zu entwickeln und diese in einer Solr-Installation zu betreiben.

Die folgenden Code-Beispiele basieren auf Solr 5.2.1, allerdings hat sich die Schnittstelle seit Solr 4 nur unmerklich verändert und sie können deshalb ohne Einschränkung auch dafür verwendet werden.

Einsatzmöglichkeiten

Grundsätzlich kann eine SearchComponent auch nicht mehr, als mit den Dokumenten in einem Solr-Core zu arbeiten und einen Algorithmus abzubilden, der aus diesen Dokumenten Ergebnisse extrahiert.

Der Vorteil liegt darin, auf diese Dokumente zugreifen zu können, ohne deren Felder über Solrs HTTP-Schnittstelle übertragen zu müssen. Darüber hinaus kann eine einzelne SearchComponent im Laufe ihrer Ausführung beliebig viele Anfragen an den Solr-Index stellen und somit zusätzliche Suchanfragen überflüssig machen, was weitere Verarbeitungszeit einspart.

Eine eigene SearchComponent kommt also immer dann infrage, wenn ansonsten mehrere Suchanfragen gestellt oder sehr viele Dokumente bzw. Dokumentfelder angefragt werden müssten.

Die SearchComponent erstellen

Den vollständigen Quelltext der Klasse können Sie ebenfalls direkt herunterladen:
[call-to-action id=”785″/]

Eine SearchComponent muss von der abstrakten Klasse org.apache.solr.handler.component.SearchComponent erben:

package com.indoqa.solr.demo;

import java.io.IOException;

import org.apache.solr.handler.component.ResponseBuilder;
import org.apache.solr.handler.component.SearchComponent;

public class DemoSearchComponent extends SearchComponent {

    @Override
    public String getDescription() {
        return "Indoqa DemoSearchComponent";
    }

    @Override
    public void prepare(ResponseBuilder rb) throws IOException {
    }

    @Override
    public void process(ResponseBuilder rb) throws IOException {
    }
}

Die Methode getDescription() liefert eine Beschreibung der SearchComponent für die SolrInfoMBean, ein Ansatz zur Beschreibung konfigurierbarer Komponenten, um diese leichter identifizieren zu können. Die Dokumentation von Solr empfiehlt dafür eine kurze Beschreibung der Komponente, die nicht länger als ein bis zwei Zeilen ist.

Die Methode prepare(ResponseBuilder rb) wird aufgerufen, bevor die eigentliche Verarbeitung erfolgt und ist dafür gedacht, den ResponseBuilder korrekt zu initialisieren bzw. die Suchparameter – die ebenfalls im ResponseBuilder gespeichert sind – anzupassen.
Zu bedenken ist dabei, dass zunächst die prepare-Methode aller beteiligten SearchComponents aufgerufen und erst dann die SearchComponent mit der eigentlichen Verarbeitung beginnt.
Ist für die eigene SearchComponent keine Initialisierung bzw. Modifizierung des ResponseBuilders notwendig, kann diese Methode einfach leer gelassen werden.

Die Methode process(ResponseBuilder rb) beinhaltet die eigentliche Bearbeitung der – ggfs. durch prepare veränderten – Suchanfrage.
Dazu kann die SearchComponent auf die Suchparameter zugreifen oder die Ergebnisse der bereits ausgeführten SearchComponents verwenden.
Über den ResponseBuilder kann ausserdem der SolrIndexSearcher erreicht werden, mit dem zusätzliche Abfragen durchgeführt werden können.

Als Beispiel soll unsere DemoSearchComponent die Länge der Werte eines bestimmten Feldes aller Dokumente im Suchergebnis ermitteln und diese Zahl an das Suchergebnis anhängen. Dazu müssen wir die Methode process(ResponseBuilder rb) implementieren.

Zunächst definieren wir einen Parameter, mit dem festgelegt wird, welches Feld analysiert werden soll. Ist dieser Parameter nicht gesetzt, soll unsere SearchComponent einfach nichts unternehmen:

    @Override
    public void process(ResponseBuilder rb) throws IOException {
        SolrParams params = rb.req.getParams();
        String fieldName = params.get("demo.field");
        if (fieldName == null) {
            return;
        }
    }

Als nächstes ermitteln wir das Feld mit dem UniqueKey, das den eineindeutigen Schlüsselwert der Dokumente enthält. Diesen werden wir verwenden, um die einzelnen Werte den jeweiligen Dokumenten im Suchergebnis zuzuordnen.
Zusammen mit dem angeforderten Feldnamen für die Analyse ergibt dies die Felder, die wir in weiterer Folge von den Dokumenten auslesen müssen.

    @Override
    public void process(ResponseBuilder rb) throws IOException {
        SolrParams params = rb.req.getParams();
        String fieldName = params.get("demo.field");
        if (fieldName == null) {
            return;
        }

        SchemaField uniqueKeyField = rb.req.getSchema().getUniqueKeyField();

        Set<String> fieldNames = new HashSet<>();
        fieldNames.add(fieldName);
        fieldNames.add(uniqueKeyField.getName());
    }

Nun definieren wir eine NamedList, in die wir unsere ermittelten Werte eintragen können und fügen diese dem Suchergebnis hinzu.

    @Override
    public void process(ResponseBuilder rb) throws IOException {
        SolrParams params = rb.req.getParams();
        String fieldName = params.get("demo.field");
        if (fieldName == null) {
            return;
        }

        SchemaField uniqueKeyField = rb.req.getSchema().getUniqueKeyField();

        Set<String> fieldNames = new HashSet<>();
        fieldNames.add(fieldName);
        fieldNames.add(uniqueKeyField.getName());

        NamedList<Integer> result = new NamedList<>();
        rb.rsp.add("demo", result);
    }

Jetzt können wir über die Dokumente im Suchergebnis iterieren und die benötigten Felder aus dem Suchindex lesen. Dazu verwenden wir den SolrIndexSearcher, den wir über den ResponseBuilder erreichen können.
Es ist empfehlenswert, so wenige Felder wie möglich über den SolrSearchIndex auslesen zu lassen, da sich dies direkt auf die Ausführungsgeschwindigkeit auswirkt.

    @Override
    public void process(ResponseBuilder rb) throws IOException {
        SolrParams params = rb.req.getParams();
        String fieldName = params.get("demo.field");
        if (fieldName == null) {
            return;
        }

        SchemaField uniqueKeyField = rb.req.getSchema().getUniqueKeyField();

        Set<String> fieldNames = new HashSet<>();
        fieldNames.add(fieldName);
        fieldNames.add(uniqueKeyField.getName());

        NamedList<Integer> result = new NamedList<>();
        rb.rsp.add("demo", result);

        DocList docList = rb.getResults().docList;
        SolrIndexSearcher searcher = rb.req.getSearcher();

        for (DocIterator iterator = docList.iterator(); iterator.hasNext();) {
            Document document = searcher.doc(iterator.next(), fields);
        }
    }

Die Ermittlung der tatsächlichen Länge der Werte des gewünschten Feldes lagern wir für mehr Übersichtlichkeit in eine eigene Methode aus. Den ermittelten Wert fügen wir unter dem Schlüsselwert des Dokuments zur NamedList hinzu:

    @Override
    public void process(ResponseBuilder rb) throws IOException {
        SolrParams params = rb.req.getParams();
        String fieldName = params.get("demo.field");
        if (fieldName == null) {
            return;
        }

        SchemaField uniqueKeyField = rb.req.getSchema().getUniqueKeyField();

        Set<String> fieldNames = new HashSet<>();
        fieldNames.add(fieldName);
        fieldNames.add(uniqueKeyField.getName());

        NamedList<Integer> result = new NamedList<>();
        rb.rsp.add("demo", result);

        DocList docList = rb.getResults().docList;
        SolrIndexSearcher searcher = rb.req.getSearcher();

        for (DocIterator iterator = docList.iterator(); iterator.hasNext();) {
            Document document = searcher.doc(iterator.next(), fields);

            String docKey = document.getField(uniqueKeyField.getName()).stringValue();
            int totalFieldLength = this.getTotalFieldLength(document, fieldName);
            result.add(docKey, totalFieldLength);
        }
    }

Jetzt fehlt noch die passende Methode für den Aufruf aus Zeile 25.
Zunächst definieren wir eine Ergebnisvariable und weisen den Startwert zu:

    private int getTotalFieldLength(Document document, String fieldName) {
        int result = 0;

        return result;
    }

Nun iterieren wir über die gewünschten Feldwerte des Dokuments und addieren die jeweilige Länge ihrer Werte zur Ergebnisvariable hinzu.
Dabei ist zu beachten, dass Felder, die als multiValued konfiguriert sind, mehrfach vorkommen können, da jedes einzelne Feld maximal einen Wert tragen kann.
Für Fehlertoleranz sollte eine Prüfung auf Null-Werte ebenfalls nicht fehlen:

    private int getTotalFieldLength(Document document, String fieldName) {
        int result = 0;

        IndexableField[] indexableFields = document.getFields(fieldName);
        for (IndexableField eachIndexableField : indexableFields) {
            String stringValue = eachIndexableField.stringValue();
            if (stringValue == null) {
                continue;
            }

            result += stringValue.length();
        }

        return result;
    }

Die SearchComponent installieren

Um unsere SearchComponent zu installieren, müssen wir die kompilierte Klasse in eine Jar-Datei verpacken und diese – wie eine herkömmliche Solr-Erweiterung – in das Verzeichnis lib kopieren. Dadurch steht unsere DemoSearchComponent als Klasse zur Verfügung.

Damit sie tatsächlich verwendet werden kann, müssen wir sie allerdings erst noch konfigurieren.
Dazu müssen wir sie als SearchComponent in der solrconfig.xml eintragen und einem RequestHandler als aktive SearchComponent hinzufügen:

  <!-- A request handler that returns indented JSON by default -->
  <requestHandler name="/query" class="solr.SearchHandler">
     <lst name="defaults">
       <str name="echoParams">explicit</str>
       <str name="wt">json</str>
       <str name="indent">true</str>
       <str name="df">id</str>
     </lst>
  </requestHandler>

  <searchComponent name="demo-component" class="com.indoqa.solr.demo.DemoSearchComponent" />

  <requestHandler name="/demo" class="solr.SearchHandler">
    <lst name="defaults">
       <str name="echoParams">explicit</str>
       <str name="wt">json</str>
       <str name="indent">true</str>
       <str name="df">id</str>
    </lst>

    <arr name="last-components">
      <str>demo-component</str>
    </arr>
  </requestHandler>

  <!-- realtime get handler, guaranteed to return the latest stored fields of any document, without the need to commit or open a new searcher. The current implementation relies on the updateLog feature being enabled. ** WARNING ** Do NOT disable the realtime get handler at /get if you are using SolrCloud otherwise any leader election will cause a full sync in ALL replicas for the shard in question. Similarly, a replica recovery will also always fetch the complete index from the leader because a partial sync will not be possible in the absence of this handler. -->
  <requestHandler name="/get" class="solr.RealTimeGetHandler">
    <lst name="defaults">
      <str name="omitHeader">true</str>
      <str name="wt">json</str>
      <str name="indent">true</str>
    </lst>
  </requestHandler>

Unsere DemoSearchComponent wird also nur dann aufgerufen, wenn wir den RequestHandler “/demo” verwenden.
Nach einem Neustart von Solr können wir unsere DemoSearchComponent bereits verwenden:

http://localhost:8983/solr/indoqa-test-core/demo?q=*%3A*&rows=5&demo.field=text
<?xml version="1.0" encoding="UTF-8"?>
<response>
  <lst name="responseHeader">
    <int name="status">0</int>
    <int name="QTime">1</int>
    <lst name="params">
      <str name="q">*:*</str>
      <str name="rows">5</str>
      <str name="demo.field">text</str>
    </lst>
  </lst>
  <result name="response" numFound="1868" start="0">
    <doc>
      <str name="id">dokument-1</str>
      <str name="titel">Dokument 1</str>
      <str name="ersteller">user-3</str>
      <str name="text">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam n</str>
    </doc>
    <doc>
      <str name="id">dokument-2</str>
      <str name="titel">Dokument 2</str>
      <str name="ersteller">user-2</str>
      <str name="text">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dol</str>
    </doc>
    <doc>
      <str name="id">dokument-3</str>
      <str name="titel">Dokument 3</str>
      <str name="ersteller">user-7</str>
      <str name="text">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy e</str>
    </doc>
    <doc>
      <str name="id">dokument-4</str>
      <str name="titel">Dokument 4</str>
      <str name="ersteller">user-1</str>
      <str name="text">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invid</str>
    </doc>
    <doc>
      <str name="id">dokument-5</str>
      <str name="titel">Dokument 5</str>
      <str name="ersteller">user-2</str>
      <str name="text">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam e</str>
    </doc>
  </result>
  <lst name="demo">
    <int name="dokument-1">67</int>
    <int name="dokument-2">112</int>
    <int name="dokument-3">74</int>
    <int name="dokument-4">92</int>
    <int name="dokument-5">132</int>
  </lst>
</response>

Da unsere DemoSearchComponent nicht an die Feldliste gebunden ist, die vom Aufruf definiert wurde, kann die Feldlänge sogar dann ermittelt werden, wenn das Feld gar nicht im Ergebnis vorkommen soll:

http://localhost:8983/solr/indoqa-test-core/demo?q=*%3A*&rows=5&demo.field=text&fl=id
<?xml version="1.0" encoding="UTF-8"?>
<response>
  <lst name="responseHeader">
    <int name="status">0</int>
    <int name="QTime">1</int>
    <lst name="params">
      <str name="q">*:*</str>
      <str name="rows">5</str>
      <str name="demo.field">text</str>
      <str name="fl">id</str>
    </lst>
  </lst>
  <result name="response" numFound="1868" start="0">
    <doc>
      <str name="id">dokument-1</str>
    </doc>
    <doc>
      <str name="id">dokument-2</str>
    </doc>
    <doc>
      <str name="id">dokument-3</str>
    </doc>
    <doc>
      <str name="id">dokument-4</str>
    </doc>
    <doc>
      <str name="id">dokument-5</str>
    </doc>
  </result>
  <lst name="demo">
    <int name="dokument-1">67</int>
    <int name="dokument-2">112</int>
    <int name="dokument-3">74</int>
    <int name="dokument-4">92</int>
    <int name="dokument-5">132</int>
  </lst>
</response>