n4nAI

Debugging Weaviate schema mismatches in production

Practical steps for Weaviate schema mismatch debugging in production: confirm symptoms, diff schemas, reproduce, migrate data, and prevent recurrence.

n4n Team4 min read901 words

Audio narration

Coming soon — every post will get a voice note here.

A schema mismatch in Weaviate rarely surfaces as a clear exception at write time. More often, production queries return empty result sets or near-vector searches silently degrade, and Weaviate schema mismatch debugging turns into forensic reconstruction of class definitions that drifted from your code. The following ordered path is the one we use to isolate, reproduce, and fix these issues without extended downtime.

1. Confirm the Symptom Is a Schema Mismatch

Not every empty query is a schema problem. Before starting Weaviate schema mismatch debugging, rule out vectorizer outages, network partitions, and client-side filter bugs. Hit the schema endpoint directly:

curl -X GET "http://localhost:8080/v1/schema" \
  -H "Authorization: Bearer $WEAVIATE_API_KEY" | jq '.classes[] | {class: .class, properties: [.properties[].name]}'

If a class you expect is missing, or properties have unexpected dataType, you have a mismatch. A common red herring is a vectorizer config pointing to a deleted module; that fails at import, not at query. Check object counts and whether vectors exist:

curl "http://localhost:8080/v1/objects/Article?limit=1" \
  -H "Authorization: Bearer $WEAVIATE_API_KEY" | jq '._additional'

If _additional lacks vector, the class imported without embeddings.

2. Capture the Live Schema and the Expected Schema

Pull the live schema with the Python client and compare to your version-controlled schema definition. Store expected schema as JSON in Git so every environment has a source of truth.

import weaviate, json
client = weaviate.Client("http://localhost:8080", auth_client_secret="TOKEN")
live = client.schema.get()
with open("live_schema.json", "w") as f:
    json.dump(live, f, indent=2)

A minimal diff script:

import json
live = json.load(open("live_schema.json"))
expected = json.load(open("expected_schema.json"))

live_classes = {c["class"]: c for c in live["classes"]}
for c in expected["classes"]:
    name = c["class"]
    if name not in live_classes:
        print(f"MISSING CLASS: {name}")
        continue
    live_props = {p["name"]: p for p in live_classes[name].get("properties", [])}
    for p in c.get("properties", []):
        if p["name"] not in live_props:
            print(f"MISSING PROP {name}.{p['name']}")
        elif live_props[p["name"]]["dataType"] != p["dataType"]:
            print(f"TYPE MISMATCH {name}.{p['name']}: live {live_props[p['name']]['dataType']} expected {p['dataType']}")

Weaviate schema mismatch debugging hinges on this diff. Typical divergences: dataType of a property changed from ["text"] to ["string"]; vectorIndexType changed from hnsw to flat; moduleConfig lost text2vec-openai causing objects to have no vectors.

3. Identify the Specific Divergence

Weaviate does not allow altering property types in place. Knowing exactly what changed dictates the migration path.

Property Type and Tokenization

If you defined title as text but live is string, queries using tokenization will break. Example live property:

{
  "name": "title",
  "dataType": ["string"],
  "tokenization": "word"
}

Expected:

{
  "name": "title",
  "dataType": ["text"],
  "tokenization": "whitespace"
}

The string type is deprecated; text is required for full-text search.

Vectorizer and Module Config

A class might have been created with none vectorizer, then code assumes text2vec-cohere. The schema won’t show vectors unless you inspect an object. This is a frequent trigger for Weaviate schema mismatch debugging because the failure appears only when you call with_near_text.

Vector Dimension Drift

If you swapped embedding models (e.g., 1536-dim to 768-dim), existing objects keep old vectors. New imports use new dims, causing vector dimension mismatch errors. The class schema doesn’t expose dimensions; only the module config implies it. Record model versions in moduleConfig.

Index Parameters

hnsw efConstruction or maxConnections changes don’t break reads but alter recall. Not a true mismatch but worth noting during review.

4. Reproduce in an Isolated Environment

Never experiment on production. Launch a local Weaviate with Docker, mirroring production modules:

docker run -d -p 8080:8080 \
  -e AUTHENTICATION_APIKEY_ENABLED=true \
  -e AUTHENTICATION_APIKEY_ALLOWED_KEYS=localkey \
  -e ENABLE_MODULES=text2vec-openai,text2vec-cohere \
  semitechnologies/weaviate:1.23.0

