Add full-text search

- Index notes in a separate, rebuildable SQLite database (index.db) using FTS5 trigram,
  synced from watcher snapshots so external edits are picked up; terms under 3 characters
  fall back to LIKE
- GET /api/v1/search?q= returns matching notes with the first matching line and a snippet;
  path matches rank first
- Sidebar search panel (Ctrl+Shift+F) with highlighted terms and keyboard navigation;
  opening a result jumps to and selects the match
- Restore scroll position on tab switch with scroll snapshots; restoring scrollTop drifted
  by a few lines after line heights were re-estimated
This commit is contained in:
eezd committed 2026-10-16 20:17:24 +08:00
1 parent a11e0ec498
commit 55f0ee174e
13 files changed
+792 -20

No files matched your search

+16 -2
View File
@@ -16,6 +16,7 @@ import (
"rawmark/internal/auth"
"rawmark/internal/events"
"rawmark/internal/search"
"rawmark/internal/vault"
)
@@ -30,12 +31,13 @@ type Server struct {
vault *vault.Vault
auth *auth.Manager
hub *events.Hub
index *search.Index
// OnSetup 在首次初始化完成后调用(用于删除启动码文件)
OnSetup func()
}
func New(v *vault.Vault, a *auth.Manager, hub *events.Hub) *Server {
return &Server{vault: v, auth: a, hub: hub}
func New(v *vault.Vault, a *auth.Manager, hub *events.Hub, index *search.Index) *Server {
return &Server{vault: v, auth: a, hub: hub, index: index}
}
func (s *Server) Handler(static fs.FS) http.Handler {
@@ -59,6 +61,7 @@ func (s *Server) Handler(static fs.FS) http.Handler {
"POST /api/v1/trash/{id}/restore": s.trashRestore,
"DELETE /api/v1/trash/{id}": s.trashPurge,
"GET /api/v1/events": s.events,
"GET /api/v1/search": s.search,
}
for pattern, h := range protected {
mux.Handle(pattern, s.requireAuth(h))
@@ -211,6 +214,17 @@ func (s *Server) tree(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, map[string]any{"entries": entries})
}
const searchLimit = 50
func (s *Server) search(w http.ResponseWriter, r *http.Request) {
results, err := s.index.Search(r.URL.Query().Get("q"), searchLimit)
if err != nil {
internalError(w, r, err)
return
}
writeJSON(w, 200, map[string]any{"results": results})
}
func (s *Server) readFile(w http.ResponseWriter, r *http.Request) {
data, rev, err := s.vault.Read(r.PathValue("path"))
if err != nil {