In diesem Eintrag wollen wir demonstrieren, wie es mit einfachen Mitteln möglich ist, RSS-Parsing und Volltextsuche prototypisch umzusetzen.

Die Idee dafür entstand in dem Forschungsprojekt coolnavigate, zusammen mit der TU Dresden. Bei coolnavigate werden Strategien untersucht, die zum Erstellen adaptiver und verteilter Anwendungen im mobilen Web dienen. Wir von queo, als Industriepartner, waren zuständig für die Erstellung eines „Demonstrators in Form eines mobilen und kontextsensitiven Informationssystems für den Finanzmarkt“. Mit anderen Worten: wir verbinden die schnelllebigen Informationen des Internets, aus Blogs und RSS-Feeds, mit starren Daten aus dem Finanzsektor.

Doch wie lassen sich Informationen aus einer Vielzahl von Quellen möglichst effizient aber dennoch intuitiv finden? Einhundert Feeds für jede Anfrage live zu durchsuchen kann kaum der richtige Ansatz sein. Die Lösung: Crawling und Indexing. Zum Thema Crawling lassen sich nach kurzer Suche eine Vielzahl verschiedener Frameworks finden. Da wir uns speziell auf RSS-Feeds stützen wollen, entscheiden wir uns für Rome. Zur Content-Extraktion nutzen wir Boilerpipe. Als Indexer kommt Apache Solr zum Einsatz.

Apache Solr einrichten

Apache Solr kann auf verschiedene Arten eingesetzt werden: als Standalone, eingebettet in eine Anwendung oder als Applikation in einem Servlet-Container wie Tomcat. Wir entscheiden uns für Letzteres und deployen Solr im gleichen Tomcat wie unsere restlichen Anwendungen. Dies erleichtert uns automatisiertes Testen in isolierten Umgebungen. Für das Deployment muss lediglich das heruntergeladene Archiv entpackt und die Datei dist/solr-<version>.war dem Tomcat zur Verfügung gestellt werden (Deployment über das Tomcat-Web-Interface, Eclipse oder durch Kopieren der Datei in <tomcat>/webapps/solr.war). Außer dem WAR-Archiv benötigt Tomcat noch einige Bibliotheken, die in <solr>/example/lib/ext/ zu finden sind und ins <lib>-Verzeichnis von Tomcat kopiert werden müssen.

Alle Daten zur Indizierung werden von Solr in einem frei wählbaren Verzeichnis verwaltet. Wir nennen es hier $solr_home. Dieses Verzeichnis wird üblicherweise nicht per Hand angelegt, sondern ist eine Kopie von <solr>/example/solr/collection1. Den Pfad zu $solr_home liest Solr aus der System-Property solr.solr.home (-Dsolr.solr.home=“<solr_home>“), welches Tomcat in der Eclipse Run-Configuration oder direkt beim Starten mitgeteilt werden muss. Läuft der Server, so sollte Solr bereits jetzt auf http://localhost:8080/solr/ erreichbar sein.

Jeder Eintrag im Index von Solr wird Dokument genannt. Jedes Dokument hat genau definierte Felder, die über die Konfigurationsdatei schema.xml festgelegt wird. Das kopierte Verzeichnis enthält bereits eine solche Datei ($solr_home/collection1/conf/schema.xml), welche wir nur entsprechend anpassen müssen. Zunächst legen wir die Felder fest, die ein Dokument umfassen soll. In unserem Fall benötigen wir u.a. Informationen zur Quelle, Titel und Inhalt, sowie einen Link zu einem Bild. In der schema.xml fügen wir daher zwischen den Tags <fields> und </fields> folgendes ein:

<field name="source-id" type="string" indexed="true" stored="true" />
<field name="link" type="string" indexed="true" stored="true" multiValued="false" />
<field name="title" type="text" indexed="true" stored="true" />
<field name="description" type="text" indexed="true" stored="false" />
<field name="display_desc" type="html" indexed="false" stored="true" />
<field name="fulltext" type="text" indexed="true" stored="false" />
<field name="creator" type="string" indexed="true" stored="true" />
<field name="date" type="date" indexed="true" stored="true" />
<field name="img_url" type="string" indexed="false" stored="true" />
<field name="all_text" type="text" indexed="true" stored="false" multiValued="true" />

<copyField source="source-id" dest="all_text" />
<copyField source="link" dest="all_text" />
<copyField source="title" dest="all_text" />
<copyField source="description" dest="all_text" />
<copyField source="fulltext" dest="all_text" />

