@monlite/fts 0.5.3 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -26,7 +26,9 @@ function catchUp(db, coll, fields) {
26
26
  const sqlite = db.sqlite;
27
27
  ensureState(db);
28
28
  const hw = getHighWater(db, coll);
29
- const docs = sqlite.prepare(`SELECT _id, updated_at FROM "${coll}" WHERE updated_at >= ?`).all(hw);
29
+ const docs = sqlite.prepare(
30
+ `SELECT _id, updated_at FROM "${coll}" WHERE updated_at >= ? OR _id NOT IN (SELECT doc_id FROM ${IDMAP} WHERE coll = ?)`
31
+ ).all(hw, coll);
30
32
  let max = hw;
31
33
  for (const d of docs) {
32
34
  indexDoc(db, coll, fields, d._id);
@@ -36,7 +38,9 @@ function catchUp(db, coll, fields) {
36
38
  `SELECT rowid AS rid, doc_id FROM "${ftsTable(coll)}" WHERE doc_id NOT IN (SELECT _id FROM "${coll}")`
37
39
  ).all();
38
40
  const del = sqlite.prepare(`DELETE FROM "${ftsTable(coll)}" WHERE rowid = ?`);
39
- const delMap = sqlite.prepare(`DELETE FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`);
41
+ const delMap = sqlite.prepare(
42
+ `DELETE FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`
43
+ );
40
44
  for (const o of orphans) {
41
45
  del.run(o.rid);
42
46
  delMap.run(coll, o.doc_id);
@@ -60,10 +64,12 @@ function extractText(doc, path) {
60
64
  function indexDoc(db, coll, fields, id) {
61
65
  const sqlite = db.sqlite;
62
66
  const prev = sqlite.prepare(`SELECT rid FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`).get(coll, id);
63
- if (prev) sqlite.prepare(`DELETE FROM "${ftsTable(coll)}" WHERE rowid = ?`).run(prev.rid);
67
+ if (prev)
68
+ sqlite.prepare(`DELETE FROM "${ftsTable(coll)}" WHERE rowid = ?`).run(prev.rid);
64
69
  const doc = db.collection(coll).getRaw(id);
65
70
  if (!doc) {
66
- if (prev) sqlite.prepare(`DELETE FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`).run(coll, id);
71
+ if (prev)
72
+ sqlite.prepare(`DELETE FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`).run(coll, id);
67
73
  return;
68
74
  }
69
75
  const cols = fields.map((_, i) => `"${col(i)}"`).join(", ");
@@ -120,10 +126,10 @@ function search(db, coll, spec, query, opts) {
120
126
  }
121
127
  const out = [];
122
128
  for (const r of rows) {
129
+ if (out.length >= limit) break;
123
130
  if (allowed && !allowed.has(r.doc_id)) continue;
124
131
  const doc = coll.getRaw(r.doc_id);
125
132
  if (doc) out.push({ ...doc, _score: -r.rank });
126
- if (out.length >= limit) break;
127
133
  }
128
134
  return Promise.resolve(out);
129
135
  }
@@ -179,12 +185,22 @@ function createSearchIndex(db) {
179
185
  };
180
186
  const known = (name) => {
181
187
  if (configs.has(name)) return configs.get(name);
182
- const row = db.sqlite.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name = ?`).get(name);
183
- if (row) {
184
- configs.set(name, { fields: [], filterFields: [] });
185
- return configs.get(name);
188
+ const row = db.sqlite.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name = ?`).get(name);
189
+ if (!row?.sql) return void 0;
190
+ const cols = row.sql.match(/fts5\s*\(([\s\S]*)\)/i);
191
+ const fields = [];
192
+ const filterFields = [];
193
+ if (cols) {
194
+ for (const raw of cols[1].split(",")) {
195
+ const part = raw.trim();
196
+ const unindexed = /\bUNINDEXED\b/i.test(part);
197
+ const colName = part.replace(/\bUNINDEXED\b/i, "").trim().replace(/^"(.*)"$/, "$1");
198
+ if (!colName || colName === "doc_id") continue;
199
+ (unindexed ? filterFields : fields).push(colName);
200
+ }
186
201
  }
187
- return void 0;
202
+ configs.set(name, { fields, filterFields });
203
+ return configs.get(name);
188
204
  };
189
205
  return {
190
206
  ensureCollection(name, { fields, filterFields = [] }) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type {\n Collection,\n Doc,\n Monlite,\n MonlitePlugin,\n WhereInput,\n WithId,\n} from \"@monlite/core\";\n\n/** Map of collection name → searchable field paths (dot-notation allowed). */\nexport type FtsSpec = Record<string, string[]>;\n\nexport interface SearchOptions<T = Doc> {\n /** Max results (default 50). */\n limit?: number;\n /** Additionally constrain matches with a normal monlite where clause. */\n where?: WhereInput<T>;\n /**\n * When `where` is set, how many ranked matches to pull before filtering (then\n * trimmed to `limit`). Larger = better recall for selective filters.\n * Default `max(limit * 10, 200)`.\n */\n candidates?: number;\n}\n\nexport type SearchResult<T = Doc> = WithId<T> & { _score: number };\n\n// Make `collection.search()` typed wherever @monlite/fts is imported.\ndeclare module \"@monlite/core\" {\n interface Collection<T> {\n search(query: string, opts?: SearchOptions<T>): Promise<SearchResult<T>[]>;\n /** Pick up documents written by another process; returns counts. */\n catchUp(): { indexed: number; removed: number };\n }\n}\n\nconst ftsTable = (coll: string) => `${coll}_fts`;\nconst col = (i: number) => `f${i}`;\nconst STATE = \"_monlite_fts_state\";\n// doc_id → fts rowid map, so the per-doc re-index can DELETE by rowid (O(log n))\n// instead of scanning the fts table on the UNINDEXED doc_id column (O(n), which\n// made bulk ingestion O(n²)).\nconst IDMAP = \"_monlite_fts_ids\";\n\nfunction ensureState(db: Monlite): void {\n db.sqlite.exec(\n `CREATE TABLE IF NOT EXISTS ${STATE} (coll TEXT PRIMARY KEY, high_water INTEGER NOT NULL)`,\n );\n db.sqlite.exec(\n `CREATE TABLE IF NOT EXISTS ${IDMAP} (coll TEXT NOT NULL, doc_id TEXT NOT NULL, rid INTEGER NOT NULL, PRIMARY KEY (coll, doc_id))`,\n );\n}\nfunction getHighWater(db: Monlite, coll: string): number {\n const row = db.sqlite\n .prepare(`SELECT high_water FROM ${STATE} WHERE coll = ?`)\n .get(coll) as { high_water: number } | undefined;\n return row?.high_water ?? 0;\n}\nfunction setHighWater(db: Monlite, coll: string, value: number): void {\n db.sqlite\n .prepare(\n `INSERT INTO ${STATE}(coll, high_water) VALUES (?, ?) ON CONFLICT(coll) DO UPDATE SET high_water = excluded.high_water`,\n )\n .run(coll, value);\n}\n\n/**\n * Incrementally index documents written by another process (and drop entries for\n * cross-process deletes), so a separate searcher process becomes fresh without a\n * full {@link reindex}. Returns how many docs were (re)indexed and removed.\n */\nexport function catchUp(\n db: Monlite,\n coll: string,\n fields: string[],\n): { indexed: number; removed: number } {\n const sqlite = db.sqlite;\n ensureState(db);\n const hw = getHighWater(db, coll);\n const docs = sqlite\n .prepare(`SELECT _id, updated_at FROM \"${coll}\" WHERE updated_at >= ?`)\n .all(hw) as Array<{ _id: string; updated_at: number }>;\n let max = hw;\n for (const d of docs) {\n indexDoc(db, coll, fields, d._id);\n if (d.updated_at > max) max = d.updated_at;\n }\n // Remove index rows whose document was deleted (possibly by another process).\n const orphans = sqlite\n .prepare(\n `SELECT rowid AS rid, doc_id FROM \"${ftsTable(coll)}\" WHERE doc_id NOT IN (SELECT _id FROM \"${coll}\")`,\n )\n .all() as Array<{ rid: number; doc_id: string }>;\n const del = sqlite.prepare(`DELETE FROM \"${ftsTable(coll)}\" WHERE rowid = ?`);\n const delMap = sqlite.prepare(`DELETE FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`);\n for (const o of orphans) { del.run(o.rid); delMap.run(coll, o.doc_id); }\n setHighWater(db, coll, max);\n return { indexed: docs.length, removed: orphans.length };\n}\n\n/** Extract searchable text for a field path from a document. */\nfunction extractText(doc: Record<string, any>, path: string): string {\n let cur: any = doc;\n for (const seg of path.split(\".\")) {\n if (cur == null) return \"\";\n cur = cur[seg];\n }\n if (cur == null) return \"\";\n if (typeof cur === \"string\") return cur;\n if (Array.isArray(cur))\n return cur\n .filter((x) => x != null)\n .map(String)\n .join(\" \");\n if (typeof cur === \"number\" || typeof cur === \"boolean\") return String(cur);\n return \"\";\n}\n\nfunction indexDoc(\n db: Monlite,\n coll: string,\n fields: string[],\n id: string,\n): void {\n const sqlite = db.sqlite;\n const prev = sqlite\n .prepare(`SELECT rid FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`)\n .get(coll, id) as { rid: number } | undefined;\n if (prev) sqlite.prepare(`DELETE FROM \"${ftsTable(coll)}\" WHERE rowid = ?`).run(prev.rid);\n const doc = db.collection(coll).getRaw(id);\n if (!doc) {\n if (prev) sqlite.prepare(`DELETE FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`).run(coll, id);\n return; // deleted\n }\n const cols = fields.map((_, i) => `\"${col(i)}\"`).join(\", \");\n const placeholders = fields.map(() => \"?\").join(\", \");\n const values = fields.map((f) => extractText(doc, f));\n const res = sqlite\n .prepare(\n `INSERT INTO \"${ftsTable(coll)}\"(doc_id, ${cols}) VALUES (?, ${placeholders})`,\n )\n .run(id, ...values);\n sqlite\n .prepare(\n `INSERT INTO ${IDMAP}(coll, doc_id, rid) VALUES (?, ?, ?) ON CONFLICT(coll, doc_id) DO UPDATE SET rid = excluded.rid`,\n )\n .run(coll, id, Number(res.lastInsertRowid));\n}\n\n/** Rebuild a collection's FTS index from scratch. */\nexport function reindex(db: Monlite, coll: string, fields: string[]): void {\n const sqlite = db.sqlite;\n sqlite.exec(`DELETE FROM \"${ftsTable(coll)}\"`);\n sqlite.prepare(`DELETE FROM ${IDMAP} WHERE coll = ?`).run(coll);\n for (const doc of db.collection(coll).findManyCore({})) {\n indexDoc(db, coll, fields, (doc as WithId<Doc>)._id);\n }\n}\n\n/**\n * Run an FTS5 MATCH. Untrusted input can contain FTS5 syntax (a stray `\"`, a bare\n * `AND`/`*`, column filters) that throws \"fts5: syntax error\" — so on error, retry\n * with the text quoted as literal phrase tokens. Never throws on user input.\n */\nfunction ftsMatch(\n db: Monlite,\n coll: string,\n query: string,\n fetch: number,\n): Array<{ doc_id: string; rank: number }> {\n const sql =\n `SELECT doc_id, rank FROM \"${ftsTable(coll)}\" ` +\n `WHERE \"${ftsTable(coll)}\" MATCH ? ORDER BY rank LIMIT ?`;\n const run = (q: string) =>\n db.sqlite.prepare(sql).all(q, fetch) as Array<{\n doc_id: string;\n rank: number;\n }>;\n try {\n return run(query);\n } catch {\n const safe = query\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .map((t) => `\"${t.replace(/\"/g, '\"\"')}\"`)\n .join(\" \");\n if (!safe) return [];\n try {\n return run(safe);\n } catch {\n return [];\n }\n }\n}\n\nfunction search<T = Doc>(\n db: Monlite,\n coll: Collection<T>,\n spec: FtsSpec,\n query: string,\n opts?: SearchOptions<T>,\n): Promise<SearchResult<T>[]> {\n const fields = spec[coll.name];\n if (!fields) {\n throw new Error(\n `Collection \"${coll.name}\" is not configured for full-text search`,\n );\n }\n const limit = opts?.limit ?? 50;\n // With a where filter, over-fetch ranked matches then filter + trim to limit,\n // so a selective filter doesn't drop results that exist further down the rank.\n // Cap the pool so the `_id IN (...)` filter can't exceed SQLite's variable limit.\n const fetch = opts?.where\n ? Math.min(Math.max(opts.candidates ?? limit * 10, 200), 10_000)\n : limit;\n const rows = ftsMatch(db, coll.name, query, fetch);\n\n let allowed: Set<string> | null = null;\n if (opts?.where) {\n const ids = rows.map((r) => r.doc_id);\n const idIn = { _id: { in: ids } } as WhereInput<T>;\n const matching = coll.findManyCore({\n where: { AND: [opts.where, idIn] } as WhereInput<T>,\n });\n allowed = new Set(matching.map((d) => d._id));\n }\n\n const out: SearchResult<T>[] = [];\n for (const r of rows) {\n if (allowed && !allowed.has(r.doc_id)) continue;\n const doc = coll.getRaw(r.doc_id);\n if (doc) out.push({ ...doc, _score: -r.rank } as SearchResult<T>);\n if (out.length >= limit) break;\n }\n return Promise.resolve(out);\n}\n\n/**\n * Full-text search plugin (SQLite FTS5). Pass it to `createDb({ plugins: [...] })`\n * with a map of collection → searchable fields. Adds `collection.search()`, keeps\n * the index in sync on every write, and backfills existing documents on open.\n *\n * ```ts\n * const db = createDb(\"./app.db\", { plugins: [fts({ posts: [\"title\", \"body\"] })] });\n * await db.collection(\"posts\").search(\"hello world\");\n * ```\n */\nexport function fts(spec: FtsSpec): MonlitePlugin {\n let database: Monlite;\n return {\n name: \"fts\",\n init(db) {\n database = db;\n ensureState(db);\n for (const [coll, fields] of Object.entries(spec)) {\n const cols = fields.map((_, i) => `\"${col(i)}\"`).join(\", \");\n db.sqlite.exec(\n `CREATE VIRTUAL TABLE IF NOT EXISTS \"${ftsTable(coll)}\" ` +\n `USING fts5(doc_id UNINDEXED, ${cols})`,\n );\n // Migration: backfill the doc_id→rowid map for any existing fts rows\n // (databases written before the map existed), so re-index deletes hit\n // the right row instead of leaving duplicates.\n db.sqlite\n .prepare(\n `INSERT OR IGNORE INTO ${IDMAP}(coll, doc_id, rid) SELECT ?, doc_id, rowid FROM \"${ftsTable(coll)}\"`,\n )\n .run(coll);\n // Backfill when the index is empty (e.g. enabling FTS on an existing db).\n const count = db.sqlite\n .prepare(`SELECT count(*) AS n FROM \"${ftsTable(coll)}\"`)\n .get() as { n: number };\n if (count.n === 0) reindex(db, coll, fields);\n // Pick up anything other processes wrote since we last indexed.\n catchUp(db, coll, fields);\n }\n },\n afterWrite(db, { collection, ids }) {\n const fields = spec[collection];\n if (!fields) return;\n for (const id of ids) indexDoc(db, collection, fields, id);\n setHighWater(db, collection, Date.now()); // our index is current to now\n },\n collectionMethods: {\n search: (collection, query: string, opts?: SearchOptions) =>\n search(database, collection, spec, query, opts),\n catchUp: (collection) =>\n catchUp(database, collection.name, spec[collection.name] ?? []),\n },\n };\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// Dynamic search index (programmatic, not document-bound)\n//\n// The `fts()` plugin attaches `collection.search()` to a DOCUMENT collection with\n// a STATIC spec. When you instead need a programmatic full-text index over\n// collections created at RUNTIME — RAG corpora, per-tenant indexes — use\n// `createSearchIndex(db)`. Each collection is its own FTS5 table; `fields` are\n// indexed for search and `filterFields` are stored UNINDEXED so a `where` scopes\n// the MATCH (e.g. keyword search within one case/tenant). Synchronous.\n// ────────────────────────────────────────────────────────────────────────────\n\nconst FTS_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;\nfunction ftsIdent(name: string): string {\n if (!FTS_IDENT.test(name))\n throw new Error(`@monlite/fts: unsafe collection/field name \"${name}\"`);\n return name;\n}\n\nexport interface SearchIndexOptions {\n /** Text fields indexed for full-text search. */\n fields: string[];\n /** Fields stored UNINDEXED, for exact `where` filtering (scoped search). Default `[]`. */\n filterFields?: string[];\n}\n\nexport interface SearchIndexPoint {\n id: string;\n /** Indexed text, keyed by field name (the configured `fields`). */\n fields: Record<string, string>;\n /** Filter values, keyed by field name (the configured `filterFields`). */\n filters?: Record<string, string>;\n}\n\nexport interface SearchIndexHit {\n id: string;\n /** Relevance (higher = better; derived from BM25 rank). */\n score: number;\n}\n\nexport interface SearchIndex {\n ensureCollection(name: string, opts: SearchIndexOptions): void;\n upsert(name: string, points: SearchIndexPoint[]): void;\n search(\n name: string,\n query: string,\n opts?: { limit?: number; where?: Record<string, string> },\n ): SearchIndexHit[];\n delete(\n name: string,\n opts: { id?: string; where?: Record<string, string> },\n ): void;\n}\n\n/**\n * A programmatic, dynamic full-text index over `@monlite/core` (SQLite FTS5) —\n * collections created at runtime, with optional scoped filtering.\n *\n * ```ts\n * const idx = createSearchIndex(db);\n * idx.ensureCollection(\"docs\", { fields: [\"title\", \"body\"], filterFields: [\"docId\"] });\n * idx.upsert(\"docs\", [{ id: \"c1\", fields: { title, body }, filters: { docId: \"d1\" } }]);\n * idx.search(\"docs\", \"hello world\", { where: { docId: \"d1\" }, limit: 10 }); // scoped\n * ```\n */\nexport function createSearchIndex(db: Monlite): SearchIndex {\n const configs = new Map<\n string,\n { fields: string[]; filterFields: string[] }\n >();\n\n const create = (name: string, fields: string[], filterFields: string[]) => {\n const n = ftsIdent(name);\n const fcols = fields.map(ftsIdent).join(\", \");\n const ucols = filterFields\n .map((f) => `, ${ftsIdent(f)} UNINDEXED`)\n .join(\"\");\n db.sqlite.exec(\n `CREATE VIRTUAL TABLE IF NOT EXISTS \"${n}\" USING fts5(doc_id UNINDEXED, ${fcols}${ucols})`,\n );\n configs.set(name, { fields, filterFields });\n return configs.get(name)!;\n };\n\n const known = (name: string) => {\n if (configs.has(name)) return configs.get(name)!;\n const row = db.sqlite\n .prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name = ?`)\n .get(name);\n if (row) {\n configs.set(name, { fields: [], filterFields: [] });\n return configs.get(name)!;\n }\n return undefined;\n };\n\n return {\n ensureCollection(name, { fields, filterFields = [] }) {\n if (!configs.has(name)) create(name, fields, filterFields);\n },\n\n upsert(name, points) {\n if (!points?.length) return;\n const cfg = configs.get(name);\n if (!cfg)\n throw new Error(\n `@monlite/fts: ensureCollection(\"${name}\") before upsert`,\n );\n const cols = [...cfg.fields, ...cfg.filterFields];\n const colList = cols.map((c) => `, ${c}`).join(\"\");\n const ph = cols.map(() => \", ?\").join(\"\");\n const del = db.sqlite.prepare(`DELETE FROM \"${name}\" WHERE doc_id = ?`);\n const ins = db.sqlite.prepare(\n `INSERT INTO \"${name}\"(doc_id${colList}) VALUES (?${ph})`,\n );\n db.sqlite.exec(\"BEGIN\");\n try {\n for (const p of points) {\n del.run(p.id);\n const vals = [\n ...cfg.fields.map((f) => p.fields?.[f] ?? \"\"),\n ...cfg.filterFields.map((f) => p.filters?.[f] ?? \"\"),\n ];\n ins.run(p.id, ...vals);\n }\n db.sqlite.exec(\"COMMIT\");\n } catch (err) {\n try {\n db.sqlite.exec(\"ROLLBACK\");\n } catch {\n /* ignore */\n }\n throw err;\n }\n },\n\n search(name, query, opts = {}) {\n const cfg = known(name);\n if (!cfg) return [];\n const limit = opts.limit ?? 50;\n const where = Object.entries(opts.where ?? {}).filter(\n ([, v]) => v != null,\n );\n const clause = where.map(([k]) => ` AND ${ftsIdent(k)} = ?`).join(\"\");\n const sql =\n `SELECT doc_id, rank FROM \"${name}\" ` +\n `WHERE \"${name}\" MATCH ?${clause} ORDER BY rank LIMIT ?`;\n let rows: Array<{ doc_id: string; rank: number }>;\n try {\n rows = db.sqlite\n .prepare(sql)\n .all(query, ...where.map(([, v]) => v), limit) as never;\n } catch {\n return [];\n }\n return rows.map((r) => ({ id: r.doc_id, score: -r.rank }));\n },\n\n delete(name, { id, where }) {\n const cfg = known(name);\n if (!cfg) return;\n if (id != null) {\n db.sqlite.prepare(`DELETE FROM \"${name}\" WHERE doc_id = ?`).run(id);\n return;\n }\n const pairs = Object.entries(where ?? {}).filter(([, v]) => v != null);\n if (!pairs.length) return;\n const clause = pairs.map(([k]) => `${ftsIdent(k)} = ?`).join(\" AND \");\n db.sqlite\n .prepare(`DELETE FROM \"${name}\" WHERE ${clause}`)\n .run(...pairs.map(([, v]) => v));\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type {\n Collection,\n Doc,\n Monlite,\n MonlitePlugin,\n WhereInput,\n WithId,\n} from \"@monlite/core\";\n\n/** Map of collection name → searchable field paths (dot-notation allowed). */\nexport type FtsSpec = Record<string, string[]>;\n\nexport interface SearchOptions<T = Doc> {\n /** Max results (default 50). */\n limit?: number;\n /** Additionally constrain matches with a normal monlite where clause. */\n where?: WhereInput<T>;\n /**\n * When `where` is set, how many ranked matches to pull before filtering (then\n * trimmed to `limit`). Larger = better recall for selective filters.\n * Default `max(limit * 10, 200)`.\n */\n candidates?: number;\n}\n\nexport type SearchResult<T = Doc> = WithId<T> & { _score: number };\n\n// Make `collection.search()` typed wherever @monlite/fts is imported.\ndeclare module \"@monlite/core\" {\n interface Collection<T> {\n search(query: string, opts?: SearchOptions<T>): Promise<SearchResult<T>[]>;\n /** Pick up documents written by another process; returns counts. */\n catchUp(): { indexed: number; removed: number };\n }\n}\n\nconst ftsTable = (coll: string) => `${coll}_fts`;\nconst col = (i: number) => `f${i}`;\nconst STATE = \"_monlite_fts_state\";\n// doc_id → fts rowid map, so the per-doc re-index can DELETE by rowid (O(log n))\n// instead of scanning the fts table on the UNINDEXED doc_id column (O(n), which\n// made bulk ingestion O(n²)).\nconst IDMAP = \"_monlite_fts_ids\";\n\nfunction ensureState(db: Monlite): void {\n db.sqlite.exec(\n `CREATE TABLE IF NOT EXISTS ${STATE} (coll TEXT PRIMARY KEY, high_water INTEGER NOT NULL)`,\n );\n db.sqlite.exec(\n `CREATE TABLE IF NOT EXISTS ${IDMAP} (coll TEXT NOT NULL, doc_id TEXT NOT NULL, rid INTEGER NOT NULL, PRIMARY KEY (coll, doc_id))`,\n );\n}\nfunction getHighWater(db: Monlite, coll: string): number {\n const row = db.sqlite\n .prepare(`SELECT high_water FROM ${STATE} WHERE coll = ?`)\n .get(coll) as { high_water: number } | undefined;\n return row?.high_water ?? 0;\n}\nfunction setHighWater(db: Monlite, coll: string, value: number): void {\n db.sqlite\n .prepare(\n `INSERT INTO ${STATE}(coll, high_water) VALUES (?, ?) ON CONFLICT(coll) DO UPDATE SET high_water = excluded.high_water`,\n )\n .run(coll, value);\n}\n\n/**\n * Incrementally index documents written by another process (and drop entries for\n * cross-process deletes), so a separate searcher process becomes fresh without a\n * full {@link reindex}. Returns how many docs were (re)indexed and removed.\n */\nexport function catchUp(\n db: Monlite,\n coll: string,\n fields: string[],\n): { indexed: number; removed: number } {\n const sqlite = db.sqlite;\n ensureState(db);\n const hw = getHighWater(db, coll);\n // `updated_at >= hw` catches recent writes, but a document synced in with an\n // older (past) timestamp sits BELOW the high-water — also index anything missing\n // from the index entirely, so cross-process/past-dated writes don't go unsearchable.\n const docs = sqlite\n .prepare(\n `SELECT _id, updated_at FROM \"${coll}\" WHERE updated_at >= ? ` +\n `OR _id NOT IN (SELECT doc_id FROM ${IDMAP} WHERE coll = ?)`,\n )\n .all(hw, coll) as Array<{ _id: string; updated_at: number }>;\n let max = hw;\n for (const d of docs) {\n indexDoc(db, coll, fields, d._id);\n if (d.updated_at > max) max = d.updated_at;\n }\n // Remove index rows whose document was deleted (possibly by another process).\n const orphans = sqlite\n .prepare(\n `SELECT rowid AS rid, doc_id FROM \"${ftsTable(coll)}\" WHERE doc_id NOT IN (SELECT _id FROM \"${coll}\")`,\n )\n .all() as Array<{ rid: number; doc_id: string }>;\n const del = sqlite.prepare(`DELETE FROM \"${ftsTable(coll)}\" WHERE rowid = ?`);\n const delMap = sqlite.prepare(\n `DELETE FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`,\n );\n for (const o of orphans) {\n del.run(o.rid);\n delMap.run(coll, o.doc_id);\n }\n setHighWater(db, coll, max);\n return { indexed: docs.length, removed: orphans.length };\n}\n\n/** Extract searchable text for a field path from a document. */\nfunction extractText(doc: Record<string, any>, path: string): string {\n let cur: any = doc;\n for (const seg of path.split(\".\")) {\n if (cur == null) return \"\";\n cur = cur[seg];\n }\n if (cur == null) return \"\";\n if (typeof cur === \"string\") return cur;\n if (Array.isArray(cur))\n return cur\n .filter((x) => x != null)\n .map(String)\n .join(\" \");\n if (typeof cur === \"number\" || typeof cur === \"boolean\") return String(cur);\n return \"\";\n}\n\nfunction indexDoc(\n db: Monlite,\n coll: string,\n fields: string[],\n id: string,\n): void {\n const sqlite = db.sqlite;\n const prev = sqlite\n .prepare(`SELECT rid FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`)\n .get(coll, id) as { rid: number } | undefined;\n if (prev)\n sqlite\n .prepare(`DELETE FROM \"${ftsTable(coll)}\" WHERE rowid = ?`)\n .run(prev.rid);\n const doc = db.collection(coll).getRaw(id);\n if (!doc) {\n if (prev)\n sqlite\n .prepare(`DELETE FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`)\n .run(coll, id);\n return; // deleted\n }\n const cols = fields.map((_, i) => `\"${col(i)}\"`).join(\", \");\n const placeholders = fields.map(() => \"?\").join(\", \");\n const values = fields.map((f) => extractText(doc, f));\n const res = sqlite\n .prepare(\n `INSERT INTO \"${ftsTable(coll)}\"(doc_id, ${cols}) VALUES (?, ${placeholders})`,\n )\n .run(id, ...values);\n sqlite\n .prepare(\n `INSERT INTO ${IDMAP}(coll, doc_id, rid) VALUES (?, ?, ?) ON CONFLICT(coll, doc_id) DO UPDATE SET rid = excluded.rid`,\n )\n .run(coll, id, Number(res.lastInsertRowid));\n}\n\n/** Rebuild a collection's FTS index from scratch. */\nexport function reindex(db: Monlite, coll: string, fields: string[]): void {\n const sqlite = db.sqlite;\n sqlite.exec(`DELETE FROM \"${ftsTable(coll)}\"`);\n sqlite.prepare(`DELETE FROM ${IDMAP} WHERE coll = ?`).run(coll);\n for (const doc of db.collection(coll).findManyCore({})) {\n indexDoc(db, coll, fields, (doc as WithId<Doc>)._id);\n }\n}\n\n/**\n * Run an FTS5 MATCH. Untrusted input can contain FTS5 syntax (a stray `\"`, a bare\n * `AND`/`*`, column filters) that throws \"fts5: syntax error\" — so on error, retry\n * with the text quoted as literal phrase tokens. Never throws on user input.\n */\nfunction ftsMatch(\n db: Monlite,\n coll: string,\n query: string,\n fetch: number,\n): Array<{ doc_id: string; rank: number }> {\n const sql =\n `SELECT doc_id, rank FROM \"${ftsTable(coll)}\" ` +\n `WHERE \"${ftsTable(coll)}\" MATCH ? ORDER BY rank LIMIT ?`;\n const run = (q: string) =>\n db.sqlite.prepare(sql).all(q, fetch) as Array<{\n doc_id: string;\n rank: number;\n }>;\n try {\n return run(query);\n } catch {\n const safe = query\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .map((t) => `\"${t.replace(/\"/g, '\"\"')}\"`)\n .join(\" \");\n if (!safe) return [];\n try {\n return run(safe);\n } catch {\n return [];\n }\n }\n}\n\nfunction search<T = Doc>(\n db: Monlite,\n coll: Collection<T>,\n spec: FtsSpec,\n query: string,\n opts?: SearchOptions<T>,\n): Promise<SearchResult<T>[]> {\n const fields = spec[coll.name];\n if (!fields) {\n throw new Error(\n `Collection \"${coll.name}\" is not configured for full-text search`,\n );\n }\n const limit = opts?.limit ?? 50;\n // With a where filter, over-fetch ranked matches then filter + trim to limit,\n // so a selective filter doesn't drop results that exist further down the rank.\n // Cap the pool so the `_id IN (...)` filter can't exceed SQLite's variable limit.\n const fetch = opts?.where\n ? Math.min(Math.max(opts.candidates ?? limit * 10, 200), 10_000)\n : limit;\n const rows = ftsMatch(db, coll.name, query, fetch);\n\n let allowed: Set<string> | null = null;\n if (opts?.where) {\n const ids = rows.map((r) => r.doc_id);\n const idIn = { _id: { in: ids } } as WhereInput<T>;\n const matching = coll.findManyCore({\n where: { AND: [opts.where, idIn] } as WhereInput<T>,\n });\n allowed = new Set(matching.map((d) => d._id));\n }\n\n const out: SearchResult<T>[] = [];\n for (const r of rows) {\n if (out.length >= limit) break; // check BEFORE pushing (limit:0 → 0 results)\n if (allowed && !allowed.has(r.doc_id)) continue;\n const doc = coll.getRaw(r.doc_id);\n if (doc) out.push({ ...doc, _score: -r.rank } as SearchResult<T>);\n }\n return Promise.resolve(out);\n}\n\n/**\n * Full-text search plugin (SQLite FTS5). Pass it to `createDb({ plugins: [...] })`\n * with a map of collection → searchable fields. Adds `collection.search()`, keeps\n * the index in sync on every write, and backfills existing documents on open.\n *\n * ```ts\n * const db = createDb(\"./app.db\", { plugins: [fts({ posts: [\"title\", \"body\"] })] });\n * await db.collection(\"posts\").search(\"hello world\");\n * ```\n */\nexport function fts(spec: FtsSpec): MonlitePlugin {\n let database: Monlite;\n return {\n name: \"fts\",\n init(db) {\n database = db;\n ensureState(db);\n for (const [coll, fields] of Object.entries(spec)) {\n const cols = fields.map((_, i) => `\"${col(i)}\"`).join(\", \");\n db.sqlite.exec(\n `CREATE VIRTUAL TABLE IF NOT EXISTS \"${ftsTable(coll)}\" ` +\n `USING fts5(doc_id UNINDEXED, ${cols})`,\n );\n // Migration: backfill the doc_id→rowid map for any existing fts rows\n // (databases written before the map existed), so re-index deletes hit\n // the right row instead of leaving duplicates.\n db.sqlite\n .prepare(\n `INSERT OR IGNORE INTO ${IDMAP}(coll, doc_id, rid) SELECT ?, doc_id, rowid FROM \"${ftsTable(coll)}\"`,\n )\n .run(coll);\n // Backfill when the index is empty (e.g. enabling FTS on an existing db).\n const count = db.sqlite\n .prepare(`SELECT count(*) AS n FROM \"${ftsTable(coll)}\"`)\n .get() as { n: number };\n if (count.n === 0) reindex(db, coll, fields);\n // Pick up anything other processes wrote since we last indexed.\n catchUp(db, coll, fields);\n }\n },\n afterWrite(db, { collection, ids }) {\n const fields = spec[collection];\n if (!fields) return;\n for (const id of ids) indexDoc(db, collection, fields, id);\n setHighWater(db, collection, Date.now()); // our index is current to now\n },\n collectionMethods: {\n search: (collection, query: string, opts?: SearchOptions) =>\n search(database, collection, spec, query, opts),\n catchUp: (collection) =>\n catchUp(database, collection.name, spec[collection.name] ?? []),\n },\n };\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// Dynamic search index (programmatic, not document-bound)\n//\n// The `fts()` plugin attaches `collection.search()` to a DOCUMENT collection with\n// a STATIC spec. When you instead need a programmatic full-text index over\n// collections created at RUNTIME — RAG corpora, per-tenant indexes — use\n// `createSearchIndex(db)`. Each collection is its own FTS5 table; `fields` are\n// indexed for search and `filterFields` are stored UNINDEXED so a `where` scopes\n// the MATCH (e.g. keyword search within one case/tenant). Synchronous.\n// ────────────────────────────────────────────────────────────────────────────\n\nconst FTS_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;\nfunction ftsIdent(name: string): string {\n if (!FTS_IDENT.test(name))\n throw new Error(`@monlite/fts: unsafe collection/field name \"${name}\"`);\n return name;\n}\n\nexport interface SearchIndexOptions {\n /** Text fields indexed for full-text search. */\n fields: string[];\n /** Fields stored UNINDEXED, for exact `where` filtering (scoped search). Default `[]`. */\n filterFields?: string[];\n}\n\nexport interface SearchIndexPoint {\n id: string;\n /** Indexed text, keyed by field name (the configured `fields`). */\n fields: Record<string, string>;\n /** Filter values, keyed by field name (the configured `filterFields`). */\n filters?: Record<string, string>;\n}\n\nexport interface SearchIndexHit {\n id: string;\n /** Relevance (higher = better; derived from BM25 rank). */\n score: number;\n}\n\nexport interface SearchIndex {\n ensureCollection(name: string, opts: SearchIndexOptions): void;\n upsert(name: string, points: SearchIndexPoint[]): void;\n search(\n name: string,\n query: string,\n opts?: { limit?: number; where?: Record<string, string> },\n ): SearchIndexHit[];\n delete(\n name: string,\n opts: { id?: string; where?: Record<string, string> },\n ): void;\n}\n\n/**\n * A programmatic, dynamic full-text index over `@monlite/core` (SQLite FTS5) —\n * collections created at runtime, with optional scoped filtering.\n *\n * ```ts\n * const idx = createSearchIndex(db);\n * idx.ensureCollection(\"docs\", { fields: [\"title\", \"body\"], filterFields: [\"docId\"] });\n * idx.upsert(\"docs\", [{ id: \"c1\", fields: { title, body }, filters: { docId: \"d1\" } }]);\n * idx.search(\"docs\", \"hello world\", { where: { docId: \"d1\" }, limit: 10 }); // scoped\n * ```\n */\nexport function createSearchIndex(db: Monlite): SearchIndex {\n const configs = new Map<\n string,\n { fields: string[]; filterFields: string[] }\n >();\n\n const create = (name: string, fields: string[], filterFields: string[]) => {\n const n = ftsIdent(name);\n const fcols = fields.map(ftsIdent).join(\", \");\n const ucols = filterFields\n .map((f) => `, ${ftsIdent(f)} UNINDEXED`)\n .join(\"\");\n db.sqlite.exec(\n `CREATE VIRTUAL TABLE IF NOT EXISTS \"${n}\" USING fts5(doc_id UNINDEXED, ${fcols}${ucols})`,\n );\n configs.set(name, { fields, filterFields });\n return configs.get(name)!;\n };\n\n const known = (name: string) => {\n if (configs.has(name)) return configs.get(name)!;\n const row = db.sqlite\n .prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name = ?`)\n .get(name) as { sql?: string } | undefined;\n if (!row?.sql) return undefined;\n // Recover the REAL schema from the fts5 table definition so a reopened index\n // can index/search correctly. Caching empty fields here silently made every\n // later upsert insert an unsearchable row. doc_id is skipped; UNINDEXED columns\n // are filterFields, the rest are searchable fields.\n const cols = row.sql.match(/fts5\\s*\\(([\\s\\S]*)\\)/i);\n const fields: string[] = [];\n const filterFields: string[] = [];\n if (cols) {\n for (const raw of cols[1].split(\",\")) {\n const part = raw.trim();\n const unindexed = /\\bUNINDEXED\\b/i.test(part);\n const colName = part\n .replace(/\\bUNINDEXED\\b/i, \"\")\n .trim()\n .replace(/^\"(.*)\"$/, \"$1\");\n if (!colName || colName === \"doc_id\") continue;\n (unindexed ? filterFields : fields).push(colName);\n }\n }\n configs.set(name, { fields, filterFields });\n return configs.get(name)!;\n };\n\n return {\n ensureCollection(name, { fields, filterFields = [] }) {\n if (!configs.has(name)) create(name, fields, filterFields);\n },\n\n upsert(name, points) {\n if (!points?.length) return;\n const cfg = configs.get(name);\n if (!cfg)\n throw new Error(\n `@monlite/fts: ensureCollection(\"${name}\") before upsert`,\n );\n const cols = [...cfg.fields, ...cfg.filterFields];\n const colList = cols.map((c) => `, ${c}`).join(\"\");\n const ph = cols.map(() => \", ?\").join(\"\");\n const del = db.sqlite.prepare(`DELETE FROM \"${name}\" WHERE doc_id = ?`);\n const ins = db.sqlite.prepare(\n `INSERT INTO \"${name}\"(doc_id${colList}) VALUES (?${ph})`,\n );\n db.sqlite.exec(\"BEGIN\");\n try {\n for (const p of points) {\n del.run(p.id);\n const vals = [\n ...cfg.fields.map((f) => p.fields?.[f] ?? \"\"),\n ...cfg.filterFields.map((f) => p.filters?.[f] ?? \"\"),\n ];\n ins.run(p.id, ...vals);\n }\n db.sqlite.exec(\"COMMIT\");\n } catch (err) {\n try {\n db.sqlite.exec(\"ROLLBACK\");\n } catch {\n /* ignore */\n }\n throw err;\n }\n },\n\n search(name, query, opts = {}) {\n const cfg = known(name);\n if (!cfg) return [];\n const limit = opts.limit ?? 50;\n const where = Object.entries(opts.where ?? {}).filter(\n ([, v]) => v != null,\n );\n const clause = where.map(([k]) => ` AND ${ftsIdent(k)} = ?`).join(\"\");\n const sql =\n `SELECT doc_id, rank FROM \"${name}\" ` +\n `WHERE \"${name}\" MATCH ?${clause} ORDER BY rank LIMIT ?`;\n let rows: Array<{ doc_id: string; rank: number }>;\n try {\n rows = db.sqlite\n .prepare(sql)\n .all(query, ...where.map(([, v]) => v), limit) as never;\n } catch {\n return [];\n }\n return rows.map((r) => ({ id: r.doc_id, score: -r.rank }));\n },\n\n delete(name, { id, where }) {\n const cfg = known(name);\n if (!cfg) return;\n if (id != null) {\n db.sqlite.prepare(`DELETE FROM \"${name}\" WHERE doc_id = ?`).run(id);\n return;\n }\n const pairs = Object.entries(where ?? {}).filter(([, v]) => v != null);\n if (!pairs.length) return;\n const clause = pairs.map(([k]) => `${ftsIdent(k)} = ?`).join(\" AND \");\n db.sqlite\n .prepare(`DELETE FROM \"${name}\" WHERE ${clause}`)\n .run(...pairs.map(([, v]) => v));\n },\n };\n}\n"]}
package/dist/index.js CHANGED
@@ -24,7 +24,9 @@ function catchUp(db, coll, fields) {
24
24
  const sqlite = db.sqlite;
25
25
  ensureState(db);
26
26
  const hw = getHighWater(db, coll);
27
- const docs = sqlite.prepare(`SELECT _id, updated_at FROM "${coll}" WHERE updated_at >= ?`).all(hw);
27
+ const docs = sqlite.prepare(
28
+ `SELECT _id, updated_at FROM "${coll}" WHERE updated_at >= ? OR _id NOT IN (SELECT doc_id FROM ${IDMAP} WHERE coll = ?)`
29
+ ).all(hw, coll);
28
30
  let max = hw;
29
31
  for (const d of docs) {
30
32
  indexDoc(db, coll, fields, d._id);
@@ -34,7 +36,9 @@ function catchUp(db, coll, fields) {
34
36
  `SELECT rowid AS rid, doc_id FROM "${ftsTable(coll)}" WHERE doc_id NOT IN (SELECT _id FROM "${coll}")`
35
37
  ).all();
36
38
  const del = sqlite.prepare(`DELETE FROM "${ftsTable(coll)}" WHERE rowid = ?`);
37
- const delMap = sqlite.prepare(`DELETE FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`);
39
+ const delMap = sqlite.prepare(
40
+ `DELETE FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`
41
+ );
38
42
  for (const o of orphans) {
39
43
  del.run(o.rid);
40
44
  delMap.run(coll, o.doc_id);
@@ -58,10 +62,12 @@ function extractText(doc, path) {
58
62
  function indexDoc(db, coll, fields, id) {
59
63
  const sqlite = db.sqlite;
60
64
  const prev = sqlite.prepare(`SELECT rid FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`).get(coll, id);
61
- if (prev) sqlite.prepare(`DELETE FROM "${ftsTable(coll)}" WHERE rowid = ?`).run(prev.rid);
65
+ if (prev)
66
+ sqlite.prepare(`DELETE FROM "${ftsTable(coll)}" WHERE rowid = ?`).run(prev.rid);
62
67
  const doc = db.collection(coll).getRaw(id);
63
68
  if (!doc) {
64
- if (prev) sqlite.prepare(`DELETE FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`).run(coll, id);
69
+ if (prev)
70
+ sqlite.prepare(`DELETE FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`).run(coll, id);
65
71
  return;
66
72
  }
67
73
  const cols = fields.map((_, i) => `"${col(i)}"`).join(", ");
@@ -118,10 +124,10 @@ function search(db, coll, spec, query, opts) {
118
124
  }
119
125
  const out = [];
120
126
  for (const r of rows) {
127
+ if (out.length >= limit) break;
121
128
  if (allowed && !allowed.has(r.doc_id)) continue;
122
129
  const doc = coll.getRaw(r.doc_id);
123
130
  if (doc) out.push({ ...doc, _score: -r.rank });
124
- if (out.length >= limit) break;
125
131
  }
126
132
  return Promise.resolve(out);
127
133
  }
@@ -177,12 +183,22 @@ function createSearchIndex(db) {
177
183
  };
178
184
  const known = (name) => {
179
185
  if (configs.has(name)) return configs.get(name);
180
- const row = db.sqlite.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name = ?`).get(name);
181
- if (row) {
182
- configs.set(name, { fields: [], filterFields: [] });
183
- return configs.get(name);
186
+ const row = db.sqlite.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name = ?`).get(name);
187
+ if (!row?.sql) return void 0;
188
+ const cols = row.sql.match(/fts5\s*\(([\s\S]*)\)/i);
189
+ const fields = [];
190
+ const filterFields = [];
191
+ if (cols) {
192
+ for (const raw of cols[1].split(",")) {
193
+ const part = raw.trim();
194
+ const unindexed = /\bUNINDEXED\b/i.test(part);
195
+ const colName = part.replace(/\bUNINDEXED\b/i, "").trim().replace(/^"(.*)"$/, "$1");
196
+ if (!colName || colName === "doc_id") continue;
197
+ (unindexed ? filterFields : fields).push(colName);
198
+ }
184
199
  }
185
- return void 0;
200
+ configs.set(name, { fields, filterFields });
201
+ return configs.get(name);
186
202
  };
187
203
  return {
188
204
  ensureCollection(name, { fields, filterFields = [] }) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type {\n Collection,\n Doc,\n Monlite,\n MonlitePlugin,\n WhereInput,\n WithId,\n} from \"@monlite/core\";\n\n/** Map of collection name → searchable field paths (dot-notation allowed). */\nexport type FtsSpec = Record<string, string[]>;\n\nexport interface SearchOptions<T = Doc> {\n /** Max results (default 50). */\n limit?: number;\n /** Additionally constrain matches with a normal monlite where clause. */\n where?: WhereInput<T>;\n /**\n * When `where` is set, how many ranked matches to pull before filtering (then\n * trimmed to `limit`). Larger = better recall for selective filters.\n * Default `max(limit * 10, 200)`.\n */\n candidates?: number;\n}\n\nexport type SearchResult<T = Doc> = WithId<T> & { _score: number };\n\n// Make `collection.search()` typed wherever @monlite/fts is imported.\ndeclare module \"@monlite/core\" {\n interface Collection<T> {\n search(query: string, opts?: SearchOptions<T>): Promise<SearchResult<T>[]>;\n /** Pick up documents written by another process; returns counts. */\n catchUp(): { indexed: number; removed: number };\n }\n}\n\nconst ftsTable = (coll: string) => `${coll}_fts`;\nconst col = (i: number) => `f${i}`;\nconst STATE = \"_monlite_fts_state\";\n// doc_id → fts rowid map, so the per-doc re-index can DELETE by rowid (O(log n))\n// instead of scanning the fts table on the UNINDEXED doc_id column (O(n), which\n// made bulk ingestion O(n²)).\nconst IDMAP = \"_monlite_fts_ids\";\n\nfunction ensureState(db: Monlite): void {\n db.sqlite.exec(\n `CREATE TABLE IF NOT EXISTS ${STATE} (coll TEXT PRIMARY KEY, high_water INTEGER NOT NULL)`,\n );\n db.sqlite.exec(\n `CREATE TABLE IF NOT EXISTS ${IDMAP} (coll TEXT NOT NULL, doc_id TEXT NOT NULL, rid INTEGER NOT NULL, PRIMARY KEY (coll, doc_id))`,\n );\n}\nfunction getHighWater(db: Monlite, coll: string): number {\n const row = db.sqlite\n .prepare(`SELECT high_water FROM ${STATE} WHERE coll = ?`)\n .get(coll) as { high_water: number } | undefined;\n return row?.high_water ?? 0;\n}\nfunction setHighWater(db: Monlite, coll: string, value: number): void {\n db.sqlite\n .prepare(\n `INSERT INTO ${STATE}(coll, high_water) VALUES (?, ?) ON CONFLICT(coll) DO UPDATE SET high_water = excluded.high_water`,\n )\n .run(coll, value);\n}\n\n/**\n * Incrementally index documents written by another process (and drop entries for\n * cross-process deletes), so a separate searcher process becomes fresh without a\n * full {@link reindex}. Returns how many docs were (re)indexed and removed.\n */\nexport function catchUp(\n db: Monlite,\n coll: string,\n fields: string[],\n): { indexed: number; removed: number } {\n const sqlite = db.sqlite;\n ensureState(db);\n const hw = getHighWater(db, coll);\n const docs = sqlite\n .prepare(`SELECT _id, updated_at FROM \"${coll}\" WHERE updated_at >= ?`)\n .all(hw) as Array<{ _id: string; updated_at: number }>;\n let max = hw;\n for (const d of docs) {\n indexDoc(db, coll, fields, d._id);\n if (d.updated_at > max) max = d.updated_at;\n }\n // Remove index rows whose document was deleted (possibly by another process).\n const orphans = sqlite\n .prepare(\n `SELECT rowid AS rid, doc_id FROM \"${ftsTable(coll)}\" WHERE doc_id NOT IN (SELECT _id FROM \"${coll}\")`,\n )\n .all() as Array<{ rid: number; doc_id: string }>;\n const del = sqlite.prepare(`DELETE FROM \"${ftsTable(coll)}\" WHERE rowid = ?`);\n const delMap = sqlite.prepare(`DELETE FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`);\n for (const o of orphans) { del.run(o.rid); delMap.run(coll, o.doc_id); }\n setHighWater(db, coll, max);\n return { indexed: docs.length, removed: orphans.length };\n}\n\n/** Extract searchable text for a field path from a document. */\nfunction extractText(doc: Record<string, any>, path: string): string {\n let cur: any = doc;\n for (const seg of path.split(\".\")) {\n if (cur == null) return \"\";\n cur = cur[seg];\n }\n if (cur == null) return \"\";\n if (typeof cur === \"string\") return cur;\n if (Array.isArray(cur))\n return cur\n .filter((x) => x != null)\n .map(String)\n .join(\" \");\n if (typeof cur === \"number\" || typeof cur === \"boolean\") return String(cur);\n return \"\";\n}\n\nfunction indexDoc(\n db: Monlite,\n coll: string,\n fields: string[],\n id: string,\n): void {\n const sqlite = db.sqlite;\n const prev = sqlite\n .prepare(`SELECT rid FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`)\n .get(coll, id) as { rid: number } | undefined;\n if (prev) sqlite.prepare(`DELETE FROM \"${ftsTable(coll)}\" WHERE rowid = ?`).run(prev.rid);\n const doc = db.collection(coll).getRaw(id);\n if (!doc) {\n if (prev) sqlite.prepare(`DELETE FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`).run(coll, id);\n return; // deleted\n }\n const cols = fields.map((_, i) => `\"${col(i)}\"`).join(\", \");\n const placeholders = fields.map(() => \"?\").join(\", \");\n const values = fields.map((f) => extractText(doc, f));\n const res = sqlite\n .prepare(\n `INSERT INTO \"${ftsTable(coll)}\"(doc_id, ${cols}) VALUES (?, ${placeholders})`,\n )\n .run(id, ...values);\n sqlite\n .prepare(\n `INSERT INTO ${IDMAP}(coll, doc_id, rid) VALUES (?, ?, ?) ON CONFLICT(coll, doc_id) DO UPDATE SET rid = excluded.rid`,\n )\n .run(coll, id, Number(res.lastInsertRowid));\n}\n\n/** Rebuild a collection's FTS index from scratch. */\nexport function reindex(db: Monlite, coll: string, fields: string[]): void {\n const sqlite = db.sqlite;\n sqlite.exec(`DELETE FROM \"${ftsTable(coll)}\"`);\n sqlite.prepare(`DELETE FROM ${IDMAP} WHERE coll = ?`).run(coll);\n for (const doc of db.collection(coll).findManyCore({})) {\n indexDoc(db, coll, fields, (doc as WithId<Doc>)._id);\n }\n}\n\n/**\n * Run an FTS5 MATCH. Untrusted input can contain FTS5 syntax (a stray `\"`, a bare\n * `AND`/`*`, column filters) that throws \"fts5: syntax error\" — so on error, retry\n * with the text quoted as literal phrase tokens. Never throws on user input.\n */\nfunction ftsMatch(\n db: Monlite,\n coll: string,\n query: string,\n fetch: number,\n): Array<{ doc_id: string; rank: number }> {\n const sql =\n `SELECT doc_id, rank FROM \"${ftsTable(coll)}\" ` +\n `WHERE \"${ftsTable(coll)}\" MATCH ? ORDER BY rank LIMIT ?`;\n const run = (q: string) =>\n db.sqlite.prepare(sql).all(q, fetch) as Array<{\n doc_id: string;\n rank: number;\n }>;\n try {\n return run(query);\n } catch {\n const safe = query\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .map((t) => `\"${t.replace(/\"/g, '\"\"')}\"`)\n .join(\" \");\n if (!safe) return [];\n try {\n return run(safe);\n } catch {\n return [];\n }\n }\n}\n\nfunction search<T = Doc>(\n db: Monlite,\n coll: Collection<T>,\n spec: FtsSpec,\n query: string,\n opts?: SearchOptions<T>,\n): Promise<SearchResult<T>[]> {\n const fields = spec[coll.name];\n if (!fields) {\n throw new Error(\n `Collection \"${coll.name}\" is not configured for full-text search`,\n );\n }\n const limit = opts?.limit ?? 50;\n // With a where filter, over-fetch ranked matches then filter + trim to limit,\n // so a selective filter doesn't drop results that exist further down the rank.\n // Cap the pool so the `_id IN (...)` filter can't exceed SQLite's variable limit.\n const fetch = opts?.where\n ? Math.min(Math.max(opts.candidates ?? limit * 10, 200), 10_000)\n : limit;\n const rows = ftsMatch(db, coll.name, query, fetch);\n\n let allowed: Set<string> | null = null;\n if (opts?.where) {\n const ids = rows.map((r) => r.doc_id);\n const idIn = { _id: { in: ids } } as WhereInput<T>;\n const matching = coll.findManyCore({\n where: { AND: [opts.where, idIn] } as WhereInput<T>,\n });\n allowed = new Set(matching.map((d) => d._id));\n }\n\n const out: SearchResult<T>[] = [];\n for (const r of rows) {\n if (allowed && !allowed.has(r.doc_id)) continue;\n const doc = coll.getRaw(r.doc_id);\n if (doc) out.push({ ...doc, _score: -r.rank } as SearchResult<T>);\n if (out.length >= limit) break;\n }\n return Promise.resolve(out);\n}\n\n/**\n * Full-text search plugin (SQLite FTS5). Pass it to `createDb({ plugins: [...] })`\n * with a map of collection → searchable fields. Adds `collection.search()`, keeps\n * the index in sync on every write, and backfills existing documents on open.\n *\n * ```ts\n * const db = createDb(\"./app.db\", { plugins: [fts({ posts: [\"title\", \"body\"] })] });\n * await db.collection(\"posts\").search(\"hello world\");\n * ```\n */\nexport function fts(spec: FtsSpec): MonlitePlugin {\n let database: Monlite;\n return {\n name: \"fts\",\n init(db) {\n database = db;\n ensureState(db);\n for (const [coll, fields] of Object.entries(spec)) {\n const cols = fields.map((_, i) => `\"${col(i)}\"`).join(\", \");\n db.sqlite.exec(\n `CREATE VIRTUAL TABLE IF NOT EXISTS \"${ftsTable(coll)}\" ` +\n `USING fts5(doc_id UNINDEXED, ${cols})`,\n );\n // Migration: backfill the doc_id→rowid map for any existing fts rows\n // (databases written before the map existed), so re-index deletes hit\n // the right row instead of leaving duplicates.\n db.sqlite\n .prepare(\n `INSERT OR IGNORE INTO ${IDMAP}(coll, doc_id, rid) SELECT ?, doc_id, rowid FROM \"${ftsTable(coll)}\"`,\n )\n .run(coll);\n // Backfill when the index is empty (e.g. enabling FTS on an existing db).\n const count = db.sqlite\n .prepare(`SELECT count(*) AS n FROM \"${ftsTable(coll)}\"`)\n .get() as { n: number };\n if (count.n === 0) reindex(db, coll, fields);\n // Pick up anything other processes wrote since we last indexed.\n catchUp(db, coll, fields);\n }\n },\n afterWrite(db, { collection, ids }) {\n const fields = spec[collection];\n if (!fields) return;\n for (const id of ids) indexDoc(db, collection, fields, id);\n setHighWater(db, collection, Date.now()); // our index is current to now\n },\n collectionMethods: {\n search: (collection, query: string, opts?: SearchOptions) =>\n search(database, collection, spec, query, opts),\n catchUp: (collection) =>\n catchUp(database, collection.name, spec[collection.name] ?? []),\n },\n };\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// Dynamic search index (programmatic, not document-bound)\n//\n// The `fts()` plugin attaches `collection.search()` to a DOCUMENT collection with\n// a STATIC spec. When you instead need a programmatic full-text index over\n// collections created at RUNTIME — RAG corpora, per-tenant indexes — use\n// `createSearchIndex(db)`. Each collection is its own FTS5 table; `fields` are\n// indexed for search and `filterFields` are stored UNINDEXED so a `where` scopes\n// the MATCH (e.g. keyword search within one case/tenant). Synchronous.\n// ────────────────────────────────────────────────────────────────────────────\n\nconst FTS_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;\nfunction ftsIdent(name: string): string {\n if (!FTS_IDENT.test(name))\n throw new Error(`@monlite/fts: unsafe collection/field name \"${name}\"`);\n return name;\n}\n\nexport interface SearchIndexOptions {\n /** Text fields indexed for full-text search. */\n fields: string[];\n /** Fields stored UNINDEXED, for exact `where` filtering (scoped search). Default `[]`. */\n filterFields?: string[];\n}\n\nexport interface SearchIndexPoint {\n id: string;\n /** Indexed text, keyed by field name (the configured `fields`). */\n fields: Record<string, string>;\n /** Filter values, keyed by field name (the configured `filterFields`). */\n filters?: Record<string, string>;\n}\n\nexport interface SearchIndexHit {\n id: string;\n /** Relevance (higher = better; derived from BM25 rank). */\n score: number;\n}\n\nexport interface SearchIndex {\n ensureCollection(name: string, opts: SearchIndexOptions): void;\n upsert(name: string, points: SearchIndexPoint[]): void;\n search(\n name: string,\n query: string,\n opts?: { limit?: number; where?: Record<string, string> },\n ): SearchIndexHit[];\n delete(\n name: string,\n opts: { id?: string; where?: Record<string, string> },\n ): void;\n}\n\n/**\n * A programmatic, dynamic full-text index over `@monlite/core` (SQLite FTS5) —\n * collections created at runtime, with optional scoped filtering.\n *\n * ```ts\n * const idx = createSearchIndex(db);\n * idx.ensureCollection(\"docs\", { fields: [\"title\", \"body\"], filterFields: [\"docId\"] });\n * idx.upsert(\"docs\", [{ id: \"c1\", fields: { title, body }, filters: { docId: \"d1\" } }]);\n * idx.search(\"docs\", \"hello world\", { where: { docId: \"d1\" }, limit: 10 }); // scoped\n * ```\n */\nexport function createSearchIndex(db: Monlite): SearchIndex {\n const configs = new Map<\n string,\n { fields: string[]; filterFields: string[] }\n >();\n\n const create = (name: string, fields: string[], filterFields: string[]) => {\n const n = ftsIdent(name);\n const fcols = fields.map(ftsIdent).join(\", \");\n const ucols = filterFields\n .map((f) => `, ${ftsIdent(f)} UNINDEXED`)\n .join(\"\");\n db.sqlite.exec(\n `CREATE VIRTUAL TABLE IF NOT EXISTS \"${n}\" USING fts5(doc_id UNINDEXED, ${fcols}${ucols})`,\n );\n configs.set(name, { fields, filterFields });\n return configs.get(name)!;\n };\n\n const known = (name: string) => {\n if (configs.has(name)) return configs.get(name)!;\n const row = db.sqlite\n .prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name = ?`)\n .get(name);\n if (row) {\n configs.set(name, { fields: [], filterFields: [] });\n return configs.get(name)!;\n }\n return undefined;\n };\n\n return {\n ensureCollection(name, { fields, filterFields = [] }) {\n if (!configs.has(name)) create(name, fields, filterFields);\n },\n\n upsert(name, points) {\n if (!points?.length) return;\n const cfg = configs.get(name);\n if (!cfg)\n throw new Error(\n `@monlite/fts: ensureCollection(\"${name}\") before upsert`,\n );\n const cols = [...cfg.fields, ...cfg.filterFields];\n const colList = cols.map((c) => `, ${c}`).join(\"\");\n const ph = cols.map(() => \", ?\").join(\"\");\n const del = db.sqlite.prepare(`DELETE FROM \"${name}\" WHERE doc_id = ?`);\n const ins = db.sqlite.prepare(\n `INSERT INTO \"${name}\"(doc_id${colList}) VALUES (?${ph})`,\n );\n db.sqlite.exec(\"BEGIN\");\n try {\n for (const p of points) {\n del.run(p.id);\n const vals = [\n ...cfg.fields.map((f) => p.fields?.[f] ?? \"\"),\n ...cfg.filterFields.map((f) => p.filters?.[f] ?? \"\"),\n ];\n ins.run(p.id, ...vals);\n }\n db.sqlite.exec(\"COMMIT\");\n } catch (err) {\n try {\n db.sqlite.exec(\"ROLLBACK\");\n } catch {\n /* ignore */\n }\n throw err;\n }\n },\n\n search(name, query, opts = {}) {\n const cfg = known(name);\n if (!cfg) return [];\n const limit = opts.limit ?? 50;\n const where = Object.entries(opts.where ?? {}).filter(\n ([, v]) => v != null,\n );\n const clause = where.map(([k]) => ` AND ${ftsIdent(k)} = ?`).join(\"\");\n const sql =\n `SELECT doc_id, rank FROM \"${name}\" ` +\n `WHERE \"${name}\" MATCH ?${clause} ORDER BY rank LIMIT ?`;\n let rows: Array<{ doc_id: string; rank: number }>;\n try {\n rows = db.sqlite\n .prepare(sql)\n .all(query, ...where.map(([, v]) => v), limit) as never;\n } catch {\n return [];\n }\n return rows.map((r) => ({ id: r.doc_id, score: -r.rank }));\n },\n\n delete(name, { id, where }) {\n const cfg = known(name);\n if (!cfg) return;\n if (id != null) {\n db.sqlite.prepare(`DELETE FROM \"${name}\" WHERE doc_id = ?`).run(id);\n return;\n }\n const pairs = Object.entries(where ?? {}).filter(([, v]) => v != null);\n if (!pairs.length) return;\n const clause = pairs.map(([k]) => `${ftsIdent(k)} = ?`).join(\" AND \");\n db.sqlite\n .prepare(`DELETE FROM \"${name}\" WHERE ${clause}`)\n .run(...pairs.map(([, v]) => v));\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type {\n Collection,\n Doc,\n Monlite,\n MonlitePlugin,\n WhereInput,\n WithId,\n} from \"@monlite/core\";\n\n/** Map of collection name → searchable field paths (dot-notation allowed). */\nexport type FtsSpec = Record<string, string[]>;\n\nexport interface SearchOptions<T = Doc> {\n /** Max results (default 50). */\n limit?: number;\n /** Additionally constrain matches with a normal monlite where clause. */\n where?: WhereInput<T>;\n /**\n * When `where` is set, how many ranked matches to pull before filtering (then\n * trimmed to `limit`). Larger = better recall for selective filters.\n * Default `max(limit * 10, 200)`.\n */\n candidates?: number;\n}\n\nexport type SearchResult<T = Doc> = WithId<T> & { _score: number };\n\n// Make `collection.search()` typed wherever @monlite/fts is imported.\ndeclare module \"@monlite/core\" {\n interface Collection<T> {\n search(query: string, opts?: SearchOptions<T>): Promise<SearchResult<T>[]>;\n /** Pick up documents written by another process; returns counts. */\n catchUp(): { indexed: number; removed: number };\n }\n}\n\nconst ftsTable = (coll: string) => `${coll}_fts`;\nconst col = (i: number) => `f${i}`;\nconst STATE = \"_monlite_fts_state\";\n// doc_id → fts rowid map, so the per-doc re-index can DELETE by rowid (O(log n))\n// instead of scanning the fts table on the UNINDEXED doc_id column (O(n), which\n// made bulk ingestion O(n²)).\nconst IDMAP = \"_monlite_fts_ids\";\n\nfunction ensureState(db: Monlite): void {\n db.sqlite.exec(\n `CREATE TABLE IF NOT EXISTS ${STATE} (coll TEXT PRIMARY KEY, high_water INTEGER NOT NULL)`,\n );\n db.sqlite.exec(\n `CREATE TABLE IF NOT EXISTS ${IDMAP} (coll TEXT NOT NULL, doc_id TEXT NOT NULL, rid INTEGER NOT NULL, PRIMARY KEY (coll, doc_id))`,\n );\n}\nfunction getHighWater(db: Monlite, coll: string): number {\n const row = db.sqlite\n .prepare(`SELECT high_water FROM ${STATE} WHERE coll = ?`)\n .get(coll) as { high_water: number } | undefined;\n return row?.high_water ?? 0;\n}\nfunction setHighWater(db: Monlite, coll: string, value: number): void {\n db.sqlite\n .prepare(\n `INSERT INTO ${STATE}(coll, high_water) VALUES (?, ?) ON CONFLICT(coll) DO UPDATE SET high_water = excluded.high_water`,\n )\n .run(coll, value);\n}\n\n/**\n * Incrementally index documents written by another process (and drop entries for\n * cross-process deletes), so a separate searcher process becomes fresh without a\n * full {@link reindex}. Returns how many docs were (re)indexed and removed.\n */\nexport function catchUp(\n db: Monlite,\n coll: string,\n fields: string[],\n): { indexed: number; removed: number } {\n const sqlite = db.sqlite;\n ensureState(db);\n const hw = getHighWater(db, coll);\n // `updated_at >= hw` catches recent writes, but a document synced in with an\n // older (past) timestamp sits BELOW the high-water — also index anything missing\n // from the index entirely, so cross-process/past-dated writes don't go unsearchable.\n const docs = sqlite\n .prepare(\n `SELECT _id, updated_at FROM \"${coll}\" WHERE updated_at >= ? ` +\n `OR _id NOT IN (SELECT doc_id FROM ${IDMAP} WHERE coll = ?)`,\n )\n .all(hw, coll) as Array<{ _id: string; updated_at: number }>;\n let max = hw;\n for (const d of docs) {\n indexDoc(db, coll, fields, d._id);\n if (d.updated_at > max) max = d.updated_at;\n }\n // Remove index rows whose document was deleted (possibly by another process).\n const orphans = sqlite\n .prepare(\n `SELECT rowid AS rid, doc_id FROM \"${ftsTable(coll)}\" WHERE doc_id NOT IN (SELECT _id FROM \"${coll}\")`,\n )\n .all() as Array<{ rid: number; doc_id: string }>;\n const del = sqlite.prepare(`DELETE FROM \"${ftsTable(coll)}\" WHERE rowid = ?`);\n const delMap = sqlite.prepare(\n `DELETE FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`,\n );\n for (const o of orphans) {\n del.run(o.rid);\n delMap.run(coll, o.doc_id);\n }\n setHighWater(db, coll, max);\n return { indexed: docs.length, removed: orphans.length };\n}\n\n/** Extract searchable text for a field path from a document. */\nfunction extractText(doc: Record<string, any>, path: string): string {\n let cur: any = doc;\n for (const seg of path.split(\".\")) {\n if (cur == null) return \"\";\n cur = cur[seg];\n }\n if (cur == null) return \"\";\n if (typeof cur === \"string\") return cur;\n if (Array.isArray(cur))\n return cur\n .filter((x) => x != null)\n .map(String)\n .join(\" \");\n if (typeof cur === \"number\" || typeof cur === \"boolean\") return String(cur);\n return \"\";\n}\n\nfunction indexDoc(\n db: Monlite,\n coll: string,\n fields: string[],\n id: string,\n): void {\n const sqlite = db.sqlite;\n const prev = sqlite\n .prepare(`SELECT rid FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`)\n .get(coll, id) as { rid: number } | undefined;\n if (prev)\n sqlite\n .prepare(`DELETE FROM \"${ftsTable(coll)}\" WHERE rowid = ?`)\n .run(prev.rid);\n const doc = db.collection(coll).getRaw(id);\n if (!doc) {\n if (prev)\n sqlite\n .prepare(`DELETE FROM ${IDMAP} WHERE coll = ? AND doc_id = ?`)\n .run(coll, id);\n return; // deleted\n }\n const cols = fields.map((_, i) => `\"${col(i)}\"`).join(\", \");\n const placeholders = fields.map(() => \"?\").join(\", \");\n const values = fields.map((f) => extractText(doc, f));\n const res = sqlite\n .prepare(\n `INSERT INTO \"${ftsTable(coll)}\"(doc_id, ${cols}) VALUES (?, ${placeholders})`,\n )\n .run(id, ...values);\n sqlite\n .prepare(\n `INSERT INTO ${IDMAP}(coll, doc_id, rid) VALUES (?, ?, ?) ON CONFLICT(coll, doc_id) DO UPDATE SET rid = excluded.rid`,\n )\n .run(coll, id, Number(res.lastInsertRowid));\n}\n\n/** Rebuild a collection's FTS index from scratch. */\nexport function reindex(db: Monlite, coll: string, fields: string[]): void {\n const sqlite = db.sqlite;\n sqlite.exec(`DELETE FROM \"${ftsTable(coll)}\"`);\n sqlite.prepare(`DELETE FROM ${IDMAP} WHERE coll = ?`).run(coll);\n for (const doc of db.collection(coll).findManyCore({})) {\n indexDoc(db, coll, fields, (doc as WithId<Doc>)._id);\n }\n}\n\n/**\n * Run an FTS5 MATCH. Untrusted input can contain FTS5 syntax (a stray `\"`, a bare\n * `AND`/`*`, column filters) that throws \"fts5: syntax error\" — so on error, retry\n * with the text quoted as literal phrase tokens. Never throws on user input.\n */\nfunction ftsMatch(\n db: Monlite,\n coll: string,\n query: string,\n fetch: number,\n): Array<{ doc_id: string; rank: number }> {\n const sql =\n `SELECT doc_id, rank FROM \"${ftsTable(coll)}\" ` +\n `WHERE \"${ftsTable(coll)}\" MATCH ? ORDER BY rank LIMIT ?`;\n const run = (q: string) =>\n db.sqlite.prepare(sql).all(q, fetch) as Array<{\n doc_id: string;\n rank: number;\n }>;\n try {\n return run(query);\n } catch {\n const safe = query\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .map((t) => `\"${t.replace(/\"/g, '\"\"')}\"`)\n .join(\" \");\n if (!safe) return [];\n try {\n return run(safe);\n } catch {\n return [];\n }\n }\n}\n\nfunction search<T = Doc>(\n db: Monlite,\n coll: Collection<T>,\n spec: FtsSpec,\n query: string,\n opts?: SearchOptions<T>,\n): Promise<SearchResult<T>[]> {\n const fields = spec[coll.name];\n if (!fields) {\n throw new Error(\n `Collection \"${coll.name}\" is not configured for full-text search`,\n );\n }\n const limit = opts?.limit ?? 50;\n // With a where filter, over-fetch ranked matches then filter + trim to limit,\n // so a selective filter doesn't drop results that exist further down the rank.\n // Cap the pool so the `_id IN (...)` filter can't exceed SQLite's variable limit.\n const fetch = opts?.where\n ? Math.min(Math.max(opts.candidates ?? limit * 10, 200), 10_000)\n : limit;\n const rows = ftsMatch(db, coll.name, query, fetch);\n\n let allowed: Set<string> | null = null;\n if (opts?.where) {\n const ids = rows.map((r) => r.doc_id);\n const idIn = { _id: { in: ids } } as WhereInput<T>;\n const matching = coll.findManyCore({\n where: { AND: [opts.where, idIn] } as WhereInput<T>,\n });\n allowed = new Set(matching.map((d) => d._id));\n }\n\n const out: SearchResult<T>[] = [];\n for (const r of rows) {\n if (out.length >= limit) break; // check BEFORE pushing (limit:0 → 0 results)\n if (allowed && !allowed.has(r.doc_id)) continue;\n const doc = coll.getRaw(r.doc_id);\n if (doc) out.push({ ...doc, _score: -r.rank } as SearchResult<T>);\n }\n return Promise.resolve(out);\n}\n\n/**\n * Full-text search plugin (SQLite FTS5). Pass it to `createDb({ plugins: [...] })`\n * with a map of collection → searchable fields. Adds `collection.search()`, keeps\n * the index in sync on every write, and backfills existing documents on open.\n *\n * ```ts\n * const db = createDb(\"./app.db\", { plugins: [fts({ posts: [\"title\", \"body\"] })] });\n * await db.collection(\"posts\").search(\"hello world\");\n * ```\n */\nexport function fts(spec: FtsSpec): MonlitePlugin {\n let database: Monlite;\n return {\n name: \"fts\",\n init(db) {\n database = db;\n ensureState(db);\n for (const [coll, fields] of Object.entries(spec)) {\n const cols = fields.map((_, i) => `\"${col(i)}\"`).join(\", \");\n db.sqlite.exec(\n `CREATE VIRTUAL TABLE IF NOT EXISTS \"${ftsTable(coll)}\" ` +\n `USING fts5(doc_id UNINDEXED, ${cols})`,\n );\n // Migration: backfill the doc_id→rowid map for any existing fts rows\n // (databases written before the map existed), so re-index deletes hit\n // the right row instead of leaving duplicates.\n db.sqlite\n .prepare(\n `INSERT OR IGNORE INTO ${IDMAP}(coll, doc_id, rid) SELECT ?, doc_id, rowid FROM \"${ftsTable(coll)}\"`,\n )\n .run(coll);\n // Backfill when the index is empty (e.g. enabling FTS on an existing db).\n const count = db.sqlite\n .prepare(`SELECT count(*) AS n FROM \"${ftsTable(coll)}\"`)\n .get() as { n: number };\n if (count.n === 0) reindex(db, coll, fields);\n // Pick up anything other processes wrote since we last indexed.\n catchUp(db, coll, fields);\n }\n },\n afterWrite(db, { collection, ids }) {\n const fields = spec[collection];\n if (!fields) return;\n for (const id of ids) indexDoc(db, collection, fields, id);\n setHighWater(db, collection, Date.now()); // our index is current to now\n },\n collectionMethods: {\n search: (collection, query: string, opts?: SearchOptions) =>\n search(database, collection, spec, query, opts),\n catchUp: (collection) =>\n catchUp(database, collection.name, spec[collection.name] ?? []),\n },\n };\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// Dynamic search index (programmatic, not document-bound)\n//\n// The `fts()` plugin attaches `collection.search()` to a DOCUMENT collection with\n// a STATIC spec. When you instead need a programmatic full-text index over\n// collections created at RUNTIME — RAG corpora, per-tenant indexes — use\n// `createSearchIndex(db)`. Each collection is its own FTS5 table; `fields` are\n// indexed for search and `filterFields` are stored UNINDEXED so a `where` scopes\n// the MATCH (e.g. keyword search within one case/tenant). Synchronous.\n// ────────────────────────────────────────────────────────────────────────────\n\nconst FTS_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;\nfunction ftsIdent(name: string): string {\n if (!FTS_IDENT.test(name))\n throw new Error(`@monlite/fts: unsafe collection/field name \"${name}\"`);\n return name;\n}\n\nexport interface SearchIndexOptions {\n /** Text fields indexed for full-text search. */\n fields: string[];\n /** Fields stored UNINDEXED, for exact `where` filtering (scoped search). Default `[]`. */\n filterFields?: string[];\n}\n\nexport interface SearchIndexPoint {\n id: string;\n /** Indexed text, keyed by field name (the configured `fields`). */\n fields: Record<string, string>;\n /** Filter values, keyed by field name (the configured `filterFields`). */\n filters?: Record<string, string>;\n}\n\nexport interface SearchIndexHit {\n id: string;\n /** Relevance (higher = better; derived from BM25 rank). */\n score: number;\n}\n\nexport interface SearchIndex {\n ensureCollection(name: string, opts: SearchIndexOptions): void;\n upsert(name: string, points: SearchIndexPoint[]): void;\n search(\n name: string,\n query: string,\n opts?: { limit?: number; where?: Record<string, string> },\n ): SearchIndexHit[];\n delete(\n name: string,\n opts: { id?: string; where?: Record<string, string> },\n ): void;\n}\n\n/**\n * A programmatic, dynamic full-text index over `@monlite/core` (SQLite FTS5) —\n * collections created at runtime, with optional scoped filtering.\n *\n * ```ts\n * const idx = createSearchIndex(db);\n * idx.ensureCollection(\"docs\", { fields: [\"title\", \"body\"], filterFields: [\"docId\"] });\n * idx.upsert(\"docs\", [{ id: \"c1\", fields: { title, body }, filters: { docId: \"d1\" } }]);\n * idx.search(\"docs\", \"hello world\", { where: { docId: \"d1\" }, limit: 10 }); // scoped\n * ```\n */\nexport function createSearchIndex(db: Monlite): SearchIndex {\n const configs = new Map<\n string,\n { fields: string[]; filterFields: string[] }\n >();\n\n const create = (name: string, fields: string[], filterFields: string[]) => {\n const n = ftsIdent(name);\n const fcols = fields.map(ftsIdent).join(\", \");\n const ucols = filterFields\n .map((f) => `, ${ftsIdent(f)} UNINDEXED`)\n .join(\"\");\n db.sqlite.exec(\n `CREATE VIRTUAL TABLE IF NOT EXISTS \"${n}\" USING fts5(doc_id UNINDEXED, ${fcols}${ucols})`,\n );\n configs.set(name, { fields, filterFields });\n return configs.get(name)!;\n };\n\n const known = (name: string) => {\n if (configs.has(name)) return configs.get(name)!;\n const row = db.sqlite\n .prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name = ?`)\n .get(name) as { sql?: string } | undefined;\n if (!row?.sql) return undefined;\n // Recover the REAL schema from the fts5 table definition so a reopened index\n // can index/search correctly. Caching empty fields here silently made every\n // later upsert insert an unsearchable row. doc_id is skipped; UNINDEXED columns\n // are filterFields, the rest are searchable fields.\n const cols = row.sql.match(/fts5\\s*\\(([\\s\\S]*)\\)/i);\n const fields: string[] = [];\n const filterFields: string[] = [];\n if (cols) {\n for (const raw of cols[1].split(\",\")) {\n const part = raw.trim();\n const unindexed = /\\bUNINDEXED\\b/i.test(part);\n const colName = part\n .replace(/\\bUNINDEXED\\b/i, \"\")\n .trim()\n .replace(/^\"(.*)\"$/, \"$1\");\n if (!colName || colName === \"doc_id\") continue;\n (unindexed ? filterFields : fields).push(colName);\n }\n }\n configs.set(name, { fields, filterFields });\n return configs.get(name)!;\n };\n\n return {\n ensureCollection(name, { fields, filterFields = [] }) {\n if (!configs.has(name)) create(name, fields, filterFields);\n },\n\n upsert(name, points) {\n if (!points?.length) return;\n const cfg = configs.get(name);\n if (!cfg)\n throw new Error(\n `@monlite/fts: ensureCollection(\"${name}\") before upsert`,\n );\n const cols = [...cfg.fields, ...cfg.filterFields];\n const colList = cols.map((c) => `, ${c}`).join(\"\");\n const ph = cols.map(() => \", ?\").join(\"\");\n const del = db.sqlite.prepare(`DELETE FROM \"${name}\" WHERE doc_id = ?`);\n const ins = db.sqlite.prepare(\n `INSERT INTO \"${name}\"(doc_id${colList}) VALUES (?${ph})`,\n );\n db.sqlite.exec(\"BEGIN\");\n try {\n for (const p of points) {\n del.run(p.id);\n const vals = [\n ...cfg.fields.map((f) => p.fields?.[f] ?? \"\"),\n ...cfg.filterFields.map((f) => p.filters?.[f] ?? \"\"),\n ];\n ins.run(p.id, ...vals);\n }\n db.sqlite.exec(\"COMMIT\");\n } catch (err) {\n try {\n db.sqlite.exec(\"ROLLBACK\");\n } catch {\n /* ignore */\n }\n throw err;\n }\n },\n\n search(name, query, opts = {}) {\n const cfg = known(name);\n if (!cfg) return [];\n const limit = opts.limit ?? 50;\n const where = Object.entries(opts.where ?? {}).filter(\n ([, v]) => v != null,\n );\n const clause = where.map(([k]) => ` AND ${ftsIdent(k)} = ?`).join(\"\");\n const sql =\n `SELECT doc_id, rank FROM \"${name}\" ` +\n `WHERE \"${name}\" MATCH ?${clause} ORDER BY rank LIMIT ?`;\n let rows: Array<{ doc_id: string; rank: number }>;\n try {\n rows = db.sqlite\n .prepare(sql)\n .all(query, ...where.map(([, v]) => v), limit) as never;\n } catch {\n return [];\n }\n return rows.map((r) => ({ id: r.doc_id, score: -r.rank }));\n },\n\n delete(name, { id, where }) {\n const cfg = known(name);\n if (!cfg) return;\n if (id != null) {\n db.sqlite.prepare(`DELETE FROM \"${name}\" WHERE doc_id = ?`).run(id);\n return;\n }\n const pairs = Object.entries(where ?? {}).filter(([, v]) => v != null);\n if (!pairs.length) return;\n const clause = pairs.map(([k]) => `${ftsIdent(k)} = ?`).join(\" AND \");\n db.sqlite\n .prepare(`DELETE FROM \"${name}\" WHERE ${clause}`)\n .run(...pairs.map(([, v]) => v));\n },\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monlite/fts",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Full-text search for @monlite/core, powered by SQLite FTS5. Adds collection.search().",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -22,11 +22,6 @@
22
22
  "dist"
23
23
  ],
24
24
  "sideEffects": false,
25
- "scripts": {
26
- "build": "tsup",
27
- "test": "vitest run",
28
- "typecheck": "tsc --noEmit"
29
- },
30
25
  "keywords": [
31
26
  "monlite",
32
27
  "fts",
@@ -53,7 +48,7 @@
53
48
  "node": ">=18"
54
49
  },
55
50
  "dependencies": {
56
- "@monlite/core": "workspace:^"
51
+ "@monlite/core": "^2.6.12"
57
52
  },
58
53
  "devDependencies": {
59
54
  "@types/node": "^22.10.0",
@@ -61,5 +56,10 @@
61
56
  "tsup": "^8.3.5",
62
57
  "typescript": "^5.7.2",
63
58
  "vitest": "^2.1.8"
59
+ },
60
+ "scripts": {
61
+ "build": "tsup",
62
+ "test": "vitest run",
63
+ "typecheck": "tsc --noEmit"
64
64
  }
65
- }
65
+ }