Skip to content

Compatibility

A test that passes on a fake proves something only if the fake behaves like the real system where the test looks. This page is the list you need to judge that for osmem: what is implemented, what is approximated, and what fails on purpose.

Unsupported features generally return an error, usually 400 unsupported_operation_exception with a reason ending in “not supported by osmem”, rather than a plausible empty result. Response shapes, status codes, error types and messages follow OpenSearch (checked against OpenSearch 3.8.0, with 2.19.1 as a second reference) so that client libraries decode them normally. The cases listed here describe tested behavior, not complete parity with every OpenSearch feature.

Indices. Create with settings, mappings and aliases; delete with wildcards and _all; exists; get; _mapping get and put; _settings get and put, validated against OpenSearch’s settings registry (unknown, invalid, private, static and final settings fail with OpenSearch’s messages; include_defaults, flat_settings and settings_filter work); _stats with every section and level=shards; _resolve/index; _refresh, _flush and _forcemerge as no-ops; _open and _close with a real closed state (closed indices reject searches, reads and writes with index_closed_exception, are skipped by wildcards unless expand_wildcards includes them, show as close and red in _cat/indices and cluster health, and accept analysis settings that apply when the index reopens); index and cluster blocks (index.blocks.*, cluster.blocks.*) for reads, writes and metadata changes; cluster health with level, the wait_for_* parameters and the 408 timeout response; cluster settings, state and stats; _nodes with the real address for sniffing clients. Composable, component and legacy index templates are composed and applied on creation, including auto-created indices, and simulation reports overlapping templates. Hidden indices and hidden aliases follow expand_wildcards, ignore_unavailable and allow_no_indices with OpenSearch’s resolution rules, including -name exclusions.

Cat APIs. _cat/indices, aliases, health, count, nodes, master/cluster_manager, plugins, templates, shards, segments, recovery, allocation, thread_pool, pending_tasks, fielddata, nodeattrs, tasks, repositories, snapshots and segment_replication, with format, h (including aliases and wildcards), v, s, help, bytes, time and pri, and OpenSearch’s column layout.

Documents. _doc with auto ids, validated op_type, if_seq_no/if_primary_term on index, update and delete, and internal, external, external_gt and external_gte version types. DELETE enforces external versions, and deleted documents leave a tombstone for index.gc_deletes (60s by default) so versions continue after a delete. _create; _update with doc, doc_as_upsert, upsert (which creates a missing index) and detect_noop; _source; _mget with per-document failures; _bulk parsed line by line like OpenSearch, with per-item errors for require_alias, empty ids and required routing; _delete_by_query and _update_by_query (a query is required; conflicts, max_docs, slices and wait_for_completion=false tasks); _reindex with source and destination validation, max_docs and external destination versioning. GET and search hits return _routing and _ignored. _count accepts only query in the body. A write through an alias that points at several indices without a write index fails as on OpenSearch.

Mapping. text (analyzer, search_analyzer, multi-fields), match_only_text, keyword (normalizer, ignore_above, including zero), every numeric type with OpenSearch’s value validation and coercion, boolean, date and date_nanos with named formats, Java patterns and epoch formats (including dates outside 1677–2262 and nanosecond precision), geo_point in its input formats, ip, object and nested, field aliases, null_value, copy_to, index: false (queries fall back to doc values as on OpenSearch), enabled: false, ignore_malformed (the field is skipped and listed in _ignored), coerce, dynamic: true/false/strict/strict_allow_templates/false_allow_templates, dynamic templates (match, path_match, match_mapping_type, {name} and {dynamic_type}), range types (integer_range, long_range, float_range, double_range, date_range, ip_range), search_as_you_type with its ._2gram… and ._index_prefix subfields, flat_object, join, rank_feature, rank_features, knn_vector (values are validated; kNN search is not implemented) and completion with contexts (category and geo), include_in_parent and include_in_root on nested fields, and similarity: boolean. Mapping parameters are validated per field type: unknown or invalid parameters fail with OpenSearch’s “Failed to parse mapping” errors, and an update that changes a parameter OpenSearch does not allow to change fails with “Cannot update parameter [x] from [a] to [b]”. total_fields.limit, depth.limit, nested_fields.limit, field_name_length.limit, per-document nested_objects.limit and index-level index.mapping.ignore_malformed are applied. Dynamic mapping follows OpenSearch: strings become text with a .keyword sub-field, dates and numbers are detected according to date_detection and numeric_detection, and dotted keys expand into objects. derived fields and star-tree composite mappings are rejected as unsupported.

