• Anleitung
  • 14. Februar 2018

Dokumentensuche mit Tika und Elasticsearch

Die Recherche in Dokumenten-Leaks und Akten sind eine Herausforderungen für Journalisten. Diese Anleitung soll dabei helfen, Dokumente mit Tika und Elasticsearch durchsuchbar zu machen.

Eine neue Herausforderung für Journalisten ist der Umgang mit Dokumentensammlungen, welche durch Leaks, Behörden-Anfragen oder Scraping in die Redaktionen kommen. Das stellt vor allem investigativ arbeitende Journalisten vor eine Herausforderung. Wer keine Zeit hat Aktenordner zu wälzen braucht Werkzeuge, um aus den oftmals unstrukturierten Dokumentensammlungen schlau zu werden.

Ich zeige euch in dieser Anleitung, wie man Text aus PDFs und anderen Dokumenten extrahiert und durchsuchbar macht. Dafür verwenden wir Apache Tika, Elasticsearch und Node.js.

Achtung: Diese Anleitung setzt ein solides technisches Grundwissen voraus und richtet sich an Fortgeschrittene

Tika

Apache Tika erkennt und extrahiert Metadaten und Text aus verschiedenen Dateitypen (zum Beispiel PDF, DOC und XLS). Alle Dateitypen können über eine einzige Schnittstelle verarbeitet werden, wodurch Tika für die Indexierung, Inhaltsanalyse und Übersetzung von Dokumenten in großem Stil verwendet werden kann. Tika ist eine Java-Anwendung (Standalone oder Server), welche durch verschiedene Parser und Detektoren erweitert werden kann.

Installation

Tika kann über Homebrew installiert oder einfach von der Homepage heruntergeladen werden.

brew install tika

Es gibt verschiedene JAR-Releases von Tika:

  • Source: Source Code zum selbst kompilieren und modifizieren
  • App: Standalone-Anwendung mit (fast) allen Abhängigkeiten
  • Server: Server-Anwendung mit REST-Interface
  • Eval: Tool zum Vergleichen verschiedener Tools und Versionen

In machen Fällen bietet es sich an, Tika aus dem Source Code selbst zu bauen. Dieses Vorgehen ermöglicht eine bessere Kontrolle über die verwendeten Abhängigkeiten und die Konfiguration.

Text extrahieren

Die häufigste Form von Dokumenten im Bereich Journalismus sind PDFs. Wurden diese direkt aus Word oder eine vergleichbaren Textverarbeitungssoftware erstellt, lässt sich der Text verhältnismäßig einfach extrahieren. Etwas schwieriger wird es bei Scans von Dokumenten. Um den Text aus Bildern zu extrahieren, verwendet der Tika-PDF-Parser die Open-Source-OCR-Lösung Tesseract.

brew install tesseract --with-all-languages --with-serial-num-pack

Wenn Tesseract auf dem System installiert ist, versucht Tika automatisch, Tesseract für die Extraktion von Bildern zu verwenden.

java -jar tika-app-1.17.jar -t -i ./pdf -o ./text

Standardgemäß klappt das aber nicht perfekt, weil Abhängigkeiten zum Verarbeiten von TIFF, JPEG2000 und BigJPEG-Bildern fehlen.

In diesem Fall muss man Tika aus dem Source Code neu kompiliert werden. Dazu müssen die fehlenden Abhängigkeiten im Maven-Projekt hinzugefügt werden. Hier ein Auszug aus der pom.xml im Ordner tika-app:

<dependency>
  <groupId>com.github.jai-imageio</groupId>
  <artifactId>jai-imageio-core</artifactId>
  <version>1.3.1</version>
</dependency>

<dependency>
  <groupId>com.github.jai-imageio</groupId>
  <artifactId>jai-imageio-jpeg2000</artifactId>
  <version>1.3.0</version>
</dependency>

<dependency>
  <groupId>com.levigo.jbig2</groupId>
  <artifactId>levigo-jbig2-imageio</artifactId>
  <version>2.0</version>
