// Package search implementiert SRC-01: den Manticore-RT-Suchindex für // Mail-Inhalte. Bekannter Fehler vermeiden (siehe known-issues-archivmail.md // #11/#12): archivmail baute WHERE-Klauseln und teils Spalten-/Tabellennamen // dynamisch über fmt.Sprintf/strings.Join zusammen. Für dieses Paket gilt // verbindlich: Spalten- und Tabellennamen kommen AUSSCHLIESSLICH aus den // Konstanten dieser Datei, nirgendwo sonst im Paket wird ein Feld- oder // Tabellenname zur Laufzeit zusammengesetzt. Suchanfragen laufen über die // strukturierte Manticore-HTTP-JSON-API (Query/Insert-Sub, kein // String-Zusammenbau von SQL), nicht über die SQL-Schnittstelle. package search import "hash/fnv" // IndexName ist der einzige Ort, an dem der Manticore-Indexname als // Literal steht. const IndexName = "mail_documents" // Statische Feld-Whitelist des mail_documents-Index (muss deckungsgleich // mit migrations/0001_mail_documents.sql bleiben). const ( FieldTenantSlug = "tenant_slug" FieldMessageID = "message_id" FieldSubject = "subject" FieldBody = "body" FieldAttachmentText = "attachment_text" FieldSentAt = "sent_at" // Facettenfelder (SRC-05), nachgezogen über migrations/0002..0005. FieldSender = "sender" FieldMailbox = "mailbox" FieldAttachmentType = "attachment_type" FieldTag = "tag" // OCR-Sprach-/Qualitätsfelder (SRC-10), nachgezogen über // migrations/0006..0007. FieldOCRLanguage = "ocr_language" FieldOCRConfidence = "ocr_confidence" ) // FacetField ist ein geschlossener Typ für die vier zulässigen // Facetten-/Filterdimensionen (SRC-11, Akzeptanzkriterium 2): die // Zulässigkeitsprüfung in facets.go läuft über ein erschöpfendes // switch/case auf diesem Typ, NICHT über das Durchsuchen einer Liste — // selbst ein vergessener Eintrag in einer Whitelist-Liste könnte dort // nichts mehr durchlassen, weil keine solche Liste mehr die Entscheidung // trifft. FacetFields (unten) ist nur noch eine abgeleitete // Aufzählungshilfe für Iteration, keine Prüfgrundlage. type FacetField string const ( FacetFieldSender FacetField = FacetField(FieldSender) FacetFieldMailbox FacetField = FacetField(FieldMailbox) FacetFieldAttachmentType FacetField = FacetField(FieldAttachmentType) FacetFieldTag FacetField = FacetField(FieldTag) ) // IsValid entscheidet über Zulässigkeit als Facetten-/Filterfeld über // ein geschlossenes switch/case (Akzeptanzkriterium 2) statt eine Liste // zu durchsuchen. func (f FacetField) IsValid() bool { switch f { case FacetFieldSender, FacetFieldMailbox, FacetFieldAttachmentType, FacetFieldTag: return true default: return false } } // ParseFacetField wandelt eine externe Zeichenkette (z. B. aus einem // HTTP-Query-Parameter) in ein FacetField um — liefert false bei jedem // Wert, der nicht exakt einer der geschlossenen Konstanten entspricht. // Einzige vorgesehene Stelle, an der ein Client-Feldname überhaupt zu // einem FacetField werden kann. func ParseFacetField(raw string) (FacetField, bool) { f := FacetField(raw) return f, f.IsValid() } // FacetFields sind die je Kachel unterstützten Filterdimensionen // (Akzeptanzkriterium 1: Absender, Postfach, Anhangstyp, Tag — Zeitraum // läuft separat über FieldSentAt als Bereichsfacette, siehe facets.go). // Nur zur Iteration gedacht (z. B. "berechne alle Facetten") — die // Zulässigkeitsprüfung selbst läuft über FacetField.IsValid(), nicht // über Mitgliedschaft in dieser Liste. var FacetFields = []FacetField{FacetFieldSender, FacetFieldMailbox, FacetFieldAttachmentType, FacetFieldTag} // DocumentID berechnet deterministisch die Manticore-Dokument-ID aus // Mandant und Message-ID (FNV-1a, 64 Bit). Deterministisch statt einer // separat vergebenen ID, damit Re-Indexierung (Index) und Löschung // (Delete) für dieselbe Mail immer dieselbe Dokument-ID referenzieren, // ohne dass der Aufrufer sie zwischenspeichern muss (SRC-02: Löschungen // müssen ohne zusätzlichen Zustand nachgezogen werden können). func DocumentID(tenantSlug, messageID string) uint64 { h := fnv.New64a() _, _ = h.Write([]byte(tenantSlug)) _, _ = h.Write([]byte{0}) _, _ = h.Write([]byte(messageID)) return h.Sum64() }