diff options
author | Alan Pearce | 2024-07-12 20:52:52 +0200 |
---|---|---|
committer | Alan Pearce | 2024-07-12 21:20:15 +0200 |
commit | 6781684647ad7f131054a93e1d13105d865c0e83 (patch) | |
tree | 307a688125f18128f582f7754af9a5451f0cb109 | |
parent | 442345898802e93b0b884fd0f1c3492e08c34049 (diff) | |
download | searchix-6781684647ad7f131054a93e1d13105d865c0e83.tar.lz searchix-6781684647ad7f131054a93e1d13105d865c0e83.tar.zst searchix-6781684647ad7f131054a93e1d13105d865c0e83.zip |
feat: enable combined search
-rw-r--r-- | internal/components/combined.templ | 38 | ||||
-rw-r--r-- | internal/components/data.go | 3 | ||||
-rw-r--r-- | internal/components/page.templ | 26 | ||||
-rw-r--r-- | internal/components/results.templ | 19 | ||||
-rw-r--r-- | internal/config/importer-type.go | 7 | ||||
-rw-r--r-- | internal/index/search.go | 69 | ||||
-rw-r--r-- | internal/server/mux.go | 50 |
7 files changed, 158 insertions, 54 deletions
diff --git a/internal/components/combined.templ b/internal/components/combined.templ new file mode 100644 index 0000000..eeacdb5 --- /dev/null +++ b/internal/components/combined.templ @@ -0,0 +1,38 @@ +package components + +import ( + "go.alanpearce.eu/searchix/internal/index" + "go.alanpearce.eu/searchix/internal/nix" +) + +templ Combined(result *index.Result) { + <table> + <thead> + <tr> + <th scope="col">Attribute</th> + <th scope="col">Description</th> + </tr> + </thead> + <tbody> + for _, hit := range result.Hits { + <tr> + <td> + @openCombinedDialogLink(nix.GetKey(hit.Data)) + </td> + <td> + switch hit.Data.(type) { + case nix.Option: + if o := convertMatch[nix.Option](hit.Data); o != nil { + @markdown(firstSentence(o.Description)) + } + case nix.Package: + if o := convertMatch[nix.Package](hit.Data); o != nil { + { firstSentence(o.Description) } + } + } + </td> + </tr> + } + </tbody> + </table> +} diff --git a/internal/components/data.go b/internal/components/data.go index 8f4fe62..601102d 100644 --- a/internal/components/data.go +++ b/internal/components/data.go @@ -11,9 +11,8 @@ import ( type TemplateData struct { Sources []*config.Source - Source config.Source + Source *config.Source Query string - Results bool SourceResult *bleve.SearchResult ExtraHeadHTML string Code int diff --git a/internal/components/page.templ b/internal/components/page.templ index da0322e..ade677f 100644 --- a/internal/components/page.templ +++ b/internal/components/page.templ @@ -18,11 +18,17 @@ templ Page(tdata TemplateData) { <link href={ sheet.URL } rel="stylesheet" integrity={ "sha256-" + sheet.Base64SHA256 }/> } @Unsafe(tdata.ExtraHeadHTML) + <link + rel="search" + type="application/opensearchdescription+xml" + title={ "Searchix " + sourceNameAndType(nil) } + href={ string(joinPath("/all", "opensearch.xml")) } + /> for _, source := range tdata.Sources { <link rel="search" type="application/opensearchdescription+xml" - title={ "Searchix " + sourceNameAndType(*source) } + title={ "Searchix " + sourceNameAndType(source) } href={ string(joinPath("/", source.Importer.String(), source.Key, "opensearch.xml")) } /> } @@ -31,9 +37,19 @@ templ Page(tdata TemplateData) { <header> <nav> <h1><a href="/">Searchix</a></h1> + <a + if tdata.Source == nil { + if tdata.SourceResult != nil || tdata.Query != "" { + class="current" + } + href="/all/search" + } else { + href={ joinPathQuery("/all/search", tdata.Query) } + } + >All</a> for _, source := range tdata.Sources { <a - if tdata.Source.Name == source.Name { + if tdata.Source != nil && tdata.Source.Name == source.Name { class="current" href={ joinPath("/", source.Importer.String(), source.Key, "search") } } else { @@ -65,7 +81,11 @@ func Unsafe(html string) templ.Component { }) } -func sourceNameAndType(source config.Source) string { +func sourceNameAndType(source *config.Source) string { + if source == nil { + return "Combined" + } + switch source.Importer { case config.Options: return source.Name + " " + source.Importer.String() diff --git a/internal/components/results.templ b/internal/components/results.templ index 6d85e16..226b71e 100644 --- a/internal/components/results.templ +++ b/internal/components/results.templ @@ -3,6 +3,7 @@ package components import ( "strconv" "go.alanpearce.eu/searchix/internal/nix" + "go.alanpearce.eu/searchix/internal/config" ) func convertMatch[I nix.Importable](m nix.Importable) *I { @@ -16,11 +17,15 @@ func convertMatch[I nix.Importable](m nix.Importable) *I { templ Results(r ResultData) { if r.Query != "" { if r.Results != nil && r.Results.Total > 0 { - switch r.Results.Hits[0].Data.(type) { - case nix.Option: - @Options(r.Results) - case nix.Package: - @Packages(r.Results) + if r.Source != nil { + switch r.Source.Importer { + case config.Options: + @Options(r.Results) + case config.Packages: + @Packages(r.Results) + } + } else { + @Combined(r.Results) } <footer aria-label="pagination"> <nav id="pagination"> @@ -50,3 +55,7 @@ templ ResultsPage(r ResultData) { templ openDialogLink(attr string) { <a class="open-dialog" href={ templ.SafeURL(attr) }>{ attr }</a> } + +templ openCombinedDialogLink(attr string) { + <a class="open-dialog" href={ templ.SafeURL("/" + attr) }>{ attr }</a> +} diff --git a/internal/config/importer-type.go b/internal/config/importer-type.go index 9bd50b6..6003d7d 100644 --- a/internal/config/importer-type.go +++ b/internal/config/importer-type.go @@ -9,13 +9,16 @@ import ( type ImporterType int const ( - UnknownType = iota + All = iota - 1 + UnknownType Packages Options ) func (i ImporterType) String() string { switch i { + case All: + return "combined" case Packages: return "packages" case Options: @@ -27,6 +30,8 @@ func (i ImporterType) String() string { func (i ImporterType) Singular() string { switch i { + case All: + return "combined" case Packages: return "package" case Options: diff --git a/internal/index/search.go b/internal/index/search.go index 03a8e60..c5b8c6b 100644 --- a/internal/index/search.go +++ b/internal/index/search.go @@ -19,7 +19,7 @@ import ( const ResultsPerPage = 20 type DocumentMatch struct { - search.DocumentMatch + *search.DocumentMatch Data nix.Importable } @@ -53,9 +53,18 @@ func (index *ReadIndex) GetEnabledSources() ([]string, error) { return enabledSources, nil } -func (index *ReadIndex) GetSource(ctx context.Context, name string) (*bleve.SearchResult, error) { - query := bleve.NewTermQuery(name) - query.SetField("Source") +func (index *ReadIndex) GetSource( + ctx context.Context, + source *config.Source, +) (*bleve.SearchResult, error) { + var query query.Query + if source == nil { + query = bleve.NewMatchAllQuery() + } else { + tq := bleve.NewTermQuery(source.Name) + tq.SetField("Source") + query = tq + } search := bleve.NewSearchRequest(query) result, err := index.index.SearchInContext(ctx, search) @@ -68,7 +77,7 @@ func (index *ReadIndex) GetSource(ctx context.Context, name string) (*bleve.Sear return nil, errors.WithMessagef( err, "failed to execute search to find source %s in index", - name, + source, ) } } @@ -89,7 +98,7 @@ func (index *ReadIndex) search( ctx context.Context, request *bleve.SearchRequest, ) (*Result, error) { - request.Fields = []string{"_data"} + request.Fields = []string{"_data", "Source"} bleveResult, err := index.index.SearchInContext(ctx, request) select { @@ -103,6 +112,7 @@ func (index *ReadIndex) search( results := make([]DocumentMatch, min(ResultsPerPage, bleveResult.Total)) var buf bytes.Buffer for i, result := range bleveResult.Hits { + results[i].DocumentMatch = bleveResult.Hits[i] _, err = buf.WriteString(result.Fields["_data"].(string)) if err != nil { return nil, errors.WithMessage(err, "error fetching result data") @@ -133,37 +143,28 @@ func (index *ReadIndex) Search( userQuery := bleve.NewMatchQuery(keyword) userQuery.Analyzer = "option_name" - query.AddMust( - setField(bleve.NewTermQuery(source.Key), "Source"), - userQuery, - ) - - switch source.Importer { - case config.Packages: - // ...and boost it if it matches any of these - query.AddShould( - setField(bleve.NewMatchQuery(keyword), "MainProgram"), - setField(bleve.NewMatchQuery(keyword), "Name"), - setField(bleve.NewMatchQuery(keyword), "Attribute"), - ) - case config.Options: - query.AddShould( - setField(bleve.NewMatchQuery(keyword), "Loc"), - setField(bleve.NewMatchQuery(keyword), "Name"), - ) - nameLiteralQuery := bleve.NewMatchQuery(keyword) - nameLiteralQuery.SetField("Name") - nameLiteralQuery.Analyzer = standard.Name - query.AddShould( - nameLiteralQuery, - ) - default: - return nil, errors.Errorf( - "unsupported source type for search: %s", - source.Importer.String(), + if source != nil { + query.AddMust( + setField(bleve.NewTermQuery(source.Key), "Source"), + userQuery, ) } + // ...and boost it if it matches any of these + query.AddShould( + setField(bleve.NewMatchQuery(keyword), "MainProgram"), + setField(bleve.NewMatchQuery(keyword), "Name"), + setField(bleve.NewMatchQuery(keyword), "Attribute"), + ) + query.AddShould( + setField(bleve.NewMatchQuery(keyword), "Loc"), + setField(bleve.NewMatchQuery(keyword), "Name"), + ) + nameLiteralQuery := bleve.NewMatchQuery(keyword) + nameLiteralQuery.SetField("Name") + nameLiteralQuery.Analyzer = standard.Name + query.AddShould(nameLiteralQuery) + search := bleve.NewSearchRequest(query) search.Size = ResultsPerPage diff --git a/internal/server/mux.go b/internal/server/mux.go index d1a0e38..f98e03e 100644 --- a/internal/server/mux.go +++ b/internal/server/mux.go @@ -89,11 +89,14 @@ func NewMux( createSearchHandler := func(importerType config.ImporterType) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var err error - source := cfg.Importer.Sources[r.PathValue("source")] - if source == nil || importerType != source.Importer { - errorHandler(w, r, http.StatusText(http.StatusNotFound), http.StatusNotFound) + var source *config.Source + if importerType != config.All { + source = cfg.Importer.Sources[r.PathValue("source")] + if source == nil || importerType != source.Importer { + errorHandler(w, r, http.StatusText(http.StatusNotFound), http.StatusNotFound) - return + return + } } ctx, cancel := context.WithTimeout(r.Context(), searchTimeout) @@ -127,7 +130,7 @@ func NewMux( tdata := components.ResultData{ TemplateData: components.TemplateData{ ExtraHeadHTML: cfg.Web.ExtraHeadHTML, - Source: *source, + Source: source, Sources: sources, Assets: frontend.Assets, Query: qs, @@ -181,19 +184,18 @@ func NewMux( errorHandler(w, r, err.Error(), http.StatusInternalServerError) } } else { - sourceResult, err := index.GetSource(ctx, source.Key) + sourceResult, err := index.GetSource(ctx, source) if err != nil { errorHandler(w, r, err.Error(), http.StatusInternalServerError) return } - w.Header().Add("Cache-Control", "max-age=14400") err = components.SearchPage( components.TemplateData{ ExtraHeadHTML: cfg.Web.ExtraHeadHTML, Sources: sources, - Source: *source, + Source: source, SourceResult: sourceResult, Assets: frontend.Assets, }, @@ -208,6 +210,7 @@ func NewMux( } } + mux.HandleFunc("/all/search", createSearchHandler(config.All)) mux.HandleFunc("/options/{source}/search", createSearchHandler(config.Options)) mux.HandleFunc("/packages/{source}/search", createSearchHandler(config.Packages)) @@ -245,7 +248,7 @@ func NewMux( tdata := components.DocumentData{ TemplateData: components.TemplateData{ ExtraHeadHTML: cfg.Web.ExtraHeadHTML, - Source: *source, + Source: source, Sources: sources, Assets: frontend.Assets, }, @@ -265,6 +268,8 @@ func NewMux( } mux.HandleFunc("/options/{source}/{id}", createSourceIDHandler(config.Options)) mux.HandleFunc("/packages/{source}/{id}", createSourceIDHandler(config.Packages)) + mux.HandleFunc("/option/{source}/{id}", createSourceIDHandler(config.Options)) + mux.HandleFunc("/package/{source}/{id}", createSourceIDHandler(config.Packages)) createOpenSearchXMLHandler := func(importerType config.ImporterType) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { @@ -312,6 +317,33 @@ func NewMux( mux.HandleFunc("/options/{source}/opensearch.xml", createOpenSearchXMLHandler(config.Options)) mux.HandleFunc("/packages/{source}/opensearch.xml", createOpenSearchXMLHandler(config.Packages)) + mux.HandleFunc("/all/opensearch.xml", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Cache-Control", "max-age=604800") + w.Header().Set("Content-Type", "application/opensearchdescription+xml") + osd := &opensearch.Description{ + ShortName: "Searchix Combined", + LongName: "Search nix options and packages with Searchix", + Description: "Search nix options and packages with Searchix", + SearchForm: cfg.Web.BaseURL.JoinPath("all/search"), + URL: opensearch.URL{ + Method: "get", + Type: "text/html", + Template: cfg.Web.BaseURL.JoinPath("all/search"). + AddRawQuery("query", "{searchTerms}"), + }, + } + enc := xml.NewEncoder(w) + enc.Indent("", " ") + err := enc.Encode(osd) + if err != nil { + // no errorHandler; HTML does not make sense here + http.Error( + w, + fmt.Sprintf("OpenSearch XML encoding error: %v", err), + http.StatusInternalServerError, + ) + } + }) mux.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) { asset, found := frontend.Assets.ByPath[r.URL.Path] |