detached 46e2e0d

refactor: remove map lookups from history ranking sort

commit
46e2e0d37194eb05ddda364d17888828477c55f6
author
brice · · 2026-03-11 03:58 UTC
parent
4524185
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +145 -24
M internal/infrastructure/persistence/sqlite/history_repo.go +47 -24
@@ -210,43 +210,66 @@ func rankHistoryMatches(
210210 }
211211
212212 queryLower := strings.ToLower(strings.TrimSpace(query))
213- matches := make([]entity.HistoryMatch, 0, len(candidates))
213+ ranked := make([]rankedHistoryMatch, 0, len(candidates))
214214 for _, candidate := range candidates {
215215 candidate.score = computeHistoryCandidateScore(candidate, queryLower, words)
216- matches = append(matches, entity.HistoryMatch{
217- Entry: candidate.entry,
218- Score: candidate.score,
216+ ranked = append(ranked, rankedHistoryMatch{
217+ match: entity.HistoryMatch{
218+ Entry: candidate.entry,
219+ Score: candidate.score,
220+ },
221+ score: candidate.score,
222+ exactURLMatch: candidate.exactURLMatch,
223+ prefixURL: candidate.prefixURL,
224+ urlMatch: candidate.urlMatch,
225+ prefixTitle: candidate.prefixTitle,
219226 })
220227 }
221228
222- sort.Slice(matches, func(i, j int) bool {
223- left := candidates[matches[i].Entry.ID]
224- right := candidates[matches[j].Entry.ID]
225- if matches[i].Score != matches[j].Score {
226- return matches[i].Score > matches[j].Score
229+ sortRankedHistoryMatches(ranked)
230+
231+ matches := make([]entity.HistoryMatch, len(ranked))
232+ for i := range ranked {
233+ matches[i] = ranked[i].match
234+ }
235+ return matches
236+}
237+
238+type rankedHistoryMatch struct {
239+ match entity.HistoryMatch
240+ score float64
241+ exactURLMatch bool
242+ prefixURL bool
243+ urlMatch bool
244+ prefixTitle bool
245+}
246+
247+func sortRankedHistoryMatches(items []rankedHistoryMatch) {
248+ sort.Slice(items, func(i, j int) bool {
249+ li, ri := &items[i], &items[j]
250+ if li.score != ri.score {
251+ return li.score > ri.score
227252 }
228- if left.exactURLMatch != right.exactURLMatch {
229- return left.exactURLMatch
253+ if li.exactURLMatch != ri.exactURLMatch {
254+ return li.exactURLMatch
230255 }
231- if left.prefixURL != right.prefixURL {
232- return left.prefixURL
256+ if li.prefixURL != ri.prefixURL {
257+ return li.prefixURL
233258 }
234- if left.urlMatch != right.urlMatch {
235- return left.urlMatch
259+ if li.urlMatch != ri.urlMatch {
260+ return li.urlMatch
236261 }
237- if left.prefixTitle != right.prefixTitle {
238- return left.prefixTitle
262+ if li.prefixTitle != ri.prefixTitle {
263+ return li.prefixTitle
239264 }
240- if matches[i].Entry.VisitCount != matches[j].Entry.VisitCount {
241- return matches[i].Entry.VisitCount > matches[j].Entry.VisitCount
265+ if li.match.Entry.VisitCount != ri.match.Entry.VisitCount {
266+ return li.match.Entry.VisitCount > ri.match.Entry.VisitCount
242267 }
243- if !matches[i].Entry.LastVisited.Equal(matches[j].Entry.LastVisited) {
244- return matches[i].Entry.LastVisited.After(matches[j].Entry.LastVisited)
268+ if !li.match.Entry.LastVisited.Equal(ri.match.Entry.LastVisited) {
269+ return li.match.Entry.LastVisited.After(ri.match.Entry.LastVisited)
245270 }
246- return matches[i].Entry.URL < matches[j].Entry.URL
271+ return li.match.Entry.URL < ri.match.Entry.URL
247272 })
248-
249- return matches
250273 }
251274
252275 func ensureHistoryCandidate(candidates map[int64]*historySearchCandidate, entry *entity.HistoryEntry) *historySearchCandidate {
@@ -210,43 +210,66 @@ func rankHistoryMatches(
210 }210 }
211 211
212 queryLower := strings.ToLower(strings.TrimSpace(query))212 queryLower := strings.ToLower(strings.TrimSpace(query))
213- matches := make([]entity.HistoryMatch, 0, len(candidates))213+ ranked := make([]rankedHistoryMatch, 0, len(candidates))
214 for _, candidate := range candidates {214 for _, candidate := range candidates {
215 candidate.score = computeHistoryCandidateScore(candidate, queryLower, words)215 candidate.score = computeHistoryCandidateScore(candidate, queryLower, words)
216- matches = append(matches, entity.HistoryMatch{216+ ranked = append(ranked, rankedHistoryMatch{
217- Entry: candidate.entry,217+ match: entity.HistoryMatch{
218- Score: candidate.score,218+ Entry: candidate.entry,
219+ Score: candidate.score,
220+ },
221+ score: candidate.score,
222+ exactURLMatch: candidate.exactURLMatch,
223+ prefixURL: candidate.prefixURL,
224+ urlMatch: candidate.urlMatch,
225+ prefixTitle: candidate.prefixTitle,
219 })226 })
220 }227 }
221 228
222- sort.Slice(matches, func(i, j int) bool {229+ sortRankedHistoryMatches(ranked)
223- left := candidates[matches[i].Entry.ID]230+
224- right := candidates[matches[j].Entry.ID]231+ matches := make([]entity.HistoryMatch, len(ranked))
225- if matches[i].Score != matches[j].Score {232+ for i := range ranked {
226- return matches[i].Score > matches[j].Score233+ matches[i] = ranked[i].match
234+ }
235+ return matches
236+}
237+
238+type rankedHistoryMatch struct {
239+ match entity.HistoryMatch
240+ score float64
241+ exactURLMatch bool
242+ prefixURL bool
243+ urlMatch bool
244+ prefixTitle bool
245+}
246+
247+func sortRankedHistoryMatches(items []rankedHistoryMatch) {
248+ sort.Slice(items, func(i, j int) bool {
249+ li, ri := &items[i], &items[j]
250+ if li.score != ri.score {
251+ return li.score > ri.score
227 }252 }
228- if left.exactURLMatch != right.exactURLMatch {253+ if li.exactURLMatch != ri.exactURLMatch {
229- return left.exactURLMatch254+ return li.exactURLMatch
230 }255 }
231- if left.prefixURL != right.prefixURL {256+ if li.prefixURL != ri.prefixURL {
232- return left.prefixURL257+ return li.prefixURL
233 }258 }
234- if left.urlMatch != right.urlMatch {259+ if li.urlMatch != ri.urlMatch {
235- return left.urlMatch260+ return li.urlMatch
236 }261 }
237- if left.prefixTitle != right.prefixTitle {262+ if li.prefixTitle != ri.prefixTitle {
238- return left.prefixTitle263+ return li.prefixTitle
239 }264 }
240- if matches[i].Entry.VisitCount != matches[j].Entry.VisitCount {265+ if li.match.Entry.VisitCount != ri.match.Entry.VisitCount {
241- return matches[i].Entry.VisitCount > matches[j].Entry.VisitCount266+ return li.match.Entry.VisitCount > ri.match.Entry.VisitCount
242 }267 }
243- if !matches[i].Entry.LastVisited.Equal(matches[j].Entry.LastVisited) {268+ if !li.match.Entry.LastVisited.Equal(ri.match.Entry.LastVisited) {
244- return matches[i].Entry.LastVisited.After(matches[j].Entry.LastVisited)269+ return li.match.Entry.LastVisited.After(ri.match.Entry.LastVisited)
245 }270 }
246- return matches[i].Entry.URL < matches[j].Entry.URL271+ return li.match.Entry.URL < ri.match.Entry.URL
247 })272 })
248-
249- return matches
250 }273 }
251 274
252 func ensureHistoryCandidate(candidates map[int64]*historySearchCandidate, entry *entity.HistoryEntry) *historySearchCandidate {275 func ensureHistoryCandidate(candidates map[int64]*historySearchCandidate, entry *entity.HistoryEntry) *historySearchCandidate {
A internal/infrastructure/persistence/sqlite/history_repo_rank_test.go +98 -0
@@ -0,0 +1,98 @@
1+package sqlite
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/bnema/dumber/internal/domain/entity"
8+)
9+
10+func TestSortRankedHistoryMatches(t *testing.T) {
11+ now := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
12+ older := now.Add(-24 * time.Hour)
13+
14+ items := []rankedHistoryMatch{
15+ {
16+ match: entity.HistoryMatch{Entry: &entity.HistoryEntry{ID: 1, URL: "z-url", VisitCount: 5, LastVisited: now}, Score: 10},
17+ score: 10,
18+ exactURLMatch: false,
19+ prefixURL: false,
20+ urlMatch: false,
21+ prefixTitle: false,
22+ },
23+ {
24+ match: entity.HistoryMatch{Entry: &entity.HistoryEntry{ID: 2, URL: "a-url", VisitCount: 5, LastVisited: now}, Score: 20},
25+ score: 20,
26+ exactURLMatch: false,
27+ prefixURL: false,
28+ urlMatch: false,
29+ prefixTitle: false,
30+ },
31+ {
32+ match: entity.HistoryMatch{Entry: &entity.HistoryEntry{ID: 3, URL: "b-url", VisitCount: 5, LastVisited: now}, Score: 10},
33+ score: 10,
34+ exactURLMatch: true,
35+ prefixURL: false,
36+ urlMatch: false,
37+ prefixTitle: false,
38+ },
39+ {
40+ match: entity.HistoryMatch{Entry: &entity.HistoryEntry{ID: 4, URL: "c-url", VisitCount: 5, LastVisited: now}, Score: 10},
41+ score: 10,
42+ exactURLMatch: false,
43+ prefixURL: true,
44+ urlMatch: false,
45+ prefixTitle: false,
46+ },
47+ {
48+ match: entity.HistoryMatch{Entry: &entity.HistoryEntry{ID: 5, URL: "d-url", VisitCount: 5, LastVisited: now}, Score: 10},
49+ score: 10,
50+ exactURLMatch: false,
51+ prefixURL: false,
52+ urlMatch: true,
53+ prefixTitle: false,
54+ },
55+ {
56+ match: entity.HistoryMatch{Entry: &entity.HistoryEntry{ID: 6, URL: "e-url", VisitCount: 5, LastVisited: now}, Score: 10},
57+ score: 10,
58+ exactURLMatch: false,
59+ prefixURL: false,
60+ urlMatch: false,
61+ prefixTitle: true,
62+ },
63+ {
64+ match: entity.HistoryMatch{Entry: &entity.HistoryEntry{ID: 7, URL: "f-url", VisitCount: 10, LastVisited: now}, Score: 10},
65+ score: 10,
66+ exactURLMatch: false,
67+ prefixURL: false,
68+ urlMatch: false,
69+ prefixTitle: false,
70+ },
71+ {
72+ match: entity.HistoryMatch{Entry: &entity.HistoryEntry{ID: 8, URL: "g-url", VisitCount: 5, LastVisited: now}, Score: 10},
73+ score: 10,
74+ exactURLMatch: false,
75+ prefixURL: false,
76+ urlMatch: false,
77+ prefixTitle: false,
78+ },
79+ {
80+ match: entity.HistoryMatch{Entry: &entity.HistoryEntry{ID: 9, URL: "h-url", VisitCount: 5, LastVisited: older}, Score: 10},
81+ score: 10,
82+ exactURLMatch: false,
83+ prefixURL: false,
84+ urlMatch: false,
85+ prefixTitle: false,
86+ },
87+ }
88+
89+ sortRankedHistoryMatches(items)
90+
91+ wantOrder := []int64{2, 3, 4, 5, 6, 7, 8, 1, 9}
92+ for i, want := range wantOrder {
93+ got := items[i].match.Entry.ID
94+ if got != want {
95+ t.Errorf("position %d: want ID %d, got ID %d", i, want, got)
96+ }
97+ }
98+}
@@ -0,0 +1,98 @@
1+package sqlite
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/bnema/dumber/internal/domain/entity"
8+)
9+
10+func TestSortRankedHistoryMatches(t *testing.T) {
11+ now := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
12+ older := now.Add(-24 * time.Hour)
13+
14+ items := []rankedHistoryMatch{
15+ {
16+ match: entity.HistoryMatch{Entry: &entity.HistoryEntry{ID: 1, URL: "z-url", VisitCount: 5, LastVisited: now}, Score: 10},
17+ score: 10,
18+ exactURLMatch: false,
19+ prefixURL: false,
20+ urlMatch: false,
21+ prefixTitle: false,
22+ },
23+ {
24+ match: entity.HistoryMatch{Entry: &entity.HistoryEntry{ID: 2, URL: "a-url", VisitCount: 5, LastVisited: now}, Score: 20},
25+ score: 20,
26+ exactURLMatch: false,
27+ prefixURL: false,
28+ urlMatch: false,
29+ prefixTitle: false,
30+ },
31+ {
32+ match: entity.HistoryMatch{Entry: &entity.HistoryEntry{ID: 3, URL: "b-url", VisitCount: 5, LastVisited: now}, Score: 10},
33+ score: 10,
34+ exactURLMatch: true,
35+ prefixURL: false,
36+ urlMatch: false,
37+ prefixTitle: false,
38+ },
39+ {
40+ match: entity.HistoryMatch{Entry: &entity.HistoryEntry{ID: 4, URL: "c-url", VisitCount: 5, LastVisited: now}, Score: 10},
41+ score: 10,
42+ exactURLMatch: false,
43+ prefixURL: true,
44+ urlMatch: false,
45+ prefixTitle: false,
46+ },
47+ {
48+ match: entity.HistoryMatch{Entry: &entity.HistoryEntry{ID: 5, URL: "d-url", VisitCount: 5, LastVisited: now}, Score: 10},
49+ score: 10,
50+ exactURLMatch: false,
51+ prefixURL: false,
52+ urlMatch: true,
53+ prefixTitle: false,
54+ },
55+ {
56+ match: entity.HistoryMatch{Entry: &entity.HistoryEntry{ID: 6, URL: "e-url", VisitCount: 5, LastVisited: now}, Score: 10},
57+ score: 10,
58+ exactURLMatch: false,
59+ prefixURL: false,
60+ urlMatch: false,
61+ prefixTitle: true,
62+ },
63+ {
64+ match: entity.HistoryMatch{Entry: &entity.HistoryEntry{ID: 7, URL: "f-url", VisitCount: 10, LastVisited: now}, Score: 10},
65+ score: 10,
66+ exactURLMatch: false,
67+ prefixURL: false,
68+ urlMatch: false,
69+ prefixTitle: false,
70+ },
71+ {
72+ match: entity.HistoryMatch{Entry: &entity.HistoryEntry{ID: 8, URL: "g-url", VisitCount: 5, LastVisited: now}, Score: 10},
73+ score: 10,
74+ exactURLMatch: false,
75+ prefixURL: false,
76+ urlMatch: false,
77+ prefixTitle: false,
78+ },
79+ {
80+ match: entity.HistoryMatch{Entry: &entity.HistoryEntry{ID: 9, URL: "h-url", VisitCount: 5, LastVisited: older}, Score: 10},
81+ score: 10,
82+ exactURLMatch: false,
83+ prefixURL: false,
84+ urlMatch: false,
85+ prefixTitle: false,
86+ },
87+ }
88+
89+ sortRankedHistoryMatches(items)
90+
91+ wantOrder := []int64{2, 3, 4, 5, 6, 7, 8, 1, 9}
92+ for i, want := range wantOrder {
93+ got := items[i].match.Entry.ID
94+ if got != want {
95+ t.Errorf("position %d: want ID %d, got ID %d", i, want, got)
96+ }
97+ }
98+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close