Skip to main content

SereneDB Team

Sep 7, 2026 · 29 minutes read

SereneDB vs ParadeDB vs TigerData vs Postgres

92 search and analytics queries across the whole Postgres family, at 100M and 1B logs

Postgres is an awesome database. It is everywhere, it powers huge deployments and it has an extension ecosystem so rich that some extensions turn into products on their own. From day one we decided SereneDB would be Postgres compatible, partly to join that ecosystem and mostly to bring the fastest search and analytics into it.

That might raise the obvious objection. Why would you need SereneDB at all? You already run Postgres and in the worst case you could install a couple of extensions.

That is what this post is about. We took vanilla Postgres 18, ParadeDB which ships pg_search, TigerData which ships pg_textsearch and put all three next to SereneDB on the same box, over the same 100M and 1B OpenTelemetry logs, answering the same 92 queries.

TL;DR

If you only want just the numbers first.

Results are at serenedb.com/searchbench and the benchmark itself lives at github.com/serenedb/searchbench. Elasticsearch, OpenSearch and ArangoDB are hidden in the view above to keep this about Postgres. Unhide them on the dashboard whenever you want.

Chapter 1. Three ways to search in Postgres

"Great question! You're absolutely right that Postgres can handle this, and honestly, for most teams, most workloads, and most budgets it's the pragmatic choice."

Claude

Vanilla FTS

It's the obvious baseline everybody already has. Postgres full-text search is Good Enough! is the canonical post and there are a dozen more saying the same thing. You need to create a tsvector column, put a GIN index on it, you rank with ts_rank_cd and you're set.

For a lot of products that really is good enough. GIN hands you a bitmap of matching rows and then Postgres does the rest. There is no BM25. Ranking is ts_rank over weighted term frequencies or ts_rank_cd over cover density, both don't take into account global stats, so nothing gets weighted by how rare a word is. Search supports terms, AND, OR, NOT, phrase, proximity and prefix. Fuzzy, regexp over the term dictionary, infix wildcards and minimum should match are not there at all, so full-scan is the only option.

pg_textsearch

pg_textsearch comes from Timescale, now TigerData. It went v1.0 GA on 3 April 2026 under an OSS license and it is a genuinely ambitious piece of work. They wrote the whole search engine library in C directly on Postgres pages: tokenization, postings format, compression and the scoring loop. Block-Max WAND drives the top-k path and index builds run in parallel.

One important detail is that the bm25 index serves ORDER BY body <@> query LIMIT k and nothing else, so the index ranks but it cannot count or group.

pg_search is ParadeDB. Underneath it is Tantivy, the Rust search library Paul Masurel started in 2016 as a port of Lucene's ideas. Tantivy is fast, well tested and the engine behind Quickwit, so starting there is a reasonable bet. Its team also introduced Search Benchmark, The Game, the best known benchmark for search engine libraries, which SereneDB's engine IResearch won back in March 2026.

The hard part of the integration is that Tantivy expects to own its files and Postgres expects to own its pages. ParadeDB solved that with a custom block storage layout that puts Tantivy's segment files inside Postgres buffers, under Postgres MVCC and inside Postgres WAL. Their fast fields are columnar, which is what lets an aggregation push down into the index instead of walking the heap. That is a lot of engineering and it shows.

So the two extensions take genuinely different routes. One takes a mature Rust search library and makes it live inside Postgres pages. The other writes an entire search engine from scratch in C on those same pages. Both are great pieces of engineering and for us it was really interesting to see where SereneDB stands against them.

Chapter 2. What about analytics

"It's worth noting that 'analytics' can mean quite different things depending on your team, your data, and your goals. Let me unpack that a little."

Claude

ParadeDB

ParadeDB used to answer this with pg_analytics, a DuckDB backed lakehouse extension. That repo is archived now with its last commit in March 2025 and the analytics story moved inside pg_search. Columnar fast fields, bucket and metric aggregations and facets all run off the BM25 index itself. Their Aggregate Scan push-down is what makes a GROUP BY cheap. One index, two jobs.

TigerData

TigerData comes at it from the time series side and gives you two ways to store a chunk.

Hypercore

A hypertable is a table partitioned by a time column. TimescaleDB splits it into chunks covering fixed time ranges. Each chunk is a real table underneath with its own indexes and its own statistics. The planner throws away every chunk outside your WHERE clause before it reads anything, which is the trick most of TigerData's wins come from.

