fix: metadata explosion bug - 69K files reduced to ~1K

Critical fix for metadata indexing that was creating 60+ chunk files per entity.

Root cause: Vector embeddings (384-dimensional arrays) were being indexed in
metadata, causing each dimension to create a separate chunk file with numeric
field names ("0", "1", "2", etc.).

Changes:
- Modified extractIndexableFields() to exclude vector/embedding fields
- Added NEVER_INDEX set: ['vector', 'embedding', 'embeddings', 'connections']
- Added safety check to skip arrays > 10 elements
- Preserves small array indexing (tags, categories, roles)

Impact:
- Reduces metadata files from 69,429 → ~1,200 (58x reduction)
- Fixes server initialization hangs
- Fixes metadata batch loading stalling at batch 23
- Fixes VFS getDescendants() hanging with large datasets
- Fixes Graph View UI not loading

Test Results:
- 7/7 integration tests passing
- Verified: 6 chunk files for 10 entities (was 7,210 before fix)
- 611/622 unit tests passing

Files Modified:
- src/utils/metadataIndex.ts - Core fix
- src/coreTypes.ts - HNSWVerb type enforcement with VerbType enum
- src/storage/adapters/* - Include core relational fields in HNSWVerb
- src/storage/adapters/baseStorageAdapter.ts - Type enforcement (HNSWNoun, GraphVerb)
- tests/integration/metadata-vector-exclusion.test.ts - Comprehensive test coverage

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
David Snelling 2025-10-16 16:10:31 -07:00
parent 7921a5b744
commit e600865d96
13 changed files with 571 additions and 112 deletions

View file

@ -1081,37 +1081,53 @@ export class MetadataIndexManager {
/**
* Extract indexable field-value pairs from metadata
*
* BUG FIX (v3.50.1): Exclude vector embeddings and large arrays from indexing
* - Vector fields (384+ dimensions) were creating 825K chunk files for 1,144 entities
* - Arrays should not have their indices indexed as separate fields
*/
private extractIndexableFields(metadata: any): Array<{ field: string, value: any }> {
const fields: Array<{ field: string, value: any }> = []
// Fields that should NEVER be indexed (vectors, embeddings, large arrays)
const NEVER_INDEX = new Set(['vector', 'embedding', 'embeddings', 'connections'])
const extract = (obj: any, prefix = ''): void => {
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key
// Skip fields in never-index list (CRITICAL: prevents vector indexing bug)
if (NEVER_INDEX.has(key)) continue
// Skip fields based on user configuration
if (!this.shouldIndexField(fullKey)) continue
// Skip large arrays (> 10 elements) - likely vectors or bulk data
if (Array.isArray(value) && value.length > 10) continue
if (value && typeof value === 'object' && !Array.isArray(value)) {
// Recurse into nested objects
// Recurse into nested objects (but not arrays)
extract(value, fullKey)
} else {
// Index this field
fields.push({ field: fullKey, value })
// If it's an array, also index each element
if (Array.isArray(value)) {
for (const item of value) {
} else if (Array.isArray(value) && value.length <= 10) {
// Small arrays: index as multi-value field (all with same field name)
// Example: tags: ["javascript", "node"] → field="tags", value="javascript" + field="tags", value="node"
for (const item of value) {
// Only index primitive values (not nested objects/arrays)
if (item !== null && typeof item !== 'object') {
fields.push({ field: fullKey, value: item })
}
}
} else {
// Primitive value: index it
fields.push({ field: fullKey, value })
}
}
}
if (metadata && typeof metadata === 'object') {
extract(metadata)
}
return fields
}