diff options
| author | Ivar Løvlie <38570165+ivarlovlie@users.noreply.github.com> | 2026-03-31 12:27:46 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-31 12:27:46 +0200 |
| commit | 8d7cda6e578e684483c0b5c7391c48e5b9ac5192 (patch) | |
| tree | d2b6506db2de72b3a6982cfbe69925b88936de90 /internal/db/search.go | |
| parent | 33f214f6cd9729473bb55fd7b3b923d5d960bb98 (diff) | |
| parent | 3cb7c82cf7c4e050148f69be23590a7fbe587a27 (diff) | |
| download | nebbet.no-8d7cda6e578e684483c0b5c7391c48e5b9ac5192.tar.xz nebbet.no-8d7cda6e578e684483c0b5c7391c48e5b9ac5192.zip | |
Merge pull request #1 from ivarlovlie/claude/static-site-sqlite-setup-mrcAr
Diffstat (limited to 'internal/db/search.go')
| -rw-r--r-- | internal/db/search.go | 113 |
1 files changed, 113 insertions, 0 deletions
diff --git a/internal/db/search.go b/internal/db/search.go new file mode 100644 index 0000000..b2c9b49 --- /dev/null +++ b/internal/db/search.go @@ -0,0 +1,113 @@ +package db + +import ( + "database/sql" + + _ "nebbet.no/internal/sqlitedrv" +) + +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("sqlite", 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, '<mark>', '</mark>', '...', 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() +} |
