fix(vector_stores): report a MongoDB text field that no matched document has

Atlas matches on the vector alone, so a mistyped mongodb_text_field still returns
confidently scored results whose content is empty, and the model is handed an
empty context with nothing to explain it. When every matched document lacks the
field the search now says which setting to fix; a sparse document among others
that do have it, and a document whose text is genuinely the empty string, both
still come back normally.

Unrecognised mongodb_* parameters are named too. The params model has to ignore
unrelated keys because litellm_params carries plenty of them, which turned a
mistyped mongodb_collection into "mongodb_collection is required" pointing the
reader at a key they can see they have set.
This commit is contained in:
Yuneng Jiang 2026-09-02 11:12:51 -07:00
parent a472484291
commit 1fed1029e0
No known key found for this signature in database
2 changed files with 103 additions and 8 deletions

View file

@ -128,6 +128,12 @@ class _MongoDBSearchParams(BaseModel):
return self.mongodb_collection
_MONGODB_PARAM_PREFIX: Final = "mongodb_"
_KNOWN_MONGODB_PARAMS: Final = frozenset(
name for name in _MongoDBSearchParams.model_fields if name.startswith(_MONGODB_PARAM_PREFIX)
)
class MongoDBVectorStoreConfig(BaseDirectVectorStoreConfig):
def __init__(
self,
@ -142,6 +148,22 @@ class MongoDBVectorStoreConfig(BaseDirectVectorStoreConfig):
self.sync_client_factory = sync_client_factory if sync_client_factory is not None else get_sync_client
self.async_client_factory = async_client_factory if async_client_factory is not None else get_async_client
@staticmethod
def _reject_unknown_params(litellm_params: Mapping[str, object]) -> None:
"""The params model ignores unrelated keys because litellm_params carries plenty of them,
which would otherwise turn a mistyped mongodb_collection into 'mongodb_collection is
required' pointing at a key the reader can see they have set."""
unknown: Final = sorted(
key
for key in litellm_params
if key.startswith(_MONGODB_PARAM_PREFIX) and key not in _KNOWN_MONGODB_PARAMS
)
if unknown:
raise config_error(
f"Unrecognised MongoDB vector store parameter(s): {', '.join(unknown)}. "
f"Supported: {', '.join(sorted(_KNOWN_MONGODB_PARAMS))}."
)
@staticmethod
def _query_text(query: str | Sequence[str]) -> str:
text: Final = query if isinstance(query, str) else " ".join(query)
@ -219,20 +241,22 @@ class MongoDBVectorStoreConfig(BaseDirectVectorStoreConfig):
]
@staticmethod
def _field_value(document: Mapping[str, object], dotted_path: str) -> str:
def _field_value(document: Mapping[str, object], dotted_path: str) -> str | None:
"""None means the path is absent from the document, which is what separates a
mistyped mongodb_text_field from a document whose text is genuinely empty."""
current: object = document
for segment in dotted_path.split("."):
if not isinstance(current, Mapping):
return ""
current = current.get(segment)
return "" if current is None else str(current)
if not isinstance(current, Mapping) or segment not in current:
return None
current = current[segment]
return None if current is None else str(current)
@classmethod
def _to_result(cls, document: Mapping[str, object], text_field: str) -> VectorStoreSearchResult:
document_id: Final = document.get("_id")
identifier: Final = None if document_id is None else str(document_id)
content: Final = [ # mutable-ok: VectorStoreSearchResult declares a list of content parts
VectorStoreResultContent(text=cls._field_value(document, text_field), type="text")
VectorStoreResultContent(text=cls._field_value(document, text_field) or "", type="text")
]
raw_score: Final = document.get(SCORE_FIELD_NAME)
return VectorStoreSearchResult(
@ -242,6 +266,20 @@ class MongoDBVectorStoreConfig(BaseDirectVectorStoreConfig):
filename=identifier,
)
@classmethod
def _raise_for_missing_text_field(
cls, documents: Sequence[Mapping[str, object]], text_field: str, database: str, collection: str
) -> None:
"""Atlas happily matches vectors in documents that carry no text at all, so a mistyped
mongodb_text_field returns well-scored results whose content is empty and feeds an empty
context to the model. Every matched document lacking the field is the misconfiguration."""
if documents and all(cls._field_value(document, text_field) is None for document in documents):
raise config_error(
f"None of the {len(documents)} matched documents in '{database}.{collection}' has a "
f"'{text_field}' field, so every result would carry empty text. Set mongodb_text_field "
"to the field holding the readable text; it accepts a dotted path such as metadata.body."
)
@classmethod
def _to_response(
cls, documents: Sequence[Mapping[str, object]], query_text: str, text_field: str
@ -284,6 +322,7 @@ class MongoDBVectorStoreConfig(BaseDirectVectorStoreConfig):
litellm_params: Mapping[str, object],
timeout: float | httpx.Timeout | None = None,
) -> VectorStoreSearchResponse:
self._reject_unknown_params(litellm_params)
params: Final = _MongoDBSearchParams.model_validate(litellm_params)
query_text: Final = self._query_text(query)
key: Final = self._client_key(params, timeout)
@ -315,6 +354,7 @@ class MongoDBVectorStoreConfig(BaseDirectVectorStoreConfig):
e, index_name=vector_store_id, database=database, collection=collection
) from e
self._raise_for_unusable_index(catalogue, vector_store_id, database, collection)
self._raise_for_missing_text_field(documents, params.text_field, database, collection)
return self._to_response(documents, query_text, params.text_field)
async def aexecute_search_vector_store_request(
@ -326,6 +366,7 @@ class MongoDBVectorStoreConfig(BaseDirectVectorStoreConfig):
litellm_params: Mapping[str, object],
timeout: float | httpx.Timeout | None = None,
) -> VectorStoreSearchResponse:
self._reject_unknown_params(litellm_params)
params: Final = _MongoDBSearchParams.model_validate(litellm_params)
query_text: Final = self._query_text(query)
key: Final = self._client_key(params, timeout)
@ -359,6 +400,7 @@ class MongoDBVectorStoreConfig(BaseDirectVectorStoreConfig):
e, index_name=vector_store_id, database=database, collection=collection
) from e
self._raise_for_unusable_index(catalogue, vector_store_id, database, collection)
self._raise_for_missing_text_field(documents, params.text_field, database, collection)
return self._to_response(documents, query_text, params.text_field)
def transform_create_vector_store_request(

View file

@ -275,12 +275,30 @@ def test_response_reads_a_dotted_text_field_path():
assert response["data"][0]["content"][0]["text"] == "nested text"
def test_response_tolerates_a_document_missing_the_text_field():
config, _, _ = _config(documents=[{"_id": 1, "score": 0.5}])
def test_response_tolerates_a_sparse_document_missing_the_text_field():
config, _, _ = _config(documents=[{"_id": 1, "score": 0.5}, {"_id": 2, "text": "has text", "score": 0.4}])
response = _search(config)
assert response["data"][0]["content"][0]["text"] == ""
assert response["data"][1]["content"][0]["text"] == "has text"
def test_a_present_but_empty_text_field_is_not_treated_as_a_misconfiguration():
config, _, _ = _config(documents=[{"_id": 1, "text": "", "score": 0.5}])
response = _search(config)
assert response["data"][0]["content"][0]["text"] == ""
def test_matches_that_all_lack_the_text_field_name_the_setting_to_fix():
"""Atlas matches on the vector, so a mistyped mongodb_text_field returns confidently
scored results whose content is empty and hands the model an empty context."""
config, _, _ = _config(documents=[{"_id": 1, "score": 0.9}, {"_id": 2, "score": 0.8}])
with pytest.raises(BadRequestError, match="mongodb_text_field"):
_search(config)
def test_response_tolerates_a_document_missing_a_score():
@ -930,3 +948,38 @@ def test_a_rejected_search_that_is_not_an_auth_failure_keeps_the_generic_message
translated = translate_mongo_error(error, index_name="idx", database="db", collection="coll")
assert "mongodb_connection_string" not in str(translated)
class TestUnrecognisedParameters:
"""litellm_params carries plenty of keys this provider does not own, so the params model has
to ignore extras. That turns a mistyped mongodb_collection into 'mongodb_collection is
required', pointing the reader at a key they can see they have set."""
def test_a_mistyped_parameter_is_named(self):
config, _, _ = _config()
with pytest.raises(BadRequestError, match="mongodb_collectoin"):
_search(config, litellm_params={"mongodb_collectoin": "embedded_movies"})
def test_the_supported_names_are_listed(self):
config, _, _ = _config()
with pytest.raises(BadRequestError, match="mongodb_connection_string"):
_search(config, litellm_params={"mongodb_databse": "sample_mflix"})
def test_unrelated_litellm_params_are_still_ignored(self):
config, _, _ = _config(documents=[{"_id": 1, "text": "hit", "score": 0.9}])
response = _search(
config,
litellm_params={"use_litellm_proxy": False, "use_in_pass_through": False, "vector_store_id": "x"},
)
assert len(response["data"]) == 1
@pytest.mark.asyncio
async def test_the_async_path_rejects_them_too(self):
config, _, _ = _async_config()
with pytest.raises(BadRequestError, match="mongodb_collectoin"):
await _asearch(config, litellm_params={"mongodb_collectoin": "embedded_movies"})