@3sln/trove 0.0.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/README.md +1227 -0
- package/package.json +75 -0
- package/packages/core/src/collections/index.js +249 -0
- package/packages/core/src/errors.js +186 -0
- package/packages/core/src/identity/discovery.js +210 -0
- package/packages/core/src/identity/index.js +188 -0
- package/packages/core/src/identity/jwt.js +199 -0
- package/packages/core/src/index.js +104 -0
- package/packages/core/src/indexers/contribution.js +115 -0
- package/packages/core/src/indexers/registry.js +162 -0
- package/packages/core/src/indexing.js +340 -0
- package/packages/core/src/issues.js +150 -0
- package/packages/core/src/kv.js +0 -0
- package/packages/core/src/links.js +141 -0
- package/packages/core/src/metadata/cursor.js +73 -0
- package/packages/core/src/metadata/interface.js +244 -0
- package/packages/core/src/metadata/memory.js +270 -0
- package/packages/core/src/metadata/sqlite.js +412 -0
- package/packages/core/src/notifications/index.js +139 -0
- package/packages/core/src/notifications/webpush.js +217 -0
- package/packages/core/src/plugins/contributions.js +177 -0
- package/packages/core/src/plugins/identity.js +98 -0
- package/packages/core/src/plugins/index.js +225 -0
- package/packages/core/src/plugins/indexers.js +142 -0
- package/packages/core/src/plugins/installStore.js +134 -0
- package/packages/core/src/plugins/package.js +102 -0
- package/packages/core/src/plugins/packageStore.js +61 -0
- package/packages/core/src/plugins/runtime.js +101 -0
- package/packages/core/src/plugins/sql.js +52 -0
- package/packages/core/src/retry.js +74 -0
- package/packages/core/src/scan.js +302 -0
- package/packages/core/src/search/embeddings.js +128 -0
- package/packages/core/src/search/index.js +200 -0
- package/packages/core/src/search/keywordStore.js +107 -0
- package/packages/core/src/search/sqliteStores.js +455 -0
- package/packages/core/src/search/tagMatch.js +59 -0
- package/packages/core/src/search/transformer.js +195 -0
- package/packages/core/src/search/vectorStore.js +274 -0
- package/packages/core/src/search/vectorize.js +249 -0
- package/packages/core/src/sidecar/document.js +213 -0
- package/packages/core/src/sidecar/index.js +174 -0
- package/packages/core/src/sidecar/manager.js +239 -0
- package/packages/core/src/sidecar/store.js +46 -0
- package/packages/core/src/signedUrls.js +170 -0
- package/packages/core/src/sqlite-d1.js +162 -0
- package/packages/core/src/sqlite-driver.js +42 -0
- package/packages/core/src/sqlite.js +162 -0
- package/packages/core/src/storage/filesystem.js +283 -0
- package/packages/core/src/storage/interface.js +222 -0
- package/packages/core/src/storage/memory.js +113 -0
- package/packages/core/src/storage/prefixed.js +75 -0
- package/packages/core/src/storage/s3.js +316 -0
- package/packages/core/src/storage/s3sigv4.js +185 -0
- package/packages/core/src/tasks.js +228 -0
- package/packages/core/src/uploads.js +386 -0
- package/packages/core/src/util.js +125 -0
- package/packages/core/src/vfs.js +666 -0
- package/packages/plugin-sdk/src/browser.js +316 -0
- package/packages/plugin-sdk/src/index.js +32 -0
- package/packages/plugin-sdk/src/protocol.js +59 -0
- package/packages/plugin-sdk/src/rpc.js +95 -0
- package/packages/server/src/adapters/bun.js +78 -0
- package/packages/server/src/adapters/node.js +115 -0
- package/packages/server/src/adapters/staticAssets.js +123 -0
- package/packages/server/src/adapters/webDist.js +70 -0
- package/packages/server/src/adapters/worker-tasks.js +206 -0
- package/packages/server/src/adapters/worker.js +159 -0
- package/packages/server/src/cachePolicy.js +34 -0
- package/packages/server/src/engine/README.md +88 -0
- package/packages/server/src/engine/actions/scanCollection.js +114 -0
- package/packages/server/src/engine/index.js +95 -0
- package/packages/server/src/engine/lazy.js +25 -0
- package/packages/server/src/engine/providers/access.js +363 -0
- package/packages/server/src/engine/providers/core.js +405 -0
- package/packages/server/src/engine/providers/scan.js +67 -0
- package/packages/server/src/index.js +698 -0
- package/packages/server/src/manifest.js +98 -0
- package/packages/server/src/mcp/auth.js +40 -0
- package/packages/server/src/mcp/index.js +213 -0
- package/packages/server/src/mcp/protocol.js +181 -0
- package/packages/server/src/mcp/tools.js +351 -0
- package/packages/server/src/router.js +229 -0
- package/packages/server/src/routes.js +1066 -0
- package/packages/server/src/scope.js +43 -0
- package/packages/web/dist/assets/chunk-4xqbzebh.js +5 -0
- package/packages/web/dist/assets/chunk-4xqbzebh.js.map +9 -0
- package/packages/web/dist/assets/chunk-h05bxfbs.js +5 -0
- package/packages/web/dist/assets/chunk-h05bxfbs.js.map +10 -0
- package/packages/web/dist/assets/main-4cxs7prw.js +356 -0
- package/packages/web/dist/assets/main-4cxs7prw.js.map +103 -0
- package/packages/web/dist/assets/styles-kcx1x337.css +1 -0
- package/packages/web/dist/icon.svg +11 -0
- package/packages/web/dist/index.html +16 -0
- package/packages/web/dist/sql-wasm.wasm +0 -0
- package/packages/web/dist/sw.js +186 -0
- package/packages/web/src/bl/actions.js +410 -0
- package/packages/web/src/bl/activity.js +306 -0
- package/packages/web/src/bl/commands.js +274 -0
- package/packages/web/src/bl/fileType.js +49 -0
- package/packages/web/src/bl/index.js +70 -0
- package/packages/web/src/bl/links.js +54 -0
- package/packages/web/src/bl/offline.js +268 -0
- package/packages/web/src/bl/openers.js +71 -0
- package/packages/web/src/bl/pluginInstall.js +59 -0
- package/packages/web/src/bl/services.js +143 -0
- package/packages/web/src/bl/social.js +234 -0
- package/packages/web/src/bl/tagQuery.js +44 -0
- package/packages/web/src/main.js +10 -0
- package/packages/web/src/platform/api.js +529 -0
- package/packages/web/src/platform/commands.js +89 -0
- package/packages/web/src/platform/context.js +77 -0
- package/packages/web/src/platform/contributions.js +156 -0
- package/packages/web/src/platform/index.js +150 -0
- package/packages/web/src/platform/keybindings.js +199 -0
- package/packages/web/src/platform/mediaUrls.js +137 -0
- package/packages/web/src/platform/navigation.js +131 -0
- package/packages/web/src/platform/notifications.js +50 -0
- package/packages/web/src/platform/overlay.js +81 -0
- package/packages/web/src/platform/pluginClientDb.js +132 -0
- package/packages/web/src/platform/pluginDock.js +141 -0
- package/packages/web/src/platform/pluginFrames.js +194 -0
- package/packages/web/src/platform/pluginHost.js +648 -0
- package/packages/web/src/platform/pluginMedia.js +62 -0
- package/packages/web/src/platform/pluginModules.js +90 -0
- package/packages/web/src/platform/pluginNet.js +71 -0
- package/packages/web/src/platform/pluginPackage.js +247 -0
- package/packages/web/src/platform/pluginRpc.js +377 -0
- package/packages/web/src/platform/pluginSigning.js +168 -0
- package/packages/web/src/platform/pluginStore.js +67 -0
- package/packages/web/src/platform/settings.js +101 -0
- package/packages/web/src/platform/spatialNav.js +286 -0
- package/packages/web/src/platform/viewport.js +123 -0
- package/packages/web/src/platform/voice.js +133 -0
- package/packages/web/src/platform/voiceSearch.js +155 -0
- package/packages/web/src/platform/whenclause.js +162 -0
- package/packages/web/src/platform/workbench.js +156 -0
- package/packages/web/src/runtime.js +73 -0
- package/packages/web/src/styles.css +1382 -0
- package/packages/web/src/ui/components/activityBar.js +35 -0
- package/packages/web/src/ui/components/activityPanel.js +132 -0
- package/packages/web/src/ui/components/commandPalette.js +154 -0
- package/packages/web/src/ui/components/editorArea.js +75 -0
- package/packages/web/src/ui/components/launcher.js +392 -0
- package/packages/web/src/ui/components/openers/index.js +212 -0
- package/packages/web/src/ui/components/openers/markdown.js +222 -0
- package/packages/web/src/ui/components/overlays.js +255 -0
- package/packages/web/src/ui/components/phoneChrome.js +188 -0
- package/packages/web/src/ui/components/pluginReview.js +151 -0
- package/packages/web/src/ui/components/pluginsView.js +120 -0
- package/packages/web/src/ui/components/settingsView.js +258 -0
- package/packages/web/src/ui/components/social.js +290 -0
- package/packages/web/src/ui/components/statusBar.js +198 -0
- package/packages/web/src/ui/components/views/grid.js +115 -0
- package/packages/web/src/ui/components/views/index.js +155 -0
- package/packages/web/src/ui/components/views/list.js +50 -0
- package/packages/web/src/ui/components/views/parts.js +58 -0
- package/packages/web/src/ui/compositions/workbench.js +125 -0
- package/packages/web/src/ui/format.js +33 -0
- package/packages/web/src/ui/icon.js +81 -0
- package/packages/web/src/ui/media.js +114 -0
- package/packages/web/src/ui/sanitize.js +86 -0
- package/packages/web/src/workbench.js +205 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// SearchService — orchestrates hybrid semantic + keyword search over two
|
|
2
|
+
// PLUGGABLE, async stores: a VectorStore (dense) and a KeywordStore (sparse).
|
|
3
|
+
// It owns neither implementation — you inject them (defaults are in-memory), so
|
|
4
|
+
// a deployment can point vectors at Qdrant/pgvector and keywords at Postgres FTS
|
|
5
|
+
// while core stays platform-agnostic. The embedding provider is injected too.
|
|
6
|
+
//
|
|
7
|
+
// Documents enter three ways, all namespaced by indexerId:
|
|
8
|
+
// 1. Server indexers (IndexerRegistry) — run on upload, text extracted here.
|
|
9
|
+
// 2. Plugin indexers — push { text } or precomputed { vector } via the API.
|
|
10
|
+
// 3. Name index — every node's name/path stays keyword-searchable.
|
|
11
|
+
//
|
|
12
|
+
// Results are grouped by node, scored by best chunk, and returned with a snippet.
|
|
13
|
+
|
|
14
|
+
import { MemoryVectorStore } from './vectorStore.js';
|
|
15
|
+
import { MemoryKeywordStore, tokenize } from './keywordStore.js';
|
|
16
|
+
import { TroveError } from '../errors.js';
|
|
17
|
+
|
|
18
|
+
export class SearchService {
|
|
19
|
+
/**
|
|
20
|
+
* @param {object} deps
|
|
21
|
+
* @param {import('./embeddings.js').EmbeddingProvider} deps.embeddings
|
|
22
|
+
* @param {import('./vectorStore.js').VectorStore} [deps.vectorStore] injected; default in-memory
|
|
23
|
+
* @param {import('./keywordStore.js').KeywordStore} [deps.keywordStore] injected; default in-memory
|
|
24
|
+
* @param {number} [deps.semanticWeight] 0..1 blend (default 0.7 dense / 0.3 lexical)
|
|
25
|
+
*/
|
|
26
|
+
constructor({ embeddings, vectorStore, keywordStore, semanticWeight = 0.7 }) {
|
|
27
|
+
if (!embeddings) throw TroveError.invalid('SearchService requires an embeddings provider');
|
|
28
|
+
this.embeddings = embeddings;
|
|
29
|
+
this.vectors = vectorStore ?? new MemoryVectorStore({ dimensions: embeddings.dimensions });
|
|
30
|
+
this.keywords = keywordStore ?? new MemoryKeywordStore();
|
|
31
|
+
this.semanticWeight = semanticWeight;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A wire-safe description of the configured backends (for /api/capabilities), so the
|
|
35
|
+
* route doesn't reach into internal class identities. */
|
|
36
|
+
describe() {
|
|
37
|
+
return {
|
|
38
|
+
vectorStore: this.vectors?.constructor?.name || null,
|
|
39
|
+
keywordStore: this.keywords?.constructor?.name || null,
|
|
40
|
+
embeddings: this.embeddings?.constructor?.name || null,
|
|
41
|
+
dimensions: this.vectors?.dimensions || null,
|
|
42
|
+
// Whether the index survives a restart. Stated outright rather than left to be
|
|
43
|
+
// inferred from a class name, because "your search index is rebuilt from scratch
|
|
44
|
+
// every restart" is an operational fact an admin should be able to read off
|
|
45
|
+
// /api/capabilities without knowing which store maps to which guarantee.
|
|
46
|
+
durable: this.vectors instanceof MemoryVectorStore || this.keywords instanceof MemoryKeywordStore ? false : true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Index documents for a node under one indexer namespace. Replaces prior docs
|
|
52
|
+
* from the same (nodeId, indexerId) so re-indexing is clean. Accepts { text }
|
|
53
|
+
* (embedded here) or { vector } (precomputed by a plugin).
|
|
54
|
+
*/
|
|
55
|
+
async indexDocuments(nodeId, indexerId, documents) {
|
|
56
|
+
if (!documents?.length) {
|
|
57
|
+
await Promise.all([
|
|
58
|
+
this.vectors.removeByNodeIndexer(nodeId, indexerId),
|
|
59
|
+
this.keywords.removeByNodeIndexer(nodeId, indexerId),
|
|
60
|
+
]);
|
|
61
|
+
return { indexed: 0 };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Embed BEFORE removing anything. Both removals used to run first, so a transient
|
|
65
|
+
// embedding outage during a re-index left the file with zero vectors and zero
|
|
66
|
+
// keywords — recoverable through the issue, but unfindable in the meantime, and the
|
|
67
|
+
// old index was perfectly good until we threw it away.
|
|
68
|
+
const needEmbedding = documents.filter((d) => !d.vector && d.text);
|
|
69
|
+
const embedded = needEmbedding.length ? await this.embeddings.embed(needEmbedding.map((d) => d.text)) : [];
|
|
70
|
+
await Promise.all([
|
|
71
|
+
this.vectors.removeByNodeIndexer(nodeId, indexerId),
|
|
72
|
+
this.keywords.removeByNodeIndexer(nodeId, indexerId),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
const vectorDocs = [];
|
|
76
|
+
const keywordDocs = [];
|
|
77
|
+
let vi = 0;
|
|
78
|
+
let counter = 0;
|
|
79
|
+
for (const d of documents) {
|
|
80
|
+
const docId = d.id || `${indexerId}:${nodeId}:${counter++}`;
|
|
81
|
+
const vector = d.vector || (d.text ? embedded[vi++] : null);
|
|
82
|
+
if (vector) {
|
|
83
|
+
if (this.vectors.dimensions && vector.length !== this.vectors.dimensions) {
|
|
84
|
+
throw TroveError.invalid(`Document vector dim ${vector.length} != index dim ${this.vectors.dimensions}`);
|
|
85
|
+
}
|
|
86
|
+
vectorDocs.push({ id: docId, nodeId, indexerId, vector, fields: d.fields });
|
|
87
|
+
}
|
|
88
|
+
keywordDocs.push({ id: docId, nodeId, indexerId, text: d.text || '', fields: d.fields || {} });
|
|
89
|
+
}
|
|
90
|
+
await Promise.all([
|
|
91
|
+
vectorDocs.length ? this.vectors.add(vectorDocs) : null,
|
|
92
|
+
this.keywords.add(keywordDocs),
|
|
93
|
+
]);
|
|
94
|
+
return { indexed: documents.length };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Keep names/paths searchable regardless of content indexers. */
|
|
98
|
+
async indexName(node) {
|
|
99
|
+
await this.keywords.removeByNodeIndexer(node.id, 'core.name');
|
|
100
|
+
await this.keywords.add([{ id: `name:${node.id}`, nodeId: node.id, indexerId: 'core.name', text: node.name, fields: { name: node.name, collectionId: node.collectionId } }]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Does at least one configured store hold nothing?
|
|
105
|
+
*
|
|
106
|
+
* This is the startup signal for "the index was lost" — a drive with files always
|
|
107
|
+
* has at least one keyword doc per node (indexName writes one), so an empty store
|
|
108
|
+
* beside a non-empty metadata store means the index needs rebuilding. Asked per
|
|
109
|
+
* store rather than of the pair, because a deployment can persist vectors and not
|
|
110
|
+
* keywords (or the reverse), and half a search index is still a broken one.
|
|
111
|
+
*
|
|
112
|
+
* @returns {Promise<boolean|null>} null when neither store can report a count — a
|
|
113
|
+
* store that can't say is never taken as evidence that a rebuild is needed.
|
|
114
|
+
*/
|
|
115
|
+
async looksUnindexed() {
|
|
116
|
+
const [vectors, keywords] = await Promise.all([safeCount(this.vectors), safeCount(this.keywords)]);
|
|
117
|
+
if (vectors == null && keywords == null) return null;
|
|
118
|
+
return (vectors ?? 1) === 0 || (keywords ?? 1) === 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async removeNode(nodeId) {
|
|
122
|
+
await Promise.all([this.vectors.removeByNode(nodeId), this.keywords.removeByNode(nodeId)]);
|
|
123
|
+
}
|
|
124
|
+
async removeIndexer(indexerId) {
|
|
125
|
+
await Promise.all([this.vectors.removeByIndexer(indexerId), this.keywords.removeByIndexer(indexerId)]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {string} query
|
|
130
|
+
* @param {{limit?: number, indexers?: string[], mode?: 'hybrid'|'semantic'|'keyword'}} [opts]
|
|
131
|
+
* @returns {Promise<Array<{nodeId, score, indexerId, snippet, fields}>>}
|
|
132
|
+
*/
|
|
133
|
+
async search(query, opts = {}) {
|
|
134
|
+
const limit = opts.limit ?? 20;
|
|
135
|
+
const mode = opts.mode ?? 'hybrid';
|
|
136
|
+
const perNode = new Map();
|
|
137
|
+
|
|
138
|
+
// Dense / semantic and sparse / lexical run concurrently.
|
|
139
|
+
const [dense, sparse] = await Promise.all([
|
|
140
|
+
mode === 'keyword' ? [] : this.#dense(query, limit, opts.indexers),
|
|
141
|
+
mode === 'semantic' ? [] : this.keywords.search(query, { limit: limit * 4, indexers: opts.indexers }),
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
const semanticW = mode === 'semantic' ? 1 : this.semanticWeight;
|
|
145
|
+
const lexicalW = mode === 'keyword' ? 1 : 1 - this.semanticWeight;
|
|
146
|
+
|
|
147
|
+
for (const r of dense) accumulate(perNode, r.nodeId, r.score * semanticW, r.indexerId, r.docId, r.fields);
|
|
148
|
+
for (const r of sparse) accumulate(perNode, r.nodeId, r.score * lexicalW, r.indexerId, r.docId, r.fields);
|
|
149
|
+
|
|
150
|
+
const ranked = [...perNode.values()].sort((a, b) => b.score - a.score).slice(0, limit);
|
|
151
|
+
// Attach snippets (best-effort; keyword store owns the text).
|
|
152
|
+
for (const r of ranked) r.snippet = await this.keywords.snippet(r.docId, query);
|
|
153
|
+
return ranked.map(({ docId, best, ...rest }) => rest);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async #dense(query, limit, indexers) {
|
|
157
|
+
const qv = await this.embeddings.embedOne(query);
|
|
158
|
+
return this.vectors.query(qv, { limit: limit * 4, indexers });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function safeCount(store) {
|
|
163
|
+
try { return (await store?.count?.()) ?? null; } catch { return null; }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Fold one document's hit into its node's running score.
|
|
168
|
+
*
|
|
169
|
+
* A node can be hit by several documents and by both channels (dense and sparse), and
|
|
170
|
+
* the score has to grow with that rather than being replaced by whichever hit arrived
|
|
171
|
+
* with the larger number. The old form assigned `cur.score = score` on the better hit,
|
|
172
|
+
* DISCARDING everything accumulated so far — so a file matching both channels could
|
|
173
|
+
* rank below one matching only lexically, which is the opposite of what hybrid search
|
|
174
|
+
* is for. It also moved `docId`/`indexerId` without moving `fields`, so the snippet
|
|
175
|
+
* came from one chunk and the fields from another.
|
|
176
|
+
*
|
|
177
|
+
* Now: the score is the best hit plus a small bonus for each additional one, and the
|
|
178
|
+
* document a result POINTS at is always the best-scoring one, with its own fields.
|
|
179
|
+
*/
|
|
180
|
+
function accumulate(map, nodeId, score, indexerId, docId, fields) {
|
|
181
|
+
const cur = map.get(nodeId);
|
|
182
|
+
if (!cur) {
|
|
183
|
+
map.set(nodeId, { nodeId, score, best: score, indexerId, docId, fields });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// Every additional matching chunk adds a little — matching in ten places is more
|
|
187
|
+
// relevant than matching in one.
|
|
188
|
+
cur.score += score * 0.1;
|
|
189
|
+
if (score > cur.best) {
|
|
190
|
+
// A new best: raise the floor to it, keeping the bonuses already earned, and take
|
|
191
|
+
// the whole descriptor from this document so snippet and fields agree.
|
|
192
|
+
cur.score += score - cur.best;
|
|
193
|
+
cur.best = score;
|
|
194
|
+
cur.indexerId = indexerId;
|
|
195
|
+
cur.docId = docId;
|
|
196
|
+
cur.fields = fields;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export { tokenize };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// KeywordStore — the pluggable lexical (sparse) half of hybrid search, mirroring
|
|
2
|
+
// VectorStore's async, injectable shape. The in-memory default keeps a token set
|
|
3
|
+
// per document and scores by query-term coverage; swap in an implementation over
|
|
4
|
+
// SQLite FTS5, Postgres tsvector, Elasticsearch, etc. for durability/scale.
|
|
5
|
+
// SearchService composes a VectorStore + a KeywordStore, so a deployment can mix
|
|
6
|
+
// (e.g. Qdrant for vectors, Postgres FTS for keywords) — core hardcodes neither.
|
|
7
|
+
|
|
8
|
+
import { TroveError } from '../errors.js';
|
|
9
|
+
|
|
10
|
+
const STOP = new Set('a an the of to in on for and or is are be as at by with from this that it'.split(' '));
|
|
11
|
+
|
|
12
|
+
export function tokenize(text) {
|
|
13
|
+
return String(text)
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
16
|
+
.split(/\s+/)
|
|
17
|
+
.filter((t) => t.length > 1 && !STOP.has(t));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class KeywordStore {
|
|
21
|
+
async add(docs) {
|
|
22
|
+
throw TroveError.unsupported('add not implemented');
|
|
23
|
+
}
|
|
24
|
+
async removeByNode(nodeId) {
|
|
25
|
+
throw TroveError.unsupported('removeByNode not implemented');
|
|
26
|
+
}
|
|
27
|
+
async removeByIndexer(indexerId) {
|
|
28
|
+
throw TroveError.unsupported('removeByIndexer not implemented');
|
|
29
|
+
}
|
|
30
|
+
async removeByNodeIndexer(nodeId, indexerId) {
|
|
31
|
+
throw TroveError.unsupported('removeByNodeIndexer not implemented');
|
|
32
|
+
}
|
|
33
|
+
/** @returns {Promise<Array<{docId,nodeId,indexerId,score,fields}>>} */
|
|
34
|
+
async search(query, opts) {
|
|
35
|
+
throw TroveError.unsupported('search not implemented');
|
|
36
|
+
}
|
|
37
|
+
/** Optional: a highlighted excerpt for a doc (null if unavailable). */
|
|
38
|
+
async snippet(docId, query) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
/** Optional: number of stored documents. Null when the store can't say — which is
|
|
42
|
+
* not the same as zero, and the startup rebuild check depends on the difference. */
|
|
43
|
+
async count() {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class MemoryKeywordStore extends KeywordStore {
|
|
49
|
+
constructor() {
|
|
50
|
+
super();
|
|
51
|
+
this.docs = new Map(); // docId -> { nodeId, indexerId, text, tokens:Set, fields }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async add(docs) {
|
|
55
|
+
for (const d of docs) {
|
|
56
|
+
const text = d.text || '';
|
|
57
|
+
this.docs.set(d.id, {
|
|
58
|
+
nodeId: d.nodeId, indexerId: d.indexerId, text,
|
|
59
|
+
tokens: new Set(tokenize(text + ' ' + Object.values(d.fields || {}).join(' '))),
|
|
60
|
+
fields: d.fields || {},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async removeByNode(nodeId) {
|
|
65
|
+
for (const [id, r] of this.docs) if (r.nodeId === nodeId) this.docs.delete(id);
|
|
66
|
+
}
|
|
67
|
+
async removeByIndexer(indexerId) {
|
|
68
|
+
for (const [id, r] of this.docs) if (r.indexerId === indexerId) this.docs.delete(id);
|
|
69
|
+
}
|
|
70
|
+
async removeByNodeIndexer(nodeId, indexerId) {
|
|
71
|
+
for (const [id, r] of this.docs) if (r.nodeId === nodeId && r.indexerId === indexerId) this.docs.delete(id);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async search(query, opts = {}) {
|
|
75
|
+
const q = tokenize(query);
|
|
76
|
+
if (!q.length) return [];
|
|
77
|
+
const allow = opts.indexers ? new Set(opts.indexers) : null;
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const [docId, r] of this.docs) {
|
|
80
|
+
if (allow && !allow.has(r.indexerId)) continue;
|
|
81
|
+
let hit = 0;
|
|
82
|
+
for (const t of q) if (r.tokens.has(t)) hit++;
|
|
83
|
+
if (hit > 0) out.push({ docId, nodeId: r.nodeId, indexerId: r.indexerId, score: hit / q.length, fields: r.fields });
|
|
84
|
+
}
|
|
85
|
+
out.sort((a, b) => b.score - a.score);
|
|
86
|
+
return out.slice(0, opts.limit ?? 40);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async snippet(docId, query) {
|
|
90
|
+
const r = this.docs.get(docId);
|
|
91
|
+
if (!r?.text) return null;
|
|
92
|
+
const q = tokenize(query);
|
|
93
|
+
const lower = r.text.toLowerCase();
|
|
94
|
+
let at = -1;
|
|
95
|
+
for (const t of q) {
|
|
96
|
+
const i = lower.indexOf(t);
|
|
97
|
+
if (i >= 0) { at = i; break; }
|
|
98
|
+
}
|
|
99
|
+
if (at < 0) return r.text.slice(0, 160).trim();
|
|
100
|
+
const start = Math.max(0, at - 60);
|
|
101
|
+
return (start > 0 ? '…' : '') + r.text.slice(start, start + 200).trim() + '…';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async count() {
|
|
105
|
+
return this.docs.size;
|
|
106
|
+
}
|
|
107
|
+
}
|