Hypercore then decides how each chunk is stored. It keeps the newest chunk in rowstore for fast inserts while older chunks get compressed into columnstore. They report 90 to 98 percent compression. Add continuous aggregates on top and you have a real analytics engine for time ordered data.

Columnstore

Rowstore chunks are the default and that is what the TigerData column in this post runs on. You can also declare the chunks columnar at birth, which is where those compression numbers are supposed to come from and where scans over a couple of columns get cheap. We built a second adapter that does exactly that and put the same 92 queries through it.

The catch is that a columnar chunk holds no search index. Converting a chunk drops its indexes and only the columnstore's own sparse minmax and bloom indexes survive, which skip batches of rows rather than locating the rows containing a term. The hypercore access method that once carried B-trees over columnar chunks shipped in TimescaleDB 2.18 and was removed in 2.22, so today nothing puts a BM25 or GIN index in front of a columnar chunk.

CREATE INDEX does not fail either. It succeeds and indexes nothing, because the rows now live in an internal compressed relation while the chunk the index hangs off is empty. Top-k queries then come back with zero rows instead of an error and their scores collapse to zero, which turns any ranking into an arbitrary slice. Fast, silent and wrong is the worst thing a benchmark can publish, so the adapter builds no index at all and every query that resolves the BM25 index by name is recorded as a failure.

On paper all three of these hold up. ParadeDB gets aggregation for free out of the index it already built for search. TigerData gets compression plus time based pruning, which is exactly what a log workload wants. Columnstore pushes that trade further and gives up the text index entirely for a much smaller footprint. Every one of them is a reasonable answer and that is exactly why we wanted to measure them.

Chapter 3. What SearchBench measures

"Judging empirically, the extension approach should be competitive here given the architectural similarities."

Claude

SearchBench is our open benchmark for search and analytics, which we announced at Berlin Buzzwords and first published in July when we ran it against ArangoDB. The shape follows ClickBench: every engine implements the same seven shell scripts and a shared driver orchestrates them, so adding an engine is an afternoon of work.

We run two scales, 100M and 1B log records and measure three things at each: time to ingest and index, size on disk and the median latency across the 92 queries. Result caching is off everywhere.

Timing. Three runs per query, we report the best of the last two. Each measurement is the client round trip for one query, taken from psql's \timing, so client spawn and connect cost stays out of the number.

The 60 second rule. Any single query gets 60 seconds. If it does not come back in that window we record that it did not finish. There are a lot of those below and we want to be precise about what they mean: a query has been executing for more than 60s and hit the cap.

What this does not measure. One query at a time from a single client, so everything here is latency and none of it is throughput. No concurrent load, no ingest running alongside queries, no updates or deletes, nothing distributed and no memory footprint. All fair questions about a log store and none of them answered here.

We try to give every engine the best configuration we can build for it. That is the rule above all the others, because without it none of these numbers are worth publishing. We will not always have managed it, which is why every adapter sits in the repo where you can check our work.

The data

The corpus is generated OpenTelemetry logs. We use the set published by TextBench instead of rolling our own. Every record has 15 columns describing one log message:

Timestampwhen
TraceId, SpanId, TraceFlagstrace correlation
SeverityText, SeverityNumberlog level, as text and as a number
ServiceNamewhich service emitted it
Bodythe message itself and the only full-text field
ResourceSchemaUrl, ResourceAttributeswhat produced the log
ScopeSchemaUrl, ScopeName, ScopeVersion, ScopeAttributeswhich instrumentation scope
LogAttributesper-record key/values

The three *Attributes columns are JSON maps. The database has to store all 15 columns, including the ones no query touches. That rule matters more than it sounds, because an engine that only keeps the searchable text will report a beautiful index size and then be useless for actually reading your logs.

The queries

92 of them, in five families:

familywhat it is
counthow many logs match
top_k (bm25)top 100 ordered by relevance, BM25 scored
group_bymatches bucketed by service or by time
top_k (time)top 100 ordered by timestamp, newest first, which is log tailing
joincorrelate two services through a shared trace id

Two of those five are top 100 problems and they differ only in the ordering key: top_k (bm25) ranks by relevance, top_k (time) ranks by timestamp. Keeping them apart matters, because the two get answered by completely different machinery.

Each family is then crossed with how you are asking: single term, conjunction, disjunction, minimum should match, phrase, phrase with proximity, prefix, regexp, wildcard, fuzzy, negation and time windows. Plus a term frequency dimension, because a word in 40 percent of your logs and a word in 0.001 percent of them are different problems.

