The AWS Retail Demo Store is a useful reality check because its search service talks to OpenSearch through the ordinary opensearch-py API. Its product search uses dis_max, match_bool_prefix, category collapse with named inner_hits, and _source: false. Its similar-products route uses more_like_this.

I replayed those request bodies unchanged against native OpenSearch 3.9.0 and this gateway backed by Qdrant 1.19.2. The replay created a settings-only products index and wrote individual products, just as the demo’s local indexing script does. Both endpoints returned the fields the application consumes, including product IDs, named category hits, and similar-product results that exclude the source product.

What passed

Check OpenSearch Gateway + Qdrant
Index, search, and similar-product HTTP contract Pass Pass
Repeated search p50 at 1K 5.75 ms 1.37 ms
Repeated search p95 at 1K 7.10 ms 2.36 ms
Repeated search p50 at 1M 30.08 ms 1.63 ms
Repeated search p95 at 1M 33.62 ms 2.85 ms
First similar-product request at 1M 135.15 ms 33.04 ms

The gateway also needed two small compatibility fixes exposed by this replay: settings-only indexes now route dynamically mapped text fields through the catch-all lexical vector, and a missing Qdrant point is treated like a missing Elasticsearch document. The latter matters when more_like_this looks up its source document.

The resource trade-off

These are fresh local Docker volumes, after loading unique generated product names and descriptions and while the indexes were retained. They are footprint snapshots, not a production sizing or cloud-billing benchmark.

Corpus Service CPU snapshot Memory snapshot Data volume
1K OpenSearch 3.9.0 3.83% 1.090 GiB 630 KiB
1K Gateway + Qdrant 1.19.2 1.31% combined 310 MiB combined 419 MB combined
1M OpenSearch 3.9.0 0.92% 1.241 GiB 75.2 MB
1M Gateway + Qdrant 1.19.2 0.08% combined 581 MiB combined 876 MB combined

The negative trade-off is real: Qdrant used more disk. At 1M documents, the gateway-plus-Qdrant volume was about 11.6× the OpenSearch volume. The gateway process itself remained small; Qdrant carries the searchable representation and sparse index. OpenSearch’s JVM used more memory in these snapshots, but CPU and memory readings are instantaneous and environment-dependent.

The disk did not grow linearly in this experiment. Moving from 1K to 1M documents increased the OpenSearch volume about 119× and the Qdrant volume about 2.1×. That is evidence of substantial fixed index overhead at the 1K point, not a promise that Qdrant storage is always sublinear. The generator uses five product families with unique names and descriptions; a production catalogue with more varied fields, longer text, replicas, retention, or different sparse-index settings can have a different slope.

I also retried the same 10M-document bulk run after a complete Docker cleanup, with about 27 GiB free. Qdrant reached roughly 9.57M documents, but the process was killed for exceeding available memory before the final search could complete. That is a capacity limit of this test host, not a failed compatibility assertion, so there is no honest 10M performance number to publish from this machine. A completed 10M comparison needs more memory.

What did not become identical

Passing the application contract does not make the two engines rank identically. The native result produced two category groups for the fixture; the gateway produced one bounded candidate group. Scores and similar-product ordering also differ because Qdrant’s sparse retrieval is not Lucene/OpenSearch scoring. Refresh timing and the gateway’s bounded candidate window remain explicit trade-offs.

So the honest conclusion is narrow: the Retail Demo Store’s product-search and similar-product request shapes are a good fit for the gateway, with no application-side URL or response-handling changes in this replay. If exact OpenSearch ranking or category diversity is part of the product contract, validate that separately before migrating.

Reproduce the check with:

python3 validation/retail_demo.py \
  --native http://localhost:19311 \
  --gateway http://localhost:19313 \
  --docs 1000000 \
  --workers 32 \
  --output validation/evidence/retail-demo-2026-10-17-1m-clean.json

The request shapes come from the demo’s search service, and its settings-only indexing flow is shown in local_index_products.py. The clean 1K and 1M reports are retail-demo-2026-10-17-1k-clean.json and retail-demo-2026-10-17-1m-clean.json.