Positionaler Term-Vektor-Abgleich in Lucene
Lassen Sie uns ein wenig mehr über positionale Term-Vektor-Matches lernen. Von Zeit zu Zeit stellen Benutzer auf der Lucene-Mailingliste eine…
Lassen Sie uns ein wenig mehr über positionale Term-Vektor-Matches lernen. Von Zeit zu Zeit stellen Benutzer auf der Lucene-Mailingliste eine Variante der folgenden Frage:
Was ist der beste Weg, um ein Fenster von Wörtern rund um einen übereinstimmenden Begriff in einem Dokument zu erhalten?
Ein Wortfenster um einen Treffer herum zu erhalten, kann für viele Dinge nützlich sein, unter anderem:
- Hervorhebung (obwohl ich dafür das Lucene-Paket Highlighter empfehlen würde)
- Co-occurrence-Analyse
- Stimmungsanalyse
- Beantwortung von Fragen
Da invertierte Indizes so strukturiert sind, ist das Abrufen von Inhalten rund um eine Übereinstimmung leider nicht effizient, ohne dass bei der Indizierung zusätzliche Arbeit anfällt. In Lucene beinhaltet diese „zusätzliche Arbeit“ die Erstellung und Speicherung von Termvektoren mit Positions- und Offset-Informationen.
Das Speichern von Termvektor-Informationen kann durch Hinzufügen des entsprechenden Codes während der Feldkonstruktion erfolgen, wie im folgenden Indizierungsbeispiel, in dem ich einen Index aus ein paar Dummy-Dokumenten erstelle (der vollständige Code befindet sich am Ende dieses Beitrags):
RAMDirectory ramDir = new RAMDirectory(); //Index some made up content IndexWriter writer = new IndexWriter(ramDir, new StandardAnalyzer(), true, IndexWriter.MaxFieldLength.UNLIMITED); for (int i = 0; i < DOCS.length; i++){ Document doc = new Document(); Field id = new Field("id", "doc_" + i, Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS); doc.add(id); //Store both position and offset information Field text = new Field("content", DOCS[i], Field.Store.NO, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS); doc.add(text); writer.addDocument(doc); } writer.close();
Beachten Sie die Verwendung von Field.TermVector.WITH_POSITIONS_OFFSETS bei der Erstellung des Textfelds. Damit wird Lucene angewiesen, Termvektorinformationen pro Dokument (also nicht invertiert) mit Positions- und Offsetinformationen zu speichern. (Beachten Sie bitte, dass auch andere Speicheroptionen verfügbar sind, siehe Field.TermVector. Beachten Sie auch, dass das Speichern von Termvektoren Speicherplatz kostet).
Der Vollständigkeit halber: Das DOCS-Array sieht wie folgt aus:
public static String [] DOCS = { "The quick red fox jumped over the lazy brown dogs.", "Mary had a little lamb whose fleece was white as snow.", "Moby Dick is a story of a whale and a man obsessed.", "The robber wore a black fleece jacket and a baseball cap.", "The English Springer Spaniel is the best of all dogs." };
Nun, da wir einen Index erstellt haben, müssen wir eine Suche durchführen. In unserem Fall müssen wir eine positionsbasierte Suche durchführen, im Gegensatz zu der eher traditionellen dokumentenbasierten Suche. Mit anderen Worten, es reicht nicht aus, einfach nur zu wissen, ob ein Begriff in einem Dokument vorkommt oder nicht (denken Sie an TermQuery), wir müssen wissen, wo im Dokument die Übereinstimmung aufgetreten ist. Lucene ermöglicht die positionsbasierte Suche durch eine Reihe von Abfrageklassen, die zusammen als SpanQueries bekannt sind. (Siehe SpanQuery und seine Derivate im Paket org.apache.lucene.search.spans ).
Auch hier ist ein Beispiel angebracht. Nehmen wir an, wir möchten herausfinden, wo der Begriff „Vlies“ vorkommt. In diesem Fall beginnen wir mit einer „normalen“ Suche, bei der wir eine Abfrage an den Index stellen und die Dcoument id und Score ausdrucken:
IndexSearcher searcher = new IndexSearcher(ramDir); // Do a search using SpanQuery SpanTermQuery fleeceQ = new SpanTermQuery(new Term("content", "fleece")); TopDocs results = searcher.search(fleeceQ, 10); for (int i = 0; i < results.scoreDocs.length; i++) { ScoreDoc scoreDoc = results.scoreDocs[i]; System.out.println("Score Doc: " + scoreDoc); }
Dieser Code sieht ziemlich genau so aus wie der Code einer einfachen Suche, mit der Ausnahme, dass ich eine SpanTermQuery anstelle einer TermQuery verwendet habe. In der Tat ist das bis jetzt nicht besonders interessant und wahrscheinlich auch langsamer als die vergleichbare TermQuery.
Was macht es interessant? Wenn Sie sich die SpanQuery API ansehen, werden Sie eine Methode namens getSpans() bemerken. Die Methode getSpans() liefert Positionsinformationen darüber, wo eine Übereinstimmung aufgetreten ist. Um die Positionsinformationen auszudrucken, könnte man also Folgendes tun:
Spans spans = fleeceQ.getSpans(searcher.getIndexReader()); while (spans.next() == true){ System.out.println("Doc: " + spans.doc() + " Start: " + spans.start() + " End: " + spans.end()); }
Beachten Sie zunächst, dass das Abrufen der Spans völlig unabhängig von der Ausführung der eigentlichen Abfrage ist. Tatsächlich müssen Sie die Abfrage nicht zuerst ausführen. Zweitens sind die Start- und Endwerte die Positionen der Token, nicht die Offsets.
Angesichts der Positionsinformationen stellt sich nun die Frage, wie wir nur diese Token um das Spiel herum bekommen. Um diese Frage zu beantworten, brauchen wir ein paar Dinge:
- Die Angabe eines Fensters in Form von Positionen. Ich möchte zum Beispiel, dass die Begriffe innerhalb von zwei Positionen vom Anfang und Ende der Spanne liegen.
- Eine TermVectorMapper-Implementierung, die sowohl das Fenster als auch die Position kennt. Stellen Sie sich einen TermVectorMapper als das Äquivalent eines SAX-Parsers für die Termvektoren von Lucene vor. Anstatt die Datenstruktur zu übernehmen (wie DOM), bietet er Call Backs und überlässt es Ihnen, dem Programmierer, die Datenstrukturen zu bestimmen. Sehen Sie sich den PositionBasedTermVectorMapper für eine nützliche Implementierung an.
Als schnellen Hack (der keineswegs Produktionsqualität hat) habe ich den folgenden Code erstellt, der den obigen Druckcode abändert:
WindowTermVectorMapper tvm = new WindowTermVectorMapper(); int window = 2;//get the words within two of the match, inclusive of the boundaries while (spans.next() == true) { System.out.println("Doc: " + spans.doc() + " Start: " + spans.start() + " End: " + spans.end()); //build up the window tvm.start = spans.start() - window; tvm.end = spans.end() + window; reader.getTermFreqVector(spans.doc(), "content", tvm); for (WindowEntry entry : tvm.entries.values()) { System.out.println("Entry: " + entry); } //clear out the entries for the next round tvm.entries.clear(); }
In diesem Teil des Codes erstelle ich zunächst einen WindowTermVectorMapper (WTVM, schöner Name, oder?) und teile dem WTVM dann in der Spans-Schleife mit, wie mein Fenster aussieht. Als Nächstes frage ich den IndexReader von Lucene nach dem TermVector und übergebe meinen TermVectorMapper. Schließlich gebe ich die Einträge aus.
Die letzte nützliche Information ist natürlich, wie die WTVM aussieht. Hier ist der nützlichste Codeschnipsel:
public void map(String term, int frequency, TermVectorOffsetInfo[] offsets, int[] positions) { for (int i = 0; i < positions.length; i++) {//unfortunately, we still have to loop over the positions //we'll make this inclusive of the boundaries if (positions[i] >= start && positions[i] < end){ WindowEntry entry = entries.get(term); if (entry == null) { entry = new WindowEntry(term); entries.put(term, entry); } entry.positions.add(positions[i]); } } }
Wie Sie sehen, schaue ich mir nur die Positionen an und überprüfe, ob der aktuelle Term einen Eintrag hat, der innerhalb der Start- und Endzeitpunkte liegt. Natürlich können Sie hier noch mehr interessante Dinge tun, aber das überlasse ich Ihnen. Außerdem gibt es in der Lucene-Distribution ein paar TermVectorMapper-Implementierungen, die Sie als Beispiele verwenden können.
Das war’s dann auch schon. Von hier aus können Sie sich leicht verschiedene Möglichkeiten vorstellen, wie Sie die vom Term Vector Mapper zurückgegebenen Informationen nutzen können, um Informationen über die Terme in einem Fenster zu verarbeiten.
Den vollständigen Code finden Sie unten. Er ist nur für Demonstrationszwecke gedacht. Bitte beachten Sie die Haftungsausschlüsse, etc.
package com.lucidimagination.noodles; /** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import org.apache.lucene.store.RAMDirectory; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.Term; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.TermVectorMapper; import org.apache.lucene.index.TermVectorOffsetInfo; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.spans.SpanTermQuery; import org.apache.lucene.search.spans.Spans; import java.io.IOException; import java.util.LinkedHashMap; import java.util.List; import java.util.ArrayList; /** * This class is for demonstration purposes only. No warranty, guarantee, etc. is implied. * * This is not production quality code! * * **/ public class TermVectorFun { public static String[] DOCS = { "The quick red fox jumped over the lazy brown dogs.", "Mary had a little lamb whose fleece was white as snow.", "Moby Dick is a story of a whale and a man obsessed.", "The robber wore a black fleece jacket and a baseball cap.", "The English Springer Spaniel is the best of all dogs." }; public static void main(String[] args) throws IOException { RAMDirectory ramDir = new RAMDirectory(); //Index some made up content IndexWriter writer = new IndexWriter(ramDir, new StandardAnalyzer(), true, IndexWriter.MaxFieldLength.UNLIMITED); for (int i = 0; i < DOCS.length; i++) { Document doc = new Document(); Field id = new Field("id", "doc_" + i, Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS); doc.add(id); //Store both position and offset information Field text = new Field("content", DOCS[i], Field.Store.NO, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS); doc.add(text); writer.addDocument(doc); } writer.close(); //Get a searcher IndexSearcher searcher = new IndexSearcher(ramDir); // Do a search using SpanQuery SpanTermQuery fleeceQ = new SpanTermQuery(new Term("content", "fleece")); TopDocs results = searcher.search(fleeceQ, 10); for (int i = 0; i < results.scoreDocs.length; i++) { ScoreDoc scoreDoc = results.scoreDocs[i]; System.out.println("Score Doc: " + scoreDoc); } IndexReader reader = searcher.getIndexReader(); Spans spans = fleeceQ.getSpans(reader); WindowTermVectorMapper tvm = new WindowTermVectorMapper(); int window = 2;//get the words within two of the match while (spans.next() == true) { System.out.println("Doc: " + spans.doc() + " Start: " + spans.start() + " End: " + spans.end()); //build up the window tvm.start = spans.start() - window; tvm.end = spans.end() + window; reader.getTermFreqVector(spans.doc(), "content", tvm); for (WindowEntry entry : tvm.entries.values()) { System.out.println("Entry: " + entry); } //clear out the entries for the next round tvm.entries.clear(); } } } //Not thread-safe class WindowTermVectorMapper extends TermVectorMapper { int start; int end; LinkedHashMap entries = new LinkedHashMap(); public void map(String term, int frequency, TermVectorOffsetInfo[] offsets, int[] positions) { for (int i = 0; i < positions.length; i++) {//unfortunately, we still have to loop over the positions //we'll make this inclusive of the boundaries if (positions[i] >= start && positions[i] < end){ WindowEntry entry = entries.get(term); if (entry == null) { entry = new WindowEntry(term); entries.put(term, entry); } entry.positions.add(positions[i]); } } } public void setExpectations(String field, int numTerms, boolean storeOffsets, boolean storePositions) { // do nothing for this example //See also the PositionBasedTermVectorMapper. } } class WindowEntry{ String term; List positions = new ArrayList();//a term could appear more than once w/in a position WindowEntry(String term) { this.term = term; } @Override public String toString() { return "WindowEntry{" + "term='" + term + ''' + ", positions=" + positions + '}'; } }