package web
import (
"errors"
"html/template"
"net/http"
"sort"
"strings"
"munin/internal/entry"
"munin/internal/store"
)
// Wie viele Treffer die Suche über alle Projekte je Projekt zeigt. Genug, um
// das richtige Projekt zu erkennen; wer mehr will, sucht dort weiter.
const workspaceSucheJeProjekt = 5
// workspaceSucheData sind die Daten der Suche über alle Projekte.
type workspaceSucheData struct {
Query string
Gruppen []sucheGruppe
Treffer int // Summe über alle Gruppen
Projekte int // so viele Projekte wurden durchsucht
}
// sucheGruppe sind die Treffer eines Projekts.
type sucheGruppe struct {
Project store.Project
Items []entryListItem
Mehr bool // es gibt mehr als workspaceSucheJeProjekt Treffer
besten float64
}
// handleWorkspaceSearch sucht in allen Projekten des gewählten Workspace
// (AUSBAUSTUFEN.md 5.13) — für Workspace- und Plattform-Admins, die ohnehin
// alle Projekte sehen. Beantwortet „In welchem Projekt stand das nochmal?".
//
// Bewusst nur in der Weboberfläche und nur für diese Rollen: Am Zugriff der
// API-Keys ändert sich nichts, ein Key sieht weiter genau ein Projekt (siehe
// AUSBAUSTUFEN.md 4.9).
//
// Die Treffer kommen nach Projekt gruppiert. Eine gemeinsame Rangfolge über
// alle Projekte gäbe es nicht ehrlich: Jedes Projekt hat seinen eigenen
// BM25-Index, und deren Werte sind nicht vergleichbar. Die Projekte stehen
// aber nach ihrem besten Treffer sortiert — das passendste oben.
func (s *Server) handleWorkspaceSearch(w http.ResponseWriter, r *http.Request) {
ws, sess := s.workspaceAndSession(r)
ctx := r.Context()
query := strings.TrimSpace(r.URL.Query().Get("q"))
daten := workspaceSucheData{Query: query}
if query != "" {
projekte, err := ws.Store.ListProjects(ctx, sess.WorkspaceID)
if err != nil {
s.serverError(w, "Projekte konnten nicht geladen werden", err)
return
}
daten.Projekte = len(projekte)
for _, p := range projekte {
idx, err := ws.Search.For(ctx, p.ID)
if err != nil {
s.serverError(w, "Suchindex konnte nicht geladen werden", err)
return
}
// Einen mehr holen als gezeigt, um "weitere Treffer" anbieten zu
// können.
treffer := idx.SearchFuzzy(query, workspaceSucheJeProjekt+1)
if len(treffer) == 0 {
continue
}
gruppe := sucheGruppe{Project: p, besten: treffer[0].Score}
for i, t := range treffer {
if i == workspaceSucheJeProjekt {
gruppe.Mehr = true
break
}
e, err := ws.Store.EntryBySlug(ctx, p.ID, t.DocID)
if errors.Is(err, store.ErrNotFound) {
continue // Index kennt einen Eintrag, den es nicht mehr gibt
}
if err != nil {
s.serverError(w, "Eintrag konnte nicht geladen werden", err)
return
}
gruppe.Items = append(gruppe.Items, entryListItem{
Entry: e,
Snippet: template.HTML(entry.HighlightSnippet(e.Content, query)),
})
}
if len(gruppe.Items) > 0 {
daten.Gruppen = append(daten.Gruppen, gruppe)
daten.Treffer += len(gruppe.Items)
}
}
sort.SliceStable(daten.Gruppen, func(i, j int) bool {
return daten.Gruppen[i].besten > daten.Gruppen[j].besten
})
}
s.render(w, r, "suche.html", http.StatusOK, pageData{
Title: "Suche in allen Projekten",
Bereich: "suche",
Data: daten,
})
}