Vorwort: wieso ein Blog zu PHP, Solr und Lucene?

Wieso ein Blog zu PHP, Solr und Lucene?
Gegenstand und Ausgangspunkt all unserer Aktivitäten auf diesem Gebiet war ein Projekt um ein Nachrichtenportal und die Aufgabe, Recherchen und Analysen im Nachrichtenbestand von über 10 Million News performant zu handeln. Die MySQL Volltextsuche kam da schnell an Ihre grenzen, Oracle war keine Alternative.
Es reifte also die Frage, wie können andere (etwa die Internetsuchmaschiene google) immense Datenmengen spielend handeln?
Wir lösten den MySQL volltext mit Lucene ab. Der Performancegewinn war dramatisch. Suchen im Datenbestand, die vorher über 10 Sekunden dauerten, brauchen mittels Lucene und Solr nur selten mehr als 20ms!
Eine neue Welt tat sich auf, die es zu erobern galt und schnell fiel auf, dass deutschsprachige Seiten zum Thema Mangelware sind. Dies soll sich mit diesem Blog ein wenig ändern.

Sie haben Fragen zu Solr/Lucene/PHP? Schreiben sie uns einen Kommentar!

Mittwoch, 22. Juni 2011

schema.xml

Die schema.xml liegt üblicher Weise im conf Verzeichnis einer jeden SOLR Instanz  und dient zur Konfiguration des Lucene Index: wie werden Daten im Index gehalten und in wie weit werden sie noch bearbeitet (Stemming/Wortstammbildung, Stoppworte, etc)

Im ersten Moment wirkt diese Datei unübersichtlich und erschlagend. Tatsächlich ist sie aber nur voll gepackt mit Beispielen, die im konkreten Fall oftmals unnötig sind.

Die schema.xml kann man grob in 3 Bereiche unterteilen:

  1. grundlegende Konfiguration
  2. Konfigurationder Datenfelder
  3. Definitionen von Datenfeldtypen
Wichtig für die grundlegende Konfiguration sind insbesondere folgende Einträge:
<uniqueKey>LFNR</uniqueKey>
<defaultSearchField>TEXT</defaultSearchField>
<solrQueryParser defaultOperator="OR"/> 
uniqueKey definiert dabei quasi den PrimaryKey, defaultSearchField das Feld, in dem gesucht wird, wenn bei einer Suche kein spezifisches Feld angegeben wird. Beispielsweise ginge eine Suche nach q=berlin per Default nun immer auf das Feld TEXT. solrQueryParser definiert, wie mehrere Begriffe innerhalb einer Phrase verknüpft werden. Eine suche nach q=Joachim Löw ist daher zu lesen wie q=Jochim OR Löw und bringt also Treffer, in denen mindestens eines der beiden Wörter vorhanden ist.

Ganz markant in der schema.xml ist die Konfiguration jedes Datenfeldes. Jedes in Lucene / Solr abgelegte Dokument setzt sich zusammen aus Feldern (Eingangsdatum, Überschrift, Volltext, etc).  Ein Dokument kann daher auch als Datensatz betrachtet werden.

Jedes Feld ist dabei zu definieren. Eine solche Definition könnte wie folgt aussehen:
<field name="LFNR" type="long" indexed="true" stored="true"/>
<field name="TEXT" type="text_de" indexed="true" stored="false" multiValued="true"/>


Die Parameter indexed und stored werden getrennt behandelt. Das heißt: ein Element (Feld) kann in Lucene zwar indexiert sein, muss dafür aber nicht gespeichert werden. Dies ist dann interessant, wenn man tatsächlich Lucene / Sol nur als Volltextindex nutzen möchte, die Daten selber aber aus einer Datenbank (MySql, Oracle,..) bezogen werden.
Eine solche Konstellation ist dann sinnvoll, wenn große Datenmengen verarbeitet werden.
Würde man grundsätzlich all das, was man in Lucene indexiert gleichermaßen auch als Volltext abspeichern, würde der Index erheblich anwachsen, was wiederum auf die Performance von Suchen im Index geht...
..denn solange der Index komplett in den RAM des Server passt, so lange kann Solr seine höchste Geschwindigkeit erreichen.Ziel ist es also, den Index möglichst übersichtlich/klein zu halten.

 In unserem Beispiel also ist der uniqueKey (LFNR) in Lucene gespeichert und indexiert. Das Feld LFNR ist dabei auch der primary Key in der Referenzdatenbank, aus welcher die Dokumente nach Lucene importiert werden.
