blob: 48c40352201531e52505f66259916b22abbec1e4 [file] [edit]
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"strconv"
"strings"
"contrib.go.opencensus.io/integrations/ocsql"
_ "github.com/jackc/pgx/v5/stdlib"
"golang.org/x/pkgsite/internal/config"
"golang.org/x/pkgsite/internal/config/serverconfig"
"golang.org/x/pkgsite/internal/database"
"golang.org/x/pkgsite/internal/embeddings"
"golang.org/x/pkgsite/internal/postgres"
)
func openDB(cfg *config.Config) (*postgres.DB, error) {
ocDriver, err := database.RegisterOCWrapper("pgx", ocsql.WithAllTraceOptions())
if err != nil {
return nil, fmt.Errorf("unable to register the ocsql driver: %v", err)
}
ddb, err := database.Open(ocDriver, cfg.DBConnInfo(), cfg.InstanceID)
if err != nil {
return nil, err
}
ddb.SetPoolSettings(cfg.DBMaxOpenConns, cfg.DBMaxIdleConns, cfg.DBConnMaxLifetime, cfg.DBConnMaxIdleTime)
return postgres.NewBypassingLicenseCheck(ddb), nil
}
func printHelp() {
fmt.Fprintf(os.Stderr, `searcheval - Search Quality Evaluation & Tuning Tool
Usage:
searcheval <command> [flags] [args]
Commands:
eval Run MRR@5 quality evaluation against ground-truth golden queries dataset.
log-eval Run Keyword vs Hybrid comparative evaluation against raw query logs list.
search <q> Run interactive side-by-side search debugging for a query string.
embed Generate Vertex AI embeddings for packages in search_documents.
seed [mods] Seed 100 popular Go modules (or custom modules passed as args) into a local PostgreSQL instance.
help Show this help guide.
Flags (for eval, log-eval, search):
-queries <path>
Path to golden queries JSON file (default: testdata/golden_queries.json)
-log-queries <path>
Path to raw query log list file (default: private/devtools/cmd/search/20210721-10m.queries)
-text-weights <D,C,B,A>
Comma-separated ts_rank section weights for keyword search (e.g. "0.1,0.2,1.0,1.0")
-popularity-weight <float>
Import count popularity exponent parameter (default: uses DB config)
-vector-weight <float>
Reciprocal Rank Fusion (RRF) vector weight (default: uses DB config)
-limit <int>
Number of search results to display per engine (default: 10)
Examples:
# Run MRR evaluation on golden queries dataset
go run ./devtools/cmd/searcheval eval
# Run evaluation with custom vector RRF weight
go run ./devtools/cmd/searcheval eval -vector-weight=1.5
# Run comparative evaluation on production query logs
go run ./devtools/cmd/searcheval log-eval
# Interactive search comparison for a query string
go run ./devtools/cmd/searcheval search "structured logger"
# Seed custom modules into local database
go run ./devtools/cmd/searcheval seed github.com/go-chi/chi/v5@v5.0.12 github.com/gin-gonic/gin@v1.9.1
`)
}
func main() {
if len(os.Args) < 2 {
printHelp()
os.Exit(1)
}
cmd := os.Args[1]
if cmd == "help" || cmd == "-help" || cmd == "--help" || cmd == "-h" {
printHelp()
os.Exit(0)
}
ctx := context.Background()
cfg, err := serverconfig.Init(ctx)
if err != nil {
log.Fatalf("serverconfig.Init: %v", err)
}
db, err := openDB(cfg)
if err != nil {
log.Fatalf("openDB: %v", err)
}
defer db.Close()
cfg.EnableVectorSearch = true
if cfg.LocationID == "" {
cfg.LocationID = "us-central1"
}
client, err := embeddings.NewClient(ctx, cfg)
if err != nil {
log.Fatalf("embeddings.NewClient (Vertex AI): %v", err)
}
searcher := &dbSearcher{db: db, client: client}
switch cmd {
case "eval":
evalCmd := flag.NewFlagSet("eval", flag.ExitOnError)
queriesFile := evalCmd.String("queries", "devtools/cmd/searcheval/testdata/golden_queries.json", "path to golden queries JSON file")
textWeights := evalCmd.String("text-weights", "", "comma-separated ts_rank weights (D,C,B,A)")
popWeight := evalCmd.Float64("popularity-weight", -1.0, "popularity exponent parameter")
vectorWeight := evalCmd.Float64("vector-weight", -1.0, "RRF vector search weight")
evalCmd.Parse(os.Args[2:])
scoringParams := parseScoringParams(*textWeights, *popWeight, *vectorWeight)
runEvalCmd(ctx, searcher, *queriesFile, scoringParams)
case "log-eval", "prod-eval":
logEvalCmd := flag.NewFlagSet("log-eval", flag.ExitOnError)
logQueries := logEvalCmd.String("log-queries", "private/devtools/cmd/search/20210721-10m.queries", "path to raw query log list file")
textWeights := logEvalCmd.String("text-weights", "", "comma-separated ts_rank weights (D,C,B,A)")
popWeight := logEvalCmd.Float64("popularity-weight", -1.0, "popularity exponent parameter")
vectorWeight := logEvalCmd.Float64("vector-weight", -1.0, "RRF vector search weight")
logEvalCmd.Parse(os.Args[2:])
scoringParams := parseScoringParams(*textWeights, *popWeight, *vectorWeight)
runLogEvalCmd(ctx, searcher, *logQueries, scoringParams)
case "search":
searchCmd := flag.NewFlagSet("search", flag.ExitOnError)
limit := searchCmd.Int("limit", 10, "number of search results to display")
textWeights := searchCmd.String("text-weights", "", "comma-separated ts_rank weights (D,C,B,A)")
popWeight := searchCmd.Float64("popularity-weight", -1.0, "popularity exponent parameter")
vectorWeight := searchCmd.Float64("vector-weight", -1.0, "RRF vector search weight")
searchCmd.Parse(os.Args[2:])
if searchCmd.NArg() < 1 {
log.Fatalf("search command requires a query string argument")
}
query := strings.Join(searchCmd.Args(), " ")
scoringParams := parseScoringParams(*textWeights, *popWeight, *vectorWeight)
runSearchCmd(ctx, searcher, query, *limit, scoringParams)
case "seed":
seedCmd := flag.NewFlagSet("seed", flag.ExitOnError)
seedCmd.Parse(os.Args[2:])
runSeed(ctx, db, cfg, seedCmd.Args())
case "embed":
embedCmd := flag.NewFlagSet("embed", flag.ExitOnError)
embedCmd.Parse(os.Args[2:])
runEmbed(ctx, db, client)
default:
printHelp()
os.Exit(1)
}
}
func parseScoringParams(rawTextWeights string, popWeight, vectorWeight float64) postgres.SearchScoringParams {
params := postgres.SearchScoringParams{
TextWeights: [4]float64{0.1, 0.2, 1.0, 1.0},
PopularityWeight: 1.0,
VectorWeight: 1.0,
}
if rawTextWeights != "" {
parts := strings.Split(rawTextWeights, ",")
if len(parts) != 4 {
log.Fatalf("invalid text-weights format, must be 4 comma-separated values")
}
for i, p := range parts {
val, err := strconv.ParseFloat(p, 64)
if err != nil {
log.Fatalf("invalid text-weight value: %v", err)
}
params.TextWeights[i] = val
}
}
if popWeight >= 0 {
params.PopularityWeight = popWeight
}
if vectorWeight >= 0 {
params.VectorWeight = vectorWeight
}
return params
}