Problem – Retrieval‑Augmented Generation (RAG) Returns Empty or Incomplete Answers in Batch Jobs
In a production batch pipeline, dozens to thousands of queries are sent to Elasticsearch via the _msearch API. The retrieval component successfully runs, but the generation stage receives either an empty context field or a truncated list of documents. Consequently the final answers are missing critical information or are completely blank.
Typical symptoms observed in logs:
2026-05-30T12:14:07.842Z ERROR [rag-orchestrator] - No documents found for query id 7f3a9c12‑d4b1‑4e8a‑a9f2‑c5e6b7f9d3e1, skipping generation step
2026-05-30T12:14:07.845Z WARN [batch-worker] - ElasticsearchException: search_phase_execution_exception: all shards failed
2026-05-30T12:14:07.846Z WARN [batch-worker] - RejectedExecutionException: thread pool [search] is full
2026-05-30T12:14:07.850Z INFO [rag-orchestrator] - Generated answer: "" (empty string)
These messages appear despite the index containing relevant knowledge‑base documents and the msearch request returning hits when executed individually.
Root Cause Analysis
1. Query Parameter Omission in Batch Requests
The official RAG guide (Elastic Documentation – RAG guide) shows a retrieval query that explicitly sets size (e.g., "size": 5) to limit the number of context documents. In several batch jobs the size field was omitted, causing Elasticsearch to fall back to the default size=10. When combined with a from offset inherited from a previous request, the effective window exceeded max_result_window, triggering the error:
Result window is too large, request [from + size] exceeds max_result_window
Because the error is thrown per‑shard, the orchestrator logs “all shards failed” and treats the response as a successful empty hit set.
2. Thread‑Pool Saturation Under High Concurrency
Batch pipelines often run msearch with a concurrency level that exceeds the default search thread pool size (defaults to node.max_local_storage_nodes * 3). When the pool is exhausted, Elasticsearch returns RejectedExecutionException: thread pool [search] is full. The RAG layer interprets the partial success as “no documents” for the affected queries.
3. Index Refresh Lag
Knowledge‑base updates are indexed just before the batch starts. With the default refresh interval of 1s, newly indexed documents may not be searchable during the first few msearch calls. This timing issue is documented in the ingest pipeline guide (Ingest pipelines) and explains why some queries receive zero hits while later ones succeed.
4. Scripted Scoring Misconfiguration
Some implementations use a script_score query to boost relevance based on custom metadata. A subtle syntax error (e.g., missing closing brace) in the search template causes the script to evaluate to null, which Elasticsearch treats as a filter that discards all documents. The RAG orchestrator logs “No documents found for query id …” even though the request syntax is otherwise valid.
Investigation and Debugging Steps
- Reproduce with a single query. Run the same query that fails in the batch using
curlorPOST /index/_search. Verify that hits are returned. - Inspect the
_msearchpayload. Capture the raw HTTP body sent by the batch worker. - Check thread‑pool health. Use the Cat API:
curl -XGET 'http://es-node:9200/_cat/thread_pool/search?v&h=id,active,rejected,largest'
Look for non‑zero rejected counts.
- Validate index refresh timing. Query the
_refreshAPI before the batch starts:
curl -XPOST 'http://es-node:9200/_refresh'
curl -XGET 'http://es-node:9200/_scripts/rag_template?pretty'
Look for JSON parsing errors such as “Failed to parse query: Unexpected token”.
es.log:2026-05-30 12:14:07,842 [search] [node-1] [WARN ][o.e.search.SearchPhaseController] [node-1] search_phase_execution_exception: all shards failed
Resolution – Making Retrieval Reliable in Batch RAG Pipelines
1. Explicitly Set size and Enforce max_result_window
Update the search template to always include a safe size (e.g., 5) and guard against large from values.
Before (template snippet):
{
"size": "{{size}}",
"query": { ... }
}
After (template snippet):
{
"size": 5,
"query": { ... }
}
2. Increase Search Thread‑Pool Capacity
Adjust the node settings in elasticsearch.yml or via the Cluster Update Settings API:
# elasticsearch.yml
thread_pool.search.size: 30
thread_pool.search.queue_size: 200
# API call
curl -XPUT 'http://es-node:9200/_cluster/settings' -H 'Content-Type: application/json' -d '
{
"persistent": {
"thread_pool.search.size": "30",
"thread_pool.search.queue_size": "200"
}
}'
Raising the pool prevents RejectedExecutionException under high concurrency.
3. Force Index Refresh Before Batch Starts
Integrate an explicit refresh step in the pipeline orchestration script:
# Bash snippet
curl -s -XPOST "http://es-node:9200/_refresh" && echo "Index refreshed"
Alternatively, set refresh_interval to -1 during bulk indexing and manually refresh once the bulk load completes.
4. Harden Search Template Validation
Enable strict JSON validation and add a fallback clause that logs the rendered query before execution.
# Example of a wrapper script in Python
import json, logging, requests
def render_template(template_id, params):
resp = requests.get(f"http://es-node:9200/_scripts/{template_id}")
tmpl = resp.json()['_source']['script']['source']
rendered = tmpl.format(**params)
try:
json.loads(rendered) # validates JSON
except json.JSONDecodeError as e:
logging.error(f"Template rendering failed: {e}")
raise
return rendered
Verification – Confirming the Fix
- Run a representative batch of
_msearchrequests and verify that each response contains ahits.total.value > 0field. - Check orchestrator logs for the absence of “No documents found for query id” messages.
- Monitor the
searchthread‑pool metrics;rejectedcount should stay at zero. - Execute a downstream generation call and confirm that the answer includes the expected context snippets.
Sample successful log excerpt:
2026-05-31T03:02:15.123Z INFO [rag-orchestrator] - Retrieved 4 documents for query id 9b2e1f4a‑c7d3‑4f8b‑a1e5‑d9f6c2b8e7a0
2026-05-31T03:02:15.456Z INFO [generation] - Generated answer: "The user can reset their password by clicking the 'Forgot password' link on the login page..."
Prevention – Operational Guardrails
| Guardrail | Implementation |
|---|---|
| Enforce query size limits | Include size in every search template; add CI lint that rejects templates missing the field. |
| Thread‑pool capacity monitoring | Set up Kibana alerts on node.search.rejected > 0 over a 5‑minute window. |
| Refresh awareness | Ingest pipeline emits a custom event after bulk indexing; downstream batch job subscribes and waits for the event before starting. |
| Template syntax validation | Run a pre‑deployment test that renders each stored script with a dummy payload and parses the result. |
| Result window safety | Configure index.max_result_window to a conservative value (e.g., 1000) and add a request‑validation step that aborts if from + size exceeds it. |
FAQ – Common Follow‑Up Questions
- Why does the retrieval work for a single query but fail in
_msearch? In batch mode the request payload may omit required parameters (likesize) or inherit pagination offsets from previous sub‑requests, leading to result‑window violations. - How can I tell which documents were actually sent to the generation model? Enable
_sourcein the retrieval query and log thehits.hits._sourcearray before passing it downstream. The RAG guide recommends using ascript_fieldsprocessor to format the context. - What is the safest concurrency level for
_msearch? Start with a concurrency ofnode.max_local_storage_nodes * 2and monitornode.search.rejected. Adjust upward only after confirming that the thread‑pool queue remains low. - Can I avoid explicit refreshes and still see newly indexed docs? Set
refresh_intervalto-1during bulk load, then callPOST /_refreshonce. This guarantees visibility without incurring the overhead of per‑request refreshes. - Is there a way to automatically retry queries that hit
RejectedExecutionException? Wrap the_msearchcall in a retry loop with exponential back‑off. Elasticsearch client libraries (e.g., the Java High Level REST Client) exposeRetryableExceptionthat can be caught and retried.
Related Topic Hub: Data Infrastructure Troubleshooting Hub