SereneDB

An Elasticsearch alternative for search and analytics in SQL

SereneDB is an Apache 2.0 database that ranks text with BM25 and answers in SQL over the PostgreSQL protocol. Where Elasticsearch searches the documents you ingest into its indices, SereneDB also indexes Parquet files, Iceberg tables and PostgreSQL or ClickHouse tables where they already are, and one query searches, joins and aggregates them.

SereneDB
29.0 ms
Elasticsearch
85.6 ms

median hot query time · the 88 queries both finish

SearchBench/ 1B OpenTelemetry logs · 92 queries · one node per engine
3.0×faster median query38×faster ingest1.4×smaller on disk
View Full Benchmark →
SereneDBgeomean 1.11×load 5m 10s
Elasticsearchgeomean 3.45×load 3h 19m
Elasticsearch columnargeomean 6.74×load 3h 28m
fastest2×4×8×+timed out (60 s)hover a cell to see the query
Why teams switch

Search, joins and aggregations in one SQL engine

Teams that switch from Elasticsearch keep BM25 relevance and gain the rest of SQL. In SereneDB a search is one clause of a query, so the same statement joins what it finds to other tables and aggregates it.

01

> Search and analytics in one statement

A BM25 match (@@) sits in the same SQL statement as GROUP BY, joins, window functions and CTEs, so a search result is aggregated or joined without a second query.

02

> Index data where it already lives

Build an inverted index over Parquet, CSV or JSON files on S3 or disk, Iceberg tables, or PostgreSQL and ClickHouse tables. The rows stay at the source; OpenTelemetry logs can also arrive over OTLP/HTTP.

03

> Apache 2.0

SereneDB is licensed under Apache 2.0 and installs as one binary with Docker, a .deb package or a tarball. There is no JVM heap to size.

04

> Keep your clients while you port

An Elasticsearch-compatible REST API answers _bulk and _search with match, term, range and bool queries and terms, date histogram and metric aggregations. Every index it creates is also a SQL table.

Side by side

SereneDB vs Elasticsearch

Both rank text with BM25. They differ in where the data has to be, how you query it, and what runs around the engine.

CapabilitySereneDBElasticsearch
Data locationYes: Indexes files and tables in place: Parquet, CSV and JSON on S3 or disk, Iceberg, attached PostgreSQL and ClickHouseNo: Searches the indices you ingest into; ES|QL data federation over S3 files is experimental in 9.5
Query languageYes: PostgreSQL-dialect SQL over the PostgreSQL wire protocolYes: Query DSL, ES|QL and SQL; its SQL reads one index at a time
JoinsYes: Any SQL join across tables, indexes, files and attached databasesPartly: ES|QL LOOKUP JOIN onto a lookup-mode index; no joins between regular indices
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
Vector and hybrid searchYes: HNSW and IVF in the same inverted index; hybrid ranking written in SQLYes: HNSW with BBQ quantization by default from 384 dimensions, filtered kNN, RRF and linear retrievers
AnalyticsYes: GROUP BY, exact percentiles, window functions and CTEs next to the search predicatePartly: Aggregations, capped at 65,536 buckets by default; percentiles are approximate; ES|QL documents no window functions
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
Schema changesYes: ALTER TABLE adds, renames and drops columns of a regular table; ADD COLUMN leaves indexes as they arePartly: New fields map dynamically; changing a field's type needs a new index and a reindex
Dashboards and integrationsPartly: Grafana through its PostgreSQL data source, psql, DBeaver, DataGrip, SereneUIYes: Kibana, and integrations through Elastic Agent, Beats and Logstash
LicenseYes: Apache 2.0Partly: AGPLv3, SSPL or Elastic License 2.0 for the source, ELv2 binaries; some features need a paid subscription

✓ supported · ~ supported with limits · ✕ not supported. Elasticsearch facts from Elastic's documentation for 9.5, SereneDB facts from its documentation and runs on 26.10.7. Last reviewed October 30, 2026.


Same question, two queries

Query DSL in, SQL out

Each tab asks one question in Elasticsearch and in SereneDB. The SQL ran on SereneDB 26.10.7 on October 30, 2026; the Elasticsearch requests follow Elastic's 9.5 documentation.

Aggregation

Elasticsearch — Query DSL
GET logs/_search
{
  "size": 0,
  "query": {
    "bool": {
      "must":   [{ "match": { "message": "timeout" } }],
      "filter": [{ "range": { "@timestamp": { "gte": "now-1h" } } }]
    }
  },
  "aggs": {
    "per_15m": {
      "date_histogram": { "field": "@timestamp", "fixed_interval": "15m" },
      "aggs": {
        "p95_ms": {
          "percentiles": { "field": "latency_ms", "percents": [95] }
        }
      }
    }
  }
}
SereneDB — SQL
SELECT time_bucket(INTERVAL '15 minutes', ts) AS bucket,
       count(*)                               AS errors,
       round(quantile_cont(latency_ms, 0.95)) AS p95_ms