Queries. Queries are parsed completely before they run, and malformed queries fail with OpenSearch’s error types and messages. match (operator, minimum_should_match, fuzziness with transpositions, zero_terms_query), match_phrase and match_phrase_prefix on real token positions (stop-word gaps, position_increment_gap, max_expansions), match_bool_prefix, multi_match (every type, field patterns, boosts, lenient, cross_fields grouped by analyzer), combined_fields, common, term and terms typed by the mapping (terms lookup, case_insensitive), terms_set, range on numbers and dates (date math, format, time_zone), exists, prefix, wildcard, regexp (Lucene syntax and flags), fuzzy (max_expansions, transpositions), ids, bool, constant_score, dis_max (tie_breaker), boosting (negative and negative_boost), function_score (weight, field_value_factor, random_score, decay functions, every score and boost mode, max_boost, min_score), nested (score_mode, ignore_unmapped, inner_hits), query_string (Lucene’s classic syntax, including regular expressions, escapes and syntax errors) and simple_query_string (flags), geo_distance, geo_bounding_box and geo_polygon (geohash and WKT BBOX input, every distance unit), range queries on range fields (relation), rank_feature (saturation, log, sigmoid, linear), parent_id, has_child and has_parent (without inner_hits), term queries on flat_object subfields, wrapper, intervals (match, prefix, wildcard, fuzzy, regexp, all_of, any_of and filter as real position-based matching, not a presence approximation), span_term, span_near and span_multi, more_like_this, distance_feature (date, date_nanos and geo_point pivots), and geo_shape against geo_point fields (the intersects relation with envelope, polygon and circle shapes). Term-level queries score like OpenSearch (constant 1 × boost), named queries add matched_queries to hits, and index.query.default_field is honored.

Search. from/size up to index.max_result_window (10,000 unless set per index), sort by field with order, missing, mode and unmapped_type (a format key is rejected, as on OpenSearch), _score, _doc, _id, search_after with OpenSearch’s sentinel values for missing numbers and dates, scroll, point in time snapshots, _source filtering, fields, docvalue_fields, version, seq_no_primary_term, track_total_hits, track_scores, min_score, post_filter, collapse with inner_hits, rescore (window_size, weights and every score mode), indices_boost, sliced scroll and point in time, _count, _msearch, typed_keys, preference=_shards:, phase_took, sorting by _geo_distance, _field_caps (including metadata fields, index_filter, meta and the non-searchable and non-aggregatable index lists), /{index}/_validate/query with explain, rewrite and all_shards, /{index}/_explain/{id} and explain in hits (both honor a filtered alias’s filter), _search_shards (modeled on the configured shard/replica count, with routing, preference and a request-body slice), /{index}/_termvectors/{id} and _mtermvectors for already-indexed documents (field_statistics, term_statistics, positions, offsets, realtime), filter_path (including exclusions and **), pretty, rest_total_hits_as_int, the source URL parameter, gzip request bodies. Highlighting implements OpenSearch’s unified, plain and fvh highlighters: sentence and word boundary scanners, fragment_size, number_of_fragments, order: score, no_match_size, require_field_match, highlight_query, matched_fields, tags_schema, the html encoder and max_analyzer_offset, including highlights in nested and collapse inner hits. Search totals count matching documents before search_after paging or collapse. terminate_after counts documents per shard in index order, and size-0 searches with aggregations report terminated_early as OpenSearch 3.8 does. Search body keys are validated in document order with OpenSearch’s messages, and score functions run only when scores are needed.

