SereneDB

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.

SereneDB
27.0 ms
OpenSearch
195.2 ms

median hot query time · the 83 queries both finish

SearchBench/ 1B OpenTelemetry logs · 92 queries · SereneDB 26.09.1 vs OpenSearch 3.8.0
7.2×faster median query vs OpenSearch 3.8.081 of 83shared queries faster37×faster ingest
View Full Benchmark →
SereneDBgeomean 1.11×load 5m 10s
OpenSearchgeomean 6.55×load 3h 13m
fastest2×4×8×+no Query DSL formhover a cell to see the query
Why teams switch

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.

01

> 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.

02

> 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.

03

> 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.

04

> 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.

Side by side

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.

CapabilitySereneDBOpenSearch
Query languageYes: PostgreSQL-dialect SQL over the PostgreSQL wire protocolYes: Query DSL, plus SQL and PPL through the SQL plugin
JoinsYes: Any SQL join across tables, indexes, files and attached databasesPartly: 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 searchYes: IResearch: BM25 and other scorers, phrase, fuzzy and regex queries, highlighting, stemming in 34 languagesYes: Lucene: BM25 by default, language analyzers, fuzzy queries, highlighting
AnalyticsYes: GROUP BY, exact percentiles, window functions and CTEs next to the search predicatePartly: Aggregations, capped at 65,535 buckets by default; percentiles are approximate
Vector and hybrid searchYes: HNSW and IVF in the same inverted index as text, filters inside the HNSW walk, hybrid ranking written in SQLYes: 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 transactionsYes: UPDATE, DELETE and multi-statement ACID transactions; the search index catches up within about a secondPartly: Per-document updates and update by query without rollback; no multi-document transactions
OpenTelemetry ingestYes: OTLP/HTTP endpoints for logs, traces and metrics, written straight to tablesPartly: Through Data Prepper's OTLP source, a separate collector, into OpenSearch indexes
REST API and clientsYes: The PostgreSQL ecosystem of drivers and tools, plus Elasticsearch Query DSL through the Elasticsearch-compatible APIYes: The full OpenSearch REST API and its official clients
DashboardsPartly: Grafana through its PostgreSQL data source, psql, DBeaver, DataGrip, SereneUIYes: OpenSearch Dashboards, with the Alerting and Security Analytics plugins
Access controlYes: Roles, table and column privilegesYes: Security plugin: document- and field-level security, audit logs, SAML, OpenID Connect and LDAP
RetentionYes: CREATE JOB runs a DELETE on a scheduleYes: Index State Management policies roll indices over and delete them
LicenseYes: Apache 2.0Yes: 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.


Same question, two queries

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

OpenSearch — Query DSL
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" } }
  }
}
SereneDB — SQL
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

OpenSearch — Query DSL
GET logs/_search
{
  "size": 10,
  "query": {
    "bool": {
      "must":   [{ "match": { "message": "timeout" } }],
      "filter": [{ "term":  { "level": "error" } }]
    }
  }
}
SereneDB — SQL
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

OpenSearch — Query DSL
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] }
        }
      }
    }
  }
}
SereneDB — SQL
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

OpenSearch — Query DSL
GET docs/_search
{
  "size": 3,
  "query": {
    "knn": {
      "embedding": {
        "vector": [0.10, 0.20, 0.88, 0.10],
        "k": 3,
        "filter": { "term": { "category": "errors" } }
      }
    }
  }
}
SereneDB — SQL
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

OpenSearch — PPL
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.message
SereneDB — SQL
SELECT 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.


Migration

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.

  1. 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'
  2. 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 _bulk files 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 their otlphttp exporter 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.ndjson
  3. Query in SQL

    Every index is a table in the es schema, and its text fields are searchable through the <index>$text inverted 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"

FAQ

Questions from OpenSearch users

Go to docs →

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.