<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://dstockton.github.io/qdrant-es-gateway/feed.xml" rel="self" type="application/atom+xml" /><link href="https://dstockton.github.io/qdrant-es-gateway/" rel="alternate" type="text/html" /><updated>2026-09-24T20:57:37+00:00</updated><id>https://dstockton.github.io/qdrant-es-gateway/feed.xml</id><title type="html">Qdrant ES Gateway</title><subtitle>Elasticsearch-compatible application search backed by the Qdrant engine</subtitle><entry><title type="html">A timeout is a feature</title><link href="https://dstockton.github.io/qdrant-es-gateway/production/reliability/2026/09/24/a-timeout-is-a-feature/" rel="alternate" type="text/html" title="A timeout is a feature" /><published>2026-09-24T00:00:00+00:00</published><updated>2026-09-24T00:00:00+00:00</updated><id>https://dstockton.github.io/qdrant-es-gateway/production/reliability/2026/09/24/a-timeout-is-a-feature</id><content type="html" xml:base="https://dstockton.github.io/qdrant-es-gateway/production/reliability/2026/09/24/a-timeout-is-a-feature/"><![CDATA[<p>What happens when Qdrant stops responding?</p>

<p><code class="language-plaintext highlighter-rouge">QDRANT_CONNECT_TIMEOUT_MS</code> and <code class="language-plaintext highlighter-rouge">QDRANT_REQUEST_TIMEOUT_MS</code> configure upstream timeouts, defaulting to five seconds and three minutes. The gateway logs both values at startup.</p>

<p>With <code class="language-plaintext highlighter-rouge">ASYNC_PAYLOAD_WRITES=true</code>, eligible payload updates run in background tasks. <code class="language-plaintext highlighter-rouge">ASYNC_WRITE_QUEUE</code> limits concurrent tasks to 256 by default; further updates receive an error while all slots are occupied. Failed background writes are logged.</p>

<p>CI checks Rust dependencies for known advisories. A manual release dry run builds and scans the image and packages the Helm chart without logging into GHCR or publishing a tag.</p>]]></content><author><name></name></author><category term="production" /><category term="reliability" /><summary type="html"><![CDATA[What happens when Qdrant stops responding?]]></summary></entry><entry><title type="html">A missing delete is not a successful delete</title><link href="https://dstockton.github.io/qdrant-es-gateway/2026/09/24/missing-deletes-are-not-successes/" rel="alternate" type="text/html" title="A missing delete is not a successful delete" /><published>2026-09-24T00:00:00+00:00</published><updated>2026-09-24T00:00:00+00:00</updated><id>https://dstockton.github.io/qdrant-es-gateway/2026/09/24/missing-deletes-are-not-successes</id><content type="html" xml:base="https://dstockton.github.io/qdrant-es-gateway/2026/09/24/missing-deletes-are-not-successes/"><![CDATA[<p>Deleting an absent document is intentionally idempotent, but Elasticsearch still tells the client that nothing was removed. The gateway previously returned HTTP 200 with <code class="language-plaintext highlighter-rouge">"result": "deleted"</code> because Qdrant’s point-delete operation succeeds even when the point does not exist.</p>

<p>The gateway now checks the authoritative document store before deleting. A missing ID returns HTTP 404 with <code class="language-plaintext highlighter-rouge">"result": "not_found"</code>, matching Elasticsearch’s <a href="https://www.elastic.co/docs/api/doc/elasticsearch/v8/operation/operation-delete">delete result vocabulary</a>:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"_index"</span><span class="p">:</span><span class="w"> </span><span class="s2">"products"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"missing"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"_version"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
  </span><span class="nl">"result"</span><span class="p">:</span><span class="w"> </span><span class="s2">"not_found"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"_shards"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
    </span><span class="nl">"successful"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
    </span><span class="nl">"failed"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Bulk delete keeps Elasticsearch’s useful nuance: the item has status 404 and <code class="language-plaintext highlighter-rouge">"result": "not_found"</code>, but it has no <code class="language-plaintext highlighter-rouge">error</code> object and the top-level <code class="language-plaintext highlighter-rouge">errors</code> flag remains <code class="language-plaintext highlighter-rouge">false</code>. Missing updates are different because they failed to apply; those still carry <code class="language-plaintext highlighter-rouge">document_missing_exception</code> and set <code class="language-plaintext highlighter-rouge">errors</code> to <code class="language-plaintext highlighter-rouge">true</code>.</p>

