Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

67 lines
2.4 KiB
Go

package query
import (
"sort"
"github.com/zzet/gortex/internal/search/rerank"
)
// crossRepoRerank reassigns per-channel candidate ranks repository by
// repository so a large repo's corpus size cannot bury a small repo's
// best hits in a multi-repo workspace.
//
// The backend indexes every tracked repo into one merged corpus, so a
// raw BM25 / vector rank reflects how a hit fares against ALL repos at
// once — a 30k-symbol repo's matches crowd out a 500-symbol repo's
// matches even when the latter are a better answer. Reassigning the
// TextRank / VectorRank counters per repo means each repo's #1 hit is
// rank 0, each repo's #2 hit is rank 1, and so on. The downstream
// rerank's bm25 and semantic signals use the reciprocal-rank (RRF)
// kernel, so per-repo ranks turn those signals into a genuine
// cross-repo RRF fusion — the structural and session signals then
// discriminate between the repos' top hits.
//
// No-op when the candidate set comes from a single repository (or is
// empty): the merged ranks are already a fair within-repo order.
func crossRepoRerank(cands []*rerank.Candidate) {
repos := make(map[string]struct{}, 4)
for _, c := range cands {
if c != nil && c.Node != nil {
repos[c.Node.RepoPrefix] = struct{}{}
}
}
if len(repos) < 2 {
return
}
reassignChannelPerRepo(cands, func(c *rerank.Candidate) *int { return &c.TextRank })
reassignChannelPerRepo(cands, func(c *rerank.Candidate) *int { return &c.VectorRank })
}
// reassignChannelPerRepo renumbers one channel's ranks (selected by
// rankOf) so the counter resets at each repository. Candidates absent
// from the channel (rank < 0) are left untouched. Ties on the prior
// global rank — the exact-name fallback tier assigns several
// candidates the same rank — break on node ID so the renumbering is
// deterministic.
func reassignChannelPerRepo(cands []*rerank.Candidate, rankOf func(*rerank.Candidate) *int) {
ranked := make([]*rerank.Candidate, 0, len(cands))
for _, c := range cands {
if c != nil && c.Node != nil && *rankOf(c) >= 0 {
ranked = append(ranked, c)
}
}
sort.SliceStable(ranked, func(i, j int) bool {
ri, rj := *rankOf(ranked[i]), *rankOf(ranked[j])
if ri != rj {
return ri < rj
}
return ranked[i].Node.ID < ranked[j].Node.ID
})
counter := make(map[string]int, 4)
for _, c := range ranked {
repo := c.Node.RepoPrefix
*rankOf(c) = counter[repo]
counter[repo]++
}
}