OpenSearch-compatible _search adapter
GrowlerDB ships an optional, read-path-first adapter that speaks a documented subset of the
OpenSearch _search Query DSL (decision D4). It exists for client
& tool ecosystem compatibility and migration — the native /v1/search
PK API remains the primary, first-class surface.
It is off by default. Enable it on the gateway:
growlerdb gateway --node-addr http://node:50051 --opensearch ...
Then point an OpenSearch client at the gateway’s REST port:
curl -s localhost:8081/myindex/_search -H 'content-type: application/json' -d '{
"query": { "bool": {
"must": [{ "match": { "title": "alert" } }],
"filter": [{ "range": { "ts": { "gte": "1700000000" } } }]
}},
"size": 20,
"sort": [{ "ts": "desc" }]
}'
How it works
The adapter translates the DSL into GrowlerDB’s native query string, which the engine parses into its canonical AST and executes through the normal search path. Results are then shaped as OpenSearch documents:
_idis synthesized from the composite key: the partition field values then the identifier values, joined by#(e.g.42#u1)._sourceis filled by PK hydration (GetByKey) — the authoritative row from Iceberg, governed by the catalog. Search stays PK-based internally; the client sees documents.- The response carries
took,timed_out,_shards, andhits.total(a true cross-shard count). A down shard sets_shards.failed(never a silent gap).
The Authorization / tenant headers are forwarded to the engine, so the adapter is governed by the
same auth + tenant scoping as the native API.
Supported query DSL
| Clause | Support | Maps to |
|---|---|---|
match_all |
✅ | MatchAll (via the *:* idiom — a cheap all-docs query) |
match |
✅ | analyzed term(s); multi-token ⇒ OR of tokens |
match_phrase |
✅ | Phrase (ordered, adjacency) |
multi_match |
✅ | OR of field:value across fields |
term |
✅ | Term (exact / analyzed per field type) |
terms |
✅ | OR of Terms |
range (gte/gt/lte/lt) |
✅ | Range with inclusive/exclusive bounds |
bool (must/filter/must_not/should) |
✅ | Bool — see should/filter caveats |
exists, prefix, wildcard, fuzzy, regexp, ids, … |
❌ | clear 501 error |
Request body
from/size→ offset / page size (defaultsize= 10).sort→ native sort keys. Accepts"field",{ "field": "asc"|"desc" }, and{ "field": { "order": ... } }._scoreentries are dropped (native ranks by score by default).highlight→ opt into server-side highlighting.fields(the map keys) names the TEXT fields to highlight;number_of_fragments→ max fragments per field andfragment_size→ the fragment window (top-level or per-field, a per-field value winning). Each hit then carries a standard OpenSearchhighlightobject (field → ["…<em>term</em>…"], HTML-escaped). Custom pre/post tags,type, andorderare ignored — GrowlerDB emits<em>-marked, escaped fragments.queryabsent → match-all.
Caveats (documented limitations, not bugs)
bool.shouldis honored for matching only when there is nomust/filter(matching OpenSearch’s defaultminimum_should_match). With amust/filterpresent,shouldis scoring-only in OpenSearch — not expressible in the query string — so it’s dropped from the predicate.minimum_should_matchis not supported.bool.filteris treated likemust(a required conjunct). The non-scoring distinction isn’t modelled by the read adapter.- Value charset.
term/terms/range/matchtoken values must be simple tokens (no whitespace or query metacharacters); a value like"a b"or"a:b"returns a clear501pointing you to the native API.match_phraseaccepts spaces (it becomes a quoted phrase). - Write path / aggregations / scripting / mappings / percolators are out of scope — use
GrowlerDB’s connectors for ingestion and
/v1/searchaggregations for analytics.
Unsupported input always returns a structured error ({"error": {"type", "reason"}, "status"})
rather than a wrong result.