<p>The regression test exercises standalone and bulk missing deletes in both storage modes. The trade-off is one point lookup before each delete, including successful deletes. As with the update existence check, a concurrent writer can race between the lookup and delete; the gateway does not yet implement Elasticsearch sequence numbers or primary-term concurrency controls.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Deleting an absent document is intentionally idempotent, but Elasticsearch still tells the client that nothing was removed. The gateway previously returned HTTP 200 with "result": "deleted" because Qdrant’s point-delete operation succeeds even when the point does not exist.]]></summary></entry><entry><title type="html">A missing update is not an upsert</title><link href="https://dstockton.github.io/qdrant-es-gateway/2026/09/24/missing-updates-are-not-upserts/" rel="alternate" type="text/html" title="A missing update is not an upsert" /><published>2026-09-24T00:00:00+00:00</published><updated>2026-09-24T00:00:00+00:00</updated><id>https://dstockton.github.io/qdrant-es-gateway/2026/09/24/missing-updates-are-not-upserts</id><content type="html" xml:base="https://dstockton.github.io/qdrant-es-gateway/2026/09/24/missing-updates-are-not-upserts/"><![CDATA[<p>Elasticsearch’s update API does not create a missing document unless the request explicitly asks for upsert behavior. The gateway previously lost that distinction: an update to an absent ID could return <code class="language-plaintext highlighter-rouge">"result": "updated"</code>, even though Qdrant had no point to change. A text-field update could go further and create the document from an empty source.</p>

<p>The gateway now checks existence before every partial update. Missing documents return HTTP 404 with Elasticsearch’s <code class="language-plaintext highlighter-rouge">document_missing_exception</code> shape:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"error"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"document_missing_exception"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"reason"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[missing]: document missing"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"index"</span><span class="p">:</span><span class="w"> </span><span class="s2">"products"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"shard"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="mi">404</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Bulk updates report the same item-level error and set the top-level <code class="language-plaintext highlighter-rouge">errors</code> flag to <code class="language-plaintext highlighter-rouge">true</code>. Requests with <code class="language-plaintext highlighter-rouge">"doc_as_upsert": true</code> remain explicit creation requests; they return HTTP 201, including as bulk item statuses.</p>

<p>The regression test runs the missing, bulk, and <code class="language-plaintext highlighter-rouge">doc_as_upsert</code> cases against mock Qdrant responses in both storage modes. This correctness check adds one point lookup to partial updates that previously avoided a read. It does not add support for scripted updates or the separate <code class="language-plaintext highlighter-rouge">upsert</code> document form, and the gateway still does not emulate Elasticsearch’s optimistic-concurrency or version counters.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Elasticsearch’s update API does not create a missing document unless the request explicitly asks for upsert behavior. The gateway previously lost that distinction: an update to an absent ID could return "result": "updated", even though Qdrant had no point to change. A text-field update could go further and create the document from an empty source.]]></summary></entry><entry><title type="html">0.1.2: safer defaults for production traffic</title><link href="https://dstockton.github.io/qdrant-es-gateway/releases/2026/09/24/release-0-1-2/" rel="alternate" type="text/html" title="0.1.2: safer defaults for production traffic" /><published>2026-09-24T00:00:00+00:00</published><updated>2026-09-24T00:00:00+00:00</updated><id>https://dstockton.github.io/qdrant-es-gateway/releases/2026/09/24/release-0-1-2</id><content type="html" xml:base="https://dstockton.github.io/qdrant-es-gateway/releases/2026/09/24/release-0-1-2/"><![CDATA[<p>Version 0.1.2 is a small operational release.</p>