Painless scripts. A read-only, dependency-free subset of Painless (painlessscript-go) runs script_fields, the script and script_score queries, the script_score function of function_score, sort by _script, and the bucket_script/bucket_selector pipeline aggregations. A document script reads doc['field'].value/.size(), params and _score; a bucket_script/bucket_selector script reads only params (its own static params merged with the buckets_path values, never doc[...]). Scripts can use Math, DateTime/Instant, Java-shaped String methods, and local (script-declared) lists and maps. A script that fails to compile or throws at runtime reports script_exception, as on OpenSearch.

Aggregations. Aggregation requests are parsed and validated before they run, with OpenSearch’s errors. Buckets: terms (size, order, min_doc_count, missing, include/exclude with Lucene regular expressions, show_term_doc_count_error), multi_terms, rare_terms, significant_terms and significant_text (jlh, chi_square, gnd, mutual_information and percentage heuristics), range, date_range, ip_range, geo_distance, histogram, date_histogram (calendar and fixed intervals, time_zone with DST-correct rounding, offset, format, extended_bounds, gap filling), auto_date_histogram, variable_width_histogram, filter, filters, adjacency_matrix, missing, global, nested and reverse_nested, sampler, composite (including as a child of reverse_nested) with after, geohash_grid and geotile_grid. Every numeric metric and range/composite bucket keys handle unsigned_long fields beyond 2^53, not just doubles. Metrics: avg, sum, min, max, value_count, stats, extended_stats, cardinality, percentiles and percentile_ranks (t-digest, as on OpenSearch; the hdr method is not implemented), median_absolute_deviation, top_hits, weighted_avg, geo_bounds, geo_centroid. Pipelines: cumulative_sum, derivative, bucket_sort, avg/sum/min/max/stats/extended_stats/percentiles_bucket, serial_diff, moving_avg (simple, linear and ewma models), bucket_script and bucket_selector (buckets_path as a single path or a name-to-path object; the script sees only params, never doc[...]). search.max_buckets is read from cluster settings (transient, then persistent) rather than a fixed default. Responses render meta, typed_keys and the *_as_string values of format. Sub-aggregations nest.

Suggesters. suggest in a _search body runs term (edit-distance corrections against indexed terms), phrase (n-gram candidates scored with a stupid-backoff language model, an approximation of Lucene’s smoothed model — see Approximated) and completion suggesters. Completion fields support contexts (category and geo, including mandatory-context validation at index time), skip_duplicates, weight ordering and fuzzy prefix matching.

Analysis. _analyze follows OpenSearch’s token types, UTF-16 offsets and explain output for the built-in analyzers, tokenizers, token filters and char filters osmem implements; components it does not implement fail with an error that ends in “not supported by osmem”. Analysis settings are validated when an index is created. Indexing and search still tokenize with bleve-based analyzers: standard, simple, whitespace, keyword, stop, pattern and bleve’s language analyzers, and custom analyzers built from tokenizers (standard, whitespace, keyword, letter, pattern, ngram, edge_ngram, char_group, uax_url_email), token filters (lowercase, asciifolding, stop, ngram, edge_ngram, shingle, stemmer, snowball, porter_stem, truncate, length, unique, reverse, cjk_bigram, cjk_width) and char filters (html_strip, pattern_replace); normalizers. With Japanese support: kuromoji analyzer, kuromoji_tokenizer with modes, user dictionaries, kuromoji_baseform, kuromoji_part_of_speech, cjk_width, ja_stop, kuromoji_stemmer, kuromoji_readingform.

  • Scores. bleve’s BM25 is not Lucene’s. Rankings agree for ordinary queries; _score values and ties of full-text queries do not. Constant-score parts (filters, term-level queries) score as on OpenSearch.
  • Numeric range and sort precision. Term queries on integral fields use exact values, but range queries and sorting on very large long values can still lose precision beyond 2^53.
  • Text fielddata. Sorting a text field is enabled when fielddata:true; osmem sorts the field’s analyzed terms. Text aggregations with fielddata are still rejected, and sorting without fielddata remains an error.
  • Search options without effect. timeout and stats are validated but have no effect, profile returns OpenSearch’s response structure with zero timings, a named search_pipeline must exist, and runtime_mappings is rejected.
  • Analyzers. Indexing uses bleve’s analyzers, so the terms that are indexed can differ from what _analyze reports for the same analyzer. Language analyzers use bleve’s stemmers and stop lists. Japanese segmentation comes from kagome with the IPA dictionary rather than kuromoji and can differ on unknown words; Korean and Chinese are CJK bigrams. Components of a custom analyzer that indexing does not implement are skipped with a warning.
  • Exactness. cardinality is exact, while OpenSearch approximates it. Percentiles use the same t-digest algorithm as OpenSearch, but values can differ slightly on data sets whose centroids merge.
  • Pre-aggregated document counts. Bucket aggregations ignore _doc_count; OpenSearch can use _doc_count to count pre-aggregated documents.
  • Shards and routing. Responses report the configured shard counts, and terminate_after and slices follow OpenSearch’s per-shard rules. Get, exists, delete, update, mget and single-document reads/writes compute OpenSearch’s routing hash and treat a wrong or missing routing value the way a real wrong shard would (not found), on indices with more than one shard. Documents are still not physically re-keyed by (routing, id): writing the same _id with a different routing value still updates the one stored document rather than coexisting with it on another shard.
  • Refresh. Writes are visible immediately. A test cannot observe the window between a write and a refresh.
  • Error traces. error_trace=true adds a stack_trace to every error osmem renders and to each of its root_cause and caused_by entries, the way OpenSearch does, but each one is a single fabricated frame naming the exception rather than a real Java stack trace.

