Um Apache Lucene mal auszuprobieren, habe ich meine Blog-Software, die auch in Java geschrieben ist, um eine Volltextsuche erweitert. Dies war mit recht wenig Handgriffen erledigt. Daher gibts jetzt hier ein paar Code-Snippets, um eine minimale Volltextsuche mit Apache Lucene zu implementieren.
Maven-Dependencies:
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>8.3.1</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>8.3.1</version>
</dependency>
Zunächst benötigt Lucene ein Directory. Dies ist eine abstrakte Klasse für den Zugriff auf den Datenspeicher, den Lucene benutzt. Hier kann man sich dann für eine Implementierung entscheiden, z.B. FSDirectory, wenn Lucene seine Indexdaten im Dateisystem speichern soll.
Directory fsIndex;
...
fsIndex = FSDirectory.open(new File("/var/blogindex/").toPath());
Benötigt wird auch ein Analyzer, der benutzt wird, um Text zu analysieren und in einzelne Tokens zu teilen. Hier habe ich den StandardAnalyzer benutzt.
StandardAnalyzer analyzer;
...
analyzer = new StandardAnalyzer();
Jetzt ist die grundlegende Initialisierung auch schon abgeschlossen und wir können Dokumente zum Index hinzufügen. Meine Blog-Software hat eine Klasse Article
mit den Gettern getTitle()
, getTags()
, getContent()
und getID()
. Diese drei Attribute möchten wir auch als Felder in einem Lucene-Document speichern. Hierfür benötigen wir zunächst einen IndexWriter.
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
IndexWriter writer = new IndexWriter(fsIndex, indexWriterConfig);
Dann erstellen wir ein Lucene Document.
Document document = new Document();
document.add(new TextField("title", article.getTitle(), Field.Store.YES));
document.add(new TextField("tags", article.getTags(), Field.Store.YES));
document.add(new TextField("content", article.getContent(), Field.Store.NO));
document.add(new StringField("id", Integer.toString(article.getID()), Field.Store.YES));
Mit Field.Store.YES
kann man angeben, dass der Originalwert im Index gespeichert werden soll. Bei Field.Store.NO
wird dieser nicht gespeichert. Des Weiteren nutze ich hier auch verschiedene Feld-Typen. Der Unterschied zwischen TextField und StringField ist, dass beim TextField der Inhalt analysiert, also tokenized wird. Beim Inhalt des Blog-Artikels ist dies natürlich wichtig, bei der ID hingegen will man das Gegenteil. Die ID brauchen wir zum Einen, um aus dem Lucene-Index dann den passenden Artikel in der SQL-Datenbank zu finden, zum anderen muss die ID aber auch in Lucene indiziert sein, damit wir Documents aus dem Lucene-Index entfernen oder updaten können.
Es gibt noch jede Menge andere Feld-Typen, z.B. für numerische Werte, oder Werte, die nicht indiziert, aber gespeichert werden sollen. Siehe Field.
Jetzt wo wir das Document erstellt haben, kann dies zum Index hinzugefügt werden. Hierfür gibt es die IndexWriter-Methode addDocument()
. Ich benutze hingegen updateDocument()
, die das Dokument vorher löscht, falls es vorhanden ist. Dies ist wichtig, da beim Update von Blog-Artikeln somit im Lucene-Index nicht mehrere Documents für den Artikel gespeichert werden.
writer.updateDocument(new Term("id", Integer.toString(article.getID())), document);
Abschließend ist es wichtig, die Methode commit()
des IndexWriter aufzurufen. Ebenfalls können wir die close()
Methode aufrufen, da wir mit dem Indizieren eines Artikels fertig sind.
writer.commit();
writer.close();
Nun können wir uns noch Anschauen, wie man den Index durchsuchen kann. Hierfür benötigen wir einen IndexReader und IndexSearcher.
IndexReader indexReader = DirectoryReader.open(fsIndex);
IndexSearcher searcher = new IndexSearcher(indexReader);
Außerdem wird ein Query für die Suche benötigt. Hierfür gibt es unzählige Möglichkeiten, diesen zu erstellen. Ich habe mich für MultiFieldQueryParser entschieden, da dieser einen Query erstellen kann, der mehrere Felder durchsuchen kann. Diese Klasse gehört nicht mehr zur Lucene Core Lib, sondern zur Queryparser Lib.
String[] fields = {"title", "tags", "content"};
Query query = new MultiFieldQueryParser(fields, analyzer).parse(queryString);
Jetzt können wir mit dem Searcher und dem Query Dokumente suchen:
TopDocs topDocs = searcher.search(query, numResults);
for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
Document doc = searcher.doc(scoreDoc.doc);
// TODO: get fields from doc
}
indexReader.close();
Aus dem Document kann man dann die gespeicherten Werte der Fields wieder rausholen.
Die Suchfunktion lässt sich hier im Blog jetzt nutzen, in dem ein Parameter q=query an die URL angehängt wird. Ein Beispiel. Ist noch nicht besonders schön, aber das war jetzt auch nur schnell ran gefrickelt und ich habe eh vor, die Blogsoftware demnächst komplett neu zu schreiben.