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
+30 -2
View File
@@ -1,6 +1,7 @@
<script>
import { createEventDispatcher, onDestroy, onMount, tick } from 'svelte'
import { t } from './i18n'
import { expandKeyboardVariants } from './util/keyboardLayout'
export let wailsCall = async () => []
export let typeLabel = (type) => type || ''
@@ -42,9 +43,36 @@
}
loading = true
try {
results = await wailsCall('Search', q) || []
// Build query variants: original + keyboard-swapped + lowercased
const variants = expandKeyboardVariants(q)
// Deduplicate preserving order
const seenVariants = new Set()
const queries = []
for (const v of variants) {
if (!seenVariants.has(v)) {
seenVariants.add(v)
queries.push(v)
}
}
// Execute all variant queries in parallel, collect results
const allResults = await Promise.all(
queries.map(v => wailsCall('Search', v).catch(() => []))
)
// Merge and deduplicate by node ID (or type+title for links/actions)
const merged = new Map()
// First pass: results from the original query get priority
for (let qi = 0; qi < allResults.length; qi++) {
const arr = allResults[qi] || []
for (const r of arr) {
const key = r.type + ':' + r.nodeId + ':' + (r.targetId || '') + ':' + r.title
if (!merged.has(key)) {
merged.set(key, r)
}
}
}
results = Array.from(merged.values())
selectedIndex = 0
open = true
open = results.length > 0
} catch (e) {
results = []
open = false