LlamaIndex StorageContext — index reloaded with mismatched embed_model
Persistence and reload look simple until a config change swaps the embed_model behind the scenes. Retrieval scores go from meaningful to random with no visible error.
Quick fix (TL;DR)
StorageContext holds the docstore, index_store, and vector_store. Reloading via load_index_from_storage requires the same embed_model as was used during indexing. Fix by (a) persisting embed_model config alongside the index, (b) explicitly passing embed_model on reload, (c) validating dimension match and model identity, and (d) using an ingestion pipeline's docstore for incremental updates.Real error messages you'll see
These are the exact strings returned by the LlamaIndex framework and its integrations when this error occurs. Copy-paste-searching any of them should land on this page.
# Ingested with text-embedding-3-small # Reloaded and Settings.embed_model changed to text-embedding-3-large # Queries return nodes with cosine ~0.1 for what should be exact matches
ValueError: Query vector dimension 1536 does not match index dimension 3072. # text-embedding-3-small (1536-d) index queried with text-embedding-3-large (3072-d)
FileNotFoundError: [Errno 2] No such file or directory: './storage/docstore.json' # persist_dir path wrong, or vector_store was remote and not persisted locally
Reference
What lives in StorageContext
| Store | Contains | Persistence |
|---|---|---|
docstore | Original Documents / Nodes with metadata | JSON file in persist_dir |
index_store | Index-specific data (tree structure, keyword tables) | JSON file in persist_dir |
vector_store | Embeddings + metadata | Depends on backend (local JSON or remote) |
graph_store | Knowledge graph (for KG indexes) | JSON file in persist_dir |
property_graph_store | Property graph nodes/edges | JSON or Neo4j etc. |
Root causes, ranked by frequency
Based on developer reports across LlamaIndex forums, GitHub issues, and Discord community during 2025–2026.
- 26%Settings.embed_model changed between ingest and reload. Config drift; no error on reload.
- 18%Vector store dimension differs from current embed_model. Hard error; the loud version.
- 14%persist_dir path different or missing. Fresh index built accidentally; data loss.
- 10%Remote vector store not preserved on persist. persist saves docstore locally but relies on remote server for vectors.
- 8%Multiple environments using different Settings. Dev uses one embed_model; prod uses another.
- 7%Custom node parser not deterministic. Same doc parsed differently across sessions; node IDs drift.
- 7%Docstore corruption from concurrent writes. Two processes persisting to same dir.
- 10%load_index_from_storage without explicit embed_model. Falls back to Settings default; may be wrong.
Fixes — copy-paste solutions
Persist embed_model config alongside the index
Write a small metadata file next to persist_dir that records the embed_model class, model name, and dimension. Validate on load.
import json from pathlib import Path from llama_index.core import VectorStoreIndex, StorageContext, Settings, load_index_from_storage from llama_index.embeddings.openai import OpenAIEmbedding def persist_index(index, persist_dir: str): """Persist index AND its embedding config for safe reload.""" p = Path(persist_dir) p.mkdir(parents=True, exist_ok=True) # Standard index persist index.storage_context.persist(persist_dir=persist_dir) # Also record the embed_model config embed_model = index._embed_model config = { "embed_model_class": type(embed_model).__name__, "embed_model_name": getattr(embed_model, "model_name", None) or getattr(embed_model, "model", None), "embed_dim": getattr(embed_model, "embed_dim", None), # Include any provider-specific config that affects embeddings "additional_kwargs": getattr(embed_model, "additional_kwargs", {}), } (p / "embed_config.json").write_text(json.dumps(config, indent=2)) print(f"Persisted index and config to {persist_dir}") def load_index_safely(persist_dir: str, embed_model=None): """Load index, verifying embedding config matches what was persisted.""" p = Path(persist_dir) # Load the config config_path = p / "embed_config.json" if not config_path.exists(): raise ValueError( f"No embed_config.json in {persist_dir}. This index was persisted " f"without safety metadata; cannot verify embed_model match." ) saved_config = json.loads(config_path.read_text()) # Verify current embed_model matches if embed_model is None: embed_model = Settings.embed_model current_class = type(embed_model).__name__ current_model = getattr(embed_model, "model_name", None) or getattr(embed_model, "model", None) if current_class != saved_config["embed_model_class"]: raise ValueError( f"embed_model class mismatch: index was built with {saved_config['embed_model_class']}, " f"but current is {current_class}. Retrieval will be broken." ) if current_model != saved_config["embed_model_name"]: raise ValueError( f"embed_model name mismatch: index was built with {saved_config['embed_model_name']}, " f"but current is {current_model}. Retrieval will be broken." ) storage_context = StorageContext.from_defaults(persist_dir=persist_dir) return load_index_from_storage(storage_context, embed_model=embed_model) # Usage embed_model = OpenAIEmbedding(model="text-embedding-3-small") Settings.embed_model = embed_model # Persist persist_index(index, "./storage/my_index") # Later — safe load index = load_index_safely("./storage/my_index", embed_model=embed_model)
embed_config.json pattern is a small safety belt with large payoff. It converts silent quality regressions into loud errors — always preferable.Pass embed_model explicitly to load_index_from_storage
On load, always pass embed_model= explicitly. Even if Settings happen to be correct, being explicit prevents accidents.
from llama_index.core import StorageContext, load_index_from_storage, Settings from llama_index.embeddings.openai import OpenAIEmbedding from llama_index.llms.openai import OpenAI # --- Explicit reload with all config --- def rebuild_query_engine(persist_dir: str): """Rebuild query engine from persisted storage with explicit config.""" # Recreate the exact embed_model used at ingest embed_model = OpenAIEmbedding( model="text-embedding-3-small", embed_batch_size=32, ) llm = OpenAI(model="gpt-4o-mini") # Set as Settings (helpful for downstream) Settings.embed_model = embed_model Settings.llm = llm # Load with explicit reference (do not rely on Settings) storage_context = StorageContext.from_defaults(persist_dir=persist_dir) index = load_index_from_storage( storage_context, embed_model=embed_model, # ← explicit llm=llm, # ← explicit ) return index.as_query_engine(similarity_top_k=6, llm=llm) # --- For remote vector stores, reload the store connection --- from llama_index.vector_stores.chroma import ChromaVectorStore import chromadb def reload_with_remote_vector_store(persist_dir: str): # Reconnect to the remote vector store client = chromadb.PersistentClient(path="./chroma_db") collection = client.get_collection("my_docs") vector_store = ChromaVectorStore(chroma_collection=collection) # persist_dir only contains docstore + index_store; vector_store is remote storage_context = StorageContext.from_defaults( vector_store=vector_store, # ← from remote persist_dir=persist_dir, # ← docstore, index_store from local ) embed_model = OpenAIEmbedding(model="text-embedding-3-small") return load_index_from_storage(storage_context, embed_model=embed_model)
Settings is convenient for prototyping but a foot-gun in production. Explicit config is worth the extra line — it makes intent auditable.Detect dimension mismatch early with a smoke test
After every load, run a canonical smoke query and check that a known-good node comes back with a reasonable score. Fail loud if it does not.
from llama_index.core import VectorStoreIndex, load_index_from_storage from typing import Tuple # --- Canonical smoke test defined at ingest time, stored with index --- def run_smoke_test(index, canonical_query: str, expected_source: str, min_score: float = 0.4) -> Tuple[bool, str]: """Return (passed, message). Raises on hard failures like dimension mismatch.""" try: retriever = index.as_retriever(similarity_top_k=5) nodes = retriever.retrieve(canonical_query) except ValueError as e: if "dimension" in str(e).lower(): return False, f"DIMENSION MISMATCH: {e}" raise if not nodes: return False, "Retriever returned 0 nodes — likely empty index or filter issue" top_score = nodes[0].score if top_score < min_score: return False, ( f"Top score {top_score:.3f} below threshold {min_score:.3f} — " f"embed_model may be different from ingest" ) top_source = nodes[0].metadata.get("source", "") if expected_source not in top_source: return False, ( f"Top result {top_source} does not match expected {expected_source}. " f"Index may be corrupted or embed_model wrong." ) return True, f"OK — top score {top_score:.3f}, source {top_source}" # --- Usage after every load --- index = load_index_safely("./storage/my_index", embed_model=embed_model) passed, msg = run_smoke_test( index, canonical_query="canonical smoke test query", expected_source="doc_smoke_canonical_source", min_score=0.4, ) if not passed: raise RuntimeError(f"Index smoke test failed: {msg}") print(f"Smoke test: {msg}")
Prevention checklist
Ship these seven safeguards once and this error stops appearing in your logs.
- Persist embed_model config alongside the index; validate on every load.
- Pass
embed_model=explicitly toload_index_from_storage. - For remote vector stores, remember persist_dir only saves docstore + index_store.
- Run a canonical smoke test after every load; fail deploy if it fails.
- Track embed_model as part of your infra config; changes require index rebuild.
- Version-pin embedding models with their dates (
text-embedding-3-smallis versioned). - For CI, include a "rebuild index" step whenever embed_model or corpus changes.
Frequently asked questions
index.insert(node) or index.insert_nodes(nodes) — the new nodes are embedded with the current embed_model. Just make sure it matches the original.Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.