That grid is what produces 92. Every query carries its tags in the result file, so you can slice by family or by filter or by term frequency instead of staring at one aggregate. In the raw files the two top 100 families are tagged top_k and recent.

Chapter 4. How we set it up

"I've verified this thoroughly."

Claude

The machine

One GCP n2-standard-32, so 32 vCPUs of Intel Ice Lake at 2.6 GHz and 128 GB of RAM, running Ubuntu 24.04.4 LTS on a 3.9 TB pd-ssd. One instance of each engine, no clustering and no sharding anywhere.

engineversionimage
SereneDB26.09.0serenedb/serenedb:26.09.0
ParadeDBpg_search 0.25.0 on Postgres 18paradedb/paradedb:0.25.0-pg18
TigerDatapg_textsearch 1.4.0 on Postgres 18.4 with TimescaleDB 2.29.2timescale/timescaledb-ha:pg18.4-ts2.29.2-all
Postgres18.4 with fuzzystrmatchpostgres:18-alpine

All three Postgres engines get the same server config

Where we started. The three images do not begin from the same place. ParadeDB's image and TigerData's timescaledb-ha both self tune to the host at initdb. Plain postgres:18-alpine ships stock. Either way the defaults are not necessarily the best config for this box or this workload, so we tuned all three.

What we tuned and why.

  • Parallelism. max_parallel_workers_per_gather ships at 2, which had vanilla Postgres answering on 3 processes while the self tuned images ran on 17. We open the whole worker pool so every engine gets the same width.
  • Memory. shared_buffers lands at about a quarter of the box and effective_cache_size is a planner hint at roughly three quarters. work_mem is per sort or hash node per worker, so what really sizes a parallel hash join is that value times 17 processes.
  • Index builds. This is where the biggest win was. On a 10M sweep the maintenance workers took a build from 52.5 seconds at 4 to 34.2 at 8 to 24.5 at 16. Memory flattened early, 40.9 at 1 GB against 34.2 at 2 GB and 34.3 at 4 GB, so the 6 GB we set is headroom for the billion row build rather than a win at 100M.
  • Read path. Postgres 18's async IO pool ships 3 io_workers that every backend funnels its reads through. At 100M a cold Q08 took 16.2 seconds on 3 against 1.44 on 16. That one setting pushed 7 TigerData queries past the 60 second cap on its own. Hot latency is untouched either way, so it is purely a cold path fix.
  • Write path. A larger max_wal_size means fewer checkpoint stalls during a bulk ingest.
  • Container. Docker gives a container 64 MB of /dev/shm. A parallel hash table here is bounded by work_mem times 17 processes, so 6.2 GiB. Even 8 GB overflows at scale.

All of it lives in one file called lib/pg-tuning.sh, sourced by every Postgres-wire adapter and sized for 128 GB and 32 cores:

settingvalue
shared_buffers32 GB
work_mem374 MB
effective_cache_size94 GB
max_parallel_workers, _per_gather, max_worker_processes16
max_parallel_maintenance_workers16
maintenance_work_mem6 GB
io_workers16
max_wal_size8 GB
/dev/shm24 GB

What each engine got

Every engine indexes the same text the same way and stores all 15 columns. What follows is each setting we chose and the reason we chose it.

SereneDB. A search table is a storage engine built on IResearch, our own C++ search library. The IResearch columnstore holds the data and indexes are optional markup on top of it, inverted or geo or vector. We declare the table with CREATE TABLE ... WITH (storage = 'search'), mark up one column with an inverted index and load the parquet through a view once. Every query after that runs against the table.

  • All 15 columns live in the columnstore, so retrieval, filtering and aggregation never leave the table.
  • The en dictionary is keyword with frequency, norm and position on. Frequencies and norms are what BM25 scores from and positions are what the phrase and proximity queries need.
  • The inverted index marks up ts_split_by_non_alpha(Body, true), which lowercases and splits on runs of non alphanumeric characters. That is the tokenization every other engine here is matched against. Lookups walk a term dictionary an automaton can traverse, which is why regexp, prefix, wildcard and fuzzy stay index lookups rather than scans.
  • optimize_top_k = 'bm25(1.2, 0.75)' sits on the table and turns on pruning for the 21 scored queries, with the same k1 and b everyone else gets.

