Die Suche findet, was in einer Datei steht. Tags halten fest, was nicht drinsteht.
In den ersten beiden Teilen ging es darum, den Bestand überhaupt durchsuchbar zu machen und den Stack auf einen eigenen Server zu bekommen. Danach steht da ein Index über zehn-, zwanzigtausend Dokumente, und die Volltextsuche beantwortet Fragen, die vorher eine halbe Stunde Ordner-Klickerei waren. Ein paar Fragen beantwortet sie aber weiterhin nicht — und zwar systematisch, nicht mangels besserer Suchwörter.
Tags im Detail
docs/tags.md beschreibt Sichtbarkeiten, Rechte und die Automatik; docs/api.md die HTTP-Schnittstelle samt OpenAPI-Datei zum Import in Postman, Insomnia oder Bruno.
Das Problem: ein Vorgang liegt selten in einem Ordner
Ein Beispiel, wie es in jeder gewachsenen Ablage vorkommt. Am Dach war etwas nicht in Ordnung, und daraus wurde über anderthalb Jahre ein Vorgang: das ursprüngliche Angebot liegt im Projektordner, die Schlussrechnung in Buchhaltung/2023, das Abnahmeprotokoll als Scan im Posteingang, der Mailverlauf mit dem Dachdecker als .eml-Export, dazu zwei Schreiben der Versicherung — und die liegen in einer zweiten Nextcloud, weil das Versicherungszeug seit jeher woanders gepflegt wird.
Sieben Dokumente, fünf Ablageorte, zwei Instanzen. Die Suche nach „Dachziegel" findet vier davon und außerdem vierzig andere, in denen das Wort auch vorkommt. Das Abnahmeprotokoll findet sie gar nicht, da steht das Wort nicht drin.
Das lässt sich mit keinem besseren Suchbegriff reparieren, weil die Information, die fehlt, in keiner der Dateien steht: dass diese sieben zusammengehören. Die Facetten aus Teil 1 helfen hier auch nicht weiter — Instanz, Ordner, Dateityp, Jahr, Größe beschreiben, wo etwas liegt und was es ist. Nicht, wozu es gehört.
Genau da setzen Tags an. Sie sind die einzige Facette, die jemand absichtlich vergibt.
Ein Wort, das man selbst vergibt
Zwei Sorten Tag, und der Unterschied ist nicht Kosmetik.
Instanz-Tags gehören der Installation. Ein Administrator pflegt sie unter Administration › Tags, und alle sehen dieselbe Liste. Das ist das gemeinsame Vokabular — „Vertrag", „Gewährleistung", „Freigegeben".
Private Tags gehören der Person, die sie angelegt hat. Anlegen kann die jeder, direkt aus der Tag-Auswahl an einem Dokument heraus. Niemand sonst sieht sie, ein Administrator eingeschlossen.
Dazu kommt der Scope, und der entscheidet, wer eine gesetzte Markierung sieht: bei global alle, die auf das Dokument Zugriff haben, bei personal nur, wer sie gesetzt hat.
Warum das zwei getrennte Achsen sind, merkt man beim ersten „Prüfen". Zwei Leute arbeiten an derselben Ablage, beide brauchen einen Stapel „muss ich noch anschauen". Wäre die Markierung global, sähe jeder den Stapel des anderen und müsste raten, welcher seiner ist. Legte jeder sein eigenes privates Tag an, stünde in der Filterleiste zweimal fast dasselbe Wort. Die brauchbare Kombination ist ein Instanz-Tag mit Scope personal: gemeinsames Wort, getrennte Stapel. Der umgekehrte Fall ist „Freigegeben" — global sichtbar, aber mit assign_policy: admin nur von Administratoren zu setzen.
Der Scope steht fest, sobald das Tag existiert. Nachträglich umschalten würde rückwirkend ändern, wer was gesehen hat: Ein „Prüfen", das gestern persönlich war und heute global ist, legt zweihundert Markierungen offen, ohne dass jemand gefragt wurde. Deshalb weist die Änderung dieses Feld ab. Wer sich vertan hat, legt ein neues Tag an — unangenehm, aber sichtbar.
„Neu": was seit Montag reinkam
Die zweite Frage, an der die Volltextsuche vorbeiläuft, ist die simpelste überhaupt: Was ist eigentlich dazugekommen?
Nach Datum sortieren hilft nur bedingt, denn das Datum einer Datei ist ihr Änderungsdatum. Ein Scan von 2019, den gestern jemand hochgeladen hat, steht damit weit unten in der Liste — obwohl er für den Index brandneu ist. Und genau das ist meistens die Frage, die man hat.
Deswegen bringt jede Installation ein Systemtag new mit. Es hängt sich an jedes Dokument, das zum ersten Mal indexiert wird, und fällt nach einer eingestellten Zeit wieder ab — voreingestellt sind sieben Tage. Damit wird „zuletzt reingekommen" ein Filter statt einer Sortierung.
Drei Dinge halten das brauchbar:
Der erste Crawl eines Ordners markiert nichts. Beim ersten Durchlauf ist der gesamte Bestand neu, und „Neu" auf zwanzigtausend Dokumenten sagt nichts. Der erste vollständige Lauf zieht die Grundlinie, ab dem zweiten bekommen neue Dateien das Tag.
Ein Rebuild feuert es nicht erneut. Jedes Dokument merkt sich seine erste erfolgreiche Indexierung ein einziges Mal. Sonst wäre nach jedem Neuaufbau des Index der komplette Bestand wieder „neu". Nebenbei hat das den Rebuild selbst verändert: Er hat früher die Dokumentzeilen weggeworfen und den Crawl neu schreiben lassen. Mit Tags an diesen Zeilen hätte das die Handarbeit anderer Leute mitgenommen, also bleiben die Zeilen jetzt stehen und nur ihr Zustand wird zurückgesetzt.
Und Index leeren setzt die Grundlinie zurück, weil danach ohnehin alles von vorn beginnt.
Der Lauf, der abgelaufene Markierungen wieder abräumt, geht alle fünf Minuten durch. Wer eine geänderte Haltbarkeit sofort sehen will, statt zu warten:
make artisan CMD="nextsearch:tags-sweep"
Löschen lässt sich das Systemtag nicht, und sein Kürzel, sein Scope und seine Automatik sind fest. Name, Farbe, Haltbarkeit und der Schalter „an/aus" gehören dagegen dem, der die Instanz betreibt.
Der Altbestand: einmal suchen, einmal setzen
Ein Vokabular hilft ab dem Tag, an dem man es einführt. Der Bestand von gestern ist damit nicht markiert, und das ist der eigentliche Berg. Die sieben Dokumente aus dem Dach-Vorgang klickt man von Hand. Wenn derselbe Betrieb aber seit Jahren für einen arbeitet und alles, was mit ihm zu tun hat, unter ein Tag soll, stehen da schnell ein paar hundert Treffer. Spätestens dann hört Klicken auf, eine Option zu sein.
Dafür gibt es seit 0.3.0 eine HTTP-Schnittstelle unter /api/v1. Authentifiziert wird mit einem persönlichen Schlüssel aus Benutzereinstellungen › API-Schlüssel, als Bearer-Token. Der Klartext wird genau einmal angezeigt, in der Datenbank liegt nur sein SHA-256-Hash.
Wichtig am Rechtemodell: Der Schlüssel bringt keine eigenen Rechte mit. Eine Anfrage kann exakt das, was ihr Besitzer auch in der Oberfläche kann — dieselben Ordner, dieselben Tags, dieselben Administratorrechte oder eben keine. Es gibt bewusst keinen Service-Account, der mehr darf als ein Mensch. Und ein Schlüssel kann keinen weiteren Schlüssel ausstellen; das geht nur über die angemeldete Sitzung.
Der Ablauf, der sich in der Praxis bewährt hat, geht über die Oberfläche: Filter zusammenklicken, bis die Trefferliste stimmt, und erst dann dieselbe Suche über die API in ein Tag gießen. Die Schnittstelle nutzt dieselbe Suche wie die Oberfläche, also liefert sie auch dasselbe.
KEY=nxs_…
HOST=https://search.example.org
# 1. Alles finden, was den Betrieb erwähnt — über beide Instanzen hinweg
UUIDS=$(curl -sG -H "Authorization: Bearer $KEY" \
--data-urlencode "q=dachdeckerei meiners" \
--data-urlencode 'filters={"year":["2023","2024"]}' \
--data-urlencode "per_page=100" \
"$HOST/api/v1/documents" | jq -c '[.hits[].uuid]')
# 2. In einem Aufruf markieren
curl -s -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d "{\"documents\":$UUIDS,\"add\":[\"dach-gewaehrleistung\"]}" \
"$HOST/api/v1/documents/tags"
{ "updated": 63, "skipped": [], "unknown": [] }
Bis zu 500 Dokumente pro Aufruf, bis zu 50 Tags in add und in remove. Darüber blättert man mit page; total_pages steht in der Antwort der Suche. Tags spricht man über ihr Kürzel an oder über ihre UUID — und ein Kürzel ist nur pro Besitzer eindeutig. Wer ein privates Tag vertrag hat und daneben ein gleichnamiges Instanz-Tag, trifft mit dem Kürzel sein eigenes. Wenn es darauf ankommt: UUID nehmen.
Was kein Suchwort trifft — das Abnahmeprotokoll von oben etwa —, bleibt Handarbeit. Drei Stück von Hand nachtragen, nachdem dreiundsechzig in einem Aufruf durch sind: damit kann man leben.
Wenn etwas nicht geht, geht nichts halb
Zwei Fehlerfälle, die absichtlich unterschiedlich ausgehen, und der Unterschied ist der interessante Teil.
Ein unbekanntes oder nicht erlaubtes Tag kippt den ganzen Aufruf mit 422. Nichts wird still weggelassen. Ein Tippfehler im Kürzel ist ein Tippfehler, den man korrigieren will — und nicht etwas, das man drei Wochen später bemerkt, weil die Hälfte des Bestands unmarkiert geblieben ist. Ein Aufruf, der Erfolg meldet, hat alles getan, was in ihm stand.
Beim Massen-Aufruf ist es umgekehrt. Dokumente ohne Zugriff kommen als skipped zurück, UUIDs, zu denen nichts passt, als unknown — der Aufruf läuft trotzdem durch. Bei fünfhundert Dokumenten aus einer breiten Suche ist ein Ordner, auf den man nie berechtigt war, der Normalfall und kein Grund, die anderen 480 liegen zu lassen. Die Antwort sagt dann aber, welche es waren.
Der Volltext selbst reist übrigens nie mit. Die Suche liefert nur den hervorgehobenen Ausschnitt, und <mark> ist die einzige Auszeichnung darin — alles andere ist escaped. Wer die Datei sehen will, öffnet sie in NextSearch, wo das sichtbar ist.
Was die Schnittstelle bewusst nicht kann
Keine Dateiinhalte, keine Downloads, keine Index-Verwaltung, keine Benutzerverwaltung. Das bleibt in der Oberfläche, wo die Folgen sichtbar sind.
Was es außerdem nicht gibt, ist ein zweiter Weg an derselben Stelle vorbei: Alles unter /api ohne das v1 gehört der Oberfläche, läuft über das Session-Cookie und ändert sich ohne Ankündigung. Das ist kein Vertrag und soll auch keiner werden. Wer etwas vermisst, macht besser ein Issue auf, als sich dort zu bedienen.
Das Limit liegt bei 120 Anfragen pro Minute und Schlüssel, einstellbar über API_RATE_LIMIT; X-RateLimit-Remaining steht in jeder Antwort. Die vollständige Beschreibung liegt als OpenAPI-Datei im Repository unter docs/api/openapi.yaml.
Fazit
Volltextsuche beantwortet Inhaltsfragen. Zugehörigkeit und Zustand sind keine Inhaltsfragen — die muss jemand setzen, und danach sind sie ein Filter wie jeder andere. Tags machen genau das, mit einem Sichtbarkeitsmodell, das man in zwei Sätzen erklären kann, und einem Systemtag, das die häufigste Frage von allein beantwortet.
Die API ist dazu das Werkzeug für den Berg, der schon da liegt. Suche zusammenklicken, einmal absetzen, fertig. Für den täglichen Gebrauch bleibt die Oberfläche schneller.
Beides steckt in NextSearch 0.3.0.
Call to Action
NextSearch ist Open Source und liegt auf GitHub: https://github.com/McGo/NextSearch
Wer den Umgang mit Tags ausprobieren will, ohne eine eigene Nextcloud anzufassen: make demo zieht eine Wegwerf-Instanz hoch, make demo-seed kippt Beispieldateien hinein.
Im nächsten Teil geht es um denselben Werkzeugkasten für Assistenten — NextSearch bekommt einen MCP-Server, über den ein Client suchen und Tags setzen kann, mit denselben Rechten und ohne dass jemand einen Schlüssel herumreicht. Das kommt mit 0.4.0.
