How to Speed Up Phrase Search with bigram_index
How to Speed Up Phrase Search with bigram_index
Author: Sergey Nikolaev<br>Published: May 02, 2026 - 7 Min read
TL;DR<br>bigram_index<br>can be used for several purposes, and in this article we focus specifically on phrase-search performance: on the 1M-document benchmark below, bigram_index='all' improved QPS by about 2.9x and cut average phrase-query latency by about 3.2x.<br>If your main problem is matching xt850 against xt 850 rather than speeding up phrase search, see<br>How to Make xt850 Match xt 850<br>Phrase search can be expensive. Even when a query is short, the engine still has to verify ordering and adjacency, and that work gets more noticeable when:<br>the individual words are common<br>the dataset is large<br>phrase queries are frequent in your workload<br>That is exactly what<br>bigram_index<br>is for.<br>What bigram indexing actually does<br>Normally, a phrase like "noise cancelling headphones" is handled as separate tokens that also need to appear in the right order and next to each other. Bigram indexing lets Manticore pre-store adjacent token pairs such as:<br>noise cancelling<br>cancelling headphones<br>That gives the engine a faster way to narrow down candidate documents during phrase matching.<br>This article focuses specifically on phrase acceleration.<br>Important caveat: bigrams work at tokenization level<br>This is the part that is easy to miss when you only look at the happy-path speedup story.<br>bigram_index works at the tokenization level only. It does not account for later transformations such as morphology, wordforms, or stopwords, and that can materially change phrase-matching expectations.<br>The practical conclusion is simple: bigrams can be excellent for phrase speed, but if your index relies heavily on morphology, wordforms, or stopwords, test the actual phrase behavior you care about before rolling the setting out broadly.<br>Mode 1: Default behavior<br>This is the baseline. No explicit bigram indexing is enabled, so no bigram posting lists are stored.<br>Use it when:<br>phrase search is rare<br>documents are short<br>you want the leanest indexing path<br>Example<br>DROP TABLE IF EXISTS bi_none_demo;
CREATE TABLE bi_none_demo(title text);
INSERT INTO bi_none_demo VALUES<br>(1,'wireless noise cancelling headphones'),<br>(2,'noise cancelling microphone'),<br>(3,'wireless gaming headset');
SELECT id, title FROM bi_none_demo WHERE MATCH('"noise cancelling"');
This is the baseline behavior. The query matches the expected rows, but Manticore has no precomputed bigram posting lists to help resolve the phrase more efficiently.<br>Mode 2: all<br>bigram_index = all
This is the most aggressive phrase-acceleration mode. Every adjacent token pair gets indexed as a bigram.<br>Use it when:<br>exact phrase search is a core feature<br>phrase queries often include common words and produce many candidates<br>you want the strongest phrase acceleration<br>you do not want to tune a frequent-word list<br>Example<br>DROP TABLE IF EXISTS bi_all_demo;
CREATE TABLE bi_all_demo(title text)<br>bigram_index='all';
INSERT INTO bi_all_demo VALUES<br>(1,'lord of the rings trilogy'),<br>(2,'house of the dragon season 2'),<br>(3,'made for iphone charger');
SELECT id, title FROM bi_all_demo WHERE MATCH('"house of the dragon"');<br>SELECT id, title FROM bi_all_demo WHERE MATCH('"made for iphone"');
The important point here is not different matches, but different indexing strategy: all stores every adjacent pair, so phrase queries have the maximum amount of bigram help available at search time.<br>The reason to choose all is when phrase search becomes more expensive because many documents match the individual words, and Manticore then has to do more positional verification to confirm the exact phrase. all helps by narrowing candidates earlier.<br>Mode 3: first_freq<br>bigram_index = first_freq<br>bigram_freq_words = for, of, the, with
This mode stores a pair only when the first token is in your frequent-word list.<br>Use it when:<br>phrase search matters<br>you want a lighter alternative to all<br>many phrases in your data contain words that are genuinely frequent in your own corpus<br>With the list above:<br>for iphone is eligible<br>of the is eligible<br>the dragon is eligible<br>made for is not eligible<br>lord of is not eligible<br>For production use, do not pick bigram_freq_words from memory. Derive it from your own data. A practical way is to dump dictionary stats with<br>indextool<br>using --dumpdict ... --stats, review the most frequent tokens, and then build a small bigram_freq_words list from those results.<br>Example<br>DROP TABLE IF EXISTS bi_first_freq_demo;
CREATE TABLE bi_first_freq_demo(title text)<br>bigram_index='first_freq'<br>bigram_freq_words='for,of,the,with';
INSERT INTO bi_first_freq_demo VALUES<br>(1,'made for iphone charger'),<br>(2,'lord of the rings trilogy'),<br>(3,'house of the dragon season 2');
SELECT id, title FROM bi_first_freq_demo WHERE MATCH('"made for iphone"');<br>SELECT id, title FROM bi_first_freq_demo WHERE MATCH('"lord of the"');
The queries still return the expected rows. What changes is which pairs get...