rag-memory-epf-mcp 3.1.5 → 3.2.1
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 +6 -1
- package/dist/index.js +44 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ A **project-local RAG memory** MCP server — knowledge graph + multilingual vec
|
|
|
12
12
|
- **Project-local isolation** — each project gets its own `.memory/rag-memory.db`. Multiple projects run simultaneously without interference.
|
|
13
13
|
- **3-signal hybrid search** — vector similarity (bge-m3, 1024-dim) + FTS5 BM25 keyword matching + knowledge graph re-ranking, combined via Reciprocal Rank Fusion
|
|
14
14
|
- **100+ languages** — Korean, Chinese, Japanese, Arabic, and more. Cross-lingual search works out of the box.
|
|
15
|
+
- **Graph-aware scoring** — per-entity geometric decay (0.5^i) with hard cap prevents any single document from dominating results
|
|
15
16
|
- **27 MCP tools** — entity/relation CRUD, document pipeline, multi-hop graph traversal, export/import, temporal queries
|
|
16
17
|
- **SQLite optimized** — WAL mode, 32MB cache, 256MB mmap, FTS5 triggers, 7 indexes
|
|
17
18
|
- **MCP SDK 1.27.1** — Tool Annotations (readOnly/destructive/idempotent), latest protocol 2025-11-25
|
|
@@ -55,7 +56,7 @@ Place this `.mcp.json` in each project folder with its own `DB_FILE_PATH`. Each
|
|
|
55
56
|
| `embedChunks` | Generate 1024-dim embeddings + auto-link entities | idempotent |
|
|
56
57
|
| `embedAllEntities` | Batch embed all entities (32 parallel) | idempotent |
|
|
57
58
|
| `extractTerms` | Extract potential entity terms | — |
|
|
58
|
-
| `linkEntitiesToDocument` |
|
|
59
|
+
| `linkEntitiesToDocument` | Link entities to chunks where they actually appear (text-matched) | idempotent |
|
|
59
60
|
| `deleteDocuments` | Remove documents and associated data | destructive |
|
|
60
61
|
| `listDocuments` | View all stored documents | readOnly |
|
|
61
62
|
|
|
@@ -127,6 +128,10 @@ storeDocument(id, content, metadata)
|
|
|
127
128
|
|
|
128
129
|
## Changelog
|
|
129
130
|
|
|
131
|
+
### v3.2.0
|
|
132
|
+
- **Chunk-level entity linking in `linkEntitiesToDocument`** — entities are now linked only to chunks where they actually appear (using `buildEntityMatcher` word-boundary/CJK matching), instead of blanket-linking to all chunks. Fixes search result domination by heavily-linked documents.
|
|
133
|
+
- **Graph boost decay + hard cap** — per-entity scores are sorted descending and decayed geometrically (0.5^i): 1st entity 100%, 2nd 50%, 3rd 25%, etc. Hard cap at 0.4 prevents graph signal from overwhelming vector similarity.
|
|
134
|
+
|
|
130
135
|
### v3.0.0
|
|
131
136
|
- **Back to self-contained embeddings** — reverted from Ollama dependency (v2.x) to built-in `@huggingface/transformers` with bge-m3 (1024-dim). No external services required.
|
|
132
137
|
- **Cross-lingual search** — auto-detects non-English queries and performs dual-language search
|
package/dist/index.js
CHANGED
|
@@ -1264,11 +1264,7 @@ class RAGKnowledgeGraphManager {
|
|
|
1264
1264
|
if (chunks.length === 0)
|
|
1265
1265
|
return 0;
|
|
1266
1266
|
// Get all entities with observations for richer matching
|
|
1267
|
-
const entities = this.db.prepare(`SELECT
|
|
1268
|
-
GROUP_CONCAT(o.content, ' ||| ') as observations
|
|
1269
|
-
FROM entities e
|
|
1270
|
-
LEFT JOIN observations o ON o.entityId = e.id
|
|
1271
|
-
GROUP BY e.id`).all();
|
|
1267
|
+
const entities = this.db.prepare(`SELECT id, name, entityType, observations FROM entities`).all();
|
|
1272
1268
|
// Minimum name length: 2 for CJK (e.g. "할랄"), 4 for Latin (avoid "API", "Bug")
|
|
1273
1269
|
const MIN_LEN_CJK = 2;
|
|
1274
1270
|
const MIN_LEN_LATIN = 4;
|
|
@@ -1284,7 +1280,13 @@ class RAGKnowledgeGraphManager {
|
|
|
1284
1280
|
// Also collect observation-derived aliases (short keywords from observations)
|
|
1285
1281
|
const aliases = [];
|
|
1286
1282
|
if (entity.observations) {
|
|
1287
|
-
|
|
1283
|
+
let obs;
|
|
1284
|
+
try {
|
|
1285
|
+
obs = JSON.parse(entity.observations);
|
|
1286
|
+
}
|
|
1287
|
+
catch {
|
|
1288
|
+
obs = [];
|
|
1289
|
+
}
|
|
1288
1290
|
for (const ob of obs) {
|
|
1289
1291
|
// Extract file paths or identifiers mentioned in observations (e.g. "gemini_converter.py")
|
|
1290
1292
|
const pathMatch = ob.match(/[\w\-]+\.\w{1,4}\b/g);
|
|
@@ -1345,11 +1347,14 @@ class RAGKnowledgeGraphManager {
|
|
|
1345
1347
|
if (!document) {
|
|
1346
1348
|
throw new Error(`Document with ID ${documentId} not found`);
|
|
1347
1349
|
}
|
|
1348
|
-
// Get chunks for this document
|
|
1350
|
+
// Get chunks for this document (with text for chunk-level matching)
|
|
1349
1351
|
const chunks = this.db.prepare(`
|
|
1350
|
-
SELECT rowid FROM chunk_metadata WHERE document_id = ?
|
|
1352
|
+
SELECT rowid, text FROM chunk_metadata WHERE document_id = ?
|
|
1351
1353
|
`).all(documentId);
|
|
1352
1354
|
let linkedCount = 0;
|
|
1355
|
+
const insertStmt = this.db.prepare(`
|
|
1356
|
+
INSERT OR IGNORE INTO chunk_entities (chunk_rowid, entity_id) VALUES (?, ?)
|
|
1357
|
+
`);
|
|
1353
1358
|
for (const entityName of entityNames) {
|
|
1354
1359
|
const entityId = `entity_${entityName.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
|
|
1355
1360
|
// Verify entity exists
|
|
@@ -1360,14 +1365,17 @@ class RAGKnowledgeGraphManager {
|
|
|
1360
1365
|
console.warn(`Entity ${entityName} not found, skipping`);
|
|
1361
1366
|
continue;
|
|
1362
1367
|
}
|
|
1363
|
-
//
|
|
1368
|
+
// Chunk-level filtering: only link to chunks where entity actually appears
|
|
1369
|
+
const nameMatcher = this.buildEntityMatcher(entityName);
|
|
1370
|
+
let entityLinked = false;
|
|
1364
1371
|
for (const chunk of chunks) {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1372
|
+
if (nameMatcher(chunk.text)) {
|
|
1373
|
+
insertStmt.run(chunk.rowid, entityId);
|
|
1374
|
+
entityLinked = true;
|
|
1375
|
+
}
|
|
1369
1376
|
}
|
|
1370
|
-
|
|
1377
|
+
if (entityLinked)
|
|
1378
|
+
linkedCount++;
|
|
1371
1379
|
}
|
|
1372
1380
|
console.error(`✅ Entities linked: ${linkedCount} entities linked to document`);
|
|
1373
1381
|
return { documentId, linkedEntities: linkedCount };
|
|
@@ -1868,42 +1876,54 @@ class RAGKnowledgeGraphManager {
|
|
|
1868
1876
|
chunkEntities = [relEntities.source_name, relEntities.target_name];
|
|
1869
1877
|
}
|
|
1870
1878
|
}
|
|
1871
|
-
// Enhanced graph boost calculation
|
|
1879
|
+
// Enhanced graph boost calculation with decay + cap
|
|
1872
1880
|
let graphBoost = 0;
|
|
1873
1881
|
if (useGraph) {
|
|
1874
1882
|
const queryEntities = this.extractTermsFromText(query);
|
|
1875
1883
|
// Base boost for knowledge graph chunks
|
|
1876
1884
|
if (result.chunk_type === 'entity') {
|
|
1877
|
-
graphBoost += 0.15;
|
|
1885
|
+
graphBoost += 0.15;
|
|
1878
1886
|
}
|
|
1879
1887
|
else if (result.chunk_type === 'relationship') {
|
|
1880
|
-
graphBoost += 0.25;
|
|
1888
|
+
graphBoost += 0.25;
|
|
1881
1889
|
}
|
|
1882
|
-
//
|
|
1890
|
+
// Collect per-entity scores (instead of blind accumulation)
|
|
1891
|
+
const entityScores = [];
|
|
1883
1892
|
const queryLower = query.toLowerCase();
|
|
1884
1893
|
for (const entity of chunkEntities) {
|
|
1894
|
+
let score = 0;
|
|
1885
1895
|
const entityLower = entity.toLowerCase();
|
|
1886
1896
|
// Vector-matched entity (cross-lingual: "할랄 인증" → "KMF")
|
|
1887
1897
|
if (queryMatchedEntities.has(entity)) {
|
|
1888
|
-
|
|
1898
|
+
score = 0.3;
|
|
1889
1899
|
}
|
|
1890
1900
|
// Exact text match with extracted terms
|
|
1891
1901
|
else if (queryEntities.some(qe => qe.toLowerCase() === entityLower)) {
|
|
1892
|
-
|
|
1902
|
+
score = 0.3;
|
|
1893
1903
|
}
|
|
1894
1904
|
// Partial match: entity name appears in query or vice versa
|
|
1895
1905
|
else if (queryLower.includes(entityLower) || entityLower.includes(queryLower)) {
|
|
1896
|
-
|
|
1906
|
+
score = 0.2;
|
|
1897
1907
|
}
|
|
1898
1908
|
// Word-level partial match
|
|
1899
1909
|
else if (entityLower.split(/\s+/).some(word => word.length >= 3 && queryLower.includes(word))) {
|
|
1900
|
-
|
|
1910
|
+
score = 0.15;
|
|
1901
1911
|
}
|
|
1902
|
-
// Connected to a vector-matched entity
|
|
1912
|
+
// Connected to a vector-matched entity (additive, independent)
|
|
1903
1913
|
if (connectedEntities.has(entity)) {
|
|
1904
|
-
|
|
1914
|
+
score += 0.15;
|
|
1905
1915
|
}
|
|
1916
|
+
if (score > 0)
|
|
1917
|
+
entityScores.push(score);
|
|
1918
|
+
}
|
|
1919
|
+
// Geometric decay: sort descending, apply 0.5^i decay per entity
|
|
1920
|
+
entityScores.sort((a, b) => b - a);
|
|
1921
|
+
let entityBoost = 0;
|
|
1922
|
+
for (let i = 0; i < entityScores.length; i++) {
|
|
1923
|
+
entityBoost += entityScores[i] * Math.pow(0.5, i);
|
|
1906
1924
|
}
|
|
1925
|
+
// Hard cap to prevent graph domination
|
|
1926
|
+
graphBoost += Math.min(entityBoost, 0.4);
|
|
1907
1927
|
}
|
|
1908
1928
|
// Generate semantic summary
|
|
1909
1929
|
const { summary, keyHighlight, relevanceScore } = await this.generateContentSummary(result.text, primaryQueryEmbedding, chunkEntities, result.chunk_type === 'relationship' ? 1 : 2 // Shorter summary for relationships
|