Skip to content

fix(oss): auto-detect embedding dimension to fix Qdrant mismatch with non-OpenAI embedders#4297

Merged
whysosaket merged 5 commits intomainfrom
fix/qdrant-dimension-mismatch
Mar 12, 2026
Merged

fix(oss): auto-detect embedding dimension to fix Qdrant mismatch with non-OpenAI embedders#4297
whysosaket merged 5 commits intomainfrom
fix/qdrant-dimension-mismatch

Conversation

@utkarsh240799
Copy link
Copy Markdown
Contributor

@utkarsh240799 utkarsh240799 commented Mar 11, 2026

Problem

Users using non-OpenAI embedding providers (Ollama/nomic-embed-text, Google/Gemini, HuggingFace, etc.) hit Bad Request errors when using Qdrant because the vector store was always created with a hardcoded default dimension of 1536 (OpenAI's dimension), regardless of the actual embedding model's output dimension.

This caused three related issues:

Solution

1. Probe-based dimension auto-detection

When no explicit dimension is provided, the Memory class now embeds a short probe string ("dimension probe") at initialization time and reads the resulting vector length to determine the correct dimension. This happens before the vector store is created.

Dimension resolution order:

  1. Explicit vectorStore.config.dimension (user override)
  2. embedder.config.embeddingDims (embedder config)
  3. Auto-detected via probe embedding (new fallback)

2. Deferred vector store creation

The vector store is no longer created in the Memory constructor. Instead, it's created in an async _autoInitialize() method after the dimension is known. All public Memory methods (add, search, getAll, etc.) await initialization before proceeding via a lazy init gate (_ensureInitialized()).

3. Atomic Qdrant collection creation

Replaced the check-then-create pattern (TOCTOU race) with create-and-catch-409:

try {
  await this.client.createCollection(name, { vectors: { size, distance: "Cosine" } });
} catch (error) {
  if (error?.status === 409) {
    // Collection already exists — verify dimension matches
  } else {
    throw error;
  }
}

4. Idempotent init guards for all vector stores

Every vector store with async initialization (Qdrant, Redis, Supabase, AzureAISearch, Vectorize) now uses a _initPromise singleton guard. This prevents double-initialization when Memory explicitly calls await vectorStore.initialize() after the constructor already fired it:

private _initPromise?: Promise<void>;

async initialize(): Promise<void> {
  if (!this._initPromise) {
    this._initPromise = this._doInitialize();
  }
  return this._initPromise;
}

Without this guard:

  • Redis: Double connect()"Socket already opened", plus dropIndex/createIndex destroys the index the first call just built
  • Qdrant: Double createCollection409 Conflict crash
  • Vectorize: Double indexes.create() → Cloudflare API error
  • Supabase/AzureAISearch: Redundant network calls and potential transient failures

5. Google embedder fix

Fixed hardcoded outputDimensionality: 768 in google.ts — now uses this.embeddingDims from config.

Files Changed

File Change
src/oss/src/memory/index.ts Deferred vector store creation, lazy init gate, probe-based dimension detection
src/oss/src/config/manager.ts Dimension resolution: explicit > embeddingDims > undefined (triggers probe)
src/oss/src/vector_stores/qdrant.ts Atomic create-and-catch-409, idempotent init, transient error resilience
src/oss/src/vector_stores/redis.ts Idempotent init guard
src/oss/src/vector_stores/supabase.ts Idempotent init guard
src/oss/src/vector_stores/azure_ai_search.ts Idempotent init guard
src/oss/src/vector_stores/vectorize.ts Idempotent init guard
src/oss/src/embeddings/google.ts Use embeddingDims config instead of hardcoded 768

Testing

Unit Tests (61 tests)

  • dimension-autodetect.test.ts (22 tests) — ConfigManager dimension resolution, MemoryVectorStore backward compat, Memory auto-initialization, error propagation
  • config-manager.test.ts (5 tests) — ConfigManager dimension priority chain
  • vector-stores-compat.test.ts (34 tests) — Backward compatibility for all 7 vector store implementations (MemoryVectorStore, Qdrant, Redis, Supabase, AzureAISearch, Vectorize, Langchain) with mocked clients

End-to-End Tests (21 tests, against real instances)

Total: 82 tests, all passing

Checklist

utkarsh240799 and others added 3 commits March 11, 2026 01:03
… non-OpenAI embedders

When using embedders like Ollama's nomic-embed-text (768 dims), the
hardcoded 1536 default caused Qdrant to reject vectors with Bad Request.

- Probe embedder at init to auto-detect dimension when not explicitly set
- Defer vector store creation until dimension is known
- Gate all public Memory methods behind lazy init promise
- Fix Qdrant collection creation race condition (TOCTOU → atomic create + catch 409)
- Fix Google embedder hardcoded outputDimensionality (768 → config value)
- Use absolute path for default historyDbPath (~/.mem0/memory.db)
- Propagate init errors to callers with clear message suggesting explicit config

Fixes #4212, #4173, #4056

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ection

- Make Qdrant.initialize() idempotent with promise gate to prevent
  double-init race between constructor and explicit callers
- Make ensureCollection resilient to transient 500 errors during
  dimension verification after 409
- Await vectorStore.initialize() in Memory._autoInitialize() to
  guarantee collections exist before any public method runs
- Add 8 e2e tests against real Qdrant verifying all three issues:
  dimension mismatch (#4212/#4173), race condition (#4056),
  and memory_migrations dimension isolation (#4056)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevent double-initialization when Memory explicitly calls initialize()
after the constructor already fired it. Without this guard, Redis reconnects,
Supabase re-verifies, AzureAISearch re-lists indexes, and Vectorize
re-creates indexes on every call.

Adds 13 Redis Stack e2e tests and 34 backward-compatibility unit tests
covering all vector store implementations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@whysosaket
Copy link
Copy Markdown
Member

Once init fails, the Memory instance is permanently dead... no retry, no recovery. If the embedder was briefly down at startup, user has to recreate everything.

@whysosaket
Copy link
Copy Markdown
Member

E2E tests hard-fail without local Qdrant/Redis and have no skip mechanism.

whysosaket
whysosaket previously approved these changes Mar 11, 2026
- Log transient verification errors in ensureCollection instead of
  silently swallowing them (auth/network failures are now visible)
- Auto-retry initialization on failure so a transiently unavailable
  embedder or vector store at startup doesn't permanently kill the
  Memory instance
- Skip e2e tests gracefully when Qdrant/Redis containers aren't running

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@whysosaket whysosaket merged commit 59c3b05 into main Mar 12, 2026
3 checks passed
@whysosaket whysosaket deleted the fix/qdrant-dimension-mismatch branch March 12, 2026 16:15
jamebobob pushed a commit to jamebobob/mem0-vigil-recall that referenced this pull request Mar 29, 2026
… non-OpenAI embedders (mem0ai#4297)

Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Qdrant collection created with wrong dimensions when using Ollama embedder (nomic-embed-text)

2 participants