@stablemodels/qmd-cf 0.1.0

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunker.d.ts +11 -0
  3. package/dist/chunker.d.ts.map +1 -0
  4. package/dist/chunker.js +199 -0
  5. package/dist/chunker.js.map +1 -0
  6. package/dist/fts.d.ts +19 -0
  7. package/dist/fts.d.ts.map +1 -0
  8. package/dist/fts.js +109 -0
  9. package/dist/fts.js.map +1 -0
  10. package/dist/hash.d.ts +7 -0
  11. package/dist/hash.d.ts.map +1 -0
  12. package/dist/hash.js +14 -0
  13. package/dist/hash.js.map +1 -0
  14. package/dist/index.d.ts +56 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +57 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/qmd.d.ts +158 -0
  19. package/dist/qmd.d.ts.map +1 -0
  20. package/dist/qmd.js +462 -0
  21. package/dist/qmd.js.map +1 -0
  22. package/dist/rrf.d.ts +22 -0
  23. package/dist/rrf.d.ts.map +1 -0
  24. package/dist/rrf.js +92 -0
  25. package/dist/rrf.js.map +1 -0
  26. package/dist/schema.d.ts +14 -0
  27. package/dist/schema.d.ts.map +1 -0
  28. package/dist/schema.js +128 -0
  29. package/dist/schema.js.map +1 -0
  30. package/dist/testing.d.ts +77 -0
  31. package/dist/testing.d.ts.map +1 -0
  32. package/dist/testing.js +242 -0
  33. package/dist/testing.js.map +1 -0
  34. package/dist/types.d.ts +118 -0
  35. package/dist/types.d.ts.map +1 -0
  36. package/dist/types.js +9 -0
  37. package/dist/types.js.map +1 -0
  38. package/dist/vector.d.ts +38 -0
  39. package/dist/vector.d.ts.map +1 -0
  40. package/dist/vector.js +174 -0
  41. package/dist/vector.js.map +1 -0
  42. package/package.json +49 -0
  43. package/src/bun-sqlite.d.ts +17 -0
  44. package/src/chunker.ts +250 -0
  45. package/src/fts.ts +140 -0
  46. package/src/hash.ts +13 -0
  47. package/src/index.ts +72 -0
  48. package/src/qmd.ts +706 -0
  49. package/src/rrf.ts +115 -0
  50. package/src/schema.ts +147 -0
  51. package/src/testing.ts +303 -0
  52. package/src/types.ts +124 -0
  53. package/src/vector.ts +236 -0
