@lojban/semantic-search-mcp 1.0.8 → 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 +25 -2
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';
|
|
@@ -40,6 +41,26 @@ const indexStatus: IndexStatus = {
|
|
|
40
41
|
let currentIndexingAbortController: AbortController | null = null;
|
|
41
42
|
let currentJobId = 0;
|
|
42
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
|
+
|
|
43
64
|
/**
|
|
44
65
|
* Request indexing of directories. If another indexing job is running, it is aborted first.
|
|
45
66
|
* Then a new job is started (clears index and rebuilds).
|
|
@@ -87,7 +108,6 @@ async function startIndexing(
|
|
|
87
108
|
console.error(`Scanning ${directories.length} directories (background indexing)...`);
|
|
88
109
|
|
|
89
110
|
let indexedCount = 0;
|
|
90
|
-
const BATCH_SIZE = 512;
|
|
91
111
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
112
|
let currentBatch: any[] = [];
|
|
93
113
|
|
|
@@ -110,12 +130,14 @@ async function startIndexing(
|
|
|
110
130
|
};
|
|
111
131
|
|
|
112
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)`);
|
|
113
135
|
|
|
114
136
|
for await (const line of scanDirectories(directories)) {
|
|
115
137
|
if (signal.aborted) break;
|
|
116
138
|
|
|
117
139
|
currentBatch.push(line);
|
|
118
|
-
if (currentBatch.length >=
|
|
140
|
+
if (currentBatch.length >= batchSize) {
|
|
119
141
|
if (processingPromise) {
|
|
120
142
|
await processingPromise;
|
|
121
143
|
}
|
|
@@ -123,6 +145,7 @@ async function startIndexing(
|
|
|
123
145
|
|
|
124
146
|
const batchToProcess = currentBatch;
|
|
125
147
|
currentBatch = [];
|
|
148
|
+
batchSize = getAdaptiveBatchSize();
|
|
126
149
|
|
|
127
150
|
processingPromise = processBatch(batchToProcess).catch((err) => {
|
|
128
151
|
console.error('Error in background batch processing:', err);
|