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) }