Maßgeschneiderte Suche - JAXenter

Maßgeschneiderte Suche

Intelligente Suchanfragen

Eine Suchanfrage mit einem einzelnen Suchterm wie „Britney“ ist sicher ein häufiger Anwendungsfall. Für konkretere oder ungenauere Abfragen steht eine Vielzahl von Operatoren und Variationen inklusive Platzhalter zur Verfügung.

Suchanfrage Bedeutung
Moon OR Frank Alle Dokumente, in denen entweder „Moon'“ oder ‚“Frank“ (oder beides) vorkommt.
Moon AND Frank Alle Dokumente, in denen auf jeden Fall beide Stichwörter vorkommen.
„Frank Zappa“ Dokumente, in denen die beiden Wörter „Frank“ und „Zappa“ in dieser Reihenfolge direkt aufeinander folgen.
Zap* Alle Dokumente, in denen ein Wort vorkommt, das mit „Zap“ beginnt. Der Platzhalter darf nicht am Wortanfang „*ppa“ oder alleine stehen „*“.
title:Montreux Alle Dokumente, in denen das Wort „Montreux“ im Feld title vorkommt.
-title:Ship AND artist:Zappa Alle Dokumente, deren Feld „title“ nicht „Ship“ enthält, aber bei denen gleichzeitig im Feld „artist“ das Wort „Zappa“ vorkommt.
Zap?a Das Fragezeichen ist ein Platzhalter für genau ein Zeichen. Findet „Zappa“ und „Zapta“.
datum:[20081201 TO 20081231] Alle Dokumente, deren Feld „datum“ einen Wert zwischen 1.12. und 31.12.2008 hat.

Besonderes Augenmerk sei nun kurz auf jene Anfragen gelegt, bei denen vor dem Suchwort kein Feld (mit Doppelpunkt abgetrennt) angegeben wird, wie z. B. bei „Frank Zappa“. Wird hier etwa über alle Felder aller Dokumente gesucht? Die Antwort lautet: Nein. Es wird hier nur über ein einziges Feld gesucht. Welches das ist, wird von der Implementierung der Suche festgelegt. Erinnern wir uns an zwei entscheidende Stellen aus unserem Codebeispiel aus der letzten Folge:

Document document = new Document();
document.add(new Field("alltext", reader));
// ... Dokument im Index ablegen ...
// Suche wird vorbereitet
QueryParser queryParser = new QueryParser("alltext", new StandardAnalyzer());
Query query = queryParser.parse("stichwort");
TopDocs topDocs = indexSearcher.search(query, 10);
// ... Treffer ausgeben ...

Ein Dokument mit dem Feld alltext wird in den Index eingefügt. Anschließend wird ein Parser instantiiert, dem dieser Feldname als Voreinstellung mitgegeben wird. Überall dort, wo der Benutzer keinen Feldnamen angibt, wird der Parser nun alltext voraussetzen und nur dieses Feld durchsuchen. Konkret bedeutet das, dass der Ausdruck "title:Ship AND Zappa" vom QueryParser intern als "title:Ship AND alltext:Zappa" verarbeitet wird. In den meisten Anwendungsfällen bietet es sich also an, sämtliche Texte in einem besonderen Feld abzulegen, das als Überfeld fungiert, zusätzlich zu den dedizierten Feldern.

Document document = new Document();
// jeder Teil des Textes wird für sich indiziert...
document.add(new Field("title", title));
document.add(new Field("author", author));
document.add(new Field("chapters", chapters));
// ...und nochmal alle zusammen:
StringBuilder alltext = new StringBuilder().
alltext.append(title).append(" ");
alltext.append(author).append(" ");
alltext.append(chapters).append(" ");
document.add(new Field("alltext", alltext.toString()));
// ... Dokument im Index ablegen ...
Pimp my Suche

Nirgends steht geschrieben, dass die Suchanfrage des Nutzers ungefiltert an Lucene übergeben werden muss. Im Gegenteil: Je nach Anwendung kann es sinnvoll sein, die Anfrage nach der Eingabe durch den User und vor der Übergabe an Lucene anzureichern. Dies kann die Ergebnisliste wesentlich verbessern: Wer beim Stöbern im Webshop jeder Anfrage einen Term hinzufügt, welcher Treffer mit ausgelaufenen oder nicht lieferbaren Produkten ausschließt, tut sich und seinen Kunden einen Gefallen. Dies darf den Kunden gleichzeitig aber nicht bevormunden. Sucht er nach der Bedienungsanleitung eines ausgelaufenen Produkts, so sollte er diese nach wie vor finden können. Nachfolgend noch ein kurzes Beispiel für eine zielgruppenspezifische Suche.

