@outfitter/index 0.1.0-rc.1 → 0.1.0-rc.2
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/fts5.d.ts +3 -285
- package/dist/fts5.js +3 -247
- package/dist/index.d.ts +4 -303
- package/dist/index.js +6 -6
- package/dist/migrations.d.ts +1 -12
- package/dist/migrations.js +3 -43
- package/dist/shared/@outfitter/index-1me624ny.js +46 -0
- package/dist/shared/@outfitter/index-3xe3cd6r.d.ts +7 -0
- package/dist/shared/@outfitter/index-8fwmfq7d.js +1 -0
- package/dist/shared/@outfitter/index-bbzmc40h.js +7 -0
- package/dist/shared/@outfitter/index-ewvzd5f9.d.ts +42 -0
- package/dist/shared/@outfitter/index-gf30ny51.js +251 -0
- package/dist/shared/@outfitter/index-m0h11hv5.d.ts +249 -0
- package/dist/shared/@outfitter/index-mmgx1j0h.d.ts +13 -0
- package/dist/types.d.ts +2 -258
- package/dist/types.js +1 -0
- package/dist/version.d.ts +1 -6
- package/dist/version.js +5 -4
- package/package.json +2 -2
package/dist/fts5.d.ts
CHANGED
|
@@ -1,286 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
interface MigrationRegistry<TContext> {
|
|
5
|
-
register(fromVersion: number, toVersion: number, migrate: (context: TContext) => Result<void, StorageError>): void;
|
|
6
|
-
migrate(context: TContext, fromVersion: number, toVersion: number): Result<void, StorageError>;
|
|
7
|
-
}
|
|
8
|
-
interface IndexMigrationContext {
|
|
9
|
-
readonly db: Database;
|
|
10
|
-
}
|
|
11
|
-
type IndexMigrationRegistry = MigrationRegistry<IndexMigrationContext>;
|
|
12
|
-
import { Result as Result2, StorageError as StorageError2 } from "@outfitter/contracts";
|
|
13
|
-
/**
|
|
14
|
-
* FTS5 tokenizer options for text analysis.
|
|
15
|
-
*
|
|
16
|
-
* - `unicode61`: Default tokenizer with Unicode support (recommended for most use cases)
|
|
17
|
-
* - `porter`: Applies Porter stemming algorithm for English text (finds related word forms)
|
|
18
|
-
* - `trigram`: Splits text into 3-character sequences (good for substring matching)
|
|
19
|
-
*/
|
|
20
|
-
type TokenizerType = "unicode61" | "porter" | "trigram";
|
|
21
|
-
/**
|
|
22
|
-
* Options for creating an FTS5 index.
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* ```typescript
|
|
26
|
-
* const options: IndexOptions = {
|
|
27
|
-
* path: "/path/to/index.db",
|
|
28
|
-
* tableName: "documents",
|
|
29
|
-
* tokenizer: "porter",
|
|
30
|
-
* };
|
|
31
|
-
*
|
|
32
|
-
* const index = createIndex(options);
|
|
33
|
-
* ```
|
|
34
|
-
*/
|
|
35
|
-
interface IndexOptions {
|
|
36
|
-
/**
|
|
37
|
-
* Absolute path to the SQLite database file.
|
|
38
|
-
* The file will be created if it does not exist.
|
|
39
|
-
*/
|
|
40
|
-
path: string;
|
|
41
|
-
/**
|
|
42
|
-
* Name of the FTS5 virtual table.
|
|
43
|
-
* @defaultValue "documents"
|
|
44
|
-
*/
|
|
45
|
-
tableName?: string;
|
|
46
|
-
/**
|
|
47
|
-
* FTS5 tokenizer for text analysis.
|
|
48
|
-
* @defaultValue "unicode61"
|
|
49
|
-
*/
|
|
50
|
-
tokenizer?: TokenizerType;
|
|
51
|
-
/**
|
|
52
|
-
* Optional tool identifier recorded in index metadata.
|
|
53
|
-
*/
|
|
54
|
-
tool?: string;
|
|
55
|
-
/**
|
|
56
|
-
* Optional tool version recorded in index metadata.
|
|
57
|
-
*/
|
|
58
|
-
toolVersion?: string;
|
|
59
|
-
/**
|
|
60
|
-
* Optional migration registry for upgrading older index versions.
|
|
61
|
-
*/
|
|
62
|
-
migrations?: IndexMigrationRegistry;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* A document to be indexed in the FTS5 index.
|
|
66
|
-
*
|
|
67
|
-
* Documents have a unique ID, searchable content, and optional metadata.
|
|
68
|
-
* The metadata is stored as JSON and can be used to attach additional
|
|
69
|
-
* information that is returned with search results.
|
|
70
|
-
*
|
|
71
|
-
* @example
|
|
72
|
-
* ```typescript
|
|
73
|
-
* const doc: IndexDocument = {
|
|
74
|
-
* id: "note-123",
|
|
75
|
-
* content: "This is the searchable text content",
|
|
76
|
-
* metadata: { title: "My Note", createdAt: Date.now() },
|
|
77
|
-
* };
|
|
78
|
-
* ```
|
|
79
|
-
*/
|
|
80
|
-
interface IndexDocument {
|
|
81
|
-
/** Unique identifier for this document */
|
|
82
|
-
id: string;
|
|
83
|
-
/** Searchable text content */
|
|
84
|
-
content: string;
|
|
85
|
-
/**
|
|
86
|
-
* Optional metadata associated with the document.
|
|
87
|
-
* Stored as JSON and returned with search results.
|
|
88
|
-
*/
|
|
89
|
-
metadata?: Record<string, unknown>;
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Query parameters for searching the FTS5 index.
|
|
93
|
-
*
|
|
94
|
-
* Uses FTS5 query syntax which supports:
|
|
95
|
-
* - Simple terms: `search term`
|
|
96
|
-
* - Phrases: `"exact phrase"`
|
|
97
|
-
* - Boolean operators: `term1 AND term2`, `term1 OR term2`, `NOT term`
|
|
98
|
-
* - Prefix matching: `term*`
|
|
99
|
-
* - Grouping: `(term1 OR term2) AND term3`
|
|
100
|
-
*
|
|
101
|
-
* @example
|
|
102
|
-
* ```typescript
|
|
103
|
-
* // Simple search
|
|
104
|
-
* const query1: SearchQuery = { query: "typescript" };
|
|
105
|
-
*
|
|
106
|
-
* // Phrase search with pagination
|
|
107
|
-
* const query2: SearchQuery = {
|
|
108
|
-
* query: '"error handling"',
|
|
109
|
-
* limit: 10,
|
|
110
|
-
* offset: 20,
|
|
111
|
-
* };
|
|
112
|
-
* ```
|
|
113
|
-
*/
|
|
114
|
-
interface SearchQuery {
|
|
115
|
-
/** FTS5 query string */
|
|
116
|
-
query: string;
|
|
117
|
-
/**
|
|
118
|
-
* Maximum number of results to return.
|
|
119
|
-
* @defaultValue 25
|
|
120
|
-
*/
|
|
121
|
-
limit?: number;
|
|
122
|
-
/**
|
|
123
|
-
* Number of results to skip (for pagination).
|
|
124
|
-
* @defaultValue 0
|
|
125
|
-
*/
|
|
126
|
-
offset?: number;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* A single search result from an FTS5 query.
|
|
130
|
-
*
|
|
131
|
-
* Results include the document ID, BM25 relevance score, content,
|
|
132
|
-
* and any associated metadata. Optional highlights show matching
|
|
133
|
-
* snippets from the content.
|
|
134
|
-
*
|
|
135
|
-
* @typeParam T - Type of the metadata (defaults to `unknown`)
|
|
136
|
-
*
|
|
137
|
-
* @example
|
|
138
|
-
* ```typescript
|
|
139
|
-
* interface NoteMetadata {
|
|
140
|
-
* title: string;
|
|
141
|
-
* tags: string[];
|
|
142
|
-
* }
|
|
143
|
-
*
|
|
144
|
-
* const result: SearchResult<NoteMetadata> = {
|
|
145
|
-
* id: "note-123",
|
|
146
|
-
* score: 0.85,
|
|
147
|
-
* content: "Full document content...",
|
|
148
|
-
* metadata: { title: "My Note", tags: ["typescript"] },
|
|
149
|
-
* highlights: ["...matching <b>snippet</b>..."],
|
|
150
|
-
* };
|
|
151
|
-
* ```
|
|
152
|
-
*/
|
|
153
|
-
interface SearchResult<T = unknown> {
|
|
154
|
-
/** Document ID */
|
|
155
|
-
id: string;
|
|
156
|
-
/**
|
|
157
|
-
* BM25 relevance ranking score.
|
|
158
|
-
* Higher scores indicate better matches.
|
|
159
|
-
* Note: FTS5 BM25 returns negative values (closer to 0 = better match).
|
|
160
|
-
*/
|
|
161
|
-
score: number;
|
|
162
|
-
/** Full document content */
|
|
163
|
-
content: string;
|
|
164
|
-
/** Document metadata (if present) */
|
|
165
|
-
metadata?: T;
|
|
166
|
-
/**
|
|
167
|
-
* Matching snippets from the content.
|
|
168
|
-
* Uses FTS5 snippet() function for context-aware highlights.
|
|
169
|
-
*/
|
|
170
|
-
highlights?: string[];
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* The FTS5 index interface for full-text search operations.
|
|
174
|
-
*
|
|
175
|
-
* Provides methods for adding, searching, and removing documents
|
|
176
|
-
* from an SQLite FTS5 index. All operations return `Result` types
|
|
177
|
-
* for explicit error handling.
|
|
178
|
-
*
|
|
179
|
-
* @typeParam T - Type of document metadata (defaults to `unknown`)
|
|
180
|
-
*
|
|
181
|
-
* @example
|
|
182
|
-
* ```typescript
|
|
183
|
-
* const index = createIndex<NoteMetadata>({ path: "./index.db" });
|
|
184
|
-
*
|
|
185
|
-
* // Add documents
|
|
186
|
-
* await index.add({ id: "1", content: "Hello world", metadata: { title: "Greeting" } });
|
|
187
|
-
*
|
|
188
|
-
* // Search
|
|
189
|
-
* const results = await index.search({ query: "hello" });
|
|
190
|
-
* if (results.isOk()) {
|
|
191
|
-
* for (const result of results.value) {
|
|
192
|
-
* console.log(result.id, result.score);
|
|
193
|
-
* }
|
|
194
|
-
* }
|
|
195
|
-
*
|
|
196
|
-
* // Cleanup
|
|
197
|
-
* index.close();
|
|
198
|
-
* ```
|
|
199
|
-
*/
|
|
200
|
-
interface Index<T = unknown> {
|
|
201
|
-
/**
|
|
202
|
-
* Add a single document to the index.
|
|
203
|
-
* If a document with the same ID exists, it will be replaced.
|
|
204
|
-
*
|
|
205
|
-
* @param doc - Document to add
|
|
206
|
-
* @returns Result indicating success or StorageError
|
|
207
|
-
*/
|
|
208
|
-
add(doc: IndexDocument): Promise<Result2<void, StorageError2>>;
|
|
209
|
-
/**
|
|
210
|
-
* Add multiple documents to the index in a single transaction.
|
|
211
|
-
* More efficient than calling add() multiple times.
|
|
212
|
-
* If a document with the same ID exists, it will be replaced.
|
|
213
|
-
*
|
|
214
|
-
* @param docs - Array of documents to add
|
|
215
|
-
* @returns Result indicating success or StorageError
|
|
216
|
-
*/
|
|
217
|
-
addMany(docs: IndexDocument[]): Promise<Result2<void, StorageError2>>;
|
|
218
|
-
/**
|
|
219
|
-
* Search the index using FTS5 query syntax.
|
|
220
|
-
* Returns results ranked by BM25 relevance score.
|
|
221
|
-
*
|
|
222
|
-
* @param query - Search query parameters
|
|
223
|
-
* @returns Result containing array of search results or StorageError
|
|
224
|
-
*/
|
|
225
|
-
search(query: SearchQuery): Promise<Result2<SearchResult<T>[], StorageError2>>;
|
|
226
|
-
/**
|
|
227
|
-
* Remove a document from the index by ID.
|
|
228
|
-
* No error is returned if the document does not exist.
|
|
229
|
-
*
|
|
230
|
-
* @param id - Document ID to remove
|
|
231
|
-
* @returns Result indicating success or StorageError
|
|
232
|
-
*/
|
|
233
|
-
remove(id: string): Promise<Result2<void, StorageError2>>;
|
|
234
|
-
/**
|
|
235
|
-
* Remove all documents from the index.
|
|
236
|
-
*
|
|
237
|
-
* @returns Result indicating success or StorageError
|
|
238
|
-
*/
|
|
239
|
-
clear(): Promise<Result2<void, StorageError2>>;
|
|
240
|
-
/**
|
|
241
|
-
* Close the index and release resources.
|
|
242
|
-
* The index should not be used after calling close().
|
|
243
|
-
*/
|
|
244
|
-
close(): void;
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Creates an FTS5 full-text search index.
|
|
248
|
-
*
|
|
249
|
-
* Uses SQLite FTS5 virtual table for fast full-text search with BM25 ranking.
|
|
250
|
-
* The database is configured with WAL mode for better concurrency.
|
|
251
|
-
*
|
|
252
|
-
* @typeParam T - Type of document metadata (defaults to `unknown`)
|
|
253
|
-
* @param options - Index options including database path and table configuration
|
|
254
|
-
* @returns An Index instance for managing documents and searching
|
|
255
|
-
*
|
|
256
|
-
* @example
|
|
257
|
-
* ```typescript
|
|
258
|
-
* // Create an index with default settings
|
|
259
|
-
* const index = createIndex({ path: "./data/index.db" });
|
|
260
|
-
*
|
|
261
|
-
* // Add documents
|
|
262
|
-
* await index.add({
|
|
263
|
-
* id: "doc-1",
|
|
264
|
-
* content: "Hello world",
|
|
265
|
-
* metadata: { title: "Greeting" },
|
|
266
|
-
* });
|
|
267
|
-
*
|
|
268
|
-
* // Search
|
|
269
|
-
* const results = await index.search({ query: "hello" });
|
|
270
|
-
*
|
|
271
|
-
* // Cleanup
|
|
272
|
-
* index.close();
|
|
273
|
-
* ```
|
|
274
|
-
*
|
|
275
|
-
* @example
|
|
276
|
-
* ```typescript
|
|
277
|
-
* // Create an index with Porter stemmer for English text
|
|
278
|
-
* const index = createIndex({
|
|
279
|
-
* path: "./data/notes.db",
|
|
280
|
-
* tableName: "notes_fts",
|
|
281
|
-
* tokenizer: "porter",
|
|
282
|
-
* });
|
|
283
|
-
* ```
|
|
284
|
-
*/
|
|
285
|
-
declare function createIndex<T = unknown>(options: IndexOptions): Index<T>;
|
|
1
|
+
import { createIndex } from "./shared/@outfitter/index-ewvzd5f9";
|
|
2
|
+
import "./shared/@outfitter/index-m0h11hv5";
|
|
3
|
+
import "./shared/@outfitter/index-mmgx1j0h";
|
|
286
4
|
export { createIndex };
|
package/dist/fts5.js
CHANGED
|
@@ -1,252 +1,8 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from "./version.js";
|
|
7
|
-
|
|
8
|
-
// packages/index/src/fts5.ts
|
|
9
|
-
import { Database } from "bun:sqlite";
|
|
10
|
-
import { existsSync, mkdirSync } from "fs";
|
|
11
|
-
import { dirname } from "path";
|
|
12
|
-
import { Result } from "@outfitter/contracts";
|
|
13
|
-
var DEFAULT_TABLE_NAME = "documents";
|
|
14
|
-
var DEFAULT_TOKENIZER = "unicode61";
|
|
15
|
-
var DEFAULT_LIMIT = 25;
|
|
16
|
-
var DEFAULT_OFFSET = 0;
|
|
17
|
-
var VALID_TOKENIZERS = {
|
|
18
|
-
unicode61: true,
|
|
19
|
-
porter: true,
|
|
20
|
-
trigram: true
|
|
21
|
-
};
|
|
22
|
-
var TABLE_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
23
|
-
var DEFAULT_TOOL = "outfitter-index";
|
|
24
|
-
var DEFAULT_TOOL_VERSION = "0.0.0";
|
|
25
|
-
function createStorageError(message, cause) {
|
|
26
|
-
return {
|
|
27
|
-
_tag: "StorageError",
|
|
28
|
-
message,
|
|
29
|
-
cause
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
function assertValidTableName(tableName) {
|
|
33
|
-
if (!TABLE_NAME_PATTERN.test(tableName)) {
|
|
34
|
-
throw new Error(`Invalid table name: ${tableName}`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
function assertValidTokenizer(tokenizer) {
|
|
38
|
-
if (!Object.hasOwn(VALID_TOKENIZERS, tokenizer)) {
|
|
39
|
-
throw new Error(`Invalid tokenizer: ${tokenizer}`);
|
|
40
|
-
}
|
|
41
|
-
return tokenizer;
|
|
42
|
-
}
|
|
43
|
-
function getUserVersion(db) {
|
|
44
|
-
const row = db.query("PRAGMA user_version").get();
|
|
45
|
-
return row?.user_version ?? 0;
|
|
46
|
-
}
|
|
47
|
-
function setUserVersion(db, version) {
|
|
48
|
-
if (!Number.isInteger(version) || version < 0) {
|
|
49
|
-
throw new Error(`Invalid user_version: ${version}`);
|
|
50
|
-
}
|
|
51
|
-
db.run(`PRAGMA user_version = ${version}`);
|
|
52
|
-
}
|
|
53
|
-
function ensureMetaTable(db) {
|
|
54
|
-
db.run(`CREATE TABLE IF NOT EXISTS ${INDEX_META_TABLE} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`);
|
|
55
|
-
}
|
|
56
|
-
function readIndexMetadata(db) {
|
|
57
|
-
try {
|
|
58
|
-
const row = db.query(`SELECT value FROM ${INDEX_META_TABLE} WHERE key = ?`).get(INDEX_META_KEY);
|
|
59
|
-
if (!row) {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
const parsed = JSON.parse(row.value);
|
|
63
|
-
return parsed;
|
|
64
|
-
} catch {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
function writeIndexMetadata(db, metadata) {
|
|
69
|
-
ensureMetaTable(db);
|
|
70
|
-
db.run(`INSERT OR REPLACE INTO ${INDEX_META_TABLE} (key, value) VALUES (?, ?)`, [INDEX_META_KEY, JSON.stringify(metadata)]);
|
|
71
|
-
}
|
|
72
|
-
function createIndex(options) {
|
|
73
|
-
const tableName = options.tableName ?? DEFAULT_TABLE_NAME;
|
|
74
|
-
assertValidTableName(tableName);
|
|
75
|
-
const tokenizer = assertValidTokenizer(options.tokenizer ?? DEFAULT_TOKENIZER);
|
|
76
|
-
const tool = options.tool ?? DEFAULT_TOOL;
|
|
77
|
-
const toolVersion = options.toolVersion ?? DEFAULT_TOOL_VERSION;
|
|
78
|
-
const dir = dirname(options.path);
|
|
79
|
-
if (!existsSync(dir)) {
|
|
80
|
-
mkdirSync(dir, { recursive: true });
|
|
81
|
-
}
|
|
82
|
-
const db = new Database(options.path);
|
|
83
|
-
db.run("PRAGMA journal_mode=WAL");
|
|
84
|
-
const currentVersion = getUserVersion(db);
|
|
85
|
-
if (currentVersion === 0) {
|
|
86
|
-
const metadata = {
|
|
87
|
-
version: INDEX_VERSION,
|
|
88
|
-
created: new Date().toISOString(),
|
|
89
|
-
tool,
|
|
90
|
-
toolVersion
|
|
91
|
-
};
|
|
92
|
-
setUserVersion(db, INDEX_VERSION);
|
|
93
|
-
writeIndexMetadata(db, metadata);
|
|
94
|
-
} else if (currentVersion !== INDEX_VERSION) {
|
|
95
|
-
if (!options.migrations) {
|
|
96
|
-
throw new Error(`Index version ${currentVersion} does not match ${INDEX_VERSION}. Provide migrations or rebuild the index.`);
|
|
97
|
-
}
|
|
98
|
-
const context = { db };
|
|
99
|
-
const result = options.migrations.migrate(context, currentVersion, INDEX_VERSION);
|
|
100
|
-
if (result.isErr()) {
|
|
101
|
-
throw new Error(`Failed to migrate index: ${result.error.message}`);
|
|
102
|
-
}
|
|
103
|
-
const existing = readIndexMetadata(db);
|
|
104
|
-
const metadata = {
|
|
105
|
-
version: INDEX_VERSION,
|
|
106
|
-
created: existing?.created ?? new Date().toISOString(),
|
|
107
|
-
tool,
|
|
108
|
-
toolVersion
|
|
109
|
-
};
|
|
110
|
-
setUserVersion(db, INDEX_VERSION);
|
|
111
|
-
writeIndexMetadata(db, metadata);
|
|
112
|
-
} else if (!readIndexMetadata(db)) {
|
|
113
|
-
const metadata = {
|
|
114
|
-
version: INDEX_VERSION,
|
|
115
|
-
created: new Date().toISOString(),
|
|
116
|
-
tool,
|
|
117
|
-
toolVersion
|
|
118
|
-
};
|
|
119
|
-
writeIndexMetadata(db, metadata);
|
|
120
|
-
}
|
|
121
|
-
db.run(`
|
|
122
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS ${tableName}
|
|
123
|
-
USING fts5(
|
|
124
|
-
id UNINDEXED,
|
|
125
|
-
content,
|
|
126
|
-
metadata UNINDEXED,
|
|
127
|
-
tokenize='${tokenizer}'
|
|
128
|
-
)
|
|
129
|
-
`);
|
|
130
|
-
let isClosed = false;
|
|
131
|
-
function checkClosed() {
|
|
132
|
-
if (isClosed) {
|
|
133
|
-
return Result.err(createStorageError("Index is closed"));
|
|
134
|
-
}
|
|
135
|
-
return Result.ok(undefined);
|
|
136
|
-
}
|
|
137
|
-
return {
|
|
138
|
-
async add(doc) {
|
|
139
|
-
const closedCheck = checkClosed();
|
|
140
|
-
if (closedCheck.isErr()) {
|
|
141
|
-
return closedCheck;
|
|
142
|
-
}
|
|
143
|
-
try {
|
|
144
|
-
const metadataJson = doc.metadata ? JSON.stringify(doc.metadata) : null;
|
|
145
|
-
db.run(`DELETE FROM ${tableName} WHERE id = ?`, [doc.id]);
|
|
146
|
-
db.run(`INSERT INTO ${tableName} (id, content, metadata) VALUES (?, ?, ?)`, [doc.id, doc.content, metadataJson]);
|
|
147
|
-
return Result.ok(undefined);
|
|
148
|
-
} catch (error) {
|
|
149
|
-
return Result.err(createStorageError(error instanceof Error ? error.message : "Failed to add document", error));
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
async addMany(docs) {
|
|
153
|
-
const closedCheck = checkClosed();
|
|
154
|
-
if (closedCheck.isErr()) {
|
|
155
|
-
return closedCheck;
|
|
156
|
-
}
|
|
157
|
-
try {
|
|
158
|
-
db.run("BEGIN TRANSACTION");
|
|
159
|
-
try {
|
|
160
|
-
const deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE id = ?`);
|
|
161
|
-
const insertStmt = db.prepare(`INSERT INTO ${tableName} (id, content, metadata) VALUES (?, ?, ?)`);
|
|
162
|
-
for (const doc of docs) {
|
|
163
|
-
const metadataJson = doc.metadata ? JSON.stringify(doc.metadata) : null;
|
|
164
|
-
deleteStmt.run(doc.id);
|
|
165
|
-
insertStmt.run(doc.id, doc.content, metadataJson);
|
|
166
|
-
}
|
|
167
|
-
deleteStmt.finalize();
|
|
168
|
-
insertStmt.finalize();
|
|
169
|
-
db.run("COMMIT");
|
|
170
|
-
return Result.ok(undefined);
|
|
171
|
-
} catch (error) {
|
|
172
|
-
db.run("ROLLBACK");
|
|
173
|
-
throw error;
|
|
174
|
-
}
|
|
175
|
-
} catch (error) {
|
|
176
|
-
return Result.err(createStorageError(error instanceof Error ? error.message : "Failed to add documents", error));
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
async search(query) {
|
|
180
|
-
const closedCheck = checkClosed();
|
|
181
|
-
if (closedCheck.isErr()) {
|
|
182
|
-
return closedCheck;
|
|
183
|
-
}
|
|
184
|
-
const limit = query.limit ?? DEFAULT_LIMIT;
|
|
185
|
-
const offset = query.offset ?? DEFAULT_OFFSET;
|
|
186
|
-
try {
|
|
187
|
-
const rows = db.query(`
|
|
188
|
-
SELECT
|
|
189
|
-
id,
|
|
190
|
-
content,
|
|
191
|
-
metadata,
|
|
192
|
-
bm25(${tableName}) as score,
|
|
193
|
-
snippet(${tableName}, 1, '<b>', '</b>', '...', 32) as highlight
|
|
194
|
-
FROM ${tableName}
|
|
195
|
-
WHERE ${tableName} MATCH ?
|
|
196
|
-
ORDER BY bm25(${tableName}) ASC
|
|
197
|
-
LIMIT ? OFFSET ?
|
|
198
|
-
`).all(query.query, limit, offset);
|
|
199
|
-
const results = rows.map((row) => {
|
|
200
|
-
const result = {
|
|
201
|
-
id: row.id,
|
|
202
|
-
content: row.content,
|
|
203
|
-
score: row.score,
|
|
204
|
-
highlights: [row.highlight]
|
|
205
|
-
};
|
|
206
|
-
if (row.metadata) {
|
|
207
|
-
try {
|
|
208
|
-
result.metadata = JSON.parse(row.metadata);
|
|
209
|
-
} catch {}
|
|
210
|
-
}
|
|
211
|
-
return result;
|
|
212
|
-
});
|
|
213
|
-
return Result.ok(results);
|
|
214
|
-
} catch (error) {
|
|
215
|
-
return Result.err(createStorageError(error instanceof Error ? error.message : "Search failed", error));
|
|
216
|
-
}
|
|
217
|
-
},
|
|
218
|
-
async remove(id) {
|
|
219
|
-
const closedCheck = checkClosed();
|
|
220
|
-
if (closedCheck.isErr()) {
|
|
221
|
-
return closedCheck;
|
|
222
|
-
}
|
|
223
|
-
try {
|
|
224
|
-
db.run(`DELETE FROM ${tableName} WHERE id = ?`, [id]);
|
|
225
|
-
return Result.ok(undefined);
|
|
226
|
-
} catch (error) {
|
|
227
|
-
return Result.err(createStorageError(error instanceof Error ? error.message : "Failed to remove document", error));
|
|
228
|
-
}
|
|
229
|
-
},
|
|
230
|
-
async clear() {
|
|
231
|
-
const closedCheck = checkClosed();
|
|
232
|
-
if (closedCheck.isErr()) {
|
|
233
|
-
return closedCheck;
|
|
234
|
-
}
|
|
235
|
-
try {
|
|
236
|
-
db.run(`DELETE FROM ${tableName}`);
|
|
237
|
-
return Result.ok(undefined);
|
|
238
|
-
} catch (error) {
|
|
239
|
-
return Result.err(createStorageError(error instanceof Error ? error.message : "Failed to clear index", error));
|
|
240
|
-
}
|
|
241
|
-
},
|
|
242
|
-
close() {
|
|
243
|
-
if (!isClosed) {
|
|
244
|
-
isClosed = true;
|
|
245
|
-
db.close();
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
}
|
|
3
|
+
createIndex
|
|
4
|
+
} from "./shared/@outfitter/index-gf30ny51.js";
|
|
5
|
+
import"./shared/@outfitter/index-bbzmc40h.js";
|
|
250
6
|
export {
|
|
251
7
|
createIndex
|
|
252
8
|
};
|