internal/postgres: improve vector search quality

Incorporate package popularity directly into the vector candidate
ranking CTE before computing the Reciprocal Rank Fusion to prevent
low-import package from diluting well-known packages.

Previously, vector search ranked candidates exclusively by semantic distance. This allowed low-quality or obscure packages to dominate the top vector results, pushing popular packages out of the aggregation window.

There is now this approach: 1. Fetch the top N nearest neighbors for the query using the index. 2. Rerank the candidates using a combined score that accounts for cosine similarity and popularity.

This ensures that the vector rank (rank_vec) provided better reflects package quality.

Change-Id: I1a5861bb5ba8268bf0c10fb129a2e0a3d7a8980b
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/815060
kokoro-CI: kokoro <noreply+kokoro@google.com>
Auto-Submit: Ethan Lee <ethanalee@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/postgres/search.go b/internal/postgres/search.go
index 56e0d03..6807d4e 100644
--- a/internal/postgres/search.go
+++ b/internal/postgres/search.go
@@ -315,6 +315,7 @@
 func buildVectorSearchQuery(opts SearchOptions, scoreExprStr string, q string, limit int) (string, []any) {
 	candidateLimit := max(100, opts.Offset+limit)
 	vectorWeight := sanitizeFloat(opts.ScoringParams.VectorWeight, 1.0)
+	popWeight := sanitizeFloat(opts.ScoringParams.PopularityWeight, 1.0)
 
 	query := fmt.Sprintf(`
 		WITH text_search AS (
@@ -327,11 +328,19 @@
 		),
 		vector_search AS (
 			SELECT package_path, version, module_path, commit_time, imported_by_count,
-				ROW_NUMBER() OVER (ORDER BY embedding <=> $2::halfvec, imported_by_count DESC) AS rank_vec
-			FROM search_documents
-			WHERE imported_by_count >= 1 AND embedding IS NOT NULL
-			ORDER BY embedding <=> $2::halfvec
-			LIMIT %d
+				ROW_NUMBER() OVER (
+					ORDER BY ((1.0 - (embedding <=> $2::halfvec)) * pow(ln(exp(1)+imported_by_count), %f) *
+					         CASE WHEN redistributable THEN 1 ELSE %f END *
+					         CASE WHEN COALESCE(has_go_mod, true) THEN 1 ELSE %f END) DESC,
+					         commit_time DESC, package_path
+				) AS rank_vec
+			FROM (
+				SELECT package_path, version, module_path, commit_time, imported_by_count, redistributable, has_go_mod, embedding
+				FROM search_documents
+				WHERE imported_by_count >= 1 AND embedding IS NOT NULL
+				ORDER BY embedding <=> $2::halfvec
+				LIMIT %d
+			) nn
 		),
 		combined AS (
 			SELECT
@@ -349,7 +358,7 @@
 		FROM combined
 		ORDER BY score DESC, commit_time DESC, package_path
 		LIMIT $3
-		OFFSET $4`, scoreExprStr, candidateLimit, candidateLimit, vectorWeight)
+		OFFSET $4`, scoreExprStr, candidateLimit, popWeight, nonRedistributablePenalty, noGoModPenalty, candidateLimit, vectorWeight)
 	args := []any{q, formatVector(opts.Vector), limit, opts.Offset}
 	return query, args
 }
diff --git a/internal/postgres/search_test.go b/internal/postgres/search_test.go
index d0467cf..6a75610 100644
--- a/internal/postgres/search_test.go
+++ b/internal/postgres/search_test.go
@@ -1641,6 +1641,12 @@
 	if !strings.Contains(queryDefault, "LIMIT 100") {
 		t.Errorf("expected default candidate limit 100, query was:\n%s", queryDefault)
 	}
+	if !strings.Contains(queryDefault, fmt.Sprintf("CASE WHEN redistributable THEN 1 ELSE %f END", nonRedistributablePenalty)) {
+		t.Errorf("expected query to contain redistributable penalty (%f)", nonRedistributablePenalty)
+	}
+	if !strings.Contains(queryDefault, fmt.Sprintf("CASE WHEN COALESCE(has_go_mod, true) THEN 1 ELSE %f END", noGoModPenalty)) {
+		t.Errorf("expected query to contain no-go-mod penalty (%f)", noGoModPenalty)
+	}
 	if len(args) != 4 {
 		t.Errorf("expected 4 query args, got %d", len(args))
 	}