Intranet oder Extranet?

Angenommen, Ihre Daten werden von einem internen Team genutzt. Diese dürfen mehr Informationen sehen und durchsuchen als User auf der Internetseite. Es gibt verschiedene Möglichkeiten, dies umzusetzen. Der einfachste Fall ist, dass einzelne Treffer nur bei einer intern abgeschickten Suche zurückgeliefert werden. Dies kann durch ein spezielles Sichtbarkeitsfeld gelöst werden. Bei der Internetsuche würde automatisch ein Term hinzugefügt, der über dieses Feld alle zugriffsbeschränkten Dokumente aussortiert:
"visibility:external"

Eine weitergehende Anwendung wäre es, jedem Dokument ein zusätzliches Indexfeld mitzugeben, in dessen Inhalt alle internen Daten aufgenommen werden. Die interne Suche würde dann zusätzlich über dieses Feld durchgeführt. Damit wird verhindert, dass Nutzer einen Treffer bekommen, bei dem sie keinen Rückschluss darauf ziehen können, warum gerade dieses Dokument so gut zu ihrer Suche passt. Sie können die betreffenden Daten einfach nicht einsehen.

Der Rahmen steht

In dieser Folge über Lucene wurde hauptsächlich anhand von Beispielen erklärt, welche Möglichkeiten es gibt, Lucene-Dokumente und -Suchanfragen zu variieren. Diese Vielseitigkeit ist gleichzeitig eine Crux. Es gibt kaum fertige allgemeine Rezepte, die jeden Anwendungsfall abdecken. Und Lucene Features wie Boost und Payload haben wir noch nicht einmal angerissen. Es ist aber hoffentlich klargeworden, dass der Schlüssel zum Erfolg in der richtigen Wahl der Felder liegt.

Die Lösung

Schuldig bin ich Lesern des ersten Teils noch die Antwort, wie viele Wörter dort mit „ein“ begannen oder sich so ähnlich anhörten wie „wann“. Die passende Suche dazu lautet:
"ein* OR wann~" Als Ergebnis liefert Lucene 53 Treffer. Es sei zugegeben, dass die Suche nach den genauen Treffern innerhalb eines Textes nicht der typische Anwendungsfall ist, aber er veranschaulicht sehr gut, wie Lucene die einzelnen Terme trifft.

Das Besondere an diesem Beispiel ist, dass zur Umsetzung der Quelltext in einzelne Worte zerlegt und jedes Wort als separates Dokument gespeichert wird, damit jeder Treffer als eigener Eintrag ins Ergebnis eingeht.

File indexBase = new File(... lokales Verzeichnis zum Ablegen des Index ...);
// Quelltext laden
URL url = new URL("http://www.brainlounge.de/publications/01_lucene_published.txt");
URLConnection urlCon = url.openConnection();
InputStreamReader reader = new InputStreamReader(urlCon.getInputStream());
IndexWriter indexWriter = new IndexWriter(indexBase, new StandardAnalyzer(),
					     true, IndexWriter.MaxFieldLength.LIMITED);
// Text in Worte zerlegen
LetterTokenizer tokenizer = new LetterTokenizer(reader);
Token token;
while ((token = tokenizer.next(new Token())) != null) {
    String term = token.term();
    // pro Document ein Wort, dieses wird gespeichert um es später 
    // wieder anzeigen zu können.  
    Document document = new Document();
    document.add(new Field("alltext", term, Field.Store.YES, Field.Index.ANALYZED));
    indexWriter.addDocument(document);
}
 reader.close();
indexWriter.commit();
indexWriter.close();
// Abfrage erzeugen und absetzen
QueryParser queryParser = new QueryParser("alltext", new StandardAnalyzer());
Query query = queryParser.parse("ein* OR wann~");
TopDocCollector collector = new TopDocCollector(200);
IndexSearcher indexSearcher = new IndexSearcher( 
		      FSDirectory.getDirectory(indexBase));
indexSearcher.search(query, collector);
System.out.println("Anzahl der gefundenen Treffer für '" + query.toString() + 
                   "': " + collector.getTotalHits());
// Ausgabe aller passenden Wörter
ScoreDoc[] hits = collector.topDocs().scoreDocs;
for (int i = 0; i 

In der nächsten Folge geht es um Apache Solr, einen kompletten Suchserver, der auf dem Lucene-Framework aufbaut.

Bernd Fondermann ist freiberuflicher Software-Architekt und Consultant in Frankfurt a. M. Er beschäftigt sich mit innovativen Open-Source-Technologien bei der Apache Software Foundation und ist derzeit PMC Chair und Vice President Apache Labs.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.