Files
patrick 9a24ea29e1 FDN-01: repository & projektgerüst
Git-Repository für bestehenden archivdms-Code initialisiert, Branch-/Commit-Konvention (feature/<ticket>-<slug>-Branches, Ticket-Prefix in Commit-Nachricht) etabliert.
2026-08-11 21:27:53 +02:00

95 lines
3.7 KiB
Go

// Package index is the (Phase 1) full-text search sync layer for archivdms.
//
// PostgreSQL remains the single source of truth; this package keeps a
// secondary, per-tenant Manticore Search index (Hybrid BM25+Vektor is a later
// phase) in sync with the documents table. Only the write/sync half is
// implemented here — there is deliberately NO search endpoint yet (Phase 2/3).
//
// Design guarantees:
// - The index is best-effort. When Manticore is not configured (empty DSN)
// the whole thing degrades to a no-op: the Indexer is nil and every caller
// skips silently.
// - An index error must NEVER be propagated to the originating HTTP request.
// Callers log and move on. Postgres stays authoritative, so a stale index
// is a recoverable, non-fatal condition (a later reindex CLI, Phase 3,
// rebuilds it).
//
// This package intentionally has NO dependency on internal/storage to avoid an
// import cycle: storage builds DocumentDoc values and calls into here.
package index
import (
"context"
"time"
)
// DocumentDoc is the index representation of a stored document. It is the
// projection of a documents row plus its resolved taxonomy (tags) and ACL
// (visibility group IDs) that the search index needs.
type DocumentDoc struct {
ID int64
TenantID int64
Title string
DocType string // deprecated free-text doc_type
Correspondent string // deprecated free-text correspondent
OCRText string
Tags []string
TagIDs []int64
DocTypeID *int64
CorrespondentID *int64
ACLGroupIDs []int64
RetainUntil *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
// SearchQuery is the (Phase 3) full-text + attribute query against a single
// tenant's index. It intentionally carries only what the index needs to return
// a ranked list of documents.id values; the caller re-hydrates the full
// document rows from Postgres (the source of truth) afterwards.
type SearchQuery struct {
// Query is the raw user full-text term. It is escaped before it ever
// reaches a MATCH() expression — callers pass it verbatim.
Query string
// TagIDs, when non-empty, restricts hits to documents carrying ANY of
// these tag ids (MVA filter).
TagIDs []int64
// DocTypeID, when non-nil, restricts hits to that document type.
DocTypeID *int64
// ACLGroupIDs applies the group-resolved document ACL: when non-nil, only
// documents visible to ANY of these permission groups are returned. A nil
// slice means "no ACL filter" (domain_admin/superadmin). An explicitly
// empty (non-nil) slice would match nothing — callers must short-circuit
// that case before querying.
ACLGroupIDs []int64
// Page is 1-based; PageSize caps hits per page.
Page int
PageSize int
}
// SearchHit is a single ranked result: a documents.id plus its BM25 score.
type SearchHit struct {
ID int64
Score float64
}
// Indexer syncs a single (tenant-scoped) document index. Implementations must
// never block or fail the calling request on transient backend errors beyond
// returning the error for the caller to log.
type Indexer interface {
// IndexSync inserts or replaces the document (id-based upsert).
IndexSync(ctx context.Context, doc DocumentDoc) error
// Delete removes the document from the index by its documents.id.
Delete(ctx context.Context, id int64) error
// Search runs a full-text + attribute query and returns the ranked hits
// for the requested page plus the total match count (across all pages).
Search(ctx context.Context, q SearchQuery) (hits []SearchHit, total int, err error)
}
// TenantIndexer hands out per-tenant Indexer instances, each backed by its own
// RT table (documents_tenant_<id>).
type TenantIndexer interface {
ForTenant(tenantID int64) Indexer
Close() error
}