feat: Add search operators tag: and in:
This commit is contained in:
1 parent
81925934e3
commit
b94693ddce
2 files changed
+326
No files matched your search
@@ -0,0 +1,68 @@
|
||||
'use strict';
|
||||
// Full-text search index in <vault>/.notes/index.sqlite (SQLite FTS5).
|
||||
// The Markdown files are the source of truth; this file can be deleted and is rebuilt on launch.
|
||||
// Uses better-sqlite3 in the app; falls back to node:sqlite so the tests run on plain Node 22.
|
||||
|
||||
function loadDriver() {
|
||||
try { return require('better-sqlite3'); } catch {
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
return DatabaseSync;
|
||||
}
|
||||
}
|
||||
|
||||
const likeEscape = (s) => s.replace(/[\\%_]/g, (c) => '\\' + c);
|
||||
|
||||
function openDb(file) {
|
||||
const Database = loadDriver();
|
||||
const db = new Database(file);
|
||||
db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS notes USING fts5(
|
||||
id UNINDEXED, title, tags, body, tokenize = 'unicode61 remove_diacritics 2'
|
||||
)`);
|
||||
const del = db.prepare('DELETE FROM notes WHERE id = ?');
|
||||
const delTree = db.prepare("DELETE FROM notes WHERE id = ? OR id LIKE ? ESCAPE '\\'");
|
||||
const ins = db.prepare('INSERT INTO notes (id, title, tags, body) VALUES (?, ?, ?, ?)');
|
||||
const find = db.prepare(`SELECT id, title, snippet(notes, 3, char(1), char(2), '…', 14) AS snip
|
||||
FROM notes WHERE notes MATCH ? ORDER BY bm25(notes, 0.0, 8.0, 4.0, 1.0) LIMIT ?`);
|
||||
const row = (p) => [p.id, p.title || '', (p.tags || []).join(' '), p.body || ''];
|
||||
|
||||
return {
|
||||
upsert(p) { del.run(p.id); ins.run(...row(p)); },
|
||||
remove(id) { delTree.run(id, likeEscape(id) + '/%'); },
|
||||
rebuild(pages) {
|
||||
db.exec('BEGIN');
|
||||
try {
|
||||
db.exec('DELETE FROM notes');
|
||||
for (const p of pages) if (p.exists) ins.run(...row(p));
|
||||
db.exec('COMMIT');
|
||||
} catch (err) {
|
||||
db.exec('ROLLBACK');
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
search(q, limit = 20) {
|
||||
const parts = String(q).match(/(?:tag:|in:|)\S+/g) || [];
|
||||
const terms = [];
|
||||
const fields = [];
|
||||
for (const p of parts) {
|
||||
if (p.startsWith('tag:')) { fields.push('tags: ' + p.slice(4).replace(/[^\w-]/g, '')); }
|
||||
else if (p.startsWith('in:')) { fields.push('title: ' + p.slice(3).replace(/[^\w-]/g, '')); }
|
||||
else { terms.push(p); }
|
||||
}
|
||||
|
||||
const words = (terms.join(' ').match(/[\p{L}\p{N}_]+/gu) || []).slice(0, 8);
|
||||
let match = words.map((t) => `"${t}"*`).join(' ');
|
||||
if (fields.length) {
|
||||
match = fields.map(f => `${f.split(':')[0]}: "${f.split(':')[1].trim()}"*`).join(' ') + (match ? ' ' + match : '');
|
||||
}
|
||||
if (!match) return [];
|
||||
try {
|
||||
return find.all(match, limit).map((r) => ({ id: r.id, title: r.title, snip: r.snip }));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
close() { try { db.close(); } catch { /* already closed */ } }
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { openDb };
|
||||
Reference in new issue
Block a user