diff --git a/description.xml b/description.xml
index 240295f..2adc5c3 100644
--- a/description.xml
+++ b/description.xml
@@ -5,7 +5,7 @@
xmlns:xlink="http://www.w3.org/1999/xlink">
-
+
Paperless-Connector
diff --git a/pythonpath/paperless_client.py b/pythonpath/paperless_client.py
index 6c28157..cda2382 100644
--- a/pythonpath/paperless_client.py
+++ b/pythonpath/paperless_client.py
@@ -7,6 +7,7 @@ von Hand kodiert.
"""
import json
+import re
import mimetypes
import os
import ssl
@@ -242,14 +243,119 @@ class PaperlessClient:
self._json("documents/", {"page_size": 1})
return True
+ # Felder, die die Trefferliste braucht. Ohne diese Einschränkung
+ # liefert Paperless je Treffer auch den vollständigen Inhalt - bei
+ # fünfzig Treffern sind das schnell mehrere Megabyte.
+ SUCHFELDER = ("id,title,created,added,archive_serial_number,"
+ "correspondent,document_type,tags,original_file_name")
+
+ # Zeichen mit besonderer Bedeutung in der Abfragesprache des
+ # Suchindex. Unbehandelt führen sie zu Syntaxfehlern oder zu
+ # Treffern, die niemand erwartet - ein Bindestrich im Namen wäre
+ # sonst ein Ausschluss.
+ _SONDERZEICHEN = re.compile(r'[+\-&|!(){}\[\]^"~*?:\\/]')
+
def search(self, query, limit=50):
- params = {"page_size": limit,
- "ordering": "-added",
- "fields": "id,title,created,added,archive_serial_number,"
- "correspondent,document_type,tags,original_file_name"}
- if query:
- params["query"] = query
- return (self._json("documents/", params) or {}).get("results", [])
+ """
+ Dokumente suchen. Vier Wege, in dieser Reihenfolge:
+
+ 1. Archiv-Seriennummer, wenn die Eingabe nur aus Ziffern
+ besteht. Sie ist ein eigenes Feld - die Volltextsuche
+ findet sie NICHT.
+
+ 2. Die GLOBALE SUCHE von Paperless (/api/search/). Das ist
+ dieselbe Schnittstelle, die die Suchleiste oben in der
+ Weboberfläche bedient. Sie verbindet Titelsuche und
+ Volltext und liefert damit die Ergebnisse, die man aus der
+ Oberfläche kennt.
+
+ 3. Teilzeichenkette in Titel und Inhalt. Greift, wenn die
+ globale Suche nichts liefert oder in der eingesetzten
+ Version fehlt.
+
+ 4. Volltextsuche über den Index, mit UND-Verknüpfung. Sie
+ beherrscht Wortstämme und findet auch Beugungsformen.
+
+ Zwei Fallstricke, die hier berücksichtigt sind:
+
+ * Bei der Volltextsuche darf KEIN ordering mitgegeben werden.
+ Paperless sortiert dort nach Relevanz; eine eigene
+ Sortierangabe führte dazu, dass die Suchanfrage verworfen
+ wurde und schlicht die zuletzt hinzugefügten Dokumente kamen.
+
+ * Ohne UND-Verknüpfung sucht der Index mit ODER. "Anschreiben
+ Markus Straub" lieferte dann alles, was irgendeines der drei
+ Wörter enthält - der gesuchte Treffer ging darin unter.
+ """
+ q = (query or "").strip()
+
+ if not q:
+ return self._suchlauf({"ordering": "-added"}, limit)
+
+ # 1. Seriennummer
+ if q.isdigit():
+ treffer = self._suchlauf({"archive_serial_number": q}, limit)
+ if treffer:
+ return treffer
+
+ # 2. Globale Suche wie in der Weboberfläche
+ treffer = self._globale_suche(q, limit)
+ if treffer:
+ return treffer
+
+ # 3. Titel und Inhalt
+ treffer = self._suchlauf({"title_content": q,
+ "ordering": "-created"}, limit)
+ if treffer:
+ return treffer
+
+ # 4. Volltext mit UND, ohne ordering.
+ #
+ # Nach dem Ersetzen der Sonderzeichen ERNEUT zerlegen: Aus
+ # "Müller-Lüdenscheidt" würde sonst ein Wort mit Leerzeichen,
+ # und die UND-Verknüpfung wäre wieder aufgehoben.
+ worte = self._SONDERZEICHEN.sub(" ", q).split()
+ if not worte:
+ return []
+ return self._suchlauf({"query": " AND ".join(worte)}, limit)
+
+ def _globale_suche(self, q, limit):
+ """
+ Die Schnittstelle hinter der Suchleiste der Weboberfläche.
+
+ Sie gibt es erst ab neueren Paperless-Ständen, und die Form der
+ Antwort ist nicht überall gleich - mal ein Feld "documents", mal
+ "results". Deshalb wird beides angenommen und bei jedem Fehler
+ stillschweigend auf den nächsten Weg ausgewichen.
+ """
+ try:
+ antwort = self._json("search/", {"query": q,
+ "page_size": limit})
+ except PaperlessError:
+ return []
+ if not antwort:
+ return []
+
+ for schluessel in ("documents", "results"):
+ rows = antwort.get(schluessel)
+ if isinstance(rows, list) and rows:
+ # Die globale Suche liefert je nach Version verschiedene
+ # Objektarten. Nur echte Dokumente durchlassen.
+ return [r for r in rows
+ if isinstance(r, dict) and "id" in r
+ and "title" in r][:limit]
+ return []
+
+ def _suchlauf(self, zusatz, limit):
+ params = {"page_size": limit, "fields": self.SUCHFELDER}
+ params.update(zusatz)
+ try:
+ return (self._json("documents/", params) or {}).get("results", [])
+ except PaperlessError:
+ # Ein nicht unterstützter Parameter soll nicht den ganzen
+ # Dialog zum Scheitern bringen - der nächste Weg wird
+ # ohnehin probiert.
+ return []
def document(self, doc_id, full_perms=False):
params = {"full_perms": "true"} if full_perms else None