detached 37c9b58

fix: normalize FTS queries with slashes

commit
37c9b582dee195f8159c1551f5102e486e2b603d
author
brice · · 2026-01-26 04:28 UTC
parent
654b259
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +29 -5
M internal/infrastructure/persistence/sqlite/history_repo.go +8 -5
@@ -50,10 +50,13 @@ func (r *historyRepo) FindByURL(ctx context.Context, url string) (*entity.Histor
5050 func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]entity.HistoryMatch, error) {
5151 log := logging.FromContext(ctx)
5252
53- // Replace periods with spaces before splitting - FTS5 tokenizer treats periods as separators
54- // so "gordon.bne" in URLs is indexed as "gordon" + "bne" tokens
55- // This allows searching "gordon.bne" to match URLs containing both terms
56- normalizedQuery := strings.ReplaceAll(query, ".", " ")
53+ // Replace separators with spaces before splitting - FTS5 tokenizer treats these as separators
54+ // so "gordon.bne" or "github.com/bnema" are indexed as separate tokens
55+ // This allows searching "gordon.bne" or "github.com/bnema" to match URLs containing both terms
56+ normalizedQuery := strings.NewReplacer(
57+ ".", " ",
58+ "/", " ",
59+ ).Replace(query)
5760
5861 // Split query into words and add prefix matching to each
5962 // This enables multi-word searches like "github issues" -> "github* issues*"
@@ -148,7 +151,7 @@ func sanitizeFTS5Word(word string) string {
148151 var result strings.Builder
149152 for _, r := range word {
150153 switch r {
151- case '"', '*', '(', ')', ':', '^', '-':
154+ case '"', '*', '(', ')', ':', '^', '-', '/':
152155 // Skip FTS5 special characters
153156 default:
154157 result.WriteRune(r)
@@ -50,10 +50,13 @@ func (r *historyRepo) FindByURL(ctx context.Context, url string) (*entity.Histor
50 func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]entity.HistoryMatch, error) {50 func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]entity.HistoryMatch, error) {
51 log := logging.FromContext(ctx)51 log := logging.FromContext(ctx)
52 52
53- // Replace periods with spaces before splitting - FTS5 tokenizer treats periods as separators53+ // Replace separators with spaces before splitting - FTS5 tokenizer treats these as separators
54- // so "gordon.bne" in URLs is indexed as "gordon" + "bne" tokens54+ // so "gordon.bne" or "github.com/bnema" are indexed as separate tokens
55- // This allows searching "gordon.bne" to match URLs containing both terms55+ // This allows searching "gordon.bne" or "github.com/bnema" to match URLs containing both terms
56- normalizedQuery := strings.ReplaceAll(query, ".", " ")56+ normalizedQuery := strings.NewReplacer(
57+ ".", " ",
58+ "/", " ",
59+ ).Replace(query)
57 60
58 // Split query into words and add prefix matching to each61 // Split query into words and add prefix matching to each
59 // This enables multi-word searches like "github issues" -> "github* issues*"62 // This enables multi-word searches like "github issues" -> "github* issues*"
@@ -148,7 +151,7 @@ func sanitizeFTS5Word(word string) string {
148 var result strings.Builder151 var result strings.Builder
149 for _, r := range word {152 for _, r := range word {
150 switch r {153 switch r {
151- case '"', '*', '(', ')', ':', '^', '-':154+ case '"', '*', '(', ')', ':', '^', '-', '/':
152 // Skip FTS5 special characters155 // Skip FTS5 special characters
153 default:156 default:
154 result.WriteRune(r)157 result.WriteRune(r)
M internal/infrastructure/persistence/sqlite/history_repo_test.go +21 -0
@@ -546,6 +546,7 @@ func TestHistoryRepository_Search_SpecialCharacters(t *testing.T) {
546546 `example"test`, // quotes
547547 "example^", // caret
548548 "example-test", // hyphen
549+ "example/", // slash
549550 }
550551
551552 for _, q := range specialQueries {
@@ -555,3 +556,23 @@ func TestHistoryRepository_Search_SpecialCharacters(t *testing.T) {
555556 _ = results
556557 }
557558 }
559+
560+func TestHistoryRepository_Search_SlashSeparatedQuery(t *testing.T) {
561+ ctx := historyTestCtx()
562+ dbPath := filepath.Join(t.TempDir(), "dumber.db")
563+
564+ db, err := sqlite.NewConnection(ctx, dbPath)
565+ require.NoError(t, err)
566+ t.Cleanup(func() { _ = db.Close() })
567+
568+ repo := sqlite.NewHistoryRepository(db)
569+
570+ require.NoError(t, repo.Save(ctx, &entity.HistoryEntry{
571+ URL: "https://github.com/bnema/dumber",
572+ Title: "Dumber",
573+ }))
574+
575+ results, err := repo.Search(ctx, "github.com/bnema", 10)
576+ require.NoError(t, err)
577+ require.NotEmpty(t, results, "slash-separated query should match history")
578+}
@@ -546,6 +546,7 @@ func TestHistoryRepository_Search_SpecialCharacters(t *testing.T) {
546 `example"test`, // quotes546 `example"test`, // quotes
547 "example^", // caret547 "example^", // caret
548 "example-test", // hyphen548 "example-test", // hyphen
549+ "example/", // slash
549 }550 }
550 551
551 for _, q := range specialQueries {552 for _, q := range specialQueries {
@@ -555,3 +556,23 @@ func TestHistoryRepository_Search_SpecialCharacters(t *testing.T) {
555 _ = results556 _ = results
556 }557 }
557 }558 }
559+
560+func TestHistoryRepository_Search_SlashSeparatedQuery(t *testing.T) {
561+ ctx := historyTestCtx()
562+ dbPath := filepath.Join(t.TempDir(), "dumber.db")
563+
564+ db, err := sqlite.NewConnection(ctx, dbPath)
565+ require.NoError(t, err)
566+ t.Cleanup(func() { _ = db.Close() })
567+
568+ repo := sqlite.NewHistoryRepository(db)
569+
570+ require.NoError(t, repo.Save(ctx, &entity.HistoryEntry{
571+ URL: "https://github.com/bnema/dumber",
572+ Title: "Dumber",
573+ }))
574+
575+ results, err := repo.Search(ctx, "github.com/bnema", 10)
576+ require.NoError(t, err)
577+ require.NotEmpty(t, results, "slash-separated query should match history")
578+}

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