@zenera/rag 1.1.9 → 1.1.10

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 (42) hide show
  1. package/README.md +154 -10
  2. package/dist/command.js +2 -1
  3. package/dist/docs/assemble.d.ts +52 -0
  4. package/dist/docs/assemble.js +127 -0
  5. package/dist/docs/build.d.ts +34 -0
  6. package/dist/docs/build.js +108 -0
  7. package/dist/docs/chunk.d.ts +73 -0
  8. package/dist/docs/chunk.js +586 -0
  9. package/dist/docs/command.d.ts +3 -0
  10. package/dist/docs/command.js +529 -0
  11. package/dist/docs/files.d.ts +94 -0
  12. package/dist/docs/files.js +80 -0
  13. package/dist/docs/index.d.ts +13 -0
  14. package/dist/docs/index.js +13 -0
  15. package/dist/docs/load.d.ts +28 -0
  16. package/dist/docs/load.js +212 -0
  17. package/dist/docs/lookup.d.ts +80 -0
  18. package/dist/docs/lookup.js +147 -0
  19. package/dist/docs/parse.d.ts +95 -0
  20. package/dist/docs/parse.js +372 -0
  21. package/dist/docs/readme.d.ts +6 -0
  22. package/dist/docs/readme.js +122 -0
  23. package/dist/docs/render.d.ts +13 -0
  24. package/dist/docs/render.js +46 -0
  25. package/dist/docs/repl.d.ts +7 -0
  26. package/dist/docs/repl.js +130 -0
  27. package/dist/docs/search.d.ts +92 -0
  28. package/dist/docs/search.js +251 -0
  29. package/dist/docs/store.d.ts +55 -0
  30. package/dist/docs/store.js +171 -0
  31. package/dist/docs/tools.d.ts +10 -0
  32. package/dist/docs/tools.js +300 -0
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.js +3 -0
  35. package/dist/schema/command.js +3 -0
  36. package/dist/schema/query.js +1 -0
  37. package/dist/schema/search.d.ts +2 -0
  38. package/dist/schema/search.js +18 -2
  39. package/dist/schema/store.d.ts +4 -2
  40. package/dist/schema/store.js +16 -9
  41. package/dist/schema/tools.js +21 -2
  42. package/package.json +17 -4
