fix: global search case-insensitive + keyboard layout swap

Unified search normalization across InternalLinkPicker and GlobalSearch:

1. GlobalSearch.svelte: multi-variant search (same as InternalLinkPicker)
   - expandKeyboardVariants() for RU/EN layout swap
   - Parallel Search queries with dedup by type+nodeId+targetId+title
   - 180ms debounce preserved

2. Backend: fix LOWER() in SQL for links/actions
   - Replace LOWER(column) LIKE with lowercased columns (title_lower, url_lower, etc.)
   - Migration 020: add lowercased columns + indexes for links and actions
   - BackfillLinksLower() + BackfillActionsLower() in storage.go
   - Update INSERT in bindings_links.go and action.go to populate lowercased columns

3. FTS5 search: Unicode case-insensitive
   - Index lowercased title/content/tags in search_index
   - sanitizeFTS() now lowercases query before MATCH
   - RebuildFTS() called after migrations

4. Case-insensitive search for nodes (already done in previous commit, verified):
   - title_lower column with Go strings.ToLower
   - Search() queries title_lower with lowercased query

All test suites PASS, full build OK.
This commit is contained in:
2026-06-15 10:52:34 +08:00
parent 88eb99e9af
commit 700e4dae5b
9 changed files with 187 additions and 32 deletions
+5 -3
View File
@@ -127,9 +127,11 @@ func (a *App) createResolvedLink(nodeID, rawURL, title, note, source, capturedAt
now := time.Now().UTC().Format(time.RFC3339)
id := util.UUID7()
if _, err := a.db.Exec(
`INSERT INTO links (id,node_id,title,url,hostname,note,source,captured_at,created_at,updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?)`,
id, nodeID, title, rawURL, hostnameForURL(rawURL), note, source, capturedAt, now, now); err != nil {
`INSERT INTO links (id,node_id,title,url,hostname,note,source,captured_at,created_at,updated_at,
title_lower,url_lower,hostname_lower,note_lower)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
id, nodeID, title, rawURL, hostnameForURL(rawURL), note, source, capturedAt, now, now,
strings.ToLower(title), strings.ToLower(rawURL), strings.ToLower(hostnameForURL(rawURL)), strings.ToLower(note)); err != nil {
return nil, err
}
return a.getLink(id)
+2 -2
View File
@@ -321,7 +321,7 @@ func (a *App) Search(query string) ([]SearchResultDTO, error) {
FROM links l
LEFT JOIN nodes n ON n.id = l.node_id
WHERE n.deleted_at IS NULL
AND (LOWER(l.title) LIKE ? OR LOWER(l.url) LIKE ? OR LOWER(l.hostname) LIKE ? OR LOWER(COALESCE(l.note,'')) LIKE ?)
AND (l.title_lower LIKE ? OR l.url_lower LIKE ? OR l.hostname_lower LIKE ? OR l.note_lower LIKE ?)
ORDER BY l.created_at DESC
LIMIT ?`,
likeQuery(query), likeQuery(query), likeQuery(query), likeQuery(query), 20-len(out))
@@ -359,7 +359,7 @@ func (a *App) Search(query string) ([]SearchResultDTO, error) {
FROM actions ac
LEFT JOIN nodes n ON n.id = ac.node_id
WHERE n.deleted_at IS NULL
AND (LOWER(ac.title) LIKE ? OR LOWER(ac.kind) LIKE ? OR LOWER(COALESCE(ac.url,'')) LIKE ? OR LOWER(COALESCE(ac.command,'')) LIKE ?)
AND (ac.title_lower LIKE ? OR ac.kind_lower LIKE ? OR ac.url_lower LIKE ? OR ac.command_lower LIKE ?)
ORDER BY ac.created_at DESC
LIMIT ?`,
likeQuery(query), likeQuery(query), likeQuery(query), likeQuery(query), 20-len(out))
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -19,7 +19,7 @@
background: #13131f;
}
</style>
<script type="module" crossorigin src="/assets/main-Dn9NERfF.js"></script>
<script type="module" crossorigin src="/assets/main-FdTYg97q.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-bQpH1es2.css">
</head>
<body>