<p>The gateway now makes upstream timeouts visible and configurable, bounds background payload writes, and waits briefly when SQLite is busy instead of failing immediately. CI also checks Rust dependencies for known advisories and offers a manual release dry run that builds, scans, and packages without publishing.</p>

<p>The client contract is unchanged. Upgrade the gateway, keep the Elasticsearch endpoint, and tune the new settings only if your workload needs to.</p>]]></content><author><name></name></author><category term="releases" /><summary type="html"><![CDATA[Version 0.1.2 is a small operational release.]]></summary></entry><entry><title type="html">Releases should not depend on remembering three version files</title><link href="https://dstockton.github.io/qdrant-es-gateway/production/releases/2026/09/24/release-please/" rel="alternate" type="text/html" title="Releases should not depend on remembering three version files" /><published>2026-09-24T00:00:00+00:00</published><updated>2026-09-24T00:00:00+00:00</updated><id>https://dstockton.github.io/qdrant-es-gateway/production/releases/2026/09/24/release-please</id><content type="html" xml:base="https://dstockton.github.io/qdrant-es-gateway/production/releases/2026/09/24/release-please/"><![CDATA[<p>The gateway has one release version, but it appears in more than one place: Rust, the Helm chart, and the lockfile.</p>

<p>That is exactly the sort of job a human does once, then forgets on a Friday afternoon.</p>

<p>Release Please now watches <code class="language-plaintext highlighter-rouge">main</code> and opens a release pull request when Conventional Commit messages add a feature or fix. The pull request updates the changelog and keeps the Rust and Helm versions together. Once it is reviewed and merged, Release Please creates the tag and GitHub release; the existing publish pipeline sees that tag and adds the container, chart package, SBOM, and attestation.</p>

<p>The useful part is the pause in the middle: automation prepares the release, but a person still gets to read it before anything ships.</p>]]></content><author><name></name></author><category term="production" /><category term="releases" /><summary type="html"><![CDATA[The gateway has one release version, but it appears in more than one place: Rust, the Helm chart, and the lockfile.]]></summary></entry><entry><title type="html">A missing document is a 404</title><link href="https://dstockton.github.io/qdrant-es-gateway/2026/09/23/missing-documents-are-404s/" rel="alternate" type="text/html" title="A missing document is a 404" /><published>2026-09-23T00:00:00+00:00</published><updated>2026-09-23T00:00:00+00:00</updated><id>https://dstockton.github.io/qdrant-es-gateway/2026/09/23/missing-documents-are-404s</id><content type="html" xml:base="https://dstockton.github.io/qdrant-es-gateway/2026/09/23/missing-documents-are-404s/"><![CDATA[<p>Elasticsearch’s get-document API distinguishes a missing document at both layers of the response: the JSON body contains <code class="language-plaintext highlighter-rouge">"found": false</code>, and the HTTP status is 404. The gateway previously returned the right body with HTTP 200.</p>

<p>That mismatch matters to clients. An application using the status code to select its not-found path could treat an absent document as a successful fetch, even though the response body said otherwise. The gateway now returns 404 while preserving the Elasticsearch-shaped body and <code class="language-plaintext highlighter-rouge">X-Elastic-Product</code> response header.</p>

<p>The regression test exercises both source-storage modes: <code class="language-plaintext highlighter-rouge">_source</code> embedded in the searchable Qdrant collection and the optional dedicated document projection. In each mode, an empty Qdrant lookup must produce the same result:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">404</span> <span class="ne">Not Found</span>
<span class="na">X-Elastic-Product</span><span class="p">:</span> <span class="s">Elasticsearch</span>

