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:
@@ -88,11 +88,13 @@ func (s *Service) Create(nodeID, kind, title, command, workingDir, url string, a
|
||||
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO actions (id,node_id,title,kind,command,args_json,working_dir,url,
|
||||
confirm_required,capture_output,created_at,updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
confirm_required,capture_output,created_at,updated_at,
|
||||
title_lower,kind_lower,url_lower,command_lower)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
rec.ID, rec.NodeID, rec.Title, rec.Kind, rec.Command, string(argsJSON),
|
||||
rec.WorkingDir, rec.URL, boolInt(rec.ConfirmRequired), boolInt(rec.CaptureOutput),
|
||||
rec.CreatedAt.Format(time.RFC3339), rec.UpdatedAt.Format(time.RFC3339),
|
||||
strings.ToLower(rec.Title), strings.ToLower(rec.Kind), strings.ToLower(rec.URL), strings.ToLower(rec.Command),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -32,7 +32,7 @@ func (s *Service) Index(nodeID, title, content, path, tags, docType string) erro
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO search_index (node_id,title,content,path,tags,type)
|
||||
VALUES (?,?,?,?,?,?)`,
|
||||
nodeID, title, content, path, tags, docType,
|
||||
nodeID, strings.ToLower(title), strings.ToLower(content), path, strings.ToLower(tags), docType,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -92,7 +92,9 @@ func (s *Service) Search(query string) ([]Result, error) {
|
||||
|
||||
func sanitizeFTS(q string) string {
|
||||
// Wrap in double quotes for phrase search, escape inner quotes.
|
||||
// Also lowercase the query since we index lowercased content.
|
||||
q = strings.TrimSpace(q)
|
||||
q = strings.ToLower(q)
|
||||
q = strings.ReplaceAll(q, `"`, `""`)
|
||||
return `"` + q + `"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package storage
|
||||
|
||||
// migration020 — add title_lower columns to links and actions for case-insensitive search.
|
||||
const migration020 = `
|
||||
ALTER TABLE links ADD COLUMN title_lower TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE links ADD COLUMN url_lower TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE links ADD COLUMN hostname_lower TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE links ADD COLUMN note_lower TEXT NOT NULL DEFAULT '';
|
||||
|
||||
UPDATE links SET title_lower = LOWER(title) WHERE title_lower = '';
|
||||
UPDATE links SET url_lower = LOWER(url) WHERE url_lower = '';
|
||||
UPDATE links SET hostname_lower = LOWER(hostname) WHERE hostname_lower = '';
|
||||
UPDATE links SET note_lower = LOWER(note) WHERE note_lower = '';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_links_title_lower ON links(title_lower);
|
||||
CREATE INDEX IF NOT EXISTS idx_links_url_lower ON links(url_lower);
|
||||
CREATE INDEX IF NOT EXISTS idx_links_hostname_lower ON links(hostname_lower);
|
||||
CREATE INDEX IF NOT EXISTS idx_links_note_lower ON links(note_lower);
|
||||
|
||||
ALTER TABLE actions ADD COLUMN title_lower TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE actions ADD COLUMN kind_lower TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE actions ADD COLUMN url_lower TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE actions ADD COLUMN command_lower TEXT NOT NULL DEFAULT '';
|
||||
|
||||
UPDATE actions SET title_lower = LOWER(title) WHERE title_lower = '';
|
||||
UPDATE actions SET kind_lower = LOWER(kind) WHERE kind_lower = '';
|
||||
UPDATE actions SET url_lower = LOWER(url) WHERE url_lower = '';
|
||||
UPDATE actions SET command_lower = LOWER(command) WHERE command_lower = '';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_actions_title_lower ON actions(title_lower);
|
||||
CREATE INDEX IF NOT EXISTS idx_actions_kind_lower ON actions(kind_lower);
|
||||
CREATE INDEX IF NOT EXISTS idx_actions_url_lower ON actions(url_lower);
|
||||
CREATE INDEX IF NOT EXISTS idx_actions_command_lower ON actions(command_lower);
|
||||
`
|
||||
@@ -43,6 +43,16 @@ func Open(path string) (*DB, error) {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := w.BackfillLinksLower(); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := w.BackfillActionsLower(); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
// Rebuild FTS5 index to pick up lowercased indexing changes
|
||||
_ = w.RebuildFTS()
|
||||
return w, nil
|
||||
}
|
||||
|
||||
@@ -80,6 +90,7 @@ var migrationFiles = map[int]string{
|
||||
17: migration017,
|
||||
18: migration018,
|
||||
19: migration019,
|
||||
20: migration020,
|
||||
}
|
||||
|
||||
func (db *DB) runInitialSchema() error {
|
||||
@@ -172,3 +183,79 @@ func (db *DB) BackfillTitleLower() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BackfillLinksLower populates lowercased columns for links where they are empty.
|
||||
func (db *DB) BackfillLinksLower() error {
|
||||
rows, err := db.Query("SELECT id, title, url, hostname, COALESCE(note,'') FROM links WHERE title_lower = ''")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type linkLower struct {
|
||||
id, title, url, hostname, note string
|
||||
}
|
||||
var items []linkLower
|
||||
for rows.Next() {
|
||||
var l linkLower
|
||||
if err := rows.Scan(&l.id, &l.title, &l.url, &l.hostname, &l.note); err != nil {
|
||||
return err
|
||||
}
|
||||
items = append(items, l)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, l := range items {
|
||||
if _, err := db.Exec(
|
||||
"UPDATE links SET title_lower=?, url_lower=?, hostname_lower=?, note_lower=? WHERE id=?",
|
||||
strings.ToLower(l.title), strings.ToLower(l.url), strings.ToLower(l.hostname), strings.ToLower(l.note), l.id,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BackfillActionsLower populates lowercased columns for actions where they are empty.
|
||||
func (db *DB) BackfillActionsLower() error {
|
||||
rows, err := db.Query("SELECT id, title, kind, COALESCE(url,''), COALESCE(command,'') FROM actions WHERE title_lower = ''")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type actionLower struct {
|
||||
id, title, kind, url, command string
|
||||
}
|
||||
var items []actionLower
|
||||
for rows.Next() {
|
||||
var a actionLower
|
||||
if err := rows.Scan(&a.id, &a.title, &a.kind, &a.url, &a.command); err != nil {
|
||||
return err
|
||||
}
|
||||
items = append(items, a)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, a := range items {
|
||||
if _, err := db.Exec(
|
||||
"UPDATE actions SET title_lower=?, kind_lower=?, url_lower=?, command_lower=? WHERE id=?",
|
||||
strings.ToLower(a.title), strings.ToLower(a.kind), strings.ToLower(a.url), strings.ToLower(a.command), a.id,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RebuildFTS rebuilds the FTS5 search index (e.g. after schema changes).
|
||||
func (db *DB) RebuildFTS() error {
|
||||
_, _ = db.Exec(`CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
|
||||
node_id UNINDEXED, title, content, path, tags, type)`)
|
||||
_, err := db.Exec("DELETE FROM search_index")
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user