ParadeDB. Tantivy living inside Postgres pages, reached through the @@@ operator and a custom scan. Its fast fields are columnar, so aggregations push down into the index instead of walking the heap. One BM25 index, shaped the way that measured best for ParadeDB rather than copied from ours.

  • Four columns are indexed and searchable: body full text, service_name as a literal, severity_number as numeric and timestamp as a datetime. That is three more than SereneDB indexes, where only Body gets an inverted index and the rest are filtered out of the columnstore. Giving ParadeDB the extra three is what measured best for it, so that is what it got.
  • Three more are stored as columnar fast fields with no postings. severity_text and scope_name are GROUP BY keys. Without the fast field they lose the Aggregate Scan push-down and run 8 to 10x slower. trace_id is insurance on the join column.
  • body uses the pdb.simple tokenizer, which does the same lowercase and split as SereneDB. We verified this on a live instance because paradedb.schema() reports pdb.simple under the name default.

TigerData, hypertable. pg_textsearch BM25 on a hypertable, which is one logical table that TimescaleDB stores as many physical chunks, each covering a slice of the time range and each a real table with its own indexes and its own statistics. That shapes everything below. Indexes get built per chunk, BM25 corpus statistics are per chunk and the planner can drop whole chunks that fall outside a time filter.

  • The adapter runs two text indexes on the same table, which is forced rather than chosen. A BM25 index answers ORDER BY body <@> query LIMIT k and nothing else, so every query that counts or groups needs somewhere else to go. BM25 serves 16 of the 21 scored top-k queries and a GIN tsvector index serves every boolean and aggregating query, using the same SQL as the vanilla Postgres adapter. Running the whole workload through BM25 instead means sequential scans, which measured several times slower.
  • We cut it into 8 chunks. That gives the GIN families a Parallel Append and gives time ordered LIMIT queries an early exit off the automatic per chunk timestamp index.
  • Accepted caveat: because those statistics are per chunk, each one normalises BM25 over its own eighth of the data. Scores shift by 1 to 2 percent and members of a large tie class can swap at the LIMIT boundary. Every row returned is still a true match.

TigerData, columnstore. The same engine with the chunks stored columnar, which trades every index away for compression. The second adapter from Chapter 2 uses the same image, schema and queries, so a delta against the row above isolates the storage engine.

  • tsdb.enable_columnstore = true declares the chunks columnar at birth rather than converting them on an age policy.
  • tsdb.segmentby = 'service_name' because that is the workload's one low cardinality equality key. It appears in nine queries plus the b.service_name side of all nine joins, so segmenting on it lets those filters skip whole batches.
  • tsdb.orderby = 'timestamp DESC' because that is the sort of every recent query. It also puts a minmax sparse index on timestamp, which is what lets the BETWEEN windows prune.
  • Load then convert, with compress_chunk() over show_chunks() after ingest, so load_time covers the columnar rewrite the same way the rowstore's covers its index builds.
  • No search index at all, for the reason in Chapter 2.

Postgres. A tsvector of lexemes with a GIN index over it, which gives you set membership and nothing more. The index is built over the to_tsvector('simple', body) expression, with the queries written against that identical expression so the planner matches it.

  • simple rather than english, so there is no stemming and no stopword removal. That matches the raw token model every other engine uses.
  • fuzzystrmatch is installed for levenshtein(), which is the only fuzzy matching Postgres has.
  • ts_rank_cd stands in for BM25 on the top-k family. It is not BM25 and those results compare in intent rather than in score.

One thing we enjoyed more than we should have: all three Postgres adapters ingest the corpus through a serened container, because none of them can read parquet.

Where the dialects do not line up

Not every engine can express every shape in the grid. Where one cannot, we wrote the closest thing that returns the same rows and listed it here rather than quietly dropping the query.

shapeSereneDBParadeDBTigerDataPostgres
minimum should matchts_any([...], 2)six pairwise ORs by handsix way OR inside to_tsquerysix way OR inside to_tsquery
prefixts_starts_withparadedb.regex()conn:* on GINconn:* on GIN
prefix anchored regexpts_regexpparadedb.regex()rewritten to charg:*rewritten to charg:*
infix wildcard, mid word regexpts_like, ts_regexpparadedb.regex()body ~* seq scanbody ~* seq scan
fuzzyts_levenshteinparadedb.fuzzy_term()levenshtein() seq scanlevenshtein() seq scan
relevance scoreBM25()pdb.score()BM25 on 16 top-k, ts_rank_cd on 5ts_rank_cd

