From c670ac7019421d636fcfbcd003ce236a8bb662ab Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Fri, 13 Nov 2026 11:15:06 -0800 Subject: [PATCH] fix(search): stop zeroing the score of users and content with no followers or reposts The popularity factor multiplied the text score by the raw count, so an artist with 0 followers scored 0 and sorted below every fuzzy match, even on an exact handle match. Scale by count+1 instead. Co-Authored-By: Claude Fable 5.2 --- api/searchv1/dsl_helpers.go | 15 ++++++++++----- api/searchv1/user_search.go | 7 ++----- api/v1_search_test.go | 22 ++++++++++++++++++++++ 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/api/searchv1/dsl_helpers.go b/api/searchv1/dsl_helpers.go index e7a7bae4..b2386892 100644 --- a/api/searchv1/dsl_helpers.go +++ b/api/searchv1/dsl_helpers.go @@ -38,6 +38,14 @@ func sortWithField(innerQuery map[string]any, sortField, direction string) strin return dsl } +// countScoreFunction scales a score by count+1. A plain field_value_factor +// multiplies by the raw count, which zeroes out anything with a count of 0 +// and buries it below every other match, exact or not. +func countScoreFunction(field string) string { + source := fmt.Sprintf("(doc['%s'].size() == 0 ? 0 : doc['%s'].value) + 1", field, field) + return fmt.Sprintf(`"script_score": {"script": {"source": %q}}`, source) +} + func BuildFunctionScoreDSL(scoreField string, weight float64, innerQuery map[string]any) string { innerJson, err := json.Marshal(innerQuery) if err != nil { @@ -51,10 +59,7 @@ func BuildFunctionScoreDSL(scoreField string, weight float64, innerQuery map[str "query": %s, "functions": [ { - "field_value_factor": { - "field": %q, - "missing": 0 - }, + %s, "weight": %g } ], @@ -62,7 +67,7 @@ func BuildFunctionScoreDSL(scoreField string, weight float64, innerQuery map[str "score_mode": "multiply" } } - }`, innerJson, scoreField, weight) + }`, innerJson, countScoreFunction(scoreField), weight) return dsl } diff --git a/api/searchv1/user_search.go b/api/searchv1/user_search.go index 30b00f7c..2ff9e097 100644 --- a/api/searchv1/user_search.go +++ b/api/searchv1/user_search.go @@ -108,10 +108,7 @@ func buildUserFunctionScoreDSL(followerWeight float64, queryString string, inner "query": %s, "functions": [ { - "field_value_factor": { - "field": "follower_count", - "missing": 0 - }, + %s, "weight": %g }, { @@ -147,7 +144,7 @@ func buildUserFunctionScoreDSL(followerWeight float64, queryString string, inner "score_mode": "multiply" } } - }`, innerJson, followerWeight, queryNoSpaces, queryNoSpaces) + }`, innerJson, countScoreFunction("follower_count"), followerWeight, queryNoSpaces, queryNoSpaces) return dsl } diff --git a/api/v1_search_test.go b/api/v1_search_test.go index 82edcca3..0bedcb7d 100644 --- a/api/v1_search_test.go +++ b/api/v1_search_test.go @@ -36,6 +36,16 @@ func TestSearch(t *testing.T) { "handle": "find_infix_works", "name": "asdf", }, + { + "user_id": 1005, + "handle": "zerofans", + "name": "zerofans", + }, + { + "user_id": 1006, + "handle": "zerofansclub", + "name": "zerofansclub", + }, }, "tracks": { { @@ -151,6 +161,7 @@ func TestSearch(t *testing.T) { }, "follows": { {"follower_user_id": 1001, "followee_user_id": 1002}, + {"follower_user_id": 1001, "followee_user_id": 1006}, }, "reposts": { { @@ -211,6 +222,17 @@ func TestSearch(t *testing.T) { }) } + // users: an exact handle match with no followers still ranks first + { + status, body := testGet(t, app, "/v1/users/search?query=zerofans") + require.Equal(t, 200, status) + jsonAssert(t, body, map[string]any{ + "data.#": 2, + "data.0.handle": "zerofans", + "data.1.handle": "zerofansclub", + }) + } + { status, body := testGet(t, app, "/v1/users/search?query=monoist") require.Equal(t, 200, status)