feat: search multiple sources
Alan Pearce alan@alanpearce.eu
Tue, 07 May 2024 21:32:36 +0200
4 files changed, 60 insertions(+), 42 deletions(-)
M frontend/templates/blocks/search.gotmpl → frontend/templates/blocks/search.gotmpl
@@ -2,7 +2,7 @@ {{- template "main" . }} {{- template "js" . }} {{- define "main" }} - <form id="search" action="/options/{{ .Source }}/results"> + <form id="search" action="results"> <label for="query">{{ sourceName .Source }} option search</label> <fieldset> <input id="query" name="query" type="search" value="{{ .Query }}" />
M frontend/templates/index.gotmpl → frontend/templates/index.gotmpl
@@ -11,9 +11,9 @@ <body> <header> <nav> <h1><a href="/">Searchix</a></h1> - <a href="/search/nixos">NixOS</a> - <a href="/search/darwin">Darwin</a> - <a href="/search/home-manager">Home Manager</a> + <a href="/options/nixos/search">NixOS</a> + <a href="/options/darwin/search">Darwin</a> + <a href="/options/home-manager/search">Home Manager</a> </nav> </header> <main>
M internal/search/search.go → internal/search/search.go
@@ -17,15 +17,17 @@ ) const maxResults = 10 -var index bleve.Index -var docs sync.Map - type Result[T options.NixOption] struct { *bleve.SearchResult Results []T } -func Index() error { +type Index[T options.NixOption] struct { + index bleve.Index + docs *sync.Map +} + +func New[T options.NixOption](kind string) (*Index[T], error) { bleve.SetLog(log.Default()) textFieldMapping := bleve.NewTextFieldMapping() nameMapping := bleve.NewTextFieldMapping() @@ -41,18 +43,19 @@ indexMapping := bleve.NewIndexMapping() indexMapping.AddDocumentMapping("option", optionMapping) var err error - index, err = bleve.NewMemOnly(indexMapping) + index, err := bleve.NewMemOnly(indexMapping) // index, err = bleve.New(path.Join(cfg.DataPath, const indexFilename = "index.bleve"), indexMapping) if err != nil { - return errors.WithMessage(err, "error opening index") + return nil, errors.WithMessage(err, "error opening index") } batch := index.NewBatch() - _ = batch + + var docs sync.Map - jsonFile, err := os.Open(path.Join("data", "processed", "darwin-options.json")) + jsonFile, err := os.Open(path.Join("data", "processed", kind+".json")) if err != nil { - return errors.WithMessage(err, "error opening json file") + return nil, errors.WithMessage(err, "error opening json file") } dec := jstream.NewDecoder(jsonFile, 1) @@ -63,35 +66,38 @@ ZeroFields: true, Result: &opt, }) if err != nil { - return errors.WithMessage(err, "could not create struct decoder") + return nil, errors.WithMessage(err, "could not create struct decoder") } for mv := range dec.Stream() { err := ms.Decode(mv.Value) // stores in opt if err != nil { - return errors.WithMessagef(err, "could not decode object into option, object: %#v", mv.Value) + return nil, errors.WithMessagef(err, "could not decode object into option, object: %#v", mv.Value) } docs.Store(opt.Option, opt) err = batch.Index(opt.Option, opt) if err != nil { - return errors.WithMessagef(err, "could not index option %s", opt.Option) + return nil, errors.WithMessagef(err, "could not index option %s", opt.Option) } } err = index.Batch(batch) if err != nil { - return errors.WithMessage(err, "failed to run batch index operation") + return nil, errors.WithMessage(err, "failed to run batch index operation") } - return nil + return &Index[T]{ + index, + &docs, + }, nil } -func Search[T options.NixOption](ctx context.Context, keyword string) (*Result[T], error) { +func (index *Index[T]) Search(ctx context.Context, keyword string) (*Result[T], error) { query := bleve.NewMatchQuery(keyword) search := bleve.NewSearchRequest(query) - bleveResult, err := index.SearchInContext(ctx, search) + bleveResult, err := index.index.SearchInContext(ctx, search) select { case <-ctx.Done(): return nil, ctx.Err() @@ -102,7 +108,7 @@ } results := make([]T, min(maxResults, bleveResult.Total)) for i, result := range bleveResult.Hits { - doc, _ := docs.Load(result.ID) + doc, _ := index.docs.Load(result.ID) results[i] = doc.(T) if i > maxResults { break @@ -115,9 +121,3 @@ results, }, nil } } - -func Load(name string) any { - doc, _ := docs.Load(name) - - return doc -}
M internal/server/server.go → internal/server/server.go
@@ -72,10 +72,23 @@ config.CSP.ScriptSrc = slices.Insert(config.CSP.ScriptSrc, 0, "'unsafe-inline'") config.CSP.ConnectSrc = slices.Insert(config.CSP.ConnectSrc, 0, "'self'") } +var index = map[string]*search.Index[options.NixOption]{} + +var sourceFileName = map[string]string{ + "darwin": "darwin-options", + "home-manager": "home-manager-options", + "nixos": "nixos-options-nixos-unstable", +} + func init() { - err := search.Index() - if err != nil { - log.Fatalf("could not build search index, error: %#v", err) + var err error + + for source, filename := range sourceFileName { + index[source], err = search.New(filename) + if err != nil { + log.Fatalf("could not build search index, error: %#v", err) + } + } } @@ -122,19 +135,19 @@ http.Error(w, err.Error(), http.StatusInternalServerError) } }) - mux.HandleFunc("/search/{source}", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/options/{source}/search", func(w http.ResponseWriter, r *http.Request) { source := r.PathValue("source") - switch source { - case "nixos", "darwin", "home-manager": - err := templates["search"].Execute(w, TemplateData{ - LiveReload: jsSnippet, - Source: source, - }) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - default: + if index[source] == nil { http.Error(w, "Unknown source", http.StatusNotFound) + + return + } + err := templates["search"].Execute(w, TemplateData{ + LiveReload: jsSnippet, + Source: source, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) } }) @@ -143,7 +156,12 @@ mux.HandleFunc("/options/{source}/results", func(w http.ResponseWriter, r *http.Request) { source := r.PathValue("source") ctx, cancel := context.WithTimeoutCause(r.Context(), timeout, errors.New("timeout")) defer cancel() - results, err := search.Search(ctx, r.URL.Query().Get("query")) + if index[source] == nil { + http.Error(w, "Unknown source", http.StatusNotFound) + + return + } + results, err := index[source].Search(ctx, r.URL.Query().Get("query")) if err != nil { if err == context.DeadlineExceeded { http.Error(w, "Search timed out", http.StatusInternalServerError)