Eine Suche im Volltext-Index von Solr / Lucene könnte dann als Treffer die LFNR zurück liefern. Über diese LFNR wird dann der Volltext aus der DB geladen.

Der Parameter multiValued gibt an, ob mehrere Felder in diesem Feld vereint werden.
In der schema.xml kommt dazu ein sogenanntes copyField zum Einsatz. Dabei werden die Informationen eines Feldes in ein anderes "kopiert".
Beispiel: die Suche erfolgt per Default im Feld TEXT. Darüber hinaus besitzt des Dokument aber noch eine Überschrift (Feld: UEBSCHRIFT). Mein Ziel ist es, dass sämtliche Suche im (Voll-)TEXT, auch auf das Feld UEBSCHRIFT angewandt werden. Dazu lege ich ein copyField wie folgt an:
<copyField source="UEBSCHRIFT" dest="TEXT" />
Suchphrasen werden dann auch gefunden, wenn sie im (Voll-)TEXT oder im Feld UEBSCHRIFT stehen.

Mit dem Parameter type= erfolgt die Definitionen von Datenfeldtypen.
Hier wird Solr mitgeteilt, welche art von Daten sich in einem Feld befindet und wie diese verarbeitet werden soll.

Ein solcher fieldType Bereich kann sich dabei in verschiedenen analyzer unterteilen. Dadurch kann innerhalb eines Feldes eine unterschiedliche Verarbeitung der Daten beim Data Import bzw. bei der Abfrage (query) erreicht werden. Innerhalb dieser Analyzer-Tags findet die eigentliche Definition statt:

<fieldType name="text_de" class="solr.TextField" positionIncrementGap="100">
      <analyzer type="index">
        <tokenizer class="solr.StandardTokenizerFactory"/> 
        <filter class="solr.LowerCaseFilterFactory"/>
        <filter class="solr.StopFilterFactory"
                ignoreCase="true"
                words="stopwords-de.txt"
                enablePositionIncrements="true"
                />
        <filter class="solr.WordDelimiterFilterFactory" generateWordParts="1" generateNumberParts="1" catenateWords="1" catenateNumbers="1" catenateAll="0" splitOnCaseChange="1"/>
        <filter class="solr.KeywordMarkerFilterFactory" protected="protwords.txt"/>
        <filter class="solr.SnowballPorterFilterFactory" language="German2" />
      </analyzer>
      <analyzer type="query">
        <tokenizer class="solr.StandardTokenizerFactory"/>
        <filter class="solr.SynonymFilterFactory" synonyms="synonyms-de.txt" ignoreCase="true" expand="true"/>
        <filter class="solr.WordDelimiterFilterFactory" generateWordParts="1" generateNumberParts="1" catenateWords="1" catenateNumbers="1" catenateAll="0" splitOnCaseChange="1"/>
        <filter class="solr.LowerCaseFilterFactory"/>
        <filter class="solr.KeywordMarkerFilterFactory" protected="protwords.txt"/>
         <filter class="solr.SnowballPorterFilterFactory" language="German2" />
      </analyzer>
    </fieldType>
In diesem Beispiel gibt es 2 analzyer. Einer vom type="index" (für das indexieren der Daten), der andere vom type="query" zum abfragen der Daten.
Der tokenizer ist dabei das Element, mit dem Solr / Lucene Textblöcke in einzelne Worte spaltet.
Danach folgen Filter. Beim index analyzer werden also die zu importierenden Texte erste mittels tokenizer in einzelne Worte zerlegt.

