The OpenSearch alternative you query with SQL
SereneDB is an Apache 2.0 database for teams moving off OpenSearch: it ranks logs and documents with BM25, aggregates and joins them, and runs vector search over the same rows in one PostgreSQL-compatible SQL query. It also answers a subset of the Elasticsearch REST API that OpenSearch grew from, so simple index, bulk and search calls keep working.
median hot query time · the 83 queries both finish
Search, analytics and joins in one SQL engine
In OpenSearch a question that needs search, aggregations and a join is split in two: Query DSL searches, and the join moves to PPL or the SQL plugin. In SereneDB the same question is one SQL query, because BM25 search, analytics and vector search share one engine.
> Search and analytics in one statement
A BM25 match (@@) sits in the same SQL statement as GROUP BY, joins, window functions and CTEs. OpenSearch's Query DSL has no SQL-style join, so SearchBench's nine join queries have no form there, and joins move to PPL or the SQL plugin.
> Lower query times at a billion logs
On SearchBench's log search and analytics queries over 1B OpenTelemetry logs, SereneDB 26.09.1 had a 7.2× lower median hot query time than OpenSearch 3.8.0 on the 83 queries both run, was faster on 81 of them, and loaded and indexed the logs in 5 min 10 s against 3 h 13 min.
> One binary, OTLP built in
SereneDB installs as one binary with Docker, a .deb package or a tarball, with no JVM heap to size. OpenTelemetry collectors export logs, traces and metrics straight to its OTLP/HTTP endpoints, where OpenSearch takes them through Data Prepper's OTLP source.
> Vectors beside the text
HNSW and IVF indexes live in the same inverted index as the BM25 text, a WHERE filter is applied inside the HNSW walk, and hybrid ranking such as reciprocal rank fusion is written in SQL.
SereneDB vs OpenSearch
Both rank text with BM25 and both index vectors with HNSW. They differ in how you query them, whether one query can search, join and aggregate, and what runs around the engine.
| Capability | SereneDB | OpenSearch |
|---|---|---|
| Query language | Yes: PostgreSQL-dialect SQL over the PostgreSQL wire protocol | Yes: Query DSL, plus SQL and PPL through the SQL plugin |
| Joins | Yes: Any SQL join across tables, indexes, files and attached databases | Partly: No SQL-style join in Query DSL; PPL join caps a right-side subsearch at 50,000 rows by default, and SQL joins take two indexes, with no GROUP BY on the result |
| Full-text search | Yes: IResearch: BM25 and other scorers, phrase, fuzzy and regex queries, highlighting, stemming in 34 languages | Yes: Lucene: BM25 by default, language analyzers, fuzzy queries, highlighting |
| Analytics | Yes: GROUP BY, exact percentiles, window functions and CTEs next to the search predicate | Partly: Aggregations, capped at 65,535 buckets by default; percentiles are approximate |
| Vector and hybrid search | Yes: HNSW and IVF in the same inverted index as text, filters inside the HNSW walk, hybrid ranking written in SQL | Yes: k-NN plugin: HNSW on the Faiss and Lucene engines, IVF on Faiss, filtered k-NN, hybrid queries normalized or fused with RRF in a search pipeline |
| Updates and transactions | Yes: UPDATE, DELETE and multi-statement ACID transactions; the search index catches up within about a second | Partly: Per-document updates and update by query without rollback; no multi-document transactions |
| OpenTelemetry ingest | Yes: OTLP/HTTP endpoints for logs, traces and metrics, written straight to tables | Partly: Through Data Prepper's OTLP source, a separate collector, into OpenSearch indexes |
| REST API and clients | Yes: The PostgreSQL ecosystem of drivers and tools, plus Elasticsearch Query DSL through the Elasticsearch-compatible API | Yes: The full OpenSearch REST API and its official clients |
| Dashboards | Partly: Grafana through its PostgreSQL data source, psql, DBeaver, DataGrip, SereneUI | Yes: OpenSearch Dashboards, with the Alerting and Security Analytics plugins |
| Access control | Yes: Roles, table and column privileges | Yes: Security plugin: document- and field-level security, audit logs, SAML, OpenID Connect and LDAP |
| Retention | Yes: CREATE JOB runs a DELETE on a schedule | Yes: Index State Management policies roll indices over and delete them |
| License | Yes: Apache 2.0 | Yes: Apache 2.0; a Linux Foundation project since 2024 |
✓ supported · ~ supported with limits · ✕ not supported. OpenSearch is the Apache 2.0 fork of Elasticsearch 7.10.2 that AWS started in 2021; the Elasticsearch comparison covers the original. OpenSearch facts from OpenSearch's documentation for 3.9, SereneDB facts from its documentation and runs on 26.10.7. Last reviewed October 31, 2026.
OpenSearch Query DSL in, SQL out
Each tab asks one question of the same log lines in OpenSearch and in SereneDB. The SQL ran on SereneDB 26.10.7 on October 31, 2026; the OpenSearch requests follow OpenSearch's 3.9 documentation.
Phrase search by service
GET logs/_search
{
"size": 0,
"query": {
"bool": {
"must": [{ "match_phrase": { "message": "connection refused" } }],
"filter": [{ "range": { "@timestamp": {
"gte": "2026-10-31T09:00:00Z",
"lt": "2026-10-31T10:00:00Z"
} } }]
}
},
"aggs": {
"by_service": { "terms": { "field": "service" } }
}
}SELECT service, count(*) AS hits
FROM logs
WHERE message @@ ts_phrase('connection refused')
AND ts >= '2026-10-31 09:00:00'
AND ts < '2026-10-31 10:00:00'
GROUP BY service
ORDER BY hits DESC, service;logs is a search table (WITH (storage = 'search')) with an inverted index on message, where ts_phrase matches the words in order, as match_phrase does; service is a keyword field in OpenSearch. GROUP BY returns every service, where the terms aggregation returns the top 10. Sent to SereneDB's Elasticsearch-compatible API, the same request returns the same four buckets.
Top matches
GET logs/_search
{
"size": 10,
"query": {
"bool": {
"must": [{ "match": { "message": "timeout" } }],
"filter": [{ "term": { "level": "error" } }]
}
}
}SELECT ts, service, message,
BM25(logs.tableoid) AS score
FROM logs
WHERE message @@ 'timeout'
AND level = 'error'
ORDER BY score DESC, ts DESC
LIMIT 10;BM25() ranks the rows the @@ match finds, highest first, as OpenSearch's _score does by default, and level = 'error' keeps only errors, like the filter clause. Rows with equal scores come newest first in SQL.
Errors per minute
GET logs/_search
{
"size": 0,
"query": {
"bool": {
"filter": [
{ "term": { "level": "error" } },
{ "range": { "@timestamp": {
"gte": "2026-10-31T09:00:00Z",
"lt": "2026-10-31T10:00:00Z"
} } }
]
}
},
"aggs": {
"per_minute": {
"date_histogram": {
"field": "@timestamp",
"calendar_interval": "1m",
"min_doc_count": 1
},
"aggs": {
"p95_ms": {
"percentiles": { "field": "latency_ms", "percents": [95] }
}
}
}
}
}SELECT date_trunc('minute', ts) AS minute,
count(*) AS errors,
round(quantile_cont(latency_ms, 0.95)) AS p95_ms
FROM logs
WHERE level = 'error'
AND ts >= '2026-10-31 09:00:00'
AND ts < '2026-10-31 10:00:00'
GROUP BY minute
ORDER BY minute;date_trunc('minute', ts) groups like a one-minute calendar_interval, and GROUP BY returns only minutes with errors, as min_doc_count: 1 does. quantile_cont gives an exact 95th percentile; OpenSearch's percentiles aggregation estimates it.
k-NN with a filter
GET docs/_search
{
"size": 3,
"query": {
"knn": {
"embedding": {
"vector": [0.10, 0.20, 0.88, 0.10],
"k": 3,
"filter": { "term": { "category": "errors" } }
}
}
}
}SELECT id, title
FROM docs
WHERE category = 'errors'
ORDER BY embedding <=> [0.10, 0.20, 0.88, 0.10]::FLOAT[4]
LIMIT 3;docs is a search table with an HNSW index on embedding built for cosine distance, which <=> computes. The category filter is applied inside the HNSW walk, as OpenSearch's efficient k-NN filtering applies it during the search.
Join on trace ID
source = logs as f
| where service = 'frontend' and match(message, 'failed')
| inner join ON f.trace_id = p.trace_id [ source = logs
| where service = 'payments'
| fields trace_id, message ] as p
| fields f.trace_id, f.message, p.messageSELECT f.ts, f.trace_id, f.message,
p.message AS payment_log
FROM logs AS f
JOIN logs AS p ON p.trace_id = f.trace_id
WHERE f.service = 'frontend'
AND f.message @@ 'failed'
AND p.service = 'payments'
ORDER BY f.ts;Failed frontend requests whose trace also reached the payments service, the shape of SearchBench's join queries. Query DSL cannot express it; PPL joins the index with a subsearch, capped at 50,000 rows by default. SereneDB joins the table with itself in plain SQL.
Move off OpenSearch in three steps
Run SereneDB next to OpenSearch, send it the same documents, then port the queries and move traffic once the results match. Migrating from Elasticsearch maps match, match_phrase, fuzzy queries and aggregations to SQL, for OpenSearch too.
Start SereneDB with its HTTP APIs
One listener serves the Elasticsearch-compatible API and OTLP on host port 9201, clear of OpenSearch's 9200, next to PostgreSQL connections on 7890.
$ docker run -d --name serenedb -p 7890:7890 -p 9201:9200 -e POSTGRES_PASSWORD=<password> --entrypoint serened serenedb/serenedb --server_directory=/var/lib/serenedb --listen='postgres://0.0.0.0:7890,http://0.0.0.0:9200?api=es&api=otel'Send it the documents
Create each index first with
PUT /<index>and its flat mapping, since SereneDB maps no field on write, then post your_bulkfiles and jobs to port 9201, one index per request. Read older documents out of OpenSearch with a scroll search and post them the same way; OpenTelemetry collectors point theirotlphttpexporter at the same port.$ curl -u postgres:<password> -X POST 'http://localhost:9201/_bulk?refresh=true' -H 'Content-Type: application/x-ndjson' --data-binary @logs.ndjsonQuery in SQL
Every index is a table in the
esschema, and its text fields are searchable through the<index>$textinverted index. Port Query DSL and PPL to SQL, rebuild dashboards in Grafana on its PostgreSQL data source, and retire OpenSearch once the results match.$ psql -h localhost -p 7890 -U postgres -c "SELECT service, count(*) FROM es.\"logs\$text\" WHERE message @@ 'timeout' GROUP BY service"
Yes. Clients and _bulk jobs connect to SereneDB's Elasticsearch-compatible API, which takes indices with flat mappings, _bulk, _search with match, match_phrase, term, range and bool queries, terms, date_histogram and metric aggregations, and scroll. SQL over the PostgreSQL protocol covers everything else, joins included, and the three migration steps above cover the move.
Yes. On SereneDB 26.10.7 the official Python client, opensearch-py 3.2.0, created an index, bulk-loaded documents, searched, aggregated and scrolled through SereneDB's Elasticsearch-compatible API. curl and _bulk files work the same way, one index per request, and OpenTelemetry collectors export to its OTLP endpoints.
The ones its own documentation describes: the JVM heap has to be sized, at half of the system's RAM by OpenSearch's recommendation; a field's type cannot change once it is mapped, so a new type means a new index and a reindex; Query DSL has no SQL-style join, so joins move to PPL or the SQL plugin; and percentile aggregations are approximate. SereneDB answers search, joins and exact aggregates in one SQL query.
OpenSearch is a search and analytics engine: it stores JSON documents in Lucene indexes, answers Query DSL and adds SQL and PPL through a plugin, and its bulk API applies each action on its own, with no transaction across documents. SereneDB is a SQL database with ACID transactions and joins that also builds inverted indexes for full-text and vector search, so a search result joins and aggregates like any other table.
Yes. Both are licensed under Apache 2.0. OpenSearch is developed in the opensearch-project organization on GitHub, and SereneDB at github.com/serenedb/serenedb.
Yes. SereneDB builds HNSW and IVF indexes over FLOAT[N] columns with L2, cosine, inner product and L1 distances, in the same inverted index as text. A WHERE filter is applied inside the HNSW walk, and hybrid ranking such as reciprocal rank fusion is written in SQL, so one query can filter, rank text and rank vectors together.
No. OpenSearch Dashboards reads OpenSearch and a few other data sources such as Prometheus, and PostgreSQL is not among them. Dashboards move to Grafana, which connects to SereneDB through its built-in PostgreSQL data source and runs SQL queries.
On SearchBench, yes. Over 1B OpenTelemetry logs, SereneDB 26.09.1 had a 7.2× lower median hot query time than OpenSearch 3.8.0 on the 83 queries both finish (27.0 ms against 195.2 ms), was faster on 81 of them and loaded the data 37× faster. On cold runs SereneDB's lead is 2.1×. SearchBench runs one query at a time, and OpenSearch 3.9 has not been measured yet.