diff --git a/api/app.py b/api/app.py index 46534d1..16a99f6 100644 --- a/api/app.py +++ b/api/app.py @@ -53,6 +53,7 @@ configure_logging("api") logger = logging.getLogger(__name__) DEFAULT_BATCH_SIZE = 50 +FIRST_BATCH_SIZE = 5 _rate_limit_config = RateLimitConfig.from_env() @@ -288,6 +289,7 @@ async def _stream_from_cache( }) + "\n" count = 0 + is_first_batch = True for feature_batch in get_cached_features(query_parameters, batch_size=batch_size): # Apply decision filtering if decision_filter != "everything": @@ -302,9 +304,27 @@ async def _stream_from_cache( ] if limit and count + len(feature_batch) > limit: feature_batch = feature_batch[:limit - count] - count += len(feature_batch) - if feature_batch: - yield json.dumps({"type": "batch", "features": feature_batch}) + "\n" + + # Split first batch into smaller primer batch and remainder + if is_first_batch and len(feature_batch) > FIRST_BATCH_SIZE: + # Yield primer batch + first_features = feature_batch[:FIRST_BATCH_SIZE] + count += len(first_features) + yield json.dumps({"type": "batch", "features": first_features}) + "\n" + + # Yield remainder + remaining_features = feature_batch[FIRST_BATCH_SIZE:] + count += len(remaining_features) + if remaining_features: + yield json.dumps({"type": "batch", "features": remaining_features}) + "\n" + is_first_batch = False + else: + # Normal batch yielding + count += len(feature_batch) + if feature_batch: + yield json.dumps({"type": "batch", "features": feature_batch}) + "\n" + is_first_batch = False + if limit and count >= limit: break @@ -341,6 +361,7 @@ async def _stream_from_db( try: count = 0 batch: list[dict] = [] + current_batch_target = FIRST_BATCH_SIZE # Start with smaller first batch for row in repository.stream_listings_optimized( query_parameters, limit=limit, page_size=batch_size ): @@ -359,11 +380,13 @@ async def _stream_from_db( batch.append(feature) count += 1 - if len(batch) >= batch_size: + if len(batch) >= current_batch_target: if staging_key: cache_features_batch_staged(staging_key, batch) yield json.dumps({"type": "batch", "features": batch}) + "\n" batch = [] + # After first batch, use normal batch size + current_batch_target = batch_size if batch: if staging_key: diff --git a/tests/test_listing_geojson.py b/tests/test_listing_geojson.py index 240d602..8735b7a 100644 --- a/tests/test_listing_geojson.py +++ b/tests/test_listing_geojson.py @@ -302,3 +302,16 @@ class TestStreamingEndpoint: assert feature["properties"]["qm"] is None assert feature["properties"]["qmprice"] is None + def test_first_batch_is_smaller(self, client, mock_repository): + """Test that the first batch is smaller than subsequent batches for fast first paint.""" + response = client.get("/api/listing_geojson/stream?listing_type=RENT&batch_size=50&limit=10") + lines = response.text.strip().split("\n") + messages = [json.loads(line) for line in lines] + + batch_messages = [m for m in messages if m["type"] == "batch"] + assert len(batch_messages) >= 1 + + # First batch should contain FIRST_BATCH_SIZE (5) or fewer features + from api.app import FIRST_BATCH_SIZE + assert len(batch_messages[0]["features"]) <= FIRST_BATCH_SIZE +