Skip to content

Bug: import() does not rebuild HNSW index — recall() always returns [] after load #315

@RobLe3

Description

@RobLe3

Summary

After calling engine.import(data), engine.recall() always returns [] even though memories are fully present in this.memories. The HNSW vector index is not rebuilt during import, and the empty-result path in recall() does not fall through to the brute-force fallback.

Root Cause

import() populates this.memories but never calls vectorDb.insert(). After import, recall() calls vectorDb.search(), receives an empty array (no exception thrown), and returns it immediately. The brute-force fallback only triggers on a caught error — not on empty results:

// import() — only this.memories.set() is called, vectorDb is never touched
import(data) {
    for (const mem of data.memories) {
        this.memories.set(mem.id, mem);  // ✓ Map populated
        // vectorDb.insert() never called  ✗ HNSW stays empty
    }
}

// recall() — HNSW returns [] without throwing → brute-force never runs
const results = await this.vectorDb.search({ vector, k: topK });
return results.map(...).filter(...);  // silently returns []
// catch block only fires on thrown errors, not on empty results

By contrast, remember() correctly calls both:

this.memories.set(id, entry);
await this.vectorDb.insert({ id, vector, metadata });  // ← import() misses this

Reproduction

const engine = new IntelligenceEngine({ embeddingDim: 128 });
await engine.remember('test content about SWARM routing');

const exported = engine.export();

// Simulate reload
const engine2 = new IntelligenceEngine({ embeddingDim: 128 });
engine2.import(exported);

const results = await engine2.recall('SWARM routing');
console.log(results); // [] — expected non-empty
console.log(engine2.memories.size); // > 0 — memories are present

Suggested Fix

Option A — Rebuild HNSW in import():

import(data, merge = false) {
    // ... existing import logic ...

    // Rebuild HNSW index from restored memories
    if (this.vectorDb && data.memories?.length) {
        for (const mem of data.memories) {
            if (mem.embedding?.length) {
                this.vectorDb.insert({
                    id: mem.id,
                    vector: new Float32Array(mem.embedding),
                    metadata: JSON.stringify({ content: mem.content?.slice(0, 100), type: mem.type }),
                }).catch(() => {});
            }
        }
    }
}

Option B — Guard in recall() (defensive, one line):

const results = await this.vectorDb.search({ vector, k: topK });
if (results.length > 0) return results.map(...).filter(...); // only return if HNSW has results
// fall through to brute-force

Option A is the correct fix. Option B is a safe short-term guard.

Impact

Every application that uses export() / import() for persistence gets broken recall after any restart or reload. The round-trip is effectively a recall-destroying operation with no error or warning. The this.memories Map is intact but the HNSW index is permanently empty until memories are individually re-added via remember().

Environment

  • ruvector@0.2.19
  • Node.js v20.19.5
  • dist/core/intelligence-engine.jsimport() ~line 876, recall() ~line 367

Best regards,
Rob / Roble

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions