17.9.2016 - Btrfs

Einleitung

Vor einiger Zeit habe ich ja über das ZFS-Dateisystem referiert. Das funktioniert so weit sehr gut, aber es gibt einen Nachteil: Wenn an sich das System zerschossen hat, ist das nicht schlimm, denn man hat ja ein Backup von gestern auf dem ZFS-Raid, nicht wahr?

Ja, aber leider war man (d.h.: ich) nicht vorausblickend genug, um ein entsprechende Rettungssystem zusammenzustellen. Das ist immer noch nicht schlimm, man kann Linux ja jederzeit von einem für praktisch alle Distributionen erhältlichen Live-System von Stick oder CD booten.

Tja, der kleine Nachteil ist: Diese Live-Systeme enthalten idR keine ZFS-Treiber. Das heisst, man muss zuerst aus dem Live-System ein Arbeitssystem installieren, dort ZFS installieren, dann die Backup_Disks mounten und schliesslich das Backup herüberkopieren. Vermutlich gibt es auch einen einfacheren Weg, aber den habe ich nicht gefunden.

Andererseits existiert ja Btrfs, dessen grösste Nachteile (1) der sperrige Name und (2) der ewige Beta-Status sind. Btrfs (ob Sie es lieber “Butter-eff-ess”, “Better-eff-ess” oder “Beh Teh Err Eff Ess” aussprechen wollen, sei Ihnen überlassen), ist ganz offensichtlich von ZFS inspiriert. Sein Hautpvorteil ist: Ungeachtet des ewigen Beta-Status ist es inzwischen bei praktisch allen Distributionen Teil des Kernsystems und damit auch der Live-Medien. Man kann auf Btrs-Disks also sofort nach dem Booten des Systems zugreifen.

Installation

Wie gesagt, bei den meisten Systemen sind die Treiber an Bord. Man kann Btrfs Disks ganz normal mit “mount” einbinden. Was nicht bei allen Systemen an Bord ist, sind die Userspace-Tools (Im Wesentlichen das Utility-Programm btrfs). Wenn nötig, installiert man sie mit sudo pacman -S btrfs-progs bzw. sudo apt-get install btrfs-tools.

Konzept

Wie ZFS ist auch Btrfs gleichzeitig ein Festplattenverwaltungstool und ein Dateisystem. Man kann ein Btrfs Dateisystem entweder auf einer Partition eines anderweitig partitionierten Datenträgers installieren, oder auf einem ganzen Datenträger. So oder so kann man das Filesystem später dynamisch erweitern. Eebnfalls ganz ähnlich wie bei ZFS kann man mehrere Datenträger zu Raid0, Raid1 oder Raid5 -Verbänden zusammenschliessen. Bei Btrfs kann man allerdings for Daten und Metadaten getrennt angeben, in welcher Form man sie verwalten wird.

In medias res

Ein neues Btrfs-Dateisystem erstellt man zum Beispiel so:

makefs.btrfs -m raid1 -d raid1 -f /dev/sdc /dev/sdd

Hier haben wir jetzt also zwei ganze Platten, sdc und sdd (welche gleich gross sein sollten) verknpftm, um darauf ein btrfs-Dateisystem zu erstellen, bei welchem sowohl die Daten (-d), als auch die Metadaten (-m) als Raid1 organisiert sind, also auf beide Platten gespiegelt werden. Wenn wir nur einzelne Partitionen verwenden wollten, hätten wir auch diese angeben können:

makefs.btrfs -m raid1 -d raid0 -f /dev/sdc1 /dev/sde8

Das -f bewirkt, dass Btrfs die Aufgabe auch dann erledigt, wenn bereits ein Dateisystem oder eine Partitionierung auf dem Ziel vorhanden ist. Aber damit tut es das wirklich sofort, also bitte dreimal kontrollieren, ob wirklich die richtigen Medien formatiert werden…

Snapshots

Die sind auf den ersten Blick ein wenig umständlicher, als bei zfs. Sie basieren auf “Subvolumes”, und werden darum in den üblichen Btrfs-Dokumentationen erst nach den Subvolumes erwähnt, und wenn man vom schwer verständlichen Subvolume-Konzept schon so ermüdet ist, dass man gar nicht mehr folgen kann, wenn die Rede dann auf Snapshots kommt.

