UNIXwork

Tags

c unix dav shell linux xattr solaris links x11 java rant fun webdav sync gnome apple benchmark network ldap oracle wtf analytics xnedit macos windows graalvm bsd curl mac apache virtualbox rhel zfs sparc tomcat microsoft freebsd arm

Meine Lektionen aus der Entwicklung von dav 1.3

23. Dezember 2019

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.

Fulltext Search Engines Teil 2: Apache Lucene Howto

22. Dezember 2019

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.

Fulltext Search Engines Teil 1: Lucene, Solr und Elasticsearch

21. Dezember 2019

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.

dav-sync: Symlinks synchronisieren

20. Dezember 2019

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.

dav-sync Datei-Splitting

19. Dezember 2019

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.

Zurück Weiter