@jiangzhx/adcli 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.
- package/README.md +100 -0
- package/dist/cli.js +1383 -0
- package/dist/jiangzhx-adcli-0.1.0.tgz +0 -0
- package/docs/commands.md +209 -0
- package/package.json +54 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1383 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
import { readFile } from "fs/promises";
|
|
6
|
+
import path from "path";
|
|
7
|
+
|
|
8
|
+
// node_modules/minisearch/dist/es/index.js
|
|
9
|
+
var ENTRIES = "ENTRIES";
|
|
10
|
+
var KEYS = "KEYS";
|
|
11
|
+
var VALUES = "VALUES";
|
|
12
|
+
var LEAF = "";
|
|
13
|
+
|
|
14
|
+
class TreeIterator {
|
|
15
|
+
constructor(set, type) {
|
|
16
|
+
const node = set._tree;
|
|
17
|
+
const keys = Array.from(node.keys());
|
|
18
|
+
this.set = set;
|
|
19
|
+
this._type = type;
|
|
20
|
+
this._path = keys.length > 0 ? [{ node, keys }] : [];
|
|
21
|
+
}
|
|
22
|
+
next() {
|
|
23
|
+
const value = this.dive();
|
|
24
|
+
this.backtrack();
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
dive() {
|
|
28
|
+
if (this._path.length === 0) {
|
|
29
|
+
return { done: true, value: undefined };
|
|
30
|
+
}
|
|
31
|
+
const { node, keys } = last$1(this._path);
|
|
32
|
+
if (last$1(keys) === LEAF) {
|
|
33
|
+
return { done: false, value: this.result() };
|
|
34
|
+
}
|
|
35
|
+
const child = node.get(last$1(keys));
|
|
36
|
+
this._path.push({ node: child, keys: Array.from(child.keys()) });
|
|
37
|
+
return this.dive();
|
|
38
|
+
}
|
|
39
|
+
backtrack() {
|
|
40
|
+
if (this._path.length === 0) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const keys = last$1(this._path).keys;
|
|
44
|
+
keys.pop();
|
|
45
|
+
if (keys.length > 0) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this._path.pop();
|
|
49
|
+
this.backtrack();
|
|
50
|
+
}
|
|
51
|
+
key() {
|
|
52
|
+
return this.set._prefix + this._path.map(({ keys }) => last$1(keys)).filter((key) => key !== LEAF).join("");
|
|
53
|
+
}
|
|
54
|
+
value() {
|
|
55
|
+
return last$1(this._path).node.get(LEAF);
|
|
56
|
+
}
|
|
57
|
+
result() {
|
|
58
|
+
switch (this._type) {
|
|
59
|
+
case VALUES:
|
|
60
|
+
return this.value();
|
|
61
|
+
case KEYS:
|
|
62
|
+
return this.key();
|
|
63
|
+
default:
|
|
64
|
+
return [this.key(), this.value()];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
[Symbol.iterator]() {
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
var last$1 = (array) => {
|
|
72
|
+
return array[array.length - 1];
|
|
73
|
+
};
|
|
74
|
+
var fuzzySearch = (node, query, maxDistance) => {
|
|
75
|
+
const results = new Map;
|
|
76
|
+
if (query === undefined)
|
|
77
|
+
return results;
|
|
78
|
+
const n = query.length + 1;
|
|
79
|
+
const m = n + maxDistance;
|
|
80
|
+
const matrix = new Uint8Array(m * n).fill(maxDistance + 1);
|
|
81
|
+
for (let j = 0;j < n; ++j)
|
|
82
|
+
matrix[j] = j;
|
|
83
|
+
for (let i = 1;i < m; ++i)
|
|
84
|
+
matrix[i * n] = i;
|
|
85
|
+
recurse(node, query, maxDistance, results, matrix, 1, n, "");
|
|
86
|
+
return results;
|
|
87
|
+
};
|
|
88
|
+
var recurse = (node, query, maxDistance, results, matrix, m, n, prefix) => {
|
|
89
|
+
const offset = m * n;
|
|
90
|
+
key:
|
|
91
|
+
for (const key of node.keys()) {
|
|
92
|
+
if (key === LEAF) {
|
|
93
|
+
const distance = matrix[offset - 1];
|
|
94
|
+
if (distance <= maxDistance) {
|
|
95
|
+
results.set(prefix, [node.get(key), distance]);
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
let i = m;
|
|
99
|
+
for (let pos = 0;pos < key.length; ++pos, ++i) {
|
|
100
|
+
const char = key[pos];
|
|
101
|
+
const thisRowOffset = n * i;
|
|
102
|
+
const prevRowOffset = thisRowOffset - n;
|
|
103
|
+
let minDistance = matrix[thisRowOffset];
|
|
104
|
+
const jmin = Math.max(0, i - maxDistance - 1);
|
|
105
|
+
const jmax = Math.min(n - 1, i + maxDistance);
|
|
106
|
+
for (let j = jmin;j < jmax; ++j) {
|
|
107
|
+
const different = char !== query[j];
|
|
108
|
+
const rpl = matrix[prevRowOffset + j] + +different;
|
|
109
|
+
const del = matrix[prevRowOffset + j + 1] + 1;
|
|
110
|
+
const ins = matrix[thisRowOffset + j] + 1;
|
|
111
|
+
const dist = matrix[thisRowOffset + j + 1] = Math.min(rpl, del, ins);
|
|
112
|
+
if (dist < minDistance)
|
|
113
|
+
minDistance = dist;
|
|
114
|
+
}
|
|
115
|
+
if (minDistance > maxDistance) {
|
|
116
|
+
continue key;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
recurse(node.get(key), query, maxDistance, results, matrix, i, n, prefix + key);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
class SearchableMap {
|
|
125
|
+
constructor(tree = new Map, prefix = "") {
|
|
126
|
+
this._size = undefined;
|
|
127
|
+
this._tree = tree;
|
|
128
|
+
this._prefix = prefix;
|
|
129
|
+
}
|
|
130
|
+
atPrefix(prefix) {
|
|
131
|
+
if (!prefix.startsWith(this._prefix)) {
|
|
132
|
+
throw new Error("Mismatched prefix");
|
|
133
|
+
}
|
|
134
|
+
const [node, path] = trackDown(this._tree, prefix.slice(this._prefix.length));
|
|
135
|
+
if (node === undefined) {
|
|
136
|
+
const [parentNode, key] = last(path);
|
|
137
|
+
for (const k of parentNode.keys()) {
|
|
138
|
+
if (k !== LEAF && k.startsWith(key)) {
|
|
139
|
+
const node2 = new Map;
|
|
140
|
+
node2.set(k.slice(key.length), parentNode.get(k));
|
|
141
|
+
return new SearchableMap(node2, prefix);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return new SearchableMap(node, prefix);
|
|
146
|
+
}
|
|
147
|
+
clear() {
|
|
148
|
+
this._size = undefined;
|
|
149
|
+
this._tree.clear();
|
|
150
|
+
}
|
|
151
|
+
delete(key) {
|
|
152
|
+
this._size = undefined;
|
|
153
|
+
return remove(this._tree, key);
|
|
154
|
+
}
|
|
155
|
+
entries() {
|
|
156
|
+
return new TreeIterator(this, ENTRIES);
|
|
157
|
+
}
|
|
158
|
+
forEach(fn) {
|
|
159
|
+
for (const [key, value] of this) {
|
|
160
|
+
fn(key, value, this);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
fuzzyGet(key, maxEditDistance) {
|
|
164
|
+
return fuzzySearch(this._tree, key, maxEditDistance);
|
|
165
|
+
}
|
|
166
|
+
get(key) {
|
|
167
|
+
const node = lookup(this._tree, key);
|
|
168
|
+
return node !== undefined ? node.get(LEAF) : undefined;
|
|
169
|
+
}
|
|
170
|
+
has(key) {
|
|
171
|
+
const node = lookup(this._tree, key);
|
|
172
|
+
return node !== undefined && node.has(LEAF);
|
|
173
|
+
}
|
|
174
|
+
keys() {
|
|
175
|
+
return new TreeIterator(this, KEYS);
|
|
176
|
+
}
|
|
177
|
+
set(key, value) {
|
|
178
|
+
if (typeof key !== "string") {
|
|
179
|
+
throw new Error("key must be a string");
|
|
180
|
+
}
|
|
181
|
+
this._size = undefined;
|
|
182
|
+
const node = createPath(this._tree, key);
|
|
183
|
+
node.set(LEAF, value);
|
|
184
|
+
return this;
|
|
185
|
+
}
|
|
186
|
+
get size() {
|
|
187
|
+
if (this._size) {
|
|
188
|
+
return this._size;
|
|
189
|
+
}
|
|
190
|
+
this._size = 0;
|
|
191
|
+
const iter = this.entries();
|
|
192
|
+
while (!iter.next().done)
|
|
193
|
+
this._size += 1;
|
|
194
|
+
return this._size;
|
|
195
|
+
}
|
|
196
|
+
update(key, fn) {
|
|
197
|
+
if (typeof key !== "string") {
|
|
198
|
+
throw new Error("key must be a string");
|
|
199
|
+
}
|
|
200
|
+
this._size = undefined;
|
|
201
|
+
const node = createPath(this._tree, key);
|
|
202
|
+
node.set(LEAF, fn(node.get(LEAF)));
|
|
203
|
+
return this;
|
|
204
|
+
}
|
|
205
|
+
fetch(key, initial) {
|
|
206
|
+
if (typeof key !== "string") {
|
|
207
|
+
throw new Error("key must be a string");
|
|
208
|
+
}
|
|
209
|
+
this._size = undefined;
|
|
210
|
+
const node = createPath(this._tree, key);
|
|
211
|
+
let value = node.get(LEAF);
|
|
212
|
+
if (value === undefined) {
|
|
213
|
+
node.set(LEAF, value = initial());
|
|
214
|
+
}
|
|
215
|
+
return value;
|
|
216
|
+
}
|
|
217
|
+
values() {
|
|
218
|
+
return new TreeIterator(this, VALUES);
|
|
219
|
+
}
|
|
220
|
+
[Symbol.iterator]() {
|
|
221
|
+
return this.entries();
|
|
222
|
+
}
|
|
223
|
+
static from(entries) {
|
|
224
|
+
const tree = new SearchableMap;
|
|
225
|
+
for (const [key, value] of entries) {
|
|
226
|
+
tree.set(key, value);
|
|
227
|
+
}
|
|
228
|
+
return tree;
|
|
229
|
+
}
|
|
230
|
+
static fromObject(object) {
|
|
231
|
+
return SearchableMap.from(Object.entries(object));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
var trackDown = (tree, key, path = []) => {
|
|
235
|
+
if (key.length === 0 || tree == null) {
|
|
236
|
+
return [tree, path];
|
|
237
|
+
}
|
|
238
|
+
for (const k of tree.keys()) {
|
|
239
|
+
if (k !== LEAF && key.startsWith(k)) {
|
|
240
|
+
path.push([tree, k]);
|
|
241
|
+
return trackDown(tree.get(k), key.slice(k.length), path);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
path.push([tree, key]);
|
|
245
|
+
return trackDown(undefined, "", path);
|
|
246
|
+
};
|
|
247
|
+
var lookup = (tree, key) => {
|
|
248
|
+
if (key.length === 0 || tree == null) {
|
|
249
|
+
return tree;
|
|
250
|
+
}
|
|
251
|
+
for (const k of tree.keys()) {
|
|
252
|
+
if (k !== LEAF && key.startsWith(k)) {
|
|
253
|
+
return lookup(tree.get(k), key.slice(k.length));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
var createPath = (node, key) => {
|
|
258
|
+
const keyLength = key.length;
|
|
259
|
+
outer:
|
|
260
|
+
for (let pos = 0;node && pos < keyLength; ) {
|
|
261
|
+
for (const k of node.keys()) {
|
|
262
|
+
if (k !== LEAF && key[pos] === k[0]) {
|
|
263
|
+
const len = Math.min(keyLength - pos, k.length);
|
|
264
|
+
let offset = 1;
|
|
265
|
+
while (offset < len && key[pos + offset] === k[offset])
|
|
266
|
+
++offset;
|
|
267
|
+
const child2 = node.get(k);
|
|
268
|
+
if (offset === k.length) {
|
|
269
|
+
node = child2;
|
|
270
|
+
} else {
|
|
271
|
+
const intermediate = new Map;
|
|
272
|
+
intermediate.set(k.slice(offset), child2);
|
|
273
|
+
node.set(key.slice(pos, pos + offset), intermediate);
|
|
274
|
+
node.delete(k);
|
|
275
|
+
node = intermediate;
|
|
276
|
+
}
|
|
277
|
+
pos += offset;
|
|
278
|
+
continue outer;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const child = new Map;
|
|
282
|
+
node.set(key.slice(pos), child);
|
|
283
|
+
return child;
|
|
284
|
+
}
|
|
285
|
+
return node;
|
|
286
|
+
};
|
|
287
|
+
var remove = (tree, key) => {
|
|
288
|
+
const [node, path] = trackDown(tree, key);
|
|
289
|
+
if (node === undefined) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
node.delete(LEAF);
|
|
293
|
+
if (node.size === 0) {
|
|
294
|
+
cleanup(path);
|
|
295
|
+
} else if (node.size === 1) {
|
|
296
|
+
const [key2, value] = node.entries().next().value;
|
|
297
|
+
merge(path, key2, value);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
var cleanup = (path) => {
|
|
301
|
+
if (path.length === 0) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const [node, key] = last(path);
|
|
305
|
+
node.delete(key);
|
|
306
|
+
if (node.size === 0) {
|
|
307
|
+
cleanup(path.slice(0, -1));
|
|
308
|
+
} else if (node.size === 1) {
|
|
309
|
+
const [key2, value] = node.entries().next().value;
|
|
310
|
+
if (key2 !== LEAF) {
|
|
311
|
+
merge(path.slice(0, -1), key2, value);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
var merge = (path, key, value) => {
|
|
316
|
+
if (path.length === 0) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const [node, nodeKey] = last(path);
|
|
320
|
+
node.set(nodeKey + key, value);
|
|
321
|
+
node.delete(nodeKey);
|
|
322
|
+
};
|
|
323
|
+
var last = (array) => {
|
|
324
|
+
return array[array.length - 1];
|
|
325
|
+
};
|
|
326
|
+
var OR = "or";
|
|
327
|
+
var AND = "and";
|
|
328
|
+
var AND_NOT = "and_not";
|
|
329
|
+
|
|
330
|
+
class MiniSearch {
|
|
331
|
+
constructor(options) {
|
|
332
|
+
if ((options === null || options === undefined ? undefined : options.fields) == null) {
|
|
333
|
+
throw new Error('MiniSearch: option "fields" must be provided');
|
|
334
|
+
}
|
|
335
|
+
const autoVacuum = options.autoVacuum == null || options.autoVacuum === true ? defaultAutoVacuumOptions : options.autoVacuum;
|
|
336
|
+
this._options = {
|
|
337
|
+
...defaultOptions,
|
|
338
|
+
...options,
|
|
339
|
+
autoVacuum,
|
|
340
|
+
searchOptions: { ...defaultSearchOptions, ...options.searchOptions || {} },
|
|
341
|
+
autoSuggestOptions: { ...defaultAutoSuggestOptions, ...options.autoSuggestOptions || {} }
|
|
342
|
+
};
|
|
343
|
+
this._index = new SearchableMap;
|
|
344
|
+
this._documentCount = 0;
|
|
345
|
+
this._documentIds = new Map;
|
|
346
|
+
this._idToShortId = new Map;
|
|
347
|
+
this._fieldIds = {};
|
|
348
|
+
this._fieldLength = new Map;
|
|
349
|
+
this._avgFieldLength = [];
|
|
350
|
+
this._nextId = 0;
|
|
351
|
+
this._storedFields = new Map;
|
|
352
|
+
this._dirtCount = 0;
|
|
353
|
+
this._currentVacuum = null;
|
|
354
|
+
this._enqueuedVacuum = null;
|
|
355
|
+
this._enqueuedVacuumConditions = defaultVacuumConditions;
|
|
356
|
+
this.addFields(this._options.fields);
|
|
357
|
+
}
|
|
358
|
+
add(document) {
|
|
359
|
+
const { extractField, stringifyField, tokenize, processTerm, fields, idField } = this._options;
|
|
360
|
+
const id = extractField(document, idField);
|
|
361
|
+
if (id == null) {
|
|
362
|
+
throw new Error(`MiniSearch: document does not have ID field "${idField}"`);
|
|
363
|
+
}
|
|
364
|
+
if (this._idToShortId.has(id)) {
|
|
365
|
+
throw new Error(`MiniSearch: duplicate ID ${id}`);
|
|
366
|
+
}
|
|
367
|
+
const shortDocumentId = this.addDocumentId(id);
|
|
368
|
+
this.saveStoredFields(shortDocumentId, document);
|
|
369
|
+
for (const field of fields) {
|
|
370
|
+
const fieldValue = extractField(document, field);
|
|
371
|
+
if (fieldValue == null)
|
|
372
|
+
continue;
|
|
373
|
+
const tokens = tokenize(stringifyField(fieldValue, field), field);
|
|
374
|
+
const fieldId = this._fieldIds[field];
|
|
375
|
+
const uniqueTerms = new Set(tokens).size;
|
|
376
|
+
this.addFieldLength(shortDocumentId, fieldId, this._documentCount - 1, uniqueTerms);
|
|
377
|
+
for (const term of tokens) {
|
|
378
|
+
const processedTerm = processTerm(term, field);
|
|
379
|
+
if (Array.isArray(processedTerm)) {
|
|
380
|
+
for (const t of processedTerm) {
|
|
381
|
+
this.addTerm(fieldId, shortDocumentId, t);
|
|
382
|
+
}
|
|
383
|
+
} else if (processedTerm) {
|
|
384
|
+
this.addTerm(fieldId, shortDocumentId, processedTerm);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
addAll(documents) {
|
|
390
|
+
for (const document of documents)
|
|
391
|
+
this.add(document);
|
|
392
|
+
}
|
|
393
|
+
addAllAsync(documents, options = {}) {
|
|
394
|
+
const { chunkSize = 10 } = options;
|
|
395
|
+
const acc = { chunk: [], promise: Promise.resolve() };
|
|
396
|
+
const { chunk, promise } = documents.reduce(({ chunk: chunk2, promise: promise2 }, document, i) => {
|
|
397
|
+
chunk2.push(document);
|
|
398
|
+
if ((i + 1) % chunkSize === 0) {
|
|
399
|
+
return {
|
|
400
|
+
chunk: [],
|
|
401
|
+
promise: promise2.then(() => new Promise((resolve) => setTimeout(resolve, 0))).then(() => this.addAll(chunk2))
|
|
402
|
+
};
|
|
403
|
+
} else {
|
|
404
|
+
return { chunk: chunk2, promise: promise2 };
|
|
405
|
+
}
|
|
406
|
+
}, acc);
|
|
407
|
+
return promise.then(() => this.addAll(chunk));
|
|
408
|
+
}
|
|
409
|
+
remove(document) {
|
|
410
|
+
const { tokenize, processTerm, extractField, stringifyField, fields, idField } = this._options;
|
|
411
|
+
const id = extractField(document, idField);
|
|
412
|
+
if (id == null) {
|
|
413
|
+
throw new Error(`MiniSearch: document does not have ID field "${idField}"`);
|
|
414
|
+
}
|
|
415
|
+
const shortId = this._idToShortId.get(id);
|
|
416
|
+
if (shortId == null) {
|
|
417
|
+
throw new Error(`MiniSearch: cannot remove document with ID ${id}: it is not in the index`);
|
|
418
|
+
}
|
|
419
|
+
for (const field of fields) {
|
|
420
|
+
const fieldValue = extractField(document, field);
|
|
421
|
+
if (fieldValue == null)
|
|
422
|
+
continue;
|
|
423
|
+
const tokens = tokenize(stringifyField(fieldValue, field), field);
|
|
424
|
+
const fieldId = this._fieldIds[field];
|
|
425
|
+
const uniqueTerms = new Set(tokens).size;
|
|
426
|
+
this.removeFieldLength(shortId, fieldId, this._documentCount, uniqueTerms);
|
|
427
|
+
for (const term of tokens) {
|
|
428
|
+
const processedTerm = processTerm(term, field);
|
|
429
|
+
if (Array.isArray(processedTerm)) {
|
|
430
|
+
for (const t of processedTerm) {
|
|
431
|
+
this.removeTerm(fieldId, shortId, t);
|
|
432
|
+
}
|
|
433
|
+
} else if (processedTerm) {
|
|
434
|
+
this.removeTerm(fieldId, shortId, processedTerm);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
this._storedFields.delete(shortId);
|
|
439
|
+
this._documentIds.delete(shortId);
|
|
440
|
+
this._idToShortId.delete(id);
|
|
441
|
+
this._fieldLength.delete(shortId);
|
|
442
|
+
this._documentCount -= 1;
|
|
443
|
+
}
|
|
444
|
+
removeAll(documents) {
|
|
445
|
+
if (documents) {
|
|
446
|
+
for (const document of documents)
|
|
447
|
+
this.remove(document);
|
|
448
|
+
} else if (arguments.length > 0) {
|
|
449
|
+
throw new Error("Expected documents to be present. Omit the argument to remove all documents.");
|
|
450
|
+
} else {
|
|
451
|
+
this._index = new SearchableMap;
|
|
452
|
+
this._documentCount = 0;
|
|
453
|
+
this._documentIds = new Map;
|
|
454
|
+
this._idToShortId = new Map;
|
|
455
|
+
this._fieldLength = new Map;
|
|
456
|
+
this._avgFieldLength = [];
|
|
457
|
+
this._storedFields = new Map;
|
|
458
|
+
this._nextId = 0;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
discard(id) {
|
|
462
|
+
const shortId = this._idToShortId.get(id);
|
|
463
|
+
if (shortId == null) {
|
|
464
|
+
throw new Error(`MiniSearch: cannot discard document with ID ${id}: it is not in the index`);
|
|
465
|
+
}
|
|
466
|
+
this._idToShortId.delete(id);
|
|
467
|
+
this._documentIds.delete(shortId);
|
|
468
|
+
this._storedFields.delete(shortId);
|
|
469
|
+
(this._fieldLength.get(shortId) || []).forEach((fieldLength, fieldId) => {
|
|
470
|
+
this.removeFieldLength(shortId, fieldId, this._documentCount, fieldLength);
|
|
471
|
+
});
|
|
472
|
+
this._fieldLength.delete(shortId);
|
|
473
|
+
this._documentCount -= 1;
|
|
474
|
+
this._dirtCount += 1;
|
|
475
|
+
this.maybeAutoVacuum();
|
|
476
|
+
}
|
|
477
|
+
maybeAutoVacuum() {
|
|
478
|
+
if (this._options.autoVacuum === false) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const { minDirtFactor, minDirtCount, batchSize, batchWait } = this._options.autoVacuum;
|
|
482
|
+
this.conditionalVacuum({ batchSize, batchWait }, { minDirtCount, minDirtFactor });
|
|
483
|
+
}
|
|
484
|
+
discardAll(ids) {
|
|
485
|
+
const autoVacuum = this._options.autoVacuum;
|
|
486
|
+
try {
|
|
487
|
+
this._options.autoVacuum = false;
|
|
488
|
+
for (const id of ids) {
|
|
489
|
+
this.discard(id);
|
|
490
|
+
}
|
|
491
|
+
} finally {
|
|
492
|
+
this._options.autoVacuum = autoVacuum;
|
|
493
|
+
}
|
|
494
|
+
this.maybeAutoVacuum();
|
|
495
|
+
}
|
|
496
|
+
replace(updatedDocument) {
|
|
497
|
+
const { idField, extractField } = this._options;
|
|
498
|
+
const id = extractField(updatedDocument, idField);
|
|
499
|
+
this.discard(id);
|
|
500
|
+
this.add(updatedDocument);
|
|
501
|
+
}
|
|
502
|
+
vacuum(options = {}) {
|
|
503
|
+
return this.conditionalVacuum(options);
|
|
504
|
+
}
|
|
505
|
+
conditionalVacuum(options, conditions) {
|
|
506
|
+
if (this._currentVacuum) {
|
|
507
|
+
this._enqueuedVacuumConditions = this._enqueuedVacuumConditions && conditions;
|
|
508
|
+
if (this._enqueuedVacuum != null) {
|
|
509
|
+
return this._enqueuedVacuum;
|
|
510
|
+
}
|
|
511
|
+
this._enqueuedVacuum = this._currentVacuum.then(() => {
|
|
512
|
+
const conditions2 = this._enqueuedVacuumConditions;
|
|
513
|
+
this._enqueuedVacuumConditions = defaultVacuumConditions;
|
|
514
|
+
return this.performVacuuming(options, conditions2);
|
|
515
|
+
});
|
|
516
|
+
return this._enqueuedVacuum;
|
|
517
|
+
}
|
|
518
|
+
if (this.vacuumConditionsMet(conditions) === false) {
|
|
519
|
+
return Promise.resolve();
|
|
520
|
+
}
|
|
521
|
+
this._currentVacuum = this.performVacuuming(options);
|
|
522
|
+
return this._currentVacuum;
|
|
523
|
+
}
|
|
524
|
+
async performVacuuming(options, conditions) {
|
|
525
|
+
const initialDirtCount = this._dirtCount;
|
|
526
|
+
if (this.vacuumConditionsMet(conditions)) {
|
|
527
|
+
const batchSize = options.batchSize || defaultVacuumOptions.batchSize;
|
|
528
|
+
const batchWait = options.batchWait || defaultVacuumOptions.batchWait;
|
|
529
|
+
let i = 1;
|
|
530
|
+
for (const [term, fieldsData] of this._index) {
|
|
531
|
+
for (const [fieldId, fieldIndex] of fieldsData) {
|
|
532
|
+
for (const [shortId] of fieldIndex) {
|
|
533
|
+
if (this._documentIds.has(shortId)) {
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (fieldIndex.size <= 1) {
|
|
537
|
+
fieldsData.delete(fieldId);
|
|
538
|
+
} else {
|
|
539
|
+
fieldIndex.delete(shortId);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (this._index.get(term).size === 0) {
|
|
544
|
+
this._index.delete(term);
|
|
545
|
+
}
|
|
546
|
+
if (i % batchSize === 0) {
|
|
547
|
+
await new Promise((resolve) => setTimeout(resolve, batchWait));
|
|
548
|
+
}
|
|
549
|
+
i += 1;
|
|
550
|
+
}
|
|
551
|
+
this._dirtCount -= initialDirtCount;
|
|
552
|
+
}
|
|
553
|
+
await null;
|
|
554
|
+
this._currentVacuum = this._enqueuedVacuum;
|
|
555
|
+
this._enqueuedVacuum = null;
|
|
556
|
+
}
|
|
557
|
+
vacuumConditionsMet(conditions) {
|
|
558
|
+
if (conditions == null) {
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
let { minDirtCount, minDirtFactor } = conditions;
|
|
562
|
+
minDirtCount = minDirtCount || defaultAutoVacuumOptions.minDirtCount;
|
|
563
|
+
minDirtFactor = minDirtFactor || defaultAutoVacuumOptions.minDirtFactor;
|
|
564
|
+
return this.dirtCount >= minDirtCount && this.dirtFactor >= minDirtFactor;
|
|
565
|
+
}
|
|
566
|
+
get isVacuuming() {
|
|
567
|
+
return this._currentVacuum != null;
|
|
568
|
+
}
|
|
569
|
+
get dirtCount() {
|
|
570
|
+
return this._dirtCount;
|
|
571
|
+
}
|
|
572
|
+
get dirtFactor() {
|
|
573
|
+
return this._dirtCount / (1 + this._documentCount + this._dirtCount);
|
|
574
|
+
}
|
|
575
|
+
has(id) {
|
|
576
|
+
return this._idToShortId.has(id);
|
|
577
|
+
}
|
|
578
|
+
getStoredFields(id) {
|
|
579
|
+
const shortId = this._idToShortId.get(id);
|
|
580
|
+
if (shortId == null) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
return this._storedFields.get(shortId);
|
|
584
|
+
}
|
|
585
|
+
search(query, searchOptions = {}) {
|
|
586
|
+
const { searchOptions: globalSearchOptions } = this._options;
|
|
587
|
+
const searchOptionsWithDefaults = { ...globalSearchOptions, ...searchOptions };
|
|
588
|
+
const rawResults = this.executeQuery(query, searchOptions);
|
|
589
|
+
const results = [];
|
|
590
|
+
for (const [docId, { score, terms, match }] of rawResults) {
|
|
591
|
+
const quality = terms.length || 1;
|
|
592
|
+
const result = {
|
|
593
|
+
id: this._documentIds.get(docId),
|
|
594
|
+
score: score * quality,
|
|
595
|
+
terms: Object.keys(match),
|
|
596
|
+
queryTerms: terms,
|
|
597
|
+
match
|
|
598
|
+
};
|
|
599
|
+
Object.assign(result, this._storedFields.get(docId));
|
|
600
|
+
if (searchOptionsWithDefaults.filter == null || searchOptionsWithDefaults.filter(result)) {
|
|
601
|
+
results.push(result);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (query === MiniSearch.wildcard && searchOptionsWithDefaults.boostDocument == null) {
|
|
605
|
+
return results;
|
|
606
|
+
}
|
|
607
|
+
results.sort(byScore);
|
|
608
|
+
return results;
|
|
609
|
+
}
|
|
610
|
+
autoSuggest(queryString, options = {}) {
|
|
611
|
+
options = { ...this._options.autoSuggestOptions, ...options };
|
|
612
|
+
const suggestions = new Map;
|
|
613
|
+
for (const { score, terms } of this.search(queryString, options)) {
|
|
614
|
+
const phrase = terms.join(" ");
|
|
615
|
+
const suggestion = suggestions.get(phrase);
|
|
616
|
+
if (suggestion != null) {
|
|
617
|
+
suggestion.score += score;
|
|
618
|
+
suggestion.count += 1;
|
|
619
|
+
} else {
|
|
620
|
+
suggestions.set(phrase, { score, terms, count: 1 });
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const results = [];
|
|
624
|
+
for (const [suggestion, { score, terms, count }] of suggestions) {
|
|
625
|
+
results.push({ suggestion, terms, score: score / count });
|
|
626
|
+
}
|
|
627
|
+
results.sort(byScore);
|
|
628
|
+
return results;
|
|
629
|
+
}
|
|
630
|
+
get documentCount() {
|
|
631
|
+
return this._documentCount;
|
|
632
|
+
}
|
|
633
|
+
get termCount() {
|
|
634
|
+
return this._index.size;
|
|
635
|
+
}
|
|
636
|
+
static loadJSON(json, options) {
|
|
637
|
+
if (options == null) {
|
|
638
|
+
throw new Error("MiniSearch: loadJSON should be given the same options used when serializing the index");
|
|
639
|
+
}
|
|
640
|
+
return this.loadJS(JSON.parse(json), options);
|
|
641
|
+
}
|
|
642
|
+
static async loadJSONAsync(json, options) {
|
|
643
|
+
if (options == null) {
|
|
644
|
+
throw new Error("MiniSearch: loadJSON should be given the same options used when serializing the index");
|
|
645
|
+
}
|
|
646
|
+
return this.loadJSAsync(JSON.parse(json), options);
|
|
647
|
+
}
|
|
648
|
+
static getDefault(optionName) {
|
|
649
|
+
if (defaultOptions.hasOwnProperty(optionName)) {
|
|
650
|
+
return getOwnProperty(defaultOptions, optionName);
|
|
651
|
+
} else {
|
|
652
|
+
throw new Error(`MiniSearch: unknown option "${optionName}"`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
static loadJS(js, options) {
|
|
656
|
+
const { index, documentIds, fieldLength, storedFields, serializationVersion } = js;
|
|
657
|
+
const miniSearch = this.instantiateMiniSearch(js, options);
|
|
658
|
+
miniSearch._documentIds = objectToNumericMap(documentIds);
|
|
659
|
+
miniSearch._fieldLength = objectToNumericMap(fieldLength);
|
|
660
|
+
miniSearch._storedFields = objectToNumericMap(storedFields);
|
|
661
|
+
for (const [shortId, id] of miniSearch._documentIds) {
|
|
662
|
+
miniSearch._idToShortId.set(id, shortId);
|
|
663
|
+
}
|
|
664
|
+
for (const [term, data] of index) {
|
|
665
|
+
const dataMap = new Map;
|
|
666
|
+
for (const fieldId of Object.keys(data)) {
|
|
667
|
+
let indexEntry = data[fieldId];
|
|
668
|
+
if (serializationVersion === 1) {
|
|
669
|
+
indexEntry = indexEntry.ds;
|
|
670
|
+
}
|
|
671
|
+
dataMap.set(parseInt(fieldId, 10), objectToNumericMap(indexEntry));
|
|
672
|
+
}
|
|
673
|
+
miniSearch._index.set(term, dataMap);
|
|
674
|
+
}
|
|
675
|
+
return miniSearch;
|
|
676
|
+
}
|
|
677
|
+
static async loadJSAsync(js, options) {
|
|
678
|
+
const { index, documentIds, fieldLength, storedFields, serializationVersion } = js;
|
|
679
|
+
const miniSearch = this.instantiateMiniSearch(js, options);
|
|
680
|
+
miniSearch._documentIds = await objectToNumericMapAsync(documentIds);
|
|
681
|
+
miniSearch._fieldLength = await objectToNumericMapAsync(fieldLength);
|
|
682
|
+
miniSearch._storedFields = await objectToNumericMapAsync(storedFields);
|
|
683
|
+
for (const [shortId, id] of miniSearch._documentIds) {
|
|
684
|
+
miniSearch._idToShortId.set(id, shortId);
|
|
685
|
+
}
|
|
686
|
+
let count = 0;
|
|
687
|
+
for (const [term, data] of index) {
|
|
688
|
+
const dataMap = new Map;
|
|
689
|
+
for (const fieldId of Object.keys(data)) {
|
|
690
|
+
let indexEntry = data[fieldId];
|
|
691
|
+
if (serializationVersion === 1) {
|
|
692
|
+
indexEntry = indexEntry.ds;
|
|
693
|
+
}
|
|
694
|
+
dataMap.set(parseInt(fieldId, 10), await objectToNumericMapAsync(indexEntry));
|
|
695
|
+
}
|
|
696
|
+
if (++count % 1000 === 0)
|
|
697
|
+
await wait(0);
|
|
698
|
+
miniSearch._index.set(term, dataMap);
|
|
699
|
+
}
|
|
700
|
+
return miniSearch;
|
|
701
|
+
}
|
|
702
|
+
static instantiateMiniSearch(js, options) {
|
|
703
|
+
const { documentCount, nextId, fieldIds, averageFieldLength, dirtCount, serializationVersion } = js;
|
|
704
|
+
if (serializationVersion !== 1 && serializationVersion !== 2) {
|
|
705
|
+
throw new Error("MiniSearch: cannot deserialize an index created with an incompatible version");
|
|
706
|
+
}
|
|
707
|
+
const miniSearch = new MiniSearch(options);
|
|
708
|
+
miniSearch._documentCount = documentCount;
|
|
709
|
+
miniSearch._nextId = nextId;
|
|
710
|
+
miniSearch._idToShortId = new Map;
|
|
711
|
+
miniSearch._fieldIds = fieldIds;
|
|
712
|
+
miniSearch._avgFieldLength = averageFieldLength;
|
|
713
|
+
miniSearch._dirtCount = dirtCount || 0;
|
|
714
|
+
miniSearch._index = new SearchableMap;
|
|
715
|
+
return miniSearch;
|
|
716
|
+
}
|
|
717
|
+
executeQuery(query, searchOptions = {}) {
|
|
718
|
+
if (query === MiniSearch.wildcard) {
|
|
719
|
+
return this.executeWildcardQuery(searchOptions);
|
|
720
|
+
}
|
|
721
|
+
if (typeof query !== "string") {
|
|
722
|
+
const options2 = { ...searchOptions, ...query, queries: undefined };
|
|
723
|
+
const results2 = query.queries.map((subquery) => this.executeQuery(subquery, options2));
|
|
724
|
+
return this.combineResults(results2, options2.combineWith);
|
|
725
|
+
}
|
|
726
|
+
const { tokenize, processTerm, searchOptions: globalSearchOptions } = this._options;
|
|
727
|
+
const options = { tokenize, processTerm, ...globalSearchOptions, ...searchOptions };
|
|
728
|
+
const { tokenize: searchTokenize, processTerm: searchProcessTerm } = options;
|
|
729
|
+
const terms = searchTokenize(query).flatMap((term) => searchProcessTerm(term)).filter((term) => !!term);
|
|
730
|
+
const queries = terms.map(termToQuerySpec(options));
|
|
731
|
+
const results = queries.map((query2) => this.executeQuerySpec(query2, options));
|
|
732
|
+
return this.combineResults(results, options.combineWith);
|
|
733
|
+
}
|
|
734
|
+
executeQuerySpec(query, searchOptions) {
|
|
735
|
+
const options = { ...this._options.searchOptions, ...searchOptions };
|
|
736
|
+
const boosts = (options.fields || this._options.fields).reduce((boosts2, field) => ({ ...boosts2, [field]: getOwnProperty(options.boost, field) || 1 }), {});
|
|
737
|
+
const { boostDocument, weights, maxFuzzy, bm25: bm25params } = options;
|
|
738
|
+
const { fuzzy: fuzzyWeight, prefix: prefixWeight } = { ...defaultSearchOptions.weights, ...weights };
|
|
739
|
+
const data = this._index.get(query.term);
|
|
740
|
+
const results = this.termResults(query.term, query.term, 1, query.termBoost, data, boosts, boostDocument, bm25params);
|
|
741
|
+
let prefixMatches;
|
|
742
|
+
let fuzzyMatches;
|
|
743
|
+
if (query.prefix) {
|
|
744
|
+
prefixMatches = this._index.atPrefix(query.term);
|
|
745
|
+
}
|
|
746
|
+
if (query.fuzzy) {
|
|
747
|
+
const fuzzy = query.fuzzy === true ? 0.2 : query.fuzzy;
|
|
748
|
+
const maxDistance = fuzzy < 1 ? Math.min(maxFuzzy, Math.round(query.term.length * fuzzy)) : fuzzy;
|
|
749
|
+
if (maxDistance)
|
|
750
|
+
fuzzyMatches = this._index.fuzzyGet(query.term, maxDistance);
|
|
751
|
+
}
|
|
752
|
+
if (prefixMatches) {
|
|
753
|
+
for (const [term, data2] of prefixMatches) {
|
|
754
|
+
const distance = term.length - query.term.length;
|
|
755
|
+
if (!distance) {
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
fuzzyMatches === null || fuzzyMatches === undefined || fuzzyMatches.delete(term);
|
|
759
|
+
const weight = prefixWeight * term.length / (term.length + 0.3 * distance);
|
|
760
|
+
this.termResults(query.term, term, weight, query.termBoost, data2, boosts, boostDocument, bm25params, results);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (fuzzyMatches) {
|
|
764
|
+
for (const term of fuzzyMatches.keys()) {
|
|
765
|
+
const [data2, distance] = fuzzyMatches.get(term);
|
|
766
|
+
if (!distance) {
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
const weight = fuzzyWeight * term.length / (term.length + distance);
|
|
770
|
+
this.termResults(query.term, term, weight, query.termBoost, data2, boosts, boostDocument, bm25params, results);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return results;
|
|
774
|
+
}
|
|
775
|
+
executeWildcardQuery(searchOptions) {
|
|
776
|
+
const results = new Map;
|
|
777
|
+
const options = { ...this._options.searchOptions, ...searchOptions };
|
|
778
|
+
for (const [shortId, id] of this._documentIds) {
|
|
779
|
+
const score = options.boostDocument ? options.boostDocument(id, "", this._storedFields.get(shortId)) : 1;
|
|
780
|
+
results.set(shortId, {
|
|
781
|
+
score,
|
|
782
|
+
terms: [],
|
|
783
|
+
match: {}
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
return results;
|
|
787
|
+
}
|
|
788
|
+
combineResults(results, combineWith = OR) {
|
|
789
|
+
if (results.length === 0) {
|
|
790
|
+
return new Map;
|
|
791
|
+
}
|
|
792
|
+
const operator = combineWith.toLowerCase();
|
|
793
|
+
const combinator = combinators[operator];
|
|
794
|
+
if (!combinator) {
|
|
795
|
+
throw new Error(`Invalid combination operator: ${combineWith}`);
|
|
796
|
+
}
|
|
797
|
+
return results.reduce(combinator) || new Map;
|
|
798
|
+
}
|
|
799
|
+
toJSON() {
|
|
800
|
+
const index = [];
|
|
801
|
+
for (const [term, fieldIndex] of this._index) {
|
|
802
|
+
const data = {};
|
|
803
|
+
for (const [fieldId, freqs] of fieldIndex) {
|
|
804
|
+
data[fieldId] = Object.fromEntries(freqs);
|
|
805
|
+
}
|
|
806
|
+
index.push([term, data]);
|
|
807
|
+
}
|
|
808
|
+
return {
|
|
809
|
+
documentCount: this._documentCount,
|
|
810
|
+
nextId: this._nextId,
|
|
811
|
+
documentIds: Object.fromEntries(this._documentIds),
|
|
812
|
+
fieldIds: this._fieldIds,
|
|
813
|
+
fieldLength: Object.fromEntries(this._fieldLength),
|
|
814
|
+
averageFieldLength: this._avgFieldLength,
|
|
815
|
+
storedFields: Object.fromEntries(this._storedFields),
|
|
816
|
+
dirtCount: this._dirtCount,
|
|
817
|
+
index,
|
|
818
|
+
serializationVersion: 2
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
termResults(sourceTerm, derivedTerm, termWeight, termBoost, fieldTermData, fieldBoosts, boostDocumentFn, bm25params, results = new Map) {
|
|
822
|
+
if (fieldTermData == null)
|
|
823
|
+
return results;
|
|
824
|
+
for (const field of Object.keys(fieldBoosts)) {
|
|
825
|
+
const fieldBoost = fieldBoosts[field];
|
|
826
|
+
const fieldId = this._fieldIds[field];
|
|
827
|
+
const fieldTermFreqs = fieldTermData.get(fieldId);
|
|
828
|
+
if (fieldTermFreqs == null)
|
|
829
|
+
continue;
|
|
830
|
+
let matchingFields = fieldTermFreqs.size;
|
|
831
|
+
const avgFieldLength = this._avgFieldLength[fieldId];
|
|
832
|
+
for (const docId of fieldTermFreqs.keys()) {
|
|
833
|
+
if (!this._documentIds.has(docId)) {
|
|
834
|
+
this.removeTerm(fieldId, docId, derivedTerm);
|
|
835
|
+
matchingFields -= 1;
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
const docBoost = boostDocumentFn ? boostDocumentFn(this._documentIds.get(docId), derivedTerm, this._storedFields.get(docId)) : 1;
|
|
839
|
+
if (!docBoost)
|
|
840
|
+
continue;
|
|
841
|
+
const termFreq = fieldTermFreqs.get(docId);
|
|
842
|
+
const fieldLength = this._fieldLength.get(docId)[fieldId];
|
|
843
|
+
const rawScore = calcBM25Score(termFreq, matchingFields, this._documentCount, fieldLength, avgFieldLength, bm25params);
|
|
844
|
+
const weightedScore = termWeight * termBoost * fieldBoost * docBoost * rawScore;
|
|
845
|
+
const result = results.get(docId);
|
|
846
|
+
if (result) {
|
|
847
|
+
result.score += weightedScore;
|
|
848
|
+
assignUniqueTerm(result.terms, sourceTerm);
|
|
849
|
+
const match = getOwnProperty(result.match, derivedTerm);
|
|
850
|
+
if (match) {
|
|
851
|
+
match.push(field);
|
|
852
|
+
} else {
|
|
853
|
+
result.match[derivedTerm] = [field];
|
|
854
|
+
}
|
|
855
|
+
} else {
|
|
856
|
+
results.set(docId, {
|
|
857
|
+
score: weightedScore,
|
|
858
|
+
terms: [sourceTerm],
|
|
859
|
+
match: { [derivedTerm]: [field] }
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return results;
|
|
865
|
+
}
|
|
866
|
+
addTerm(fieldId, documentId, term) {
|
|
867
|
+
const indexData = this._index.fetch(term, createMap);
|
|
868
|
+
let fieldIndex = indexData.get(fieldId);
|
|
869
|
+
if (fieldIndex == null) {
|
|
870
|
+
fieldIndex = new Map;
|
|
871
|
+
fieldIndex.set(documentId, 1);
|
|
872
|
+
indexData.set(fieldId, fieldIndex);
|
|
873
|
+
} else {
|
|
874
|
+
const docs = fieldIndex.get(documentId);
|
|
875
|
+
fieldIndex.set(documentId, (docs || 0) + 1);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
removeTerm(fieldId, documentId, term) {
|
|
879
|
+
if (!this._index.has(term)) {
|
|
880
|
+
this.warnDocumentChanged(documentId, fieldId, term);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const indexData = this._index.fetch(term, createMap);
|
|
884
|
+
const fieldIndex = indexData.get(fieldId);
|
|
885
|
+
if (fieldIndex == null || fieldIndex.get(documentId) == null) {
|
|
886
|
+
this.warnDocumentChanged(documentId, fieldId, term);
|
|
887
|
+
} else if (fieldIndex.get(documentId) <= 1) {
|
|
888
|
+
if (fieldIndex.size <= 1) {
|
|
889
|
+
indexData.delete(fieldId);
|
|
890
|
+
} else {
|
|
891
|
+
fieldIndex.delete(documentId);
|
|
892
|
+
}
|
|
893
|
+
} else {
|
|
894
|
+
fieldIndex.set(documentId, fieldIndex.get(documentId) - 1);
|
|
895
|
+
}
|
|
896
|
+
if (this._index.get(term).size === 0) {
|
|
897
|
+
this._index.delete(term);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
warnDocumentChanged(shortDocumentId, fieldId, term) {
|
|
901
|
+
for (const fieldName of Object.keys(this._fieldIds)) {
|
|
902
|
+
if (this._fieldIds[fieldName] === fieldId) {
|
|
903
|
+
this._options.logger("warn", `MiniSearch: document with ID ${this._documentIds.get(shortDocumentId)} has changed before removal: term "${term}" was not present in field "${fieldName}". Removing a document after it has changed can corrupt the index!`, "version_conflict");
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
addDocumentId(documentId) {
|
|
909
|
+
const shortDocumentId = this._nextId;
|
|
910
|
+
this._idToShortId.set(documentId, shortDocumentId);
|
|
911
|
+
this._documentIds.set(shortDocumentId, documentId);
|
|
912
|
+
this._documentCount += 1;
|
|
913
|
+
this._nextId += 1;
|
|
914
|
+
return shortDocumentId;
|
|
915
|
+
}
|
|
916
|
+
addFields(fields) {
|
|
917
|
+
for (let i = 0;i < fields.length; i++) {
|
|
918
|
+
this._fieldIds[fields[i]] = i;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
addFieldLength(documentId, fieldId, count, length) {
|
|
922
|
+
let fieldLengths = this._fieldLength.get(documentId);
|
|
923
|
+
if (fieldLengths == null)
|
|
924
|
+
this._fieldLength.set(documentId, fieldLengths = []);
|
|
925
|
+
fieldLengths[fieldId] = length;
|
|
926
|
+
const averageFieldLength = this._avgFieldLength[fieldId] || 0;
|
|
927
|
+
const totalFieldLength = averageFieldLength * count + length;
|
|
928
|
+
this._avgFieldLength[fieldId] = totalFieldLength / (count + 1);
|
|
929
|
+
}
|
|
930
|
+
removeFieldLength(documentId, fieldId, count, length) {
|
|
931
|
+
if (count === 1) {
|
|
932
|
+
this._avgFieldLength[fieldId] = 0;
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const totalFieldLength = this._avgFieldLength[fieldId] * count - length;
|
|
936
|
+
this._avgFieldLength[fieldId] = totalFieldLength / (count - 1);
|
|
937
|
+
}
|
|
938
|
+
saveStoredFields(documentId, doc) {
|
|
939
|
+
const { storeFields, extractField } = this._options;
|
|
940
|
+
if (storeFields == null || storeFields.length === 0) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
let documentFields = this._storedFields.get(documentId);
|
|
944
|
+
if (documentFields == null)
|
|
945
|
+
this._storedFields.set(documentId, documentFields = {});
|
|
946
|
+
for (const fieldName of storeFields) {
|
|
947
|
+
const fieldValue = extractField(doc, fieldName);
|
|
948
|
+
if (fieldValue !== undefined)
|
|
949
|
+
documentFields[fieldName] = fieldValue;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
MiniSearch.wildcard = Symbol("*");
|
|
954
|
+
var getOwnProperty = (object, property) => Object.prototype.hasOwnProperty.call(object, property) ? object[property] : undefined;
|
|
955
|
+
var combinators = {
|
|
956
|
+
[OR]: (a, b) => {
|
|
957
|
+
for (const docId of b.keys()) {
|
|
958
|
+
const existing = a.get(docId);
|
|
959
|
+
if (existing == null) {
|
|
960
|
+
a.set(docId, b.get(docId));
|
|
961
|
+
} else {
|
|
962
|
+
const { score, terms, match } = b.get(docId);
|
|
963
|
+
existing.score = existing.score + score;
|
|
964
|
+
existing.match = Object.assign(existing.match, match);
|
|
965
|
+
assignUniqueTerms(existing.terms, terms);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return a;
|
|
969
|
+
},
|
|
970
|
+
[AND]: (a, b) => {
|
|
971
|
+
const combined = new Map;
|
|
972
|
+
for (const docId of b.keys()) {
|
|
973
|
+
const existing = a.get(docId);
|
|
974
|
+
if (existing == null)
|
|
975
|
+
continue;
|
|
976
|
+
const { score, terms, match } = b.get(docId);
|
|
977
|
+
assignUniqueTerms(existing.terms, terms);
|
|
978
|
+
combined.set(docId, {
|
|
979
|
+
score: existing.score + score,
|
|
980
|
+
terms: existing.terms,
|
|
981
|
+
match: Object.assign(existing.match, match)
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
return combined;
|
|
985
|
+
},
|
|
986
|
+
[AND_NOT]: (a, b) => {
|
|
987
|
+
for (const docId of b.keys())
|
|
988
|
+
a.delete(docId);
|
|
989
|
+
return a;
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
var defaultBM25params = { k: 1.2, b: 0.7, d: 0.5 };
|
|
993
|
+
var calcBM25Score = (termFreq, matchingCount, totalCount, fieldLength, avgFieldLength, bm25params) => {
|
|
994
|
+
const { k, b, d } = bm25params;
|
|
995
|
+
const invDocFreq = Math.log(1 + (totalCount - matchingCount + 0.5) / (matchingCount + 0.5));
|
|
996
|
+
return invDocFreq * (d + termFreq * (k + 1) / (termFreq + k * (1 - b + b * fieldLength / avgFieldLength)));
|
|
997
|
+
};
|
|
998
|
+
var termToQuerySpec = (options) => (term, i, terms) => {
|
|
999
|
+
const fuzzy = typeof options.fuzzy === "function" ? options.fuzzy(term, i, terms) : options.fuzzy || false;
|
|
1000
|
+
const prefix = typeof options.prefix === "function" ? options.prefix(term, i, terms) : options.prefix === true;
|
|
1001
|
+
const termBoost = typeof options.boostTerm === "function" ? options.boostTerm(term, i, terms) : 1;
|
|
1002
|
+
return { term, fuzzy, prefix, termBoost };
|
|
1003
|
+
};
|
|
1004
|
+
var defaultOptions = {
|
|
1005
|
+
idField: "id",
|
|
1006
|
+
extractField: (document, fieldName) => document[fieldName],
|
|
1007
|
+
stringifyField: (fieldValue, fieldName) => fieldValue.toString(),
|
|
1008
|
+
tokenize: (text) => text.split(SPACE_OR_PUNCTUATION),
|
|
1009
|
+
processTerm: (term) => term.toLowerCase(),
|
|
1010
|
+
fields: undefined,
|
|
1011
|
+
searchOptions: undefined,
|
|
1012
|
+
storeFields: [],
|
|
1013
|
+
logger: (level, message) => {
|
|
1014
|
+
if (typeof (console === null || console === undefined ? undefined : console[level]) === "function")
|
|
1015
|
+
console[level](message);
|
|
1016
|
+
},
|
|
1017
|
+
autoVacuum: true
|
|
1018
|
+
};
|
|
1019
|
+
var defaultSearchOptions = {
|
|
1020
|
+
combineWith: OR,
|
|
1021
|
+
prefix: false,
|
|
1022
|
+
fuzzy: false,
|
|
1023
|
+
maxFuzzy: 6,
|
|
1024
|
+
boost: {},
|
|
1025
|
+
weights: { fuzzy: 0.45, prefix: 0.375 },
|
|
1026
|
+
bm25: defaultBM25params
|
|
1027
|
+
};
|
|
1028
|
+
var defaultAutoSuggestOptions = {
|
|
1029
|
+
combineWith: AND,
|
|
1030
|
+
prefix: (term, i, terms) => i === terms.length - 1
|
|
1031
|
+
};
|
|
1032
|
+
var defaultVacuumOptions = { batchSize: 1000, batchWait: 10 };
|
|
1033
|
+
var defaultVacuumConditions = { minDirtFactor: 0.1, minDirtCount: 20 };
|
|
1034
|
+
var defaultAutoVacuumOptions = { ...defaultVacuumOptions, ...defaultVacuumConditions };
|
|
1035
|
+
var assignUniqueTerm = (target, term) => {
|
|
1036
|
+
if (!target.includes(term))
|
|
1037
|
+
target.push(term);
|
|
1038
|
+
};
|
|
1039
|
+
var assignUniqueTerms = (target, source) => {
|
|
1040
|
+
for (const term of source) {
|
|
1041
|
+
if (!target.includes(term))
|
|
1042
|
+
target.push(term);
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
var byScore = ({ score: a }, { score: b }) => b - a;
|
|
1046
|
+
var createMap = () => new Map;
|
|
1047
|
+
var objectToNumericMap = (object) => {
|
|
1048
|
+
const map = new Map;
|
|
1049
|
+
for (const key of Object.keys(object)) {
|
|
1050
|
+
map.set(parseInt(key, 10), object[key]);
|
|
1051
|
+
}
|
|
1052
|
+
return map;
|
|
1053
|
+
};
|
|
1054
|
+
var objectToNumericMapAsync = async (object) => {
|
|
1055
|
+
const map = new Map;
|
|
1056
|
+
let count = 0;
|
|
1057
|
+
for (const key of Object.keys(object)) {
|
|
1058
|
+
map.set(parseInt(key, 10), object[key]);
|
|
1059
|
+
if (++count % 1000 === 0) {
|
|
1060
|
+
await wait(0);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return map;
|
|
1064
|
+
};
|
|
1065
|
+
var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1066
|
+
var SPACE_OR_PUNCTUATION = /[\n\r\p{Z}\p{P}]+/u;
|
|
1067
|
+
|
|
1068
|
+
// src/lib/search/tokenize.ts
|
|
1069
|
+
var asciiTokenPattern = /[a-z0-9][a-z0-9_./:-]*/g;
|
|
1070
|
+
var cjkCharPattern = /[\u3400-\u9fff]/;
|
|
1071
|
+
function tokenize(input) {
|
|
1072
|
+
const tokens = [];
|
|
1073
|
+
const normalized = input.toLowerCase();
|
|
1074
|
+
let cjkRun = "";
|
|
1075
|
+
let asciiRun = "";
|
|
1076
|
+
for (const char of normalized) {
|
|
1077
|
+
if (cjkCharPattern.test(char)) {
|
|
1078
|
+
flushAscii(tokens, asciiRun);
|
|
1079
|
+
asciiRun = "";
|
|
1080
|
+
cjkRun += char;
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
if (isAsciiTokenChar(char)) {
|
|
1084
|
+
flushCjk(tokens, cjkRun);
|
|
1085
|
+
cjkRun = "";
|
|
1086
|
+
asciiRun += char;
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
flushCjk(tokens, cjkRun);
|
|
1090
|
+
flushAscii(tokens, asciiRun);
|
|
1091
|
+
cjkRun = "";
|
|
1092
|
+
asciiRun = "";
|
|
1093
|
+
}
|
|
1094
|
+
flushCjk(tokens, cjkRun);
|
|
1095
|
+
flushAscii(tokens, asciiRun);
|
|
1096
|
+
return tokens;
|
|
1097
|
+
}
|
|
1098
|
+
function flushCjk(tokens, value) {
|
|
1099
|
+
if (!value) {
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
for (const char of value) {
|
|
1103
|
+
tokens.push(char);
|
|
1104
|
+
}
|
|
1105
|
+
for (let size = 2;size <= 3; size += 1) {
|
|
1106
|
+
for (let index = 0;index <= value.length - size; index += 1) {
|
|
1107
|
+
tokens.push(value.slice(index, index + size));
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
function flushAscii(tokens, value) {
|
|
1112
|
+
if (!value) {
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
for (const match of value.matchAll(asciiTokenPattern)) {
|
|
1116
|
+
const token = match[0];
|
|
1117
|
+
tokens.push(token);
|
|
1118
|
+
for (const part of token.split(/[^a-z0-9]+/)) {
|
|
1119
|
+
if (part && part !== token) {
|
|
1120
|
+
tokens.push(part);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
function isAsciiTokenChar(char) {
|
|
1126
|
+
return /^[a-z0-9_./:-]$/.test(char);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// src/lib/search/searcher.ts
|
|
1130
|
+
function createMiniSearch() {
|
|
1131
|
+
return new MiniSearch({
|
|
1132
|
+
fields: ["title", "platform_text", "headings_text", "id", "public_path", "source_url"],
|
|
1133
|
+
searchOptions: {
|
|
1134
|
+
boost: {
|
|
1135
|
+
title: 8,
|
|
1136
|
+
platform_text: 6,
|
|
1137
|
+
headings_text: 5,
|
|
1138
|
+
id: 3,
|
|
1139
|
+
public_path: 2,
|
|
1140
|
+
source_url: 1
|
|
1141
|
+
},
|
|
1142
|
+
prefix: true
|
|
1143
|
+
},
|
|
1144
|
+
tokenize
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
function toMiniSearchDocuments(documents, platformAliases = {}) {
|
|
1148
|
+
return documents.map((document) => ({
|
|
1149
|
+
...document,
|
|
1150
|
+
headings_text: document.headings.join(`
|
|
1151
|
+
`),
|
|
1152
|
+
platform_text: [document.platform, ...platformAliases[document.platform] ?? []].join(" ")
|
|
1153
|
+
}));
|
|
1154
|
+
}
|
|
1155
|
+
async function searchDocuments(input) {
|
|
1156
|
+
const query = input.query.trim();
|
|
1157
|
+
if (!query) {
|
|
1158
|
+
return [];
|
|
1159
|
+
}
|
|
1160
|
+
const platformAliases = input.platformAliases ?? {};
|
|
1161
|
+
const documents = input.platform ? input.documents.filter((document) => document.platform === input.platform) : input.documents;
|
|
1162
|
+
const documentsById = new Map(documents.map((document) => [document.id, document]));
|
|
1163
|
+
const minisearch = input.miniSearch ? MiniSearch.loadJSON(JSON.stringify(input.miniSearch), {
|
|
1164
|
+
fields: ["title", "platform_text", "headings_text", "id", "public_path", "source_url"],
|
|
1165
|
+
searchOptions: {
|
|
1166
|
+
boost: {
|
|
1167
|
+
title: 8,
|
|
1168
|
+
platform_text: 6,
|
|
1169
|
+
headings_text: 5,
|
|
1170
|
+
id: 3,
|
|
1171
|
+
public_path: 2,
|
|
1172
|
+
source_url: 1
|
|
1173
|
+
},
|
|
1174
|
+
prefix: true
|
|
1175
|
+
},
|
|
1176
|
+
tokenize
|
|
1177
|
+
}) : createMiniSearch();
|
|
1178
|
+
if (!input.miniSearch) {
|
|
1179
|
+
minisearch.addAll(toMiniSearchDocuments(documents, platformAliases));
|
|
1180
|
+
}
|
|
1181
|
+
const limit = input.limit ?? 10;
|
|
1182
|
+
const candidates = minisearch.search(query).slice(0, Math.max(200, limit * 50)).map((result) => toSearchResult(result, documentsById, query, platformAliases)).filter((result) => result !== null);
|
|
1183
|
+
const seen = new Set(candidates.map((result) => result.id));
|
|
1184
|
+
for (const document of documents) {
|
|
1185
|
+
const score = rankDocument(query, document, platformAliases);
|
|
1186
|
+
if (score > 0 && !seen.has(document.id)) {
|
|
1187
|
+
candidates.push({ ...document, score, terms: tokenize(query) });
|
|
1188
|
+
seen.add(document.id);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return candidates.sort((left, right) => right.score - left.score).slice(0, limit);
|
|
1192
|
+
}
|
|
1193
|
+
function toSearchResult(result, documentsById, query, platformAliases) {
|
|
1194
|
+
const document = documentsById.get(String(result.id));
|
|
1195
|
+
if (!document) {
|
|
1196
|
+
return null;
|
|
1197
|
+
}
|
|
1198
|
+
return {
|
|
1199
|
+
...document,
|
|
1200
|
+
score: Math.log1p(result.score) * 30 + rankDocument(query, document, platformAliases),
|
|
1201
|
+
terms: result.terms
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
function rankDocument(query, document, platformAliases = {}) {
|
|
1205
|
+
const terms = Array.from(new Set(tokenize(query).filter((term) => term.length > 1)));
|
|
1206
|
+
const compactQuery = compact(query);
|
|
1207
|
+
return scoreField(compactQuery, terms, document.title, 3000, 350) + scoreField(compactQuery, terms, [document.platform, ...platformAliases[document.platform] ?? []].join(" "), 1200, 260) + scoreField(compactQuery, terms, document.headings.join(" "), 1500, 220) + scoreField(compactQuery, terms, document.public_path, 800, 120) + scoreField(compactQuery, terms, document.source_url, 500, 80) + scoreField(compactQuery, terms, document.text, 250, 45);
|
|
1208
|
+
}
|
|
1209
|
+
function scoreField(compactQuery, terms, value, exactBoost, termBoost) {
|
|
1210
|
+
const field = compact(value);
|
|
1211
|
+
let score = 0;
|
|
1212
|
+
if (compactQuery && field.includes(compactQuery)) {
|
|
1213
|
+
score += exactBoost;
|
|
1214
|
+
}
|
|
1215
|
+
for (const term of terms) {
|
|
1216
|
+
score += Math.min(countOccurrences(field, compact(term)), 3) * termBoost * termWeight(term);
|
|
1217
|
+
}
|
|
1218
|
+
return score;
|
|
1219
|
+
}
|
|
1220
|
+
function termWeight(term) {
|
|
1221
|
+
if (importantTerms.some((importantTerm) => term.includes(importantTerm))) {
|
|
1222
|
+
return 1.5;
|
|
1223
|
+
}
|
|
1224
|
+
if (genericChineseTerms.has(term) || [...genericChineseTerms].some((genericTerm) => term.includes(genericTerm))) {
|
|
1225
|
+
return 0.05;
|
|
1226
|
+
}
|
|
1227
|
+
return 1;
|
|
1228
|
+
}
|
|
1229
|
+
var genericChineseTerms = new Set([
|
|
1230
|
+
"广告",
|
|
1231
|
+
"数据",
|
|
1232
|
+
"接口",
|
|
1233
|
+
"文档",
|
|
1234
|
+
"获取",
|
|
1235
|
+
"查询",
|
|
1236
|
+
"创建",
|
|
1237
|
+
"更新",
|
|
1238
|
+
"删除",
|
|
1239
|
+
"投放"
|
|
1240
|
+
]);
|
|
1241
|
+
var importantTerms = [
|
|
1242
|
+
"消耗",
|
|
1243
|
+
"花费",
|
|
1244
|
+
"报表",
|
|
1245
|
+
"流水",
|
|
1246
|
+
"余额",
|
|
1247
|
+
"转化",
|
|
1248
|
+
"账户",
|
|
1249
|
+
"账号",
|
|
1250
|
+
"广告主",
|
|
1251
|
+
"成本",
|
|
1252
|
+
"cost",
|
|
1253
|
+
"report",
|
|
1254
|
+
"balance",
|
|
1255
|
+
"fund",
|
|
1256
|
+
"statement",
|
|
1257
|
+
"access",
|
|
1258
|
+
"token"
|
|
1259
|
+
];
|
|
1260
|
+
function countOccurrences(value, term) {
|
|
1261
|
+
if (!term) {
|
|
1262
|
+
return 0;
|
|
1263
|
+
}
|
|
1264
|
+
let count = 0;
|
|
1265
|
+
let index = value.indexOf(term);
|
|
1266
|
+
while (index !== -1) {
|
|
1267
|
+
count += 1;
|
|
1268
|
+
index = value.indexOf(term, index + term.length);
|
|
1269
|
+
}
|
|
1270
|
+
return count;
|
|
1271
|
+
}
|
|
1272
|
+
function compact(value) {
|
|
1273
|
+
return value.toLowerCase().replace(/\s+/g, "");
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// src/cli.ts
|
|
1277
|
+
var defaultSearchIndexUrl = "https://adcli.jiangzhx.com/search-index.json";
|
|
1278
|
+
var help = `adcli
|
|
1279
|
+
|
|
1280
|
+
Usage:
|
|
1281
|
+
adcli doc search <query> [--index ${defaultSearchIndexUrl}] [--platform tencent_ads] [--limit 10] [--json]
|
|
1282
|
+
|
|
1283
|
+
Commands:
|
|
1284
|
+
doc search Search published advertising API docs
|
|
1285
|
+
`;
|
|
1286
|
+
async function main() {
|
|
1287
|
+
const args = parseArgs(process.argv.slice(2));
|
|
1288
|
+
if (!args.domain || args.domain === "help" || args.domain === "--help" || args.domain === "-h") {
|
|
1289
|
+
console.log(help.trim());
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
if (args.domain !== "doc" || args.command !== "search") {
|
|
1293
|
+
throw new Error(`unknown command: ${[args.domain, args.command].filter(Boolean).join(" ")}`);
|
|
1294
|
+
}
|
|
1295
|
+
const query = args.query.join(" ").trim();
|
|
1296
|
+
if (!query) {
|
|
1297
|
+
throw new Error("missing search query");
|
|
1298
|
+
}
|
|
1299
|
+
const index = await loadSearchIndex(args.index);
|
|
1300
|
+
const results = await searchDocuments({
|
|
1301
|
+
query,
|
|
1302
|
+
documents: index.documents,
|
|
1303
|
+
miniSearch: index.mini_search,
|
|
1304
|
+
platformAliases: index.platform_aliases,
|
|
1305
|
+
platform: args.platform,
|
|
1306
|
+
limit: args.limit
|
|
1307
|
+
});
|
|
1308
|
+
if (args.json) {
|
|
1309
|
+
console.log(JSON.stringify({ query, results }, null, 2));
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
if (results.length === 0) {
|
|
1313
|
+
console.log("No matching docs found.");
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
for (const [index2, result] of results.entries()) {
|
|
1317
|
+
console.log(`${index2 + 1}. [${result.platform}] ${result.title}`);
|
|
1318
|
+
console.log(` Doc: ${result.public_path}`);
|
|
1319
|
+
if (result.source_url) {
|
|
1320
|
+
console.log(` Source: ${result.source_url}`);
|
|
1321
|
+
}
|
|
1322
|
+
console.log(` Score: ${result.score.toFixed(2)}`);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
function parseArgs(argv) {
|
|
1326
|
+
const args = {
|
|
1327
|
+
domain: argv[0],
|
|
1328
|
+
command: argv[1],
|
|
1329
|
+
query: [],
|
|
1330
|
+
index: process.env.ADCLI_SEARCH_INDEX ?? defaultSearchIndexUrl,
|
|
1331
|
+
limit: 10,
|
|
1332
|
+
json: false
|
|
1333
|
+
};
|
|
1334
|
+
for (let index = 2;index < argv.length; index += 1) {
|
|
1335
|
+
const value = argv[index];
|
|
1336
|
+
if (value === "--json") {
|
|
1337
|
+
args.json = true;
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
if (value === "--index") {
|
|
1341
|
+
args.index = argv[index + 1] ?? "";
|
|
1342
|
+
index += 1;
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
if (value === "--limit") {
|
|
1346
|
+
args.limit = Number.parseInt(argv[index + 1] ?? "", 10);
|
|
1347
|
+
index += 1;
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
if (value === "--platform") {
|
|
1351
|
+
args.platform = argv[index + 1];
|
|
1352
|
+
index += 1;
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
args.query.push(value ?? "");
|
|
1356
|
+
}
|
|
1357
|
+
if (!Number.isInteger(args.limit) || args.limit < 1) {
|
|
1358
|
+
throw new Error("--limit must be a positive integer");
|
|
1359
|
+
}
|
|
1360
|
+
return args;
|
|
1361
|
+
}
|
|
1362
|
+
async function loadSearchIndex(index) {
|
|
1363
|
+
if (/^https?:\/\//i.test(index)) {
|
|
1364
|
+
const response = await fetch(index);
|
|
1365
|
+
if (!response.ok) {
|
|
1366
|
+
throw new Error(`failed to fetch search index: ${index} (${response.status})`);
|
|
1367
|
+
}
|
|
1368
|
+
return await response.json();
|
|
1369
|
+
}
|
|
1370
|
+
const indexPath = path.resolve(index);
|
|
1371
|
+
try {
|
|
1372
|
+
return JSON.parse(await readFile(indexPath, "utf8"));
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1375
|
+
throw new Error(`missing search index: ${path.relative(process.cwd(), indexPath)}. Use --index ${defaultSearchIndexUrl} or run: bun run build:search-index`);
|
|
1376
|
+
}
|
|
1377
|
+
throw error;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
main().catch((error) => {
|
|
1381
|
+
console.error(error instanceof Error ? error.message : error);
|
|
1382
|
+
process.exit(1);
|
|
1383
|
+
});
|