@@ -0,0 +1,130 @@
1
+ import { bold, cyan, dim, note, write } from '@zenera/cli/lib';
2
+ import { createInterface } from 'node:readline/promises';
3
+ import { assemble } from "./assemble.js";
4
+ import { CHUNK_KINDS } from "./chunk.js";
5
+ import { renderAssembly } from "./render.js";
6
+ import { SEARCH_MODES } from "./search.js";
7
+ export async function repl(index, initial, settings = {}) {
8
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
9
+ let query = { ...initial };
10
+ note(bold(`${index.manifest.counts.documents} document(s), ${index.manifest.counts.chunks} chunks`));
11
+ note(dim(' a bare line searches; `help` lists the rest.'));
12
+ try {
13
+ if (query.query) {
14
+ query = await run(index, query, settings);
15
+ }
16
+ for (;;) {
17
+ const line = (await rl.question(cyan('docs> '))).trim();
18
+ if (line === '') {
19
+ continue;
20
+ }
21
+ if (line === 'quit' || line === 'exit') {
22
+ return;
23
+ }
24
+ if (line === 'help') {
25
+ help();
26
+ continue;
27
+ }
28
+ if (line === 'reset') {
29
+ query = {};
30
+ note(dim(' cleared, narrowings and exclusions alike'));
31
+ continue;
32
+ }
33
+ if (line === 'show') {
34
+ note(dim(` ${JSON.stringify(query)}`));
35
+ continue;
36
+ }
37
+ if (line === 'files') {
38
+ for (const name of index.resolveFiles(query.files, query.exclude_files)) {
39
+ note(dim(` ${name}`));
40
+ }
41
+ continue;
42
+ }
43
+ const [head, ...rest] = line.split(' ');
44
+ const tail = rest.join(' ').trim();
45
+ const narrowed = narrow(query, head ?? '', tail);
46
+ if (narrowed) {
47
+ query = narrowed;
48
+ continue;
49
+ }
50
+ query = await run(index, { ...query, query: line }, settings);
51
+ }
52
+ }
53
+ finally {
54
+ rl.close();
55
+ }
56
+ }
57
+ /**
58
+ * The narrowing verbs. Each one replaces its own field rather than adding to
59
+ * it — `file guides/**` twice in a row means the second pattern, which is what
60
+ * anyone typing it meant. An empty argument clears the field.
61
+ */
62
+ function narrow(query, head, tail) {
63
+ const set = (key, value) => {
64
+ note(dim(` ${head} is ${tail || 'anything'}`));
65
+ return { ...query, [key]: value };
66
+ };
67
+ switch (head) {
68
+ case 'file':
69
+ return set('files', tail ? [tail] : undefined);
70
+ case 'not':
71
+ return set('exclude_files', tail ? [tail] : undefined);
72
+ case 'section':
73
+ return set('section', tail ? [tail] : undefined);
74
+ case 'kind':
75
+ if (tail && !CHUNK_KINDS.includes(tail)) {
76
+ note(dim(` kind is one of ${CHUNK_KINDS.join(', ')}`));
77
+ return query;
78
+ }
79
+ return set('kinds', tail ? [tail] : undefined);
80
+ case 'mode':
81
+ if (tail && !SEARCH_MODES.includes(tail)) {
82
+ note(dim(` mode is one of ${SEARCH_MODES.join(', ')}`));
83
+ return query;
84
+ }
85
+ return set('mode', (tail || undefined));
86
+ case 'limit': {
87
+ const limit = Number(tail);
88
+ if (!Number.isInteger(limit) || limit < 1) {
89
+ note(dim(' limit is a whole number of at least 1'));
90
+ return query;
91
+ }
92
+ return set('limit', limit);
93
+ }
94
+ default:
95
+ return undefined;
96
+ }
97
+ }
98
+ async function run(index, query, settings) {
99
+ const result = await index.search(query);
100
+ const excerpt = await assemble(index, result.matches, settings);
101
+ if (excerpt.files.length > 0) {
102
+ write(renderAssembly(excerpt, {}));
103
+ }
104
+ note(dim(` ${result.matches.length} passage(s) in ${excerpt.files.length} document(s)` +
105
+ ` · ${excerpt.shown} line(s) · ${result.files.length} document(s) in scope`));
106
+ if (result.matches.length === 0) {
107
+ return query;
108
+ }
109
+ const seen = new Set([...(query.exclude_ids ?? []), ...result.matches.map((m) => m.id)]);
110
+ note(dim(` ${seen.size} passage(s) now excluded — \`reset\` to see them again`));
111
+ return { ...query, exclude_ids: [...seen] };
112
+ }
113
+ function help() {
114
+ for (const line of [
115
+ ' <text> search',
116
+ ' file <pattern> only documents matching it; blank for all',
117
+ ' not <pattern> drop documents matching it',
118
+ ' section <name> only under this heading, by title, id or path',
119
+ ` kind <k> ${CHUNK_KINDS.join(' | ')}`,
120
+ ` mode <m> ${SEARCH_MODES.join(' | ')}`,
121
+ ' limit <n> passages per search',
122
+ ' files the documents currently in scope',
123
+ ' show the query as it stands',
124
+ ' reset forget it, narrowings and exclusions alike',
125
+ ' quit',
126
+ ]) {
127
+ note(dim(line));
128
+ }
129
+ }
130
+ //# sourceMappingURL=repl.js.map
@@ -0,0 +1,92 @@
1
+ import type { Embedder } from '@zenera/neo';
2
+ import { type FileOutline, type HeadingRecord, type Manifest, type OpenDocs, type Outline } from './files.ts';
3
+ import { ChunkStore } from './store.ts';
4
+ export type SearchMode = 'hybrid' | 'vector' | 'text';
5
+ export declare const SEARCH_MODES: readonly SearchMode[];
6
+ export interface DocsQuery {
7
+ query?: string;
8
+ mode?: SearchMode;
9
+ /** patterns over the document name: a glob, a substring, or a regex */
10
+ files?: readonly string[];
11
+ exclude_files?: readonly string[];
12
+ /** a heading title, a structure id, or a structure path */
13
+ section?: readonly string[];
14
+ /** one of `CHUNK_KINDS` */
15
+ kinds?: readonly string[];
16
+ /** chunk ids already seen, so asking again moves on */
17
+ exclude_ids?: readonly string[];
18
+ limit?: number;
19
+ }
20
+ export interface Match {
21
+ id: string;
22
+ path: string;
23
+ kind: string;
24
+ headings: string;
25
+ structureId: string;
26
+ structurePath: string;
27
+ /** every line this chunk wants shown, headings and table header included */
28
+ lineNumbers: number[];
29
+ bodyStart: number;
30
+ bodyEnd: number;
31
+ text: string;
32
+ score: number;
33
+ /** where it placed in each leg, when it placed at all */
34
+ ranks: {
35
+ vector?: number;
36
+ text?: number;
37
+ };
38
+ /**
39
+ * And how well it did there. Fusion scores a rank, so its numbers say only
40
+ * that something placed first — a question the corpus cannot answer at all
41
+ * still returns a confident-looking `1 / (RRF_K + 0)`. These are what tell
42
+ * a caller whether first place was worth having.
43
+ */
44
+ relevance: {
45
+ vector?: number;
46
+ text?: number;
47
+ };
48
+ }
49
+ export interface DocsResult {
50
+ matches: Match[];
51
+ mode: SearchMode;
52
+ /** the documents the narrowings left in play */
53
+ files: string[];
54
+ /** the sections they left in play, as structure paths */
55
+ sections: string[];
56
+ /** distinct chunks either leg returned, before fusion trimmed them */
57
+ considered: number;
58
+ }
59
+ export declare const DEFAULT_LIMIT = 8;
60
+ export declare class DocsIndex {
61
+ #private;
62
+ readonly manifest: Manifest;
63
+ readonly outline: Outline;
64
+ constructor(index: OpenDocs, store: ChunkStore, embedder?: Embedder);
65
+ /**
66
+ * An embedder is optional because half of what this index is for needs no
67
+ * model at all: listing, grepping and reading are exact, and asking for a
68
+ * credential to do them would be asking for nothing.
69
+ */
70
+ static open(dir: string, embedder?: Embedder): Promise<DocsIndex>;
71
+ /** A document, split the way every line number in this index counts it. */
72
+ lines(name: string): Promise<string[]>;
73
+ file(name: string): FileOutline | undefined;
74
+ /** Document names matching the patterns, or all of them when there are none. */
75
+ resolveFiles(patterns?: readonly string[], exclude?: readonly string[]): string[];
76
+ /**
77
+ * Headings the terms name, over the documents still in play. A term is tried
78
+ * as a structure id, then a structure path, then as a pattern over the
79
+ * title — so `--section overview` and `--section sec:3` are the same
80
+ * argument, and neither has to be explained.
81
+ */
82
+ resolveSections(terms: readonly string[], files: readonly string[]): HeadingRecord[];
83
+ close(): void;
84
+ search(query: DocsQuery, signal?: AbortSignal): Promise<DocsResult>;
85
+ }
86
+ /**
87
+ * Inside one of these sections, or anywhere when none were asked for. The
88
+ * boundary is a path separator and not a prefix, because `doc/sec:1` is a
89
+ * prefix of `doc/sec:10` and they are different sections.
90
+ */
91
+ export declare const under: (path: string, sections: readonly string[]) => boolean;
92
+ //# sourceMappingURL=search.d.ts.map
@@ -0,0 +1,251 @@
1
+ import { assertSameEmbedding } from "../common/manifest.js";
2
+ import { loose, PatternError } from "../common/match.js";
3
+ import { CHUNK_KINDS, parseLines } from "./chunk.js";
4
+ import { openIndex, readLines, } from "./files.js";
5
+ import { ChunkStore } from "./store.js";
6
+ export const SEARCH_MODES = ['hybrid', 'vector', 'text'];
7
+ export const DEFAULT_LIMIT = 8;
8
+ /** Reciprocal rank fusion; the constant is the usual one and damps the top. */
9
+ const RRF_K = 60;
10
+ /** How much to ask each leg for, so fusion and the caps have room to work. */
11
+ const OVERFETCH = 6;
12
+ /**
13
+ * And an absolute ceiling on it. The multiple exists to survive the JavaScript
14
+ * filter and the diversifier, which is a fixed cost, not one that grows with
15
+ * the limit — at `--limit 32` the multiple alone asked for 192 rows, which on
16
+ * a small corpus is most of it. Every row fetched votes in the fusion at full
17
+ * strength, so an over-deep leg does not add recall, it adds an electorate.
18
+ */
19
+ const MAX_FETCH = 96;
20
+ /**
21
+ * Floors for the per-section and per-document caps, and the share of the limit
22
+ * they grow by once it is large enough to matter.
23
+ *
24
+ * The caps were constants, which made them a diversity nudge at the default
25
+ * limit of eight and a straitjacket above it: `--limit 20` for a question whose
26
+ * answer is one section of one file returned five chunks of the answer and
27
+ * fifteen of whatever else scored, because the sixth chunk of the right file
28
+ * was evicted in favour of the first chunk of a worse one. Asking for more
29
+ * results is asking to go deeper, and depth on a documented topic means more
30
+ * of the document that covers it.
31
+ */
32
+ const MAX_PER_SECTION = 3;
33
+ const MAX_PER_FILE = 5;
34
+ const SECTION_SHARE = 0.25;
35
+ const FILE_SHARE = 0.5;
36
+ export class DocsIndex {
37
+ manifest;
38
+ outline;
39
+ #index;
40
+ #store;
41
+ #embedder;
42
+ constructor(index, store, embedder) {
43
+ this.manifest = index.manifest;
44
+ this.outline = index.outline;
45
+ this.#index = index;
46
+ this.#store = store;
47
+ this.#embedder = embedder;
48
+ }
49
+ /**
50
+ * An embedder is optional because half of what this index is for needs no
51
+ * model at all: listing, grepping and reading are exact, and asking for a
52
+ * credential to do them would be asking for nothing.
53
+ */
54
+ static async open(dir, embedder) {
55
+ const index = await openIndex(dir);
56
+ if (embedder) {
57
+ assertSameEmbedding(index.manifest, embedder.id);
58
+ }
59
+ return new DocsIndex(index, await ChunkStore.open(dir), embedder);
60
+ }
61
+ /** A document, split the way every line number in this index counts it. */
62
+ lines(name) {
63
+ return readLines(this.#index, name);
64
+ }
65
+ file(name) {
66
+ return this.outline.files.find((f) => f.name === name);
67
+ }
68
+ /** Document names matching the patterns, or all of them when there are none. */
69
+ resolveFiles(patterns = [], exclude = []) {
70
+ const names = this.manifest.sources.map((s) => s.name);
71
+ const keep = patterns.length === 0 ? names : names.filter(anyMatch(patterns));
72
+ return exclude.length === 0 ? keep : keep.filter((name) => !anyMatch(exclude)(name));
73
+ }
74
+ /**
75
+ * Headings the terms name, over the documents still in play. A term is tried
76
+ * as a structure id, then a structure path, then as a pattern over the
77
+ * title — so `--section overview` and `--section sec:3` are the same
78
+ * argument, and neither has to be explained.
79
+ */
80
+ resolveSections(terms, files) {
81
+ if (terms.length === 0) {
82
+ return [];
83
+ }
84
+ const within = new Set(files);
85
+ const out = [];
86
+ for (const file of this.outline.files) {
87
+ if (!within.has(file.name)) {
88
+ continue;
89
+ }
90
+ for (const heading of file.headings) {
91
+ const hit = terms.some((term) => term === heading.id ||
92
+ term === heading.path ||
93
+ anyMatch([term])(heading.title));
94
+ if (hit) {
95
+ out.push(heading);
96
+ }
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+ close() {
102
+ this.#store.close();
103
+ }
104
+ async search(query, signal) {
105
+ const mode = query.mode ?? 'hybrid';
106
+ const text = (query.query ?? '').trim();
107
+ const limit = Math.max(1, query.limit ?? DEFAULT_LIMIT);
108
+ const files = this.resolveFiles(query.files, query.exclude_files);
109
+ const headings = this.resolveSections(query.section ?? [], files);
110
+ const sections = headings.map((h) => h.path);
111
+ // Two ways to ask for nothing: a pattern no document answers to, and a
112
+ // section no document has. Both are told, not silently widened.
113
+ const impossible = files.length === 0 || ((query.section?.length ?? 0) > 0 && sections.length === 0);
114
+ if (impossible || !text) {
115
+ return { matches: [], mode, files, sections, considered: 0 };
116
+ }
117
+ const filter = this.#filter(query, files, sections);
118
+ const excluded = new Set(query.exclude_ids ?? []);
119
+ const fetch = Math.min(limit * OVERFETCH, MAX_FETCH) + excluded.size;
120
+ const vector = mode === 'text' ? [] : await this.#nearest(text, filter, fetch, signal);
121
+ const lexical = mode === 'vector' ? [] : await this.#store.matching(text, filter, fetch);
122
+ const fused = fuse(vector, lexical).filter((match) => !excluded.has(match.id) &&
123
+ files.includes(match.path) &&
124
+ under(match.structurePath, sections));
125
+ return {
126
+ matches: diversify(fused, limit),
127
+ mode,
128
+ files,
129
+ sections,
130
+ considered: fused.length,
131
+ };
132
+ }
133
+ async #nearest(text, filter, limit, signal) {
134
+ if (!this.#embedder) {
135
+ throw new Error('this index was opened without an embedder, so it cannot be searched');
136
+ }
137
+ const response = await this.#embedder.embed({
138
+ input: [text],
139
+ taskType: 'query',
140
+ signal,
141
+ });
142
+ return await this.#store.nearest(Float32Array.from(response.vectors[0]), filter, limit);
143
+ }
144
+ /**
145
+ * The SQL side of the narrowing. A short, safe file list becomes a
146
+ * predicate; a long or awkward one is left to the JavaScript filter that
147
+ * runs afterwards regardless, since that is what makes the answer correct.
148
+ */
149
+ #filter(query, files, sections) {
150
+ const all = this.manifest.sources.length;
151
+ return {
152
+ kinds: kindsOf(query.kinds),
153
+ paths: files.length < all && files.length <= 64 ? files : undefined,
154
+ prefixes: sections.length > 0 && sections.length <= 64 ? sections : undefined,
155
+ };
156
+ }
157
+ }
158
+ // ---------------------------------------------------------------------------
159
+ const anyMatch = (patterns) => {
160
+ const matchers = patterns.map((pattern) => loose(pattern));
161
+ return (value) => matchers.some((match) => match(value));
162
+ };
163
+ /**
164
+ * Inside one of these sections, or anywhere when none were asked for. The
165
+ * boundary is a path separator and not a prefix, because `doc/sec:1` is a
166
+ * prefix of `doc/sec:10` and they are different sections.
167
+ */
168
+ export const under = (path, sections) => sections.length === 0 ||
169
+ sections.some((section) => path === section || path.startsWith(`${section}/`));
170
+ function kindsOf(kinds) {
171
+ if (!kinds || kinds.length === 0) {
172
+ return undefined;
173
+ }
174
+ for (const kind of kinds) {
175
+ if (!CHUNK_KINDS.includes(kind)) {
176
+ throw new PatternError(`${kind} is not a kind of block: it is one of ${CHUNK_KINDS.join(', ')}`);
177
+ }
178
+ }
179
+ return [...kinds];
180
+ }
181
+ /**
182
+ * Reciprocal rank fusion over the chunk id. Fusing on the id rather than on a
183
+ * physical row means the ordering survives a compaction of the store, and it is
184
+ * also the only key the two legs are guaranteed to agree on.
185
+ */
186
+ function fuse(vector, lexical) {
187
+ const merged = new Map();
188
+ const add = (hits, leg) => {
189
+ for (const hit of hits) {
190
+ const existing = merged.get(hit.record.id) ?? match(hit);
191
+ existing.score += 1 / (RRF_K + hit.rank);
192
+ existing.ranks[leg] = hit.rank;
193
+ existing.relevance[leg] = hit.relevance;
194
+ merged.set(hit.record.id, existing);
195
+ }
196
+ };
197
+ add(vector, 'vector');
198
+ add(lexical, 'text');
199
+ return [...merged.values()].sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
200
+ }
201
+ function match(hit) {
202
+ const { record } = hit;
203
+ return {
204
+ id: record.id,
205
+ path: record.path,
206
+ kind: record.kind,
207
+ headings: record.headings,
208
+ structureId: record.structureId,
209
+ structurePath: record.structurePath,
210
+ lineNumbers: parseLines(record.lineSpec),
211
+ bodyStart: record.bodyStart,
212
+ bodyEnd: record.bodyEnd,
213
+ text: record.text,
214
+ score: 0,
215
+ ranks: {},
216
+ relevance: {},
217
+ };
218
+ }
219
+ /**
220
+ * Caps per section and per document, applied in score order. A question whose
221
+ * answer really is one section still gets it — the cap only stops a run of
222
+ * near-identical neighbours from crowding out the second place a thing is said.
223
+ */
224
+ function diversify(matches, limit) {
225
+ const sectionCap = Math.max(MAX_PER_SECTION, Math.ceil(limit * SECTION_SHARE));
226
+ const fileCap = Math.max(MAX_PER_FILE, Math.ceil(limit * FILE_SHARE));
227
+ const perSection = new Map();
228
+ const perFile = new Map();
229
+ const kept = [];
230
+ const held = [];
231
+ for (const item of matches) {
232
+ const section = `${item.path}\u0000${parentOf(item.structurePath)}`;
233
+ const sections = perSection.get(section) ?? 0;
234
+ const files = perFile.get(item.path) ?? 0;
235
+ if (sections >= sectionCap || files >= fileCap) {
236
+ held.push(item);
237
+ continue;
238
+ }
239
+ perSection.set(section, sections + 1);
240
+ perFile.set(item.path, files + 1);
241
+ kept.push(item);
242
+ if (kept.length === limit) {
243
+ return kept;
244
+ }
245
+ }
246
+ // Better a crowded answer than a short one: what the caps held back fills
247
+ // the rest, still in score order.
248
+ return [...kept, ...held].slice(0, limit);
249
+ }
250
+ const parentOf = (path) => path.slice(0, path.lastIndexOf('/')) || path;
251
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1,55 @@
1
+ import { type Connection, type Table } from '@lancedb/lancedb';
2
+ export interface ChunkRecord {
3
+ /** `${path}#c${ordinal}` — the fusion key, and stable across compaction */
4
+ id: string;
5
+ /** the document's name in the index: its relative path */
6
+ path: string;
7
+ ordinal: number;
8
+ kind: string;
9
+ /** the full-text document — wider */
10
+ text: string;
11
+ /** what the vector was made from — tighter */
12
+ embedText: string;
13
+ /** the render set, run-length encoded: `1,5,12-40` */
14
+ lineSpec: string;
15
+ bodyStart: number;
16
+ bodyEnd: number;
17
+ structureId: string;
18
+ structurePath: string;
19
+ headings: string;
20
+ tokens: number;
21
+ }
22
+ export interface StoreFilter {
23
+ kinds?: readonly string[];
24
+ /** document names, exactly as the manifest spells them */
25
+ paths?: readonly string[];
26
+ /** structure path prefixes, as `structurePath LIKE 'x%'` */
27
+ prefixes?: readonly string[];
28
+ }
29
+ export interface Hit {
30
+ record: ChunkRecord;
31
+ /** 0-based position in this leg's own result list */
32
+ rank: number;
33
+ relevance: number;
34
+ }
35
+ export interface WriteResult {
36
+ rows: number;
37
+ fts: boolean;
38
+ vector: boolean;
39
+ }
40
+ export declare function writeChunks(dir: string, rows: readonly ChunkRecord[], vectors: readonly Float32Array[]): Promise<WriteResult>;
41
+ export declare class ChunkStore {
42
+ #private;
43
+ constructor(db: Connection, table: Table);
44
+ static open(dir: string): Promise<ChunkStore>;
45
+ /** Nearest neighbours. Every row has a vector, so nothing has to be excluded. */
46
+ nearest(vector: Float32Array, filter: StoreFilter, limit: number): Promise<Hit[]>;
47
+ /**
48
+ * The lexical leg. A missing full-text index, or a query the tokenizer
49
+ * makes nothing of, answers with nothing rather than throwing: a hybrid
50
+ * search that loses one leg is a worse search, not a failed one.
51
+ */
52
+ matching(text: string, filter: StoreFilter, limit: number): Promise<Hit[]>;
53
+ close(): void;
54
+ }
55
+ //# sourceMappingURL=store.d.ts.map