Dabei ist es eigentlich nicht so arg schwierig: Subvolumes sind von aussen gesehen einfach Verzeichnisse innerhalb des Btrfs-Dateisystems. Das Besondere daran ist, dass man sie unabhängig mounten kann. Man erstellt ein Subvolume mit btrfs subvolume create /path/to/btrfs/subvolume oder, einfacher: btrfs sub create ... (Man kann alle Btrfs-Befehle so weit abkürzen, dass sie noch eindeutig sind). Und wenn man so weit ist, kann man nach Herzenslust Snapshots erstellen. Ein Snapshot ist ein exaktes Abbild eines Subvolumes, dessen Erstellung nur Sekunden dauert, egal wieviele Daten darin sind.

Daher an dieser Stelle ein etwas längeres Beispiel eines Backup-Konzeptes:

sudo mount /dev/sdc /mnt/raid
sudo btrf sub create /mnt/raid/backups
sudo btrfs sub create /mnt/raid/backups/full
sudo mkdir /mnt/raid/backups/snapshots

Dann kann man ein Backup-Skript wie folgt erstellen:

today=`date '+%Y-%m-%d'`
dest=/mnt/raid/backups
rsync -aAXHSv --exclude-from=whatToExclude.txt / ${dest}/full
btrfs sub snap -r ${dest}/full ${dest}/snapshots/full-$today

Nach jedem Lauf dieses Skripts hat man ein aktuelles Backup in /mnt/raid/backups/full, und alle früheren Backups in /mnt/raid/backups/snapshots

Ähnlich wie bei ZFS kann man Snapshots auch auf weitere Laufwerke oder an andere Btrfs-Instanzen senden. Der Befehl dazu lautet btrfs send -f destfile /path/to/snapshot. Wenn man nur eine Differenz zu einem früheren Snapshot senden möchte, macht man: btrfs send -p /path/to/older/snapshot -f destfile /path/to/new/snapshot

29.7.2016 - Arch Linux

Arch Linux hat ja den Ruf, ein Linux für eher hartgesottene Freaks zu sein.

Richtig ist, dass man bei Arch Linux mehr selber machen muss, als bei anderen Distributionen. Allerdings gibt es Arch-Varianten, die eher anfängerfreundlich gestaltet sind, z.B. Manjaro.

Ein Vorteil von Arch ist, dass es wirklich nur das enthält, was man braucht, und dass man nach der Installation schon ziemlich viel über den Aufbau “seines” Linux Systems weiss. Denn Arch hat keinen simplen Installer, wie etwa Ubuntu. Stattdessen gibt es bei Arch eine ausführliche Anleitung, wie man das System aus einem laufenden Linux heraus von null an zusammenbaut.

Ein weiterer Vorteil ist, dass ein Arch System leicht aktuell gehalten werden kann: Einerseits funktioniert es nach dem “Rolling Release”-Prinzip, es gibt also keine Release-Stufen, sondern einen kontinuierlichen Update-Prozess. Und andererseits besteht das Package-System von Arch aus einer automatisierten “Bedienungsanleitung”, wie die Software direkt aus den Quellen zusammengeabut und installiert wird. Man braucht also nicht darauf zu warten, dass der Distributions-Maintainer Updates bereitstellt.

Doch vor den Erfolg haben die Götter den Schweiss gestellt…

Installation

Die Grundinstallation ist auf der Archlinux Seite sehr gut beschrieben.

Wie es dann weiter gehen kann, kann man ebenfalls bei Arch Linux nachlesen. Auch hier wird man vieles, was andere Distributionen einem vorfertigen, noch von Hand erledigen müssen (Zum Beispiel, einen User und Gruppe anlegen). Im Folgenden arbeiten wir immer als “normaler” User, nicht als root.

Aber zunächst muss auch noch das Package-System anegkegt werden. Arch hat einen Package Manager namens “pacman”, der die Aufgaben erledigt, die man in debian-Derivaten gerne aptitude oder apt-get anvertraut. Das Standardkommando ist synchronize: pacman -S <paketname>. Um das ganze System auf den neuesten Stand zu bringen. schreibt man pacman -Syu.