</dependency>

Sind die fehlenden Abhängigkeiten hinzugefügt, muss das Projekt neu gebaut werden:

mvn install

Der fertige Build wird im Ordner target erzeugt.

Sollte man Tesseract nicht verwenden wollen, kann man den PDF Parser in einer Konfigurationsdatei config.xml entsprechend anpassen:

<?xml version="1.0" encoding="UTF-8"?>
<properties>
  <parsers>
    <parser class="org.apache.tika.parser.DefaultParser">
      <parser-exclude class="org.apache.tika.parser.ocr.TesseractOCRParser"/>
    </parser>
  </parsers>
</properties>

Die Konfigurationsdatei kann man mit dem Parameter -c übergeben werden:

java -jar tika-app-1.17.jar -c config.xml -t -i pdf -o text

Für die Verwendung in Node.js gibt es node-tika. Dies Bibliothek wurde aber schon länger nicht mehr aktualisiert und ist recht fehleranfällig. node-tika wird auch in den BR Data elasticsearch-import-tools verwendet.

Mehr Infos zur Verwendung von Tika in Verbindung mit Tesseract finden sich im Tika Wiki.

Elasticsearch

Elasticsearch ist eine mächtige und sehr schnelle Open-Source-Suchmaschine für Textdokumente. Elasticsearch ist in Java geschrieben und verwendet Apache Lucene für die Indizierung und Suche nach Texten. Die Komplexität und der Funktionsumfang von Lucene versteckt sich hinter einer einfachen REST-API, welche die Entwicklung und Integration mit bestehenden Anwendungen erleichtert. Für die REST-API sind Clients für alle gängigen Programmiersprachen verfügbar.

Wichtige Grundbegriffe für Elasticsearch sind:

  • Cluster: Der Verbund aller Nodes (Server) auf denen Elasticsearch läuft
  • Node: Eine Instanz von Elasticsearch auf einem Server oder dem eigenen Rechner
  • Index: Eine Sammlung an Dokumenten und damit verbundene Einstellungen (Analyzer, Mapping)
  • Document: Ein Datenbankeintrag für jedes Dokument (JSON-Objekt mit Text im Feld body)

Für den skalierbaren Produktiveinsatz:

  • Shard: Eine Instanz von Elasticsearch mit einem Teil der Dokumente
  • Replica: Eine Instanz von Elasticsearch mit einer Kopie eines Index

In der Entwicklungsphase kann der Cluster nur aus einem Node mit einem Index bestehen.

Zum Einarbeiten in Elasticsearch empfiehlt sich der Definitive Guide.

Installation

Elasticsearch kann man auf Mac OS X einfach über Homebrew installieren. Wir arbeiten momentan noch mit der Version 2.4 von Elasticsearch. Ein Update ist aber geplant.

brew install elasticsearch@2.4

Elasticsearch kann man entweder direkt ausführen...

elasticsearch@2.4

... oder als Service starten:

brew services start elasticsearch@2.4

Für die ordnungsgmäße Ausführung braucht es eine gültige Konfiguration:

Um herauszufinden, wo auf dem System die Konfiguration liegt, kann man brew info aufrufen:

brew info elasticsearch@2.4

Zum einfachen Starten und Stoppen der über Homebrew installieren Dienste kann man das LaunchRocket PrefPane verwenden. Linux-Benutzer können Elasticsearch mit systemd oder init starten.

Status und Verwaltung

Den aktuellen Status des Elasticsearch-Clusters kann man über das REST-Interface abfragen:

curl -XGET 'http://localhost:9200/_cluster/health?pretty=1'

Viel komfortabler lassen sich die verschiedenen Nodes und Indizes aber mit der Chrome Extension ElasticSearch Head verwalten.

Falls es Unassigned shards-Warnungen gibt, können für die Entwicklung, die Anzahl der Replicas auf 0 gesetzt werden:

