diff --git a/internal/frontend/search.go b/internal/frontend/search.go index de14a6c3c..8eb453716 100644 --- a/internal/frontend/search.go +++ b/internal/frontend/search.go @@ -43,11 +43,19 @@ func (s *Server) serveSearch(w http.ResponseWriter, r *http.Request, ds internal } ctx := r.Context() - query := searchQuery(r) - mode := searchMode(r) + query, filters := searchQueryAndFilters(r) if !utf8.ValidString(query) { return &serverError{status: http.StatusBadRequest} } + if len(filters) > 1 { + return &serverError{ + status: http.StatusBadRequest, + epage: &errorPage{ + messageTemplate: template.MakeTrustedTemplate( + `

Search query contains more than one symbol.

`), + }, + } + } if len(query) > maxSearchQueryLength { return &serverError{ status: http.StatusBadRequest, @@ -80,13 +88,17 @@ func (s *Server) serveSearch(w http.ResponseWriter, r *http.Request, ds internal }, } } - if path := searchRequestRedirectPath(ctx, ds, query); path != "" { http.Redirect(w, r, path, http.StatusFound) return nil } - page, err := fetchSearchPage(ctx, db, query, pageParams, mode == searchModeSymbol) + var symbol string + if len(filters) > 0 { + symbol = filters[0] + } + mode := searchMode(r) + page, err := fetchSearchPage(ctx, db, query, symbol, pageParams, mode == searchModeSymbol) if err != nil { return fmt.Errorf("fetchSearchPage(ctx, db, %q): %v", query, err) } @@ -172,7 +184,7 @@ type subResult struct { // fetchSearchPage fetches data matching the search query from the database and // returns a SearchPage. -func fetchSearchPage(ctx context.Context, db *postgres.DB, query string, +func fetchSearchPage(ctx context.Context, db *postgres.DB, query, symbol string, pageParams paginationParams, searchSymbols bool) (*SearchPage, error) { maxResultCount := maxSearchOffset + pageParams.limit @@ -180,13 +192,13 @@ func fetchSearchPage(ctx context.Context, db *postgres.DB, query string, if experiment.IsActive(ctx, internal.ExperimentSearchGrouping) { // When using search grouping, do pageless search: always start from the beginning. offset = 0 - query = strings.TrimLeft(query, symbolSearchFilter) } dbresults, err := db.Search(ctx, query, postgres.SearchOptions{ MaxResults: pageParams.limit, Offset: offset, MaxResultCount: maxResultCount, SearchSymbols: searchSymbols, + SymbolFilter: symbol, }) if err != nil { return nil, err @@ -300,11 +312,11 @@ func searchMode(r *http.Request) string { if !experiment.IsActive(r.Context(), internal.ExperimentSymbolSearch) { return searchModePackage } - q := searchQuery(r) + q := rawSearchQuery(r) if strings.HasPrefix(q, symbolSearchFilter) { return searchModeSymbol } - mode := strings.TrimSpace(r.FormValue("m")) + mode := rawSearchMode(r) if mode == searchModePackage { return searchModePackage } @@ -317,10 +329,30 @@ func searchMode(r *http.Request) string { return searchModePackage } -func searchQuery(r *http.Request) string { +// searchQueryAndFilters returns the search query, trimmed of any filters, and +// the array of words that had a filter prefix. +func searchQueryAndFilters(r *http.Request) (string, []string) { + words := strings.Fields(rawSearchQuery(r)) + var filters []string + for i := range words { + if strings.HasPrefix(words[i], symbolSearchFilter) { + words[i] = strings.TrimLeft(words[i], symbolSearchFilter) + filters = append(filters, words[i]) + } + } + return strings.Join(words, " "), filters +} + +// rawSearchQuery returns the exact search query by the user. +func rawSearchQuery(r *http.Request) string { return strings.TrimSpace(r.FormValue("q")) } +// rawSearchQuery returns the exact search mode from the URL request. +func rawSearchMode(r *http.Request) string { + return strings.TrimSpace(r.FormValue("m")) +} + // shouldDefaultToSymbolSearch reports whether the symbol search mode should // default to symbol search mode based on the input. func shouldDefaultToSymbolSearch(q string) bool { diff --git a/internal/frontend/search_test.go b/internal/frontend/search_test.go index 83ad8662c..614ecd178 100644 --- a/internal/frontend/search_test.go +++ b/internal/frontend/search_test.go @@ -207,7 +207,7 @@ func TestFetchSearchPage(t *testing.T) { }, } { t.Run(test.name, func(t *testing.T) { - got, err := fetchSearchPage(ctx, testDB, test.query, paginationParams{limit: 20, page: 1}, false) + got, err := fetchSearchPage(ctx, testDB, test.query, "", paginationParams{limit: 20, page: 1}, false) if err != nil { t.Fatalf("fetchSearchPage(db, %q): %v", test.query, err) } diff --git a/internal/frontend/server.go b/internal/frontend/server.go index 93367207d..671a7a89a 100644 --- a/internal/frontend/server.go +++ b/internal/frontend/server.go @@ -314,7 +314,7 @@ func (s *Server) licensePolicyHandler() http.HandlerFunc { // newBasePage returns a base page for the given request and title. func (s *Server) newBasePage(r *http.Request, title string) basePage { - q := searchQuery(r) + q := rawSearchQuery(r) return basePage{ HTMLTitle: title, Query: q, diff --git a/internal/postgres/search.go b/internal/postgres/search.go index f194e7371..014b486b9 100644 --- a/internal/postgres/search.go +++ b/internal/postgres/search.go @@ -109,6 +109,9 @@ type SearchOptions struct { // If true, perform a symbol search. SearchSymbols bool + + // SymbolFilter is the word in a search query with a # prefix. + SymbolFilter string } // SearchResult represents a single search result from SearchDocuments.