{"_index":"products","_id":"missing","found":false}
</code></pre></div></div>

<p>This change is deliberately narrow. It does not yet emulate Elasticsearch’s version, sequence-number, or primary-term behavior, and delete/update behavior for missing documents remains a separate compatibility surface.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Elasticsearch’s get-document API distinguishes a missing document at both layers of the response: the JSON body contains "found": false, and the HTTP status is 404. The gateway previously returned the right body with HTTP 200.]]></summary></entry><entry><title type="html">Oversized requests are not bad JSON</title><link href="https://dstockton.github.io/qdrant-es-gateway/production/compatibility/2026/09/23/oversized-requests-are-not-bad-json/" rel="alternate" type="text/html" title="Oversized requests are not bad JSON" /><published>2026-09-23T00:00:00+00:00</published><updated>2026-09-23T00:00:00+00:00</updated><id>https://dstockton.github.io/qdrant-es-gateway/production/compatibility/2026/09/23/oversized-requests-are-not-bad-json</id><content type="html" xml:base="https://dstockton.github.io/qdrant-es-gateway/production/compatibility/2026/09/23/oversized-requests-are-not-bad-json/"><![CDATA[<p>The gateway bounds request bodies with <code class="language-plaintext highlighter-rouge">MAX_BODY_BYTES</code> and gives bulk ingestion a separate <code class="language-plaintext highlighter-rouge">MAX_BULK_BYTES</code> ceiling. Those limits protect memory, but the rejection previously looked like a generic HTTP 400 compatibility error. A client could not reliably tell whether to fix malformed JSON or split a valid payload into smaller requests.</p>

<p>Oversized requests now return HTTP 413 with Elasticsearch’s <code class="language-plaintext highlighter-rouge">content_too_long_exception</code> error type. The response names the active setting and its byte limit, so an operator can decide whether to batch more narrowly or deliberately change the deployment limit. Both the early gateway check and the streaming body collector use the same response shape; bulk routes identify <code class="language-plaintext highlighter-rouge">MAX_BULK_BYTES</code>, while other routes identify <code class="language-plaintext highlighter-rouge">MAX_BODY_BYTES</code>.</p>

<p>The regression test sets deliberately tiny limits and verifies the status, error type, setting name, and exact configured ceiling for a search request and a bulk request:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo <span class="nb">test </span>oversized_requests_return_413_with_the_active_limit
cargo <span class="nb">test</span> <span class="nt">--all-targets</span> <span class="nt">--all-features</span>
cargo clippy <span class="nt">--all-targets</span> <span class="nt">--all-features</span> <span class="nt">--</span> <span class="nt">-D</span> warnings
</code></pre></div></div>

<p>This does not implement automatic retrying or request splitting. HTTP 413 is intentionally non-transient: clients should reduce the body size, and operators should raise a limit only after considering the gateway’s memory budget and concurrency.</p>]]></content><author><name></name></author><category term="production" /><category term="compatibility" /><summary type="html"><![CDATA[The gateway bounds request bodies with MAX_BODY_BYTES and gives bulk ingestion a separate MAX_BULK_BYTES ceiling. Those limits protect memory, but the rejection previously looked like a generic HTTP 400 compatibility error. A client could not reliably tell whether to fix malformed JSON or split a valid payload into smaller requests.]]></summary></entry><entry><title type="html">No phantom indices after creation failures</title><link href="https://dstockton.github.io/qdrant-es-gateway/production/reliability/2026/09/22/no-phantom-indices-after-create-failures/" rel="alternate" type="text/html" title="No phantom indices after creation failures" /><published>2026-09-22T00:00:00+00:00</published><updated>2026-09-22T00:00:00+00:00</updated><id>https://dstockton.github.io/qdrant-es-gateway/production/reliability/2026/09/22/no-phantom-indices-after-create-failures</id><content type="html" xml:base="https://dstockton.github.io/qdrant-es-gateway/production/reliability/2026/09/22/no-phantom-indices-after-create-failures/"><![CDATA[<p>Creating an index spans two stores: Qdrant owns the collections and payload indexes, while SQLite keeps the gateway’s mappings and aliases. That boundary needs a deliberate publication order.</p>