Man unterscheidet generell zwei Feldtypen: <field …> definiert ein Feld eines Dokuments, welches indiziert (uns somit gesucht) werden kann (indexed=false|true). Es kann außerdem gespeichert und bei einer Anfrage zurückgegeben werden (stored=false|true).

Der Feldtyp <copyField> kopiert den Wert eines Feldes in ein anderes Feld. Ist das Zielfeld mit dem Attribut multiValued=“true“ versehen, können mehrere Felder ihre Werte in einem neuen synthetischen Feld aggregieren. Somit können komplexe Suchanfragen, die sich auf mehrere Felder beziehen, deutlich vereinfacht werden. In unserem Fall erzeugen wir also ein Feld „all_text„, das die Werte von source-id, link, title usw. zusammenfasst, wodurch Clients nur eine Suchanfrage auf dieses Feld starten müssen.

Logistische Assistenz- und Optimierungssystemen
Mit unseren individuellen logistischen Assistenz- und Optimierungssystemen lassen sich Lieferketten auf Basis geeigneter Visualisierungsansätze transparent darstellen und somit Engpässe frühzeitig erkennen.

Die eigentliche Indizierung wird schließlich über den Feld-Typen (Attribut type) konfiguriert. Primitive Typen wie date, integer und double sind bereits vordefiniert. Besondere Aufmerksamkeit gilt komplexen Typen wie text. Die Indizierung erfolgt generell in mehreren Schritten: erst wird der Feldinhalt in Token aufgetrennt, meistens an Leerzeichen. Danach werden unnötige Interpunktion, sowie Stoppwörter entfernt. Eine Liste mit Stoppwörtern lässt sich für verschiedene Sprachen leicht finden oder selbst erstellen. Es werden noch eine ganze Reihe weiterer Filter angewandt, um die Speicherung und Suche möglichst effizient zu gestalten. Wer will, kann sogar eigene Filter implementieren und dort anwenden.

Im Feldtyp wird nicht nur die Indizierung konfiguriert, sondern auch, wie Anfragen bearbeitet werden (<analyser type=“query“>). Schließlich wollen wir nicht nur nach genau einem Wort suchen. Analog zur Indizierung werden Wörter wieder getrennt und verschiedene Filter benutz, z.B. Stoppwort- und Interpunktionsentfernung. Dazu kommt nun noch ein Synonymfilter, der die Anfrage mit bekannten Synonymen der Suchwörter anreichert und die Suche weiter verbessert.

<fieldType name="text" class="solr.TextField">

    <analyzer type="index">
        <tokenizer class="solr.WhitespaceTokenizerFactory"/>
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords_en.txt" enablePositionIncrements="true" />
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords_de.txt" enablePositionIncrements="true" />
    </analyzer>

    <analyzer type="query">
        <tokenizer class="solr.WhitespaceTokenizerFactory"/>
        <filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
    </analyzer>

</fieldType>

Kommunikation mit Solr

Solr bietet standardmäßig eine REST-Schnittstelle zum Indizieren und Suchen indizierter Daten mittels JSON, XML und sogar CSV. Leicht lassen sich, mit ein paar einfachen Anfragen, Daten hinzufügen und wieder suchen. Doch wir gehen gleich eine Ebene höher und nutzen mittels Spring Data Solr eine Abstraktion dieser Schnittstelle. Dazu müssen wir zunächst in einer Klasse (RssDocument) das Mapping zum Solr-Dokument herstellen. Für alle benötigten Felder aus der schema.xml erstellen wir hierzu entsprechend annotierte Instanzvariablen, etwa so:

public class RssDocument implements RssDocumentFields {

@Field("source-id")
private String sourceId;

@Id
@Field("link")
private String link;
  ...
}

Dieses Mapping verwenden wir als Typ-Parameter in einem Subinterface des CrudRepository von Spring Data und injizieren es per Spring in einen beliebigen Service (bei uns RssDocIndexService). Das CrudRepository bietet eine intuitive Schnittstelle zum Ändern und Abrufen der Daten im Solr-Index. Das Feld annotiert mit @Id – das wir mit der URL des Feeds bestücken – wird zur Identifikation eines Dokumentes für alle Operationen verwendet.

Zur Konfiguration der Solr-Komponente von Spring Data muss die Verbindung zum Solr-Server, ein Template und das Repository entsprechend definiert werden. Das folgende Listing zeigt unseren Ausschnitt der applicationContext.xml. Die System-Property solr.baseurl muss der Anwendung beispielsweise über eine .properties-Datei verfügbar gemacht werden.

