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