diff --git a/internal/handler/search.go b/internal/handler/search.go index 8fdbeee..a944167 100644 --- a/internal/handler/search.go +++ b/internal/handler/search.go @@ -70,6 +70,20 @@ var collectionQueryByWeights = map[string]string{ "other_documents": "10,10,2,2", } +// collectionTextMatchType lets specific collections opt-in to `sum_score` +// instead of the Typesense default `max_score`. +// +// `sum_score` boosts documents that match in MULTIPLE fields, so e.g. a letter +// whose `recipient_display` AND `content` both contain the query ranks above +// a letter that only matches in `content`. Useful for collections that have a +// strong metadata field (recipient, title) tied to the user's intent. +// +// `max_score` (default elsewhere) preserves single-field dominance — important +// for verses where the canonical verse should beat commentaries that quote it. +var collectionTextMatchType = map[string]string{ + "letters": "sum_score", +} + // searchResultItem represents a single search result in the API response. type searchResultItem struct { Collection string `json:"collection"` @@ -206,6 +220,9 @@ func (h *SearchHandler) multiSearch(ctx context.Context, collections []string, q if weights != "" { params.QueryByWeights = stringPtr(weights) } + if tmt := collectionTextMatchType[col]; tmt != "" { + params.TextMatchType = stringPtr(tmt) + } searches = append(searches, params) } @@ -278,6 +295,9 @@ func (h *SearchHandler) singleSearch(ctx context.Context, collection, query stri if weights := collectionQueryByWeights[collection]; weights != "" { params.QueryByWeights = stringPtr(weights) } + if tmt := collectionTextMatchType[collection]; tmt != "" { + params.TextMatchType = stringPtr(tmt) + } sr, err := h.ts.Client.Collection(collection).Documents().Search(ctx, params) if err != nil {