FROM logs
WHERE message @@ 'timeout'
  AND ts >= now() - INTERVAL '1 hour'
GROUP BY bucket
ORDER BY bucket;

time_bucket and quantile_cont give an exact 95th percentile per 15-minute bucket. Elasticsearch's percentiles aggregation estimates it.

Search + join

Elasticsearch — ES|QL
PUT customers
{
  "settings": { "index.mode": "lookup" },
  "mappings": { "properties": {
    "customer_id": { "type": "long" },
    "plan":        { "type": "keyword" }
  } }
}

FROM logs
| WHERE @timestamp > NOW() - 1 hour
    AND MATCH(message, "timeout refused")
| LOOKUP JOIN customers ON customer_id
| STATS errors = COUNT(*) BY plan
| SORT errors DESC
SereneDB — SQL
-- customers stay in PostgreSQL, logs stay in Parquet files
ATTACH 'host=localhost dbname=postgres user=postgres'
    AS pg (TYPE postgres);
CREATE VIEW lake_logs AS
    SELECT * FROM read_parquet('/data/lake/logs_*.parquet');
CREATE INDEX lake_logs_idx ON lake_logs
    USING inverted (message english, ts) INCLUDE (customer_id);

SELECT c.plan, count(*) AS errors
FROM lake_logs_idx l
JOIN pg.customers c ON c.id = l.customer_id
WHERE l.message @@ to_tsquery('timeout OR refused')
  AND l.ts > now() - INTERVAL '1 hour'
GROUP BY c.plan
ORDER BY errors DESC, c.plan;

The logs stay in Parquet files and the customers in PostgreSQL; SereneDB joins them at query time, and s3:// paths work the same way. In Elasticsearch the right side of LOOKUP JOIN has to be an index in lookup mode.


Migration

Move off Elasticsearch in three steps

Run SereneDB next to Elasticsearch, compare the results on the same data, then switch traffic. Migrating from Elasticsearch maps match, match_phrase, fuzzy queries and aggregations to SQL.

  1. Start SereneDB

    Run it next to Elasticsearch and connect with psql or any PostgreSQL driver.

    $ docker run -d --name serenedb -p 7890:7890 -e POSTGRES_PASSWORD=<password> serenedb/serenedb
  2. Bring the data in

    Index files and tables where they are, send OpenTelemetry logs over OTLP/HTTP, or point _bulk jobs at the Elasticsearch-compatible API.

    $ psql -h localhost -p 7890 -U postgres
  3. Port queries and dashboards

    Rewrite Query DSL as SQL, rebuild dashboards in Grafana on its PostgreSQL data source, and retire Elasticsearch once the results match.

    $ psql -h localhost -p 7890 -U postgres -c "SELECT count(*) FROM logs WHERE message @@ 'timeout'"

FAQ

Questions from Elasticsearch users

Go to docs →

Its source code is available under the AGPLv3, the SSPL and the Elastic License 2.0, and the default binaries ship under the Elastic License. Elastic left Apache 2.0 for the SSPL and the Elastic License in 2021 and added the AGPL option in 2024; some features need a paid subscription. SereneDB is licensed under Apache 2.0.

No. OpenSearch is a separate project that AWS forked from Elasticsearch 7.10.2 in 2021, after Elastic's license change, and it is now run by the OpenSearch Software Foundation. Elastic still develops Elasticsearch. The OpenSearch comparison covers SereneDB against the fork.

Yes. The Elasticsearch-compatible API accepts _bulk and _search with match, match_phrase, term, range and bool queries, terms, date_histogram and metric aggregations, and scroll, and every index it creates is also a SQL table.

For simple keyword search, often. PostgreSQL's built-in full-text search ranks with ts_rank, which is not BM25, and extensions such as pg_search and pg_textsearch add BM25 inside PostgreSQL. SereneDB is a separate server that speaks the PostgreSQL protocol and ranks with BM25; the Postgres full-text search page compares the options.

Not when it already sits in Parquet, CSV or JSON files on S3 or disk, in Iceberg tables, or in PostgreSQL or ClickHouse: you create a view and an inverted index over it, and the rows stay at the source. SereneDB stores the index itself and refreshes it with REINDEX or on a schedule. Data that lives only in Elasticsearch has to be exported first.

No. Kibana reads only from Elasticsearch. Dashboards move to Grafana, which connects to SereneDB through its built-in PostgreSQL data source and runs SQL queries.

SereneDB searches with IResearch, a C++ library with BM25 and other scorers, phrase, fuzzy and regex queries, highlighting and stemming in 34 languages. Its results against Lucene and Tantivy are published in the Search Benchmark Game, and SearchBench compares it with Elasticsearch on 1B logs.

Yes. Point SereneDB at the same files, tables or log stream that your Elasticsearch ingest reads, compare the results of the same queries, and move traffic once they match.