From d02286b6944490eb74f57205b4cbe1261aa6f969 Mon Sep 17 00:00:00 2001 From: Harsh Raj Singhania Date: Tue, 22 Sep 2026 12:12:05 +0530 Subject: [PATCH] fix: validate batch_size and empty-query benchmarks Reject batch_size < 1 with ValueError("batch_size must be >= 1") instead of a raw range() error. Return zeroed ModeResult metrics when a dataset has no queries instead of dividing by zero. --- src/tenantq/benchmark.py | 4 ++-- src/tenantq/embeddings.py | 2 ++ tests/test_benchmark.py | 13 +++++++++++++ tests/test_ingest.py | 25 +++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 tests/test_ingest.py diff --git a/src/tenantq/benchmark.py b/src/tenantq/benchmark.py index e919335..c6972ea 100644 --- a/src/tenantq/benchmark.py +++ b/src/tenantq/benchmark.py @@ -152,8 +152,8 @@ def run_benchmark( idx_recall.append(recall_at_k(retrieved, ref, 10)) total_time = sum(latencies) / 1000.0 - avg_r5 = sum(r5) / len(r5) - avg_r10 = sum(r10) / len(r10) + avg_r5 = sum(r5) / len(r5) if r5 else 0.0 + avg_r10 = sum(r10) / len(r10) if r10 else 0.0 RECALL_GAUGE.labels(mode=mode, k="5").set(avg_r5) RECALL_GAUGE.labels(mode=mode, k="10").set(avg_r10) result.modes.append( diff --git a/src/tenantq/embeddings.py b/src/tenantq/embeddings.py index d614f3a..6399c60 100644 --- a/src/tenantq/embeddings.py +++ b/src/tenantq/embeddings.py @@ -123,5 +123,7 @@ def build_embedder(kind: str, dense_model: str, sparse_model: str, dense_dim: in def batched(seq: Sequence, size: int) -> Iterable[Sequence]: + if size < 1: + raise ValueError("batch_size must be >= 1") for i in range(0, len(seq), size): yield seq[i : i + size] diff --git a/tests/test_benchmark.py b/tests/test_benchmark.py index ff3d9db..297ea8b 100644 --- a/tests/test_benchmark.py +++ b/tests/test_benchmark.py @@ -5,6 +5,7 @@ import math from tenantq.benchmark import percentile, recall_at_k, run_benchmark +from tenantq.data import Dataset def test_percentile_and_recall_helpers(): @@ -31,3 +32,15 @@ def test_benchmark_produces_numbers(ingested, settings, embedder, dataset): assert not math.isnan(modes["dense"].index_recall_at_10) # hybrid should be at least as good as the weaker single mode on recall@10 assert modes["hybrid"].recall_at_10 >= min(modes["dense"].recall_at_10, modes["sparse"].recall_at_10) + + +def test_benchmark_zero_queries_returns_empty_modes(ingested, settings, embedder): + empty = Dataset(documents=[], queries=[]) + result = run_benchmark(ingested, settings, embedder, empty) + assert result.settings_summary["n_queries"] == 0 + assert result.modes + for m in result.modes: + assert m.n_queries == 0 + assert m.recall_at_5 == 0.0 + assert m.recall_at_10 == 0.0 + assert m.qps == 0.0 diff --git a/tests/test_ingest.py b/tests/test_ingest.py new file mode 100644 index 0000000..5ea1c18 --- /dev/null +++ b/tests/test_ingest.py @@ -0,0 +1,25 @@ +"""Ingestion boundary checks.""" + +from __future__ import annotations + +import pytest + +from tenantq.embeddings import batched +from tenantq.ingest import ingest_documents + + +def test_batched_rejects_non_positive_size(): + with pytest.raises(ValueError, match="batch_size must be >= 1"): + list(batched([1, 2, 3], 0)) + with pytest.raises(ValueError, match="batch_size must be >= 1"): + list(batched([1, 2, 3], -1)) + + +def test_ingest_documents_rejects_zero_batch_size(settings, embedder, dataset): + from tenantq.client import make_client + from tenantq.collection import recreate_collection + + client = make_client(settings) + recreate_collection(client, settings) + with pytest.raises(ValueError, match="batch_size must be >= 1"): + ingest_documents(client, settings, embedder, dataset.documents[:3], batch_size=0)