109 lines
3.2 KiB
Go
109 lines
3.2 KiB
Go
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,
|
|
})
|
|
}
|