
Andrey Abramov
Sep 7, 2026 · 31 minutes read
SereneDB vs ParadeDB vs TigerData vs Postgres
92 search and analytics queries across the whole Postgres family, at 100M and 1B logs
If you asked Claude about Postgres, you would probably get something like this:
"Postgres is an awesome database — and for once, 'awesome' is a measured claim rather than just an adjective!
The POSTGRES project began at UC Berkeley in 1986 under Michael Stonebraker, and the codebase now carries nearly 40 years of active development, 725+ contributors, and roughly 1.78 million lines of C. In the 2025 Stack Overflow survey, 55.6% of all developers reported using it — the most-used, most-admired, and most-wanted database for the third year running, and about 15 points ahead of MySQL. On DB-Engines it ranks #4 of 438 systems tracked: the top open-source database, the only one in the top four still gaining, and the fastest-growing database of H1 2026. The official Docker image has passed a billion pulls. The ecosystem now counts 1,000+ extensions and 40+ forks and derived databases, and in 2025 alone the two biggest data platforms paid up for a piece of it: Databricks bought Neon for about $1B, and Snowflake bought Crunchy Data for around $250M."
Claude
For once we have no notes. All of it is true. It is also why we made SereneDB Postgres compatible from day one: to join that ecosystem and to bring the fastest search and analytics into it.
Which raises 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. At a billion OpenTelemetry logs, 92 queries, 60 second cap, hot median latency:
| engine | median latency | finished |
|---|---|---|
| SereneDB | 35.5 ms | 92 of 92 |
| ParadeDB | 413.0 ms | 77 of 92 |
| TigerData | past the cap | 33 of 92 |
| Postgres 18 | past the cap | 12 of 92 |
- 1B logs
- 100M logs
Full results are at playground.serenedb.com/searchbench and the benchmark itself lives at github.com/serenedb/searchbench.
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 answers one question, which rows contain your words. Scoring, sorting and trimming down to the top 100 all happen afterwards, outside the index. 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. It also supports Block-Max WAND on the top-k path and parallel index builds.
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
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 does two jobs.
TigerData
TigerData comes at it from the time series side and gives you two ways to store a data 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. For example, if you ask for a six hour window, the planner works out which chunks cover those six hours and never opens the others. That is where most of TigerData's wins in this post 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% compression. Add continuous aggregates on top and you have a real analytics engine for time ordered data.
Columnstore
You can also declare the chunks columnar from the very beginning, which is where those compression numbers are supposed to come from and where scans over a couple of columns get cheap. So we tested that too, in a second adapter. Unfortunately it has a serious downside, namely 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.
We tried building one anyway, but CREATE INDEX does not even fail. 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. It was fast, but completely wrong, so the adapter builds no index at all and every query that resolves the BM25 index by name is recorded as a failure.
All three of these look very attractive. ParadeDB gets aggregation for free out of an index it already built for search. TigerData gets compression plus the time pruning above, which is pretty much what a log workload asks for. Columnstore takes that trade all the way and hands over the text index for a much smaller footprint. Every one of them sounds good until you run it, so we did.
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. Concurrency, ingest, updates, distribution and memory are all fair questions we don't answer.
And of course we do our best to give every engine the strongest configuration we can build for it. We will not always have managed it, which is why every adapter sits in the repo where you can check our work.
The OpenTelemetry corpus
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:
Timestamp | when |
TraceId, SpanId, TraceFlags | trace correlation |
SeverityText, SeverityNumber | log level, as text and as a number |
ServiceName | which service emitted it |
Body | the message itself and the only full-text field |
ResourceSchemaUrl, ResourceAttributes | what produced the log |
ScopeSchemaUrl, ScopeName, ScopeVersion, ScopeAttributes | which instrumentation scope |
LogAttributes | per-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 92 queries
92 of them, in five families:
| family | what it is |
|---|---|
count | how many logs match |
top_k (bm25) | top 100 ordered by relevance, BM25 scored |
group_by | matches bucketed by service or by time |
top_k (time) | top 100 ordered by timestamp, newest first, which is log tailing |
join | correlate 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. Then we cross that again with how common the word is, because searching for something that shows up in 40% of your logs is a completely different job from finding one that shows up in 0.001%.
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.
| engine | version | image |
|---|---|---|
| SereneDB | 26.09.1 | serenedb/serenedb:26.09.1 |
| ParadeDB | pg_search 0.25.0 on Postgres 18 | paradedb/paradedb:0.25.0-pg18 |
| TigerData | pg_textsearch 1.4.0 on Postgres 18.4 with TimescaleDB 2.29.2 | timescale/timescaledb-ha:pg18.4-ts2.29.2-all |
| Postgres | 18.4 with fuzzystrmatch | postgres: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_gatherships 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_bufferslands at about a quarter of the box andeffective_cache_sizeis a planner hint at roughly three quarters.work_memis 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_workersthat 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_sizemeans fewer checkpoint stalls during a bulk ingest. - Container. Docker gives a container 64 MB of
/dev/shm. A parallel hash table here is bounded bywork_memtimes 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:
| setting | value |
|---|---|
shared_buffers | 32 GB |
work_mem | 374 MB |
effective_cache_size | 94 GB |
max_parallel_workers, _per_gather, max_worker_processes | 16 |
max_parallel_maintenance_workers | 16 |
maintenance_work_mem | 6 GB |
io_workers | 16 |
max_wal_size | 8 GB |
/dev/shm | 24 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
endictionary iskeywordwithfrequency,normandpositionon. 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 samek1andbeveryone 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:
bodyfull text,service_nameas a literal,severity_numberas numeric andtimestampas a datetime. That is three more than SereneDB indexes, where onlyBodygets 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
fastfields with no postings.severity_textandscope_nameareGROUP BYkeys. Without the fast field they lose the Aggregate Scan push-down and run 8 to 10x slower.trace_idis insurance on the join column. bodyuses thepdb.simpletokenizer, which does the same lowercase and split as SereneDB. We verified this on a live instance becauseparadedb.schema()reportspdb.simpleunder the namedefault.
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 kand 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 GINtsvectorindex 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
LIMITqueries 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% 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 = truedeclares the chunks columnar from the very beginning 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 theb.service_nameside of all nine joins, so segmenting on it lets those filters skip whole batches.tsdb.orderby = 'timestamp DESC'because that is the sort of everyrecentquery. It also puts a minmax sparse index ontimestamp, which is what lets theBETWEENwindows prune.- Load then convert, with
compress_chunk()overshow_chunks()after ingest, soload_timecovers 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.
simplerather thanenglish, so there is no stemming and no stopword removal. That matches the raw token model every other engine uses.fuzzystrmatchis installed forlevenshtein(), which is the only fuzzy matching Postgres has.ts_rank_cdstands in for BM25 on the top-k family, because Postgres has nothing closer.
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.
| shape | SereneDB | ParadeDB | TigerData | Postgres |
|---|---|---|---|---|
| minimum should match | ts_any([...], 2) | six pairwise ORs by hand | six way OR inside to_tsquery | six way OR inside to_tsquery |
| prefix | ts_starts_with | paradedb.regex() | conn:* on GIN | conn:* on GIN |
| prefix anchored regexp | ts_regexp | paradedb.regex() | rewritten to charg:* | rewritten to charg:* |
| infix wildcard, mid word regexp | ts_like, ts_regexp | paradedb.regex() | body ~* seq scan | body ~* seq scan |
| fuzzy | ts_levenshtein | paradedb.fuzzy_term() | levenshtein() seq scan | levenshtein() seq scan |
| relevance score | BM25() | pdb.score() | BM25 on 16 top-k, ts_rank_cd on 5 | ts_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.
On TigerData we did try to make the fuzzy scan cheaper. Running levenshtein() against every word of every row is brutal, so we put a cheap substring test in front of it. Looking for connection within one typo? Cut the word in half and any real match still has to contain conne or ction spelled correctly, because one typo can only break one of the two halves. Two typos means cutting it into three parts, and so on. So the query first throws away every row that contains none of those pieces and only then runs the expensive comparison on what is left. It cannot miss a match, it just stops us scanning the whole table for nothing.
And ts_rank_cd is not BM25. It ranks by a different formula, so the rows Postgres hands back are not the rows the others hand back. We are timing the work here, not diffing the results.
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 time and index size
"Perfect! The load completed successfully."
Claude
Bars are normalized within each metric; labels show absolute values.
100M logs
1B logs
Load is the one place nobody is close. The next fastest after SereneDB is ParadeDB at 20x its 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% of each other, 103 to 113 GiB at 100M and 1038 to 1131 GiB at a billion, so roughly 10x SereneDB's 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 at 100M and 1B
"Let me be direct: it's not that Postgres is slow, it's that GIN was never designed for this workload."
Claude
100 million logs
Hot median latency per family, with the number of queries behind each median.
Bars are normalized within each metric; labels show absolute values.
Count, rank and group
Tail and correlate
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 SereneDB, 64.0 ms for ParadeDB and 5597.5 ms for TigerData. Postgres does not finish.
Completions inside the cap: 92 of 92 for SereneDB, 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 the table below restricts the comparison to the queries each engine actually finished.
| engine | its median on the queries it finished | SereneDB on that same subset |
|---|---|---|
| ParadeDB | 55.0 ms on 77 | 14.0 ms |
| TigerData | 1771.5 ms on 74 | 15.0 ms |
| Postgres | 4103.0 ms on 29 | 10.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 logs
Ten times the data.
Bars are normalized within each metric; labels show absolute values.
Count, rank and group
Tail and correlate
Across all 92 queries the median is 35.5 ms for SereneDB 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.
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 SereneDB's 2.1x. That's why the 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 SereneDB 8.3x on load and 10.0x on disk. ParadeDB tracks the corpus just as closely at 10.1x.
SereneDB's 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
Some of this we expected. 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. We have not stopped since then and our search optimization journey isn't over yet.
If you slice the 100M runs by query shape, the lead is uneven. About 3.5x ahead of ParadeDB on plain terms and boolean combinations, 9x to 10x on regexp, prefix and fuzzy. Those three are what a term dictionary is for: walk the sorted list of words in the index, pull out the few that match and only then go near the rows. GIN has nothing like it, so the 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 SereneDB 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 SearchBench 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.
Other SearchBench write-ups:
- SereneDB vs ClickHouse at ten billion logs
- SereneDB vs Elasticsearch vs OpenSearch vs CrateDB
- SereneDB vs ArangoDB on search performance
- The C++ search engine that won the game
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.