<beans xmlns="http://www.springframework.org/schema/beans"         
       xmlns:solr="http://www.springframework.org/schema/data/solr"
       xsi:schemaLocation="...
       http://www.springframework.org/schema/data/solr http://www.springframework.org/schema/data/solr/spring-solr-1.0.xsd
      ..." >

    <solr:solr-server id="solrServer" url="${solr.baseurl}"/>

    <bean id="solrTemplate" class="org.springframework.data.solr.core.SolrTemplate" scope="singleton">
        <constructor-arg ref="solrServer"/>
    </bean>

    <solr:repositories base-package="com.queomedia.coolnavigate.rss.service"/>

RSS-Parsing

Zum Abrufen und Interpretieren von RSS-Feeds gibt es eine Vielzahl von Bibliotheken, die wir hier nicht alle betrachten können. Wir haben uns für Rome entschieden, da es sehr schnell zu brauchbaren Ergebnissen führt und stabil läuft.

Das Parsen eines Feeds wird folgendermaßen durchgeführt, wobei die Variable url für die Feed-URL steht:

SyndFeedInput input = <strong>new</strong> SyndFeedInput();

SyndFeed feed = input.build(<strong>new</strong> XmlReader(<strong>new</strong> URL(url)));
<pre>

Das resultierende Objekt liefert eine Liste von Instanzen des Typs SyndEntry, die die eigentlichen Felder eines jeden Eintrages abbilden. Entsprechend unserer Vorbereitung passen die Felder von SyndEntry zu den Feldern der Klasse RssDocument, welche dann letztlich im Solr-Indexer landen werden.

Die meisten einfachen Textfelder, wie link, title, author usw. werden direkt übernommen. Besondere Aufmerksamkeit schenken wir description, da dieses Feld HTML enthalten kann. Wir verwenden dieses Feld gleich doppelt. Zunächst extrahieren wir den Inhalt als reinen Text zur Indizierung. Dazu bedienen wir uns einer weiteren Bibliothek, genannt boilerpipe. Diese scheint zwar nicht weiter gepflegt zu werden, genügt an dieser Stelle allerdings völlig unseren Anforderungen und liefert erstaunlich schnell und zuverlässig aus beliebigem HTML den eigentlichen Inhaltstext.

Wir finden individuelle Lösungen
Planungs- und Entscheidungsprozesse in Unternehmen basieren auf zahlreichen Daten und deren Gewichtung. Wir finden individuelle Lösungen, die Ihren speziellen Anforderungen entsprechen und binden bestehende Systeme optimal ein.

Der zweite Anwendungszweck von description ist die Anzeige. Um etwas Struktur zu behalten und es trotzdem nicht zur offenen Tür unserer Anwendung werden zu lassen, filtern wir in der Funktion cleanDisplayDescription alle Tags, bis auf <p> und <img> heraus.

Zu guter Letzt laden wir den Inhalt der Seite, auf die das Feld link verweist. Von dieser extrahieren wir wieder mit boilerpipe den Inhalt, um den Feed zusätzlich mit diesen Informationen indizieren zu können.

for (SyndEntry syndEntry : feed.getEntries()) {

    String parsedDescription = "";
    String displayDescription = "";
    final SyndContent description = entry.getDescription();
    
    if (description != null) {

        // Textinhalt mit boilerpipe extrahieren
        parsedDescription = ArticleExtractor.INSTANCE.

        getText(description.getValue());
        // HTML-tags filtern

        displayDescription = cleanDisplayDescription(description.getValue());
    }

    String extractedLinkText = extractLinkText(entry.getLink(), feedId);
    rssDocuments.add(new RssDocument(feedId,
        syndEntry.getLink(),
        syndEntry.getTitle(),
        parsedDescription,
        displayDescription,
        extractedLinkText,
        syndEntry.getAuthor(),
        syndEntry.getPublishedDate(),
        this.extractImageUrl(entry)));
}

Suchen von Dokumenten

Bisher haben wir folgendes behandelt: Index erstellen und Daten aktualisieren, parsen, sowie im Index ablegen. Aber wie kommen wir nun an diese Daten heran? Es gibt wie immer verschiedene Möglichkeiten. Statt uns mit dem Für und Wider einzelner Bibliotheken und Schnittstellen zu beschäftigen, wollen wir an dieser Stelle nur auf das Prinzip eingehen, wie eine Anfrage definiert wird. Sobald das Prinzip klar ist, sollte es sehr leicht sein die APIs von Apache Solr, Spring Data oder sogar das native REST-Interface zu verwenden.

