Verbesserung der Suche vorhandener Dokumente

This commit is contained in:
2026-09-10 17:18:30 +02:00
parent 4cd8a5622e
commit 0e4434f16d
2 changed files with 114 additions and 8 deletions

View File

@@ -5,7 +5,7 @@
xmlns:xlink="http://www.w3.org/1999/xlink"> xmlns:xlink="http://www.w3.org/1999/xlink">
<identifier value="de.mmorath.paperless"/> <identifier value="de.mmorath.paperless"/>
<version value="1.2.0"/> <version value="1.3.0"/>
<display-name> <display-name>
<name lang="de">Paperless-Connector</name> <name lang="de">Paperless-Connector</name>

View File

@@ -7,6 +7,7 @@ von Hand kodiert.
""" """
import json import json
import re
import mimetypes import mimetypes
import os import os
import ssl import ssl
@@ -242,14 +243,119 @@ class PaperlessClient:
self._json("documents/", {"page_size": 1}) self._json("documents/", {"page_size": 1})
return True 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): def search(self, query, limit=50):
params = {"page_size": limit, """
"ordering": "-added", Dokumente suchen. Vier Wege, in dieser Reihenfolge:
"fields": "id,title,created,added,archive_serial_number,"
"correspondent,document_type,tags,original_file_name"} 1. Archiv-Seriennummer, wenn die Eingabe nur aus Ziffern
if query: besteht. Sie ist ein eigenes Feld - die Volltextsuche
params["query"] = query findet sie NICHT.
return (self._json("documents/", params) or {}).get("results", [])
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): def document(self, doc_id, full_perms=False):
params = {"full_perms": "true"} if full_perms else None params = {"full_perms": "true"} if full_perms else None