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