munin/internal/search/registry.go
2026-09-15 20:36:26 +02:00

102 lines
2.9 KiB
Go

package search
import (
"context"
"fmt"
"sync"
"munin/internal/store"
)
// Registry hält je Projekt einen Suchindex.
//
// # Warum getrennte Indizes statt einem großen?
//
// Ein gemeinsamer Index mit Projektfilter wäre möglich, hätte aber zwei Nachteile:
// Die BM25-Werte (Dokumentanzahl, Durchschnittslänge, Seltenheit eines Begriffs)
// würden über Projektgrenzen hinweg berechnet — ein Begriff, der in Projekt A
// häufig ist, wäre dann auch in Projekt B abgewertet. Und ein Programmierfehler
// beim Filtern würde Inhalte quer über Projekte hinweg sichtbar machen.
//
// Getrennte Indizes machen die Abschottung strukturell statt zu einer Frage der
// richtigen WHERE-Klausel.
type Registry struct {
store *store.Store
mu sync.Mutex
indexes map[string]*Index // Schlüssel: Projekt-ID
}
// NewRegistry erstellt eine leere Registry. Die Indizes entstehen beim ersten
// Zugriff auf ein Projekt (siehe For).
func NewRegistry(s *store.Store) *Registry {
return &Registry{
store: s,
indexes: make(map[string]*Index),
}
}
// For gibt den Index eines Projekts zurück und baut ihn beim ersten Zugriff auf.
//
// Der verzögerte Aufbau hat einen praktischen Grund: Beim Serverstart alle
// Projekte einzulesen würde bei vielen Projekten den Start ausbremsen, obwohl
// die meisten davon in dieser Laufzeit vielleicht nie angefragt werden. So zahlt
// nur der erste Zugriff je Projekt.
func (r *Registry) For(ctx context.Context, projectID string) (*Index, error) {
r.mu.Lock()
defer r.mu.Unlock()
if idx, ok := r.indexes[projectID]; ok {
return idx, nil
}
idx := NewIndex()
if err := r.fill(ctx, idx, projectID); err != nil {
return nil, err
}
r.indexes[projectID] = idx
return idx, nil
}
// fill liest alle Einträge eines Projekts und nimmt sie in den Index auf.
func (r *Registry) fill(ctx context.Context, idx *Index, projectID string) error {
eintraege, err := r.store.AllEntries(ctx, projectID)
if err != nil {
return fmt.Errorf("Suchindex konnte nicht aufgebaut werden: %w", err)
}
for _, e := range eintraege {
idx.Index(e.Slug, e.Title, e.Content, e.Tags)
}
return nil
}
// Update nimmt einen geänderten Eintrag in den Index auf.
// Existiert für das Projekt noch kein Index, passiert nichts — er wird beim
// ersten Zugriff ohnehin vollständig aufgebaut.
func (r *Registry) Update(projectID string, e store.Entry) {
r.mu.Lock()
defer r.mu.Unlock()
if idx, ok := r.indexes[projectID]; ok {
idx.Index(e.Slug, e.Title, e.Content, e.Tags)
}
}
// Delete entfernt einen Eintrag aus dem Index.
func (r *Registry) Delete(projectID, slug string) {
r.mu.Lock()
defer r.mu.Unlock()
if idx, ok := r.indexes[projectID]; ok {
idx.Remove(slug)
}
}
// Forget wirft den Index eines Projekts weg — nach dem Löschen des Projekts.
func (r *Registry) Forget(projectID string) {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.indexes, projectID)
}