Auch eine Klickibunti-Oberfläche muss man selber anlegen, denn der Arch Installationsprozess führt zunächst nur mal nur zu einem reinen Textsystem. Das hat wiederum den Vorteil (oder ist es ein Nachteil?) der grossen Auswahl:

Praktisch alle Fenstersysteme gibt es auch für Arch. Sie können also frei entscheiden, ob Sie etwa Gnome oder KDE oder Lxde oder Mate oder was auch immer haben möchten. Aber Sie müssen es selber zusammenstöpseln. Daher im Folgenden eine sehr subjektive Zusammenstellung.

Fenstersystem

Zunächst mal gibt es da viele Begriffe, die zusammenspielen:

  • Display Driver: Steuert direkt die Grafikkarte an. Im Prinzip ist es heutzutage problemlos ohne Verrenkungen möglich, den Bildschirm mit (fast) jeder Grafikkarte zu einer Anzeige zu bewegen. Aber je nachdem, wie hohe Ansprüche man an die 2D/3D Grafikleistung stellt, wird man mehr oder weniger Aufwand treiben müssen, um einen möglichst genau zur eigenen Grafikkarte passenden Treiber zu finden. Man kann sich aber ohne Weiteres fürs Erste mal auf die automatisch installierten Treiber verlassen und später weiter schauen.

  • Display Server / X-Server: Zuständig für die Umwandlung von Programmbefehlen in Bildschirmpunkte. Also die niedrigste Ebene der Anzeigensteuerung. Ausserdem zuständig füs Umleiten der Anzeige an ein anderes Terminal, wenn gewünscht.

  • Display Manager: Zuständig für den Start des Fenstersystems. Erlaubt meist auch beim Login die Auswahl aus mehreren installierten Fenstersystemen.

  • Window Manager: Fenstersystem. Über dem Display Server stehende Ebene der Bildschirmansteuerung. Der Window-Manager baut Fenster und Fensterelemente auf und stellt die coolen Ein/Ausblend/Transparenzeffekte zur Verfügung.

  • Desktop Environment: Setzt auf dem Window Manager auf und sorgt für einheitliches Design und Verhalten (Themes).

In einem Linux-System braucht man nicht all diese Elemente. Wenn man einen Leistungsschwachen Computer hat, installiert man vielleicht nur den X-Server. Programmen wie xterm etx. genügt das. Oder man eght einme Stufe weiter, und installiert noch einen Window-Manager. Dann laufen die Programme in hübschen Fenstern, aber man hat noch keinen Desktop, kein Menü und keine Taskleisten. (Man kann allerdings diese Dinge durchaus einzeln nachinstallieren).

Meistens wird man heutzutage ein kompklettes Desktopsystem installieren. Dann sieht die Oberfläche aus wie aus einem Guss und funktioniert auch so, wie wir es nunmal gewöhnt sind.

Mir gefällt der Cinnamon-Desktop von Linux Mint. Natürlich kann man den auch in arch installieren. Damit der Computer gleich in den Desktop startet, füge ich gleich noch den Display Manager gdm hinzu.

Also: sudo pacman -S gdm cinnamon

Achtung: Das System bootet trotzdem zunächst in den Textmodus. Um den Desktop zu starten, muss man sudo systemctl start gdm eingeben, oder, wenn man beim Systemstart auotmatisch in den Grafikmodus kommen will, sudo systemctl enable gdm. So oder so muss man beim ersten Start, bevor man das Login-Passwort eingibt, auf das Zahnrad unter dem Namen klicken, und das gewünschte Fenstersystem, auswählen. Sonst landet man in einer eher langweiligen Stanmdardoberfläche.

Drucker

Der Anschluss von Hardware ist unter Linux ja immer ein besonderer Quell der Freude. Natürlich gibt es immer alles, aber meistens in -zig Varianten und Versionen und ohne komplette Bedienungsanleitung. Der Prozess der Druckerinstallation ist immerhin dank cups (common unix print system) ein Stück weit automatisiert.

sudo pacman -S cups gutenprint foomatic-db-gutenprint

