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 workspaceSearchPerProject = 5
|
|
|
|
// workspaceSearchData sind die Daten der Suche über alle Projekte.
|
|
type workspaceSearchData struct {
|
|
Query string
|
|
Groups []searchGroup
|
|
Hits int // Summe über alle Gruppen
|
|
Projects int // so viele Projekte wurden durchsucht
|
|
}
|
|
|
|
// searchGroup sind die Treffer eines Projekts.
|
|
type searchGroup struct {
|
|
Project store.Project
|
|
Items []entryListItem
|
|
More bool // es gibt mehr als workspaceSearchPerProject Treffer
|
|
top 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"))
|
|
|
|
data := workspaceSearchData{Query: query}
|
|
|
|
if query != "" {
|
|
projects, err := ws.Store.ListProjects(ctx, sess.WorkspaceID)
|
|
if err != nil {
|
|
s.serverError(w, "Projekte konnten nicht geladen werden", err)
|
|
return
|
|
}
|
|
data.Projects = len(projects)
|
|
|
|
for _, p := range projects {
|
|
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.
|
|
hits := idx.SearchFuzzy(query, workspaceSearchPerProject+1)
|
|
if len(hits) == 0 {
|
|
continue
|
|
}
|
|
|
|
group := searchGroup{Project: p, top: hits[0].Score}
|
|
for i, t := range hits {
|
|
if i == workspaceSearchPerProject {
|
|
group.More = 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
|
|
}
|
|
group.Items = append(group.Items, entryListItem{
|
|
Entry: e,
|
|
Snippet: template.HTML(entry.HighlightSnippet(e.Content, query)),
|
|
})
|
|
}
|
|
if len(group.Items) > 0 {
|
|
data.Groups = append(data.Groups, group)
|
|
data.Hits += len(group.Items)
|
|
}
|
|
}
|
|
|
|
sort.SliceStable(data.Groups, func(i, j int) bool {
|
|
return data.Groups[i].top > data.Groups[j].top
|
|
})
|
|
}
|
|
|
|
s.render(w, r, "search.html", http.StatusOK, pageData{
|
|
Title: "Suche in allen Projekten",
|
|
Section: "suche",
|
|
Data: data,
|
|
})
|
|
}
|