package db import ( "database/sql" _ "modernc.org/sqlite" ) type SearchDB struct { db *sql.DB } type SearchPage struct { Path string Title string Content string } type SearchResult struct { Path string Title string Snippet string } func OpenSearch(path string) (*SearchDB, error) { db, err := sql.Open("sqlite3", path) if err != nil { return nil, err } _, err = db.Exec(` CREATE TABLE IF NOT EXISTS indexed_pages ( path TEXT NOT NULL PRIMARY KEY, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE VIRTUAL TABLE IF NOT EXISTS pages_fts USING fts5( path UNINDEXED, title, content, tokenize = 'porter unicode61' ); `) if err != nil { return nil, err } return &SearchDB{db: db}, nil } func (s *SearchDB) Close() error { return s.db.Close() } func (s *SearchDB) IndexPage(p SearchPage) error { tx, err := s.db.Begin() if err != nil { return err } defer tx.Rollback() if _, err = tx.Exec(`DELETE FROM pages_fts WHERE path = ?`, p.Path); err != nil { return err } if _, err = tx.Exec( `INSERT INTO pages_fts (path, title, content) VALUES (?, ?, ?)`, p.Path, p.Title, p.Content, ); err != nil { return err } if _, err = tx.Exec(` INSERT INTO indexed_pages (path, updated_at) VALUES (?, CURRENT_TIMESTAMP) ON CONFLICT(path) DO UPDATE SET updated_at = CURRENT_TIMESTAMP `, p.Path); err != nil { return err } return tx.Commit() } func (s *SearchDB) DeletePage(path string) error { tx, err := s.db.Begin() if err != nil { return err } defer tx.Rollback() if _, err = tx.Exec(`DELETE FROM pages_fts WHERE path = ?`, path); err != nil { return err } if _, err = tx.Exec(`DELETE FROM indexed_pages WHERE path = ?`, path); err != nil { return err } return tx.Commit() } // Search runs a full-text query and returns up to 20 results with snippets. func (s *SearchDB) Search(query string) ([]SearchResult, error) { rows, err := s.db.Query(` SELECT path, title, snippet(pages_fts, 2, '', '', '...', 20) FROM pages_fts WHERE pages_fts MATCH ? ORDER BY rank LIMIT 20 `, query) if err != nil { return nil, err } defer rows.Close() var results []SearchResult for rows.Next() { var r SearchResult if err := rows.Scan(&r.Path, &r.Title, &r.Snippet); err != nil { return nil, err } results = append(results, r) } return results, rows.Err() }