Wie man in Solr eine Entitätsextraktion durchführt
Meine Arbeit bei Lucidworks besteht hauptsächlich darin, Kunden bei der Erstellung ihrer gewünschten Lösungen zu helfen. In letzter Zeit hat…
Meine Arbeit bei Lucidworks besteht hauptsächlich darin, Kunden bei der Erstellung ihrer gewünschten Lösungen zu helfen. In letzter Zeit hat sich mehr als ein Kunde nach der „Entitätsextraktion“ erkundigt. Die Entitätsextraktion, wie sie auf Wikipedia definiert wird, „versucht, atomare Elemente im Text zu finden und in vordefinierte Kategorien einzuordnen, wie z.B. die Namen von Personen, Organisationen, Orten, Zeitangaben, Mengen, Geldwerte, Prozentsätze usw.“. Wenn wir die Anforderungen unserer Kunden genauer unter die Lupe nehmen, stellt sich heraus, dass viele von ihnen unkomplizierte Lösungen haben, die integrierte (Solr 4.x) Komponenten verwenden, wie z.B.:
* Akronyme als Facetten
* Schlüsselwörter oder Phrasen aus einer festen Liste als Facetten
* Längen- und Breitenangaben als geografische Punkte
In diesem Artikel wird beschrieben und demonstriert, wie Sie diese Techniken anwenden können. Als Bonus werden wir auch URLs extrahieren, die im Text gefunden wurden. Beginnen wir mit einer Beispieleingabe und der entsprechenden Ausgabe, die jede der beschriebenen Techniken liefert.
Beispiel für den Textinhalt des Dokuments:
The CHO airport is at 38.1384683,-78.4527887. See also: http://www.lat-long.com/Latitude-Longitude-1480221-Virginia-Charlottesville_Albemarle_Airport.html
Und nach der Indizierung dieses Dokuments in Solr werden dem Dokument mehrere zusätzliche Felder hinzugefügt:
- extracted_locations: Lat/Long-Punkte, die im Inhalt des Dokuments erwähnt werden, indiziert als geographisch versierte Punkte, gespeichert im Dokument zur einfachen Verwendung in Karten oder anderswo
- links: ein gespeichertes Feld mit http(s)-Links
- Akronyme: ein durchsuchbares/filterbares/facettierbares (aber nicht gespeichertes) Feld, das alle Akronyme mit mehr als drei Buchstaben in CAPS enthält
- key_phrases: ein durchsuchbares/filterbares/facettierbares (aber auch nicht gespeichertes) Feld, das alle Schlüsselphrasen enthält, die einer angegebenen Liste entsprechen
Mit der Beispieleingabe ergibt dies eine Solr-Antwort wie diese (/query?q=*:*&facet=on&facet.field=acronyms&facet.field=key_phrases):
{ "responseHeader":{ "status":0, "QTime":3, "params":{ "facet":"on", "q":"*:*", "facet.field":["acronyms", "key_phrases"]}}, "response":{"numFound":1,"start":0,"docs":[ { "id":"/Users/erikhatcher/dev/lucene_4x/solr/example/exampledocs/gocho.txt", "content_type":["text/plain; charset=ISO-8859-1"], "resourcename":"/Users/erikhatcher/dev/lucene_4x/solr/example/exampledocs/gocho.txt", "content":["The CHO airport is at 38.1384683,-78.4527887.nSee also: http://www.lat-long.com/Latitude-Longitude-1480221-Virginia-Charlottesville_Albemarle_Airport.html"], "extracted_locations":["38.1384683,-78.4527887"], "links":["http://www.lat-long.com/Latitude-Longitude-1480221-Virginia-Charlottesville_Albemarle_Airport.html"], "link_ss":["http://www.lat-long.com/Latitude-Longitude-1480221-Virginia-Charlottesville_Albemarle_Airport.html"], "_version_":1439028523249434624}] }, "facet_counts":{ "facet_queries":{}, "facet_fields":{ "acronyms":[ "CHO",1], "key_phrases":[ "airport",1]}, "facet_dates":{}, "facet_ranges":{}}}
Beachten Sie den Unterschied zwischen den beiden Arten von Feldern, die hinzugefügt werden. Die eine Art von Feld (Akronyme und key_phrases) fügt dem Dokument indizierte Werte hinzu, aber diese Werte werden nicht gespeichert. Diese Art von Feld ist nützlich für Facettierung, Suche/Filterung, Boosting und sogar Sortierung (wenn Sie sicherstellen können, dass es höchstens einen Wert pro Dokument gibt). Die extrahierten Werte werden für diesen Feldtyp aufgrund der Art und Weise, insbesondere des Zeitpunkts der Extraktion, nicht gespeichert. Wenn diese Felder so konfiguriert wären, dass sie gespeichert werden, würden sie den vollständigen Inhaltstext enthalten, da der Inhalt mit einer copyField-Anweisung in sie kopiert wurde. Die anderen Feldtypen (Links und extracted_locations) werden gespeichert, d.h. die extrahierten Werte sind abrufbare Werte im Dokument selbst. Diese Werte werden in einem Skript vor dem Analyse-/Indexierungsprozess extrahiert und können daher gespeichert werden. Der Unterschied zwischen gespeichert und indiziert kann verwirrend sein, daher wurden beide Stile bereitgestellt, um diesen Unterschied deutlich zu machen.
Akronym-Extraktion
In den meisten Bereichen werden wir heutzutage mit TLAs (Akronyme mit drei Buchstaben, „XML dies, I.B.M. das“) überschwemmt. Es kann praktisch sein, diese als Facetten herauszuziehen und es den Benutzern zu ermöglichen, einfach zu den Dokumenten zu navigieren, die die für sie interessanten Begriffe enthalten.
In Solr 4.4 wurde ein neuer TokenFilter eingeführt, der die Indizierung von Textabschnitten, die einem Muster entsprechen, erleichtert. Hier ist die für dieses Beispiel verwendete Konfiguration:
<field name="acronyms" type="caps" indexed="true" stored="false" multiValued="true"/> <copyField source="content" dest="acronyms"/> <fieldType name="caps" class="solr.TextField" sortMissingLast="true" omitNorms="true"> <analyzer> <tokenizer class="solr.KeywordTokenizerFactory"/> <filter class="solr.PatternCaptureGroupFilterFactory" pattern="((?:[A-Z].?){3,})" preserve_original="false" /> </analyzer> </fieldType>
Wenn Sie den obigen Beispieltext über den Feldtyp „caps“ senden, wird nur das Token „CHO“ ausgegeben, das Sie oben in der Solr-Antwort für die Facette acronyms als „acronyms“:[„CHO“,1] sehen können.
Schlüsselsätze
Wenn Sie eine ausgewählte Liste spezieller Begriffe oder Phrasen für Ihre Domain haben, die Sie in Facetten umwandeln und leicht nach den Dokumenten filtern möchten, die sie enthalten, ist die Technik in diesem Abschnitt genau das Richtige für Sie.
Hier sind unsere Schemaergänzungen, die einen Feldtyp, ein entsprechendes Feld und eine Direktive zum Kopieren des Dokumentinhalts in das neue Feld key_phrases hinzufügen:
<field name="key_phrases" type="key_phrases" indexed="true" stored="false" multiValued="true"/> <copyField source="content" dest="key_phrases"/> <fieldType name="key_phrases" class="solr.TextField" sortMissingLast="true" omitNorms="true"> <analyzer> <tokenizer class="solr.WhitespaceTokenizerFactory"/> <filter class="solr.ShingleFilterFactory" minShingleSize="2" maxShingleSize="5" outputUnigramsIfNoShingles="true" /> <filter class="solr.KeepWordFilterFactory" words="keep_phrases.txt" ignoreCase="true"/> <filter class="solr.LowerCaseFilterFactory"/> </analyzer> </fieldType>
Die Datei conf/keep_phrases.txt enthält:
airport restaurant toy store
Die Konfiguration für die Feldtypanalyse verwendet einen einfachen Tokenizer mit Leerzeichen; möglicherweise müssen Sie diesen an Ihren Inhalt anpassen. Ein Shingle-Filter wird verwendet, um in diesem Fall 2 bis 5 Token zu einem einzigen Token zusammenzufassen (standardmäßig durch ein einzelnes Leerzeichen getrennt). Die maxShingleSize sollte so groß sein wie die größte Anzahl von Wörtern in einer einzelnen Phrase. Der Filter „Wort behalten“ lässt nur Token durch, die mit den Phrasen in der Datei keep_phrases.txt übereinstimmen.
Nachdem Sie den Beispieltext durch diesen Analyzer laufen ließen,
existiert diese Facette: „key_phrases“:[„airport“,1]. Damit können Sie direkt nach Dokumenten suchen, die das Wort „airport“ enthalten, oder nach anderen Schlüsselwörtern, die in den indizierten Dokumenten enthalten waren.
Extrahierte Orte
Wir könnten die gleiche Art von Token-Extraktion/Normalisierung zur Indexzeit durchführen, wie wir sie bei den Akronymen und Schlüsselwörtern gesehen haben. Damit erhalten wir aber nur einen reinen Textwert als Ausgabe einer TextField-Analysekette und haben nicht die Vorteile eines gespeicherten Wertes für die Benutzeroberfläche. Es mag Anwendungsfälle geben, in denen dies ein guter Weg zur Indizierung ist, aber wahrscheinlich wollen Sie die aus einem beliebigen Text extrahierten Längen- und Breitengrade als formalen, abrufbaren, filterbaren Geodatenfeldtyp an das Dokument anhängen. Die räumliche Suche ist ein komplexes Thema und es gibt viele Möglichkeiten für eine Vielzahl von Anwendungsfällen, von räumlicher Nähe („Sir, geben Sie mir eine Pizza in der Nähe meines Hauses“) bis hin zu komplexen Polygonschnitten, Facettierung nach Entfernungsbereichen, Gewichtung oder Sortierung nach Nähe und dergleichen. Weitere Informationen finden Sie in David Smileys Vortrag auf der Lucene Revolution ’13 und im Lucene/Solr 4 Spatial-Wiki.
Um einen Textstring in einen Rich Field-Typ zu verwandeln, muss er vom Indexer* stammen. Mit Indexer ist hier alles gemeint, was von Ihrem Anwendungscode oder Konnektor stammt, der Dokumente bis zum Ende der Aktualisierungsprozessorkette innerhalb der Aktualisierungsanforderungsbearbeitung von Solr (der RunUpdateProcessorFactory, um genau zu sein) einreicht. Die Schritte der Feldanalyse, einschließlich der oben genannten Tricks PatternCaptureGroupFilter und KeepWordFilter, werden in diesem letzten Aktualisierungsprozessor durchgeführt. Die gespeicherten Werte des Feldes werden entweder aus den übermittelten Dokumenten definiert, durch copyField-Schemadefinitionen erzeugt oder durch vorherige Aktualisierungsverarbeitungsschritte hergestellt/verändert.
* Es spricht nichts dagegen, die Ergebnisse einer Textanalysekette, wie sie Lucene’s TokenStream (Link bereitstellen) bietet, in jeder Phase zu generieren. Dies ist ein etwas fortgeschritteneres Thema, aber keine unvernünftige oder unerhörte Technik.
Es gibt einen raffinierten Skript-Aktualisierungsprozessor, mit dem wir ein winziges Stückchen JavaScript schreiben können. In der Solr 4.x-Auslieferungskonfiguration example/ collection1 kann ein Skelett update-script.js aktiviert werden, indem Sie einen kleinen Teil der solrconfig.xml auskommentieren. Das sieht dann etwa so aus:
<updateRequestProcessorChain name="script"> <processor class="solr.StatelessScriptUpdateProcessorFactory"> <str name="script">update-script.js</str> <lst name="params"> <str name="config_param">example config parameter</str> </lst> </processor> <processor class="solr.LogUpdateProcessorFactory"/> <processor class="solr.RunUpdateProcessorFactory"/> </updateRequestProcessorChain>
Die Definition des Feldes extracted_locations lautet:
<field name="extracted_locations" type="location_rpt" indexed="true" stored="true" multiValued="true"/>
wobei location_rpt ein rekursiver Präfixbaumfeldtyp ist, der bereits im Beispielschema definiert wurde.
Die neue updateRequestProcessorChain mit dem Namen „script“ ist nicht auf den Standardwert eingestellt, so dass der Indizierungspfad diese Update-Request-Prozessorkette speziell aufrufen muss. Ich habe das „einfache Post-Tool“ von Solr (in exampledocs/ post.jar) wie folgt verwendet:
java -Dauto -Dparams=update.chain=script -jar post.jar gocho.txt
Und die update-script.js:
"use strict"; function getMatches(regex, value) { var matches = []; var captures = regex.exec(value); if (captures != null) { for (i = 1; i < captures.length; i++) { // skip captures[0], as it's the whole string matches.push(captures[i]); } } return matches; } function processAdd(cmd) { doc = cmd.solrDoc; // org.apache.solr.common.SolrInputDocument id = doc.getFieldValue("id"); logger.info("update-script#processAdd: id=" + id); var content = doc.getFieldValue("content"); // Comes from /update/extract // very basic lat/long pattern matching this kind of stuff "38.1384683,-78.4527887" var location_regexp = /(-?d{1,2}.d{2,7},-?d{1,3}.d{2,7})/g; doc.setField("extracted_locations", getMatches(location_regexp, content)); // basic url pattern, http(s) protocol up through URL path, excluding query string params var links_regexp = /(https?://[a-zA-Z-_0-9.]+(?:/[a-zA-Z-_0-9.]+)*/?)/g; doc.setField("links", getMatches(links_regexp, content)); } // The functions below must be defined, but there's rarely a need to implement // anything in these. function processDelete(cmd) { // no-op } function processMergeIndexes(cmd) { // no-op } function processCommit(cmd) { // no-op } function processRollback(cmd) { // no-op } function finish() { // no-op }
Das Dokument enthält nach der Indizierung des Beispieldokuments ein indiziertes, mehrwertiges, raumbezogenes Punktfeld: „extracted_locations“:[„38.1384683,-78.4527887“]
Da dieses Feld gespeichert und indiziert wird, ermöglicht es nicht nur die Abfrage auf Dokumentenebene, sondern auch die geografische Filterung mithilfe der räumlichen Funktionen.
Links
In unserem Beispielschema ist bereits ein Feld „Links“ definiert, das wir hier einfach übernehmen werden. Es ist wie folgt definiert:
<field name="links" type="string" indexed="true" stored="true" multiValued="true"/>
In der processAdd-Methode von update-script.js fügen wir dies hinzu:
// basic url pattern, http(s) protocol up through URL path, //excluding query string params var links_regexp = /(https?://[a-zA-Z-_0-9.]+(?:/[a-zA-Z-_0-9.]+)*/?)/g; doc.setField("links", getMatches(links_regexp, content));
Nach der Indizierung mit diesem zusätzlichen Schritt enthält unser Dokument ein mehrwertiges, gespeichertes (und sogar indiziertes und damit durchsuchbares) Links-Feld: „links“:[„http://www.lat-long.com/Latitude-Longitude-1480221-Virginia-Charlottesville_Albemarle_Airport.html“].
Ein anderer Ansatz zum Extrahieren von Links (und E-Mail-Adressen)
Lucene/Solr verfügt über einen Tokenizer, der nicht nur vollständige URLs, sondern auch E-Mail-Adressen erkennt. Außerdem gibt es einen Token-Filter, der gut funktioniert und nur bestimmte Token-Typen durchlässt. Wenn Sie nur E-Mail-Adressen oder URLs durchsuchbar und facettierbar machen möchten, aber keine gespeicherten Dokumentwerte, können Sie dieselben Indexanalysetechniken verwenden, die in den vorherigen Abschnitten beschrieben wurden.
Hier finden Sie zwei Feldtypen, die unabhängig voneinander E-Mail-Adressen und URLs auslesen.
<fieldType name="emails" class="solr.TextField" sortMissingLast="true" omitNorms="true"> <analyzer> <tokenizer class="solr.UAX29URLEmailTokenizerFactory"/> <filter class="solr.TypeTokenFilterFactory" types="email_type.txt" useWhitelist="true"/> </analyzer> </fieldType> <fieldType name="urls" class="solr.TextField" sortMissingLast="true" omitNorms="true"> <analyzer> <tokenizer class="solr.UAX29URLEmailTokenizerFactory"/> <filter class="solr.TypeTokenFilterFactory" types="url_type.txt" useWhitelist="true"/> </analyzer> </fieldType>
Dabei enthält email_type.txt <EMAIL> und url_typ
e.txt enthält <URL>, beide wörtlich.
Es gibt einen entscheidenden Unterschied zwischen den beiden Ansätzen. Bei dem einen wird ein Update-Prozessor-Skript verwendet, um Links zu extrahieren, bei dem anderen werden Analyse-Tokenisierung und Filtertechniken eingesetzt. Was sind die gespeicherten Werte des Feldes? Gespeicherte Werte sind direkt mit einem Dokument verknüpft und können direkt als Teil eines Dokuments abgerufen werden. Bei beiden Ansätzen können die indizierten Begriffe identisch gemacht werden, so dass der Hauptunterschied nur in den gespeicherten Werten besteht, so dass die Wahl von den Bedürfnissen Ihrer Anwendung abhängt.
Übungen für den Leser
An den gezeigten Beispielen lassen sich viele Verbesserungen vornehmen, z.B. das Weglassen von Akronymen, die keinen Mehrwert für Ihre Anwendung darstellen (WTF?!), und das Einfügen von Synonymen für Schlüsselbegriffe. Versuchen Sie selbst, diese Feinheiten hinzuzufügen, und nutzen Sie die Solr-Analyse-Wikiseite für Tipps.
Was ist mit der UIMA?
Solr enthält die UIMA-Integration. UIMA (Unstructured Information Management Architecture) ist ein hochentwickeltes Text-Pipelining-Tool, das Text mit Merkmalen oder Entitäten annotiert. UIMA selbst ist ein Framework, in das Annotatoren integriert werden müssen. Die Solr-Integration ist komplex, und offen gesagt ist diese Art der schwereren Verarbeitung oft am besten in einer anderen Phase der Dokumentenverarbeitung durchzuführen, bevor sie an Solr gesendet wird. Dieser Artikel befasst sich nicht mit den Fähigkeiten von Solr/UIMA.
Zusammenfassung
Es gibt eine Reihe von Tools, sowohl kommerzielle als auch Open-Source-Tools, die ausgefeilte Funktionen zur Entitätsextraktion bieten. Aber bevor Sie die Komplexität erhöhen, sollten Sie zunächst prüfen, ob nicht einige einfache Tricks wie die hier gezeigten ausreichen. Mir geht es darum, pragmatisch zu sein und nicht zu viele Lösungen zu entwickeln. Tun Sie das Einfachste, was möglich ist und funktionieren könnte., aber nicht noch einfacher! Ich konnte zwar einige Anforderungen unserer Kunden mit den hier gezeigten Techniken erfüllen, aber es gab auch einige komplexere Anforderungen an die Extraktion von Entitäten durch maschinelles Lernen, die andere Ansätze erforderten.
Dies sind nur einige Beispiele. Es gibt viele Möglichkeiten, mit den integrierten Analysekomponenten und dem Update-Time-Scripting interessante Dinge zu erreichen.