Three notes on that table. TigerData and Postgres are the same everywhere except the scored family, because <@> is ORDER BY only, so every boolean and aggregating query on TigerData runs the vanilla Postgres SQL against GIN. TigerData does put a pigeonhole prefilter in front of its fuzzy scan, splitting the pattern into d+1 parts so at least one survives d edits, which narrows the scan without ever producing a false negative. And ts_rank_cd is not BM25, so the Postgres scores compare in intent rather than in value.

The columnstore variant is missing from the table because it has no index of any kind. Every text predicate there decompresses batches and filters. The 16 queries that resolve the BM25 index by name fail outright.

Chapter 5. Load and size

"Perfect! The load completed successfully."

Claude

Two numbers before a single query runs: how long it takes to get the corpus in, and what it costs you on disk once it is there.

Load and size
SereneDBParadeDBTigerData hypertableTigerData columnstorePostgres

Bars are normalized within each metric; labels show absolute values.

Load is the one place nobody is close. The next fastest after us is ParadeDB at 20x our time at 100M and 26x at a billion, then Postgres at 23x and 29x, then the two TigerData configs between 32x and 49x. Where the time goes differs. ParadeDB and Postgres are each building one text index. The hypertable builds two index families per chunk. The columnstore builds none at all and pays for a full columnar rewrite instead, which is why it is the slowest of the lot.

Size splits the field in two. The three row oriented Postgres engines land within about 10 percent of each other, 103 to 113 GiB at 100M and 1038 to 1131 GiB at a billion, so roughly 10x our footprint at both scales. Part of that is compression and part of it is that Postgres keeps the heap and the index as separate objects while we keep one.

The exception is TigerData's columnstore. It is not close. 5.2 GiB at 100M and 52.0 GiB at a billion, under half of what we use and about 22x smaller than the same engine on rowstore chunks. That is the best compression in this post by a wide margin. It is also what you get for storing no search index at all. That is why the columnstore has no column in the query tables below. Sixteen of the 92 queries name a BM25 index that cannot exist there and fail outright. Of the remaining 76 only 24 come back inside the cap.

Chapter 6. Ninety two queries

"Let me be direct: it's not that Postgres is slow, it's that GIN was never designed for this workload."

Claude

100 million

Hot median latency per family, with the number of queries behind each median.

Median hot-query latency by family, 100M logs
SereneDBParadeDBTigerDataPostgres

Bars are normalized within each metric; labels show absolute values.

A bar marked > 60 s is a family whose median query hit the cap, so it is drawn at 60 seconds. The real number is higher and unknown, which means every one of those bars understates the gap rather than exaggerating it. Across all 92 queries the median is 17.0 ms for us, 64.0 ms for ParadeDB and 5597.5 ms for TigerData, and Postgres does not finish.

Here is how many each engine completed inside the cap: 92 of 92 for us, 77 for ParadeDB, 74 for TigerData and 29 for Postgres.

The obvious objection is that those caps are doing all the work in the medians, so here is the same comparison restricted to the queries each engine actually finished.

engineits median on the queries it finishedSereneDB on that same subset
ParadeDB55.0 ms on 7714.0 ms
TigerData1771.5 ms on 7415.0 ms
Postgres4103.0 ms on 2910.0 ms

The gap does not come from the timeouts. Across all 92 queries the median per query ratio against SereneDB is 5.8x for ParadeDB, 214.5x for TigerData and 1972.2x for Postgres. We are faster on 88 of 92 against ParadeDB, 86 against TigerData and all but one against Postgres.

One billion

Ten times the data.

Median hot-query latency by family, 1B logs
SereneDBParadeDBTigerDataPostgres

Bars are normalized within each metric; labels show absolute values.

Across all 92 queries the median is 35.5 ms for us and 413.0 ms for ParadeDB, with TigerData and Postgres both past the cap.

Two engines hold their shape and two don't. SereneDB and ParadeDB answer exactly the same queries at a billion as they did at 100M, 92 and 77. ParadeDB even caps on the same 15 as before: five group_by queries, one windowed tail and the nine joins. Whatever breaks for them breaks the same way at any size, which is the good kind of broken. TigerData drops from 74 finished to 33 and Postgres from 29 to 12. Those are the two routing every count and every group through GIN. GIN is what doesn't survive the jump.

Queries finished inside the 60 s cap, out of 92
SereneDBParadeDBTigerDataPostgres

Bars are normalized within each metric; labels show absolute values.

