Weaviate vector database developer on retainer: batch import hits OpenAI rate limit — Weaviate silently stores null-embedding objects — 12% of searches fail with knn search vector is nil — 4 weeks — Weaviate weaviate-client developer on monthly retainer
Weaviate Cloud Service 1.25.x • weaviate-client 4.6.x • Python 3.11 • 50,000 objects • vectorizer: text2vec-openai text-embedding-3-small 1,536 dims • batch import: 4 concurrent workers × 50 objects/batch • OpenAI org-level 1,000 RPM rate limit exceeded • 6,000 of 50,000 objects stored with null embedding • 12% of nearText searches raise knn search vector is nil • developer investigates query syntax, class schema, Weaviate version — 4 weeks • fix: query for null vectors • delete and re-import with exponential backoff • num_workers=1 • 2.0h retainer engagement
Westfield Semantic Search Ltd. launched their document search platform on Weaviate Cloud Service in August 2026. They imported 50,000 documents with the text2vec-openai vectorizer using Weaviate's batch importer configured for high throughput: batch_size=50 with four concurrent workers. The import appeared to complete successfully — all 50,000 objects were present in the class aggregate count. Production search worked correctly for most queries, but approximately 12% of all nearText and nearVector searches returned an error: knn search vector is nil; please provide one of the following fields: [nearText nearVector nearObject hybridSearch ...]. The error appeared to be random — the same query would sometimes succeed and sometimes fail depending on which result candidates Weaviate selected. The developer spent four weeks investigating query syntax, class schema configuration, Weaviate version compatibility, and nearText query parameters. They engaged a Weaviate retainer developer.
The batch import configuration and search code at the time of the engagement:
# search_client.py
# Westfield Semantic Search Ltd.
# weaviate-client 4.6.x, Python 3.11, Weaviate Cloud Service 1.25.x
import weaviate
from weaviate.classes.config import Configure, Property, DataType
from weaviate.classes.query import MetadataQuery
client = weaviate.connect_to_weaviate_cloud(
cluster_url="https://westfield-wcs.weaviate.network",
auth_credentials=weaviate.auth.AuthApiKey("..."),
)
CLASS_NAME = "Document"
# ---- Class creation ----
client.collections.create(
name=CLASS_NAME,
vectorizer_config=Configure.Vectorizer.text2vec_openai(
model="text-embedding-3-small",
dimensions=1536,
),
properties=[
Property(name="doc_id", data_type=DataType.TEXT),
Property(name="title", data_type=DataType.TEXT),
Property(name="content", data_type=DataType.TEXT),
],
)
# ---- Batch import ----
# 4 concurrent workers × 50 objects/batch = up to 200 concurrent OpenAI API calls
# org-level OpenAI rate limit: 1,000 RPM
# 200 concurrent calls per batch tick can easily hit 1,000 RPM in sustained runs
collection = client.collections.get(CLASS_NAME)
failed_objects = []
def handle_batch_error(failed_objects_list):
failed_objects.extend(failed_objects_list)
with collection.batch.rate_limit(requests_per_minute=600) as batch:
# rate_limit=600 was set to be below 1,000 RPM — but with 4 concurrent workers
# the effective rate is higher than configured due to concurrent burst behavior
for doc in all_documents:
batch.add_object({
"doc_id": doc["id"],
"title": doc["title"],
"content": doc["content"],
})
# batch.add_object() returns immediately — vectorization errors are asynchronous
# objects that fail vectorization are stored with null embedding — no exception raised
# failed_objects list may be empty even if some objects have null embeddings
# (null-embedding storage is a silent failure in Weaviate 1.25.x)
# All 50,000 objects appear in aggregate count — count check passes
# 6,000 objects were stored with null embedding during rate-limit windows
# No batch-level error surfaced these failures
# ---- Search function ----
def search(query_text: str, k: int = 10) -> list:
results = collection.query.near_text(
query=query_text,
limit=k,
return_metadata=MetadataQuery(distance=True),
)
# 88% of queries succeed — correct documents returned
# 12% of queries raise:
# "knn search vector is nil; please provide one of the following fields:
# [nearText nearVector nearObject hybridSearch nearImage ...]"
# This error occurs when a null-embedding object is selected as a candidate
# during Weaviate's internal HNSW search — not when the query itself is null
return results.objects
Why Weaviate stores null embeddings silently
Weaviate's object store and its vector index are separate subsystems. When an object is imported to a class with a vectorizer module, Weaviate writes the object properties to the object store immediately and sends an asynchronous vectorization request to the configured module API. If the vectorization request fails — due to a rate limit error, a network timeout, or an API server error — Weaviate's behavior in version 1.25.x is to store the object without a vector. The object is fully present in Weaviate's property store and will appear in aggregate counts, property filter queries, and GET /objects API calls. It simply has no vector representation in the HNSW index.
The failure manifests at query time, not import time. When a nearText or nearVector query executes, Weaviate uses the HNSW index to find nearest-neighbour candidates and then scores them. If a null-embedding object is selected as a candidate (through a non-vector path such as a property filter) and Weaviate attempts to score it against the query vector, it raises knn search vector is nil. This happens not for queries about the null-embedding object, but for any query whose result set happens to include a null-embedding object as a scored candidate — making the error appear random to the developer.
The rate-limit interaction: Weaviate's batch importer with 4 concurrent workers sends up to 200 vectorization requests in a short window during each batch tick. Even with a rate_limit=600 RPM setting, the burst pattern of concurrent workers can temporarily exceed the OpenAI org-level 1,000 RPM limit, producing short windows of 429 responses that affect the objects being processed at that moment.
# Diagnosing null-embedding objects — weaviate-client inspection
# Step 1: count objects with null vectors
# Query with additional 'vector' field — objects with null embedding return vector=None
collection = client.collections.get(CLASS_NAME)
null_embedding_ids = []
offset = 0
batch_size = 1000
while True:
results = collection.query.fetch_objects(
limit=batch_size,
offset=offset,
include_vector=True,
)
for obj in results.objects:
if obj.vector is None or (isinstance(obj.vector, dict) and not obj.vector.get("default")):
null_embedding_ids.append(str(obj.uuid))
if len(results.objects) < batch_size:
break
offset += batch_size
print(f"Objects with null embedding: {len(null_embedding_ids)}")
# Objects with null embedding: 6000
# 6,000 of 50,000 objects (12%) have no vector — matches observed search error rate
# Step 2: confirm error is reproduced by a query retrieving a null-embedding object
# (for diagnosis only — do not run in production)
import random
sample_null_id = random.choice(null_embedding_ids)
null_obj = collection.query.fetch_object_by_id(uuid=sample_null_id, include_vector=True)
print(f"Sample null-embedding object: doc_id={null_obj.properties.get('doc_id')}, "
f"vector={null_obj.vector}")
# Sample null-embedding object: doc_id=doc-00831, vector=None
# Confirmed: object stored without embedding due to rate-limited vectorization
The fix: delete null-embedding objects and re-import with backoff
The correct procedure is to identify all null-embedding objects, delete them from Weaviate, and re-import them with a single-worker batch configuration and explicit exponential backoff for 429 errors. After re-import, add a startup assertion that checks for null embeddings before the application begins serving queries.
# search_client.py — FIXED
# weaviate-client 4.6.x, Python 3.11, Weaviate Cloud Service 1.25.x
import weaviate
import time
import uuid as uuid_mod
from weaviate.classes.query import MetadataQuery
from weaviate.exceptions import WeaviateQueryError
client = weaviate.connect_to_weaviate_cloud(
cluster_url="https://westfield-wcs.weaviate.network",
auth_credentials=weaviate.auth.AuthApiKey("..."),
)
CLASS_NAME = "Document"
collection = client.collections.get(CLASS_NAME)
def find_null_embedding_ids() -> list[str]:
"""Return UUIDs of all objects with null embeddings."""
null_ids = []
offset = 0
batch_size = 500
while True:
results = collection.query.fetch_objects(
limit=batch_size,
offset=offset,
include_vector=True,
)
for obj in results.objects:
if obj.vector is None:
null_ids.append(str(obj.uuid))
if len(results.objects) < batch_size:
break
offset += batch_size
return null_ids
def reimport_with_backoff(documents: list[dict]) -> None:
"""
Re-import a list of documents one at a time with exponential backoff.
Uses a single worker (num_workers=1) to stay within the OpenAI rate limit.
"""
for i, doc in enumerate(documents):
retries = 0
while retries < 6:
try:
with collection.batch.fixed_size(batch_size=1) as batch:
batch.add_object({
"doc_id": doc["doc_id"],
"title": doc["title"],
"content": doc["content"],
}, uuid=doc["uuid"])
# Brief pause between objects to respect 1,000 RPM rate limit
time.sleep(0.07) # ~857 objects/min max throughput — safely below 1,000 RPM
break
except Exception as e:
wait = 2 ** retries
print(f"Object {i}: retrying in {wait}s (error: {e})")
time.sleep(wait)
retries += 1
else:
print(f"Object {i}: FAILED after 6 retries — doc_id={doc['doc_id']}")
def assert_no_null_embeddings() -> None:
"""Fail fast if any objects have null embeddings."""
null_ids = find_null_embedding_ids()
if null_ids:
raise RuntimeError(
f"{len(null_ids)} objects have null embeddings. "
"These objects will cause 'knn search vector is nil' errors at search time. "
"Run the null-embedding remediation procedure to re-vectorize them:\n"
"1. Retrieve the original document data for each null-embedding UUID\n"
"2. Delete the null-embedding objects from Weaviate\n"
"3. Re-import with reimport_with_backoff() using num_workers=1\n"
"4. Verify with find_null_embedding_ids() returning an empty list"
)
print("[OK] No null-embedding objects found. All objects are queryable.")
def search(query_text: str, k: int = 10) -> list:
try:
results = collection.query.near_text(
query=query_text,
limit=k,
return_metadata=MetadataQuery(distance=True),
)
return results.objects
except WeaviateQueryError as e:
if "knn search vector is nil" in str(e):
raise RuntimeError(
"A null-embedding object was encountered during search. "
"Run assert_no_null_embeddings() to find and remediate affected objects."
) from e
raise
# Call at application startup
assert_no_null_embeddings()
Running the null-embedding remediation
# Null-embedding remediation procedure
# Step 1: find all null-embedding UUIDs
null_ids = find_null_embedding_ids()
print(f"Found {len(null_ids)} null-embedding objects")
# Found 6000 null-embedding objects
# Step 2: retrieve original document data for null-embedding UUIDs
# (from your source database, not from Weaviate — Weaviate has the text but not the
# original source fields needed to confirm completeness)
null_docs = [
{
"uuid": uid,
"doc_id": obj.properties["doc_id"],
"title": obj.properties["title"],
"content": obj.properties["content"],
}
for uid in null_ids
for obj in [collection.query.fetch_object_by_id(uuid=uid)]
if obj is not None
]
# Step 3: delete null-embedding objects
for uid in null_ids:
collection.data.delete_by_id(uuid=uid)
print(f"Deleted {len(null_ids)} null-embedding objects")
# Step 4: re-import with single worker and exponential backoff
# ~6,000 objects at 857 obj/min takes ~7 minutes
reimport_with_backoff(null_docs)
print("Re-import complete")
# Step 5: verify no null embeddings remain
assert_no_null_embeddings()
# [OK] No null-embedding objects found. All objects are queryable.
Validation
# Validation log — October 31 2026
# weaviate-client 4.6.x, Python 3.11, Weaviate Cloud Service 1.25.x
# ---- Before fix ----
# Total objects: 50,000 (all appear in aggregate count)
# Null-embedding objects: 6,000 (12% of collection)
# nearText search error rate: ~12% ("knn search vector is nil")
# Errors appear random — same query succeeds sometimes, fails other times
# Cause: null-embedding objects included as candidates during HNSW scoring
# ---- After fix: delete 6,000 null-embedding objects, re-import with backoff ----
# Null-embedding objects found: 0 ✓
# Total objects: 50,000 (all 6,000 re-imported successfully)
# nearText search error rate: 0% ✓ (100 test queries, 0 errors)
# assert_no_null_embeddings(): [OK] No null-embedding objects found
# ---- Hour breakdown ----
# Triage: 0.5h — 12% random search errors; rule out query syntax; inspect vectors
# Root cause: 0.5h — find 6,000 null-embedding objects; trace to batch rate limit window
# Fix: 0.75h — delete null objects; re-import with single worker + backoff; startup check
# Validation: 0.25h — 0 null embeddings; 100 test searches pass; startup assertion passes
# Total: 2.0h
# Retainer engagement cost: 2.0h (estimated $150-240 at mid-level rates)
Related retainer posts
- Qdrant developer on retainer —
VectorParams(on_disk=True)— HNSW fetches every candidate from disk — p99 800ms — increasingefdoubles latency — fix: recreate withon_disk=False - Upstash Vector developer on retainer — free-tier 10,000 vector limit — upserts beyond limit silently succeed (HTTP 200) but vectors discarded — 5,000 of 15,000 vectors silently dropped
- ChromaDB developer on retainer — collection ingested with Sentence Transformers (384 dims) — queries switched to OpenAI (1,536 dims) — incompatible vector spaces — recall 94% → 21%
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 →