Die Entwicklung von dav 1.3 ist ganz und gar nicht so verlaufen wie ich mir das vorgestellt hatte. Über ein Jahr später als geplant, konnte ich das Release dann vor kurzem endlich fertig stellen. Ich entwickel das jetzt allerdings im meiner Freizeit, daher variiert es natürlich, wie viel Zeit ich in die Entwicklung stecke. Trotzdem muss ich zugeben, dass die Zeit, die ich investiert habe, nicht unbedingt optimal genutzt wurde.
Schauen wir uns zunächst ein paar Zahlen der Entwicklung an.
version date loc new-loc n-month lines/month
---------------------------------------------------------------
begin 2012-11-30 0 lines 0 0 0
1.0.0 2017-08-13 14633 lines 14633 57 256
1.1.0 2017-10-07 15416 lines 783 2 391
1.2.0 2018-06-26 20465 lines 5049 8 631
1.3.0 2019-12-15 29743 lines 9278 18 515
Für die erste Version von dav habe ich natürlich relativ lange gebraucht. Es ist vermutlich deutlich einfacher, an eine bestehende Software, die man auch noch gut kennt, neues Zeug ranzubasteln, als etwas komplett neues aus dem Boden zu stampfen.
Bei der Entwicklung von dav 1.2 war ich am produktivsten. Aber wenn man nur nach lines/month geht, dann war dav 1.3 eigentlich kein Desaster (im Vergleich zu den anderen Releases). Was man aber sieht ist, dass sich die Code-Basis drastisch vergrößert hat. Viel zu viele neue Features haben den Weg in das Release gefunden. Genau das ist eines der Probleme.
Das größte Ärgernis dabei war, dass ich zwar relativ zügig neue Features implementiert hatte, aber sobald die minimal irgendwie funktioniert haben, bin ich zum nächsten neuen Feature übergegangen. Am Ende hatte ich haufen Neues, das aber nicht gut funktionierte.
Dazu kam auch ein größeres Refactoring der dav-sync Codebasis. Dieses hat natürlich bereits bestehende Funktionalität, die fehlerfrei lief, auch etwas zerstört.
Am Ende war ich Monatelang damit beschäftig, massenweise Tests zu schreiben, um überhaupt wieder Vertrauen in den Code zu entwickeln. Dies war jedoch recht erfolgreich, die Grundfunktionalität dürfte stabiler als vorher sein.
Was ich hoffentlich daraus gelernt habe: Dinge zuende entwickeln. Keine halben Sachen liegen lassen. Und am besten schließt man die Entwicklung eines neuen Features ab, in dem man diverse Tests dafür geschrieben hat, die alle problemlos durchlaufen.
Auch sollte man sich nicht zu viel auf einmal vornehmen. Ich hoffe ich kann den Spruch Release early, release often in Zukunft mehr anwenden. Das hat durchaus seine Vorteile.
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.
Lucene
Apache Lucene ist ein Fulltext Search Engine, die als Java-Bibliothek in eigene Programme eingebunden werden kann.
Lucene arbeitet mit Dokumenten, die verschiedene Felder besitzen können. Felder haben einen Namen und einen Wert. Diese Dokumente können dann zu einem Index hinzugefügt werden. Die Suche nach Dokumenten kann sich dabei auf mehrere Felder beziehen.
Lucene bietet dabei sehr umfangreiche Suchmöglichkeiten. Unterstützt wird außerdem auch ein Ranking der Suchergebnisse.
Im Prinzip lässt sich sagen, dass Apache Lucene die Feature-Messlatte für Open-Source-Volltextsuchen angibt.
Apache Solr
Apache Solr ist ein sehr verbreiteter Suchserver, der auf Apache Lucene aufbaut und den Funktionsumfang davon weitgehend über eine REST-Schnittstelle zur Verfügung stellt. Apache Solr kann man auch als Cluster betreiben und erhält damit ein skalierbares und hochverfügbares System.
Während Lucene nur einfachen Text indizieren kann, ist es mit Apache Solr möglich, verschiedene Dokumententypen zu verarbeiten. Hierfür gibt es verschiedene Content Handler, die Text aus unterschiedlichen Dateitypen extrahieren können.
Der Zugriff auf die REST-Schnittstelle kann man mit Tools wie curl oder anderen HTTP-Clients erfolgen. Es gibt allerdings auch diverse Client-Libs für alle möglichen Programmiersprachen.
Elasticsearch
Genau wie Apache Solr ist Elasticsearch ein Suchserver auf Lucene-Basis. Allerdings handelt es sich um eine kommerzielle Software, wobei ein Teil auch unter einer Open Source Lizenz steht.
Elasticsearch kann ebenfalls als verteilte Searchengine betrieben werden. Zusätzlich bietet es auch diverse Analysewerkzeuge.
Fazit
Mit Apache Lucene steht einem eine sehr mächtige Volltextsuche zur Verfügung, die sich auch recht leicht verwenden lässt (dazu mehr in einem weiteren Artikel). Die selbe Funktionalität steht einem dann auch in größerer Dimension mit Apache Solr und Elasticsearch zur Verfügung.
Mit dav-sync ist es nun möglich, Symlinks zu synchronisieren. Eine Besonderheit dabei ist, dass unter Windows Verknüpfungen von dav-sync als Symlink interpretiert werden. Windows hat zwar auch richtige Symlinks, die sind aber Mist, da normale Benutzer beispielsweise keine anlegen dürfen.
Beim Synchronisieren wird dann für die Verknüpfung oder Symlink eine leere Resource angelegt und die relative Adresse des Targets in einer WebDAV-Property gespeichert. Damit hat man ein automatisches Mapping zwischen Symlinks und Windows-Verknüpfungen. Wer also gerne über Links seine Dateien organisiert, kann dies nun auch plattformübergreifend synchronisieren. Das gibt es sonst nirgends.
Konfigurieren muss man dafür nicht viel. In die sync.xml muss innerhalb des <directory>-Elements nur folgendes eingefügt werden:
<directory>
...
<symlink-intern>sync</symlink-intern>
</directory>
Synchronisiert werden können dabei nur Links, die ein Target innerhalb des Sync-Directory haben. Links mit externen Targets können aus Sicherheitsgründen nicht synchronisiert werden.
Neben sync gibt es für das <symlink-intern>-Element noch zwei Werte, die angegeben werden können. Zum einen follow, womit angegeben wird, dass Symlinks gefolgt werden soll. Das Ziel wird als normale Datei hochgeladen. Dies ist auch der Default. Die andere Möglichkeit ist ignore, womit dav-sync dann Symlinks nicht mehr folgt, sondern ignoriert.
Es gibt auch noch das Element <symlink-extern> für Links mit einem externen Target. Hier sind die Werte follow und ignore möglich. Wie erwähnt ist sync, also das Synchronisieren von externen Links, nicht möglich.
Seit dav-sync 1.3 gibt es das Feature, dass Dateien nicht nur als Ganzes synchronisiert werden können, sondern auch nur Teile einer Datei. Hierfür wird für die Datei eine feste Block-Größe definiert und die Datei wird dann in mehrere Blöcke mit dieser Größe eingeteilt. Lokal wird dann pro Block ein Hash gespeichert, so dass erkannt werden kann, welcher Block geändert wurde und synchronisiert werden muss. Auf dem Server wird statt der Datei eine Collection angelegt, in der die einzelnen Blöcke abgelegt werden.
Das Ganze funktioniert natürlich auch Problemlos mit Verschlüsselung. Jeder Block wird als eigenständige Datei hochgeladen und dabei wie sonst auch mit AES im CBC-Mode verschlüsselt.
Natürlich muss man, um das Feature zu nutzen, mal wieder etwas konfigurieren. Zunächst sollte man sich überlegen, welche Dateien überhaupt zerlegt werden sollen. Mögliche Konfigurationsmöglichkeiten sind zum einen, Dateien mit bestimmten Namen oder Pfaden auszuwählen, zum anderen kann auch eine Mindestdateigröße angegeben werden, ab der Dateien aufgeteilt werden sollen.
Die andere Sache, die man sich überlegen muss, ist, welche Größe die einzelnen Blöcke haben sollen. Diese sollte nicht zu klein sein, da sonst viele tausende kleine Dateien auf dem Server abgelegt werden könnten.
Die Konfiguration erfolgt dann in der Datei sync.xml. Hier muss innerhalb des Sync-Directory folgendes eingefügt werden:
<directory>
...
<splitconfig>
<!-- split all files with .vmdk file extension that are bigger than 100mb -->
<split>
<blocksize>10m</blocksize>
<filter>
<include>\.vmdk$</include>
</filter>
<minsize>100m</minsize>
</split>
</splitconfig>
</directory>
Pflichtangabe ist das Element <blocksize>. Damit wird die Blockgröße in Bytes, alternativ auch mit einem Suffix wie k, m oder g, angegeben. Dazu muss entweder <minsize> oder ein <filter> definiert sein. Beides zusammen geht natürlich auch.
Mit <filter> können Regex-Filter, die auf den Dateipfad angewendet werden, konfiguriert werden. Möglich sind zum einen <include>-Filter, die zunächst definieren, welche Dateien ausgewählt werden sollen. Dies kann dann noch mit <exclude> eingeschränkt werden. Die Doku enthält dazu mehr Details.
Mit <minsize> gibt man die Größe an, die eine Datei mindestens haben muss, damit sie aufgeteilt wird. Gibt man nur dieses Element an, ohne <filter>, kann man also pauschal alle Dateien ab einer bestimmten Größe splitten.
Es können auch mehrere split-Regeln definiert werden. Dazu muss nur innerhalb des Elements <splitconfig> ein weiteres <split>-Element eingefügt werden.
Mehr als die Konfiguration muss man dann auch nicht tun. An der Benutzung von dav-sync ändert sich nichts. Wenn alles korrekt konfiguriert ist, werden dann die entsprechenden Dateien aufgeteilt und nur geänderte Blöcke der Datei werden übertragen. Der Benutzer kriegt davon erstmal nichts mit, außer dass es vielleicht schneller geht.
Eine Einschränkung gibt es allerdings. Datei-Versionierung funktioniert nicht mit gesplitteten Dateien. Genauso funktioniert es nicht, wenn <backup-on-pull>true</backup-on-pull> für das Sync-Directory konfiguriert ist, dass bei jeder Dateiänderung ein Backup der Datei in das Trash-Verzeichnis verschoben wird. Der Grund ist, dass nicht mehr kostengünstig über ein move die Datei gesichert werden kann, weil keine komplette Datei übertragen wird.
Erwähnenswert wäre auch noch, dass Datei-Splitting ohne Konfiguration der sync.xml möglich wäre, denn es kann im Prinzip pro Datei einzelnd festgelegt werden, ob diese gesplittet werden soll. Einfache dav-sync-Befehle, um das festzulegen, haben es jedoch nicht in das Release geschafft und ohne Befehle wäre das zu Hacky für diesen Artikel.
Kommentare
dev | Artikel: Datei ver- und entschlüsseln mit openssl - kompatibel mit dav
Andreas | Artikel: Datenanalyse in der Shell Teil 1: Basis-Tools
Einfach und cool!
Danke Andreas
Rudi | Artikel: Raspberry Pi1 vs Raspberry Pi4 vs Fujitsu s920 vs Sun Ultra 45
Peter | Artikel: XNEdit - Mein NEdit-Fork mit Unicode-Support
Damit wird Nedit durch XNedit ersetzt.
Danke!
Olaf | Artikel: XNEdit - Mein NEdit-Fork mit Unicode-Support
Anti-Aliasing hängt von der Schriftart ab. Mit einem bitmap font sollte die Schrift klassisch wie in nedit aussehen.
Einfach unter Preferences -> Default Settings -> Text Fonts nach einer passenden Schriftart suchen.
Peter | Artikel: XNEdit - Mein NEdit-Fork mit Unicode-Support
Mettigel | Artikel: Raspberry Pi1 vs Raspberry Pi4 vs Fujitsu s920 vs Sun Ultra 45
Ich hatte gedacht, dass der GX-415 im s920 deutlich mehr Dampf hat als der Raspi4.
Mein Thinclient verbraucht mit 16 GB RAM ~11 W idle, das ist das Dreifache vom RP4. Das muss man dem kleinen echt lassen... Sparsam ist er.
Olaf | Artikel: Raspberry Pi1 vs Raspberry Pi4 vs Fujitsu s920 vs Sun Ultra 45
Ergebnisse von der Ultra 80 wären natürlich interessant, insbesondere im Vergleich mit dem rpi1.