The following differences remain after the differential audit described at the end of this page.

Area Difference
Field types A regexp that exceeds Lucene’s determinization limit is not rejected. Hit totals of exists queries on text and shingle fields can differ after deletions. Custom BM25 k1/b parameters are ignored. Sort values of long values beyond 2^53 lose precision, and a composite aggregation’s unsigned_long bucket key can be off by one at that same boundary. date_histogram over a mapped date_range field ignores hard_bounds.
Error details When a request body has several problems, osmem can report a different one than OpenSearch, because it checks keys in sorted order rather than document order. Some rarely used cause chains differ. _mtermvectors does not reject deprecated parameter aliases (_version_type, camelCase names) the way OpenSearch does.
Explanations explain=true, _explain and Validate Query explanations are produced for constant-score queries and boolean combinations of them; BM25 and function_score explanations are not reproduced. preference=_only_nodes: is accepted without selecting nodes, and _geo_distance sort values can differ in the last digits.
Statistics and nodes _stats, _cat and cluster statistics counters are zero and store sizes are estimates.
Aggregations JSON key order is not kept, so order objects with several keys and the choice between aggs and aggregations in one request can differ. Aggregations over indices with conflicting mappings can fail where OpenSearch reports per-shard results. Terms shard_size has no effect because osmem has one search stream. profile on an aggregation always reports zero sub-aggregations.
Highlighting When both tags_schema and pre_tags are set, tags_schema wins regardless of key order. top_hits highlights do not see the query terms. index.highlight.max_analyzed_offset and boundary_scanner_locale are not implemented.
Suggesters A completion suggestion’s matched contexts are not echoed back in its option, and typed_keys does not prefix completion or phrase suggestion names. A completion field’s contexts mapping is echoed back as written (a raw distance string, lowercase type) rather than OpenSearch’s normalized form (a resolved geohash precision, uppercase type).
Other APIs Search templates and _rank_eval have no routes. _mtermvectors and _termvectors do not support the on-the-fly doc/per_field_analyzer modes, only already-indexed documents.

PIT searches check expiry and extend keep_alive when the search request supplies it.

Painless scripts that mutate a document (ctx._source in scripted updates, _update_by_query/_reindex with a script) or aggregate across documents (scripted_metric), object or array script params, using a script instead of a field in a bucket or value-source aggregation, moving_fn (its script needs a values array parameter the embedded runtime cannot construct yet), script_heuristic in significant terms, kNN and neural search, percolator, the children/parent aggregations and inner_hits on join queries, most span queries (span_or, span_first, span_not, span_containing, span_within, field_masking_span), geo_shape against an actual geo_shape-typed field (only a bounding-box approximation; geo_shape queries against geo_point fields are implemented, see Queries), the hdr method of percentiles, matrix_stats, geohex_grid, derived fields, search templates, ranking evaluation, _list/indices, _list/shards, ingest pipelines (a write that references a pipeline fails with “pipeline with id [x] does not exist”), data streams, rollover, shrink/split/clone, _tasks, security, snapshots, _nodes/stats, _nodes/usage, hot threads, YAML, CBOR and SMILE request bodies and responses (format=yaml).

