Milvus developer on retainer: collection.search() raises MilvusException: collection not loaded after create_index() — collection.load() call missing — 3 weeks — Milvus vector search developer on monthly retainer
milvus 2.4.5 standalone • pymilvus 2.4.5 • Python 3.11 • OpenAI text-embedding-3-small (1536 dims) • 200,000 vectors • create_index() builds disk index; collection.load() is required to move index into memory before any search • MilvusException: collection not loaded on every search() call • 3 weeks • fix: collection.load() + wait_for_loading_complete() • 1.5h retainer engagement
Northfield Semantic Search Ltd. built a document retrieval service in August 2026 using Milvus 2.4.5 standalone and OpenAI text-embedding-3-small embeddings. The collection held 200,000 document chunks, each represented as a 1536-dimensional float vector. After inserting all vectors and building an IVF_FLAT index, every call to collection.search() raised a MilvusException with the message collection not loaded. The team spent three weeks rebuilding the index with different parameters (nlist values of 64, 128, 256, 1024) and reinstalling Milvus twice. The error persisted. They engaged a Milvus retainer developer.
The collection setup code at the time of the engagement:
# vector_store.py
# Northfield Semantic Search Ltd.
# milvus 2.4.5 standalone, pymilvus 2.4.5, Python 3.11
from pymilvus import (
connections,
Collection,
CollectionSchema,
FieldSchema,
DataType,
utility,
)
COLLECTION_NAME = "documents"
VECTOR_DIM = 1536
METRIC_TYPE = "COSINE"
connections.connect("default", host="localhost", port="19530")
def setup_collection() -> Collection:
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=4096),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=VECTOR_DIM),
]
schema = CollectionSchema(fields=fields, description="Document embeddings")
if utility.has_collection(COLLECTION_NAME):
collection = Collection(COLLECTION_NAME)
else:
collection = Collection(COLLECTION_NAME, schema)
# Build IVF_FLAT index on the embedding field
index_params = {
"index_type": "IVF_FLAT",
"metric_type": METRIC_TYPE,
"params": {"nlist": 128},
}
collection.create_index(field_name="embedding", index_params=index_params)
# ← create_index() writes the index to disk storage
# ← the collection is NOT yet in memory
# ← NO call to collection.load() here ← BUG
# ← any search() call will raise MilvusException: collection not loaded
return collection
def search_documents(collection: Collection, query_vector: list[float], top_k: int = 10):
search_params = {"metric_type": METRIC_TYPE, "params": {"nprobe": 16}}
results = collection.search(
data=[query_vector],
anns_field="embedding",
param=search_params,
limit=top_k,
output_fields=["text"],
)
# ↑ MilvusException: collection not loaded
# ↑ Error code 65535
# ↑ Raised on every call regardless of nlist value or Milvus version
return results
Why Milvus separates indexing from loading
Milvus 2.x uses an explicit two-phase model for collection readiness. Phase one — create_index() — computes and persists the index structure to Milvus's data volume on disk. For IVF_FLAT, this is the inverted file clustering of all vectors. For HNSW, this is the proximity graph. This phase is compute-intensive and is designed to run offline, decoupled from query serving. Phase two — collection.load() — copies the index and the raw vectors from disk into the memory of the query node (RAM for CPU-based Milvus; GPU memory if a GPU index type is configured). Only after this in-memory load is complete does the collection become queryable.
This design allows Milvus to manage memory explicitly: operators can load only the collections actively receiving traffic and release the rest. The consequence for developers is that create_index() alone is not sufficient to enable search — a separate collection.load() must always follow. Databases like Qdrant and Weaviate handle this automatically on insert; Milvus requires the explicit call. The error message collection not loaded accurately describes the state — the index exists on disk but the collection has not been loaded into memory — but developers encountering it for the first time interpret it as a broader setup failure, not a missing single API call.
# Reproducing and diagnosing the error
# pymilvus 2.4.5, Python 3.11
from pymilvus import Collection, utility
collection = Collection("documents")
# Check collection load state — returns 'NotLoad', 'Loading', or 'Loaded'
print(collection.load_state)
# Output: 'NotLoad'
# ↑ Confirms the collection is on disk but not in memory
# ↑ Regardless of whether create_index() has been called
# Attempt to search without loading
try:
results = collection.search(
data=[[0.1] * 1536],
anns_field="embedding",
param={"metric_type": "COSINE", "params": {"nprobe": 16}},
limit=5,
)
except Exception as e:
print(type(e).__name__, str(e))
# Output: MilvusException collection not loaded
# Error code: 65535
# This error is raised regardless of:
# - nlist value used in create_index()
# - Whether the index was just created or has been on disk for weeks
# - Whether the Milvus process was restarted
# - Milvus standalone vs cluster mode
# The ONLY fix is collection.load()
The fix: collection.load() after create_index()
# vector_store.py — FIXED
# milvus 2.4.5, pymilvus 2.4.5, Python 3.11
from pymilvus import (
connections,
Collection,
CollectionSchema,
FieldSchema,
DataType,
utility,
)
COLLECTION_NAME = "documents"
VECTOR_DIM = 1536
METRIC_TYPE = "COSINE"
connections.connect("default", host="localhost", port="19530")
def setup_collection() -> Collection:
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=4096),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=VECTOR_DIM),
]
schema = CollectionSchema(fields=fields, description="Document embeddings")
if utility.has_collection(COLLECTION_NAME):
collection = Collection(COLLECTION_NAME)
else:
collection = Collection(COLLECTION_NAME, schema)
# Phase 1: build index on disk
index_params = {
"index_type": "IVF_FLAT",
"metric_type": METRIC_TYPE,
"params": {"nlist": 448}, # sqrt(200_000) ≈ 447; rounded to 448
}
collection.create_index(field_name="embedding", index_params=index_params)
# Phase 2: load the index and vectors into memory ← FIXED
collection.load()
# Block until the load is fully complete before returning
utility.wait_for_loading_complete(COLLECTION_NAME)
# Verify the collection is now in the Loaded state
assert collection.load_state == "Loaded", (
f"Collection {COLLECTION_NAME} is not Loaded after load(). "
f"Current state: {collection.load_state}"
)
return collection
def search_documents(collection: Collection, query_vector: list[float], top_k: int = 10):
search_params = {"metric_type": METRIC_TYPE, "params": {"nprobe": 16}}
results = collection.search(
data=[query_vector],
anns_field="embedding",
param=search_params,
limit=top_k,
output_fields=["text"],
)
# ↑ No longer raises MilvusException
# ↑ Returns top_k nearest neighbours by cosine similarity
return results
# Startup health check — add to application startup sequence
# Run after setup_collection(), before serving any traffic
from pymilvus import Collection
def assert_collection_ready(collection_name: str) -> None:
collection = Collection(collection_name)
if collection.load_state != "Loaded":
raise RuntimeError(
f"Milvus collection '{collection_name}' is not Loaded. "
f"Current state: {collection.load_state}. "
f"Call collection.load() followed by utility.wait_for_loading_complete() "
f"before accepting search traffic."
)
# Verify the index is built
indexes = collection.indexes
if not indexes:
raise RuntimeError(
f"Milvus collection '{collection_name}' has no index. "
f"Call collection.create_index() before collection.load()."
)
print(
f"[OK] Collection '{collection_name}': Loaded, "
f"{collection.num_entities} entities, "
f"index type: {indexes[0].params['index_type']}"
)
assert_collection_ready(COLLECTION_NAME)
# Output: [OK] Collection 'documents': Loaded, 200000 entities, index type: IVF_FLAT
Handling index rebuilds and Milvus restarts
Milvus does not automatically re-load a collection after a process restart. When the Milvus standalone process restarts — due to a container restart, OOM kill, or scheduled maintenance — all collections return to the NotLoad state even though their indexes are intact on disk. Any application that calls collection.search() immediately after a Milvus restart, without first calling collection.load(), will receive the same MilvusException: collection not loaded error. The startup health check above catches this case: if load_state != "Loaded" at startup, the check calls collection.load() before allowing traffic.
When rebuilding an index (to change nlist, metric type, or index type), the sequence is: collection.release() → collection.drop_index() → collection.create_index(...) → collection.load() → utility.wait_for_loading_complete(). Skipping collection.release() before drop_index() raises a different MilvusException in Milvus 2.4: index cannot be dropped while collection is loaded.
# Index rebuild sequence (correct order for Milvus 2.4)
# Required when changing nlist, metric type, or index type
from pymilvus import Collection, utility
def rebuild_index(collection_name: str, new_index_params: dict) -> None:
collection = Collection(collection_name)
# Step 1: release from memory BEFORE dropping the index
if collection.load_state == "Loaded":
collection.release()
# Step 2: drop the existing index
collection.drop_index()
# Step 3: build the new index on disk
collection.create_index(field_name="embedding", index_params=new_index_params)
# Step 4: load the new index into memory
collection.load()
utility.wait_for_loading_complete(collection_name)
print(f"Index rebuild complete. State: {collection.load_state}")
# Example: increase nlist as dataset grows from 200k to 2M vectors
rebuild_index("documents", {
"index_type": "IVF_FLAT",
"metric_type": "COSINE",
"params": {"nlist": 1414}, # sqrt(2_000_000) ≈ 1414
})
Validation
# Validation log — October 31 2026
# milvus 2.4.5 standalone; pymilvus 2.4.5; Python 3.11
# ---- Before fix ----
# collection.load_state: 'NotLoad'
# collection.search() → MilvusException: collection not loaded (error 65535)
# 100% of search requests failing for 3 weeks
# Index rebuilds with nlist=64,128,256,1024: no change — error unrelated to nlist
# ---- After fix ----
# collection.load() called after create_index()
# utility.wait_for_loading_complete("documents") completed in 42s (200k vectors, 1536 dims, IVF_FLAT)
# collection.load_state: 'Loaded'
# Startup health check: [OK] Collection 'documents': Loaded, 200000 entities, index type: IVF_FLAT
# ---- Search performance ----
# 200,000 vectors, 1536 dims, IVF_FLAT nlist=448, nprobe=16, COSINE metric
# p50 query latency: 8ms
# p99 query latency: 22ms
# Recall@10 vs brute force: 97%
# ---- Restart test ----
# Milvus process killed and restarted
# Startup health check detected load_state='NotLoad'
# collection.load() called automatically
# wait_for_loading_complete() blocked for 38s
# All subsequent search() calls: no MilvusException
# Retainer engagement cost: 1.5h (estimated $120-180 at mid-level rates)
Related retainer posts
- Weaviate developer on retainer — batch import silently drops objects when
with_batch_size()context exits beforeflush()—weaviate-client 3.x - Qdrant developer on retainer — payload filter on keyword field returns empty results when field not indexed —
qdrant-client 1.x - ChromaDB developer on retainer — embedding model switched mid-collection — cosine similarity meaningless across incompatible vector spaces — recall@10 drops 73pp — no error
Track retainer hours without a client portal
HourTab turns your time-tracker CSV into a public URL your client bookmarks. No login. No portal. CSV in, URL out.
See pricing →