package/dist/qmd.d.ts ADDED
@@ -0,0 +1,158 @@
1
+ import type { Document, EmbedFn, FtsResult, HybridSearchOptions, IndexStats, QmdConfig, SearchOptions, SearchResult, VectorResult } from "./types.js";
2
+ /**
3
+ * Qmd — Hybrid full-text + vector search for Cloudflare Durable Objects.
4
+ *
5
+ * A DO-native reimagination of qmd (https://github.com/tobi/qmd) that brings
6
+ * hybrid BM25 + semantic search to Cloudflare's edge.
7
+ *
8
+ * FTS5 runs co-located in the Durable Object's SQLite for zero-latency keyword search.
9
+ * Vector search optionally uses Cloudflare Vectorize for semantic similarity.
10
+ *
11
+ * Usage:
12
+ * ```ts
13
+ * // FTS-only (no external dependencies)
14
+ * const qmd = new Qmd(ctx.storage.sql);
15
+ *
16
+ * // Hybrid FTS + Vector
17
+ * const qmd = new Qmd(ctx.storage.sql, {
18
+ * vectorize: env.VECTORIZE,
19
+ * embedFn: (texts) => workerAiEmbed(env.AI, texts),
20
+ * });
21
+ *
22
+ * // Index a document
23
+ * await qmd.index({ id: "soul.md", content: "...", title: "Soul" });
24
+ *
25
+ * // Search
26
+ * const results = await qmd.search("what does the agent care about?");
27
+ * ```
28
+ */
29
+ export declare class Qmd {
30
+ private sql;
31
+ private vectorize;
32
+ private embedFn;
33
+ private config;
34
+ private initialized;
35
+ constructor(sql: SqlStorage, options?: {
36
+ vectorize?: Vectorize;
37
+ embedFn?: EmbedFn;
38
+ config?: QmdConfig;
39
+ });
40
+ /** Ensure the FTS5 schema is initialized. Called automatically on first operation. */
41
+ private ensureInit;
42
+ /** Whether vector search is available. */
43
+ get hasVectorSearch(): boolean;
44
+ /**
45
+ * Index a document for search.
46
+ *
47
+ * The document is chunked and inserted into FTS5. If Vectorize is configured,
48
+ * chunks are also embedded and upserted into the vector index.
49
+ *
50
+ * If the content is unchanged (same hash), chunking and vector indexing are
51
+ * skipped. Document metadata (title, namespace, etc.) is always updated.
52
+ */
53
+ index(doc: Document): Promise<{
54
+ chunks: number;
55
+ skipped: boolean;
56
+ }>;
57
+ /**
58
+ * Index multiple documents in batch.
59
+ * More efficient than calling index() in a loop when Vectorize is configured,
60
+ * as embeddings are batched.
61
+ */
62
+ indexBatch(docs: Document[]): Promise<{
63
+ documents: number;
64
+ chunks: number;
65
+ skipped: number;
66
+ }>;
67
+ /**
68
+ * Remove a document and all its chunks from the index.
69
+ * Also removes vectors from Vectorize if configured.
70
+ */
71
+ remove(docId: string): Promise<void>;
72
+ /**
73
+ * Full-text search using FTS5 BM25 ranking.
74
+ * Always available — no external dependencies needed.
75
+ */
76
+ searchFts(query: string, options?: SearchOptions): FtsResult[];
77
+ /**
78
+ * Vector similarity search using Cloudflare Vectorize.
79
+ * Requires vectorize + embedFn to be configured.
80
+ */
81
+ searchVector(query: string, options?: SearchOptions): Promise<VectorResult[]>;
82
+ /**
83
+ * Hybrid search combining FTS5 BM25 + Vectorize similarity via Reciprocal Rank Fusion.
84
+ *
85
+ * If only FTS is available, falls back to FTS-only results wrapped as SearchResult[].
86
+ * If both are available, runs FTS first as a probe. If BM25 has a strong signal
87
+ * (top score >= 0.85 with gap >= 0.15 to second), returns FTS results directly
88
+ * without the Vectorize round-trip. Otherwise, runs vector search and fuses with RRF.
89
+ */
90
+ search(query: string, options?: HybridSearchOptions): Promise<SearchResult[]>;
91
+ /**
92
+ * Get a document by ID. Returns the full reconstructed content.
93
+ */
94
+ get(docId: string): {
95
+ content: string;
96
+ title: string | null;
97
+ docType: string | null;
98
+ } | null;
99
+ /**
100
+ * Check if a document exists in the index.
101
+ */
102
+ has(docId: string): boolean;
103
+ /**
104
+ * List all indexed document IDs, optionally filtered.
105
+ */
106
+ list(options?: {
107
+ namespace?: string;
108
+ docType?: string;
109
+ }): string[];
110
+ /**
111
+ * List documents by namespace pattern. Direct SQL query — no FTS or vector search.
112
+ * Supports glob patterns: "people/*" matches all namespaces starting with "people/".
113
+ * Returns documents ordered by most recently updated first.
114
+ */
115
+ listByNamespace(pattern: string, limit?: number): Array<{
116
+ docId: string;
117
+ title: string | null;
118
+ content: string;
119
+ namespace: string | null;
120
+ }>;
121
+ /**
122
+ * Get index statistics.
123
+ */
124
+ stats(): IndexStats;
125
+ /**
126
+ * Rebuild the FTS index from scratch.
127
+ * Useful after schema changes or data corruption.
128
+ */
129
+ rebuild(): void;
130
+ /**
131
+ * Set a context description for a path prefix.
132
+ * Contexts enrich vector embeddings for all documents matching the prefix.
133
+ */
134
+ setContext(prefix: string, description: string, namespace?: string): void;
135
+ /**
136
+ * Remove a context by prefix.
137
+ */
138
+ removeContext(prefix: string, namespace?: string): void;
139
+ /**
140
+ * List all contexts, optionally filtered by namespace.
141
+ */
142
+ listContexts(namespace?: string): Array<{
143
+ prefix: string;
144
+ description: string;
145
+ namespace: string;
146
+ }>;
147
+ /**
148
+ * Get all matching contexts for a document ID.
149
+ * Matches hierarchically: for "life/areas/health/exercise.md",
150
+ * returns contexts at "", "life/", "life/areas/", "life/areas/health/".
151
+ * Results ordered from most general to most specific.
152
+ */
153
+ getContextsForDoc(docId: string): Array<{
154
+ prefix: string;
155
+ description: string;
156
+ }>;
157
+ }
158
+ //# sourceMappingURL=qmd.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qmd.d.ts","sourceRoot":"","sources":["../src/qmd.ts"],"names":[],"mappings":"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"}
package/dist/qmd.js ADDED
@@ -0,0 +1,462 @@
1
+ import { chunkText } from "./chunker.js";
2
+ import { searchFts } from "./fts.js";
3
+ import { fnv1a32 } from "./hash.js";
4
+ import { reciprocalRankFusion } from "./rrf.js";
5
+ import { initSchema } from "./schema.js";
6
+ import { indexVectors, removeVectors, searchVector } from "./vector.js";
7
+ /** Minimum normalized BM25 score to consider a "strong signal". */
8
+ const STRONG_SIGNAL_MIN_SCORE = 0.85;
9
+ /** Minimum gap between top-1 and top-2 BM25 scores for strong signal. */
10
+ const STRONG_SIGNAL_MIN_GAP = 0.15;
11
+ /**
12
+ * Qmd — Hybrid full-text + vector search for Cloudflare Durable Objects.
13
+ *
14
+ * A DO-native reimagination of qmd (https://github.com/tobi/qmd) that brings
15
+ * hybrid BM25 + semantic search to Cloudflare's edge.
16
+ *
17
+ * FTS5 runs co-located in the Durable Object's SQLite for zero-latency keyword search.
18
+ * Vector search optionally uses Cloudflare Vectorize for semantic similarity.
19
+ *
20
+ * Usage:
21
+ * ```ts
22
+ * // FTS-only (no external dependencies)
23
+ * const qmd = new Qmd(ctx.storage.sql);
24
+ *
25
+ * // Hybrid FTS + Vector
26
+ * const qmd = new Qmd(ctx.storage.sql, {
27
+ * vectorize: env.VECTORIZE,
28
+ * embedFn: (texts) => workerAiEmbed(env.AI, texts),
29
+ * });
30
+ *
31
+ * // Index a document
32
+ * await qmd.index({ id: "soul.md", content: "...", title: "Soul" });
33
+ *
34
+ * // Search
35
+ * const results = await qmd.search("what does the agent care about?");
36
+ * ```
37
+ */
38
+ export class Qmd {
39
+ sql;
40
+ vectorize;
41
+ embedFn;
42
+ config;
43
+ initialized = false;
44
+ constructor(sql, options) {
45
+ this.sql = sql;
46
+ this.vectorize = options?.vectorize ?? null;
47
+ this.embedFn = options?.embedFn ?? null;
48
+ if (this.vectorize && !this.embedFn) {
49
+ throw new Error("embedFn is required when vectorize is provided");
50
+ }
51
+ this.config = {
52
+ chunkSize: options?.config?.chunkSize ?? 3200,
53
+ chunkOverlap: options?.config?.chunkOverlap ?? 480,
54
+ tokenizer: options?.config?.tokenizer ?? "unicode61",
55
+ };
56
+ }
57
+ /** Ensure the FTS5 schema is initialized. Called automatically on first operation. */
58
+ ensureInit() {
59
+ if (this.initialized)
60
+ return;
61
+ initSchema(this.sql, this.config.tokenizer);
62
+ this.initialized = true;
63
+ }
64
+ /** Whether vector search is available. */
65
+ get hasVectorSearch() {
66
+ return this.vectorize !== null && this.embedFn !== null;
67
+ }
68
+ /**
69
+ * Index a document for search.
70
+ *
71
+ * The document is chunked and inserted into FTS5. If Vectorize is configured,
72
+ * chunks are also embedded and upserted into the vector index.
73
+ *
74
+ * If the content is unchanged (same hash), chunking and vector indexing are
75
+ * skipped. Document metadata (title, namespace, etc.) is always updated.
76
+ */
77
+ async index(doc) {
78
+ this.ensureInit();
79
+ const contentHash = fnv1a32(doc.content);
80
+ const metadataJson = doc.metadata ? JSON.stringify(doc.metadata) : null;
81
+ // Check if content is unchanged
82
+ const existing = this.sql
83
+ .exec("SELECT content_hash FROM qmd_documents WHERE id = ?", doc.id)
84
+ .toArray();
85
+ if (existing.length > 0 && existing[0].content_hash === contentHash) {
86
+ // Content unchanged — update metadata but skip re-chunking
87
+ this.sql.exec(`UPDATE qmd_documents SET title = ?, doc_type = ?, namespace = ?, metadata = ?, updated_at = datetime('now')
88
+ WHERE id = ?`, doc.title ?? null, doc.docType ?? null, doc.namespace ?? null, metadataJson, doc.id);
89
+ const chunkCount = this.sql
90
+ .exec("SELECT COUNT(*) as cnt FROM qmd_chunks WHERE doc_id = ?", doc.id)
91
+ .one().cnt;
92
+ return { chunks: chunkCount, skipped: true };
93
+ }
94
+ // Upsert document metadata with content hash
95
+ this.sql.exec(`INSERT OR REPLACE INTO qmd_documents (id, title, doc_type, namespace, metadata, content_hash, updated_at)
96
+ VALUES (?, ?, ?, ?, ?, ?, datetime('now'))`, doc.id, doc.title ?? null, doc.docType ?? null, doc.namespace ?? null, metadataJson, contentHash);
97
+ // Delete old chunks (triggers will clean up FTS)
98
+ this.sql.exec("DELETE FROM qmd_chunks WHERE doc_id = ?", doc.id);
99
+ // Chunk and insert
100
+ const chunks = chunkText(doc.id, doc.content, this.config.chunkSize, this.config.chunkOverlap);
101
+ for (const chunk of chunks) {
102
+ this.sql.exec("INSERT INTO qmd_chunks (doc_id, seq, content, char_offset) VALUES (?, ?, ?, ?)", chunk.docId, chunk.seq, chunk.text, chunk.charOffset);
103
+ }
104
+ // Vector indexing (async, non-blocking for FTS)
105
+ if (this.vectorize && this.embedFn) {
106
+ const contexts = this.getContextsForDoc(doc.id);
107
+ const contextText = contexts.map((c) => c.description).join(". ");
108
+ await indexVectors(this.vectorize, this.embedFn, chunks.map((c) => ({
109
+ docId: c.docId,
110
+ seq: c.seq,
111
+ text: c.text,
112
+ title: doc.title,
113
+ namespace: doc.namespace,
114
+ docType: doc.docType,
115
+ context: contextText || undefined,
116
+ })));
117
+ }
118
+ return { chunks: chunks.length, skipped: false };
119
+ }
120
+ /**
121
+ * Index multiple documents in batch.
122
+ * More efficient than calling index() in a loop when Vectorize is configured,
123
+ * as embeddings are batched.
124
+ */
125
+ async indexBatch(docs) {
126
+ this.ensureInit();
127
+ let totalChunks = 0;
128
+ let skippedCount = 0;
129
+ const allVectorChunks = [];
130
+ for (const doc of docs) {
131
+ const contentHash = fnv1a32(doc.content);
132
+ const metadataJson = doc.metadata ? JSON.stringify(doc.metadata) : null;
133
+ // Check if content is unchanged
134
+ const existing = this.sql
135
+ .exec("SELECT content_hash FROM qmd_documents WHERE id = ?", doc.id)
136
+ .toArray();
137
+ if (existing.length > 0 && existing[0].content_hash === contentHash) {
138
+ // Update metadata, skip re-chunking
139
+ this.sql.exec(`UPDATE qmd_documents SET title = ?, doc_type = ?, namespace = ?, metadata = ?, updated_at = datetime('now')
140
+ WHERE id = ?`, doc.title ?? null, doc.docType ?? null, doc.namespace ?? null, metadataJson, doc.id);
141
+ const chunkCount = this.sql
142
+ .exec("SELECT COUNT(*) as cnt FROM qmd_chunks WHERE doc_id = ?", doc.id)
143
+ .one().cnt;
144
+ totalChunks += chunkCount;
145
+ skippedCount++;
146
+ continue;
147
+ }
148
+ this.sql.exec(`INSERT OR REPLACE INTO qmd_documents (id, title, doc_type, namespace, metadata, content_hash, updated_at)
149
+ VALUES (?, ?, ?, ?, ?, ?, datetime('now'))`, doc.id, doc.title ?? null, doc.docType ?? null, doc.namespace ?? null, metadataJson, contentHash);
150
+ this.sql.exec("DELETE FROM qmd_chunks WHERE doc_id = ?", doc.id);
151
+ const chunks = chunkText(doc.id, doc.content, this.config.chunkSize, this.config.chunkOverlap);
152
+ for (const chunk of chunks) {
153
+ this.sql.exec("INSERT INTO qmd_chunks (doc_id, seq, content, char_offset) VALUES (?, ?, ?, ?)", chunk.docId, chunk.seq, chunk.text, chunk.charOffset);
154
+ }
155
+ totalChunks += chunks.length;
156
+ if (this.vectorize && this.embedFn) {
157
+ const contexts = this.getContextsForDoc(doc.id);
158
+ const contextText = contexts.map((c) => c.description).join(". ");
159
+ for (const c of chunks) {
160
+ allVectorChunks.push({
161
+ docId: c.docId,
162
+ seq: c.seq,
163
+ text: c.text,
164
+ title: doc.title,
165
+ namespace: doc.namespace,
166
+ docType: doc.docType,
167
+ context: contextText || undefined,
168
+ });
169
+ }
170
+ }
171
+ }
172
+ // Batch embed and upsert vectors
173
+ if (this.vectorize && this.embedFn && allVectorChunks.length > 0) {
174
+ await indexVectors(this.vectorize, this.embedFn, allVectorChunks);
175
+ }
176
+ return {
177
+ documents: docs.length,
178
+ chunks: totalChunks,
179
+ skipped: skippedCount,
180
+ };
181
+ }
182
+ /**
183
+ * Remove a document and all its chunks from the index.
184
+ * Also removes vectors from Vectorize if configured.
185
+ */
186
+ async remove(docId) {
187
+ this.ensureInit();
188
+ if (this.vectorize) {
189
+ await removeVectors(this.vectorize, this.sql, docId);
190
+ }
191
+ // Delete chunks (FTS cleanup via trigger)
192
+ this.sql.exec("DELETE FROM qmd_chunks WHERE doc_id = ?", docId);
193
+ // Delete document
194
+ this.sql.exec("DELETE FROM qmd_documents WHERE id = ?", docId);
195
+ }
196
+ /**
197
+ * Full-text search using FTS5 BM25 ranking.
198
+ * Always available — no external dependencies needed.
199
+ */
200
+ searchFts(query, options) {
201
+ this.ensureInit();
202
+ return searchFts(this.sql, query, options);
203
+ }
204
+ /**
205
+ * Vector similarity search using Cloudflare Vectorize.
206
+ * Requires vectorize + embedFn to be configured.
207
+ */
208
+ async searchVector(query, options) {
209
+ if (!this.vectorize || !this.embedFn) {
210
+ throw new Error("Vector search requires vectorize and embedFn to be configured");
211
+ }
212
+ this.ensureInit();
213
+ return searchVector(this.vectorize, this.embedFn, this.sql, query, options);
214
+ }
215
+ /**
216
+ * Hybrid search combining FTS5 BM25 + Vectorize similarity via Reciprocal Rank Fusion.
217
+ *
218
+ * If only FTS is available, falls back to FTS-only results wrapped as SearchResult[].
219
+ * If both are available, runs FTS first as a probe. If BM25 has a strong signal
220
+ * (top score >= 0.85 with gap >= 0.15 to second), returns FTS results directly
221
+ * without the Vectorize round-trip. Otherwise, runs vector search and fuses with RRF.
222
+ */
223
+ async search(query, options) {
224
+ this.ensureInit();
225
+ const limit = options?.limit ?? 10;
226
+ // Fetch more from each source for better fusion
227
+ const sourceFetchLimit = limit * 3;
228
+ const ftsOptions = {
229
+ limit: sourceFetchLimit,
230
+ docType: options?.docType,
231
+ namespace: options?.namespace,
232
+ };
233
+ // FTS-only mode
234
+ if (!this.vectorize || !this.embedFn) {
235
+ const ftsResults = searchFts(this.sql, query, ftsOptions);
236
+ return ftsResults.slice(0, limit).map((r) => ({
237
+ docId: r.docId,
238
+ score: r.score,
239
+ snippet: r.snippet,
240
+ sources: ["fts"],
241
+ sourceScores: { fts: r.score },
242
+ title: r.title,
243
+ docType: r.docType,
244
+ namespace: r.namespace,
245
+ metadata: r.metadata,
246
+ }));
247
+ }
248
+ // Hybrid mode: run FTS first for strong signal probe
249
+ const ftsResults = searchFts(this.sql, query, ftsOptions);
250
+ // Strong signal detection: if BM25 has a clear winner, skip vector search
251
+ if (ftsResults.length >= 1) {
252
+ const topScore = ftsResults[0].score;
253
+ const secondScore = ftsResults.length >= 2 ? ftsResults[1].score : 0;
254
+ if (topScore >= STRONG_SIGNAL_MIN_SCORE &&
255
+ topScore - secondScore >= STRONG_SIGNAL_MIN_GAP) {
256
+ return ftsResults.slice(0, limit).map((r) => ({
257
+ docId: r.docId,
258
+ score: r.score,
259
+ snippet: r.snippet,
260
+ sources: ["fts"],
261
+ sourceScores: { fts: r.score },
262
+ title: r.title,
263
+ docType: r.docType,
264
+ namespace: r.namespace,
265
+ metadata: r.metadata,
266
+ }));
267
+ }
268
+ }
269
+ // No strong signal — run vector search and fuse
270
+ const vectorOptions = {
271
+ limit: sourceFetchLimit,
272
+ docType: options?.docType,
273
+ namespace: options?.namespace,
274
+ };
275
+ const vectorResults = await searchVector(this.vectorize, this.embedFn, this.sql, query, vectorOptions);
276
+ return reciprocalRankFusion(ftsResults, vectorResults, {
277
+ ftsWeight: options?.ftsWeight,
278
+ vectorWeight: options?.vectorWeight,
279
+ k: options?.rrfK,
280
+ limit,
281
+ });
282
+ }
283
+ /**
284
+ * Get a document by ID. Returns the full reconstructed content.
285
+ */
286
+ get(docId) {
287
+ this.ensureInit();
288
+ const doc = this.sql
289
+ .exec("SELECT title, doc_type FROM qmd_documents WHERE id = ?", docId)
290
+ .toArray();
291
+ if (doc.length === 0)
292
+ return null;
293
+ const chunks = this.sql
294
+ .exec("SELECT content FROM qmd_chunks WHERE doc_id = ? ORDER BY seq", docId)
295
+ .toArray();
296
+ // Reconstruct content from chunks (overlap means we can't just concatenate)
297
+ // For now, return the first chunk's full text + subsequent chunks' non-overlapping portions
298
+ // This is an approximation — exact reconstruction would need char_offset tracking
299
+ const content = chunks.map((c) => c.content).join("\n\n");
300
+ return {
301
+ content,
302
+ title: doc[0].title,
303
+ docType: doc[0].doc_type,
304
+ };
305
+ }
306
+ /**
307
+ * Check if a document exists in the index.
308
+ */
309
+ has(docId) {
310
+ this.ensureInit();
311
+ const result = this.sql
312
+ .exec("SELECT COUNT(*) as cnt FROM qmd_documents WHERE id = ?", docId)
313
+ .toArray();
314
+ return result.length > 0 && result[0].cnt > 0;
315
+ }
316
+ /**
317
+ * List all indexed document IDs, optionally filtered.
318
+ */
319
+ list(options) {
320
+ this.ensureInit();
321
+ const filters = [];
322
+ const bindings = [];
323
+ if (options?.namespace) {
324
+ filters.push("namespace = ?");
325
+ bindings.push(options.namespace);
326
+ }
327
+ if (options?.docType) {
328
+ filters.push("doc_type = ?");
329
+ bindings.push(options.docType);
330
+ }
331
+ const where = filters.length > 0 ? `WHERE ${filters.join(" AND ")}` : "";
332
+ return this.sql
333
+ .exec(`SELECT id FROM qmd_documents ${where} ORDER BY id`, ...bindings)
334
+ .toArray()
335
+ .map((r) => r.id);
336
+ }
337
+ /**
338
+ * List documents by namespace pattern. Direct SQL query — no FTS or vector search.
339
+ * Supports glob patterns: "people/*" matches all namespaces starting with "people/".
340
+ * Returns documents ordered by most recently updated first.
341
+ */
342
+ listByNamespace(pattern, limit = 50) {
343
+ this.ensureInit();
344
+ let whereClause;
345
+ let binding;
346
+ if (pattern.includes("*")) {
347
+ const prefix = pattern.replace(/\*+$/, "").replace(/\/+$/, "");
348
+ whereClause = "d.namespace LIKE ?";
349
+ binding = `${prefix}/%`;
350
+ }
351
+ else {
352
+ whereClause = "d.namespace = ?";
353
+ binding = pattern;
354
+ }
355
+ const rows = this.sql
356
+ .exec(`SELECT d.id, d.title, d.namespace,
357
+ GROUP_CONCAT(c.content, '\n\n') as content
358
+ FROM qmd_documents d
359
+ JOIN qmd_chunks c ON c.doc_id = d.id
360
+ WHERE ${whereClause}
361
+ GROUP BY d.id
362
+ ORDER BY d.updated_at DESC
363
+ LIMIT ?`, binding, limit)
364
+ .toArray();
365
+ return rows.map((r) => ({
366
+ docId: r.id,
367
+ title: r.title,
368
+ content: r.content,
369
+ namespace: r.namespace,
370
+ }));
371
+ }
372
+ /**
373
+ * Get index statistics.
374
+ */
375
+ stats() {
376
+ this.ensureInit();
377
+ const docCount = this.sql
378
+ .exec("SELECT COUNT(*) as cnt FROM qmd_documents")
379
+ .one().cnt;
380
+ const chunkCount = this.sql
381
+ .exec("SELECT COUNT(*) as cnt FROM qmd_chunks")
382
+ .one().cnt;
383
+ const namespaces = this.sql
384
+ .exec("SELECT DISTINCT namespace FROM qmd_documents WHERE namespace IS NOT NULL")
385
+ .toArray()
386
+ .map((r) => r.namespace);
387
+ const docTypes = this.sql
388
+ .exec("SELECT DISTINCT doc_type FROM qmd_documents WHERE doc_type IS NOT NULL")
389
+ .toArray()
390
+ .map((r) => r.doc_type);
391
+ return {
392
+ totalDocuments: docCount,
393
+ totalChunks: chunkCount,
394
+ totalVectors: 0, // Can't query Vectorize count from binding
395
+ namespaces,
396
+ docTypes,
397
+ };
398
+ }
399
+ /**
400
+ * Rebuild the FTS index from scratch.
401
+ * Useful after schema changes or data corruption.
402
+ */
403
+ rebuild() {
404
+ this.ensureInit();
405
+ this.sql.exec("INSERT INTO qmd_chunks_fts(qmd_chunks_fts) VALUES('rebuild')");
406
+ }
407
+ // --- Context system ---
408
+ /**
409
+ * Set a context description for a path prefix.
410
+ * Contexts enrich vector embeddings for all documents matching the prefix.
411
+ */
412
+ setContext(prefix, description, namespace) {
413
+ this.ensureInit();
414
+ this.sql.exec("INSERT OR REPLACE INTO qmd_contexts (prefix, namespace, description) VALUES (?, ?, ?)", prefix, namespace ?? "", description);
415
+ }
416
+ /**
417
+ * Remove a context by prefix.
418
+ */
419
+ removeContext(prefix, namespace) {
420
+ this.ensureInit();
421
+ this.sql.exec("DELETE FROM qmd_contexts WHERE prefix = ? AND namespace = ?", prefix, namespace ?? "");
422
+ }
423
+ /**
424
+ * List all contexts, optionally filtered by namespace.
425
+ */
426
+ listContexts(namespace) {
427
+ this.ensureInit();
428
+ if (namespace !== undefined) {
429
+ return this.sql
430
+ .exec("SELECT prefix, description, namespace FROM qmd_contexts WHERE namespace = ? ORDER BY prefix", namespace)
431
+ .toArray();
432
+ }
433
+ return this.sql
434
+ .exec("SELECT prefix, description, namespace FROM qmd_contexts ORDER BY prefix")
435
+ .toArray();
436
+ }
437
+ /**
438
+ * Get all matching contexts for a document ID.
439
+ * Matches hierarchically: for "life/areas/health/exercise.md",
440
+ * returns contexts at "", "life/", "life/areas/", "life/areas/health/".
441
+ * Results ordered from most general to most specific.
442
+ */
443
+ getContextsForDoc(docId) {
444
+ this.ensureInit();
445
+ // Build all possible prefixes
446
+ const prefixes = [""];
447
+ const parts = docId.split("/");
448
+ let current = "";
449
+ for (let i = 0; i < parts.length - 1; i++) {
450
+ current += `${parts[i]}/`;
451
+ prefixes.push(current);
452
+ }
453
+ prefixes.push(docId);
454
+ const placeholders = prefixes.map(() => "?").join(", ");
455
+ return this.sql
456
+ .exec(`SELECT prefix, description FROM qmd_contexts
457
+ WHERE prefix IN (${placeholders}) AND namespace = ''
458
+ ORDER BY length(prefix)`, ...prefixes)
459
+ .toArray();
460
+ }
461
+ }
462
+ //# sourceMappingURL=qmd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qmd.js","sourceRoot":"","sources":["../src/qmd.ts"],"names":[],"mappings":"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"}
package/dist/rrf.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import type { FtsResult, SearchResult, VectorResult } from "./types.js";
2
+ /**
3
+ * Reciprocal Rank Fusion (RRF) — merge ranked result lists into a single ranking.
4
+ *
5
+ * RRF score for document d = Σ(weight_i / (k + rank_i + 1)) across all lists
6
+ * where rank_i is the 0-based position in list i.
7
+ *
8
+ * From qmd: k=60 is the standard constant. Higher k reduces the impact of
9
+ * being ranked #1 vs #5, making the fusion more conservative.
10
+ *
11
+ * Additionally applies a top-rank bonus (from qmd):
12
+ * - Rank #1 in any list: +0.05
13
+ * - Rank #2-3 in any list: +0.02
14
+ * This prevents exact matches from being diluted by expansion queries.
15
+ */
16
+ export declare function reciprocalRankFusion(ftsResults: FtsResult[], vectorResults: VectorResult[], options?: {
17
+ ftsWeight?: number;
18
+ vectorWeight?: number;
19
+ k?: number;
20
+ limit?: number;
21
+ }): SearchResult[];
22
+ //# sourceMappingURL=rrf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rrf.d.ts","sourceRoot":"","sources":["../src/rrf.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAExE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CACnC,UAAU,EAAE,SAAS,EAAE,EACvB,aAAa,EAAE,YAAY,EAAE,EAC7B,OAAO,GAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;CACV,GACJ,YAAY,EAAE,CAyFhB"}