Skip to content

Commit 2a156bb

Browse files
committed
test(search): add metadata-filter poll_until to fix flaky filter tests
## Purpose CI-0058: `test_search_with_filter_rest` failed with `assert 0 > 0` because the test only waited for vector indexing (unfiltered search returns >= 3 hits) but did not wait for the backend's metadata filter index to be ready. Vector indexing and metadata filter indexing are separate backend processes with different propagation latencies. ## Solution - Added a second `poll_until` in `test_search_with_filter_rest` and `test_search_with_filter_grpc` that specifically waits for the filtered search to return results before asserting. This converts a cryptic `AssertionError` into a clear `TimeoutError` if the backend never indexes metadata, and eliminates the race for the normal timing-dependent case. - Added `test_upsert_records_preserves_metadata_fields` to confirm at the SDK level that non-`field_map` fields (e.g. `category`) are forwarded in the NDJSON payload — ruling out SDK-side metadata stripping as a root cause. Code analysis ruled out the two other hypotheses: - H1 (SDK strips non-field_map fields): `upsert_records` shallow-copies all fields and serializes them verbatim to NDJSON. New unit test confirms this. - H2 (filter dropped from search request): `Index.search` puts `filter` in `body["query"]["filter"]` per the API spec. Existing unit test confirms this. Fixes CI-0058.
1 parent 3761bc8 commit 2a156bb

2 files changed

Lines changed: 62 additions & 3 deletions

File tree

tests/integration/test_search.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ def test_search_with_filter_rest(client: Pinecone, api_key: str) -> None:
396396
],
397397
)
398398

399-
# Wait for records to be searchable (eventual consistency)
399+
# Wait for records to be searchable (eventual consistency — vector indexing)
400400
poll_until(
401401
query_fn=lambda: index.search(
402402
namespace=namespace,
@@ -408,7 +408,23 @@ def test_search_with_filter_rest(client: Pinecone, api_key: str) -> None:
408408
description="all records searchable before filter test",
409409
)
410410

411-
# Search with category=science filter, requesting category field in hits
411+
# Wait for metadata filter indexing — may lag behind vector indexing.
412+
# poll_until raises TimeoutError (not AssertionError) if it never converges,
413+
# giving a clear signal that the backend hasn't indexed the category field.
414+
response = poll_until(
415+
query_fn=lambda: index.search(
416+
namespace=namespace,
417+
top_k=5,
418+
inputs={"text": "science and research"},
419+
filter={"category": {"$eq": "science"}},
420+
fields=["category"],
421+
),
422+
check_fn=lambda r: len(r.result.hits) > 0,
423+
timeout=60,
424+
description="filtered search returns science records",
425+
)
426+
427+
# Re-fetch with the same params so the final response is used for assertions
412428
response = index.search(
413429
namespace=namespace,
414430
top_k=5,
@@ -504,7 +520,7 @@ def test_search_with_filter_grpc(client: Pinecone, api_key: str) -> None:
504520
],
505521
)
506522

507-
# Wait for records to be searchable
523+
# Wait for records to be searchable (vector indexing)
508524
poll_until(
509525
query_fn=lambda: index.search(
510526
namespace=namespace,
@@ -516,6 +532,19 @@ def test_search_with_filter_grpc(client: Pinecone, api_key: str) -> None:
516532
description="records searchable (gRPC) before filter test",
517533
)
518534

535+
# Wait for metadata filter indexing — may lag behind vector indexing.
536+
poll_until(
537+
query_fn=lambda: index.search(
538+
namespace=namespace,
539+
top_k=5,
540+
inputs={"text": "science research"},
541+
filter={"category": {"$eq": "science"}},
542+
),
543+
check_fn=lambda r: len(r.result.hits) > 0,
544+
timeout=60,
545+
description="filtered search (gRPC) returns science records",
546+
)
547+
519548
# Search with filter via gRPC index (delegates to REST internally)
520549
response = index.search(
521550
namespace=namespace,

tests/unit/test_upsert_records.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,36 @@ def test_upsert_records_keyword_only(self) -> None:
169169
with pytest.raises(TypeError):
170170
idx.upsert_records([{"_id": "r1"}], "test-ns") # type: ignore[misc]
171171

172+
@respx.mock
173+
def test_upsert_records_preserves_metadata_fields(self) -> None:
174+
"""Non-field_map fields (e.g. 'category') are forwarded in the NDJSON payload.
175+
176+
Regression guard for CI-0058: if the SDK were to strip non-embedded fields
177+
before sending, metadata filters would silently return zero hits because the
178+
backend would have nothing to filter on.
179+
"""
180+
route = respx.post(UPSERT_URL).mock(
181+
return_value=httpx.Response(201),
182+
)
183+
idx = _make_index()
184+
idx.upsert_records(
185+
namespace="test-ns",
186+
records=[
187+
{"_id": "sci-1", "text": "Quantum mechanics.", "category": "science"},
188+
{"_id": "hist-1", "text": "The Roman Empire.", "category": "history"},
189+
],
190+
)
191+
192+
body = route.calls.last.request.content.decode("utf-8")
193+
lines = body.strip().split("\n")
194+
assert len(lines) == 2
195+
parsed_0 = json.loads(lines[0])
196+
parsed_1 = json.loads(lines[1])
197+
assert parsed_0["category"] == "science"
198+
assert parsed_0["text"] == "Quantum mechanics."
199+
assert parsed_1["category"] == "history"
200+
assert parsed_1["text"] == "The Roman Empire."
201+
172202
@respx.mock
173203
def test_upsert_records_response_bracket_access(self) -> None:
174204
respx.post(UPSERT_URL).mock(

0 commit comments

Comments
 (0)