Problem – Missing or Corrupted RAG Context after Pipeline Serialization
During the nightly data pipeline that populates a Redis vector store for Retrieval‑Augmented Generation (RAG), the downstream language‑model inference service receives prompts that lack the expected retrieved documents. The prompts contain empty context fields or truncated JSON blobs, resulting in hallucinations or incomplete answers.
Typical symptoms observed in the logs of the ingestion service and the inference service are:
2026-10-13 02:15:42,871 ERROR pipeline - SerializationError: Object of type Document is not JSON serializable
2026-10-13 02:15:43,102 WARN redis_client - Failed to set key rag:ctx:12345: TTL mismatch, existing TTL=0, new TTL=3600
2026-10-13 02:15:44,210 INFO inference - Prompt built: {"question":"What is the capital of France?","context":""}
Root Cause – Incompatible Serialization + Expiration Policy Conflict
The pipeline serializes Document objects (containing embeddings, metadata, and raw text) using json.dumps(). The default json encoder cannot handle NumPy float32 arrays that store embeddings, causing a SerializationError. The error is caught, but the code proceeds to write an empty string to Redis.
Simultaneously, the Redis client sets a TTL of 3600 seconds on the context key, while a background redis-cli --scan job that enforces a global no‑expire policy for keys matching rag:ctx:* overwrites the TTL to 0. When the key already exists with TTL=0, the SET command with EX 3600 fails silently, leaving the stale (empty) value in place.
Debug – Step‑by‑Step Investigation
1. Reproduce the failure locally
# Run the ingestion script with a single document
python ingest.py --doc-id 12345
Expected output: successful SET with a JSON payload containing embedding array.
Observed output: exception trace shown above and an empty value stored.
2. Inspect the stored key
$ redis-cli GET rag:ctx:12345
""
$ redis-cli TTL rag:ctx:12345
-1 # indicates no expiration (key persisted)
3. Verify serialization path
>>> from pipeline import serialize_document
>>> doc = Document(id="12345", text="Paris is ...", embedding=np.random.rand(768).astype('float32'))
>>> serialize_document(doc)
Traceback (most recent call last):
File ".../pipeline.py", line 42, in serialize_document
return json.dumps(doc.__dict__)
TypeError: Object of type ndarray is not JSON serializable
4. Check TTL enforcement job
$ ps aux | grep ttl_enforcer
redis-cli --scan --pattern 'rag:ctx:*' | while read key; do
redis-cli PERSIST "$key"
done
The job runs every 5 minutes and removes TTL from any key that already exists, causing the later SET EX 3600 to be ignored.
Solution – Robust Serialization and Consistent TTL Management
1. Use a serializer that handles NumPy arrays
Replace the naive json.dumps() with orjson or a custom encoder that converts ndarray to a list of floats.
# before (pipeline.py)
def serialize_document(doc):
return json.dumps(doc.__dict__)
# after (pipeline.py)
import orjson
def _encode_ndarray(obj):
if isinstance(obj, np.ndarray):
return obj.tolist()
raise TypeError
def serialize_document(doc):
return orjson.dumps(doc.__dict__, default=_encode_ndarray).decode()
2. Guard against partial writes
Only write to Redis when serialization succeeds. Use a transaction (MULTI/EXEC) to ensure the key is set with the correct TTL atomically.
# before (redis_client.py)
def store_context(key, payload, ttl):
client.set(key, payload, ex=ttl)
# after (redis_client.py)
def store_context(key, payload, ttl):
pipe = client.pipeline()
pipe.set(key, payload)
pipe.expire(key, ttl)
pipe.execute()
3. Align TTL policy
Remove the conflicting “persist‑all” job or scope it to a different key pattern. If persistence is required for a subset, modify the script to skip rag:ctx: keys.
# before (ttl_enforcer.sh)
redis-cli --scan --pattern 'rag:*' | while read key; do redis-cli PERSIST "$key"; done
# after (ttl_enforcer.sh)
redis-cli --scan --pattern 'rag:cache:*' | while read key; do redis-cli PERSIST "$key"; done
Verify – Confirming the Fix
1. Unit test serialization
def test_serialize_document():
doc = Document(id="test", text="sample", embedding=np.zeros(768, dtype='float32'))
payload = serialize_document(doc)
assert isinstance(payload, str)
data = json.loads(payload)
assert isinstance(data["embedding"], list)
assert len(data["embedding"]) == 768
2. End‑to‑end pipeline run
$ python ingest.py --doc-id 99999
INFO:pipeline:Serialized document size=12456 bytes
INFO:redis_client:Stored key rag:ctx:99999 with TTL=3600
Check Redis:
$ redis-cli GET rag:ctx:99999 | wc -c
12456
$ redis-cli TTL rag:ctx:99999
3599
3. Inference service prompt
2026-10-14 00:02:11,043 INFO inference - Prompt built: {"question":"What is the capital of France?","context":"Paris is the capital ..."}
Prevent – Operational Guardrails
- Schema validation: Use
pydanticmodels for the payload before writing to Redis; reject objects that fail validation. - Monitoring: Emit a Prometheus counter
pipeline_serialization_errors_totaland set an alert on non‑zero values. - TTL consistency check: Periodic job that verifies all
rag:ctx:*keys haveTTL > 0and logs mismatches. - CI gate: Add integration tests that simulate a failed serialization and assert that no key is created.
- Dependency lock: Pin
orjsonandnumpyversions to avoid silent API changes.
Related Topic Hub: Data Infrastructure Troubleshooting Hub
FAQ
- Why does the pipeline sometimes succeed and sometimes fail? The failure occurs only when the document contains a NumPy
float32embedding. If the embedding is already a Python list (e.g., after a previous conversion),json.dumps()succeeds. - Can I keep the “persist‑all” job and still have TTL for RAG context? Yes, by excluding the
rag:ctx:prefix from the pattern or by adding a conditional check inside the script to skip those keys. - Is
orjsonthe only serializer that works? Any serializer that can convertndarrayto a JSON‑compatible type works (e.g.,jsonpicklewith custom handlers).orjsonis preferred for speed and deterministic output. - What if I need to store binary embeddings instead of JSON? Store the raw bytes in a separate Redis key (e.g.,
rag:vec:12345) usingSETEXwithbase64encoding, and keep only metadata in the JSON context key. - How can I detect stale context keys that were written with an empty payload? Query Redis for keys where
STRLENis less than a threshold (e.g.,STRLEN key < 100) and raise an alert.