Skip to content

hivemind_find vector search returns zero results despite valid embeddings above threshold #205

Description

@srmcguirt

Summary

hivemind_find (vector search path) returns zero results for any query, even when the underlying embeddings exist and have strong cosine similarity to the query — well above the configured 0.3 threshold. hivemind_stats confirms embeddings are present (ollama_available: true, embeddings: N), and manually computing cosine similarity between the stored content and query text via Ollama directly returns scores like 0.80, 0.60, 0.61 — but the plugin's own vector search returns nothing.

Environment

  • opencode-swarm-plugin v0.63.2 (installed via bun add -g opencode-swarm-plugin, confirmed fresh reinstall — bug reproduces on a clean install, not a local corruption issue)
  • Ollama v0.30.10, mxbai-embed-large pulled and confirmed working via direct /api/embeddings calls
  • macOS (Darwin), Bun v1.3.8

Repro steps

  1. hivemind_store({ information: "The katas repo uses .editorconfig and .npmrc for consistent tooling across contributors, but has no package.json yet — it's a fresh scaffold with just docs/, README, and LICENSE.", tags: "katas,scaffold" })
  2. hivemind_stats() → confirms { memories: 1, embeddings: 1, healthy: true, ollama_available: true }
  3. hivemind_find({ query: "katas repo editorconfig npmrc package.json" }) → returns { results: [], count: 0 }

Even an exact keyword match against the stored content returns no results.

Independent verification

Bypassing the plugin entirely, I generated embeddings directly against Ollama and computed cosine similarity by hand:

# mxbai-embed-large via http://localhost:11434/api/embeddings
query = "katas repo editorconfig npmrc package.json"
stored = "The katas repo uses .editorconfig and .npmrc for consistent tooling..."
cosine_similarity(embed(query), embed(stored))  # => 0.7993

0.80 similarity is well above the threshold: 0.3 hardcoded in store.search() (packages/swarm-mail/src/memory/store.ts). The embedding model and Ollama integration are functioning correctly — the bug is isolated to the vector search query path itself.

Suspected cause

store.search() uses vector_top_k('idx_memories_embedding', vector(...), limit*2) (libSQL ANN index) joined back to the memories table, filtered by (1 - vector_distance_cos(...)) >= threshold. Given embeddings are present and well above threshold but zero rows are returned, this points to the idx_memories_embedding vector index not existing, being stale, or not populating correctly for newly-inserted rows via libsql_vector_idx.

Confirmed NOT the cause

  • Not an Ollama/embedding model issue (verified via direct API + manual cosine similarity)
  • Not a threshold issue (0.80 similarity vs. 0.3 threshold)
  • Not a stale local install (reproduces on a fresh bun remove -g + bun add -g opencode-swarm-plugin reinstall of the same published v0.63.2)
  • Not a collection filter issue (reproduces with and without explicit collection: "default")

Impact

Semantic memory search (hivemind_find without fts: true) is non-functional for all users on v0.63.2, silently returning empty results with no error — likely masked in practice by the automatic FTS fallback path only triggering on Ollama unavailability, not on vector search returning zero rows.

Further investigation: likely root cause

Traced deeper into packages/swarm-mail/src/memory/libsql-schema.ts and store.ts. Notable findings:

  1. validateLibSQLMemorySchema() is dead code. It's exported and exercised in its own test file, but it's never called anywhere in the production init path (memory-tools.ts / getMemoryAdapter). It also doesn't check for the existence of idx_memories_embedding at all — only tables/columns/FTS5. So even if it were wired in, it would report a healthy schema despite a missing or stale vector index.

  2. Most likely cause: libSQL's libsql_vector_idx() ANN index isn't guaranteed to be incrementally maintained on INSERT. Unlike a normal B-tree index, libSQL's vector index (particularly in embedded/local file mode, as opposed to Turso's hosted infrastructure) is known to require rebuilding after writes rather than updating live. This matches the repro exactly: the memories table + idx_memories_embedding already existed from a prior session; new rows were INSERTed via hivemind_store; the rows and embeddings are verifiably present in the table itself (hivemind_stats, direct cosine similarity against the raw embedding data); but vector_top_k('idx_memories_embedding', ...) returns nothing for them, because the ANN index was never updated to include the new rows.

Proposed fix

In order of preference:

  1. Best: drop the ANN index dependency for embedded use, brute-force scan instead. Memory stores here are small (hundreds to low-thousands of rows) — a full scan is fast enough and eliminates the index-staleness class of bug entirely. Replace the vector_top_k(...) JOIN memories query in store.ts's search() with a direct scan:

    SELECT m.*, vector_distance_cos(m.embedding, vector(?)) as distance
    FROM memories m
    WHERE m.embedding IS NOT NULL
      AND (1 - vector_distance_cos(m.embedding, vector(?))) >= ?
      -- + collection/decay filters
    ORDER BY distance ASC
    LIMIT ?

    This pattern already exists as a fallback branch elsewhere in store.ts (~line 289) — it just needs to become the primary path for local/embedded libSQL instead of the ANN-indexed path.

  2. Minimal: reindex after every write. After store(), run REINDEX idx_memories_embedding (or DROP INDEX + recreate) so newly-inserted vectors get folded into the ANN index. Cheap correctness patch, but adds write-path latency and doesn't fully resolve the underlying reliability question under concurrent writers.

  3. Diagnostic: make validateLibSQLMemorySchema() load-bearing. Wire it into the adapter init path, and extend it to compare vector_top_k result count against COUNT(id) for a cheap synthetic query, auto-healing (reindexing) on divergence. This also fixes the dead-code issue where the validator exists but is never invoked.

Happy to open a PR for option 1 if that's the preferred direction — it's a small, contained change to store.ts's search() method.

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