installiert eine Reihe von Druckertreibern. Die Konfiguration erledigt man dann am besten entweder mit dem Drucker-Systemsteuerungs-Applet von Cinnamon, oder über die Webseite des CUPS-Servers: http://localhost:631

Nach einigem Hin und Her gelang es mir damit, meine Drucker (Brother HL-4570CDW und Brother MFC 8510 übers Netz, Canon MG 5550 am USB-Port) anzusteuern. Bei manchen Druckern, bei mir etwa dem Dymo LabelWriter 400/450 kann man Linux Treiber direkt beim Hersteller downloaden. Die funktionieren gerade bei Spezialdruckern im allgemeinen besser, als die Standard-Treiber von Gutenprint.

Scanner

Die Scannerinstallation ist theoretisch sehr einfach geworden:

sudo pacman -S sane simple-scan

Damit wird das Backend (sane) und ein einfaches grafisches Frontend installiert. Sehr viele Scanner werden von sane (scanner access now easy) problemlos automatisch erkannt und eingebunden. Ein probeweises scanimage -L zeigt, welche Scanner sane gefunden hat.

Meiner war leider nicht dabei. Ein nwenig Recherche im Internet zeigte, dass die Dinge für den Fujitsu ScanSnap s1300i leider etwas komplizierter sind. Fujitsu vertreibt Treiber für Mac und Windows, aber nicht für Linux. Und die Schnittstellen sind offenbar nicht dokumentiert, so dass es auch keine OpenSource Treiber gibt.

Also ging ich nach der Recherche am Ende folgendermassen vor:

  • Die Windows-Treiber auf einem Windows-Computer installieren
  • Den Computer mit der Dateisuche nach *.nal Dateien durchforsten. Glücklicherweise gibt es nicht sehr viele Dateien mitn dieser Endung. Zwei davon waren dieselben, die auch in /etc/sane.d/epjitsu.conf aufgeführt sind.
  • Diese beiden Dateien auf den Linux-Computer nach /usr/share/sane/epjitsu kopieren.

Et voilà: Jetzt findet scanimage -L den Scanner, und simple-scan kann damit auch Dokumente einziehen, problemlos auch doppelseitig und mehrseitig.

Office Zubehör

yaourt -S openoffice4
sudo pacman -S evince gedit    

DICOM Viewer und PACS client

yaourt -S ginkgo-cadx

Hier ist die Auswahl nicht besonders gross. Es gibt ein paar Projekte, die zum Beispiel bei Ubuntu gelistet sind. Wenn man konkret nachschaut, gibt es darunter aber Projekte, die aufgegeben zu sein scheinen (z.B. Aeskulap), oder die nur sehr rudimentäre Funktionen bereitstellen. Kurz: Ein ernsthafter Konkurrent für Osirix (oder seine freie 64-Bit-Variante OsiriLXIV) ist nicht dabei. Aber leider stehen die Osirix Varianten nicht zur Debatte, da nur für Mac.

Ziemlich gut finde ich Ginkgo-CADx. Die Bedienung ist etwas gewöhnungsbedürftig, aber wenn man es erst mal raus hat, kann man damit eigentlich fast alles machen, was man auch mit OsiriX kann. Nur zum CD Brennen muss man externe Tools bemühen, und auf 3D-Animationen muss man verzichten. Dafür hat man ein paar Dinge, die man mit OsiriX nicht kann, zum Beispiel Bilder aller Art “dicomisieren”.

#### Diverses

  • Viele Mac-User sind an Quicksilver oder Alfred gewöhnt und vermissen das auf Linux. Ein Klon mit praktisch identischer Funktionalität ist Kupfer, welches in arch mit yaourt -S kupfer installiert werden kann.

  • Ein einfacher CD-Brenner ist Brasero -> sudo pacman -S brasero.
  • Computer durchsuchen: Catfish sudo pacman -S catfish.

15.7.2016 - Eclipse .classpath

In Java hat der classpath ja eine klar definierte Bedeutung: Er enthält alle Orte, an denen der Java Compiler und die JRE nach Klassen suchen, die das aufgerufene Programm beim Ablauf benötigt. Man übergibt den classpath beispielsweise mit java -cp /some/where:/some/other/place my_app.

In Eclipse hat jedes Projekt eine versteckte Datei .classpath, die ähnliche, aber nicht genau dieselben Aufgaben hat. In .classpath ist das Projektlayout festgelegt. Eclipse folt nämlich einer etwas anderen Philosophie, als etwa Maven:

In Maven gilt strikt “convention over configuration”. Das bedeutet, Maven erwartet, alle Teile eines Projekts an einem ganz bestimmten relativen Ort vorzufinden. Beispielsweise wird der Quellcode immer in src/main/java erwartet. Man kann das durchaus ändern, aber dazu muss man ausdrückliche Anweisungen in der pom.xml schreiben.

In Eclipse ist das umgekehrt. Eclipse erwartet Quellcode keineswegs ausdrücklich immer in ‘src’, obwohl das in Projekten standardmässig so ist. Eclipse “weiss” erst, wo die Quellen sind, wenn die Orte in .classpath festgelegt sind.

Beide Techniken haben ihre Vor- und Nachteile. Aber man sollte die Unterschiede kennen, wenn man Programme mit beiden Systemen entwickelt.

Wenn man beispielsweise .classpath in .gitignore aufführt, dann wird das Projekt nach dem Import in eine andere Eclipse-Instanc zunächst nicht kompilieren. Der classpath muss zuerst korrigiert werden. Wenn man .classpath aber ins Repository aufnimmt, dann kommt es häufig zu “Classpath-Problems”, weil manchmal implizite Systemabhängigkeiten zum Beispiel bei Libraries in .classpath aufgeführt sind.

Derartige Probleme hat man mit der Maven-Methode nicht. Dafür kann man dort nicht so einfach, mehrere Quellcode-Hierarchien in einem Projekt integrieren.

29.4.2016 - Lucene

Ein weiteres, seit langem auf meiner “to look at”-Liste firmierendes Projekt ist Apache Lucene. Die schiere gigantische Grösse des Projekts hinderte mich bisher, mich daran zu wagen. Apache-üblich vebrirgt sich der Charme der Einfachheit hinter einem Wust von gefühlt Millionen von Bibliotheken und Dokumentationsseiten.

Aber, anders als etwa bei Maven kann man die Lucene-Dokumentation auch verstehen, wenn man nicht nacheinander Informatik, Anglistik und Philosophie studiert hat, wenn man erst mal den Einstieg gefunden hat. Dieser Text hier soll ein solcher Einstieg sein.

Was es ist

Lucene ist ein Java-Framework zum Indizieren und Wiederauffinden von Dokumenten aller Art. Bei der Suche akzeptiert es diese komplexen Suchausdrücke, die wir alle von grossen Websites her so schätzen (was daran liegen mag, dass viele dieser Websites mit Lucene als Suchmaschine arbeiten).

So kann man bespielsweise nach Meier suchen, oder auch nach M??er, wenn man nicht mehr so genaus weiss, ob er Mayer, Maier, Meier oder Meyer heisst. Oder man kann auch nach Meier~ suchen, um alle Wörter zu finden, die ‘ähnlich’ sind, wie “Meier”. Oder wenn man aus der Programmierer-Ecke kommt, mag man vielleicht einen regulären Ausdruck wie /M[ae][iy]er/. All das geht, und natürlich kann man auch Suchterme kombinieren: +Hans -Fritz +Meier, oder "Hans Meier"~5 (Hans und Meier müssen vorhanden und maximal 5 Wörter auseinander sein), oder auch Hans AND (M??er OR /M(ue|ü)ller/).

