summaryrefslogtreecommitdiffstats
path: root/internal/db/search.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/db/search.go')
-rw-r--r--internal/db/search.go113
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()
+}