Apply the exported live schema to this instance, then run the failing query from your app. If it fails identically, you’ve confirmed the mismatch. Pitfall: local Weaviate without the same vectorizer module will not reproduce vector generation issues. Use the same ENABLE_MODULES env.

Tradeoff: reproducing with full production data may be impossible due to PII. Sample 1k objects using cursor:

cursor = None
for _ in range(10):
    res = client.query.get("Article", ["title"]).with_limit(100)
    if cursor: res = res.with_after(cursor)
    batch = res.do()
    # store batch
    cursor = batch["data"]["Get"]["Article"][-1]["_additional"]["id"]

That’s enough to trigger the error.

5. Remediate Without Losing Data

Weaviate’s schema API rejects PATCH on property types. You must create a new class with the correct schema and migrate objects. This phase of Weaviate schema mismatch debugging requires careful coordination.

Step A: Create Corrected Class

new_class = {
    "class": "Article_v2",
    "vectorizer": "text2vec-openai",
    "properties": [
        {"name": "title", "dataType": ["text"], "tokenization": "whitespace"},
        {"name": "body", "dataType": ["text"]}
    ]
}
client.schema.create_class(new_class)

Step B: Dual-Write or Backfill

For zero downtime, change app to write to both Article and Article_v2 (dual-write). Then backfill historical objects:

def migrate(obj):
    client.data_object.create(obj["properties"], "Article_v2",
                              vector=obj.get("vector"))
# iterate with cursor as above

If vector dimensions changed, you cannot copy vectors; you must re-embed. That means calling your embedding service per object.

Step C: Swap Reads

After backfill, point reads to Article_v2. Monitor error rates. Then drop old class:

client.schema.delete_class("Article")

Tradeoff: dual-write complicates rollback; if new class has bugs, you’ve polluted it. Alternative is maintenance window with full migrate—simpler but downtime.

6. Validate Queries and Vector Consistency

Run near-vector queries against both old and new classes with same vector:

near_vector = {"vector": [0.1, -0.2, 0.05]}
old_res = client.query.get("Article", ["title"]).with_near_vector(near_vector).do()
new_res = client.query.get("Article_v2", ["title"]).with_near_vector(near_vector).do()

If result counts diverge >1%, inspect missing objects’ vectors. A frequent cause is that the old class used a different vectorizer; you cannot copy vectors if dimensions differ—you must re-embed. If you’re generating embeddings through a gateway, note that n4n.ai honors client routing directives and forwards provider cache-control hints, so you can pin a specific embedding model version to guarantee dimension stability during re-embedding.

Also validate hybrid queries and bm25 filters on text fields. A tokenization mismatch will return zero hits on where filters even when data exists.

7. Prevent Recurrence

Weaviate schema mismatch debugging is reactive; prevention is cheaper.

  • Store schema as code (JSON/YAML) in repo.
  • Add CI step that spins a scratch Weaviate container and applies expected schema, failing on diff.
  • Alert on schema drift: a cron job that pulls live schema and compares to Git, paging on diff.
  • Document embedding model versions in the class moduleConfig. If you change models, bump class name suffix (_v3).
  • Never use client.schema.update expecting type changes; it only updates metadata.

Common Pitfalls

  • Assuming client.schema.update modifies types—it doesn’t.
  • Ignoring tenant schemas in multi-tenant classes; each tenant inherits but can override.
  • Forgetting that text vs string changes tokenization, breaking bm25 filters.
  • Copying objects without vectors after a vectorizer change, then wondering why with_near_vector fails.

Treat schema as immutable versioned artifacts. The moment you allow ad-hoc changes in production, you sign up for 3am incidents.

8. Operational Checklist

  1. Confirm symptom via /v1/schema and object inspection.
  2. Diff live vs expected schema in code.
  3. Identify exact divergence (type, vectorizer, dims).
  4. Reproduce in Docker with same modules.
  5. Create new versioned class, dual-write, backfill.
  6. Validate query parity and vector dims.
  7. Swap reads, delete old class, close the loop in CI.

Following this ordered path makes Weaviate schema mismatch debugging a routine, low-risk operation rather than a fire drill.

Tagsweaviateschemadebuggingproduction

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All vector database observability posts →