<p>Previously, the gateway wrote SQLite metadata before asking Qdrant to create anything. If Qdrant was unavailable, rejected the collection configuration, or failed while creating the optional document projection, the request returned an error but the gateway still considered the index present. Later index checks succeeded against metadata and document operations failed against a collection that did not exist.</p>

<p>Index creation now treats SQLite as the publication step. The gateway creates the sparse collection, optional document collection, and payload indexes first. Only after those operations succeed does it record the index metadata. If a later setup step or the metadata write fails, it makes a best-effort attempt to delete the collections created by that request. Cleanup failures are logged without hiding the original creation error.</p>

<p>A regression test runs against an in-process mock Qdrant. It accepts the primary collection, fails the document collection, and verifies both observable properties: the index is absent from SQLite and the primary collection receives a cleanup request.</p>

<p>To reproduce the verification:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo <span class="nb">test </span>failed_index_creation_is_not_published_and_cleans_up
cargo <span class="nb">test</span> <span class="nt">--all-targets</span>
cargo clippy <span class="nt">--all-targets</span> <span class="nt">--</span> <span class="nt">-D</span> warnings
</code></pre></div></div>

<p>This is compensating cleanup rather than a distributed transaction. A process crash between Qdrant creation and SQLite publication can still leave an orphaned collection, and a Qdrant outage can prevent cleanup. Operators should still monitor unexpected collections and retain reconciliation or backup procedures for the metadata database. The change prevents failed requests from publishing unusable gateway metadata and preserves the original metadata when an attempt to recreate an existing index is rejected upstream.</p>]]></content><author><name></name></author><category term="production" /><category term="reliability" /><summary type="html"><![CDATA[Creating an index spans two stores: Qdrant owns the collections and payload indexes, while SQLite keeps the gateway’s mappings and aliases. That boundary needs a deliberate publication order.]]></summary></entry><entry><title type="html">One replica is the safe default</title><link href="https://dstockton.github.io/qdrant-es-gateway/2026/09/22/one-replica-is-the-safe-default/" rel="alternate" type="text/html" title="One replica is the safe default" /><published>2026-09-22T00:00:00+00:00</published><updated>2026-09-22T00:00:00+00:00</updated><id>https://dstockton.github.io/qdrant-es-gateway/2026/09/22/one-replica-is-the-safe-default</id><content type="html" xml:base="https://dstockton.github.io/qdrant-es-gateway/2026/09/22/one-replica-is-the-safe-default/"><![CDATA[<p>The Helm chart used to request two gateway replicas while also provisioning one <code class="language-plaintext highlighter-rouge">ReadWriteOnce</code> volume for the SQLite metadata database. That combination looked highly available but did not supply a highly available control plane.</p>

<p>SQLite stores index mappings, vector definitions, and aliases. With a typical <code class="language-plaintext highlighter-rouge">ReadWriteOnce</code> volume, two pods may be constrained to one node or the second pod may be unable to mount the volume. Giving each pod separate storage is worse: the replicas can disagree about which indices and aliases exist. Even a shared filesystem needs locking semantics suitable for SQLite, and concurrent index administration still crosses SQLite and Qdrant without a distributed transaction.</p>

<p>The chart now defaults to one replica. CI renders the Deployment and asserts that default, while the chart and production guides spell out the requirements before an operator overrides it. This does not reduce a working default deployment from two fault-independent replicas: the old defaults never provided that guarantee.</p>

