@lojban/semantic-search-mcp 1.0.7 → 1.0.9
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 +10 -0
- package/package.json +1 -1
- package/src/index.ts +82 -27
package/README.md
CHANGED
|
@@ -101,6 +101,16 @@ To replace the entire index with new content from several places:
|
|
|
101
101
|
|
|
102
102
|
Paths can be anywhere on disk (e.g. different drives or projects); the server reads and indexes all supported text/TSV/CSV files under each directory recursively.
|
|
103
103
|
|
|
104
|
+
### Memory and batch size
|
|
105
|
+
|
|
106
|
+
Indexing uses **adaptive batch size** based on free system RAM so the OS doesn’t freeze on low-memory machines. The server reads `os.freemem()`, keeps a reserve (default 400MB), and caps batch size between 32 and 512 lines. You can tune this with env vars:
|
|
107
|
+
|
|
108
|
+
- **`SEMANTIC_SEARCH_RESERVE_MB`** — MB of RAM to keep free (default `400`).
|
|
109
|
+
- **`SEMANTIC_SEARCH_MIN_BATCH`** — minimum lines per batch (default `32`).
|
|
110
|
+
- **`SEMANTIC_SEARCH_MAX_BATCH`** — maximum lines per batch (default `512`).
|
|
111
|
+
|
|
112
|
+
Example: `SEMANTIC_SEARCH_RESERVE_MB=800 SEMANTIC_SEARCH_MAX_BATCH=256` to leave more headroom and use smaller batches.
|
|
113
|
+
|
|
104
114
|
## Example: Lojban dictionary gaps
|
|
105
115
|
|
|
106
116
|
1. Put your dictionary TSV (e.g. `jbo-eng.tsv`) in a folder.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
CallToolRequestSchema,
|
|
6
6
|
ListToolsRequestSchema,
|
|
7
7
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
+
import os from 'node:os';
|
|
8
9
|
import path from 'path';
|
|
9
10
|
import { getEmbedding, getBatchEmbeddings } from './embeddings.js';
|
|
10
11
|
import { createVectorStorage, type SearchResult, type VectorStorage } from './storage.js';
|
|
@@ -36,17 +37,51 @@ const indexStatus: IndexStatus = {
|
|
|
36
37
|
directories: [],
|
|
37
38
|
};
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
// Single "mutex": only one indexing job is allowed to run. Starting a new job aborts the previous one.
|
|
41
|
+
let currentIndexingAbortController: AbortController | null = null;
|
|
42
|
+
let currentJobId = 0;
|
|
43
|
+
|
|
44
|
+
// Adaptive batch size: reserve RAM so we don't freeze the OS (env overrides in bytes or MB)
|
|
45
|
+
const RESERVE_MB = Number(process.env.SEMANTIC_SEARCH_RESERVE_MB) || 400;
|
|
46
|
+
const RESERVE_BYTES = RESERVE_MB * 1024 * 1024;
|
|
47
|
+
const MIN_BATCH = Number(process.env.SEMANTIC_SEARCH_MIN_BATCH) || 32;
|
|
48
|
+
const MAX_BATCH = Number(process.env.SEMANTIC_SEARCH_MAX_BATCH) || 512;
|
|
49
|
+
|
|
50
|
+
/** Rough bytes per indexed line in memory: line text + path + embedding (384 floats) + overhead */
|
|
51
|
+
const BYTES_PER_LINE_ESTIMATE = 4000;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Compute batch size from current free system RAM. Keeps reserve free to avoid freezing the OS.
|
|
55
|
+
*/
|
|
56
|
+
function getAdaptiveBatchSize(): number {
|
|
57
|
+
const free = os.freemem();
|
|
58
|
+
const available = free > RESERVE_BYTES ? free - RESERVE_BYTES : Math.floor(free / 2);
|
|
59
|
+
const batch = Math.floor(available / BYTES_PER_LINE_ESTIMATE);
|
|
60
|
+
const clamped = Math.max(MIN_BATCH, Math.min(MAX_BATCH, batch));
|
|
61
|
+
return clamped;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Request indexing of directories. If another indexing job is running, it is aborted first.
|
|
66
|
+
* Then a new job is started (clears index and rebuilds).
|
|
67
|
+
*/
|
|
68
|
+
function requestIndexing(storage: VectorStorage, directories: string[]): void {
|
|
40
69
|
if (!directories.length) {
|
|
41
70
|
console.error('No directories to index. Set SEMANTIC_SEARCH_INDEX_DIRS (comma-separated paths).');
|
|
42
71
|
return;
|
|
43
72
|
}
|
|
44
73
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
74
|
+
// Abort any in-progress indexing so it doesn't conflict or flush this job's work.
|
|
75
|
+
if (currentIndexingAbortController) {
|
|
76
|
+
currentIndexingAbortController.abort();
|
|
77
|
+
currentIndexingAbortController = null;
|
|
48
78
|
}
|
|
49
79
|
|
|
80
|
+
currentJobId += 1;
|
|
81
|
+
const jobId = currentJobId;
|
|
82
|
+
currentIndexingAbortController = new AbortController();
|
|
83
|
+
const signal = currentIndexingAbortController.signal;
|
|
84
|
+
|
|
50
85
|
indexStatus.isIndexing = true;
|
|
51
86
|
indexStatus.startedAt = Date.now();
|
|
52
87
|
indexStatus.finishedAt = null;
|
|
@@ -55,13 +90,24 @@ async function startIndexing(storage: VectorStorage, directories: string[]): Pro
|
|
|
55
90
|
indexStatus.indexedLines = 0;
|
|
56
91
|
indexStatus.indexedFiles = 0;
|
|
57
92
|
|
|
93
|
+
void startIndexing(storage, directories, signal, jobId);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function startIndexing(
|
|
97
|
+
storage: VectorStorage,
|
|
98
|
+
directories: string[],
|
|
99
|
+
signal: AbortSignal,
|
|
100
|
+
jobId: number
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
const isCurrentJob = (): boolean => currentJobId === jobId;
|
|
103
|
+
|
|
58
104
|
try {
|
|
59
|
-
|
|
105
|
+
if (signal.aborted) return;
|
|
60
106
|
|
|
107
|
+
storage.clear();
|
|
61
108
|
console.error(`Scanning ${directories.length} directories (background indexing)...`);
|
|
62
109
|
|
|
63
110
|
let indexedCount = 0;
|
|
64
|
-
const BATCH_SIZE = 512;
|
|
65
111
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
112
|
let currentBatch: any[] = [];
|
|
67
113
|
|
|
@@ -79,43 +125,48 @@ async function startIndexing(storage: VectorStorage, directories: string[]): Pro
|
|
|
79
125
|
|
|
80
126
|
await storage.upsertLinesBatch(batchData);
|
|
81
127
|
indexedCount += batchToProcess.length;
|
|
82
|
-
indexStatus.indexedLines = indexedCount;
|
|
128
|
+
if (isCurrentJob()) indexStatus.indexedLines = indexedCount;
|
|
83
129
|
console.error(`Indexed ${indexedCount} lines...`);
|
|
84
130
|
};
|
|
85
131
|
|
|
86
|
-
// Pipelining: Read next batch while processing current batch
|
|
87
|
-
// We allow ONE batch to be processed in parallel with reading the next one.
|
|
88
132
|
let processingPromise: Promise<void> | null = null;
|
|
133
|
+
let batchSize = getAdaptiveBatchSize();
|
|
134
|
+
console.error(`Adaptive batch size: ${batchSize} (free RAM: ${Math.round(os.freemem() / 1024 / 1024)}MB, reserve: ${RESERVE_MB}MB)`);
|
|
89
135
|
|
|
90
136
|
for await (const line of scanDirectories(directories)) {
|
|
137
|
+
if (signal.aborted) break;
|
|
138
|
+
|
|
91
139
|
currentBatch.push(line);
|
|
92
|
-
if (currentBatch.length >=
|
|
93
|
-
// If there's a previous batch still processing, wait for it
|
|
140
|
+
if (currentBatch.length >= batchSize) {
|
|
94
141
|
if (processingPromise) {
|
|
95
142
|
await processingPromise;
|
|
96
143
|
}
|
|
144
|
+
if (signal.aborted) break;
|
|
97
145
|
|
|
98
146
|
const batchToProcess = currentBatch;
|
|
99
147
|
currentBatch = [];
|
|
148
|
+
batchSize = getAdaptiveBatchSize();
|
|
100
149
|
|
|
101
|
-
// Start processing this batch, but don't await it yet!
|
|
102
|
-
// This allows the loop to continue and read the next batch from disk.
|
|
103
150
|
processingPromise = processBatch(batchToProcess).catch((err) => {
|
|
104
151
|
console.error('Error in background batch processing:', err);
|
|
105
152
|
});
|
|
106
153
|
}
|
|
107
154
|
}
|
|
108
155
|
|
|
109
|
-
|
|
156
|
+
if (signal.aborted) {
|
|
157
|
+
console.error('Indexing aborted (new job started or cancelled).');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
110
161
|
if (processingPromise) {
|
|
111
162
|
await processingPromise;
|
|
112
163
|
}
|
|
113
|
-
|
|
114
|
-
// Process any remaining lines
|
|
115
164
|
if (currentBatch.length > 0) {
|
|
116
165
|
await processBatch(currentBatch);
|
|
117
166
|
}
|
|
118
167
|
|
|
168
|
+
if (!isCurrentJob()) return;
|
|
169
|
+
|
|
119
170
|
const stats = await storage.getStats();
|
|
120
171
|
indexStatus.indexedFiles = stats.totalFiles;
|
|
121
172
|
indexStatus.indexedLines = stats.totalLines;
|
|
@@ -126,15 +177,22 @@ async function startIndexing(storage: VectorStorage, directories: string[]): Pro
|
|
|
126
177
|
);
|
|
127
178
|
} catch (err) {
|
|
128
179
|
const message = err instanceof Error ? err.message : String(err);
|
|
129
|
-
|
|
130
|
-
|
|
180
|
+
if (isCurrentJob()) {
|
|
181
|
+
indexStatus.lastError = message;
|
|
182
|
+
indexStatus.finishedAt = Date.now();
|
|
183
|
+
}
|
|
131
184
|
console.error('Error during indexing job:', err);
|
|
132
185
|
} finally {
|
|
133
|
-
|
|
186
|
+
if (isCurrentJob()) {
|
|
187
|
+
indexStatus.isIndexing = false;
|
|
188
|
+
}
|
|
189
|
+
if (currentIndexingAbortController && currentJobId === jobId) {
|
|
190
|
+
currentIndexingAbortController = null;
|
|
191
|
+
}
|
|
134
192
|
}
|
|
135
193
|
}
|
|
136
194
|
|
|
137
|
-
|
|
195
|
+
function ensureInitialIndexing(storage: VectorStorage): void {
|
|
138
196
|
const envDirs = process.env.SEMANTIC_SEARCH_INDEX_DIRS;
|
|
139
197
|
const directories = envDirs ? envDirs.split(',').map((d) => d.trim()).filter(Boolean) : [];
|
|
140
198
|
|
|
@@ -145,8 +203,7 @@ async function ensureInitialIndexing(storage: VectorStorage): Promise<void> {
|
|
|
145
203
|
return;
|
|
146
204
|
}
|
|
147
205
|
|
|
148
|
-
|
|
149
|
-
void startIndexing(storage, directories);
|
|
206
|
+
requestIndexing(storage, directories);
|
|
150
207
|
}
|
|
151
208
|
|
|
152
209
|
async function main() {
|
|
@@ -222,10 +279,8 @@ async function main() {
|
|
|
222
279
|
);
|
|
223
280
|
}
|
|
224
281
|
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
void startIndexing(storage, directories);
|
|
228
|
-
}
|
|
282
|
+
// Abort any in-progress indexing and start a new job (clears and rebuilds).
|
|
283
|
+
requestIndexing(storage, directories);
|
|
229
284
|
|
|
230
285
|
const stats = await storage.getStats();
|
|
231
286
|
return {
|
|
@@ -313,7 +368,7 @@ async function main() {
|
|
|
313
368
|
console.error('Semantic Search MCP Server running on stdio');
|
|
314
369
|
|
|
315
370
|
// Kick off initial background indexing when the MCP server is enabled.
|
|
316
|
-
|
|
371
|
+
ensureInitialIndexing(storage);
|
|
317
372
|
}
|
|
318
373
|
|
|
319
374
|
main().catch(console.error);
|