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.
Files changed (3) hide show
  1. package/README.md +6 -1
  2. package/dist/index.js +44 -24
  3. 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` | Manually link entities to document chunks | idempotent |
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 e.id, e.name, e.entityType,
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
- const obs = entity.observations.split(' ||| ');
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
- // Link entity to all chunks of the document
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
- this.db.prepare(`
1366
- INSERT OR IGNORE INTO chunk_entities (chunk_rowid, entity_id)
1367
- VALUES (?, ?)
1368
- `).run(chunk.rowid, entityId);
1372
+ if (nameMatcher(chunk.text)) {
1373
+ insertStmt.run(chunk.rowid, entityId);
1374
+ entityLinked = true;
1375
+ }
1369
1376
  }
1370
- linkedCount++;
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; // Entities are inherently valuable
1885
+ graphBoost += 0.15;
1878
1886
  }
1879
1887
  else if (result.chunk_type === 'relationship') {
1880
- graphBoost += 0.25; // Relationships show connections
1888
+ graphBoost += 0.25;
1881
1889
  }
1882
- // Additional boost for entity matches
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
- graphBoost += 0.3;
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
- graphBoost += 0.3;
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
- graphBoost += 0.2;
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
- graphBoost += 0.15;
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
- graphBoost += 0.15;
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rag-memory-epf-mcp",
3
- "version": "3.1.5",
3
+ "version": "3.2.1",
4
4
  "description": "MCP server for project-local RAG memory with knowledge graph and multilingual vector search",
5
5
  "license": "MIT",
6
6
  "author": "bripin123",