<p>To reproduce the chart check:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm lint deploy/helm/qdrant-es-gateway
helm template smoke deploy/helm/qdrant-es-gateway <span class="se">\</span>
  <span class="nt">--show-only</span> templates/deployment.yaml | <span class="nb">grep</span> <span class="s1">'^  replicas:'</span>
</code></pre></div></div>

<p>The expected rendered value is <code class="language-plaintext highlighter-rouge">replicas: 1</code>.</p>

<p>This is a safer default, not horizontal-availability support. A production design that needs multiple gateway replicas should first move the small metadata catalog to a replicated control-plane store, or deliberately provide <code class="language-plaintext highlighter-rouge">ReadWriteMany</code> storage with verified SQLite locking and serialize index and alias changes externally. Qdrant replication protects document collections; it does not replicate the gateway’s SQLite catalog.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[The Helm chart used to request two gateway replicas while also provisioning one ReadWriteOnce volume for the SQLite metadata database. That combination looked highly available but did not supply a highly available control plane.]]></summary></entry><entry><title type="html">A slash is part of the document ID</title><link href="https://dstockton.github.io/qdrant-es-gateway/compatibility/correctness/2026/09/21/a-slash-is-part-of-the-id/" rel="alternate" type="text/html" title="A slash is part of the document ID" /><published>2026-09-21T00:00:00+00:00</published><updated>2026-09-21T00:00:00+00:00</updated><id>https://dstockton.github.io/qdrant-es-gateway/compatibility/correctness/2026/09/21/a-slash-is-part-of-the-id</id><content type="html" xml:base="https://dstockton.github.io/qdrant-es-gateway/compatibility/correctness/2026/09/21/a-slash-is-part-of-the-id/"><![CDATA[<p>Elasticsearch clients put document IDs in URL path segments. An ID such as <code class="language-plaintext highlighter-rouge">order/42</code> therefore reaches the server as <code class="language-plaintext highlighter-rouge">order%2F42</code>; spaces and non-ASCII text are encoded for the same reason. The gateway previously routed on the raw URI path, so it stored the encoded spelling as <code class="language-plaintext highlighter-rouge">_id</code> instead of the caller’s original value. A later request produced by a client from <code class="language-plaintext highlighter-rouge">order/42</code> could still find that spelling, but responses exposed <code class="language-plaintext highlighter-rouge">order%2F42</code>, breaking identity round trips and the documented arbitrary-ID guarantee.</p>

<p>Routing now percent-decodes each segment independently. Decoding after splitting is important: <code class="language-plaintext highlighter-rouge">%2F</code> becomes part of the document ID rather than a new route separator. Invalid UTF-8 is rejected as a structured bad request instead of being replaced with lossy text.</p>

<p>The focused verification covers an ID containing an encoded slash, Unicode, and a space, plus a non-UTF-8 path:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo <span class="nb">test </span>path_segments_
cargo <span class="nb">test</span> <span class="nt">--all-targets</span> <span class="nt">--all-features</span>
cargo clippy <span class="nt">--all-targets</span> <span class="nt">--all-features</span> <span class="nt">--</span> <span class="nt">-D</span> warnings
</code></pre></div></div>

<p>This is route-level compatibility, not a relaxation of index-name rules or an implementation of Elasticsearch’s full URL grammar. Reverse proxies must also preserve encoded slashes when forwarding requests; deployments should include one representative encoded ID in ingress smoke tests.</p>]]></content><author><name></name></author><category term="compatibility" /><category term="correctness" /><summary type="html"><![CDATA[Elasticsearch clients put document IDs in URL path segments. An ID such as order/42 therefore reaches the server as order%2F42; spaces and non-ASCII text are encoded for the same reason. The gateway previously routed on the raw URI path, so it stored the encoded spelling as _id instead of the caller’s original value. A later request produced by a client from order/42 could still find that spelling, but responses exposed order%2F42, breaking identity round trips and the documented arbitrary-ID guarantee.]]></summary></entry></feed>