curl -XPUT 'localhost:9200/_settings' -d '
{
  index: {
    number_of_replicas : 0
  }
}'

Im Idealfall ist der Cluster-Status jetzt grün.

Index erstellen

Leeren Index mit dem Namen my-index anlegen:

curl -XPUT localhost:9200/my-index -d '
{
  settings: {
    index: {
      number_of_replicas: 0
    }
  }
}'

Elasticsearch bestätigt gelungene Anfragen immer mit {"acknowledged":true}. Falsche oder nicht durchführbare Anfragen werden mit einer Fehlermeldung quittiert.

Eine Liste aller Indizes bekommt man mit:

curl -XGET  'localhost:9200/_cat/indices?v'

Einen Index kann man auch sehr schnell wieder löschen:

curl -XDELETE localhost:9200/my-index

Analyzer und Mapping

Analyzer sind Funktionen, die bestimmen wie mit importierten Texten umgegangen werden soll. Das sind zum Beispiel:

  • Tokenizer: Eingabetext auf Wortgrenzen aufteilen
  • Token-Filter: Vom Tokenizer ausgegebenen Token aufräumen
  • Lowercase-Token-Filter: Alle Token in Kleinbuchstaben konvertieren
  • Stop-Token-Filter: Stoppwörter (der, die, das, ein, eine ...) entfernen

Um nach Begriffen mit diakritischen Zeichen suchen zu können, legen wir einen eigenen Analyzer mit ASCII-Folding an. Der Analyser ersetzt diakritische Zeichen mit den entsprechenden ASCII-Zeichen. So wird das portugiesische Conceição in Conceicao umgewandelt.

curl -XPUT localhost:9200/my-index -d '
{
  settings: {
    analysis: {
      analyzer: {
        folding: {
          tokenizer: "standard",
          filter: ["lowercase", "asciifolding"]
        }
      }
    }
  }
}'

Definierte Analyzer können dann im Mapping verwendet werden. Im Mapping wird definiert, welcher Analyzer auf welches Feld angewandt werden soll. In unserem Beispiel wollen wir, dass Conceição im Feld body zu Conceicao im Feld body.folded wird:

curl -XPUT localhost:9200/my-index/_mapping/doc -d '
{
  properties: {
    body: {
      type: "string",
      analyzer: "standard",
      fields: {
        folded: {
          type: "string",
          analyzer: "folding"
        }
      }
    }
  }
}'

Im Mapping kann man auch die Datentypen von bestimmten Feldern festlegen, wenn man zum Beispiel mit Datumsfeldern oder Zahlen arbeitet:

curl -XPUT localhost:9200/my-index/_mapping/doc -d '
{
  properties: {
    body: {
      type: "string",
      analyzer: "standard",
      fields: {
        folded: {
          type: "string",
          analyzer: "folding"
        }
      }
    },
    meta: {
      properties: {
        timestamp: {
          type: "date",
          format: "strict_date_optional_time"
        }
      }
    }
  }
}'

Wird kein Analyzer oder Mapping angegeben, verwendet Elasticsearch (meist sinnvolle) Standardeinstellungen. Mehr Infos zu Analyzern und Mapping gibt es in der Elasticsearch Dokumentation.

In den elasticsearch-import-tools gibt es eine Skript, welches einen Elasticsearch-Index für den Import von Dokumenten vorbereitet. Der alte Index wird dabei gelöscht:

node prepare.js localhost:9200 my-index doc

Wichtig: Wenn man Analyzer oder Mapping eines bestehenden Index' ändert, sollte man die Daten danach reindizieren.

Dokumente importieren

Einzelne Daten lassen sich über die REST-API importieren:

curl -XPUT 'localhost:9200/my-index/doc/1?pretty' -d '
{
  "fileName": "text/my-textfile.txt",
  "date": "2018-02-12",
  "author": "John Doe",
  "body": "Lorem ipsum dolores sit amet."
}'

