fix: boost domain matches in omnibox search results
- commit
- d07970329eb70cc78ba5faa108426fa6d3001416
- author
- brice · · 2026-01-23 11:25 UTC
- parent
- c4c037a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +98 -3
M internal/infrastructure/persistence/sqlite/history_repo.go +17 -3
| @@ -66,13 +66,15 @@ func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]en | |||
| 66 | 66 | ftsQuery := strings.Join(parts, " ") | |
| 67 | 67 | ||
| 68 | 68 | // Search both URL and title columns, then merge results | |
| 69 | - | urlRows, urlErr := r.queries.SearchHistoryFTSUrl(ctx, sqlc.SearchHistoryFTSUrlParams{ | |
| 69 | + | // Use domain-boosted query for URL search to prioritize domain matches | |
| 70 | + | urlRows, urlErr := r.queries.SearchHistoryFTSUrlWithDomainBoost(ctx, sqlc.SearchHistoryFTSUrlWithDomainBoostParams{ | |
| 71 | + | Term: sql.NullString{String: words[0], Valid: true}, | |
| 70 | 72 | Query: ftsQuery, | |
| 71 | 73 | Limit: int64(limit), | |
| 72 | 74 | }) | |
| 73 | 75 | if urlErr != nil { | |
| 74 | 76 | log.Debug().Err(urlErr).Str("query", query).Msg("FTS URL search failed") | |
| 75 | - | urlRows = []sqlc.History{} | |
| 77 | + | urlRows = []sqlc.SearchHistoryFTSUrlWithDomainBoostRow{} | |
| 76 | 78 | } | |
| 77 | 79 | ||
| 78 | 80 | titleRows, titleErr := r.queries.SearchHistoryFTSTitle(ctx, sqlc.SearchHistoryFTSTitleParams{ | |
| @@ -97,7 +99,7 @@ func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]en | |||
| 97 | 99 | if !seen[row.ID] { | |
| 98 | 100 | seen[row.ID] = true | |
| 99 | 101 | matches = append(matches, entity.HistoryMatch{ | |
| 100 | - | Entry: historyFromRow(row), | |
| 102 | + | Entry: historyFromDomainBoostRow(row), | |
| 101 | 103 | Score: 1.0, | |
| 102 | 104 | }) | |
| 103 | 105 | } | |
| @@ -120,6 +122,18 @@ func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]en | |||
| 120 | 122 | return matches, nil | |
| 121 | 123 | } | |
| 122 | 124 | ||
| 125 | + | func historyFromDomainBoostRow(row sqlc.SearchHistoryFTSUrlWithDomainBoostRow) *entity.HistoryEntry { | |
| 126 | + | return &entity.HistoryEntry{ | |
| 127 | + | ID: row.ID, | |
| 128 | + | URL: row.Url, | |
| 129 | + | Title: row.Title.String, | |
| 130 | + | FaviconURL: row.FaviconUrl.String, | |
| 131 | + | VisitCount: row.VisitCount.Int64, | |
| 132 | + | LastVisited: row.LastVisited.Time, | |
| 133 | + | CreatedAt: row.CreatedAt.Time, | |
| 134 | + | } | |
| 135 | + | } | |
| 136 | + | ||
| 123 | 137 | // sanitizeFTS5Word removes FTS5 special characters from a search word. | |
| 124 | 138 | // FTS5 special chars: " * ( ) : ^ - AND OR NOT NEAR | |
| 125 | 139 | func sanitizeFTS5Word(word string) string { | |
| @@ -66,13 +66,15 @@ func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]en | |||||
| 66 | ftsQuery := strings.Join(parts, " ") | 66 | ftsQuery := strings.Join(parts, " ") | ||
| 67 | 67 | ||||
| 68 | // Search both URL and title columns, then merge results | 68 | // Search both URL and title columns, then merge results | ||
| 69 | - | urlRows, urlErr := r.queries.SearchHistoryFTSUrl(ctx, sqlc.SearchHistoryFTSUrlParams{ | 69 | + | // Use domain-boosted query for URL search to prioritize domain matches |
| 70 | + | urlRows, urlErr := r.queries.SearchHistoryFTSUrlWithDomainBoost(ctx, sqlc.SearchHistoryFTSUrlWithDomainBoostParams{ | |||
| 71 | + | Term: sql.NullString{String: words[0], Valid: true}, | |||
| 70 | Query: ftsQuery, | 72 | Query: ftsQuery, | ||
| 71 | Limit: int64(limit), | 73 | Limit: int64(limit), | ||
| 72 | }) | 74 | }) | ||
| 73 | if urlErr != nil { | 75 | if urlErr != nil { | ||
| 74 | log.Debug().Err(urlErr).Str("query", query).Msg("FTS URL search failed") | 76 | log.Debug().Err(urlErr).Str("query", query).Msg("FTS URL search failed") | ||
| 75 | - | urlRows = []sqlc.History{} | 77 | + | urlRows = []sqlc.SearchHistoryFTSUrlWithDomainBoostRow{} |
| 76 | } | 78 | } | ||
| 77 | 79 | ||||
| 78 | titleRows, titleErr := r.queries.SearchHistoryFTSTitle(ctx, sqlc.SearchHistoryFTSTitleParams{ | 80 | titleRows, titleErr := r.queries.SearchHistoryFTSTitle(ctx, sqlc.SearchHistoryFTSTitleParams{ | ||
| @@ -97,7 +99,7 @@ func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]en | |||||
| 97 | if !seen[row.ID] { | 99 | if !seen[row.ID] { | ||
| 98 | seen[row.ID] = true | 100 | seen[row.ID] = true | ||
| 99 | matches = append(matches, entity.HistoryMatch{ | 101 | matches = append(matches, entity.HistoryMatch{ | ||
| 100 | - | Entry: historyFromRow(row), | 102 | + | Entry: historyFromDomainBoostRow(row), |
| 101 | Score: 1.0, | 103 | Score: 1.0, | ||
| 102 | }) | 104 | }) | ||
| 103 | } | 105 | } | ||
| @@ -120,6 +122,18 @@ func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]en | |||||
| 120 | return matches, nil | 122 | return matches, nil | ||
| 121 | } | 123 | } | ||
| 122 | 124 | ||||
| 125 | + | func historyFromDomainBoostRow(row sqlc.SearchHistoryFTSUrlWithDomainBoostRow) *entity.HistoryEntry { | |||
| 126 | + | return &entity.HistoryEntry{ | |||
| 127 | + | ID: row.ID, | |||
| 128 | + | URL: row.Url, | |||
| 129 | + | Title: row.Title.String, | |||
| 130 | + | FaviconURL: row.FaviconUrl.String, | |||
| 131 | + | VisitCount: row.VisitCount.Int64, | |||
| 132 | + | LastVisited: row.LastVisited.Time, | |||
| 133 | + | CreatedAt: row.CreatedAt.Time, | |||
| 134 | + | } | |||
| 135 | + | } | |||
| 136 | + | ||||
| 123 | // sanitizeFTS5Word removes FTS5 special characters from a search word. | 137 | // sanitizeFTS5Word removes FTS5 special characters from a search word. | ||
| 124 | // FTS5 special chars: " * ( ) : ^ - AND OR NOT NEAR | 138 | // FTS5 special chars: " * ( ) : ^ - AND OR NOT NEAR | ||
| 125 | func sanitizeFTS5Word(word string) string { | 139 | func sanitizeFTS5Word(word string) string { | ||
M internal/infrastructure/persistence/sqlite/queries/history.sql +15 -0
| @@ -57,6 +57,21 @@ WHERE fts.url MATCH @query | |||
| 57 | 57 | ORDER BY h.visit_count DESC, h.last_visited DESC | |
| 58 | 58 | LIMIT @limit; | |
| 59 | 59 | ||
| 60 | + | -- name: SearchHistoryFTSUrlWithDomainBoost :many | |
| 61 | + | SELECT h.id, h.url, h.title, h.favicon_url, h.visit_count, h.last_visited, h.created_at, | |
| 62 | + | CASE | |
| 63 | + | WHEN h.url LIKE '%://' || @term || '.%' THEN 2 | |
| 64 | + | WHEN h.url LIKE '%://%.' || @term || '.%' THEN 2 | |
| 65 | + | WHEN h.url LIKE '%://' || @term || '/%' THEN 2 | |
| 66 | + | WHEN h.url LIKE '%://%.' || @term || '/%' THEN 1 | |
| 67 | + | ELSE 0 | |
| 68 | + | END as domain_boost | |
| 69 | + | FROM history_fts fts | |
| 70 | + | JOIN history h ON fts.rowid = h.id | |
| 71 | + | WHERE fts.url MATCH @query | |
| 72 | + | ORDER BY domain_boost DESC, h.visit_count DESC, h.last_visited DESC | |
| 73 | + | LIMIT @limit; | |
| 74 | + | ||
| 60 | 75 | -- name: SearchHistoryFTSTitle :many | |
| 61 | 76 | SELECT h.id, h.url, h.title, h.favicon_url, h.visit_count, h.last_visited, h.created_at | |
| 62 | 77 | FROM history_fts fts | |
| @@ -57,6 +57,21 @@ WHERE fts.url MATCH @query | |||||
| 57 | ORDER BY h.visit_count DESC, h.last_visited DESC | 57 | ORDER BY h.visit_count DESC, h.last_visited DESC | ||
| 58 | LIMIT @limit; | 58 | LIMIT @limit; | ||
| 59 | 59 | ||||
| 60 | + | -- name: SearchHistoryFTSUrlWithDomainBoost :many | |||
| 61 | + | SELECT h.id, h.url, h.title, h.favicon_url, h.visit_count, h.last_visited, h.created_at, | |||
| 62 | + | CASE | |||
| 63 | + | WHEN h.url LIKE '%://' || @term || '.%' THEN 2 | |||
| 64 | + | WHEN h.url LIKE '%://%.' || @term || '.%' THEN 2 | |||
| 65 | + | WHEN h.url LIKE '%://' || @term || '/%' THEN 2 | |||
| 66 | + | WHEN h.url LIKE '%://%.' || @term || '/%' THEN 1 | |||
| 67 | + | ELSE 0 | |||
| 68 | + | END as domain_boost | |||
| 69 | + | FROM history_fts fts | |||
| 70 | + | JOIN history h ON fts.rowid = h.id | |||
| 71 | + | WHERE fts.url MATCH @query | |||
| 72 | + | ORDER BY domain_boost DESC, h.visit_count DESC, h.last_visited DESC | |||
| 73 | + | LIMIT @limit; | |||
| 74 | + | ||||
| 60 | -- name: SearchHistoryFTSTitle :many | 75 | -- name: SearchHistoryFTSTitle :many | ||
| 61 | SELECT h.id, h.url, h.title, h.favicon_url, h.visit_count, h.last_visited, h.created_at | 76 | SELECT h.id, h.url, h.title, h.favicon_url, h.visit_count, h.last_visited, h.created_at | ||
| 62 | FROM history_fts fts | 77 | FROM history_fts fts | ||
M internal/infrastructure/persistence/sqlite/sqlc/history.sql.go +65 -0
| @@ -529,6 +529,71 @@ func (q *Queries) SearchHistoryFTSUrl(ctx context.Context, arg SearchHistoryFTSU | |||
| 529 | 529 | return items, nil | |
| 530 | 530 | } | |
| 531 | 531 | ||
| 532 | + | const SearchHistoryFTSUrlWithDomainBoost = `-- name: SearchHistoryFTSUrlWithDomainBoost :many | |
| 533 | + | SELECT h.id, h.url, h.title, h.favicon_url, h.visit_count, h.last_visited, h.created_at, | |
| 534 | + | CASE | |
| 535 | + | WHEN h.url LIKE '%://' || ?1 || '.%' THEN 2 | |
| 536 | + | WHEN h.url LIKE '%://%.' || ?1 || '.%' THEN 2 | |
| 537 | + | WHEN h.url LIKE '%://' || ?1 || '/%' THEN 2 | |
| 538 | + | WHEN h.url LIKE '%://%.' || ?1 || '/%' THEN 1 | |
| 539 | + | ELSE 0 | |
| 540 | + | END as domain_boost | |
| 541 | + | FROM history_fts fts | |
| 542 | + | JOIN history h ON fts.rowid = h.id | |
| 543 | + | WHERE fts.url MATCH ?2 | |
| 544 | + | ORDER BY domain_boost DESC, h.visit_count DESC, h.last_visited DESC | |
| 545 | + | LIMIT ?3 | |
| 546 | + | ` | |
| 547 | + | ||
| 548 | + | type SearchHistoryFTSUrlWithDomainBoostParams struct { | |
| 549 | + | Term sql.NullString `json:"term"` | |
| 550 | + | Query string `json:"query"` | |
| 551 | + | Limit int64 `json:"limit"` | |
| 552 | + | } | |
| 553 | + | ||
| 554 | + | type SearchHistoryFTSUrlWithDomainBoostRow struct { | |
| 555 | + | ID int64 `json:"id"` | |
| 556 | + | Url string `json:"url"` | |
| 557 | + | Title sql.NullString `json:"title"` | |
| 558 | + | FaviconUrl sql.NullString `json:"favicon_url"` | |
| 559 | + | VisitCount sql.NullInt64 `json:"visit_count"` | |
| 560 | + | LastVisited sql.NullTime `json:"last_visited"` | |
| 561 | + | CreatedAt sql.NullTime `json:"created_at"` | |
| 562 | + | DomainBoost int64 `json:"domain_boost"` | |
| 563 | + | } | |
| 564 | + | ||
| 565 | + | func (q *Queries) SearchHistoryFTSUrlWithDomainBoost(ctx context.Context, arg SearchHistoryFTSUrlWithDomainBoostParams) ([]SearchHistoryFTSUrlWithDomainBoostRow, error) { | |
| 566 | + | rows, err := q.db.QueryContext(ctx, SearchHistoryFTSUrlWithDomainBoost, arg.Term, arg.Query, arg.Limit) | |
| 567 | + | if err != nil { | |
| 568 | + | return nil, err | |
| 569 | + | } | |
| 570 | + | defer rows.Close() | |
| 571 | + | items := []SearchHistoryFTSUrlWithDomainBoostRow{} | |
| 572 | + | for rows.Next() { | |
| 573 | + | var i SearchHistoryFTSUrlWithDomainBoostRow | |
| 574 | + | if err := rows.Scan( | |
| 575 | + | &i.ID, | |
| 576 | + | &i.Url, | |
| 577 | + | &i.Title, | |
| 578 | + | &i.FaviconUrl, | |
| 579 | + | &i.VisitCount, | |
| 580 | + | &i.LastVisited, | |
| 581 | + | &i.CreatedAt, | |
| 582 | + | &i.DomainBoost, | |
| 583 | + | ); err != nil { | |
| 584 | + | return nil, err | |
| 585 | + | } | |
| 586 | + | items = append(items, i) | |
| 587 | + | } | |
| 588 | + | if err := rows.Close(); err != nil { | |
| 589 | + | return nil, err | |
| 590 | + | } | |
| 591 | + | if err := rows.Err(); err != nil { | |
| 592 | + | return nil, err | |
| 593 | + | } | |
| 594 | + | return items, nil | |
| 595 | + | } | |
| 596 | + | ||
| 532 | 597 | const UpsertHistory = `-- name: UpsertHistory :exec | |
| 533 | 598 | INSERT INTO history (url, title, favicon_url) | |
| 534 | 599 | VALUES (?, ?, ?) | |
| @@ -529,6 +529,71 @@ func (q *Queries) SearchHistoryFTSUrl(ctx context.Context, arg SearchHistoryFTSU | |||||
| 529 | return items, nil | 529 | return items, nil | ||
| 530 | } | 530 | } | ||
| 531 | 531 | ||||
| 532 | + | const SearchHistoryFTSUrlWithDomainBoost = `-- name: SearchHistoryFTSUrlWithDomainBoost :many | |||
| 533 | + | SELECT h.id, h.url, h.title, h.favicon_url, h.visit_count, h.last_visited, h.created_at, | |||
| 534 | + | CASE | |||
| 535 | + | WHEN h.url LIKE '%://' || ?1 || '.%' THEN 2 | |||
| 536 | + | WHEN h.url LIKE '%://%.' || ?1 || '.%' THEN 2 | |||
| 537 | + | WHEN h.url LIKE '%://' || ?1 || '/%' THEN 2 | |||
| 538 | + | WHEN h.url LIKE '%://%.' || ?1 || '/%' THEN 1 | |||
| 539 | + | ELSE 0 | |||
| 540 | + | END as domain_boost | |||
| 541 | + | FROM history_fts fts | |||
| 542 | + | JOIN history h ON fts.rowid = h.id | |||
| 543 | + | WHERE fts.url MATCH ?2 | |||
| 544 | + | ORDER BY domain_boost DESC, h.visit_count DESC, h.last_visited DESC | |||
| 545 | + | LIMIT ?3 | |||
| 546 | + | ` | |||
| 547 | + | ||||
| 548 | + | type SearchHistoryFTSUrlWithDomainBoostParams struct { | |||
| 549 | + | Term sql.NullString `json:"term"` | |||
| 550 | + | Query string `json:"query"` | |||
| 551 | + | Limit int64 `json:"limit"` | |||
| 552 | + | } | |||
| 553 | + | ||||
| 554 | + | type SearchHistoryFTSUrlWithDomainBoostRow struct { | |||
| 555 | + | ID int64 `json:"id"` | |||
| 556 | + | Url string `json:"url"` | |||
| 557 | + | Title sql.NullString `json:"title"` | |||
| 558 | + | FaviconUrl sql.NullString `json:"favicon_url"` | |||
| 559 | + | VisitCount sql.NullInt64 `json:"visit_count"` | |||
| 560 | + | LastVisited sql.NullTime `json:"last_visited"` | |||
| 561 | + | CreatedAt sql.NullTime `json:"created_at"` | |||
| 562 | + | DomainBoost int64 `json:"domain_boost"` | |||
| 563 | + | } | |||
| 564 | + | ||||
| 565 | + | func (q *Queries) SearchHistoryFTSUrlWithDomainBoost(ctx context.Context, arg SearchHistoryFTSUrlWithDomainBoostParams) ([]SearchHistoryFTSUrlWithDomainBoostRow, error) { | |||
| 566 | + | rows, err := q.db.QueryContext(ctx, SearchHistoryFTSUrlWithDomainBoost, arg.Term, arg.Query, arg.Limit) | |||
| 567 | + | if err != nil { | |||
| 568 | + | return nil, err | |||
| 569 | + | } | |||
| 570 | + | defer rows.Close() | |||
| 571 | + | items := []SearchHistoryFTSUrlWithDomainBoostRow{} | |||
| 572 | + | for rows.Next() { | |||
| 573 | + | var i SearchHistoryFTSUrlWithDomainBoostRow | |||
| 574 | + | if err := rows.Scan( | |||
| 575 | + | &i.ID, | |||
| 576 | + | &i.Url, | |||
| 577 | + | &i.Title, | |||
| 578 | + | &i.FaviconUrl, | |||
| 579 | + | &i.VisitCount, | |||
| 580 | + | &i.LastVisited, | |||
| 581 | + | &i.CreatedAt, | |||
| 582 | + | &i.DomainBoost, | |||
| 583 | + | ); err != nil { | |||
| 584 | + | return nil, err | |||
| 585 | + | } | |||
| 586 | + | items = append(items, i) | |||
| 587 | + | } | |||
| 588 | + | if err := rows.Close(); err != nil { | |||
| 589 | + | return nil, err | |||
| 590 | + | } | |||
| 591 | + | if err := rows.Err(); err != nil { | |||
| 592 | + | return nil, err | |||
| 593 | + | } | |||
| 594 | + | return items, nil | |||
| 595 | + | } | |||
| 596 | + | ||||
| 532 | const UpsertHistory = `-- name: UpsertHistory :exec | 597 | const UpsertHistory = `-- name: UpsertHistory :exec | ||
| 533 | INSERT INTO history (url, title, favicon_url) | 598 | INSERT INTO history (url, title, favicon_url) | ||
| 534 | VALUES (?, ?, ?) | 599 | VALUES (?, ?, ?) | ||
M internal/infrastructure/persistence/sqlite/sqlc/querier.go +1 -0
| @@ -72,6 +72,7 @@ type Querier interface { | |||
| 72 | 72 | SearchHistory(ctx context.Context, arg SearchHistoryParams) ([]History, error) | |
| 73 | 73 | SearchHistoryFTSTitle(ctx context.Context, arg SearchHistoryFTSTitleParams) ([]History, error) | |
| 74 | 74 | SearchHistoryFTSUrl(ctx context.Context, arg SearchHistoryFTSUrlParams) ([]History, error) | |
| 75 | + | SearchHistoryFTSUrlWithDomainBoost(ctx context.Context, arg SearchHistoryFTSUrlWithDomainBoostParams) ([]SearchHistoryFTSUrlWithDomainBoostRow, error) | |
| 75 | 76 | SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error | |
| 76 | 77 | SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error | |
| 77 | 78 | SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error | |
| @@ -72,6 +72,7 @@ type Querier interface { | |||||
| 72 | SearchHistory(ctx context.Context, arg SearchHistoryParams) ([]History, error) | 72 | SearchHistory(ctx context.Context, arg SearchHistoryParams) ([]History, error) | ||
| 73 | SearchHistoryFTSTitle(ctx context.Context, arg SearchHistoryFTSTitleParams) ([]History, error) | 73 | SearchHistoryFTSTitle(ctx context.Context, arg SearchHistoryFTSTitleParams) ([]History, error) | ||
| 74 | SearchHistoryFTSUrl(ctx context.Context, arg SearchHistoryFTSUrlParams) ([]History, error) | 74 | SearchHistoryFTSUrl(ctx context.Context, arg SearchHistoryFTSUrlParams) ([]History, error) | ||
| 75 | + | SearchHistoryFTSUrlWithDomainBoost(ctx context.Context, arg SearchHistoryFTSUrlWithDomainBoostParams) ([]SearchHistoryFTSUrlWithDomainBoostRow, error) | |||
| 75 | SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error | 76 | SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error | ||
| 76 | SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error | 77 | SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error | ||
| 77 | SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error | 78 | SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error | ||