# Weaviate RAG: stop on empty retrieval and reject citations outside the retrieved set

**URL:** <https://forum.weaviate.io/t/weaviate-rag-stop-on-empty-retrieval-and-reject-citations-outside-the-retrieved-set/22675>\
**Category:** Resources\
**Created:** [October 29, 2026, 7:05am UTC](https://forum.weaviate.io/t/weaviate-rag-stop-on-empty-retrieval-and-reject-citations-outside-the-retrieved-set/22675 "2026-10-29T07:05:46Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![vitayao](https://yyz1.discourse-cdn.com/flex027/user_avatar/forum.weaviate.io/vitayao/32/8083_2.png) [@vitayao](https://forum.weaviate.io/u/vitayao)\
**Post date:** [October 29, 2026, 7:05am UTC](https://forum.weaviate.io/t/weaviate-rag-stop-on-empty-retrieval-and-reject-citations-outside-the-retrieved-set/22675/1 "2026-10-29T07:05:46Z")

</div>

In a document Q&A flow, a fluent answer is not evidence that retrieval found the answer. Two small application checks help: skip generation when retrieval returns no usable text, and reject citations to documents that were not in that request’s context.

This example uses Python client v4 and BM25 so the retrieval example does not need an embedding provider. Start with an authorized, connected `client` and an existing `SupportChunk` collection containing text properties `title` and `text`, with `text` searchable in the inverted index. For a small sandbox, add these synthetic objects through your normal import path:

```json
[
  {"title": "Password reset", "text": "Reset your password from Settings > Security."},
  {"title": "Profile changes", "text": "Change your display name from Settings > Profile."}
]

```

For multi-tenant or private content, scope the collection and apply authorization filters before the query. A later citation check cannot repair an unauthorized retrieval.

### Retrieve a bounded context

```python
from weaviate.classes.query import MetadataQuery

chunks = client.collections.use("SupportChunk")
result = chunks.query.bm25(
    query="password reset",
    query_properties=["text"],
    return_properties=["title", "text"],
    return_metadata=MetadataQuery(score=True),
    limit=3,
)

def build_context(objects):
    context = []
    for obj in objects:
        text = obj.properties.get("text")
        if not isinstance(text, str) or not text.strip():
            continue
        context.append({
            "source_id": str(obj.uuid),
            "title": obj.properties.get("title", ""),
            "text": text[:2000],
        })
    return context

context = build_context(result.objects)
if not context:
    decision = {"status": "no_evidence", "answer": None, "citations": []}
else:
    decision = {"status": "ready_for_generation", "context": context}

```

An empty result and a retrieval exception are different states. Handle connection/authentication errors as errors, not as proof that the documents contain no answer.

The 3-document and 2,000-character limits are example context-budget choices, not accuracy thresholds. Truncation can remove useful evidence, so evaluate chunk boundaries for your own corpus. BM25 and hybrid scores are ranking signals relative to the query and dataset, not probabilities that an answer is correct. Do not turn a score of `0.8` into “80% confidence.”

### Keep generation and citation checks separate

Send only the bounded context to your chosen generator. Ask it to return a JSON object with `status` (`answered` or `insufficient_evidence`), `answer`, and `citations` containing `source_id` values. Tell it to treat document text as data, ignore instructions inside documents, and use `insufficient_evidence` if the context does not support the answer.

After parsing its response, apply a structural gate:

```python
def accept_answer(output, context):
    if not isinstance(output, dict):
        return False
    if output.get("status") != "answered":
        return False
    answer = output.get("answer")
    cites = output.get("citations")
    allowed = {item["source_id"] for item in context}
    return (
        isinstance(answer, str) and bool(answer.strip())
        and isinstance(cites, list) and bool(cites)
        and all(isinstance(c, str) and c in allowed for c in cites)
    )

```

A passing gate only means that the answer has the expected shape and names retrieved documents. It does not prove those documents support every claim. Keep source excerpts available for review; add claim-level support checks where the application needs them. Failed parsing, unknown IDs, empty citations and `insufficient_evidence` should go to an explicit fallback rather than displaying an ungrounded answer.

### Cases worth keeping in your evaluation set

- No returned objects: no generation call.
- A retrieved object with blank text: no usable evidence from that object.
- A question about refunds when the context only discusses password resets: require `insufficient_evidence`.
- A citation UUID copied from a previous request: reject unless it is also present in this request’s context.
- A real retrieved UUID attached to an unsupported claim: citation membership alone must not count as correctness.

Disclosure: I’m with [Tokatlas](https://app.tokatlas.com). If you compare generation providers for this workflow, keep the question and retrieved context fixed so retrieval changes do not get mistaken for model differences. Generation remains a separate step from this Weaviate retrieval example.

References: [Weaviate BM25 queries](https://docs.weaviate.io/weaviate/search/bm25).