<tokenizer class="solr.StandardTokenizerFactory"/>
anschließend erfolgt eine Umwandlung in Kleinbuchstaben
<filter class="solr.LowerCaseFilterFactory"/> 
und danach werden die Felder um Stoppworte bereinigt:

        <filter class="solr.StopFilterFactory"
                ignoreCase="true"
                words="stopwords-de.txt"
                enablePositionIncrements="true"
                />
Abschließend wird ein Stemmer Algorythmus angewandt. Dies ist notwendig, damit Wörter mit gleichem Wortstamm als zusammengehörend erkannt werden, etwa wie Apotheke und Apotheken, etc.

<filter class="solr.SnowballPorterFilterFactory" language="German2" />

Bei der Wortstammbildung in Solr stehen uns verschiedene "Stemmer" zur Verfügung. "German2" steht dabei für ein Verfahren, in dem Umlaute nicht auf die Grundform (ü->u, ä->a,..) reduziert werden, sondern umschrieben werden (ü->ue, ä->ae,..)
Dies kann relevant sein, denn unter German ergeben die Begriffe Völker und Volker (der Männername) den gleichen Stamm: volk. Wohingegen im Stemmer German2  die Begriffe zu voelk bzw. volk werden. Nachteil dieser Variante: auch das Wort "Volk" ist gestemmt "volk" (Wie der Name Volker -> Volk)  und würde daher fälschlicher Weise eher mit dem Namen "Volker" gefunden werden, als mit der Mehrzahl "Völker".
Hier ist also abzuwägen, was im Einzelfall die beste Alternative ist oder problematische Wörter (wie etwa der Name Volker) werden speziell definiert. Mit dem Eintrag

<filter class="solr.KeywordMarkerFilterFactory" protected="protwords.txt"/>
werden über die Datei "protwords.txt" Wörter definiert, die nicht dem Stemming unterzogen werden sollen.

Zur Aktivierung  einer jeden Änderung in der schema.xml ist mindestens der Neustart des Solr-Cores, in der Regel also der entsprechenden Solr Instanz notwendig. Viele Änderungen in der schema.xml machen darüber hinaus das Neuindexieren notwendig. Ach wenn prinzipiell Änderungen im <analyzer type="query"> grundsätzlich und gefühlt keine Neuindizierung erfordern, so ist der Neuaufbau des Index dennoch dringend empfohlen, um Nebeneffekte  zu vermeiden.

3 Kommentare:

  1. Sehr nützliche Informationen zu Solr und dann noch in deutsch, weiter so!
    Hab das Web nach Informationen zu Solr durchforstet und bin hier gelandet. Das hilft mir schon mal weiter. Danke!

    AntwortenLöschen
  2. Hallo,
    ich habe mich gestern Abend näher mit der Stemmerproblematik beschäftigt und es sieht so aus dass die deutsche Sprache ungeeignet ist im Vergleich zu anderen Sprachen da wir zusammengesetzte Wörter benutzen, was z. B. im englischen durch Trennung mit of passiert.
    Ein Algorithmus hilft an dieser Stelle nicht weiter, da muss wohl ein Lexikon her.
    Viele Grüße
    JPee

    AntwortenLöschen
    Antworten
    1. In der Tat ist die deutsche Sprache recht schwer für die vorhandenen Stemmer. Allerdings nutzen andere Sprachen ebenfalls zusammengesetzte Wörter. Bspw. High und Way für Highway und stehen aher vor einem ähnlichen Problem.
      Diese Problematik soll in Solr / Lucene mit dem Thema "Compound Words" angegangen werden. Ziel ist es, ein Wörterbuch mitzugeben.
      Einbinden könnte man dies mittels:


      In der Doku habe ich es noch nicht gefunden, aber zumindest hier gibt's einen Verweis zur Klassendefinition:
      http://lucene.apache.org/core/4_0_0-BETA/analyzers-common/org/apache/lucene/analysis/compound/DictionaryCompoundWordTokenFilterFactory.html

      Löschen