Skip to content

Coverage

Two questions, and this page answers both: which queries the library can read, and which clients it can read them out of.

Query types

Checked against the official OpenSearch API specification rather than from memory. resources/opensearch-spec.json is a committed snapshot of the type names it declares; resources/coverage.json records our stance on each one, and SpecCoverageTest fails if the two ever disagree.

46 of the 59 query types are rendered natively:

term-level term, terms, terms_set, prefix, wildcard, regexp, fuzzy, exists, range, ids
full text match, match_bool_prefix, match_phrase, match_phrase_prefix, multi_match, combined_fields, common, query_string, simple_query_string, more_like_this, intervals
compound bool, constant_score, dis_max, hybrid, function_score, script_score, boosting (filtering part), wrapper
joining nested, has_child, has_parent, parent_id
vector knn, neural
geo geo_distance, geo_bounding_box, geo_polygon, geo_shape, xy_shape
scoring rank_feature, distance_feature
other match_all, match_none, script, percolate

Vector and geo clauses keep what a reader needs and drop what they cannot use: a knn renders as image_embedding:knn(k=20), not as a thousand floats, so two searches of the same kind share a fingerprint however different their vectors. Same for a geo_distance — the radius survives, the centre does not. A shape query keeps the two parts that decide which documents match, zone:geo_shape(polygon,within), and drops the coordinates: within and disjoint over the same polygon return opposite result sets, so collapsing them would be the geo equivalent of erasing a not.

Two of them recover more than they summarise. A hybrid — the OpenSearch pattern of running a lexical and a vector clause together under a normalisation pipeline — renders as the union it matches, so q=(embedding:knn(k=20) or title|description:"hiking boots") says what the search actually combined instead of hiding it behind one word. And a wrapper is base64-decoded and parsed, so a query passed through as an opaque blob comes back as the query it always was, hashing exactly like the same query sent unwrapped.

The other 13 render as type(?). They are signalled, never dropped, and still contribute to the fingerprint — and none of them is a gap waiting to be filled. The span_* family is 9 of the 13 and stays there on purpose: nobody debugs a span query from a log line. The remaining four cannot be read even in principle — type was removed with mapping types, sltr and template only rescore or live behind another endpoint, and an agentic query hands the whole result set to a model that decides outside the DSL.

Clients captured at the transport

The transport integrations attach to a PSR-18 client, a Guzzle handler stack, or a ringphp handler. Which one your OpenSearch library offers is a property of its transport, not of its name:

client transport attach with
elasticsearch-php 8 [^es8] elastic/transport, PSR-18 Http\DigestingClient or Http\Guzzle\DigestMiddleware
opensearch-php ≥ 2.4 through GuzzleClientFactory, SymfonyClientFactory or TransportFactory PSR-18 Http\DigestingClient or Http\Guzzle\DigestMiddleware
any Guzzle, HTTPlug or PSR-18 client of your own PSR-18 Http\DigestingClient or Http\Guzzle\DigestMiddleware
opensearch-php through the deprecated ClientBuilder ezimuel/ringphp Http\Ring\DigestingHandler
opensearch-php ≤ 2.3 ezimuel/ringphp Http\Ring\DigestingHandler
elasticsearch-php 7.x ezimuel/ringphp Http\Ring\DigestingHandler

[^es8]: The integration attaches, but the pair does not work: elasticsearch-php 8 sends Content-Type: application/vnd.elasticsearch+json; compatible-with=8, and an OpenSearch node answers 406 Not Acceptable to it. The row is about the shape of its transport, not about a combination anyone can run — which is why opensearch-php exists. It is the only row this page cannot check.

A ringphp handler is a callable(array): array|FutureArrayInterface, which predates PSR-7 — so there is no request object to decorate and no stack to push onto, and it needs an integration of its own rather than an adapter.

This table is checked, not claimed. tests/Integration/ClientCaptureTest.php installs the clients themselves, sends a search through each one against a live node, and asserts what came back — every row above except the footnoted one. It also asserts the thing a reader of the table actually depends on and that no single integration's own tests can show: the same search sent by different clients through different integrations is one fingerprint. Prose was what let this page be wrong in one direction and then wrong in the other without anything failing.

The transport is one of three ways in, and the other two do not care what your client is at all: the Monolog processor digests a body your application already logs, and the command line reads the search slow log the cluster writes on its own.