osmem rejects some input that OpenSearch accepts, so that a test passing on osmem also passes on Amazon OpenSearch Service managed domains and on Amazon OpenSearch Serverless.

  • Index names starting with . fail with 400 invalid_index_name_exception (“must not start with ‘.’”), including hidden indices and auto-created indices. OpenSearch only logs a deprecation warning. Serverless refuses these names, and its data access policies cannot name them.
  • Alias names also fail when they start with . or contain upper case (invalid_alias_name_exception). OpenSearch has no lowercase rule for aliases.
  • Names outside the Serverless character set produce a warning (WithWarnings, or stderr for osmem-server). The set is [a-z0-9;&$%] for the first character and [a-z0-9+.~=_;&$%-] after it, so non-ASCII names such as Japanese ones are outside it.
  • To reproduce dot-prefixed fixtures such as .kibana, opt out with osmem.WithDotNames() or --allow-dot-names.

osmem.WithServerless(osmem.ServerlessSearch | ServerlessTimeSeries | ServerlessVectorSearch), or --serverless search|timeseries|vectorsearch, restricts the cluster further:

  • Only the operations in AWS’s “Supported OpenSearch API operations” table are routed. Everything else answers 404 with an empty body, as Serverless does. That includes GET /, _refresh, scroll, _reindex, by-query APIs, _cluster, _nodes, _stats, open/close, legacy _template, and _cat other than indices, aliases and templates.
  • refresh=true (or a bare refresh) and refresh=wait_for on writes fail with 400 status_exception (“true refresh policy is not supported.”).
  • Names outside the Serverless character set are rejected instead of warned about.
  • Time series and vector search collections refuse client-supplied document IDs. This covers PUT _doc/{id}, _create/{id}, _update/{id}, and bulk index/create items with _id or update items. They fail with 400 illegal_argument_exception (“Document ID is not supported in create/index operation request”).
  • number_of_shards, number_of_replicas and refresh_interval produce a warning, because Serverless manages them.
  • _cat/indices has no health and status columns.

Serverless publishes no index-name rule. The character set comes from the resource-name pattern of its API. The wait_for and update error wordings are inferred from the true and create/index cases.

  • opensearch-go v4 is tested in CI. go-elasticsearch v8 receives the X-Elastic-Product header it checks.
  • olivere/elastic sniffs /_nodes; osmem reports the real listen address there.
  • For clients that insist on an Elasticsearch 7 version string, set the cluster setting compatibility.override_main_response_version: true; GET / then reports 7.10.2.
  • Errors follow OpenSearch’s {"error": {"root_cause", "type", "reason", "caused_by", ...}, "status"} shape, including shard failures inside search_phase_execution_exception, OpenSearch’s rules for choosing root causes from nested exceptions, and the positions of parse errors ([line:col] reason prefixes and line/col fields, also for bodies OpenSearch re-serializes such as delete-by-query, reindex sources and alias filters). Unknown URLs return 400 with a string error. A wrong method returns a flat 405 body with an Allow header, and OPTIONS answers with the allowed methods.
  • Requests are checked the way OpenSearch’s REST layer checks them: every route rejects unrecognized URL parameters with 400 (with “did you mean” suggestions) and validates parameter values, bodies need a JSON Content-Type (406 otherwise), duplicate JSON keys are rejected, and filter_path never filters an error response.
  • Numbers in responses are written the way Java writes them (1.0, 1.0E-4). float, half_float and scaled_float doc values keep their stored precision in sort values, aggregations, fields and docvalue_fields.

The OpenSearch project maintains a broad REST API YAML test suite. Its runner is part of OpenSearch’s Java test framework, so those YAML files do not run directly as osmem Go tests. osmem has focused Go regression tests for the behaviors described above, but does not yet have a general differential runner for the full YAML corpus. A passing osmem test therefore establishes only the behavior asserted by that test.

