Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 

README.md

scout-postgres benchmarks

Reproducible latency benchmark of scout-postgres against Laravel Scout's default database driver, on a single Postgres 18 service with hot cache.

The methodology, the artisan command, and the raw numbers are all here so that anyone can rerun the benchmark on their own hardware and corpus and verify (or contradict) the package's performance claims.

Methodology

  • Corpus: 500,150 rows in a books table.
    • 500,000 generated by BookFactory (faker sentence + paragraph).
    • 100 deterministic "Modern World History Encyclopedia" rows.
    • 50 deterministic "A Comprehensive History of Modern Philosophical Thought" rows.
    • Indexed columns + weights: title:A, subtitle:B, author:B, summary:C (the summary column carries multi-paragraph text and is the dominant cost driver).
  • Query set: seven representative shapes — common single token, two words with no literal collocation, rare phrase, short prefix, typo, no-match, and a long natural-language query.
  • Execution: for each driver and each query the harness runs a 3-call warm-up (discarded), then 30 measured calls; per-call wall time captured via hrtime(true). Reported numbers are p50 and p95 of the 30 measured samples, in milliseconds.
  • Hardware: local Fedora workstation, single Postgres 18.3 service, PHP 8.5 CLI, Laravel 13.0. Cache is hot — first 3 calls warm shared_buffers and the GIN indexes.

Running it yourself

The harness lives in a sibling Laravel project — see scout-postgres-benchmark. The artisan command source is also vendored under src/ here for reference.

Quick reproduction:

git clone https://github.com/jonaspauleta/scout-postgres-benchmark.git
cd scout-postgres-benchmark
composer install
cp .env.example .env && php artisan key:generate
# Configure DB_* in .env to point at a local Postgres 14+

php artisan migrate
php artisan bench:scout --seed=500000 --runs=30 --warmup=3

Results

Run on Postgres 18.3, PHP 8.5, Laravel 13, 500,150 rows, hot cache.

Default config (500,150 rows)

Run with the shipped defaults: query_strategy=adaptive, prefix_fast_path=true, disable_jit=true, total_count=false, trigram_function=similarity, trigram_threshold=0.3. Schema migrated with the search_text cap (1000 chars).

query pgsql p50 (ms) database p50 (ms) hits (pg / db) notes
world 4.0 2.4 20 / 20 adaptive returns FTS-only on common token
modern history 5.4 2100.1 20 / 0 FTS bridges token gaps; database seq-scans 500k
philosophical exposition 5.3 2.5 20 / 20 total_count=false removes the window-aggregate
phil 4.4 2.4 20 / 20 short-prefix fast path skips trigram entirely
philosphy 45.0 1988.7 0 / 0 adaptive fallback to hybrid recovers via trigram
qwxzqwxzqwxz 8.0 2067.6 0 / 0 GIN miss is instant; LIKE seq-scans 500k
a comprehensive history of modern philosophical thought 8.8 3.1 20 / 20 FTS handles long queries

The 10× corpus growth versus the previous 50k run shows where the engine holds latency bounded and where it doesn't:

  • FTS-bound queries (world, modern history, philosophical exposition, phil, long_query) all stay sub-10 ms. GIN scans are O(matches), not O(rows).
  • The adaptive trigram fallback (philosphy) is the one path that scales linearly with the corpus — 13 ms at 50k, 45 ms at 500k. Still under any reasonable latency budget, but the dominant cost on typo queries.
  • qwxzqwxzqwxz stays cheap because the trigram bitmap is empty — the GIN index returns no candidates regardless of corpus size.

Default config (50,150 rows) — for corpus-size scaling

Same configuration, smaller corpus, same hardware:

query pgsql p50 (ms) database p50 (ms) hits (pg / db)
world 4.0 2.3 20 / 20
modern history 4.9 190.5 20 / 0
philosophical exposition 4.6 2.8 20 / 20
phil 4.0 2.7 20 / 20
philosphy 13.1 186.3 0 / 0
qwxzqwxzqwxz 7.9 191.5 0 / 0
long natural query 7.1 2.9 20 / 20

Findings

  1. scout-postgres has measurably better recall. Multi-token queries, prefix matches, and accent variants all return matches that the database driver's LIKE %term% strategy misses entirely.
  2. The adaptive strategy + short-prefix fast path are the dominant wins. The fast path skips both websearch_to_tsquery and the trigram bitmap entirely on single short tokens, so phil runs in ~4 ms even on 500k rows. Multi-token FTS queries (e.g. modern history) stay sub-10 ms because adaptive returns the FTS hits without running the trigram pass.
  3. total_count=false removes a major p95 cost. A COUNT(*) OVER() window aggregate forces materialisation of the full match set before LIMIT. With it off, latency scales with page size, not match-set size. Opt back in per-query when an exact total is required.
  4. trigram_threshold is the dominant trigram cost knob. A 2× change in threshold can change p50 by 10–100× on the hybrid fallback path. The default 0.3 is chosen to be safe for typical mixed-length corpora; tune higher for long-text corpora, lower only for short titles where false negatives are the bigger concern.
  5. The database driver wins on small literal-substring queries because LIMIT 20 short-circuits its sequential scan. It loses badly on no_match (full table seq scan) and on multi-token queries (zero recall).

Caveats

  • Benchmark uses faker-generated text. Real corpora have different trigram distributions; numbers will differ.
  • Hot cache only. Cold-cache numbers (first query after a restart) are meaningfully higher and not measured here.
  • Single-node, no replication, no concurrent load. Latency under load is not characterised.
  • Postgres 18 with default settings (no shared_buffers tuning).

The harness is set up so anyone can rerun against their own corpus — that is the only number that ultimately matters.