All das kann man auch für eigene Programme haben, wenn man nur Folgendes in seine pom.xml einsetzt:

    <dependency>
      <groupId>org.apache.lucene</groupId>
      <artifactId>lucene-core</artifactId>
      <version>${lucene-version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.lucene</groupId>
      <artifactId>lucene-analyzers-common</artifactId>
      <version>${lucene-version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.lucene</groupId>
        <artifactId>lucene-queryparser</artifactId>
        <version>${lucene-version}</version>
    </dependency>

(Wobei ${lucene-version} derzeit auf 6.0.0 ist)

Ein paar Grundprinzipien

Lucene speichert und sucht Objekte vom Typ org.apache.lucene.Document. Ein solches Document ist nicht dasselbe, was wir unter einem Dokument verstehen. Im Grunde ist es Lucene völlig egal, was für Dokumente wir speichern und wo wir sie speichern. Lucene kümmert sich nur um zweierlei: Die Metadaten und den Text-Inhalt. Ein solches Objekt, das Metadaten und eventuell Text enthält, ist ein Lucene-Document. Dieses kann, muss aber nicht mit irgendeiner Dokument-Datei zusammenhängen. Der Anwender bzw. Anwendungsprogrammieren muss sich selber darum kümmern, eine Verbindung zwischen Document und Dokument herzustellen (zum Beispiel in Form eines Links zum Dokument im Document).

Ein Document erstellt man zunächst leer, füttert es dann mit Fields und übergibt es anschliessend einem IndexWriter zum Abspeichern. Beim Befüllen mit Fields helfen einem die Lucene Analyzers, die aus einem Text die relevanten Wörter extrahieren und daraus Grundformen und Varianten bilden, sowie irrelevante Füllwörter ausblenden. Damit das gelingt, verwendet man idealerweise einen Analyzer der passenden Sprache, zum Beispiel den org.apache.lucene.analysis.de.GermanAnalyzer.

Allerdings droht das Scheitern schon einen Schritt vorher: Wie kommt man zum Beispiel bei einem PDF oder eine WORD-Datei überhaupt an den Text, den man dem Analyzer vorlegen kann? Dazu benötigt man einen Parser. Und um die Sache kurz zu machen: Eigentlich genügt ein Parser für alles, den man so ins Projekt holt:

    <dependency>
      <groupId>org.apache.tika</groupId>
      <artifactId>tika-parsers</artifactId>
      <version>${tika-version}</version>
    </dependency>

Tika ist ein weiteres extrem grosses und mächtiges Apache-Projekt, von dem wir jetzt nur eines wissen müssen: Es kann mehr als tausend verschiedene Dateiformate verstehen und analysieren.

Ein Dokument hinzufügen

Um ein Dokument zum Index hinzuzufügen, sind folgende Schritte nötig:

  • Lucene Index erstellen oder öffnen.
  • Dokument parsen.
  • Dokument analysieren, Text und Metadaten extrahieren.
  • Aus den Metadaten und dem Textinhalt ein Lucene-Document erstellen.
  • Das Lucene-Document im Index speichern.

Im Folgenden zeige ich eine Klasse, die genau das tut. (Wir verwenden hier Kotlin als Programmiersprache, aber natürlich geht es auch in Java oder Scala.)

    class FileImporter(directory: String, defaultLanguage: String="de"){

      val dir:Path=FileSystems.getDefault().getPath(directory)
      val log= Logger.getLogger("FileImporter")
      val writer : IndexWriter by lazy{
          val analyzer = when(defaultLanguage){
              "de" -> GermanAnalyzer()
              "fr" -> FrenchAnalyzer()
              "it" -> ItalianAnalyzer()
              "en" -> EnglishAnalyzer()
              else ->  StandardAnalyzer()
          }

          log.info("opening index in create or append mode")
          val conf = IndexWriterConfig(analyzer).setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND)
          val index = FSDirectory.open(dir)
          IndexWriter(index, conf)
      }

     /*
       For performance reasons, keep the IndexWriter open until the program shuts down
      */
      fun shutDown(){
          if(writer.isOpen){
              writer.commit()
              writer.deleteUnusedFiles()
              writer.close()
          }
      }
      /**
       * Index an InputStream. The Stream-Data are only analyzed, not stored.
       * @param istream InputStream with the data
       * @param handle a user defined String to identify and retrieve the original file
       * @return The plain text content as found by the parser
       * @throws IOException
       * @throws SAXException
       * @throws TikaException
       */
      fun addDocument(istream: InputStream, handle: String) : String{

          val metadata = Metadata()
          val handler = BodyContentHandler()
          val context = ParseContext()
          val parser = AutoDetectParser()

          parser.parse(istream, handler, metadata, context)

          val text = handler.toString()
          val doc = Document()

          for (k in metadata.names()) {
              val key = k.toLowerCase()
              val value = metadata.get(k)
              if (Strings.isBlank(value)) {
                  continue
              }
              if (key == "keywords") {
                  for (keyword in value.split(",?(\\s+)".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
                      doc.add(TextField(key, keyword, Field.Store.YES))
                  }
              } else {
                  doc.add(TextField(key, value, Field.Store.YES))
              }
          }

          doc.add(TextField("text", text, Field.Store.NO))
          doc.add(StringField("handle",handle, Field.Store.YES))
          writer.addDocument(doc)
          writer.commit()
          return text
      }

Das ist alles!

Dokumente suchen

Zum Suchen wollen wir Suchbegriffe wie oben beschrieben verwenden. Wir brauchen also von all den Zillionen Suchmöglichkeiten von Lucene nur eine: Den org.apache.lucene.queryparser.classic.QueryParser. Dieser Parser zerlegt die Suchanfrage in eine Lucene-genehme Form. Sodann brauchen wir einen IndexReader, der einen Index auslesen kann, und einen IndexSearcher, der einen solchen Index durchsuchen kann. Den IndexSearcher füttern wir mit unserer Query. Da eine Query je Such-Bedingungen sehr viele Resultate liefern kann (Googeln Sie mal nach “Google”), können wir ausserdem mitteilen, wieviele Treffer wir maximal sehen wollen. Und da wir die Treffer nicht nach Fund-Reihenfolge, sondern nach Bewertung gelistet haben wollen, schicken wir sie durch einen TopScoreCollector.

Also zusammengefasst:

  • Query aufbauen
  • IndexReader erstellen
  • IndexSearcher erstellen
  • TopScoreCollector mit der Maximalzahl erwünschter Treffer erstellen
  • Suche starten.

Was wir dann zurückbekommen, und hier wird es etwas verwirrlich, sind nicht Documents, sondern ein TopDocs Objekt. Dieses enthält unter Anderem ein Feld namens scoreDocs, welches ein Array aus ScoreDocs ist. Jedes dieser ScoreDocs enthält wiederum ein Feld namens doc, welches aber wieder nicht wie erhofft ein Document enthält, sondern einen Integer. Dieser Integer wiederum ist ein Index, mit dem wir die Methode doc des IndexSearchers füttern können. Und diese Methode, ja, die liefert uns das Document.

Um den ersten Treffer zu erhalten, brauchen wir also:

indexSearcher.doc(topDocs.scoreDocs[0].doc)

Naja, warum denn einfach, wenn’s auch kompliziert geht. Aber die verschiedenen Zwischentypen TopDocs und ScoreDocs enthalten natürlich auch jede Menge interssanter weiterer Felder ausser den hier eläuterten, und haben somit wohl schon ihre Berechtigung.

Der Code sieht dann so aus:

      /**
      * Query the index. Return at most 'numHits' results.
      * @param queryExpression the term(s) to find.
      * @param numHits   Number of hits to return at most
      * @return A JsonArray with Metadata of the document(s). Can be empty but is never null.
      * @throws ParseException
      * @throws IOException
      */
     fun queryDocuments(queryExpression: String, numHits: Int): JsonArray {
         val index = FSDirectory.open(dir)
         val parser = QueryParser("text", GermanAnalyzer())
         val query = parser.parse(queryExpression)
         val ir = DirectoryReader.open(index)
         val searcher = IndexSearcher(ir)
         val collector = TopScoreDocCollector.create(numHits)
         searcher.search(query, collector)
         val hits = collector.topDocs()
         val score = hits.scoreDocs
         val ret = JsonArray()
         for (sd in score) {
             val hit = searcher.doc(sd.doc)
             val jo=JsonObject()
             hit.getFields().forEach { field ->
                 jo.put(field.name(),field.stringValue())
              }
             ret.add(jo)
         }
         return ret
     }

Eines der Felder in den zurückgelieferten JsonObjects ist die beim Speichern übergebene ‘handle’, mit der die Logik ausserhalb von Lucene das Original-Dokument wieder finden sollte.

Im Prinzip ist das alles, was man braucht, um mit Lucene einzusteigen. Mit diesem Basis-Wissen sollte es einfacher sein, die umfangreiche Lucene-Dokumentation zu verstehen.