Um eine Suchanfrage zu stellen, muss zunächst ein Query-Parser ausgewählt werden, der dann letztlich bestimmt, wie eine Suchanfrage interpretiert wird. Der Standard-Parser von Solr ist recht streng und wird in der Praxis kaum verwendet. Ein syntaktischer Fehler oder ein unerlaubtes Zeichen werfen schnell Exceptions. Populären Ersatz bieten dagegen die DisMax und Extended DisMax Parser. DisMax steht dabei für Disjunct Max, was einerseits bedeutet, dass die Suche auf mehrere Suchfelder ausgeweitet wird und andererseits – bei Treffern in mehreren Feldern – nur das Feld mit der höchsten Wertung in das Ergebnis einfließt. Solr umfasst noch weitere Parser, allein in der Dokumentation sind weitere 22 beschrieben. Dennoch ist der ExtendedDisMax (edismax)-Parser für uns das Mittel zur Suche.

Eine Anfrage ist eine Menge von Parameter-Wert-Paaren, was der ganzen Sache eine gewisse Flexibilität verleiht. Der einzige Pflichtparameter ist q, der die eigentlichen Suchbegriffe umfasst. Über eine Reihe von Standardparametern kann das Verhalten zum Sortieren, Paging oder Debugging eingestellt werden. Jede Parser-Implementierung stellt darüber hinaus eine Vielzahl eigener Parameter zur Suchkonfiguration bereit.

Für unseren Fall mit edismax nehmen wir folgende Einstellungen vor:

<ul>
	<li><strong>mm=2&lt;60% </strong>--&gt; minimale Anzahl von Suchbegriffe, die pro Dokument gefunden werden müssen. In unserer Konfiguration bedeutet das: für bis zu zwei Suchbegriffe müssen alle gefunden werden, bei drei und mehr wenigstens 60%.</li>
	<li><strong>qf=title^1.5 fulltext</strong> --&gt; Liste mit Feldnamen und Faktoren, um deren Gewichtung bei Suchtreffern zu beeinflussen. Dadurch wird der sogenannte Boost-Faktor verändert und Treffer im entsprechenden Feld werden höher oder niedriger gewichtet. In umserem Fall erhöhen wir die Gewichtung von Treffern im <em>title</em>-Feld.</li>
	<li><strong>pf=description</strong> --&gt; Bewirkt eine Anhebung der Ergebnisgewichtung, wenn alle Suchbegriffe im Feld <em>description </em>vorkommen und nahe beeinander stehen.</li>
</ul>

Der edismax-Parser unterstützt noch viele weitere Parameter, mit denen es möglich ist, die Suche auf jede Anwendung oder sogar jede Anfrage genau anzupassen. Die Solr-Dokumentation oder das Apache-Wiki liefern wie immer dazu alle nötigen Informationen.

Fazit und Verbesserungen

Dieser Blogeintrag sollte eine Möglichkeit zeigen, wie man durch die Verknüpfung verschiedener Java-Technologien einen einfachen RSS-Parser- und Indexer implementieren kann. Natürlich ist dies nur ein Weg und diente in unserem speziellen Fall zur Umsetzung eines Prototypen innerhalb eines Forschungsprojektes. Für Produktivsysteme sind eine Vielzahl weiterer Faktoren wie Sicherheit, Fehlertoleranz und Performance zu beachten, denen wir hier keinerlei Beachtung geschenkt haben.

Für unseren Prototypen genügte es beispielsweise, wenn alle RSS-Feeds in regelmäßigen Abständen geparst und im Index aktualisiert werden. Je größer die Anzahl der Feeds und deren Länge, desto aufwendiger und ineffizienter wird natürlich dieses naive Verfahren. Eine einfache Verbesserung wäre beispielsweise, beim Stellen der HTTP-Anfrage einen Header zur Filterung (z.B. if-modified-since) zu setzen. Damit kann der Server entscheiden, ob und welcher Inhalt sich geändert hat und antwortet mit einer verkürzten Antwort oder gar einem 403-Not Modified. Ob und wie dieses Vorgehen von unserer RSS-Bibliothek unterstützt wird entzieht sich leider meiner Kenntnis, kann aber vom geneigten Leser garantiert in kürzester Zeit ermittelt werden.

 

Diskutieren Sie mit!

Es steht Ihnen frei einen Kommentar zu hinterlassen. Sie finden Informationen zur Verwendung Ihrer Daten in unserer Datenschutzerklärung.

Alle mit einem markierten Felder sind Pflichtfelder und müssen ausgefüllt werden