102 lines
2.9 KiB
Go
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)
|
|
}
|