Um viele Dokumente zu importieren, empfiehlt sich die Verwendung einer Library, wie elasticsearch.js für Node.js. Ein einfaches Beispiel:

const elastic = require('elasticsearch');
const client = new elastic.Client({ host: 'localhost:9200' });

client.create({
  'my-index',
  'doc',
  body: {
    fileName: 'text/my-textfile.txt',
    date: '2018-02-12',
    author: 'John Doe',
    body: 'Lorem ipsum dolores sit amet.'
  }
},
error => {
  if (error) {
    console.error(error)
  } else {
    console.log(`Inserted document ${filePath} to ElasticSearch`);
  }
});

Auch für den Dateimport gibt es ein fertiges Skript aus der BR-Data-Werkzeugkiste:

node import.js ./text localhost:9200 my-index doc

Dokumente suchen

Es gibt verschiedene Möglichkeiten eine Suchanfrage in Elasticsearch zu stellen. Zusätzlich zu den verschieden Suchmethoden, kann man die jeweilige Suchanfragen noch mit unterschiedlichen Parametern konfigurieren:

curl -XGET 'http://localhost:9200/my-index/doc/_search' -d '{
  "query" : {
    "match" : {
      "author" : "John Doe"
    }
  }
}'

Grundsätzlich wird zwischen einer Volltextsuche (mehrere Wörter) und einer Begriffssuche (ein Wort) unterschieden. Multi Match und Simple Query können ganze Sätze finden, während in der Fuzzy- oder Regex-Suche nur nach einzelnen Begriffen gesucht werden kann:

  • Multi Match Query: Findet exakte Suchausdrücke wie John Doe (Dokumentation).
  • Simple Query DSL: Findet alle Wörter eines Suchausdrucks: "John" AND "Doe". Unterstützt außerdem Wildcards und einfache Suchoperatoren (Dokumentation).
  • Fuzzy Query: Fuzzy-Suche, welche einzelne Begriffe findet, auch wenn sie Buchstabendreher enthalten Jhon (Dokumentation).
  • Regexp Query: Regex-Suche für einzelne Begriffe J.hn* (Dokumentation).

Weitere Informationen zur Suche in Elasticsearch gibt es der Dokumentation. Grundsätzliche Optionen, um die Ergebnisse einer Suche zu filtern, finde sich hier.

Ein einfaches Implementierungsbeispiel um mit elasticsearch.js Dokumente im Index my-index nach John Doe zu durchsuchen:

const elastic = require('elasticsearch');
const client = new elastic.Client({ host: 'localhost:9200' });

client.search({
  'my-index',,
  size: 500, // Limit number of results
  body: {
    query: {
      simple_query_string: {
        'John Doe',
        fields: ['body','body.folded'],
        default_operator: 'and',
        analyze_wildcard: true
      }
    },
    {
      excludes: ['body*'] // Exclude document body from response
    },
    fields: {
      body: {} // Add highlighted matches to response
    }
  }
}, (error, result) => {
  if (error) {
    console.error(error)
  } else {
    console.log(result);
  }
});

Die Elasticsearch Import Tools beinhalten einen kleinen, vorkonfigurierten Express-Server (server.js), welcher die wichtigsten Suchfunktionen in einer einfache REST-API verpackt. Einmal gestartet kann man Suchanfragen vereinfacht stellen:

curl http://localhost:3003/match/John%20Doe

Das Elasticsearch Frontend bietet eine Suchoberfläche für das Durchsuchen von großen Dokumentensammlungen in Elasticsearch. Die Web-Anwendung wurde für die Analyse von Dokumenten-Leaks entwickelt und unterstützt eine Benutzer-Authentifizierung.

Weitere Quellen

Update: Zum Thema Dokument-Mining haben wir auf der Netzwerk Recherche Jahreskonferenz einen Workshop angeboten. Die Folien dazu finden sich hier. Ich habe die Anleitung, um ein paar Informationen aus dem Workshop ergänzt