The separate OpenSearch API Specification project also provides YAML stories and an npm run test:spec runner that can target a configured OPENSEARCH_URL. Its guide’s sample coverage report evaluates about 39% of verb/path combinations. That makes it useful for request and response shape checks; the core 2.19 REST suite has broader multi-step behavior cases, but its Java test harness needs adaptation before osmem can use it.

On 2026-09-15, 32 targeted scenarios were run against the official OpenSearch 3.8.0 Docker server and the osmem handler. Both pass the same status and selected response-field assertions after the engine fixes. The 12 differences previously found at v0.1.3 are fixed for these cases.

Area Verified behavior
Write validation Rejects external versions with create operations, generated IDs, or sequence-number conditions; valid external-version and compare-and-set writes still succeed. Invalid create conditions in Bulk are rejected before writing.
Bulk Preserves integer versions above 2^53, including signed 64-bit maximum; rejects overflow; coerces JSON fractional versions toward zero. Unknown action metadata and update action versioning reject the request. A later invalid action prevents earlier writes.
Resolve index Returns indices and aliases separately, all aliases of a resolved index, and the hidden attribute. Applies wildcard options in order: none,open enables open indices; open,none produces empty arrays. hidden alone does not enable open indices. Closed indices report the closed attribute, and names that do not exist are left out instead of failing the request.
Index templates A matching composable template suppresses legacy templates. Equal-priority overlap checks follow the 3.8 stripped-pattern rule. Disjoint patterns, different priorities, and replacement of the same template remain valid.

OpenSearch 3.8 uses a deliberately permissive template overlap check: some theoretical overlaps, such as logs* and *2026 under the same prefix, can coexist. osmem follows this rule.

The repository’s testdata/compatibility/README.md describes the fixtures, real-server runner, and saved before/after responses. go test -run '^TestCompatibilityProbes$' -count=1 -v . runs the osmem regression cases; they are also part of ordinary tests. This targeted comparison does not establish complete 3.8 compatibility: unasserted error messages and response fields may differ, and the other limitations on this page still apply. osmem reports OpenSearch 3.8.0 at GET /.

Differential audit against OpenSearch 3.8.0

Section titled “Differential audit against OpenSearch 3.8.0”

On 2026-09-16 a differential harness sent the same request sequences to OpenSearch 3.8.0 (the reference), OpenSearch 2.19.1 and osmem, and compared status codes and complete response bodies after removing volatile values such as took, UUIDs, node ids and generated document ids. The 477 scenarios (6,139 requests) cover search, documents, mappings and field types, aggregations, the query DSL, highlighting, cluster and index administration, the cat APIs and HTTP-layer behavior (URL parameters, content types, methods, filter_path). When the two OpenSearch versions disagreed, osmem follows 3.8.0.

Point Requests that differ from OpenSearch 3.8.0 Scenarios with a difference
Before the fixes 3,263 447
After the fixes 308 101

Each fix added Go regression tests whose expectations were taken from OpenSearch 3.8.0 responses. Most remaining differences are listed under Known behavioral gaps and Not supported: field types whose values are not validated, features that osmem rejects on purpose, statistics that osmem does not track, and BM25 score values. The harness and its scenario files are not part of the repository.

A second pass replayed OpenSearch’s own REST API YAML test suite (314 files across search, aggregations, documents, mappings and index/cluster administration) against OpenSearch 3.8.0 and osmem the same way, ignoring the YAML files’ own assertions in favor of a direct response comparison. Of 1,200 scenarios, 495 showed a genuine difference; implementing intervals, the suggest API, rare_terms/significant_terms/significant_text/auto_date_histogram/variable_width_histogram, more_like_this, distance_feature, geo_shape against geo_point fields, span queries, _search_shards, term vectors, routing-based shard mismatches, and fixing unsigned_long aggregations, date_range bucket bounds, the dynamic search.max_buckets setting and a handful of smaller bugs brought that down to 244 (excluding differences that are only in _score/max_score values or aggregation profile timings, both already listed as approximated above). The remaining differences are folded into the sections above; a second harness (also not part of the repository) was used for this pass since the first was not preserved.