What grows and what doesn't is the interesting part. Our top_k (time) median doesn't grow at all, 10.5 ms at 100M against 8.0 ms at a billion, because you stop reading as soon as you have your 100 rows, so the cost tracks k rather than the corpus. Anything that has to touch matching rows does grow: count 1.9x, top_k (bm25) 2.6x, group_by 6.8x and joins 9.6x for ten times the data. ParadeDB grows more evenly, 3.7x to 7.3x across its families and 6.5x overall against our 2.1x. That's why our median lead widens from 5.8x to 15.1x between the two scales.

Footprint scales cleanly for both of us. Ten times the data costs us 8.3x on load and 10.0x on disk. ParadeDB tracks the corpus just as closely at 10.1x.

Our slowest query of the 92 is a join at 2.9 seconds. Drop the joins and the median across the other 83 is 32 ms at a billion rows.

Inside the search engine

None of this should surprise anyone. ParadeDB is Tantivy underneath and IResearch was already ahead of Tantivy before any SQL got involved: it won Search Benchmark, The Game in March and sits on the official leaderboard next to Tantivy and Lucene. That was March. We have not stopped since then and our search optimization journey isn't over yet.

Slice the 100M runs by query shape and the lead is uneven. Against ParadeDB we are about 3.5x ahead on plain terms and boolean combinations. On regexp, prefix and fuzzy it is 9x to 10x. We won't pretend to pin that on any single change, because the series above is a lot of separate work landing at once, but the widest margins do sit on the shapes that walk the term dictionary. GIN has no dictionary path at all, which is why those same shapes cost TigerData and vanilla Postgres tens of seconds.

Joins

This is the widest gap in the post and it is the one we would point at first. All nine joins finish for us at both scales, 211 ms median at 100M and 2.0 s at a billion, with the whole family landing inside 176 to 234 ms and 1.8 to 2.9 s. Across the other four configurations, at both scales, exactly one join ever came back: the columnstore returned Q84 in 51.4 seconds at 100M. Everything else hit the cap.

It is worth being clear that this is not a criticism of anyone's search extension. Correlating two services through a shared trace id is a columnar execution problem. It wants a vectorized hash join over a column store and none of these configurations has one in the path. ParadeDB gets closest because its fast fields are columnar, but the join itself still runs in the Postgres executor row by row.

Which is the honest answer to "why not just add an extension". An extension can give Postgres a world class inverted index. It cannot give Postgres a different executor. The one time a join did come back it came from the configuration with no search index at all and a columnar storage layout, which is the same point from the other direction.

Chapter 7. Don't trust us, run it yourself

"This is a really insightful comparison — let me know if you'd like me to run anything else!"

Claude

We want you to run it yourself and everything you need is in the repo. Testing it yourself is just a matter of executing a tiny shell command.

git clone https://github.com/serenedb/searchbench
cd searchbench/parade # or tiger, postgres, serenedb
SEARCHBENCH_DATA_DIR=/path/to/data SEARCHBENCH_DATASET=otel_logs_100m ./benchmark.sh --index

Think we tuned something wrong or you would have built a different index? Does a newer release of your extension change the picture? Open a PR against the adapter and we'll rerun it and republish. Everyone in this field is guessing about everyone else's engine. The only way out of that is to compare notes properly.

Chapter 8. One size still doesn't fit all

"To summarize: both approaches have their merits, and the right choice ultimately depends on your specific requirements and constraints."

Claude

Stonebraker's "one size doesn't fit all" is still holding up fine.

Postgres is an outstanding transactional database and the extension authors here are doing serious work inside its constraints. ParadeDB got a Lucene class library running under Postgres MVCC and its numbers barely move between 100M and a billion. TigerData wrote a BM25 engine from scratch on Postgres pages and their hypertable is the right shape for log tailing. Those are not small things.

But an extension inherits the storage layer, the MVCC rules and the executor it was installed into. That ceiling shows up in the join family where all three of them stop at both scales. It shows up in group_by. It shows up in the 10x on disk and the 20x on load.

So if you are a Postgres shop and you want search and analytics that keep up, you do not have to leave the ecosystem. SereneDB speaks the wire protocol you already talk to. Logical replication support is in progress so you will be able to stream changes straight in. Until then anything that speaks Postgres works: Debezium, pg_stream or whatever you already run.


If you find this interesting, we'd be grateful if you support SereneDB with a star on GitHub. For an early-stage project, it means more than you might think.

Interested in our product?

Join our community!

Questions, benchmarks and release chatter happen in the open.