# Benchmark results | | | |---|---| | PostgreSQL | PostgreSQL 18.4 (Homebrew) on aarch64-apple-darwin24.6.0, compiled by Apple clang version 17.0.0 (clang-1700.6.4.2), 64-bit | | ICU | 78.3 | | CPU | Apple M4 Pro | | Corpus | Wikipedia article titles, dump 20260901, 500000 titles per language | | Runs | median of 5 after one warm-up run, one backend, no parallel workers, JIT off | | LC_CTYPE | C.UTF-8 | ## Tokenizing Time to tokenize every title once, minus the time to scan the table. "vs default" divides by the default parser's time. | Language | Titles | Avg bytes | Method | µs per title | vs default | |---|--:|--:|---|--:|--:| | en | 500,000 | 21.0 | default parser | 0.748 | 1.00× | | en | 500,000 | 21.0 | intl_simple | 0.700 | 0.94× | | en | 500,000 | 21.0 | intl_search | 1.000 | 1.34× | | en | 500,000 | 21.0 | intl_search_ngram | 1.081 | 1.45× | | en | 500,000 | 21.0 | pg_trgm | 1.218 | 1.63× | | en | 500,000 | 21.0 | pg_bigm | 0.934 | 1.25× | | zh | 500,000 | 16.9 | default parser | 0.455 | 1.00× | | zh | 500,000 | 16.9 | intl_simple | 1.587 | 3.49× | | zh | 500,000 | 16.9 | intl_search | 2.109 | 4.64× | | zh | 500,000 | 16.9 | intl_search_ngram | 2.325 | 5.11× | | zh | 500,000 | 16.9 | pg_trgm | 0.773 | 1.70× | | zh | 500,000 | 16.9 | pg_bigm | 0.485 | 1.07× | | ja | 500,000 | 22.2 | default parser | 0.602 | 1.00× | | ja | 500,000 | 22.2 | intl_simple | 1.888 | 3.14× | | ja | 500,000 | 22.2 | intl_search | 2.367 | 3.93× | | ja | 500,000 | 22.2 | intl_search_ngram | 2.661 | 4.42× | | ja | 500,000 | 22.2 | pg_trgm | 1.020 | 1.70× | | ja | 500,000 | 22.2 | pg_bigm | 0.660 | 1.10× | | ko | 500,000 | 19.1 | default parser | 0.555 | 1.00× | | ko | 500,000 | 19.1 | intl_simple | 0.745 | 1.34× | | ko | 500,000 | 19.1 | intl_search | 1.056 | 1.91× | | ko | 500,000 | 19.1 | intl_search_ngram | 1.412 | 2.55× | | ko | 500,000 | 19.1 | pg_trgm | 0.898 | 1.62× | | ko | 500,000 | 19.1 | pg_bigm | 0.632 | 1.14× | | th | 366,332 | 45.9 | default parser | 1.094 | 1.00× | | th | 366,332 | 45.9 | intl_simple | 1.831 | 1.67× | | th | 366,332 | 45.9 | intl_search | 2.498 | 2.28× | | th | 366,332 | 45.9 | intl_search_ngram | 3.434 | 3.14× | | th | 366,332 | 45.9 | pg_trgm | 2.235 | 2.04× | | th | 366,332 | 45.9 | pg_bigm | 1.576 | 1.44× | ## GIN index build Single-process `CREATE INDEX` with `maintenance_work_mem = 1GB`. | Language | Method | Build seconds | Index size MB | |---|---|--:|--:| | en | default parser | 1.4 | 25.5 | | en | intl_simple | 1.3 | 23.7 | | en | intl_search | 1.5 | 23.3 | | en | intl_search_ngram | 1.5 | 23.4 | | en | pg_trgm | 1.7 | 29.5 | | en | pg_bigm | 1.7 | 20.4 | | zh | default parser | 1.3 | 38.2 | | zh | intl_simple | 1.6 | 14.0 | | zh | intl_search | 1.8 | 13.3 | | zh | intl_search_ngram | 2.2 | 17.1 | | zh | pg_trgm | 2.7 | 72.0 | | zh | pg_bigm | 2.5 | 41.8 | | ja | default parser | 1.4 | 39.6 | | ja | intl_simple | 1.7 | 18.4 | | ja | intl_search | 2.0 | 18.2 | | ja | intl_search_ngram | 3.0 | 20.7 | | ja | pg_trgm | 2.9 | 63.7 | | ja | pg_bigm | 2.5 | 33.9 | | ko | default parser | 1.4 | 30.1 | | ko | intl_simple | 1.6 | 28.6 | | ko | intl_search | 1.7 | 28.7 | | ko | intl_search_ngram | 2.6 | 24.5 | | ko | pg_trgm | 2.3 | 48.9 | | ko | pg_bigm | 2.0 | 24.3 | | th | default parser | 1.5 | 37.6 | | th | intl_simple | 1.5 | 13.7 | | th | intl_search | 2.2 | 13.6 | | th | intl_search_ngram | 5.8 | 45.5 | | th | pg_trgm | 1.6 | 23.4 | | th | pg_bigm | 1.6 | 18.8 | ## Word queries Each language runs its query terms one at a time as `count(*)`, through each method's index. The tsvector methods use `plainto_tsquery`, and the n-gram methods use `LIKE '%term%'`. Recall and overmatch come from the labeled sample below: recall is the share of real matches the method returned, and overmatch is the share of the method's returned titles that aren't real matches. | Language | Terms | Method | Queries using the index | Avg ms per query | Hits | Recall | Overmatch | |---|--:|---|--:|--:|--:|--:|--:| | en | 200 | default parser | 200 | 0.234 | 123,406 | — | — | | en | 200 | intl_simple | 200 | 0.275 | 119,996 | — | — | | en | 200 | intl_search | 200 | 0.266 | 121,265 | — | — | | en | 200 | intl_search_ngram | 200 | 0.272 | 121,265 | — | — | | en | 200 | pg_trgm | 182 | 3.060 | 708,250 | — | — | | en | 200 | pg_bigm | 200 | 2.063 | 708,250 | — | — | | zh | 200 | default parser | 200 | 0.037 | 5,396 | 10.3% | 0.0% | | zh | 200 | intl_simple | 200 | 0.164 | 68,070 | 92.6% | 15.6% | | zh | 200 | intl_search | 200 | 0.185 | 97,880 | 92.6% | 15.6% | | zh | 200 | intl_search_ngram | 200 | 0.336 | 111,835 | 99.0% | 17.6% | | zh | 200 | pg_trgm | 22 | 23.104 | 79,536 | 100.0% | 18.4% | | zh | 200 | pg_bigm | 200 | 0.111 | 79,536 | 100.0% | 18.4% | | ja | 200 | default parser | 200 | 0.036 | 7,373 | 7.0% | 0.0% | | ja | 200 | intl_simple | 200 | 0.132 | 86,443 | 86.1% | 19.5% | | ja | 200 | intl_search | 200 | 0.140 | 88,779 | 86.1% | 19.5% | | ja | 200 | intl_search_ngram | 200 | 0.530 | 184,736 | 100.0% | 53.6% | | ja | 200 | pg_trgm | 57 | 19.852 | 175,927 | 100.0% | 54.0% | | ja | 200 | pg_bigm | 200 | 0.285 | 175,927 | 100.0% | 54.0% | | ko | 200 | default parser | 200 | 0.070 | 35,053 | 46.7% | 0.0% | | ko | 200 | intl_simple | 200 | 0.099 | 36,678 | 48.2% | 1.1% | | ko | 200 | intl_search | 200 | 0.082 | 36,678 | 48.2% | 1.1% | | ko | 200 | intl_search_ngram | 200 | 0.398 | 93,757 | 100.0% | 22.0% | | ko | 200 | pg_trgm | 91 | 17.704 | 93,655 | 100.0% | 22.0% | | ko | 200 | pg_bigm | 200 | 0.139 | 93,655 | 100.0% | 22.0% | | th | 200 | default parser | 200 | 0.037 | 6,997 | 5.3% | 0.0% | | th | 200 | intl_simple | 200 | 0.294 | 215,162 | 78.9% | 43.8% | | th | 200 | intl_search | 200 | 0.292 | 215,162 | 78.9% | 43.8% | | th | 200 | intl_search_ngram | 200 | 0.586 | 447,620 | 98.2% | 69.4% | | th | 200 | pg_trgm | 133 | 17.208 | 620,044 | 100.0% | 77.2% | | th | 200 | pg_bigm | 200 | 1.089 | 620,044 | 100.0% | 77.2% | ## Labeled sample For each language, `bench/pairs.sql` draws term/title pairs at random from every title in the sample that contains one of the query terms as a substring, and `bench/labels.csv` marks each pair `match` or `over`. A pair is a `match` when the title uses the term as a word: on its own, with grammatical endings (Korean particles, Japanese inflection), or as part of a compound whose meaning includes it (`機場` in `國際機場`). It is `over` when the characters are there but the word isn't: inside a transliterated name (`阿拉` in `阿拉巴马州`), across a word boundary (`京都` in `東京都`), or inside an unrelated word. Every method is scored on the same pairs, so a method that returns every substring scores 100% recall and pays in overmatch. | Language | Labeled pairs | Real matches | |---|--:|--:| | zh | 250 | 81.6% | | ja | 250 | 46.0% | | ko | 250 | 78.0% | | th | 250 | 22.8% |