about summary refs log tree commit diff stats
path: root/internal
diff options
context:
space:
mode:
authorAlan Pearce2024-05-08 00:15:52 +0200
committerAlan Pearce2024-05-08 00:16:03 +0200
commit973345ad50f9b237714fcb364cf7f665b3909f9d (patch)
tree15225430bd5895b5140df0e301b0e6c3fb5758a8 /internal
parentf459e84ecf7307fe2eeb7fbaa5b0c50613ec04f4 (diff)
downloadsearchix-973345ad50f9b237714fcb364cf7f665b3909f9d.tar.lz
searchix-973345ad50f9b237714fcb364cf7f665b3909f9d.tar.zst
searchix-973345ad50f9b237714fcb364cf7f665b3909f9d.zip
feat: paginate search results
Diffstat (limited to 'internal')
-rw-r--r--internal/search/search.go14
-rw-r--r--internal/server/server.go57
2 files changed, 60 insertions, 11 deletions
diff --git a/internal/search/search.go b/internal/search/search.go
index b449512..83a8365 100644
--- a/internal/search/search.go
+++ b/internal/search/search.go
@@ -15,7 +15,7 @@ import (
 	"github.com/pkg/errors"
 )
 
-const maxResults = 10
+const ResultsPerPage = 20
 
 type Result[T options.NixOption] struct {
 	*bleve.SearchResult
@@ -93,9 +93,14 @@ func New[T options.NixOption](kind string) (*Index[T], error) {
 	}, nil
 }
 
-func (index *Index[T]) Search(ctx context.Context, keyword string) (*Result[T], error) {
+func (index *Index[T]) Search(ctx context.Context, keyword string, from uint64) (*Result[T], error) {
 	query := bleve.NewMatchQuery(keyword)
 	search := bleve.NewSearchRequest(query)
+	search.Size = ResultsPerPage
+
+	if from != 0 {
+		search.From = int(from)
+	}
 
 	bleveResult, err := index.index.SearchInContext(ctx, search)
 	select {
@@ -106,13 +111,10 @@ func (index *Index[T]) Search(ctx context.Context, keyword string) (*Result[T],
 			return nil, errors.WithMessage(err, "failed to execute search query")
 		}
 
-		results := make([]T, min(maxResults, bleveResult.Total))
+		results := make([]T, min(ResultsPerPage, bleveResult.Total))
 		for i, result := range bleveResult.Hits {
 			doc, _ := index.docs.Load(result.ID)
 			results[i] = doc.(T)
-			if i > maxResults {
-				break
-			}
 		}
 
 		return &Result[T]{
diff --git a/internal/server/server.go b/internal/server/server.go
index 9649ad2..db40e6f 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -9,9 +9,11 @@ import (
 	"log/slog"
 	"net"
 	"net/http"
+	"net/url"
 	"os"
 	"path"
 	"slices"
+	"strconv"
 	"time"
 
 	cfg "searchix/internal/config"
@@ -63,8 +65,11 @@ type TemplateData struct {
 
 type ResultData[T options.NixOption] struct {
 	TemplateData
-	Query   string
-	Results *search.Result[T]
+	Query          string
+	ResultsPerPage int
+	Results        *search.Result[T]
+	Prev           string
+	Next           string
 }
 
 func applyDevModeOverrides(config *cfg.Config) {
@@ -158,7 +163,16 @@ func New(runtimeConfig *Config) (*Server, error) {
 
 			return
 		}
-		results, err := index[source].Search(ctx, r.URL.Query().Get("query"))
+		qs := r.URL.Query().Get("query")
+		pg := r.URL.Query().Get("page")
+		var page uint64 = 1
+		if pg != "" {
+			page, err = strconv.ParseUint(pg, 10, 64)
+			if err != nil || page == 0 {
+				http.Error(w, "Bad query string", http.StatusBadRequest)
+			}
+		}
+		results, err := index[source].Search(ctx, qs, (page-1)*search.ResultsPerPage)
 		if err != nil {
 			if err == context.DeadlineExceeded {
 				http.Error(w, "Search timed out", http.StatusInternalServerError)
@@ -173,9 +187,42 @@ func New(runtimeConfig *Config) (*Server, error) {
 				LiveReload: jsSnippet,
 				Source:     source,
 			},
-			Query:   r.URL.Query().Get("query"),
-			Results: results,
+			ResultsPerPage: search.ResultsPerPage,
+			Query:          qs,
+			Results:        results,
 		}
+
+		hits := uint64(len(results.Hits))
+		if results.Total > hits {
+			q, err := url.ParseQuery(r.URL.RawQuery)
+			if err != nil {
+				http.Error(w, "Query string error", http.StatusBadRequest)
+
+				return
+			}
+
+			if page*search.ResultsPerPage > results.Total {
+				http.Error(w, "Not found", http.StatusNotFound)
+
+				return
+			}
+
+			if page*search.ResultsPerPage < results.Total {
+				q.Set("page", strconv.FormatUint(page+1, 10))
+				tdata.Next = "results?" + q.Encode()
+			}
+
+			if page > 1 {
+				p := page - 1
+				if p == 1 {
+					q.Del("page")
+				} else {
+					q.Set("page", strconv.FormatUint(p, 10))
+				}
+				tdata.Prev = "results?" + q.Encode()
+			}
+		}
+
 		if r.Header.Get("Fetch") == "true" {
 			w.Header().Add("Content-Type", "text/html; charset=utf-8")
 			err = templates["options"].ExecuteTemplate(w, "options.gotmpl", tdata)