<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://serenedb.com/blog</id>
    <title>SereneDB Blog Blog</title>
    <updated>2026-09-03T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://serenedb.com/blog"/>
    <subtitle>SereneDB Blog Blog</subtitle>
    <icon>https://serenedb.com/blog/img/favicon.ico</icon>
    <entry>
        <title type="html"><![CDATA[SereneDB meets LangChain: A Vector Store for RAG Pipelines]]></title>
        <id>https://serenedb.com/blog/langchain-serenedb-integration</id>
        <link href="https://serenedb.com/blog/langchain-serenedb-integration"/>
        <updated>2026-09-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[SereneDB has built a VectorStore integration package for LangChain, pairing flexible AI orchestration with the analytical power of a full-featured database. ]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="none" viewBox="0 0 28 28" class="logoAvatar_BINX"><path fill="#895AF8" d="M28 14c0-7.732-6.268-14-14-14S0 6.268 0 14s6.268 14 14 14 14-6.268 14-14"></path><path fill="#80BEFF" d="M9.345 18.655c4.556 4.556 11.416 5.084 15.32 1.178 3.906-3.905 3.379-10.764-1.178-15.32C18.931-.043 12.072-.571 8.167 3.334S4.789 14.1 9.345 18.654"></path><path fill="#fff" d="M15.649 12.35c2.278 2.278 5.676 2.574 7.59.66s1.617-5.311-.66-7.59c-2.279-2.277-5.677-2.573-7.59-.66-1.914 1.914-1.618 5.312.66 7.59"></path></svg></div></div><div class="copy_iHNG"><p class="names_ox2I">SereneDB Team</p><p class="meta_OgzX">Sep 3, 2026 · 17 minutes read</p></div></header><h2 class="title_RQ9v">SereneDB meets LangChain: A Vector Store for RAG Pipelines</h2><p style="font-size:0.9rem;margin-top:-0.4rem">SereneDB is now available as a VectorStore for LangChain applications</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg hideFirstHeading_VVMX" data-image-lightbox-root="true"><p>SereneDB has built a VectorStore integration package for LangChain, pairing flexible AI orchestration with the analytical power of a full-featured database. Three wins for your RAG pipeline:</p>
<ul>
<li><strong>Unlimited analytical power</strong> — native hybrid search, metadata filtering, and relational queries let your agents retrieve on any shape and complexity of data, far beyond simple vector matching.</li>
<li><strong>Single engine</strong>  - SereneDB is a full database, not just a vector store. All the surrounding data can stay in the same instance. No additional engines to maintain.</li>
<li><strong>Rapid development</strong> — the package handles all the store specifics, so you can focus on the actual application.</li>
</ul>
<h1>RAG in the modern world</h1>
<p>Large language models are remarkable generalists with two well-known blind spots: their
knowledge stops at a training cutoff, and they know nothing about <em>your</em> data — internal
docs, tickets, codebases, product catalogs. <strong>Retrieval-Augmented Generation (RAG)</strong> is
the pattern that closes both gaps. Instead of hoping the model memorized the answer, a
RAG application first <em>retrieves</em> the most relevant pieces of your own data and hands
them to the model as context, so the answer is grounded in real, current, private
sources — with far fewer hallucinations and even citations to back it up.</p>
<p>In a few short years RAG has gone from a research idea to the default architecture for
applied LLM systems: chat-with-your-docs assistants, support bots, enterprise search,
code assistants — nearly every "LLM + company data" product is a RAG pipeline at its
core. The mechanics have matured along the way. Documents are split into chunks and
embedded into vectors, similarity search finds candidates at query time, and modern
pipelines layer on hybrid keyword + semantic retrieval, metadata filtering, and
re-ranking to keep quality high at scale. Which means every RAG system stands on two
practical pillars: a framework to orchestrate the pipeline, and a database that can
store and search vectors efficiently.</p>
<p>Today we will talk about one of the most powerful RAG frameworks — LangChain — and how
SereneDB slots into it as the vector store.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="a-little-about-langchain">A little about LangChain<a href="https://serenedb.com/blog/langchain-serenedb-integration#a-little-about-langchain" class="hash-link" aria-label="Direct link to A little about LangChain" title="Direct link to A little about LangChain">​</a></h2>
<p><a href="https://www.langchain.com/" target="_blank" rel="noopener noreferrer">LangChain</a> is an open-source framework for building
applications on top of large language models. Instead of wiring raw API calls together
by hand, developers compose applications from standard building blocks: a model call, a
prompt template, a retriever, a memory layer. The framework's real value is in its
abstractions — every provider-specific service (an LLM, an embedding API, a database) is
hidden behind a common interface, so an application written against LangChain can swap
OpenAI for Anthropic, or one database for another, without rewriting the surrounding
Logic.</p>
<p>For making a RAG pipeline with LangChain one usually uses the  following blocks: <em>loader → splitter → embeddings → vector store</em> at ingestion time, and <em>query → embeddings → vector store (as retriever) → chat model</em> at answer time.  And here is where SereneDB appears.</p>
<h1>SereneDB in the pipeline: the vector store</h1>
<p><a href="https://github.com/serenedb/langchain-serenedb" target="_blank" rel="noopener noreferrer"><code>langchain-serenedb</code></a> plugs
<a href="https://serenedb.com/" target="_blank" rel="noopener noreferrer">SereneDB</a> into that pipeline as the <strong>VectorStore</strong>
implementation. So it fits natively into the LangChain pipeline as shown.</p>
<img src="https://serenedb.com/blog/img/blog/langchain-serenedb-integration/langchain-serenedb-light.png" alt="SereneDB in the pipeline: the vector store" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%;display:block;margin:24px auto 12px"><img src="https://serenedb.com/blog/img/blog/langchain-serenedb-integration/langchain-serenedb-dark.png" alt="SereneDB in the pipeline: the vector store" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%;display:block;margin:24px auto 12px">
<br>
<p>The integration package maps LangChain's vector store contract onto SereneDB's native capabilities: vector columns, distance operators, the IVF ANN index, BM25 full-text search, and JSON metadata.</p>
<p>The key point: <strong>You don't need to know SQL</strong>. Although SereneDB is a full SQL database underneath, the integration handles the entire database side internally — creating tables, declaring indexes with the right options, composing search queries, and translating filters. You work with Python objects and LangChain's standard vector store API; the package generates correct, index-aware SQL behind the scenes. Someone building a RAG application never needs to learn SereneDB's DDL or SQL query syntax to get full use of its engine — they can stay focused on the real deal: their documents, their retrieval quality, their application.</p>
<p>Let’s see what SereneDB brings to your RAG pipeline.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="vectors-first">Vectors first<a href="https://serenedb.com/blog/langchain-serenedb-integration#vectors-first" class="hash-link" aria-label="Direct link to Vectors first" title="Direct link to Vectors first">​</a></h3>
<p>Vector similarity search is the first thing you need when adding VectorStore to the pipeline. The integration stores your embeddings and queries them through SereneDB's IVF (inverted-file) ANN index for fast approximate nearest-neighbor search at scale. You choose the distance that fits your embeddings — cosine, euclidean, inner product, or manhattan — and the package keeps the index and every query aligned on it. Every standard LangChain entry point — <code>similarity_search</code>, <code>similarity_search_with_score</code>, MMR, <code>as_retriever()</code> — is served by that IVF index, so the whole vector-store surface is index-accelerated, not just a special "fast path."</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="what-about-metadata">What about metadata?<a href="https://serenedb.com/blog/langchain-serenedb-integration#what-about-metadata" class="hash-link" aria-label="Direct link to What about metadata?" title="Direct link to What about metadata?">​</a></h3>
<p>Vector search alone gives you the semantically closest chunks, but "closest" isn't always
"relevant" — you often want the nearest neighbors <em>among last quarter's tickets</em>, <em>from
this author</em>, <em>in the pricing category</em>. In the real world documents carry that structure by metadata — categories, timestamps, authors, tags. And constraining the similarity search by it is what turns a decent result set into the right one. The store gives you a rich filter language rather than bare equality: comparisons and ranges, set membership, existence and text patterns. Every filter compiles to SQL predicates evaluated <em>inside</em> the engine, right beside the vector scan, never as a slow post-filter in Python. Metadata can sit in a zero-ceremony JSON column or be promoted to typed columns when you know the schema — so filters ride an index scan instead of a full table scan.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="content-is-not-just-payload">Content is not just payload<a href="https://serenedb.com/blog/langchain-serenedb-integration#content-is-not-just-payload" class="hash-link" aria-label="Direct link to Content is not just payload" title="Direct link to Content is not just payload">​</a></h3>
<p>So far the content has been along for the ride — embedded into a vector, filtered by its metadata, then handed back on a hit.
But the text itself is a search signal too, and SereneDB can search in it directly. Turn on hybrid search and the store builds a combined index that fuses semantic vector similarity with SereneDB's native BM25 full-text ranking in a <em>single</em> query, so exact terms that embeddings miss — product codes, function names, rare keywords — still surface, combined by the fusion strategy you choose. And because the content column is inverted-indexed, the same machinery lets you bring full-text operators too.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="you-own-your-data">You own your data<a href="https://serenedb.com/blog/langchain-serenedb-integration#you-own-your-data" class="hash-link" aria-label="Direct link to You own your data" title="Direct link to You own your data">​</a></h3>
<p>SereneDB is self-hostable — and for RAG that is more than an ops preference.
The documents you index are usually exactly the data you <em>can't</em> send to a third party:
internal knowledge bases, contracts, source code, customer records. Run SereneDB on your
own infrastructure and the entire retrieval side of the pipeline — documents,
embeddings, queries — stays inside your perimeter. Your data stays yours.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="not-just-vectorstore">Not just VectorStore<a href="https://serenedb.com/blog/langchain-serenedb-integration#not-just-vectorstore" class="hash-link" aria-label="Direct link to Not just VectorStore" title="Direct link to Not just VectorStore">​</a></h3>
<p>VectorStore is only one corner of the system. SereneDB is a full database rather
than a dedicated vector engine, so the same instance that serves your embeddings also
serves everything around them: the relational tables your application already needs,
analytical queries over your document corpus, full-text search on its own, JSON
Storage — one connection string, one operational surface, standard PostgreSQL tooling. Your chunks can live next to the users, sessions, and business data they belong to. While this part is not covered by this LangChain integration package - you can use SQL to access the data and run additional tasks on it. The <a href="https://docs.serenedb.com/" target="_blank" rel="noopener noreferrer">SereneDB documentation</a> covers the full feature set well beyond what this integration touches.</p>
<p>And what matters the most is that all  features are in <em>one</em> engine:
IVF-accelerated vector similarity, BM25 full-text ranking fused with it in a single
hybrid query, and inverted-index-backed metadata filtering that runs next to the vector
search instead of after it. You get the retrieval quality tricks of a modern pipeline —
semantic recall, exact-term matching, precise filtering — without stitching together a
vector engine, a search engine, and a filter layer from three different products.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="getting-started">Getting started<a href="https://serenedb.com/blog/langchain-serenedb-integration#getting-started" class="hash-link" aria-label="Direct link to Getting started" title="Direct link to Getting started">​</a></h2>
<p>Now it is time to see SereneDB in action. As a tradition - a small HelloWorld example. You will need at least python 3.10 to run the example.</p>
<p>First of all we get a fresh SereneDB instance running. In this example we will use convenient script that will handle the details of creating a docker container:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">curl https://install.serenedb.com | sh</span><br></span></code></pre></div></div>
<p>Alternatively you can use other means for creating the instance - look at our <a href="https://docs.serenedb.com/quick-start" target="_blank" rel="noopener noreferrer">QuickStart guide</a></p>
<p>Now the langchain-serenedb integration package</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">pip install langchain_serenedb</span><br></span></code></pre></div></div>
<p>For the purposes of this example we will use langchain core provided fake embedder. But it is trivial to replace it with any of the embedder integrations supported by LangChain.</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> langchain_serenedb </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> SereneDBEngine</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> SereneDBVectorStore</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> IVFIndex</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> langchain_core</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">embeddings </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> DeterministicFakeEmbedding</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> langchain_core</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">documents </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> Document</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># This is the embedder. Replace it with the real one.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">my_embeddings </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> DeterministicFakeEmbedding</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">size</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">768</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Connect in a postgres style</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">engine </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> SereneDBEngine</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">from_connection_string</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"host=127.0.0.1 port=7890 user=postgres dbname=postgres"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Create the table and its IVF ANN index in one call.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># All internal SQL is generated automatically.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">engine</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">init_vectorstore_table</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"my_docs"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> vector_size</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">768</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> vector_index</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">IVFIndex</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Spin up the VectorStore</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">store </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> SereneDBVectorStore</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">create_sync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    engine</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> embedding_service</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">my_embeddings</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> table_name</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"my_docs"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># A tiny corpus to index (page content + optional metadata)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">docs </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Document</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">page_content</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"SereneDB speaks the PostgreSQL wire protocol."</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> metadata</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">{</span><span class="token string" style="color:#e3116c">"topic"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"intro"</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Document</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">page_content</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"The IVF index accelerates nearest-neighbor search."</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> metadata</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">{</span><span class="token string" style="color:#e3116c">"topic"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"index"</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Document</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">page_content</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"Hybrid search fuses BM25 keyword ranking with vectors."</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> metadata</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">{</span><span class="token string" style="color:#e3116c">"topic"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"search"</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Document</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">page_content</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"Metadata filters run inside the engine, next to the vectors."</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> metadata</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">{</span><span class="token string" style="color:#e3116c">"topic"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"filter"</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Document</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">page_content</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"Set sdb_nprobe to trade recall for latency per query."</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> metadata</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">{</span><span class="token string" style="color:#e3116c">"topic"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"tuning"</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Add some document to the index</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">store</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">add_documents</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">docs</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># And run your search!</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">results </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> store</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">similarity_search</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"how do I configure the index?"</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>From here it behaves like any LangChain vector store — <code>similarity_search</code>,
<code>similarity_search_with_score</code>, <code>max_marginal_relevance_search</code>, <code>get_by_ids</code>,
<code>delete</code>, and <code>as_retriever()</code> for dropping it into a chain. Of course this is only the basic setup, we will cover more advanced features in the following chapter.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h1>Advanced Topics</h1>
<p>Here we provide a short list of most usable features in detail. And dive into some internal machinery that powers our VectoreStore.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="features-walkthrough">Features walkthrough<a href="https://serenedb.com/blog/langchain-serenedb-integration#features-walkthrough" class="hash-link" aria-label="Direct link to Features walkthrough" title="Direct link to Features walkthrough">​</a></h3>
<p><strong>Accelerated vector search with IVF.</strong> SereneDB's ANN index is an IVF (inverted file)
index, and the integration exposes it as a plain Python object: <code>IVFIndex</code>. Declare one
and the package derives the distance metric from your chosen <code>DistanceStrategy</code>
(Euclidean/L2, cosine, inner product) and guarantees the index and every query it issues
agree on it — a consistency detail that trips people up when managing ANN indexes by
hand, handled here automatically. Optional <strong>quantization</strong> trades a little recall for a
lot of memory and speed: <code>sq8</code>, <code>sq4</code>, <code>pq</code> (with configurable sub-quantizers), or
<code>rabitq</code> (1–9 bits) — each just a constructor argument. For bulk loads, creating the
index <em>after</em> loading lets SereneDB train better IVF clusters; for incremental
workloads, creating it with the table (<code>init_vectorstore_table(..., vector_index=IVFIndex())</code>) means search is accelerated from the first row.
<code>apply_vector_index()</code>, <code>reindex()</code>, and <code>drop_vector_index()</code> manage the index on a
live store — no DDL statements to write or migrations to maintain.</p>
<p><strong>Tunable query-time recall.</strong> <code>IVFQueryOptions</code> controls the recall/latency trade-off
per query: <code>nprobe</code> sets how many IVF cluster lists are scanned (applied as the
<code>sdb_nprobe</code> session setting), and <code>rerank_factor</code> sizes the exact-distance rerank pool
when a quantized index is in play.</p>
<p><strong>Hybrid search: BM25 + vectors in one query.</strong> Semantic search is great at paraphrase
but can miss exact terms — product codes, function names, rare keywords. Build the
combined full-text + vector index with a <code>HybridIndexConfig</code>, give the store a
<code>HybridSearchConfig</code>, and it runs a <em>single</em> query that fuses SereneDB's native BM25
ranking with vector distance. Three <code>FusionStrategy</code> options are available:</p>
<ul>
<li><strong>RRF</strong> (Reciprocal Rank Fusion, the default) — combines <em>ranks</em> rather than scores
(<code>sum(1 / (rrf_k + rank))</code>), so BM25 scores and vector distances fuse without any
normalization.</li>
<li><strong>Weighted</strong> — min-max normalizes each branch (inverting distance so nearer = higher)
and takes a weighted sum, preserving score magnitudes.</li>
<li><strong>Weighted sum</strong> — a plain weighted sum of the raw branch scores.
See more detailed description with examples at our <a href="https://docs.serenedb.com/cookbook/search/reciprocal-rank-fusion" target="_blank" rel="noopener noreferrer">documentation page</a></li>
</ul>
<p><strong>Metadata filtering.</strong> Searches accept a MongoDB-style filter dictionary — <code>$eq</code>,
<code>$ne</code>, <code>$lt</code>/<code>$lte</code>/<code>$gt</code>/<code>$gte</code>, <code>$in</code>/<code>$nin</code>, <code>$between</code>, <code>$exists</code>, <code>$like</code>/<code>$ilike</code>,
composed with <code>$and</code>/<code>$or</code>/<code>$not</code>. On inverted-indexed columns you also get full-text
operators — <code>$startswith</code>, <code>$regex</code>, <code>$fuzzy</code>, <code>$ngram</code>, <code>$match</code>, <code>$phrase</code> — for prefix, regex,
typo-tolerant and phrase matching. The package translates the dictionary into database
predicates for you, so filtering runs inside the engine, right next to the vector
search — no post-filtering in Python, and no query syntax to learn.
<strong>Flexible metadata storage — and indexes for it.</strong> Metadata can live in a single
<code>JSON</code> column (zero schema ceremony) or be promoted to explicit typed columns via
<code>Column</code>/<code>ColumnDict</code> when you know your schema. Either way it can be indexed for fast
filtering: <code>MetadataColumnIndex</code> for dedicated columns and <code>JsonFieldIndex</code> for
individual fields inside the JSON blob, bundled through <code>MetadataIndexConfig</code>. Indexing
a field inside a JSON document correctly has subtle expression-matching rules at the
database level; the package encodes them so the index is always actually used — another
piece of database expertise you don't have to bring yourself.</p>
<p><strong>MMR retrieval.</strong> <code>max_marginal_relevance_search</code> is supported for result
diversification — useful when the top-<em>k</em> nearest neighbors are near-duplicates and the
chain benefits from broader coverage.</p>
<p><strong>Sync and async, one behavior.</strong> Every operation exists in both flavors —
<code>add_documents</code>/<code>aadd_documents</code>, <code>similarity_search</code>/<code>asimilarity_search</code>, and so on —
with the sync store delegating to the same async core, so behavior never drifts between
the two.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="how-it-works">How it works<a href="https://serenedb.com/blog/langchain-serenedb-integration#how-it-works" class="hash-link" aria-label="Direct link to How it works" title="Direct link to How it works">​</a></h3>
<p>Under LangChain's tidy <code>add_documents</code> / <code>similarity_search</code> surface, the package maps a
collection onto a deliberately simple SereneDB layout: <strong>one table and one <a href="https://docs.serenedb.com/sql/statements/create_index/inverted" target="_blank" rel="noopener noreferrer">inverted
index</a></strong>. Its real job is to keep the DDL it writes and every query it later issues in
lockstep — same distance metric, same extraction expressions, same dictionary — so
searches actually hit the index instead of quietly degrading into a full scan.</p>
<p><strong>The table.</strong> <code>init_vectorstore_table</code> creates a single table per collection: an <code>id</code>
primary key, a <code>content</code> TEXT column, the embedding as a fixed-size <a href="https://docs.serenedb.com/sql/data_types/array" target="_blank" rel="noopener noreferrer"><code>FLOAT[N]</code>
array</a>, and your metadata. Metadata has
two homes, and you can mix them: a catch-all <a href="https://docs.serenedb.com/data_import_and_export/json/json_type" target="_blank" rel="noopener noreferrer"><code>JSON</code>
column</a>
(the default — no schema to declare) or explicit typed columns promoted with <code>Column</code>
when you know a field's shape and want to index or constrain it.</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> langchain_serenedb </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> Column</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">engine</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">init_vectorstore_table</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"my_docs"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> vector_size</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">768</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    metadata_columns</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        Column</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"category"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"TEXT"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> nullable</span><span class="token operator" style="color:#393A34">=</span><span class="token boolean" style="color:#36acaa">False</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        Column</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"year"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"INTEGER"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>Anything you don't promote still round-trips through the JSON column, so promoting a
field is an optimization, never a requirement.</p>
<p><strong>One index, several columns.</strong> SereneDB's inverted index is not vector-only: a single
<code>USING inverted (...)</code> can carry the embedding column (with the <a href="https://docs.serenedb.com/sql/indexes/inverted/vector-search" target="_blank" rel="noopener noreferrer"><code>ivf</code> operator
class</a> for ANN), the content
column (<a href="https://docs.serenedb.com/sql/indexes/inverted/full-text-search" target="_blank" rel="noopener noreferrer">analyzed for BM25</a>),
and any number of metadata columns and JSON
sub-fields — all at once. The package builds exactly one such index per collection and
routes every search through it. That is the whole reason a hybrid, metadata-filtered
query can be <em>one</em> SQL statement: the vector ranking, the keyword ranking, and the filter
predicates are all served by the same index. And it is all powered by our own <a href="https://github.com/serenedb/serenedb/tree/main/libs/iresearch" target="_blank" rel="noopener noreferrer">IResearch library</a> that was started in 2016, and we have never stopped improving it. Check our benchmarks against other search engines in <a href="https://serenedb.com/search-benchmark-game" target="_blank" rel="noopener noreferrer">Search Benchmark Game</a> to see the actual performance results.</p>
<p><strong>The full-text dictionary.</strong> Full-text scoring needs a <a href="https://docs.serenedb.com/sql/indexes/inverted/text-analysis" target="_blank" rel="noopener noreferrer">text-search
<em>dictionary</em></a> — the
analyzer that turns raw content into scored tokens. The configuration splits cleanly by
concern: a build-time <code>HybridIndexConfig</code> describes the dictionary (used when the index is
created — the package issues a <code>CREATE TEXT SEARCH DICTIONARY</code> and references it on the
content column), while a query-time <code>HybridSearchConfig</code> carries the fusion knobs (used on
every search). The default dictionary is deliberately universal: a
<a href="https://docs.serenedb.com/sql/statements/create_text_search_dictionary/segmentation" target="_blank" rel="noopener noreferrer"><code>segmentation</code> template</a>,
lower-cased, with <code>frequency</code>, <code>position</code>, and <code>norm</code> enabled —
each one unlocking a capability, frequencies for <a href="https://docs.serenedb.com/sql/functions/search/scoring" target="_blank" rel="noopener noreferrer">BM25
scoring</a>, positions for phrase and
<a href="https://docs.serenedb.com/sql/statements/create_text_search_dictionary/ngram" target="_blank" rel="noopener noreferrer">n-gram matching</a>,
norms for the language-model scorers. The dictionary is created <em>in the table's own
schema</em> so an index living outside <code>public</code> can still resolve it — a subtlety the package
handles so you never meet it. Both sides are overridable:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> langchain_serenedb </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> HybridIndexConfig</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> HybridSearchConfig</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> FusionStrategy</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># build-time: how the content column is analyzed</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">index_cfg </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> HybridIndexConfig</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    dictionary_options</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"template = 'segmentation', case = 'lower', "</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                       </span><span class="token string" style="color:#e3116c">"frequency = true, position = true, norm = true"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># query-time: how the lexical and vector rankings fuse</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">search_cfg </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> HybridSearchConfig</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">fusion</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">FusionStrategy</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">RRF</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> scorer</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"BM25"</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p><strong>Deciding what to index.</strong> By default the package indexes <em>every declared metadata
column verbatim</em> — one token per value — which is exactly what lets a plain <code>=</code>, <code>IN</code>, or
range filter be answered from the index scan instead of a row-by-row recheck. There is a
safety net: in this index-everything mode a column whose type the inverted index cannot
take verbatim (a <code>NUMERIC</code>, a <code>UUID</code>, an <code>INTERVAL</code>) is quietly skipped, so the automatic
index can never fail to build. When you want control, <code>MetadataIndexConfig</code> states
precisely what joins the index — a subset of columns, a JSON sub-field, or a column
analyzed for full-text.</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> langchain_serenedb </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    MetadataIndexConfig</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> MetadataColumnIndex</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> JsonFieldIndex</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">metadata_index </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> MetadataIndexConfig</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    columns</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        MetadataColumnIndex</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"category"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">                                </span><span class="token comment" style="color:#999988;font-style:italic"># verbatim → =, IN, range</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        MetadataColumnIndex</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"title"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> dictionary</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"langchain_fts_dict"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># full-text</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    json_fields</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">JsonFieldIndex</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"attrs.brand"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"TEXT"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">                </span><span class="token comment" style="color:#999988;font-style:italic"># a field inside the JSON </span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>Here is a schematical data flow inside SereneDB database:</p>
<img src="https://serenedb.com/blog/img/blog/langchain-serenedb-integration/data-flow-light.png" alt="Data flow inside SereneDB database" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%;display:block;margin:24px auto 12px"><img src="https://serenedb.com/blog/img/blog/langchain-serenedb-integration/data-flow-dark.png" alt="Data flow inside SereneDB database" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%;display:block;margin:24px auto 12px">
<br>
<p>Two choices there carry real database expertise. Attaching a dictionary to a column flips
it from verbatim to full-text-analyzed: you gain the <a href="https://docs.serenedb.com/sql/functions/search/full-text" target="_blank" rel="noopener noreferrer"><code>$regex</code> / <code>$phrase</code> /
<code>$fuzzy</code></a> operators on it but
give up plain-equality pushdown (analyzed columns match through <code>@@</code>,
not <code>=</code>). And <a href="https://docs.serenedb.com/sql/indexes/inverted/json" target="_blank" rel="noopener noreferrer">indexing a field <em>inside</em> the JSON
blob</a> only works if the index expression
and the query expression are byte-identical — same <code>-&gt;&gt;</code> arrow, same <code>::type</code> cast on
both sides — or the filter pushes down but returns the wrong rows. The package generates
both sides from one definition so they cannot drift.</p>
<p><strong>Why can't the metric drift?</strong> The same principle governs the vector side. An <a href="https://docs.serenedb.com/sql/indexes/inverted/vector-search" target="_blank" rel="noopener noreferrer">IVF
index</a>
only accelerates a query whose distance operator matches the <code>metric</code> the index was built
with; let them fall out of step and the search silently reverts to a full scan. The
package derives both the index metric and the query operator from a single
<code>DistanceStrategy</code>, so the guarantee holds by construction — the recurring theme of the
whole integration: you declare intent in Python, and the package keeps the generated SQL
self-consistent.</p>
<h1>Recap</h1>
<p>By bringing vector similarity, BM25 full-text ranking, and metadata filtering together into a single, SQL-backed engine, SereneDB is a good fit for the architecture of modern LangChain RAG pipelines. Instead of managing multiple disparate systems, you get a cohesive retrieval layer that handles complex data shapes with index-accelerated performance. We invite you to try the <a href="https://pypi.org/project/langchain-serenedb" target="_blank" rel="noopener noreferrer">langchain-serenedb</a> package or dive into our <a href="https://docs.serenedb.com/" target="_blank" rel="noopener noreferrer">documentation</a> to see how SereneDB can streamline your own AI applications.</p>
<p>Additional links:</p>
<ul>
<li><a href="https://pypi.org/project/langchain-serenedb" target="_blank" rel="noopener noreferrer">https://pypi.org/project/langchain-serenedb</a></li>
<li><a href="https://docs.serenedb.com/" target="_blank" rel="noopener noreferrer">https://docs.serenedb.com</a></li>
<li><a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer">https://github.com/serenedb/serenedb</a></li>
<li><a href="https://github.com/serenedb/langchain-serenedb" target="_blank" rel="noopener noreferrer">https://github.com/serenedb/langchain-serenedb</a></li>
</ul></div>]]></content>
        <author>
            <name>SereneDB Team</name>
        </author>
        <category label="AI" term="AI"/>
        <category label="RAG" term="RAG"/>
        <category label="Search" term="Search"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The State of Serene, August 2026]]></title>
        <id>https://serenedb.com/blog/state-of-serene-2026-08</id>
        <link href="https://serenedb.com/blog/state-of-serene-2026-08"/>
        <updated>2026-09-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Issue #3 of The State of Serene: sloppy phrase search, Lucene query syntax, REINDEX and search over Iceberg, per-node score and merge control, Azure blob storage and a faster postings read path.]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><img class="avatarImage_s4Ar" src="https://serenedb.com/blog/img/abramov.jpeg" alt="Andrey Abramov" loading="lazy"></div></div><div class="copy_iHNG"><p class="names_ox2I">Andrey Abramov</p><p class="meta_OgzX">Sep 1, 2026 · 19 minutes read</p></div></header><h2 class="title_RQ9v">The State of Serene, August 2026</h2><p style="font-size:0.9rem;margin-top:-0.4rem">Issue #3: sloppy phrase search, Lucene query syntax, search over Iceberg without a second copy of your data, scoring you control and Azure</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg hideFirstHeading_VVMX" data-image-lightbox-root="true"><img src="https://serenedb.com/blog/img/blog/state-of-serene-2026-08/preview.png" alt="Serene" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%;display:block;margin:0 auto"><img src="https://serenedb.com/blog/img/blog/state-of-serene-2026-08/preview.png" alt="Serene" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%;display:block;margin:0 auto">
<br>
<p>Welcome back to <strong>The State of Serene</strong>, our monthly note on what shipped and where
SereneDB is heading.</p>
<p>August went into search. New features: sloppy phrase search, a Lucene query parser
that reads queries the way Lucene reads them, <code>REINDEX</code> for remote indexes that keep
themselves up-to-date, per-branch scoring control, an <code>idf()</code> scorer and Azure blob
storage. Plenty of performance work too: the postings read path got rewritten and
runs up to 2.9x quicker across 1076 queries, BM25 score computation nearly halved
and <code>quant = 'none'</code> vector search became a real quantizer instead of re-reading
raw vectors. The catalog also split away from the
data, which is what makes index recovery parallel. Outside the repo, SereneDB
became a selectable doc store in
<a href="https://github.com/infiniflow/ragflow/pull/17375" target="_blank" rel="noopener noreferrer">RAGFlow</a>. And we pointed the
whole thing at Dota 2 and shipped
<a href="https://wyl.serenedb.com/" target="_blank" rel="noopener noreferrer">Serene Why You Lost</a>.</p>
<p><a href="https://github.com/serenedb/serenedb/releases/tag/v26.08.2" target="_blank" rel="noopener noreferrer"><strong><code>v26.08.2</code></strong></a> is the
latest release.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-happened-in-august">What happened in August<a href="https://serenedb.com/blog/state-of-serene-2026-08#what-happened-in-august" class="hash-link" aria-label="Direct link to What happened in August" title="Direct link to What happened in August">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="sloppy-phrase-search">Sloppy phrase search<a href="https://serenedb.com/blog/state-of-serene-2026-08#sloppy-phrase-search" class="hash-link" aria-label="Direct link to Sloppy phrase search" title="Direct link to Sloppy phrase search">​</a></h3>
<p>An exact phrase misses the text that drops a word in the middle. <code>"zion machine"</code>
does not find "zion sent a machine". Until August the way to ask for that was to
build a disjunction of every gap variant by hand.</p>
<p>Slop is a budget for the whole phrase instead:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> docs_idx </span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> description @@ ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'zion machine'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> slop :</span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">3</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> docs_idx </span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> description @@ ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'group children'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">::slop</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> docs_idx </span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> b @@ to_tsquery</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'"quick fox"~1'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Three spellings for the same thing. <code>slop :=</code> sets it on the call,
<code>::slop(N)</code> applies it to a phrase that already exists, including one from
<code>phraseto_tsquery</code>. Lucene's <code>"..."~N</code> reaches it through <code>to_tsquery</code>.
Declared gaps still work: with <code>ts_phrase('group', 1, 'children', slop := 0)</code> the
budget counts deviation from the declared gap rather than from adjacency.</p>
<p>Docs: <a href="https://docs.serenedb.com/cookbook/search/phrase-and-proximity-search#proximity-search-with-slop" target="_blank" rel="noopener noreferrer">proximity search with slop</a>
and <a href="https://docs.serenedb.com/sql/functions/search/full-text" target="_blank" rel="noopener noreferrer"><code>ts_phrase</code></a>.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="lucene-query-syntax">Lucene query syntax<a href="https://serenedb.com/blog/state-of-serene-2026-08#lucene-query-syntax" class="hash-link" aria-label="Direct link to Lucene query syntax" title="Direct link to Lucene query syntax">​</a></h3>
<p><code>to_tsquery</code> takes a Lucene or Elasticsearch <code>query_string</code> expression. The parser
behind it got rebuilt to read one the way Lucene does. What it reads:</p>
<table><thead><tr><th>syntax</th><th>meaning</th></tr></thead><tbody><tr><td><code>a AND b</code> · <code>a OR b</code> · <code>a NOT b</code> · <code>+a</code> · <code>-a</code> · <code>(a b)</code></td><td>booleans, required and excluded terms, grouping</td></tr><tr><td><code>a*</code> · <code>a~2</code> · <code>a^1.5</code></td><td>prefix, fuzzy within an edit distance, boost</td></tr><tr><td><code>"a b"</code> · <code>"a b"~1</code></td><td>phrase · phrase with a slop budget</td></tr><tr><td><code>"alpha 1-3 beta"</code></td><td>a declared gap between phrase parts</td></tr><tr><td><code>"alpha bet*"</code> · <code>"alpha beta~1"</code></td><td>prefix and fuzziness per phrase part</td></tr><tr><td><code>[alpha TO omega]</code> · <code>{alpha TO omega}</code></td><td>inclusive and exclusive ranges</td></tr><tr><td><code>title&lt;beta</code> · <code>title&gt;=alpha</code></td><td>comparison bounds</td></tr><tr><td><code>(alpha beta gamma)@2</code></td><td>minimum match</td></tr><tr><td><code>fn:or</code> · <code>fn:ordered</code> · <code>fn:unordered</code> · <code>fn:atLeast</code> · <code>fn:phrase</code> · <code>fn:wildcard</code> · <code>fn:fuzzyTerm</code> · <code>fn:ngram</code> · <code>fn:maxgaps</code> · <code>fn:maxwidth</code></td><td>the flexible-parser function family</td></tr></tbody></table>
<p>Terms are whatever the syntax has not claimed, so <code>café</code>, <code>u.s.a</code> and <code>rock-n-roll</code>
are terms. A hyphen or a plus inside a word belongs to the word. A term goes
through the analyzer. A prefix, wildcard, fuzziness or range bound gets normalized
instead of tokenized, which is the distinction that keeps
<code>["alpha beta" TO gamma]</code> searching from <code>alpha beta</code> rather than from <code>alpha</code>.</p>
<p>Anything SereneDB has no algebra for is refused by name instead of quietly ignored.
A query that cannot be read says so instead of answering zero.</p>
<p>Docs: <a href="https://docs.serenedb.com/sql/functions/search/full-text#to_tsquery" target="_blank" rel="noopener noreferrer"><code>to_tsquery</code></a>.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="search-got-faster">Search got faster<a href="https://serenedb.com/blog/state-of-serene-2026-08#search-got-faster" class="hash-link" aria-label="Direct link to Search got faster" title="Direct link to Search got faster">​</a></h3>
<p>The postings read path, the term-iterator contract and score pruning all got
rewritten. Measured over the 1076 queries of search-benchmark-game against the tree
before the rewrite:</p>
<table><thead><tr><th>what the query is doing</th><th>faster by</th></tr></thead><tbody><tr><td>handing back the documents that matched</td><td>up to 2.9x</td></tr><tr><td>taking the top 100 by relevance</td><td>up to 1.7x</td></tr><tr><td>handing those back with their scores</td><td>up to 1.6x</td></tr><tr><td>counting the matches</td><td>up to 13%</td></tr></tbody></table>
<p>Computing a BM25 score came out nearly twice as fast on its own.</p>
<p>Where it comes from: two new block encodings that carry no payload at all, so a run
of consecutive doc ids decodes from its header and folds into a consumer's mask in
one operation. Block consumption moved out of the iterator and into the format
layer, so the block disjunction and the deleted-docs mask share one implementation
instead of each open-coding it. The in-block bit scatter runs 8 interleaved chains,
because consecutive doc ids usually land in the same 64-bit word and serialise the
loop on store-to-load forwarding. In-block seek is a branchless fixed-width binary
search, so there is no data-dependent branch left to mispredict.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="search-over-a-lakehouse-without-a-second-copy-of-your-data">Search over a lakehouse, without a second copy of your data<a href="https://serenedb.com/blog/state-of-serene-2026-08#search-over-a-lakehouse-without-a-second-copy-of-your-data" class="hash-link" aria-label="Direct link to Search over a lakehouse, without a second copy of your data" title="Direct link to Search over a lakehouse, without a second copy of your data">​</a></h3>
<p>Your data in your own bucket stays the source of truth, whether that is an Iceberg
table or a directory of Parquet, CSV or JSON. Any engine writes to it, Spark or
BigQuery or Flink or SereneDB itself. SereneDB holds only the derived index on local
disk: term dictionaries, vector structures, the columns you chose to index. There is
no second copy of the corpus, so losing the node means rebuilding an index rather
than restoring a database. Columns you did not index are still selectable,
materialized from the source for the rows a query actually matched.</p>
<p>The piece missing until August was the barrier between writing and querying.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">REINDEX </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> chunks_idx</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> rpr_idx </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> rpr_v </span><span class="token keyword" style="color:#00009f">USING</span><span class="token plain"> inverted</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> body rpr_en</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">WITH</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">reindex_interval </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>One pass compares the source's current committed state against what the index
holds, applies the difference and publishes atomically, so readers see the previous
complete state or the new one and never a partial index. When it returns,
everything committed before it is searchable. That turns a pipeline into three
steps: writers commit, one <code>REINDEX</code>, consumers start. <code>reindex_interval</code> runs the
pass on a schedule instead. <code>REINDEX INDEX CONCURRENTLY</code> is accepted too, because
the pass never blocks readers either way.</p>
<p>How much work a pass does depends on what it is reading:</p>
<table><thead><tr><th>source</th><th>what a pass detects</th><th>work</th></tr></thead><tbody><tr><td>Iceberg table</td><td>the diff against the table's current snapshot, row-level deletes included</td><td>delta</td></tr><tr><td>file glob (Parquet, CSV, JSON, S3)</td><td>files that appeared, changed or disappeared</td><td>delta, unchanged files are not re-read</td></tr><tr><td>base tables, attached databases, generic views</td><td>any change</td><td>full rebuild</td></tr></tbody></table>
<p>For a catalog-attached Iceberg table a pass forces a fresh table load even inside
the server's staleness window, which is what makes it a barrier rather than a hint.</p>
<p>The whole thing from an empty catalog, including the hybrid queries at the end, is
in <a href="https://docs.serenedb.com/cookbook/search/iceberg-insert-to-searchable" target="_blank" rel="noopener noreferrer">Search over Iceberg</a>.
Reference for the refresh itself:
<a href="https://docs.serenedb.com/sql/indexes/inverted/views#refreshing-the-index" target="_blank" rel="noopener noreferrer">refreshing the index</a>.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="control-how-a-score-is-evaluated">Control how a score is evaluated<a href="https://serenedb.com/blog/state-of-serene-2026-08#control-how-a-score-is-evaluated" class="hash-link" aria-label="Direct link to Control how a score is evaluated" title="Direct link to Control how a score is evaluated">​</a></h3>
<p>Scoring a boolean query is two decisions: how each branch scores and how the branch
scores combine. Both are overridable per node now.</p>
<p><code>::merge(...)</code> sets the combine policy. The default is sum, so a document matching
<code>fox</code> and <code>cat</code> gets both contributions added. <code>max</code> takes the best branch instead:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> BM25</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">idx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">tableoid</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> idx</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">body @@ </span><span class="token string" style="color:#e3116c">'fox'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">OR</span><span class="token plain"> body @@ </span><span class="token string" style="color:#e3116c">'cat'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">::</span><span class="token keyword" style="color:#00009f">merge</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'max'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>It binds to its own node, so nesting means something. <code>max</code> on an inner node still
lets the outer node add <code>dog</code> on top. <code>max</code> on the outer node takes the best of all
three.</p>
<p><code>::score(...)</code> overrides the scorer for one subtree while the rest of the query
stays on the <code>ORDER BY</code> scorer:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> BM25</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">idx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">tableoid</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> idx</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> body @@ </span><span class="token string" style="color:#e3116c">'fox'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">OR</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">body @@ </span><span class="token string" style="color:#e3116c">'cat'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">::score</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'constant(1)'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p><code>::score(NULL)</code> takes a subtree out of scoring entirely. It still selects rows,
contributes nothing and asks for no index features, so its postings are read without
frequency or norms. That is the shape Elasticsearch spells as a <code>filter</code> clause.</p>
<p>The two compose. A per-branch scorer under a group policy competes on its own
terms, so a <code>constant(10)</code> branch survives a <code>max</code> against a BM25 sibling. An
unscored branch has nothing to compare, so the other branch wins by default.</p>
<p><code>max</code> earns its keep on synonyms. Expand <code>car</code> into <code>car OR automobile</code> and the
default sum gives a document that happens to use both words two contributions. It
then outranks a document that is just as relevant and picked one spelling. Those
branches are one concept and <code>max</code> scores them as one.</p>
<p>Both <code>::score</code> and <code>::merge</code> ride on the <code>TSQUERY</code> value rather than on the
predicate, the same family as <code>::boost</code> and <code>::slop</code>, so they reach a group built by
<code>ts_any</code> or <code>ts_all</code> as readily as a parenthesised <code>OR</code>:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> BM25</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">idx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">tableoid</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> idx</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> body @@ ts_any</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">'car'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'automobile'</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">::</span><span class="token keyword" style="color:#00009f">merge</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'max'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Riding on the value is also why they survive a prepared statement.
<code>WHERE b @@ $1::score('constant(42)')</code> makes the parameter that type, so the
modifier arrives with the bound value rather than being lost before the index sees
it.</p>
<p>There is also a new <code>idf()</code> scorer: a document scores the inverse document
frequency of the matched term alone, with no term frequency and no length
normalisation. It works anywhere a scorer works, <code>optimize_top_k = 'idf()'</code>
included. It scores columns indexed without a dictionary too. <code>bm25(k1 = 0)</code> is the
same formula and used to return zero, which is fixed as well.</p>
<p>Docs: <a href="https://docs.serenedb.com/sql/functions/search/scoring" target="_blank" rel="noopener noreferrer">scoring</a>.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="azure-plus-credentials-for-every-cloud">Azure, plus credentials for every cloud<a href="https://serenedb.com/blog/state-of-serene-2026-08#azure-plus-credentials-for-every-cloud" class="hash-link" aria-label="Direct link to Azure, plus credentials for every cloud" title="Direct link to Azure, plus credentials for every cloud">​</a></h3>
<p><code>az://</code> URIs work now in <code>COPY</code>, <code>read_csv</code>, <code>read_parquet</code> and <code>glob</code>, with
<code>CREATE SECRET (TYPE azure)</code> for connection strings and scoping. You can build an
inverted index over an <code>az://</code> Parquet or CSV view the same way you would over S3.</p>
<p>Iceberg catalogs got Google service-account auth:
<code>CREATE SECRET (TYPE ICEBERG, PROVIDER google)</code> with service-account-key and GCE
metadata-server modes, so BigLake catalog tokens renew themselves under a machine
identity with no user credentials involved. Verified against a real BigLake catalog
with a forced token expiry mid-session.</p>
<p>Object-storage reads got about a fifth quicker in the same stretch: large Azure
reads 20% warm and 21% cold, large HTTPS reads 21%.</p>
<p>Docs: <a href="https://docs.serenedb.com/configuration/aws_credentials" target="_blank" rel="noopener noreferrer">AWS</a>,
<a href="https://docs.serenedb.com/configuration/azure_credentials" target="_blank" rel="noopener noreferrer">Azure</a> and
<a href="https://docs.serenedb.com/configuration/google_cloud_credentials" target="_blank" rel="noopener noreferrer">Google Cloud</a>
credentials, plus
<a href="https://docs.serenedb.com/configuration/iceberg_authentication" target="_blank" rel="noopener noreferrer">Iceberg catalog authentication</a>
and the <a href="https://docs.serenedb.com/cookbook/network_cloud_storage/biglake_iceberg" target="_blank" rel="noopener noreferrer">BigLake cookbook</a>.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="pgstream-works-against-serenedb-plus-a-wider-postgres-surface">pgstream works against SereneDB, plus a wider Postgres surface<a href="https://serenedb.com/blog/state-of-serene-2026-08#pgstream-works-against-serenedb-plus-a-wider-postgres-surface" class="hash-link" aria-label="Direct link to pgstream works against SereneDB, plus a wider Postgres surface" title="Direct link to pgstream works against SereneDB, plus a wider Postgres surface">​</a></h3>
<p>Point a Postgres logical-replication client at SereneDB and it works now.
<a href="https://github.com/xataio/pgstream" target="_blank" rel="noopener noreferrer">pgstream</a> is the one we tested with. Three
gaps each stopped it cold and silently, all three in how a pg catalog answered a
question. All three turned up by diffing its wire capture against the same client
aimed at real Postgres.</p>
<p><strong><code>pg_proc</code> and <code>pg_aggregate</code> are populated.</strong> <code>pg_proc</code> listed only macros created
in the current database, so 3461 functions existed and exactly one was visible.
<code>\df</code>, ORM introspection and <code>information_schema.routines</code> and <code>parameters</code> all saw
nothing. Both are filled from a walk over the system catalog now.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="vector-search-panorama-pruning-and-a-cheaper-descent">Vector search: Panorama pruning and a cheaper descent<a href="https://serenedb.com/blog/state-of-serene-2026-08#vector-search-panorama-pruning-and-a-cheaper-descent" class="hash-link" aria-label="Direct link to Vector search: Panorama pruning and a cheaper descent" title="Direct link to Vector search: Panorama pruning and a cheaper descent">​</a></h3>
<p><code>quant = 'none'</code> used to mean no payload at all, so every query re-read raw vectors
out of the columnstore to rank them. It is a real quantizer now, built on faiss's
<a href="https://github.com/serenedb/serenedb/pull/1006" target="_blank" rel="noopener noreferrer">Panorama</a> layout: a PCA-rotated
basis with per-level suffix norms, which lets the posting scan drop a candidate on
an exact Cauchy-Schwarz bound against the running k-th best rather than reading it
in full. Same answers, less reading.</p>
<p>Radius queries go through the payload for the same reason, with a constant pruning
threshold instead of a raw reranker over the columnstore. Under a lossy quantizer a
survivor's reported distance is rescored exactly from the index's own vectors, so
the number you get back is the real one.</p>
<p>The quantizer writer and reader protocol is a single block contract now, which is
what lets Panorama's batched layout sit alongside PQ and RaBitQ fast-scan groups and
flat scalar-quantizer records without each one growing its own hooks.</p>
<p>Then the <a href="https://github.com/serenedb/serenedb/pull/1059" target="_blank" rel="noopener noreferrer">descent itself got cheaper</a>.
Probing N clusters on a deep centroid tree meant expanding every child at every
level. Most of that work never reaches a cluster you scan.
<code>sdb_ivf_max_search_fanout</code> caps how many children a node expands, defaulting to 16,
which decouples descent width from how many clusters you asked for. It raises itself
when the cap is too small to supply the requested clusters, so it cannot starve a
query. The nprobe setting is now <code>sdb_ivf_search_nprobe</code>. <code>sdb_rerank_factor</code> takes
fractional values.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="serenedb-is-a-doc-store-in-ragflow-now">SereneDB is a doc store in RAGFlow now<a href="https://serenedb.com/blog/state-of-serene-2026-08#serenedb-is-a-doc-store-in-ragflow-now" class="hash-link" aria-label="Direct link to SereneDB is a doc store in RAGFlow now" title="Direct link to SereneDB is a doc store in RAGFlow now">​</a></h3>
<p><a href="https://github.com/infiniflow/ragflow" target="_blank" rel="noopener noreferrer">RAGFlow</a> is an open-source RAG engine with
around 90k stars. Since August SereneDB is one of the document stores you can put
behind it, on both the Go and the Python path. <code>DOC_ENGINE=serenedb</code> and it
runs. The integration is not ours. Our fellow community member
<a href="https://github.com/deadtrickster" target="_blank" rel="noopener noreferrer"><strong>deadtrickster</strong></a> wrote it: 26 files and about
3,900 lines. It
<a href="https://github.com/infiniflow/ragflow/pull/17375" target="_blank" rel="noopener noreferrer">merged upstream</a> on 4 August and
ships in RAGFlow <code>v0.27.0</code> onward.</p>
<p>Why it fits is the thing this blog keeps saying. One inverted index carries the
scored text column and the IVF vector column, so hybrid retrieval is one SQL
statement instead of two systems and a merge step. The connector puts one table per
tenant with <code>kb_id</code> as a filter column, which is the Elasticsearch layout rather
than a table per dataset, so BM25 statistics stay computed over the whole tenant
corpus instead of per knowledge base.</p>
<p>Their numbers, measured on the Python path over a 247,665-chunk multilingual corpus
with weighted fusion, against Elasticsearch on the same gold set:</p>
<table><thead><tr><th>engine</th><th>MRR</th><th>mean rank</th><th>latency</th></tr></thead><tbody><tr><td>Elasticsearch hybrid</td><td>0.82</td><td>2.8</td><td>180 ms</td></tr><tr><td>SereneDB</td><td>0.82</td><td>3.0</td><td>35 ms</td></tr></tbody></table>
<p>Same MRR, a hair behind on mean rank, roughly 5x quicker. At concurrency 16 it did
514 QPS against Elasticsearch's 241, with a p99 of 42 ms against 108 ms. Building
the index over those 247k chunks took 5.6 seconds. Migrating all 247,665 documents
through the connector produced zero errors.</p>
<p>Those are the contributor's measurements on their corpus rather than ours. We have
not reproduced them. We are quoting them because the methodology is in the PR.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="why-you-lost-an-analytics-showcase-built-on-serenedb">Why You Lost, an analytics showcase built on SereneDB<a href="https://serenedb.com/blog/state-of-serene-2026-08#why-you-lost-an-analytics-showcase-built-on-serenedb" class="hash-link" aria-label="Direct link to Why You Lost, an analytics showcase built on SereneDB" title="Direct link to Why You Lost, an analytics showcase built on SereneDB">​</a></h3>
<p><a href="https://wyl.serenedb.com/" target="_blank" rel="noopener noreferrer"><strong>Serene Why You Lost</strong></a> takes a finished Dota 2 match,
lets you pick which of the ten players is you and tells you what actually cost you
the game.</p>
<p>It exists to show what search plus analytics in one engine does to a problem that
is not logs. A match is a long event stream with ten people deciding at once and a
one-word result hiding all of it. Finding the moments that mattered is a search
problem. Judging whether a decision was good is an analytics question over thousands
of similar situations. Both run in the same queries, which is the whole argument
this database makes, applied to something you can check against your own memory of
the game.</p>
<p>A scoreboard says you died six times and finished an item late. It cannot say which
of those mattered, what led to it or what the alternative was. Why You Lost answers
at three depths. The overview accounts for how the game developed and where momentum
shifted. The deep analysis finds the moments that moved your chances of winning and
explains why they moved them. The interactive 2D replay puts those moments back in
context, with every fight on one timeline and positions, movement and health second
by second.</p>
<p>No language model writes the advice. Every comparison comes from real match data and
it prints the sample size next to the number, so you can decide for yourself whether
to believe it.</p>
<p>It went up as a seven-day test during <strong>The International 2026</strong> and it's
<a href="https://wyl.serenedb.com/" target="_blank" rel="noopener noreferrer">still online</a>. Whether it stays depends on whether people
use it, so go break it.
<a href="https://serenedb.com/blog/serene-wyl">The writeup</a> has the rest.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="search-backed-tables-take-indexes">Search-backed tables take indexes<a href="https://serenedb.com/blog/state-of-serene-2026-08#search-backed-tables-take-indexes" class="hash-link" aria-label="Direct link to Search-backed tables take indexes" title="Direct link to Search-backed tables take indexes">​</a></h3>
<p>A search-backed table is the iresearch columnstore with, as of August, optional
indexes on top. <code>WITH (storage = 'search')</code> and the rows go straight into iresearch
segments. It is not transactional. What you get back for that is speed.</p>
<p>The indexes are the new half. Until August this was a table you could not index at
all, which limited it to whatever a sequential scan could answer. Now it carries
inverted indexes with the same syntax a regular table uses. Each indexed field gets
a virtual field id that maps back to the real column on materialization, so one
column can be indexed several times with different tokenizers while sharing a
single store.</p>
<p>The bulk-load path also stopped writing WAL chunk files. It flushes and fsyncs whole
index segments and records the segment name in the WAL instead, so recovery adopts
the segment when its tick committed and background cleanup wipes it when it did not.</p>
<p>Plain inserts are at parity now. On 10M rows of ClickBench a search-backed table
does insert plus commit in 4.89 s where a transactional one takes 5.20 s. It used
to be around 30% slower than that. Give both tables inverted indexes on <code>WatchID</code>
and <code>URL</code> and it stops being a comparison:</p>
<table><thead><tr><th>phase</th><th>transactional</th><th>search-backed</th><th>faster by</th></tr></thead><tbody><tr><td>insert</td><td>28.14 s</td><td>5.94 s</td><td>4.7x</td></tr><tr><td>refresh</td><td>1,447.8 ms</td><td>69.7 ms</td><td>21x</td></tr><tr><td>total</td><td>29.59 s</td><td>6.01 s</td><td>4.9x</td></tr></tbody></table>
<p>Two limits worth knowing before you reach for it. There is no backfill yet, so
indexes have to be added while the table is empty. Only one IVF index per column is
allowed. Both are follow-ups rather than design decisions.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="also-shipped">Also shipped<a href="https://serenedb.com/blog/state-of-serene-2026-08#also-shipped" class="hash-link" aria-label="Direct link to Also shipped" title="Direct link to Also shipped">​</a></h3>
<ul>
<li><strong>DDL is transactional now.</strong> A <code>CREATE</code> or <code>DROP</code> that fails no longer leaves the
catalog half-changed.</li>
<li><strong>Recovery of inverted indexes runs in parallel</strong> and is no longer bound by how
much memory the machine has.</li>
<li><strong>DML against an inverted index runs in parallel</strong> too.</li>
<li><strong>Sequences got faster.</strong></li>
<li><strong>Fuzzy expansion has its own cap</strong>, <code>sdb_levenshtein_max_terms</code>, default 64.
It used to take its limit from <code>sdb_scored_terms_limit</code>, which is documented as
a scoring-cost knob, so setting that knob changed result sets: over 2001 terms
within edit distance 2 of <code>cat</code>, <code>SET sdb_scored_terms_limit = 50</code> returned
exactly 50 rows.</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-to-expect-in-september">What to expect in September<a href="https://serenedb.com/blog/state-of-serene-2026-08#what-to-expect-in-september" class="hash-link" aria-label="Direct link to What to expect in September" title="Direct link to What to expect in September">​</a></h2>
<ul>
<li><strong>Logical replication subscriptions and triggers.</strong> <code>CREATE SUBSCRIPTION</code> so an
attached Postgres streams its changes in rather than being re-read. August made
SereneDB usable as a replication target; this is the other half.</li>
<li><strong>Row-level security.</strong> Policies per table, on top of July's roles and grants.</li>
<li><strong>Another round of search performance.</strong> The postings work in this issue is not
finished. The next round is aiming at 1.5x to 2x.</li>
<li><strong>Geospatial.</strong> A spatial type and index surface.</li>
<li><strong>Even faster search.</strong> We've almost finished a huge rework of our search execution.</li>
<li><strong>More benchmarks.</strong> Still open from last month. Vector benchmarks are the first
ones we owe you.</li>
</ul>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="kudos">Kudos<a href="https://serenedb.com/blog/state-of-serene-2026-08#kudos" class="hash-link" aria-label="Direct link to Kudos" title="Direct link to Kudos">​</a></h2>
<p>Plenty of this month came from outside the core team.</p>
<p><a href="https://github.com/aksel2904" target="_blank" rel="noopener noreferrer"><strong>aksel2904</strong></a> built sloppy phrase search, both the
engine side and the SQL surface. He also contributed the regexp filter we shipped
back in June, which makes two features of the search surface now.</p>
<p><a href="https://github.com/deadtrickster" target="_blank" rel="noopener noreferrer"><strong>deadtrickster</strong></a> did awesome work. Apart from
the RAGFlow integration, he also built the tool we should probably have written
ourselves. <a href="https://github.com/deadtrickster/serenedash" target="_blank" rel="noopener noreferrer"><strong>serenedash</strong></a> is a live
terminal dashboard for a SereneDB server: storage and the spill split, pool memory
against RSS and swap, sessions and how far along their statements are, per-thread
CPU, a perf-backed profile and whether index maintenance is keeping up. The same
collectors are exposed over MCP, so an agent can read the live server instead of
having panels pasted at it. Credentials are optional. A panel that genuinely needs
the server says which of "no driver", "no credentials" or "cannot connect" applies
rather than drawing a zero and calling it a reading.</p>
<p>On the activity view, one keypress plans a statement that is already running.
<code>EXPLAIN</code> has always been something you could type, but aiming it at the query
currently burning a core means fetching its text out of
<code>pg_stat_activity</code> and quoting it back. On that deployment the text was 68 KB.
<code>EXPLAIN</code> does not execute, so it is safe to point at something hung.</p>
<p>He also filed several of the bugs we fixed this month, found by running SereneDB
behind a real workload and reporting exactly what broke. Thanks as well to
<a href="https://github.com/emarsden" target="_blank" rel="noopener noreferrer"><strong>emarsden</strong></a>, who keeps holding the SQL surface to
the standard and to <a href="https://github.com/deymon-d" target="_blank" rel="noopener noreferrer"><strong>deymon-d</strong></a>, who fixed phrase
with intervals.</p>
<p>And thank you to everyone with work still in progress:
<a href="https://github.com/seb-06" target="_blank" rel="noopener noreferrer"><strong>seb-06</strong></a>,
<a href="https://github.com/w3lld1" target="_blank" rel="noopener noreferrer"><strong>w3lld1</strong></a>,
<a href="https://github.com/ivan-digital" target="_blank" rel="noopener noreferrer"><strong>ivan-digital</strong></a>,
<a href="https://github.com/romanpovol" target="_blank" rel="noopener noreferrer"><strong>romanpovol</strong></a> and
<a href="https://github.com/afigor2701" target="_blank" rel="noopener noreferrer"><strong>afigor2701</strong></a>.</p>
<p>Want to be in the next one? We tag beginner-friendly work with
<a href="https://github.com/serenedb/serenedb/contribute" target="_blank" rel="noopener noreferrer"><code>good first issue</code></a>, so grab one,
ask questions in the issue and we'll get you going.</p>
<hr>
<p><a href="https://github.com/serenedb/serenedb/releases/tag/v26.08.2" target="_blank" rel="noopener noreferrer"><strong><code>v26.08.2</code></strong></a> is the
current release. Grab it and point it at something. If you'd rather look before you
install, the <a href="http://codesearch.serenedb.com/" target="_blank" rel="noopener noreferrer">code search demo</a> is live and <code>⌘K</code> on
<a href="https://docs.serenedb.com/" target="_blank" rel="noopener noreferrer">the docs</a> gets you SereneDB searching SereneDB's
documentation. Every raw benchmark result is at
<a href="https://serenedb.com/searchbench/" target="_blank" rel="noopener noreferrer">serenedb.com/searchbench</a>.</p>
<p>If you like what you see, ⭐ <a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer">star us on GitHub</a>.
Hit a rough edge? Open an issue. The Kudos section above is people who did exactly
that.</p>
<p>See you in the next State of Serene.</p></div>]]></content>
        <author>
            <name>Andrey Abramov</name>
            <uri>https://www.linkedin.com/in/andrey-abramov-57a16241/ru/?skipRedirect=true</uri>
        </author>
        <category label="News" term="News"/>
        <category label="Search" term="Search"/>
        <category label="Database" term="Database"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Serene WYL: find out what actually lost you the match]]></title>
        <id>https://serenedb.com/blog/serene-wyl</id>
        <link href="https://serenedb.com/blog/serene-wyl"/>
        <updated>2026-08-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Serene WYL turns a finished Dota 2 match into a layered review: an overview, deep analysis, an interactive replay and recommendations based on real games.]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="none" viewBox="0 0 28 28" class="logoAvatar_BINX"><path fill="#895AF8" d="M28 14c0-7.732-6.268-14-14-14S0 6.268 0 14s6.268 14 14 14 14-6.268 14-14"></path><path fill="#80BEFF" d="M9.345 18.655c4.556 4.556 11.416 5.084 15.32 1.178 3.906-3.905 3.379-10.764-1.178-15.32C18.931-.043 12.072-.571 8.167 3.334S4.789 14.1 9.345 18.654"></path><path fill="#fff" d="M15.649 12.35c2.278 2.278 5.676 2.574 7.59.66s1.617-5.311-.66-7.59c-2.279-2.277-5.677-2.573-7.59-.66-1.914 1.914-1.618 5.312.66 7.59"></path></svg></div></div><div class="copy_iHNG"><p class="names_ox2I">SereneDB Team</p><p class="meta_OgzX">Aug 20, 2026 · 4 minutes read</p></div></header><h2 class="title_RQ9v">Serene WYL: find out what actually lost you the match</h2><p style="font-size:0.9rem;margin-top:-0.4rem">Paste a Dota 2 match, choose your player and go from the big picture to the decisions that changed the game</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg hideFirstHeading_VVMX" data-image-lightbox-root="true"><p>We build a database, which means we spend a lot of time pointing it at datasets
and asking increasingly difficult questions. Usually that ends in a benchmark.
This time we pointed it at Dota 2 instead.</p>
<p>The result is <a href="https://wyl.serenedb.com/" target="_blank" rel="noopener noreferrer"><strong>Serene WYL</strong></a>, short for <strong>Why You
Lost</strong>: a tool that takes a finished match and explains what happened from one
player's point of view.</p>
<p>A scoreboard can tell you that you died six times, finished an item late or
lost the last fight. It cannot tell you which of those things actually mattered,
what led to them or what you could have done differently. WYL is built for that
second set of questions.</p>
<p>Paste a match link, choose which of the ten players is you and start with the
big picture. From there you can go as deep as you want.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="from-the-match-overview-to-individual-decisions">From the match overview to individual decisions<a href="https://serenedb.com/blog/serene-wyl#from-the-match-overview-to-individual-decisions" class="hash-link" aria-label="Direct link to From the match overview to individual decisions" title="Direct link to From the match overview to individual decisions">​</a></h2>
<p>Serene WYL presents the same match at several levels.</p>
<video autoplay="" loop="" muted="" playsinline="" preload="metadata" style="width:100%;display:block;margin:24px auto"><source src="/blog/assets/medias/match-analysis-789e1a5eb8d9c05d5d1f33f603f1657b.webm" type="video/webm"></video>
<p><em>Move from the match overview into draft, fights and build analysis.</em></p>
<p>The <strong>general analysis</strong> gives you a quick account of the game: how it developed,
where momentum shifted and which parts of the match deserve a closer look. It is
the place to start when you want to understand the game without watching the
whole replay again.</p>
<p>The <strong>deep analysis</strong> looks at the decisions behind the result. It finds moments
that changed your chances of winning, explains why they mattered and gives you a
more useful answer than a list of end-of-match statistics. You can inspect the
parts that matter to you and skip the rest.</p>
<p>The <strong>interactive 2D replay</strong> connects the analysis back to the match itself.
Fights are collected in one timeline, and you can jump directly to any of them
to see positions, movement and health change second by second. It is a compact
way to reconstruct a game while keeping the important moments in context.</p>
<video autoplay="" loop="" muted="" playsinline="" preload="metadata" style="width:100%;display:block;margin:24px auto"><source src="/blog/assets/medias/interactive-replay-09c064576500eb8ab4e9622a203a144c.webm" type="video/webm"></video>
<p><em>Jump between fights and inspect the match on an interactive 2D map.</em></p>
<p>WYL also reviews <strong>picks, builds and other match decisions</strong> in the context in
which they were made. The goal is not to hand out universal rules. It is to show
what made sense in this particular game and offer alternatives for similar
situations.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="how-it-works">How it works<a href="https://serenedb.com/blog/serene-wyl#how-it-works" class="hash-link" aria-label="Direct link to How it works" title="Direct link to How it works">​</a></h2>
<p>The flow is deliberately simple:</p>
<ol>
<li>Paste a link to a finished match.</li>
<li>Select your player.</li>
<li>Read the overview, open the deeper analysis and jump into the replay wherever
you want more context.</li>
</ol>
<p>The conclusions come from the events in the match and patterns across real
games. This is not an AI coach generating plausible-sounding advice. Every
comparison is grounded in match data, and WYL shows the sample size next to the
numbers so you can judge the evidence for yourself.</p>
<p>That matters because Dota is situational. A decision can be good in one match
and terrible in another depending on the draft, timing, map and nine other
players. Looking at one isolated number removes that context. Looking at many
similar situations puts some of it back.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="why-we-built-it">Why we built it<a href="https://serenedb.com/blog/serene-wyl#why-we-built-it" class="hash-link" aria-label="Direct link to Why we built it" title="Direct link to Why we built it">​</a></h2>
<p>SereneDB combines search and analytics in one database. A Dota match is a fun
way to show what that means: a long stream of events, ten players making
decisions at once and a simple result hiding a complicated story.</p>
<p>Serene WYL turns that stream into something you can explore at different depths.
The overview summarizes the match, the deeper layers find the moments worth
examining and the replay lets you return to the original context. It is the same
basic problem we work on with other kinds of data: find what matters, measure it
and make the result understandable.</p>
<p>The current version is a draft, and we already have many more metrics we want to
add. We are running it in test mode for seven days during The International
2026. If players find it useful, we will keep it online longer and turn it into
a deeper analytics platform.</p>
<p><a href="https://wyl.serenedb.com/" target="_blank" rel="noopener noreferrer">Try Serene WYL</a>, then tell us what it got right, what
it missed and which analysis you want next. If you want to help us keep building
experiments like this, <a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer">star SereneDB on
GitHub</a>.</p></div>]]></content>
        <author>
            <name>SereneDB Team</name>
        </author>
        <category label="Demos" term="Demos"/>
        <category label="News" term="News"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Build a search index on someone else's database]]></title>
        <id>https://serenedb.com/blog/foreign-database-search</id>
        <link href="https://serenedb.com/blog/foreign-database-search"/>
        <updated>2026-08-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Build a SereneDB full-text index over tables in Postgres or ClickHouse, choose which columns stay local and query the results through SQL.]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><img class="avatarImage_s4Ar" src="https://serenedb.com/blog/img/ivanov.jpg" alt="Pavel Ivanov" loading="lazy"></div></div><div class="copy_iHNG"><p class="names_ox2I">Pavel Ivanov</p><p class="meta_OgzX">Aug 11, 2026 · 15 minutes read</p></div></header><h2 class="title_RQ9v">Build a search index on someone else's database</h2><p style="font-size:0.9rem;margin-top:-0.4rem">SereneDB indexes tables, files and lakehouses. Now it also indexes tables that live inside Postgres and ClickHouse, without moving the rows.</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg hideFirstHeading_VVMX" data-image-lightbox-root="true"><p>Ask a team where their text lives and you usually get two answers. The data of
record sits in ClickHouse or Snowflake or a few terabytes of Parquet on S3. The
<em>searchable</em> copy sits in Elasticsearch. Between them runs a pipeline that
somebody wrote two years ago, somebody else maintains now and everyone has
opinions about.</p>
<p>The duplication is the obvious cost. Every searchable row exists twice and the
search copy tends to be the expensive one, because a document store keeps the
original JSON alongside the index so it can hand it back to you.</p>
<p>The subtler cost is that the two halves can't talk to each other. Elasticsearch
will rank your documents beautifully and then give you a list of ids. If your
next question is "and what was the weekly revenue on those?", you write
application code to stitch two result sets together. Usually you just don't ask.</p>
<p>SereneDB exists to collapse that arrangement into one engine:
Elasticsearch-grade search and ClickHouse-grade analytics, both addressed over
the Postgres wire protocol. This release extends it in a direction
<a href="https://blog.serenedb.com/search-where-your-data-lives" target="_blank" rel="noopener noreferrer">we've been working toward for a while</a>.
You can now build a full-text index over a table that belongs to a different
database entirely.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-search-means-here">What "search" means here<a href="https://serenedb.com/blog/foreign-database-search#what-search-means-here" class="hash-link" aria-label="Direct link to What &quot;search&quot; means here" title="Direct link to What &quot;search&quot; means here">​</a></h2>
<p>Lots of analytical databases have bolted on a text index and put "full-text
search" on the feature list. You find out how deep it goes the first time a
product manager asks for typo tolerance or ranking that goes beyond recency.</p>
<p>Ours comes from
<a href="https://github.com/serenedb/serenedb/tree/main/libs/iresearch" target="_blank" rel="noopener noreferrer">IResearch</a>, a C++
information retrieval library our team has been building since 2016. It's what
we benchmark against Lucene and Tantivy in
<a href="https://serenedb.com/search-benchmark-game" target="_blank" rel="noopener noreferrer">Search Benchmark, The Game</a>.
<a href="https://serenedb.com/searchbench" target="_blank" rel="noopener noreferrer">SearchBench</a> puts it against Elastic,
OpenSearch, ParadeDB and ArangoDB. In SQL you get:</p>
<ul>
<li>BM25 ranking with real term frequencies and document norms</li>
<li>phrase search and sloppy phrase search with a slop window</li>
<li>fuzzy terms, prefixes and wildcards</li>
<li>facets and aggregations computed over the matched set</li>
<li>geospatial predicates on points and geometries</li>
<li>vector search: IVF with product quantization, RaBitQ, and 4- and 8-bit scalar quantization</li>
<li>hybrid ranking that fuses text and vector results with RRF</li>
</ul>
<p>Because one planner sees the whole query, a search predicate and an analytical
rollup are the same statement:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> date_trunc</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'day'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> ts</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">day</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       </span><span class="token function" style="color:#d73a49">count</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">*</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">              </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> hits</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       </span><span class="token function" style="color:#d73a49">avg</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">rating</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">           </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> avg_rating</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> reviews_fts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> body @@ ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'battery life'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">AND</span><span class="token plain"> lang </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'en'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">GROUP</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">day</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">day</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>In the two-system world that's a search request, a JSON response, a list of ids
sent back to your app and a warehouse query with a 40,000-element <code>IN</code> clause.
Assuming the id set is small enough to ship at all.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="where-an-index-can-point">Where an index can point<a href="https://serenedb.com/blog/foreign-database-search#where-an-index-can-point" class="hash-link" aria-label="Direct link to Where an index can point" title="Direct link to Where an index can point">​</a></h2>
<p>Here's the design decision the rest of this post depends on. In SereneDB an
inverted index does not belong to one table. It asks a data source for two
things: a way to read the rows once at build time and a way to get a given row
back later if a query needs its contents. Anything that can answer both can be
indexed.</p>
<p>So <code>CREATE INDEX ... USING inverted(...)</code> currently accepts:</p>
<table><thead><tr><th>Source</th><th>In practice</th></tr></thead><tbody><tr><td>a local table</td><td>the ordinary case, with real-time column-wise updates</td></tr><tr><td>a view</td><td>index a projection, a join, a filtered subset, a JSON path</td></tr><tr><td>Parquet / CSV / NDJSON</td><td>on local disk, S3 or Azure Blob, indexed where they sit</td></tr><tr><td>Iceberg</td><td>catalog-managed lakehouse tables</td></tr><tr><td>a glob</td><td>one index spanning a whole prefix in object storage</td></tr><tr><td><strong>an attached Postgres or ClickHouse table</strong></td><td><strong>new and the subject of the rest of this post</strong></td></tr></tbody></table>
<p>The middle rows are what we've been calling zero-ETL remote search: BM25 and
vector search straight over a data lake with no ingestion job in the middle. We
wrote that up for source code in
<a href="https://blog.serenedb.com/code-search-system" target="_blank" rel="noopener noreferrer">code search over a data lake</a>.</p>
<p>A live database behaves differently from a Parquet file. It has its own primary
key, its own query planner and its own preferred way of being asked for two
thousand rows.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="connecting-a-database">Connecting a database<a href="https://serenedb.com/blog/foreign-database-search#connecting-a-database" class="hash-link" aria-label="Direct link to Connecting a database" title="Direct link to Connecting a database">​</a></h2>
<p>SereneDB speaks the Postgres wire protocol, so it borrows Postgres's vocabulary
for talking to other databases. Two foreign data wrappers ship today,
<code>postgres_fdw</code> and <code>clickhouse_fdw</code>. Nothing above the connector layer is
specific to either one, so adding an engine takes a connector and nothing else.
Two generic wrappers are in progress right now, ODBC and ADBC. Between them they
reach most of the database market. More on those at the end.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> SERVER events </span><span class="token keyword" style="color:#00009f">FOREIGN</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">DATA</span><span class="token plain"> WRAPPER clickhouse_fdw</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  OPTIONS </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">host </span><span class="token string" style="color:#e3116c">'ch.internal'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> port </span><span class="token string" style="color:#e3116c">'9000'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">database</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'prod'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">GRANT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">USAGE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">FOREIGN</span><span class="token plain"> SERVER events </span><span class="token keyword" style="color:#00009f">TO</span><span class="token plain"> analysts</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>A <code>CREATE SERVER</code> is catalog DDL. It survives restarts, re-attaches on boot,
shows up in <code>pg_foreign_server</code> and participates in dependency tracking like any
other catalog object. An <code>ATTACH</code> lasts only as long as the session that made it.
Credentials live on the server definition (one shared connection identity, the
way ClickHouse itself does it) and access is a grant.</p>
<img src="https://serenedb.com/blog/img/blog/foreign-database-search/foreign-server-light.svg" alt="A foreign server is a catalog object inside SereneDB that routes queries to Postgres and ClickHouse while SereneDB holds the inverted indexes" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%;display:block;margin:24px auto 12px"><img src="https://serenedb.com/blog/img/blog/foreign-database-search/foreign-server-dark.svg" alt="A foreign server is a catalog object inside SereneDB that routes queries to Postgres and ClickHouse while SereneDB holds the inverted indexes" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%;display:block;margin:24px auto 12px">
<p><em>A foreign server survives a restart. The server definitions and the inverted
indexes live in SereneDB; the tables stay in the databases you already run,
reachable over each engine's own protocol. Restart SereneDB and the servers
re-attach on boot.</em></p>
<p>For ClickHouse the connector talks the native TCP protocol, columnar blocks and
LZ4, over a pooled and health-checked connection layer, with projection, filter
and <code>ORDER BY</code> / <code>LIMIT</code> pushdown, cardinality from <code>system.tables</code> and automatic
rebinding when the remote schema shifts under it. After that the remote table is
just a table:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">count</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">*</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> events</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">prod</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">hits</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="indexing-it">Indexing it<a href="https://serenedb.com/blog/foreign-database-search#indexing-it" class="hash-link" aria-label="Direct link to Indexing it" title="Direct link to Indexing it">​</a></h2>
<p>Everything in this section runs as written. It points at ClickHouse's public
playground, so there is nothing to set up on the ClickHouse side at all. Two
commands and a paste. The only prerequisite is Docker.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain"># start SereneDB locally (brings up SereneUI too)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">curl -fsSL https://install.serenedb.com | sh</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># connect</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">psql -h localhost -p 7890 -U postgres</span><br></span></code></pre></div></div>
<p>The installer prints the exact connect and teardown commands when it finishes,
along with a browser URL for SereneUI — use those if it landed on a different
port. If there is no <code>psql</code> on the machine it ships one too. Then paste this at
the prompt:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> SERVER play </span><span class="token keyword" style="color:#00009f">FOREIGN</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">DATA</span><span class="token plain"> WRAPPER clickhouse_fdw</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  OPTIONS </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">host </span><span class="token string" style="color:#e3116c">'play.clickhouse.com'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> port </span><span class="token string" style="color:#e3116c">'9440'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">database</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'default'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">           </span><span class="token keyword" style="color:#00009f">user</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'play'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> secure </span><span class="token string" style="color:#e3116c">'true'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TEXT</span><span class="token plain"> SEARCH DICTIONARY en </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">template </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'segmentation'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'lower'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">VIEW</span><span class="token plain"> hn </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> title</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"by"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> score</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> url</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> play</span><span class="token punctuation" style="color:#393A34">.</span><span class="token keyword" style="color:#00009f">default</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">hackernews</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'story'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">AND</span><span class="token plain"> score </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">500</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">LIMIT</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1024</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> hn_fts </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> hn </span><span class="token keyword" style="color:#00009f">USING</span><span class="token plain"> inverted</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> title en</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  INCLUDE </span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"by"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> title</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"by"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> score</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> hn_fts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> title @@ ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'rust'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> score </span><span class="token keyword" style="color:#00009f">DESC</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">LIMIT</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">4</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">                      title                       |      by       | score</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">--------------------------------------------------+---------------+-------</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> Async-await on stable Rust                       | pietroalbini  |  1102</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> I have written a JVM in Rust                     | lukastyrychtr |   718</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> Tauri 1.0 – Electron Alternative Powered by Rust | Uninen        |   715</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> Rust/WinRT Public Preview                        | steveklabnik  |   628</span><br></span></code></pre></div></div>
<p>The index build takes about two tenths of a second, the search about the same.
Three columns end up in three places. <code>title</code> is tokenized into the index, <code>"by"</code>
is included so it comes off local disk and <code>score</code> is neither — it is fetched
back from ClickHouse by <code>id</code>, but only for the four rows that survived the
<code>LIMIT</code>. ClickHouse has no idea any of this happened.</p>
<p>The <code>LIMIT 1024</code> is there because this runs on somebody else's server. The
playground is public and rate-limited, so the example takes a small slice and
stays well inside the quota — enough to watch the thing work and no setup on
your side at all. Point it at your own ClickHouse the moment you want more than
a look. The slice is arbitrary, so your four titles will not be these four.</p>
<p>One more, because it costs people an afternoon: the dictionary above sets
<code>case = 'lower'</code>. The default is <code>none</code>, which indexes terms exactly as written
and then <code>ts_phrase('rust')</code> matches nothing at all while <code>ts_phrase('Rust')</code>
matches everything.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="how-much-of-your-data-the-index-keeps">How much of your data the index keeps<a href="https://serenedb.com/blog/foreign-database-search#how-much-of-your-data-the-index-keeps" class="hash-link" aria-label="Direct link to How much of your data the index keeps" title="Direct link to How much of your data the index keeps">​</a></h2>
<p>This is the knob people usually don't know they have.</p>
<p>By default the index holds postings, positions, norms and document lengths, plus
whatever key it needs to find a row again. Your actual column values aren't in
there. Ask for them and SereneDB goes and reads them from the source. <code>INCLUDE</code>
changes that per column: named columns get written into the index's own
columnstore and reading them afterwards costs one local read and no trip to the
source at all.</p>
<img src="https://serenedb.com/blog/img/blog/foreign-database-search/included-columns-light.svg" alt="Four requested columns resolve inside SereneDB and only the text column is fetched from ClickHouse for the ten matching rows" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%;display:block;margin:24px auto 12px"><img src="https://serenedb.com/blog/img/blog/foreign-database-search/included-columns-dark.svg" alt="Four requested columns resolve inside SereneDB and only the text column is fetched from ClickHouse for the ten matching rows" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%;display:block;margin:24px auto 12px">
<p><em>Included columns stay local. <code>id</code> and the BM25 score come out of the index
itself, <code>author</code> and <code>posted_at</code> out of the index's columnstore because they were
included. Only <code>text</code> is fetched from ClickHouse, by primary key, for the ten
rows that survived the <code>LIMIT</code>.</em></p>
<p>So you get a dial.</p>
<p><strong>Store nothing.</strong> Smallest possible index. Good when the source is fast and
local or when most queries only need counts, scores and ids anyway. A surprising
number of "search" workloads are really <code>count(*)</code> with a <code>WHERE</code> clause and
those never touch the source at all.</p>
<p><strong>Store what you display.</strong> The common setting. Include the title, the url, the
price, the timestamp: whatever a result card renders. Searches serve entirely
from the index; a user who clicks through to a full record pays one keyed read.</p>
<p><strong>Store everything.</strong> Now the index is a self-contained local mirror and the
source is only consulted when you rebuild. This is roughly what Elasticsearch
does with <code>_source</code>, except Elasticsearch decided it for you, which is part of
why the mirror costs what it does.</p>
<p>Concretely. A shop's product table holds <code>title</code>, <code>description</code> and <code>brand</code>,
which arrive with a supplier catalogue and change a few times a year, next to
<code>price_cents</code> and <code>stock</code>, which change all day and change inside transactions,
because you cannot sell stock you do not have. Index the text, include the brand
and leave the two numbers where the transactions are:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> products_fts </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> products</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">USING</span><span class="token plain"> inverted</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sku</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> title en</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> description en</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  INCLUDE </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">brand</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Search then runs entirely on local disk, matching and ranking and filtering by
brand, while the two values a customer actually acts on are read from Postgres
as the page renders. The price on the screen cannot be stale, because we do not
have it. When the index falls behind, a product from yesterday's catalogue
import is not findable yet. Nobody is ever shown a wrong price.</p>
<p>The reason to include a column is that you do not want the round trip. Everything
you leave out is read from the source when a query asks for it, so put the
columns your result page renders into the index and the search stays on local
disk.</p>
<p>There is also a limit worth knowing about. The connector can only go back to a
view that reads one source plainly. Put a join or a <code>GROUP BY</code> in the view body,
say <code>SELECT customer_id, string_agg(body, ' ') FROM tickets GROUP BY customer_id</code>
and reading real columns is refused outright with
<code>materialising real columns from this view-backed inverted index is not yet supported</code>.
That index still matches, counts and ranks. It just cannot hand you the rows.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-that-buys-you">What that buys you<a href="https://serenedb.com/blog/foreign-database-search#what-that-buys-you" class="hash-link" aria-label="Direct link to What that buys you" title="Direct link to What that buys you">​</a></h2>
<p><strong>No second copy of the corpus.</strong> You store an index and you choose per column
how much more than that you want.</p>
<p><strong>No pipeline.</strong> There's no CDC job, no queue, no mapping template drifting out
of sync with a schema, no dashboard panel asking whether the sync is behind. The
build is a DDL statement.</p>
<p><strong>Adoption without a migration.</strong> This is the part platform teams actually care
about. Your warehouse stays where it is, keeps its ingest, its retention rules
and its access control while gaining a capability from the outside. If it
doesn't work out, <code>DROP SERVER</code> puts you back exactly where you started. That's
a much easier conversation than "we should move the data."</p>
<p><strong>Results that are relations.</strong> The matched set is a table like any other, so
you can aggregate it, window it, join it to a local dimension table, park it in
a CTE or rank it by a blend of BM25 and vector distance. Over rows that are
physically in ClickHouse.</p>
<blockquote>
<p>Keep the database you have. Add search on top of it. Skip the copy in between.</p>
</blockquote>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="how-the-lookup-works">How the lookup works<a href="https://serenedb.com/blog/foreign-database-search#how-the-lookup-works" class="hash-link" aria-label="Direct link to How the lookup works" title="Direct link to How the lookup works">​</a></h2>
<p>Four things happen between <code>CREATE INDEX</code> and a row coming back.</p>
<img src="https://serenedb.com/blog/img/blog/foreign-database-search/remote-lookup-light.svg" alt="At CREATE INDEX time SereneDB finds a key and streams the remote table once; at query time it matches locally and fetches unstored columns in keyed batches" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%;display:block;margin:24px auto 12px"><img src="https://serenedb.com/blog/img/blog/foreign-database-search/remote-lookup-dark.svg" alt="At CREATE INDEX time SereneDB finds a key and streams the remote table once; at query time it matches locally and fetches unstored columns in keyed batches" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%;display:block;margin:24px auto 12px">
<p><em>The remote engine is touched in three different ways and only one of them
repeats. Reading the primary key and streaming the table happen once, when the
index is created (dashed). After that the only traffic is the keyed fetch for
columns that are neither indexed nor included, one statement per batch of
matches.</em></p>
<p>How a row is identified differs by engine and each default follows from how
that engine works. Postgres keys on <code>ctid</code>, the physical row location:
universal, no primary key required and the fetch pushes down as a TID scan.
ClickHouse keys on the table's MergeTree primary key instead, read from
<code>system.columns</code>, because part-and-offset ids do not survive a merge. Either
default can be overridden with <code>WITH (key_columns = ...)</code>.</p>
<p>The statements differ too, since each planner recognises a different shape: on
Postgres the keys travel as a bound array parameter, on ClickHouse as a native
columnar block sent alongside the query. Rows come back in whatever order the
engine produced them and are put back in place by an ordinal that travelled out
with the batch.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-else-the-connection-is-good-for">What else the connection is good for<a href="https://serenedb.com/blog/foreign-database-search#what-else-the-connection-is-good-for" class="hash-link" aria-label="Direct link to What else the connection is good for" title="Direct link to What else the connection is good for">​</a></h2>
<p>A search index is one use of a foreign server. The connection underneath has a
few others.</p>
<p><strong>One index over several engines.</strong> An index can be built over a view and a view
can be a union of tables from different servers. Fresh documents in Postgres,
the archive in ClickHouse, cold data as Parquet in object storage and a single
index across all of it. One query, one ranking and no stitching results back
together in application code. A union has no single table to key on, so this is
the keyless mode described above — postings key on a synthetic row id and you
include the columns you want back.</p>
<p><strong>Joins across engines.</strong> Facts in ClickHouse, dimensions in Postgres, one
statement. Neither side is pre-joined or copied and the search index can sit on
either of them.</p>
<p><strong>ETL without the tool.</strong> Both connectors write as well as read, so
<code>INSERT INTO ... SELECT</code> across servers is one statement: Postgres into
ClickHouse or either of them into SereneDB's own storage once you decide the data
should be local after all. The extract can be driven by a search query, which
turns "everything matching this, into that table" into a one-liner — a document
export, a training-set build, a subset migration.</p>
<p>All three pull their data through one process, so the scale they suit is a
nightly job, a subset or a dimension table. Hundreds of terabytes need a real
pipeline.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-comes-next-odbc-and-adbc">What comes next: ODBC and ADBC<a href="https://serenedb.com/blog/foreign-database-search#what-comes-next-odbc-and-adbc" class="hash-link" aria-label="Direct link to What comes next: ODBC and ADBC" title="Direct link to What comes next: ODBC and ADBC">​</a></h2>
<p>Two wrappers cover two engines. The next two cover most of the rest.</p>
<p><strong>ODBC</strong> is the lowest common denominator of database connectivity. If a
database has been sold commercially in the last thirty years, it has an ODBC
driver. A wrapper shaped like <code>clickhouse_fdw</code> but speaking ODBC points
<code>CREATE SERVER</code> at anything with a DSN: MySQL, SQL Server, Oracle, Snowflake,
Redshift, Databricks, anything with a DSN.</p>
<p><strong>ADBC</strong> is the newer Arrow-native one and it is the better fit for what we do.
Result sets arrive as Arrow columnar batches instead of one row at a time. That
matters most during an index build, where we stream a whole table exactly once.
Postgres, DuckDB, Snowflake, BigQuery and Flight SQL all have ADBC drivers
already.</p>
<p>Neither one changes anything above the connector layer. The index, the <code>INCLUDE</code>
columnstore, the key-based fetch and the SQL you write all stay as described
above. And for sources where no primary key can be discovered from metadata,
which becomes a good deal more likely once you are going through a generic
driver, the keyless path already exists: postings key on a synthetic row id and
the columns you included come back from the local columnstore.</p>
<p>You end up with one <code>CREATE SERVER</code> surface and one index mechanism. The list of
engines you can reach becomes the list of engines that ship a driver.</p>
<p>Smaller items on the connector itself: fuller batch accumulation on the lookup
path and parameterized <code>IN {keys:Array}</code> over the ClickHouse native protocol.</p>
<p>Everything here is on <code>main</code>, Apache 2.0.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">curl https://install.serenedb.com | sh</span><br></span></code></pre></div></div>
<p>Point a <code>CREATE SERVER</code> at a database you already run, build an index on a table
that isn't ours and tell us what breaks.</p></div>]]></content>
        <author>
            <name>Pavel Ivanov</name>
        </author>
        <category label="Search" term="Search"/>
        <category label="Database" term="Database"/>
        <category label="Dev" term="Dev"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[A Deep (and Fuzzy) Dive Into Search]]></title>
        <id>https://serenedb.com/blog/fuzzy-search-deep-dive</id>
        <link href="https://serenedb.com/blog/fuzzy-search-deep-dive"/>
        <updated>2026-08-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Six years ago, when we first shipped fuzzy search (yes, without agents!), we wrote up the two algorithms]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><img class="avatarImage_s4Ar" src="https://serenedb.com/blog/img/abramov.jpeg" alt="Andrey Abramov" loading="lazy"></div></div><div class="copy_iHNG"><p class="names_ox2I">Andrey Abramov</p><p class="meta_OgzX">Aug 5, 2026 · 22 minutes read</p></div></header><h2 class="title_RQ9v">A Deep (and Fuzzy) Dive Into Search</h2><p style="font-size:0.9rem;margin-top:-0.4rem">How Levenshtein automata and n-gram similarity power fuzzy matching in SereneDB</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg hideFirstHeading_VVMX" data-image-lightbox-root="true"><p>Six years ago, when we first shipped fuzzy search (yes, without agents!), we wrote up the two algorithms
that sit underneath it. The search engine has the same name but a new home since then. <a href="https://github.com/serenedb/serenedb/tree/main/libs/iresearch" target="_blank" rel="noopener noreferrer">IResearch</a> is now the search core of SereneDB and you reach it through SQL instead of a bespoke query language but the theory behind the implementation has not aged a day. This is that article, rewritten for where the code actually lives today.</p>
<p>"Fuzzy search" is an umbrella term for a family of approximate-matching
algorithms. Each one defines some similarity measure between a query term and the
terms in a dictionary, so the engine can decide which results are close enough to
show and in what order. In this post I'll walk through the two that matter most,
because they are genuinely different tools for different jobs:</p>
<ul>
<li>Approximate matching based on <strong>Levenshtein distance</strong></li>
<li>Approximate matching based on <strong>n-gram similarity</strong></li>
</ul>
<p>I'll go deep on each, flag the problems you hit when implementing them at scale and show how both are exposed in SereneDB today.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="why-fuzzy-search-at-all">Why fuzzy search at all?<a href="https://serenedb.com/blog/fuzzy-search-deep-dive#why-fuzzy-search-at-all" class="hash-link" aria-label="Direct link to Why fuzzy search at all?" title="Direct link to Why fuzzy search at all?">​</a></h2>
<p>We deal with unstructured, imperfect text everywhere. Web search taught a whole
generation that being inexact is normal: you fat-finger a query on a phone and
expect the typo to be fixed for you. But it goes well beyond autocorrect:</p>
<ul>
<li><strong>Linguistics.</strong> Identifying cognates across dictionaries is central to
historical linguistics and cognates are, by definition, not identical.</li>
<li><strong>Bioinformatics.</strong> A DNA sequence is an absurdly long string over a four-letter
alphabet (<code>ACGT</code>); quantifying variation between sequences <em>is</em> approximate
string matching.</li>
<li><strong>Records and search.</strong> Deduplicating people, products or addresses means
matching "Jon Smith" to "John Smith" and "priorty_queue" to "priority_queue".</li>
</ul>
<p>All of these need a way to say <em>how close</em> two strings are. Let's start with the
most famous answer.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="approximate-matching-based-on-levenshtein-distance">Approximate matching based on Levenshtein distance<a href="https://serenedb.com/blog/fuzzy-search-deep-dive#approximate-matching-based-on-levenshtein-distance" class="hash-link" aria-label="Direct link to Approximate matching based on Levenshtein distance" title="Direct link to Approximate matching based on Levenshtein distance">​</a></h2>
<p>The <strong>Levenshtein distance</strong> between two words is the minimum number of
insertions, deletions or substitutions needed to turn one into the other.</p>
<p>For example, the distance between <code>foo</code> and <code>bar</code> is 3, because every letter has
to be substituted:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">foo -&gt; boo -&gt; bao -&gt; bar</span><br></span></code></pre></div></div>
<p>Formally, the distance between strings <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi></mrow><annotation encoding="application/x-tex">a</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mord mathnormal">a</span></span></span></span> and <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>b</mi></mrow><annotation encoding="application/x-tex">b</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em"></span><span class="mord mathnormal">b</span></span></span></span> (of lengths <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">∣</mi><mi>a</mi><mi mathvariant="normal">∣</mi></mrow><annotation encoding="application/x-tex">|a|</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord">∣</span><span class="mord mathnormal">a</span><span class="mord">∣</span></span></span></span> and <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">∣</mi><mi>b</mi><mi mathvariant="normal">∣</mi></mrow><annotation encoding="application/x-tex">|b|</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord">∣</span><span class="mord mathnormal">b</span><span class="mord">∣</span></span></span></span>) is
<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mrow><mi mathvariant="normal">lev</mi><mo>⁡</mo></mrow><mrow><mi>a</mi><mo separator="true">,</mo><mi>b</mi></mrow></msub><mo stretchy="false">(</mo><mi mathvariant="normal">∣</mi><mi>a</mi><mi mathvariant="normal">∣</mi><mo separator="true">,</mo><mi mathvariant="normal">∣</mi><mi>b</mi><mi mathvariant="normal">∣</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">\operatorname{lev}_{a,b}(|a|, |b|)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.0361em;vertical-align:-0.2861em"></span><span class="mop"><span class="mop"><span class="mord mathrm" style="margin-right:0.01389em">lev</span></span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em"><span style="top:-2.55em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">a</span><span class="mpunct mtight">,</span><span class="mord mathnormal mtight">b</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord">∣</span><span class="mord mathnormal">a</span><span class="mord">∣</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">∣</span><span class="mord mathnormal">b</span><span class="mord">∣</span><span class="mclose">)</span></span></span></span>, where:</p>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mrow><mi mathvariant="normal">lev</mi><mo>⁡</mo></mrow><mrow><mi>a</mi><mo separator="true">,</mo><mi>b</mi></mrow></msub><mo stretchy="false">(</mo><mi>i</mi><mo separator="true">,</mo><mi>j</mi><mo stretchy="false">)</mo><mo>=</mo><mrow><mo fence="true">{</mo><mtable rowspacing="0.36em" columnalign="left left" columnspacing="1em"><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><mi>max</mi><mo>⁡</mo><mo stretchy="false">(</mo><mi>i</mi><mo separator="true">,</mo><mi>j</mi><mo stretchy="false">)</mo></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><mtext>if&nbsp;</mtext><mi>min</mi><mo>⁡</mo><mo stretchy="false">(</mo><mi>i</mi><mo separator="true">,</mo><mi>j</mi><mo stretchy="false">)</mo><mo>=</mo><mn>0</mn><mo separator="true">,</mo></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><mi>min</mi><mo>⁡</mo><mrow><mo fence="true">{</mo><mtable rowspacing="0.36em" columnalign="left left" columnspacing="1em"><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><msub><mrow><mi mathvariant="normal">lev</mi><mo>⁡</mo></mrow><mrow><mi>a</mi><mo separator="true">,</mo><mi>b</mi></mrow></msub><mo stretchy="false">(</mo><mi>i</mi><mo>−</mo><mn>1</mn><mo separator="true">,</mo><mi>j</mi><mo stretchy="false">)</mo><mo>+</mo><mn>1</mn></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><msub><mrow><mi mathvariant="normal">lev</mi><mo>⁡</mo></mrow><mrow><mi>a</mi><mo separator="true">,</mo><mi>b</mi></mrow></msub><mo stretchy="false">(</mo><mi>i</mi><mo separator="true">,</mo><mi>j</mi><mo>−</mo><mn>1</mn><mo stretchy="false">)</mo><mo>+</mo><mn>1</mn></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><msub><mrow><mi mathvariant="normal">lev</mi><mo>⁡</mo></mrow><mrow><mi>a</mi><mo separator="true">,</mo><mi>b</mi></mrow></msub><mo stretchy="false">(</mo><mi>i</mi><mo>−</mo><mn>1</mn><mo separator="true">,</mo><mi>j</mi><mo>−</mo><mn>1</mn><mo stretchy="false">)</mo><mo>+</mo><msub><mn mathvariant="bold">1</mn><mrow><mo stretchy="false">(</mo><msub><mi>a</mi><mi>i</mi></msub><mo mathvariant="normal">≠</mo><msub><mi>b</mi><mi>j</mi></msub><mo stretchy="false">)</mo></mrow></msub></mrow></mstyle></mtd></mtr></mtable></mrow></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mtext>otherwise.</mtext></mstyle></mtd></mtr></mtable></mrow></mrow><annotation encoding="application/x-tex">\operatorname{lev}_{a,b}(i, j) =
\begin{cases}
\max(i, j) &amp; \text{if } \min(i,j) = 0, \\[4pt]
\min
\begin{cases}
\operatorname{lev}_{a,b}(i-1, j) + 1 \\
\operatorname{lev}_{a,b}(i, j-1) + 1 \\
\operatorname{lev}_{a,b}(i-1, j-1) + \mathbf{1}_{(a_i \ne b_j)}
\end{cases}
&amp; \text{otherwise.}
\end{cases}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.0361em;vertical-align:-0.2861em"></span><span class="mop"><span class="mop"><span class="mord mathrm" style="margin-right:0.01389em">lev</span></span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em"><span style="top:-2.55em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">a</span><span class="mpunct mtight">,</span><span class="mord mathnormal mtight">b</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal">i</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.05724em">j</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:6.16em;vertical-align:-2.83em"></span><span class="minner"><span class="mopen"><span class="delimsizing mult"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:3.25em"><span style="top:-1.366em"><span class="pstrut" style="height:3.216em"></span><span class="delimsizinginner delim-size4"><span>⎩</span></span></span><span style="top:-1.358em"><span class="pstrut" style="height:3.216em"></span><span style="height:1.216em;width:0.8889em"><svg xmlns="http://www.w3.org/2000/svg" width="0.8889em" height="1.216em" style="width:0.8889em" viewBox="0 0 888.89 1216" preserveAspectRatio="xMinYMin"><path d="M384 0 H504 V1216 H384z M384 0 H504 V1216 H384z"></path></svg></span></span><span style="top:-3.216em"><span class="pstrut" style="height:3.216em"></span><span class="delimsizinginner delim-size4"><span>⎨</span></span></span><span style="top:-4.358em"><span class="pstrut" style="height:3.216em"></span><span style="height:1.216em;width:0.8889em"><svg xmlns="http://www.w3.org/2000/svg" width="0.8889em" height="1.216em" style="width:0.8889em" viewBox="0 0 888.89 1216" preserveAspectRatio="xMinYMin"><path d="M384 0 H504 V1216 H384z M384 0 H504 V1216 H384z"></path></svg></span></span><span style="top:-5.566em"><span class="pstrut" style="height:3.216em"></span><span class="delimsizinginner delim-size4"><span>⎧</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:2.75em"><span></span></span></span></span></span></span><span class="mord"><span class="mtable"><span class="col-align-l"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:3.33em"><span style="top:-6.732em"><span class="pstrut" style="height:4.41em"></span><span class="mord"><span class="mop">max</span><span class="mopen">(</span><span class="mord mathnormal">i</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.05724em">j</span><span class="mclose">)</span></span></span><span style="top:-3.49em"><span class="pstrut" style="height:4.41em"></span><span class="mord"><span class="mop">min</span><span class="mspace" style="margin-right:0.1667em"></span><span class="minner"><span class="mopen"><span class="delimsizing mult"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:2.35em"><span style="top:-2.2em"><span class="pstrut" style="height:3.15em"></span><span class="delimsizinginner delim-size4"><span>⎩</span></span></span><span style="top:-2.192em"><span class="pstrut" style="height:3.15em"></span><span style="height:0.316em;width:0.8889em"><svg xmlns="http://www.w3.org/2000/svg" width="0.8889em" height="0.316em" style="width:0.8889em" viewBox="0 0 888.89 316" preserveAspectRatio="xMinYMin"><path d="M384 0 H504 V316 H384z M384 0 H504 V316 H384z"></path></svg></span></span><span style="top:-3.15em"><span class="pstrut" style="height:3.15em"></span><span class="delimsizinginner delim-size4"><span>⎨</span></span></span><span style="top:-4.292em"><span class="pstrut" style="height:3.15em"></span><span style="height:0.316em;width:0.8889em"><svg xmlns="http://www.w3.org/2000/svg" width="0.8889em" height="0.316em" style="width:0.8889em" viewBox="0 0 888.89 316" preserveAspectRatio="xMinYMin"><path d="M384 0 H504 V316 H384z M384 0 H504 V316 H384z"></path></svg></span></span><span style="top:-4.6em"><span class="pstrut" style="height:3.15em"></span><span class="delimsizinginner delim-size4"><span>⎧</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.85em"><span></span></span></span></span></span></span><span class="mord"><span class="mtable"><span class="col-align-l"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:2.41em"><span style="top:-4.41em"><span class="pstrut" style="height:3.008em"></span><span class="mord"><span class="mop"><span class="mop"><span class="mord mathrm" style="margin-right:0.01389em">lev</span></span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em"><span style="top:-2.55em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">a</span><span class="mpunct mtight">,</span><span class="mord mathnormal mtight">b</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal">i</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mord">1</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.05724em">j</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mord">1</span></span></span><span style="top:-2.97em"><span class="pstrut" style="height:3.008em"></span><span class="mord"><span class="mop"><span class="mop"><span class="mord mathrm" style="margin-right:0.01389em">lev</span></span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em"><span style="top:-2.55em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">a</span><span class="mpunct mtight">,</span><span class="mord mathnormal mtight">b</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal">i</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.05724em">j</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mord">1</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mord">1</span></span></span><span style="top:-1.53em"><span class="pstrut" style="height:3.008em"></span><span class="mord"><span class="mop"><span class="mop"><span class="mord mathrm" style="margin-right:0.01389em">lev</span></span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em"><span style="top:-2.55em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">a</span><span class="mpunct mtight">,</span><span class="mord mathnormal mtight">b</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal">i</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mord">1</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.05724em">j</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mord">1</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mord"><span class="mord mathbf">1</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3448em"><span style="top:-2.5198em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mtight"><span class="mord mathnormal mtight">a</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3281em"><span style="top:-2.357em;margin-left:0em;margin-right:0.0714em"><span class="pstrut" style="height:2.5em"></span><span class="sizing reset-size3 size1 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.143em"><span></span></span></span></span></span></span><span class="mrel mtight"><span class="mrel mtight"><span class="mord vbox mtight"><span class="thinbox mtight"><span class="rlap mtight"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em"></span><span class="inner"><span class="mord mtight"><span class="mrel mtight"></span></span></span><span class="fix"></span></span></span></span></span><span class="mrel mtight">=</span></span><span class="mord mtight"><span class="mord mathnormal mtight">b</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3281em"><span style="top:-2.357em;margin-left:0em;margin-right:0.0714em"><span class="pstrut" style="height:2.5em"></span><span class="sizing reset-size3 size1 mtight"><span class="mord mathnormal mtight" style="margin-right:0.05724em">j</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2819em"><span></span></span></span></span></span></span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.3775em"><span></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.91em"><span></span></span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:2.83em"><span></span></span></span></span></span><span class="arraycolsep" style="width:1em"></span><span class="col-align-l"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:3.33em"><span style="top:-6.732em"><span class="pstrut" style="height:4.41em"></span><span class="mord"><span class="mord text"><span class="mord">if&nbsp;</span></span><span class="mspace" style="margin-right:0.1667em"></span><span class="mop">min</span><span class="mopen">(</span><span class="mord mathnormal">i</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.05724em">j</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mord">0</span><span class="mpunct">,</span></span></span><span style="top:-3.49em"><span class="pstrut" style="height:4.41em"></span><span class="mord"><span class="mord text"><span class="mord">otherwise.</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:2.83em"><span></span></span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span>
<p>Treat Levenshtein distance as our relevance measure and the goal becomes: for a
given input, find the closest terms in the dictionary.</p>
<p>The classic <a href="https://en.wikipedia.org/wiki/Wagner%E2%80%93Fischer_algorithm" target="_blank" rel="noopener noreferrer">Wagner–Fischer</a>
algorithm computes this with dynamic programming in <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>O</mi><mo stretchy="false">(</mo><mi mathvariant="normal">∣</mi><mi>a</mi><mi mathvariant="normal">∣</mi><mo>⋅</mo><mi mathvariant="normal">∣</mi><mi>b</mi><mi mathvariant="normal">∣</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">O(|a| \cdot |b|)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal" style="margin-right:0.02778em">O</span><span class="mopen">(</span><span class="mord">∣</span><span class="mord mathnormal">a</span><span class="mord">∣</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord">∣</span><span class="mord mathnormal">b</span><span class="mord">∣</span><span class="mclose">)</span></span></span></span> time, with
memory linear in the shorter string if you keep one row at a time. That's fine for
comparing two words and hopeless at <a href="https://www.youtube.com/watch?v=b2F-DItXtZs" target="_blank" rel="noopener noreferrer">"web
scale"</a>, where a real dictionary holds hundreds of thousands of terms and you'd have
to run it against every single one.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-automaton-trick">The automaton trick<a href="https://serenedb.com/blog/fuzzy-search-deep-dive#the-automaton-trick" class="hash-link" aria-label="Direct link to The automaton trick" title="Direct link to The automaton trick">​</a></h3>
<p>In 2002, Klaus U. Schulz and Stoyan Mihov published a
<a href="https://doi.org/10.1007/s10032-002-0082-8" target="_blank" rel="noopener noreferrer">beautiful result</a>:
for any fixed distance <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mord mathnormal">n</span></span></span></span> and input word <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>W</mi></mrow><annotation encoding="application/x-tex">W</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.13889em">W</span></span></span></span> of length <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>N</mi></mrow><annotation encoding="application/x-tex">N</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.10903em">N</span></span></span></span>, you can build a
deterministic automaton <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>A</mi><mo stretchy="false">(</mo><mi>W</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">A(W)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal">A</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mclose">)</span></span></span></span> that accepts <em>every</em> string within Levenshtein
distance <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mord mathnormal">n</span></span></span></span> of <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>W</mi></mrow><annotation encoding="application/x-tex">W</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.13889em">W</span></span></span></span> and you can build it in time and space linear in <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>N</mi></mrow><annotation encoding="application/x-tex">N</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.10903em">N</span></span></span></span>.</p>
<p>Once you have <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>A</mi><mo stretchy="false">(</mo><mi>W</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">A(W)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal">A</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mclose">)</span></span></span></span>, you intersect it with the term dictionary. If the dictionary
is a trie (or an FST), the automaton walks the tree and prunes entire subtrees the
moment they can no longer lead to an accepting state. Say the dictionary holds
<code>avocado</code>, <code>avalon</code>, <code>avalanche</code> and <code>cargo</code>:</p>
<div data-email-img="/img/email/mermaid/2026-08-05-fuzzy-search-deep-dive-1.png"></div>
<p>Searching for terms within distance 1 of <code>kargo</code>, the automaton never descends
into the <code>av-</code> subtree: aligning the query's <code>k</code> with the dictionary's <code>a</code> already
costs one edit and the next character (<code>a</code> against <code>v</code>) forces a second, which
exceeds the limit of 1. The whole left branch is pruned and only <code>cargo</code> survives. We
touch a handful of nodes instead of scoring the entire dictionary.</p>
<p>So the real task is:</p>
<blockquote>
<p>Given an input word <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>W</mi></mrow><annotation encoding="application/x-tex">W</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.13889em">W</span></span></span></span> of length <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>N</mi></mrow><annotation encoding="application/x-tex">N</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.10903em">N</span></span></span></span> and a maximum edit distance <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mord mathnormal">n</span></span></span></span>, build a
deterministic finite automaton (DFA) that accepts a word <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>V</mi></mrow><annotation encoding="application/x-tex">V</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.22222em">V</span></span></span></span> <strong>iff</strong>
<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">lev</mi><mo>⁡</mo><mo stretchy="false">(</mo><mi>W</mi><mo separator="true">,</mo><mi>V</mi><mo stretchy="false">)</mo><mo>≤</mo><mi>n</mi></mrow><annotation encoding="application/x-tex">\operatorname{lev}(W, V) \le n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mop"><span class="mord mathrm" style="margin-right:0.01389em">lev</span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.22222em">V</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">≤</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mord mathnormal">n</span></span></span></span>.</p>
</blockquote>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="building-it-up-from-an-nfa">Building it up from an NFA<a href="https://serenedb.com/blog/fuzzy-search-deep-dive#building-it-up-from-an-nfa" class="hash-link" aria-label="Direct link to Building it up from an NFA" title="Direct link to Building it up from an NFA">​</a></h3>
<p>It's easier to first draw a <em>non-deterministic</em> finite automaton (NFA) and worry
about determinizing later. Here is the NFA for <code>foobar</code> at distance 1:</p>
<p><img decoding="async" loading="lazy" alt="The Levenshtein NFA for the word &amp;quot;foobar&amp;quot; at maximum edit distance 1. Each state is written as an index (0–6, its position in the word) on one of two lanes (e = number of edits spent so far). Horizontal edges consume a correct character; the other three families each spend one edit. Double-ringed states are accepting." src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA4MDAgMzcwIiBmb250LWZhbWlseT0idWktbW9ub3NwYWNlLCAnSmV0QnJhaW5zIE1vbm8nLCBNZW5sbywgQ29uc29sYXMsIG1vbm9zcGFjZSIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJMZXZlbnNodGVpbiBhdXRvbWF0b24gZm9yIGZvb2JhciwgbWF4IGVkaXQgZGlzdGFuY2UgMSI+CjxtYXJrZXIgaWQ9ImFoLW1hdGNoIiB2aWV3Qm94PSIwIDAgMTAgMTAiIHJlZlg9IjkiIHJlZlk9IjUiIG1hcmtlcldpZHRoPSI3IiBtYXJrZXJIZWlnaHQ9IjciIG9yaWVudD0iYXV0byI+PHBhdGggZD0iTTAgMEwxMCA1TDAgMTB6IiBmaWxsPSIjODk1YWY4Ii8+PC9tYXJrZXI+CjxtYXJrZXIgaWQ9ImFoLWlucyIgdmlld0JveD0iMCAwIDEwIDEwIiByZWZYPSI5IiByZWZZPSI1IiBtYXJrZXJXaWR0aD0iNyIgbWFya2VySGVpZ2h0PSI3IiBvcmllbnQ9ImF1dG8iPjxwYXRoIGQ9Ik0wIDBMMTAgNUwwIDEweiIgZmlsbD0iIzI1NjNlYiIvPjwvbWFya2VyPgo8bWFya2VyIGlkPSJhaC1zdWIiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iOSIgcmVmWT0iNSIgbWFya2VyV2lkdGg9IjciIG1hcmtlckhlaWdodD0iNyIgb3JpZW50PSJhdXRvIj48cGF0aCBkPSJNMCAwTDEwIDVMMCAxMHoiIGZpbGw9IiNkYjI3NzciLz48L21hcmtlcj4KPG1hcmtlciBpZD0iYWgtZGVsIiB2aWV3Qm94PSIwIDAgMTAgMTAiIHJlZlg9IjkiIHJlZlk9IjUiIG1hcmtlcldpZHRoPSI3IiBtYXJrZXJIZWlnaHQ9IjciIG9yaWVudD0iYXV0byI+PHBhdGggZD0iTTAgMEwxMCA1TDAgMTB6IiBmaWxsPSIjMDU5NjY5Ii8+PC9tYXJrZXI+CjxsaW5lIHgxPSI4MC4wIiB5MT0iMjUzLjAiIHgyPSI4MC4wIiB5Mj0iMTM3LjAiIHN0cm9rZT0iIzI1NjNlYiIgc3Ryb2tlLXdpZHRoPSIxLjYiIHN0cm9rZS1kYXNoYXJyYXk9IjUgNCIgb3BhY2l0eT0iMC41NSIgbWFya2VyLWVuZD0idXJsKCNhaC1pbnMpIi8+CjxsaW5lIHgxPSIxODUuMCIgeTE9IjI1My4wIiB4Mj0iMTg1LjAiIHkyPSIxMzcuMCIgc3Ryb2tlPSIjMjU2M2ViIiBzdHJva2Utd2lkdGg9IjEuNiIgc3Ryb2tlLWRhc2hhcnJheT0iNSA0IiBvcGFjaXR5PSIwLjU1IiBtYXJrZXItZW5kPSJ1cmwoI2FoLWlucykiLz4KPGxpbmUgeDE9IjI5MC4wIiB5MT0iMjUzLjAiIHgyPSIyOTAuMCIgeTI9IjEzNy4wIiBzdHJva2U9IiMyNTYzZWIiIHN0cm9rZS13aWR0aD0iMS42IiBzdHJva2UtZGFzaGFycmF5PSI1IDQiIG9wYWNpdHk9IjAuNTUiIG1hcmtlci1lbmQ9InVybCgjYWgtaW5zKSIvPgo8bGluZSB4MT0iMzk1LjAiIHkxPSIyNTMuMCIgeDI9IjM5NS4wIiB5Mj0iMTM3LjAiIHN0cm9rZT0iIzI1NjNlYiIgc3Ryb2tlLXdpZHRoPSIxLjYiIHN0cm9rZS1kYXNoYXJyYXk9IjUgNCIgb3BhY2l0eT0iMC41NSIgbWFya2VyLWVuZD0idXJsKCNhaC1pbnMpIi8+CjxsaW5lIHgxPSI1MDAuMCIgeTE9IjI1My4wIiB4Mj0iNTAwLjAiIHkyPSIxMzcuMCIgc3Ryb2tlPSIjMjU2M2ViIiBzdHJva2Utd2lkdGg9IjEuNiIgc3Ryb2tlLWRhc2hhcnJheT0iNSA0IiBvcGFjaXR5PSIwLjU1IiBtYXJrZXItZW5kPSJ1cmwoI2FoLWlucykiLz4KPGxpbmUgeDE9IjYwNS4wIiB5MT0iMjUzLjAiIHgyPSI2MDUuMCIgeTI9IjEzNy4wIiBzdHJva2U9IiMyNTYzZWIiIHN0cm9rZS13aWR0aD0iMS42IiBzdHJva2UtZGFzaGFycmF5PSI1IDQiIG9wYWNpdHk9IjAuNTUiIG1hcmtlci1lbmQ9InVybCgjYWgtaW5zKSIvPgo8bGluZSB4MT0iNzEwLjAiIHkxPSIyNTMuMCIgeDI9IjcxMC4wIiB5Mj0iMTM3LjAiIHN0cm9rZT0iIzI1NjNlYiIgc3Ryb2tlLXdpZHRoPSIxLjYiIHN0cm9rZS1kYXNoYXJyYXk9IjUgNCIgb3BhY2l0eT0iMC41NSIgbWFya2VyLWVuZD0idXJsKCNhaC1pbnMpIi8+CjxsaW5lIHgxPSI5Mi4xIiB5MT0iMjU2LjYiIHgyPSIxNzIuOSIgeTI9IjEzMy40IiBzdHJva2U9IiNkYjI3NzciIHN0cm9rZS13aWR0aD0iMS42IiBvcGFjaXR5PSIwLjU1IiBtYXJrZXItZW5kPSJ1cmwoI2FoLXN1YikiLz4KPGxpbmUgeDE9IjE5Ny4xIiB5MT0iMjU2LjYiIHgyPSIyNzcuOSIgeTI9IjEzMy40IiBzdHJva2U9IiNkYjI3NzciIHN0cm9rZS13aWR0aD0iMS42IiBvcGFjaXR5PSIwLjU1IiBtYXJrZXItZW5kPSJ1cmwoI2FoLXN1YikiLz4KPGxpbmUgeDE9IjMwMi4xIiB5MT0iMjU2LjYiIHgyPSIzODIuOSIgeTI9IjEzMy40IiBzdHJva2U9IiNkYjI3NzciIHN0cm9rZS13aWR0aD0iMS42IiBvcGFjaXR5PSIwLjU1IiBtYXJrZXItZW5kPSJ1cmwoI2FoLXN1YikiLz4KPGxpbmUgeDE9IjQwNy4xIiB5MT0iMjU2LjYiIHgyPSI0ODcuOSIgeTI9IjEzMy40IiBzdHJva2U9IiNkYjI3NzciIHN0cm9rZS13aWR0aD0iMS42IiBvcGFjaXR5PSIwLjU1IiBtYXJrZXItZW5kPSJ1cmwoI2FoLXN1YikiLz4KPGxpbmUgeDE9IjUxMi4xIiB5MT0iMjU2LjYiIHgyPSI1OTIuOSIgeTI9IjEzMy40IiBzdHJva2U9IiNkYjI3NzciIHN0cm9rZS13aWR0aD0iMS42IiBvcGFjaXR5PSIwLjU1IiBtYXJrZXItZW5kPSJ1cmwoI2FoLXN1YikiLz4KPGxpbmUgeDE9IjYxNy4xIiB5MT0iMjU2LjYiIHgyPSI2OTcuOSIgeTI9IjEzMy40IiBzdHJva2U9IiNkYjI3NzciIHN0cm9rZS13aWR0aD0iMS42IiBvcGFjaXR5PSIwLjU1IiBtYXJrZXItZW5kPSJ1cmwoI2FoLXN1YikiLz4KPGxpbmUgeDE9Ijk3LjUiIHkxPSIyNjEuNyIgeDI9IjI3Mi41IiB5Mj0iMTI4LjMiIHN0cm9rZT0iIzA1OTY2OSIgc3Ryb2tlLXdpZHRoPSIxLjYiIHN0cm9rZS1kYXNoYXJyYXk9IjIgNCIgb3BhY2l0eT0iMC41NSIgbWFya2VyLWVuZD0idXJsKCNhaC1kZWwpIi8+CjxsaW5lIHgxPSIyMDIuNSIgeTE9IjI2MS43IiB4Mj0iMzc3LjUiIHkyPSIxMjguMyIgc3Ryb2tlPSIjMDU5NjY5IiBzdHJva2Utd2lkdGg9IjEuNiIgc3Ryb2tlLWRhc2hhcnJheT0iMiA0IiBvcGFjaXR5PSIwLjU1IiBtYXJrZXItZW5kPSJ1cmwoI2FoLWRlbCkiLz4KPGxpbmUgeDE9IjMwNy41IiB5MT0iMjYxLjciIHgyPSI0ODIuNSIgeTI9IjEyOC4zIiBzdHJva2U9IiMwNTk2NjkiIHN0cm9rZS13aWR0aD0iMS42IiBzdHJva2UtZGFzaGFycmF5PSIyIDQiIG9wYWNpdHk9IjAuNTUiIG1hcmtlci1lbmQ9InVybCgjYWgtZGVsKSIvPgo8bGluZSB4MT0iNDEyLjUiIHkxPSIyNjEuNyIgeDI9IjU4Ny41IiB5Mj0iMTI4LjMiIHN0cm9rZT0iIzA1OTY2OSIgc3Ryb2tlLXdpZHRoPSIxLjYiIHN0cm9rZS1kYXNoYXJyYXk9IjIgNCIgb3BhY2l0eT0iMC41NSIgbWFya2VyLWVuZD0idXJsKCNhaC1kZWwpIi8+CjxsaW5lIHgxPSI1MTcuNSIgeTE9IjI2MS43IiB4Mj0iNjkyLjUiIHkyPSIxMjguMyIgc3Ryb2tlPSIjMDU5NjY5IiBzdHJva2Utd2lkdGg9IjEuNiIgc3Ryb2tlLWRhc2hhcnJheT0iMiA0IiBvcGFjaXR5PSIwLjU1IiBtYXJrZXItZW5kPSJ1cmwoI2FoLWRlbCkiLz4KPGxpbmUgeDE9IjEwMi4wIiB5MT0iMTE1LjAiIHgyPSIxNjMuMCIgeTI9IjExNS4wIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMi40IiBvcGFjaXR5PSIxLjAiIG1hcmtlci1lbmQ9InVybCgjYWgtbWF0Y2gpIi8+Cjx0ZXh0IHg9IjEzMi41IiB5PSIxMDYuMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzg5NWFmOCIgZm9udC1zaXplPSIxNSIgZm9udC13ZWlnaHQ9IjcwMCI+RjwvdGV4dD4KPGxpbmUgeDE9IjIwNy4wIiB5MT0iMTE1LjAiIHgyPSIyNjguMCIgeTI9IjExNS4wIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMi40IiBvcGFjaXR5PSIxLjAiIG1hcmtlci1lbmQ9InVybCgjYWgtbWF0Y2gpIi8+Cjx0ZXh0IHg9IjIzNy41IiB5PSIxMDYuMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzg5NWFmOCIgZm9udC1zaXplPSIxNSIgZm9udC13ZWlnaHQ9IjcwMCI+TzwvdGV4dD4KPGxpbmUgeDE9IjMxMi4wIiB5MT0iMTE1LjAiIHgyPSIzNzMuMCIgeTI9IjExNS4wIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMi40IiBvcGFjaXR5PSIxLjAiIG1hcmtlci1lbmQ9InVybCgjYWgtbWF0Y2gpIi8+Cjx0ZXh0IHg9IjM0Mi41IiB5PSIxMDYuMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzg5NWFmOCIgZm9udC1zaXplPSIxNSIgZm9udC13ZWlnaHQ9IjcwMCI+TzwvdGV4dD4KPGxpbmUgeDE9IjQxNy4wIiB5MT0iMTE1LjAiIHgyPSI0NzguMCIgeTI9IjExNS4wIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMi40IiBvcGFjaXR5PSIxLjAiIG1hcmtlci1lbmQ9InVybCgjYWgtbWF0Y2gpIi8+Cjx0ZXh0IHg9IjQ0Ny41IiB5PSIxMDYuMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzg5NWFmOCIgZm9udC1zaXplPSIxNSIgZm9udC13ZWlnaHQ9IjcwMCI+QjwvdGV4dD4KPGxpbmUgeDE9IjUyMi4wIiB5MT0iMTE1LjAiIHgyPSI1ODMuMCIgeTI9IjExNS4wIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMi40IiBvcGFjaXR5PSIxLjAiIG1hcmtlci1lbmQ9InVybCgjYWgtbWF0Y2gpIi8+Cjx0ZXh0IHg9IjU1Mi41IiB5PSIxMDYuMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzg5NWFmOCIgZm9udC1zaXplPSIxNSIgZm9udC13ZWlnaHQ9IjcwMCI+QTwvdGV4dD4KPGxpbmUgeDE9IjYyNy4wIiB5MT0iMTE1LjAiIHgyPSI2ODguMCIgeTI9IjExNS4wIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMi40IiBvcGFjaXR5PSIxLjAiIG1hcmtlci1lbmQ9InVybCgjYWgtbWF0Y2gpIi8+Cjx0ZXh0IHg9IjY1Ny41IiB5PSIxMDYuMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzg5NWFmOCIgZm9udC1zaXplPSIxNSIgZm9udC13ZWlnaHQ9IjcwMCI+UjwvdGV4dD4KPGxpbmUgeDE9IjEwMi4wIiB5MT0iMjc1LjAiIHgyPSIxNjMuMCIgeTI9IjI3NS4wIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMi40IiBvcGFjaXR5PSIxLjAiIG1hcmtlci1lbmQ9InVybCgjYWgtbWF0Y2gpIi8+Cjx0ZXh0IHg9IjEzMi41IiB5PSIyNjYuMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzg5NWFmOCIgZm9udC1zaXplPSIxNSIgZm9udC13ZWlnaHQ9IjcwMCI+RjwvdGV4dD4KPGxpbmUgeDE9IjIwNy4wIiB5MT0iMjc1LjAiIHgyPSIyNjguMCIgeTI9IjI3NS4wIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMi40IiBvcGFjaXR5PSIxLjAiIG1hcmtlci1lbmQ9InVybCgjYWgtbWF0Y2gpIi8+Cjx0ZXh0IHg9IjIzNy41IiB5PSIyNjYuMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzg5NWFmOCIgZm9udC1zaXplPSIxNSIgZm9udC13ZWlnaHQ9IjcwMCI+TzwvdGV4dD4KPGxpbmUgeDE9IjMxMi4wIiB5MT0iMjc1LjAiIHgyPSIzNzMuMCIgeTI9IjI3NS4wIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMi40IiBvcGFjaXR5PSIxLjAiIG1hcmtlci1lbmQ9InVybCgjYWgtbWF0Y2gpIi8+Cjx0ZXh0IHg9IjM0Mi41IiB5PSIyNjYuMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzg5NWFmOCIgZm9udC1zaXplPSIxNSIgZm9udC13ZWlnaHQ9IjcwMCI+TzwvdGV4dD4KPGxpbmUgeDE9IjQxNy4wIiB5MT0iMjc1LjAiIHgyPSI0NzguMCIgeTI9IjI3NS4wIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMi40IiBvcGFjaXR5PSIxLjAiIG1hcmtlci1lbmQ9InVybCgjYWgtbWF0Y2gpIi8+Cjx0ZXh0IHg9IjQ0Ny41IiB5PSIyNjYuMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzg5NWFmOCIgZm9udC1zaXplPSIxNSIgZm9udC13ZWlnaHQ9IjcwMCI+QjwvdGV4dD4KPGxpbmUgeDE9IjUyMi4wIiB5MT0iMjc1LjAiIHgyPSI1ODMuMCIgeTI9IjI3NS4wIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMi40IiBvcGFjaXR5PSIxLjAiIG1hcmtlci1lbmQ9InVybCgjYWgtbWF0Y2gpIi8+Cjx0ZXh0IHg9IjU1Mi41IiB5PSIyNjYuMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzg5NWFmOCIgZm9udC1zaXplPSIxNSIgZm9udC13ZWlnaHQ9IjcwMCI+QTwvdGV4dD4KPGxpbmUgeDE9IjYyNy4wIiB5MT0iMjc1LjAiIHgyPSI2ODguMCIgeTI9IjI3NS4wIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMi40IiBvcGFjaXR5PSIxLjAiIG1hcmtlci1lbmQ9InVybCgjYWgtbWF0Y2gpIi8+Cjx0ZXh0IHg9IjY1Ny41IiB5PSIyNjYuMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzg5NWFmOCIgZm9udC1zaXplPSIxNSIgZm9udC13ZWlnaHQ9IjcwMCI+UjwvdGV4dD4KPGNpcmNsZSBjeD0iODAiIGN5PSIxMTUiIHI9IjIyIiBmaWxsPSIjZWRlOWZlIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMiIvPgo8dGV4dCB4PSI4MCIgeT0iMTIwLjAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM0YzFkOTUiIGZvbnQtc2l6ZT0iMTUiIGZvbnQtd2VpZ2h0PSI3MDAiPjA8L3RleHQ+CjxjaXJjbGUgY3g9IjgwIiBjeT0iMjc1IiByPSIyMiIgZmlsbD0iI2VkZTlmZSIgc3Ryb2tlPSIjODk1YWY4IiBzdHJva2Utd2lkdGg9IjIiLz4KPHRleHQgeD0iODAiIHk9IjI4MC4wIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNGMxZDk1IiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iNzAwIj4wPC90ZXh0Pgo8Y2lyY2xlIGN4PSIxODUiIGN5PSIxMTUiIHI9IjIyIiBmaWxsPSIjZWRlOWZlIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMiIvPgo8dGV4dCB4PSIxODUiIHk9IjEyMC4wIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNGMxZDk1IiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iNzAwIj4xPC90ZXh0Pgo8Y2lyY2xlIGN4PSIxODUiIGN5PSIyNzUiIHI9IjIyIiBmaWxsPSIjZWRlOWZlIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMiIvPgo8dGV4dCB4PSIxODUiIHk9IjI4MC4wIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNGMxZDk1IiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iNzAwIj4xPC90ZXh0Pgo8Y2lyY2xlIGN4PSIyOTAiIGN5PSIxMTUiIHI9IjIyIiBmaWxsPSIjZWRlOWZlIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMiIvPgo8dGV4dCB4PSIyOTAiIHk9IjEyMC4wIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNGMxZDk1IiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iNzAwIj4yPC90ZXh0Pgo8Y2lyY2xlIGN4PSIyOTAiIGN5PSIyNzUiIHI9IjIyIiBmaWxsPSIjZWRlOWZlIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMiIvPgo8dGV4dCB4PSIyOTAiIHk9IjI4MC4wIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNGMxZDk1IiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iNzAwIj4yPC90ZXh0Pgo8Y2lyY2xlIGN4PSIzOTUiIGN5PSIxMTUiIHI9IjIyIiBmaWxsPSIjZWRlOWZlIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMiIvPgo8dGV4dCB4PSIzOTUiIHk9IjEyMC4wIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNGMxZDk1IiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iNzAwIj4zPC90ZXh0Pgo8Y2lyY2xlIGN4PSIzOTUiIGN5PSIyNzUiIHI9IjIyIiBmaWxsPSIjZWRlOWZlIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMiIvPgo8dGV4dCB4PSIzOTUiIHk9IjI4MC4wIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNGMxZDk1IiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iNzAwIj4zPC90ZXh0Pgo8Y2lyY2xlIGN4PSI1MDAiIGN5PSIxMTUiIHI9IjIyIiBmaWxsPSIjZWRlOWZlIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMiIvPgo8dGV4dCB4PSI1MDAiIHk9IjEyMC4wIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNGMxZDk1IiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iNzAwIj40PC90ZXh0Pgo8Y2lyY2xlIGN4PSI1MDAiIGN5PSIyNzUiIHI9IjIyIiBmaWxsPSIjZWRlOWZlIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMiIvPgo8dGV4dCB4PSI1MDAiIHk9IjI4MC4wIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNGMxZDk1IiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iNzAwIj40PC90ZXh0Pgo8Y2lyY2xlIGN4PSI2MDUiIGN5PSIxMTUiIHI9IjIyIiBmaWxsPSIjZWRlOWZlIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMiIvPgo8dGV4dCB4PSI2MDUiIHk9IjEyMC4wIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNGMxZDk1IiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iNzAwIj41PC90ZXh0Pgo8Y2lyY2xlIGN4PSI2MDUiIGN5PSIyNzUiIHI9IjI3IiBmaWxsPSJub25lIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMiIvPgo8Y2lyY2xlIGN4PSI2MDUiIGN5PSIyNzUiIHI9IjIyIiBmaWxsPSIjZGRkNmZlIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMiIvPgo8dGV4dCB4PSI2MDUiIHk9IjI4MC4wIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNGMxZDk1IiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iNzAwIj41PC90ZXh0Pgo8Y2lyY2xlIGN4PSI3MTAiIGN5PSIxMTUiIHI9IjI3IiBmaWxsPSJub25lIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMiIvPgo8Y2lyY2xlIGN4PSI3MTAiIGN5PSIxMTUiIHI9IjIyIiBmaWxsPSIjZGRkNmZlIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMiIvPgo8dGV4dCB4PSI3MTAiIHk9IjEyMC4wIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNGMxZDk1IiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iNzAwIj42PC90ZXh0Pgo8Y2lyY2xlIGN4PSI3MTAiIGN5PSIyNzUiIHI9IjI3IiBmaWxsPSJub25lIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMiIvPgo8Y2lyY2xlIGN4PSI3MTAiIGN5PSIyNzUiIHI9IjIyIiBmaWxsPSIjZGRkNmZlIiBzdHJva2U9IiM4OTVhZjgiIHN0cm9rZS13aWR0aD0iMiIvPgo8dGV4dCB4PSI3MTAiIHk9IjI4MC4wIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNGMxZDk1IiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iNzAwIj42PC90ZXh0Pgo8dGV4dCB4PSIyNiIgeT0iMTIwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjOWFhMGE2IiBmb250LXNpemU9IjEyIj5lPTE8L3RleHQ+Cjx0ZXh0IHg9IjI2IiB5PSIyODAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM5YWEwYTYiIGZvbnQtc2l6ZT0iMTIiPmU9MDwvdGV4dD4KPGxpbmUgeDE9IjcwIiB5MT0iMzQ1IiB4Mj0iOTYiIHkyPSIzNDUiIHN0cm9rZT0iIzg5NWFmOCIgc3Ryb2tlLXdpZHRoPSIyLjQiLz4KPHRleHQgeD0iMTAyIiB5PSIzNDkiIGZpbGw9IiM5YWEwYTYiIGZvbnQtc2l6ZT0iMTIiPm1hdGNoIChXW2ldKTwvdGV4dD4KPGxpbmUgeDE9IjIxMC40IiB5MT0iMzQ1IiB4Mj0iMjM2LjQiIHkyPSIzNDUiIHN0cm9rZT0iIzI1NjNlYiIgc3Ryb2tlLXdpZHRoPSIyLjQiIHN0cm9rZS1kYXNoYXJyYXk9IjUgNCIvPgo8dGV4dCB4PSIyNDIuNCIgeT0iMzQ5IiBmaWxsPSIjOWFhMGE2IiBmb250LXNpemU9IjEyIj5pbnNlcnRpb248L3RleHQ+CjxsaW5lIHgxPSIzMjkuMiIgeTE9IjM0NSIgeDI9IjM1NS4yIiB5Mj0iMzQ1IiBzdHJva2U9IiNkYjI3NzciIHN0cm9rZS13aWR0aD0iMi40Ii8+Cjx0ZXh0IHg9IjM2MS4yIiB5PSIzNDkiIGZpbGw9IiM5YWEwYTYiIGZvbnQtc2l6ZT0iMTIiPnN1YnN0aXR1dGlvbjwvdGV4dD4KPGxpbmUgeDE9IjQ2OS42IiB5MT0iMzQ1IiB4Mj0iNDk1LjYiIHkyPSIzNDUiIHN0cm9rZT0iIzA1OTY2OSIgc3Ryb2tlLXdpZHRoPSIyLjQiIHN0cm9rZS1kYXNoYXJyYXk9IjIgNCIvPgo8dGV4dCB4PSI1MDEuNiIgeT0iMzQ5IiBmaWxsPSIjOWFhMGE2IiBmb250LXNpemU9IjEyIj5kZWxldGlvbjwvdGV4dD4KPC9zdmc+" width="800" height="370" class="img_ev3q"></p>
<p>Read it as a grid. The bottom lane (<code>e=0</code>) is "no edits spent yet"; the top lane
(<code>e=1</code>) is "one edit spent". Each state is an <em>(index, edits)</em> pair. Four families
of transitions leave a state <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mi>I</mi><mi>J</mi></msup></mrow><annotation encoding="application/x-tex">I^{J}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8413em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8413em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.09618em">J</span></span></span></span></span></span></span></span></span></span></span></span> (index <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>I</mi></mrow><annotation encoding="application/x-tex">I</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.07847em">I</span></span></span></span>, <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>J</mi></mrow><annotation encoding="application/x-tex">J</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.09618em">J</span></span></span></span> edits):</p>
<ul>
<li><strong>Match</strong>: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mi>I</mi><mi>J</mi></msup><mo>→</mo><mo stretchy="false">(</mo><mi>I</mi><mo lspace="0em" rspace="0em">+</mo><mn>1</mn><msup><mo stretchy="false">)</mo><mi>J</mi></msup></mrow><annotation encoding="application/x-tex">I^{J} \to (I{+}1)^{J}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8413em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8413em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.09618em">J</span></span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">→</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1.0913em;vertical-align:-0.25em"></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="mord"><span class="mord">+</span></span><span class="mord">1</span><span class="mclose"><span class="mclose">)</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8413em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.09618em">J</span></span></span></span></span></span></span></span></span></span></span></span>, consuming the correct next character of <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>W</mi></mrow><annotation encoding="application/x-tex">W</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.13889em">W</span></span></span></span>.
These are the horizontal edges.</li>
<li><strong>Insertion</strong>: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mi>I</mi><mi>J</mi></msup><mo>→</mo><msup><mi>I</mi><mrow><mi>J</mi><mo>+</mo><mn>1</mn></mrow></msup></mrow><annotation encoding="application/x-tex">I^{J} \to I^{J+1}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8413em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8413em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.09618em">J</span></span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">→</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:0.8413em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8413em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.09618em">J</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span></span></span></span></span></span></span></span>. An extra character in the candidate, so spend
one edit and stay at the same position.</li>
<li><strong>Substitution</strong>: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mi>I</mi><mi>J</mi></msup><mo>→</mo><mo stretchy="false">(</mo><mi>I</mi><mo lspace="0em" rspace="0em">+</mo><mn>1</mn><msup><mo stretchy="false">)</mo><mrow><mi>J</mi><mo>+</mo><mn>1</mn></mrow></msup></mrow><annotation encoding="application/x-tex">I^{J} \to (I{+}1)^{J+1}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8413em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8413em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.09618em">J</span></span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">→</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1.0913em;vertical-align:-0.25em"></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="mord"><span class="mord">+</span></span><span class="mord">1</span><span class="mclose"><span class="mclose">)</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8413em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.09618em">J</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span></span></span></span></span></span></span></span>. Wrong character, so spend one edit
and advance.</li>
<li><strong>Deletion</strong>: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mi>I</mi><mi>J</mi></msup><mo>→</mo><mo stretchy="false">(</mo><mi>I</mi><mo lspace="0em" rspace="0em">+</mo><mn>2</mn><msup><mo stretchy="false">)</mo><mrow><mi>J</mi><mo>+</mo><mn>1</mn></mrow></msup></mrow><annotation encoding="application/x-tex">I^{J} \to (I{+}2)^{J+1}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8413em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8413em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.09618em">J</span></span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">→</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1.0913em;vertical-align:-0.25em"></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="mord"><span class="mord">+</span></span><span class="mord">2</span><span class="mclose"><span class="mclose">)</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8413em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.09618em">J</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span></span></span></span></span></span></span></span>. A character of <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>W</mi></mrow><annotation encoding="application/x-tex">W</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.13889em">W</span></span></span></span> is missing from the
candidate, so skip it. (Deleting <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>K</mi></mrow><annotation encoding="application/x-tex">K</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.07153em">K</span></span></span></span> consecutive characters reaches
<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">(</mo><mi>I</mi><mo lspace="0em" rspace="0em">+</mo><mi>K</mi><mo lspace="0em" rspace="0em">+</mo><mn>1</mn><msup><mo stretchy="false">)</mo><mrow><mi>J</mi><mo>+</mo><mi>K</mi></mrow></msup></mrow><annotation encoding="application/x-tex">(I{+}K{+}1)^{J+K}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.0913em;vertical-align:-0.25em"></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="mord"><span class="mord">+</span></span><span class="mord mathnormal" style="margin-right:0.07153em">K</span><span class="mord"><span class="mord">+</span></span><span class="mord">1</span><span class="mclose"><span class="mclose">)</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8413em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.09618em">J</span><span class="mbin mtight">+</span><span class="mord mathnormal mtight" style="margin-right:0.07153em">K</span></span></span></span></span></span></span></span></span></span></span></span>.)</li>
</ul>
<p>The moment you spend an edit you move up a lane and at distance 1 the top lane
only has match edges left, because you're out of budget. That's why we say the flow
"transfers to the upper lane" as soon as an edit happens.</p>
<p>Raising the budget to 2 just adds a third lane and the deletion edges that skip
two characters at once (an <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mi>I</mi><mi>J</mi></msup><mo>→</mo><mo stretchy="false">(</mo><mi>I</mi><mo lspace="0em" rspace="0em">+</mo><mn>3</mn><msup><mo stretchy="false">)</mo><mrow><mi>J</mi><mo>+</mo><mn>2</mn></mrow></msup></mrow><annotation encoding="application/x-tex">I^{J} \to (I{+}3)^{J+2}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8413em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8413em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.09618em">J</span></span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">→</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1.0913em;vertical-align:-0.25em"></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="mord"><span class="mord">+</span></span><span class="mord">3</span><span class="mclose"><span class="mclose">)</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8413em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.09618em">J</span><span class="mbin mtight">+</span><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span> family). Each state gains
<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>O</mi><mo stretchy="false">(</mo><mi>n</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">O(n)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal" style="margin-right:0.02778em">O</span><span class="mopen">(</span><span class="mord mathnormal">n</span><span class="mclose">)</span></span></span></span> outgoing edges. This is the pattern we now need to bound.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="making-it-linear">Making it linear<a href="https://serenedb.com/blog/fuzzy-search-deep-dive#making-it-linear" class="hash-link" aria-label="Direct link to Making it linear" title="Direct link to Making it linear">​</a></h3>
<p>The naive powerset construction for the DFA gives <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>O</mi><mo stretchy="false">(</mo><msup><mn>2</mn><mrow><mo stretchy="false">(</mo><mi>n</mi><mo>+</mo><mn>1</mn><mo stretchy="false">)</mo><mi>N</mi></mrow></msup><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">O(2^{(n+1)N})</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.138em;vertical-align:-0.25em"></span><span class="mord mathnormal" style="margin-right:0.02778em">O</span><span class="mopen">(</span><span class="mord"><span class="mord">2</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.888em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mathnormal mtight">n</span><span class="mbin mtight">+</span><span class="mord mtight">1</span><span class="mclose mtight">)</span><span class="mord mathnormal mtight" style="margin-right:0.10903em">N</span></span></span></span></span></span></span></span></span><span class="mclose">)</span></span></span></span> states. It's clearly
far too loose given how regular the NFA is. Schulz and Mihov tightened it in three
steps:</p>
<ol>
<li><strong>Locality.</strong> From position <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>i</mi></mrow><annotation encoding="application/x-tex">i</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6595em"></span><span class="mord mathnormal">i</span></span></span></span> you can never reach past <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>i</mi><mo>+</mo><mi>n</mi></mrow><annotation encoding="application/x-tex">i+n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7429em;vertical-align:-0.0833em"></span><span class="mord mathnormal">i</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mord mathnormal">n</span></span></span></span> by inserting, nor
before <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>i</mi><mo>−</mo><mi>n</mi></mrow><annotation encoding="application/x-tex">i-n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7429em;vertical-align:-0.0833em"></span><span class="mord mathnormal">i</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mord mathnormal">n</span></span></span></span> by deleting. So at most <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>2</mn><mi>n</mi><mo>+</mo><mn>1</mn></mrow><annotation encoding="application/x-tex">2n+1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7278em;vertical-align:-0.0833em"></span><span class="mord">2</span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:0.6444em"></span><span class="mord">1</span></span></span></span> NFA states are ever "live" at a given
position and the powerset over them has <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mn>2</mn><mrow><mn>2</mn><mi>n</mi><mo>+</mo><mn>1</mn></mrow></msup></mrow><annotation encoding="application/x-tex">2^{2n+1}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8141em"></span><span class="mord"><span class="mord">2</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">2</span><span class="mord mathnormal mtight">n</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span></span></span></span></span></span></span></span> members. That alone drops us
to <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>O</mi><mo stretchy="false">(</mo><msup><mn>2</mn><mrow><mn>2</mn><mi>n</mi><mo>+</mo><mn>1</mn></mrow></msup><mi>N</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">O(2^{2n+1} N)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.0641em;vertical-align:-0.25em"></span><span class="mord mathnormal" style="margin-right:0.02778em">O</span><span class="mopen">(</span><span class="mord"><span class="mord">2</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">2</span><span class="mord mathnormal mtight">n</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span></span></span></span></span><span class="mord mathnormal" style="margin-right:0.10903em">N</span><span class="mclose">)</span></span></span></span> states.</li>
<li><strong>Subsumption.</strong> State <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mi>I</mi><mi>J</mi></msup></mrow><annotation encoding="application/x-tex">I^{J}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8413em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8413em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.09618em">J</span></span></span></span></span></span></span></span></span></span></span></span> subsumes any <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">(</mo><mi>I</mi><mo lspace="0em" rspace="0em">±</mo><mi>K</mi><msup><mo stretchy="false">)</mo><mrow><mi>J</mi><mo>+</mo><mi>K</mi></mrow></msup></mrow><annotation encoding="application/x-tex">(I{\pm}K)^{J+K}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.0913em;vertical-align:-0.25em"></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="mord"><span class="mord">±</span></span><span class="mord mathnormal" style="margin-right:0.07153em">K</span><span class="mclose"><span class="mclose">)</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8413em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.09618em">J</span><span class="mbin mtight">+</span><span class="mord mathnormal mtight" style="margin-right:0.07153em">K</span></span></span></span></span></span></span></span></span></span></span></span> with <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>K</mi><mo>≤</mo><mi>n</mi><mo>−</mo><mi>J</mi></mrow><annotation encoding="application/x-tex">K \le n - J</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8193em;vertical-align:-0.136em"></span><span class="mord mathnormal" style="margin-right:0.07153em">K</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">≤</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:0.6667em;vertical-align:-0.0833em"></span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.09618em">J</span></span></span></span>:
the other state sits up to <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>K</mi></mrow><annotation encoding="application/x-tex">K</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.07153em">K</span></span></span></span> positions away and paid <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>K</mi></mrow><annotation encoding="application/x-tex">K</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.07153em">K</span></span></span></span> extra edits to get
there, so everything it can still accept the cheaper state accepts too. Keeping
it around is pointless. This removes the exponential factor entirely: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>O</mi><mo stretchy="false">(</mo><msup><mi>n</mi><mn>2</mn></msup><mi>N</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">O(n^2 N)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.0641em;vertical-align:-0.25em"></span><span class="mord mathnormal" style="margin-right:0.02778em">O</span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal">n</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mord mathnormal" style="margin-right:0.10903em">N</span><span class="mclose">)</span></span></span></span>
states.</li>
<li><strong>Parametrization.</strong> The transitions depend only on the <em>distribution</em> of the
current character relative to position <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>I</mi></mrow><annotation encoding="application/x-tex">I</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.07847em">I</span></span></span></span>, not on <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>I</mi></mrow><annotation encoding="application/x-tex">I</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.07847em">I</span></span></span></span> itself. That kills the
dependency on <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>N</mi></mrow><annotation encoding="application/x-tex">N</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.10903em">N</span></span></span></span>, the last thing standing between us and linear time.</li>
</ol>
<p>The key object in step 3 is the <strong>characteristic vector</strong>. For a character <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>c</mi></mrow><annotation encoding="application/x-tex">c</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mord mathnormal">c</span></span></span></span>,
<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>χ</mi><mo stretchy="false">(</mo><mi>c</mi><mo separator="true">,</mo><mi>W</mi><mo separator="true">,</mo><mi>I</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">\chi(c, W, I)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal">χ</span><span class="mopen">(</span><span class="mord mathnormal">c</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="mclose">)</span></span></span></span> is a bit set of length <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>min</mi><mo>⁡</mo><mo stretchy="false">(</mo><mn>2</mn><mi>n</mi><mo>+</mo><mn>1</mn><mo separator="true">,</mo><mtext>&nbsp;</mtext><mi mathvariant="normal">∣</mi><mi>W</mi><mi mathvariant="normal">∣</mi><mo>−</mo><mi>I</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">\min(2n+1,\ |W|-I)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mop">min</span><span class="mopen">(</span><span class="mord">2</span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord">1</span><span class="mpunct">,</span><span class="mspace">&nbsp;</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mord">∣</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="mclose">)</span></span></span></span> whose bit <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>k</mi></mrow><annotation encoding="application/x-tex">k</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em"></span><span class="mord mathnormal" style="margin-right:0.03148em">k</span></span></span></span>, counting
from 0, is 1 iff <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>W</mi><mrow><mi>I</mi><mo>+</mo><mi>k</mi></mrow></msub><mo>=</mo><mi>c</mi></mrow><annotation encoding="application/x-tex">W_{I+k} = c</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8917em;vertical-align:-0.2083em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em"><span style="top:-2.55em;margin-left:-0.1389em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mathnormal mtight" style="margin-right:0.03148em">k</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mord mathnormal">c</span></span></span></span>. Index <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>I</mi></mrow><annotation encoding="application/x-tex">I</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.07847em">I</span></span></span></span> means <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>I</mi></mrow><annotation encoding="application/x-tex">I</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.07847em">I</span></span></span></span> characters consumed, so <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>W</mi><mi>I</mi></msub></mrow><annotation encoding="application/x-tex">W_{I}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.1389em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span></span>
is the character up next and bit 0 tells you whether the match transition is
available at all. It answers "where, in the next few characters of <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>W</mi></mrow><annotation encoding="application/x-tex">W</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.13889em">W</span></span></span></span>, does <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>c</mi></mrow><annotation encoding="application/x-tex">c</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mord mathnormal">c</span></span></span></span>
appear?" For <code>foo</code>:</p>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>χ</mi><mo stretchy="false">(</mo><mtext>f</mtext><mo separator="true">,</mo><mtext>foo</mtext><mo separator="true">,</mo><mn>0</mn><mo stretchy="false">)</mo><mo>=</mo><mo stretchy="false">⟨</mo><mn>1</mn><mo separator="true">,</mo><mn>0</mn><mo separator="true">,</mo><mn>0</mn><mo stretchy="false">⟩</mo><mspace width="2em"></mspace><mi>χ</mi><mo stretchy="false">(</mo><mtext>o</mtext><mo separator="true">,</mo><mtext>foo</mtext><mo separator="true">,</mo><mn>0</mn><mo stretchy="false">)</mo><mo>=</mo><mo stretchy="false">⟨</mo><mn>0</mn><mo separator="true">,</mo><mn>1</mn><mo separator="true">,</mo><mn>1</mn><mo stretchy="false">⟩</mo><mspace width="2em"></mspace><mi>χ</mi><mo stretchy="false">(</mo><mtext>o</mtext><mo separator="true">,</mo><mtext>foo</mtext><mo separator="true">,</mo><mn>2</mn><mo stretchy="false">)</mo><mo>=</mo><mo stretchy="false">⟨</mo><mn>1</mn><mo stretchy="false">⟩</mo></mrow><annotation encoding="application/x-tex">\chi(\text{f}, \text{foo}, 0) = \langle 1,0,0\rangle \qquad
\chi(\text{o}, \text{foo}, 0) = \langle 0,1,1\rangle \qquad
\chi(\text{o}, \text{foo}, 2) = \langle 1\rangle</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal">χ</span><span class="mopen">(</span><span class="mord text"><span class="mord">f</span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord text"><span class="mord">foo</span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">0</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mopen">⟨</span><span class="mord">1</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">0</span><span class="mclose">⟩</span><span class="mspace" style="margin-right:2em"></span><span class="mord mathnormal">χ</span><span class="mopen">(</span><span class="mord text"><span class="mord">o</span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord text"><span class="mord">foo</span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">0</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mopen">⟨</span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">1</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">1</span><span class="mclose">⟩</span><span class="mspace" style="margin-right:2em"></span><span class="mord mathnormal">χ</span><span class="mopen">(</span><span class="mord text"><span class="mord">o</span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord text"><span class="mord">foo</span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">2</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mopen">⟨</span><span class="mord">1</span><span class="mclose">⟩</span></span></span></span></span>
<p>The last one is a single bit because only one character of <code>foo</code> is left to look at.</p>
<p>Because only <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>2</mn><mi>n</mi><mo>+</mo><mn>1</mn></mrow><annotation encoding="application/x-tex">2n+1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7278em;vertical-align:-0.0833em"></span><span class="mord">2</span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:0.6444em"></span><span class="mord">1</span></span></span></span> states matter at any position, we can enumerate all <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mn>2</mn><mrow><mn>2</mn><mi>n</mi><mo>+</mo><mn>1</mn></mrow></msup></mrow><annotation encoding="application/x-tex">2^{2n+1}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8141em"></span><span class="mord"><span class="mord">2</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">2</span><span class="mord mathnormal mtight">n</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span></span></span></span></span></span></span></span>
possible character distributions and, for each, list which states become reachable.
Doing that for distance 1 turns up just <strong>five</strong> distinct reachable state sets, the
<em>parametric states</em>:</p>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mtable rowspacing="0.25em" columnalign="right left right" columnspacing="0em 1em"><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mi mathvariant="normal">∅</mi></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>=</mo><mo stretchy="false">{</mo><mo stretchy="false">}</mo></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><msub><mi>A</mi><mi>I</mi></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>=</mo><mo stretchy="false">{</mo><msup><mi>I</mi><mn>0</mn></msup><mo stretchy="false">}</mo><mo separator="true">,</mo></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mn>0</mn><mo>≤</mo><mi>I</mi><mo>≤</mo><mi mathvariant="normal">∣</mi><mi>W</mi><mi mathvariant="normal">∣</mi></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><msub><mi>B</mi><mi>I</mi></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>=</mo><mo stretchy="false">{</mo><msup><mi>I</mi><mn>1</mn></msup><mo stretchy="false">}</mo><mo separator="true">,</mo></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mn>0</mn><mo>≤</mo><mi>I</mi><mo>≤</mo><mi mathvariant="normal">∣</mi><mi>W</mi><mi mathvariant="normal">∣</mi></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><msub><mi>C</mi><mi>I</mi></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>=</mo><mo stretchy="false">{</mo><msup><mi>I</mi><mn>1</mn></msup><mo separator="true">,</mo><mo stretchy="false">(</mo><mi>I</mi><mo lspace="0em" rspace="0em">+</mo><mn>1</mn><msup><mo stretchy="false">)</mo><mn>1</mn></msup><mo stretchy="false">}</mo><mo separator="true">,</mo></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mn>0</mn><mo>≤</mo><mi>I</mi><mo>≤</mo><mi mathvariant="normal">∣</mi><mi>W</mi><mi mathvariant="normal">∣</mi><mo>−</mo><mn>1</mn></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><msub><mi>D</mi><mi>I</mi></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>=</mo><mo stretchy="false">{</mo><msup><mi>I</mi><mn>1</mn></msup><mo separator="true">,</mo><mo stretchy="false">(</mo><mi>I</mi><mo lspace="0em" rspace="0em">+</mo><mn>2</mn><msup><mo stretchy="false">)</mo><mn>1</mn></msup><mo stretchy="false">}</mo><mo separator="true">,</mo></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mn>0</mn><mo>≤</mo><mi>I</mi><mo>≤</mo><mi mathvariant="normal">∣</mi><mi>W</mi><mi mathvariant="normal">∣</mi><mo>−</mo><mn>2</mn></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><msub><mi>E</mi><mi>I</mi></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>=</mo><mo stretchy="false">{</mo><msup><mi>I</mi><mn>1</mn></msup><mo separator="true">,</mo><mo stretchy="false">(</mo><mi>I</mi><mo lspace="0em" rspace="0em">+</mo><mn>1</mn><msup><mo stretchy="false">)</mo><mn>1</mn></msup><mo separator="true">,</mo><mo stretchy="false">(</mo><mi>I</mi><mo lspace="0em" rspace="0em">+</mo><mn>2</mn><msup><mo stretchy="false">)</mo><mn>1</mn></msup><mo stretchy="false">}</mo><mo separator="true">,</mo></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mn>0</mn><mo>≤</mo><mi>I</mi><mo>≤</mo><mi mathvariant="normal">∣</mi><mi>W</mi><mi mathvariant="normal">∣</mi><mo>−</mo><mn>2</mn></mrow></mstyle></mtd></mtr></mtable><annotation encoding="application/x-tex">\begin{aligned}
\varnothing &amp;= \{\} \\
A_I &amp;= \{I^{0}\}, &amp; 0 \le I \le |W| \\
B_I &amp;= \{I^{1}\}, &amp; 0 \le I \le |W| \\
C_I &amp;= \{I^{1}, (I{+}1)^{1}\}, &amp; 0 \le I \le |W|-1 \\
D_I &amp;= \{I^{1}, (I{+}2)^{1}\}, &amp; 0 \le I \le |W|-2 \\
E_I &amp;= \{I^{1}, (I{+}1)^{1}, (I{+}2)^{1}\}, &amp; 0 \le I \le |W|-2
\end{aligned}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:9.1205em;vertical-align:-4.3103em"></span><span class="mord"><span class="mtable"><span class="col-align-r"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:4.8103em"><span style="top:-6.9703em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord amsrm">∅</span></span></span><span style="top:-5.4462em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal">A</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span><span style="top:-3.9221em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0502em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span><span style="top:-2.3979em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.07153em">C</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0715em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span><span style="top:-0.8738em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.02778em">D</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0278em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span><span style="top:0.6503em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05764em">E</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0576em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:4.3103em"><span></span></span></span></span></span><span class="col-align-l"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:4.8103em"><span style="top:-6.9703em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mopen">{</span><span class="mclose">}</span></span></span><span style="top:-5.4462em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mopen">{</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em"><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">0</span></span></span></span></span></span></span></span></span><span class="mclose">}</span><span class="mpunct">,</span></span></span><span style="top:-3.9221em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mopen">{</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em"><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span></span></span></span></span></span></span></span></span><span class="mclose">}</span><span class="mpunct">,</span></span></span><span style="top:-2.3979em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mopen">{</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em"><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span></span></span></span></span></span></span></span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="mord"><span class="mord">+</span></span><span class="mord">1</span><span class="mclose"><span class="mclose">)</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em"><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span></span></span></span></span></span></span></span></span><span class="mclose">}</span><span class="mpunct">,</span></span></span><span style="top:-0.8738em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mopen">{</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em"><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span></span></span></span></span></span></span></span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="mord"><span class="mord">+</span></span><span class="mord">2</span><span class="mclose"><span class="mclose">)</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em"><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span></span></span></span></span></span></span></span></span><span class="mclose">}</span><span class="mpunct">,</span></span></span><span style="top:0.6503em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mopen">{</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em"><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span></span></span></span></span></span></span></span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="mord"><span class="mord">+</span></span><span class="mord">1</span><span class="mclose"><span class="mclose">)</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em"><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span></span></span></span></span></span></span></span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="mord"><span class="mord">+</span></span><span class="mord">2</span><span class="mclose"><span class="mclose">)</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em"><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span></span></span></span></span></span></span></span></span><span class="mclose">}</span><span class="mpunct">,</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:4.3103em"><span></span></span></span></span></span><span class="arraycolsep" style="width:1em"></span><span class="col-align-r"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:3.3103em"><span style="top:-5.4462em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord">0</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">≤</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">≤</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mord">∣</span></span></span><span style="top:-3.9221em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord">0</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">≤</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">≤</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mord">∣</span></span></span><span style="top:-2.3979em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord">0</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">≤</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">≤</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mord">∣</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mord">1</span></span></span><span style="top:-0.8738em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord">0</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">≤</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">≤</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mord">∣</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mord">2</span></span></span><span style="top:0.6503em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord">0</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">≤</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">≤</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mord">∣</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mord">2</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:4.3103em"><span></span></span></span></span></span></span></span></span></span></span></span>
<p>Now the DFA transition function <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">Δ</mi></mrow><annotation encoding="application/x-tex">\Delta</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord">Δ</span></span></span></span> is a small lookup keyed by
(parametric state, characteristic vector). For distance 1 it fits in one table:</p>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mtable rowspacing="0.16em" columnalign="center center center center center center" columnlines="solid none none none none" columnspacing="1em" rowlines="solid none none none none none none none"><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><mi>χ</mi><mo stretchy="false">(</mo><mi>c</mi><mo separator="true">,</mo><mi>W</mi><mo separator="true">,</mo><mi>I</mi><mo stretchy="false">)</mo></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>A</mi><mi>I</mi></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>B</mi><mi>I</mi></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>C</mi><mi>I</mi></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>D</mi><mi>I</mi></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>E</mi><mi>I</mi></msub></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><mo stretchy="false">⟨</mo><mn>0</mn><mo separator="true">,</mo><mn>0</mn><mo separator="true">,</mo><mn>0</mn><mo stretchy="false">⟩</mo></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>C</mi><mi>I</mi></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mi mathvariant="normal">∅</mi></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mi mathvariant="normal">∅</mi></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mi mathvariant="normal">∅</mi></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mi mathvariant="normal">∅</mi></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><mo stretchy="false">⟨</mo><mn>0</mn><mo separator="true">,</mo><mn>0</mn><mo separator="true">,</mo><mn>1</mn><mo stretchy="false">⟩</mo></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>C</mi><mi>I</mi></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mi mathvariant="normal">∅</mi></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mi mathvariant="normal">∅</mi></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>B</mi><mrow><mi>I</mi><mo>+</mo><mn>3</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>B</mi><mrow><mi>I</mi><mo>+</mo><mn>3</mn></mrow></msub></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><mo stretchy="false">⟨</mo><mn>0</mn><mo separator="true">,</mo><mn>1</mn><mo separator="true">,</mo><mn>0</mn><mo stretchy="false">⟩</mo></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>E</mi><mi>I</mi></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mi mathvariant="normal">∅</mi></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>B</mi><mrow><mi>I</mi><mo>+</mo><mn>2</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mi mathvariant="normal">∅</mi></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>B</mi><mrow><mi>I</mi><mo>+</mo><mn>2</mn></mrow></msub></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><mo stretchy="false">⟨</mo><mn>0</mn><mo separator="true">,</mo><mn>1</mn><mo separator="true">,</mo><mn>1</mn><mo stretchy="false">⟩</mo></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>E</mi><mi>I</mi></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mi mathvariant="normal">∅</mi></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>B</mi><mrow><mi>I</mi><mo>+</mo><mn>2</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>B</mi><mrow><mi>I</mi><mo>+</mo><mn>3</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>C</mi><mrow><mi>I</mi><mo>+</mo><mn>2</mn></mrow></msub></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><mo stretchy="false">⟨</mo><mn>1</mn><mo separator="true">,</mo><mn>0</mn><mo separator="true">,</mo><mn>0</mn><mo stretchy="false">⟩</mo></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>A</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>B</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>B</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>B</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>B</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><mo stretchy="false">⟨</mo><mn>1</mn><mo separator="true">,</mo><mn>0</mn><mo separator="true">,</mo><mn>1</mn><mo stretchy="false">⟩</mo></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>A</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>B</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>B</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>D</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>D</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><mo stretchy="false">⟨</mo><mn>1</mn><mo separator="true">,</mo><mn>1</mn><mo separator="true">,</mo><mn>0</mn><mo stretchy="false">⟩</mo></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>A</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>B</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>C</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>B</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>C</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><mo stretchy="false">⟨</mo><mn>1</mn><mo separator="true">,</mo><mn>1</mn><mo separator="true">,</mo><mn>1</mn><mo stretchy="false">⟩</mo></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>A</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>B</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>C</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>D</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><msub><mi>E</mi><mrow><mi>I</mi><mo>+</mo><mn>1</mn></mrow></msub></mstyle></mtd></mtr></mtable><annotation encoding="application/x-tex">\begin{array}{c|ccccc}
\chi(c,W,I) &amp; A_I &amp; B_I &amp; C_I &amp; D_I &amp; E_I \\ \hline
\langle 0,0,0\rangle &amp; C_I &amp; \varnothing &amp; \varnothing &amp; \varnothing &amp; \varnothing \\
\langle 0,0,1\rangle &amp; C_I &amp; \varnothing &amp; \varnothing &amp; B_{I+3} &amp; B_{I+3} \\
\langle 0,1,0\rangle &amp; E_I &amp; \varnothing &amp; B_{I+2} &amp; \varnothing &amp; B_{I+2} \\
\langle 0,1,1\rangle &amp; E_I &amp; \varnothing &amp; B_{I+2} &amp; B_{I+3} &amp; C_{I+2} \\
\langle 1,0,0\rangle &amp; A_{I+1} &amp; B_{I+1} &amp; B_{I+1} &amp; B_{I+1} &amp; B_{I+1} \\
\langle 1,0,1\rangle &amp; A_{I+1} &amp; B_{I+1} &amp; B_{I+1} &amp; D_{I+1} &amp; D_{I+1} \\
\langle 1,1,0\rangle &amp; A_{I+1} &amp; B_{I+1} &amp; C_{I+1} &amp; B_{I+1} &amp; C_{I+1} \\
\langle 1,1,1\rangle &amp; A_{I+1} &amp; B_{I+1} &amp; C_{I+1} &amp; D_{I+1} &amp; E_{I+1}
\end{array}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:10.8em;vertical-align:-5.15em"></span><span class="mord"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:5.65em"><span style="top:-7.65em"><span class="pstrut" style="height:7.65em"></span><span class="mtable"><span class="arraycolsep" style="width:0.5em"></span><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:5.65em"><span style="top:-7.81em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord mathnormal">χ</span><span class="mopen">(</span><span class="mord mathnormal">c</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.07847em">I</span><span class="mclose">)</span></span></span><span style="top:-6.61em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mopen">⟨</span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">0</span><span class="mclose">⟩</span></span></span><span style="top:-5.41em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mopen">⟨</span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">1</span><span class="mclose">⟩</span></span></span><span style="top:-4.21em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mopen">⟨</span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">1</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">0</span><span class="mclose">⟩</span></span></span><span style="top:-3.01em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mopen">⟨</span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">1</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">1</span><span class="mclose">⟩</span></span></span><span style="top:-1.81em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mopen">⟨</span><span class="mord">1</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">0</span><span class="mclose">⟩</span></span></span><span style="top:-0.61em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mopen">⟨</span><span class="mord">1</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">1</span><span class="mclose">⟩</span></span></span><span style="top:0.59em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mopen">⟨</span><span class="mord">1</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">1</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">0</span><span class="mclose">⟩</span></span></span><span style="top:1.79em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mopen">⟨</span><span class="mord">1</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">1</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">1</span><span class="mclose">⟩</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:5.15em"><span></span></span></span></span></span><span class="arraycolsep" style="width:0.5em"></span><span class="vertical-separator" style="height:10.8em;border-right-width:0.04em;border-right-style:solid;margin:0 -0.02em;vertical-align:-5.15em"></span><span class="arraycolsep" style="width:0.5em"></span><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:5.65em"><span style="top:-7.81em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal">A</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span><span style="top:-6.61em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.07153em">C</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0715em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span><span style="top:-5.41em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.07153em">C</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0715em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span><span style="top:-4.21em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05764em">E</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0576em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span><span style="top:-3.01em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05764em">E</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0576em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span><span style="top:-1.81em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal">A</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:-0.61em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal">A</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:0.59em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal">A</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:1.79em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal">A</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:5.15em"><span></span></span></span></span></span><span class="arraycolsep" style="width:0.5em"></span><span class="arraycolsep" style="width:0.5em"></span><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:5.65em"><span style="top:-7.81em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0502em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span><span style="top:-6.61em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord amsrm">∅</span></span></span><span style="top:-5.41em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord amsrm">∅</span></span></span><span style="top:-4.21em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord amsrm">∅</span></span></span><span style="top:-3.01em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord amsrm">∅</span></span></span><span style="top:-1.81em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0502em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:-0.61em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0502em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:0.59em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0502em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:1.79em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0502em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:5.15em"><span></span></span></span></span></span><span class="arraycolsep" style="width:0.5em"></span><span class="arraycolsep" style="width:0.5em"></span><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:5.65em"><span style="top:-7.81em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.07153em">C</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0715em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span><span style="top:-6.61em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord amsrm">∅</span></span></span><span style="top:-5.41em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord amsrm">∅</span></span></span><span style="top:-4.21em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0502em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">2</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:-3.01em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0502em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">2</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:-1.81em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0502em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:-0.61em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0502em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:0.59em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.07153em">C</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0715em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:1.79em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.07153em">C</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0715em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:5.15em"><span></span></span></span></span></span><span class="arraycolsep" style="width:0.5em"></span><span class="arraycolsep" style="width:0.5em"></span><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:5.65em"><span style="top:-7.81em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.02778em">D</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0278em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span><span style="top:-6.61em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord amsrm">∅</span></span></span><span style="top:-5.41em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0502em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">3</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:-4.21em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord amsrm">∅</span></span></span><span style="top:-3.01em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0502em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">3</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:-1.81em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0502em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:-0.61em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.02778em">D</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0278em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:0.59em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0502em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:1.79em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.02778em">D</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0278em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:5.15em"><span></span></span></span></span></span><span class="arraycolsep" style="width:0.5em"></span><span class="arraycolsep" style="width:0.5em"></span><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:5.65em"><span style="top:-7.81em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05764em">E</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0576em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span><span style="top:-6.61em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord amsrm">∅</span></span></span><span style="top:-5.41em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0502em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">3</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:-4.21em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0502em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">2</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:-3.01em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.07153em">C</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0715em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">2</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:-1.81em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0502em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:-0.61em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.02778em">D</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0278em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:0.59em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.07153em">C</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0715em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span><span style="top:1.79em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.05764em">E</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:-0.0576em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.07847em">I</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:5.15em"><span></span></span></span></span></span><span class="arraycolsep" style="width:0.5em"></span></span></span><span style="top:-12.1em"><span class="pstrut" style="height:7.65em"></span><span class="hline" style="border-bottom-width:0.04em"></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:5.15em"><span></span></span></span></span></span></span></span></span></span>
<p>Schulz and Mihov generalized <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">Δ</mi></mrow><annotation encoding="application/x-tex">\Delta</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord">Δ</span></span></span></span> for arbitrary <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mord mathnormal">n</span></span></span></span>. Build it <strong>once</strong> and you
can then instantiate the DFA <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>A</mi><mo stretchy="false">(</mo><mi>W</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">A(W)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal">A</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mclose">)</span></span></span></span> for any word <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>W</mi></mrow><annotation encoding="application/x-tex">W</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.13889em">W</span></span></span></span> in a single linear pass over
its characters. We went from <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>O</mi><mo stretchy="false">(</mo><msup><mi>n</mi><mn>2</mn></msup><mi>N</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">O(n^2 N)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.0641em;vertical-align:-0.25em"></span><span class="mord mathnormal" style="margin-right:0.02778em">O</span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal">n</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mord mathnormal" style="margin-right:0.10903em">N</span><span class="mclose">)</span></span></span></span> down to <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>O</mi><mo stretchy="false">(</mo><mi>N</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">O(N)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal" style="margin-right:0.02778em">O</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.10903em">N</span><span class="mclose">)</span></span></span></span> and that is what makes
Levenshtein search practical. In IResearch <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">Δ</mi></mrow><annotation encoding="application/x-tex">\Delta</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord">Δ</span></span></span></span> is
<a href="https://github.com/serenedb/serenedb/blob/main/libs/iresearch/include/iresearch/utils/levenshtein_utils.hpp" target="_blank" rel="noopener noreferrer"><code>ParametricDescription</code></a>
and
<a href="https://github.com/serenedb/serenedb/blob/main/libs/iresearch/include/iresearch/utils/levenshtein_default_pdp.cpp" target="_blank" rel="noopener noreferrer"><code>DefaultPDP</code></a>
hands out one lazily built instance per (distance, transpositions) pair, nine slots
in total. The per-term DFA comes out of <code>MakeLevenshteinAutomaton</code>, wrapped by the
<a href="https://github.com/serenedb/serenedb/blob/main/libs/iresearch/include/iresearch/search/levenshtein_filter.hpp" target="_blank" rel="noopener noreferrer"><code>ByEditDistance</code></a>
filter.</p>
<p>They also showed that a tiny addition to <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">Δ</mi></mrow><annotation encoding="application/x-tex">\Delta</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord">Δ</span></span></span></span> buys you
<a href="https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance" target="_blank" rel="noopener noreferrer">Damerau–Levenshtein distance</a>,
which treats a transposition of adjacent characters as a single edit. <code>foobar</code>
→ <code>foobra</code> is distance 2 under plain Levenshtein but distance 1 under
Damerau–Levenshtein, much closer to how humans actually mistype.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="how-a-fuzzy-query-runs">How a fuzzy query runs<a href="https://serenedb.com/blog/fuzzy-search-deep-dive#how-a-fuzzy-query-runs" class="hash-link" aria-label="Direct link to How a fuzzy query runs" title="Direct link to How a fuzzy query runs">​</a></h3>
<p>Putting it together, a <code>ts_levenshtein</code> query in SereneDB flows like this:</p>
<div data-email-img="/img/email/mermaid/2026-08-05-fuzzy-search-deep-dive-2.png"></div>
<p>The dictionary walk hands back more than a list of terms. Each accepted term carries
the distance it was accepted at and the filter turns that into a per-term boost of
<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>1</mn><mo>−</mo><mi>d</mi><mi mathvariant="normal">/</mi><mi>min</mi><mo>⁡</mo><mo stretchy="false">(</mo><mi mathvariant="normal">∣</mi><mi>V</mi><mi mathvariant="normal">∣</mi><mo separator="true">,</mo><mi mathvariant="normal">∣</mi><mi>W</mi><mi mathvariant="normal">∣</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">1 - d / \min(|V|, |W|)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7278em;vertical-align:-0.0833em"></span><span class="mord">1</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal">d</span><span class="mord">/</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mop">min</span><span class="mopen">(</span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.22222em">V</span><span class="mord">∣</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mord">∣</span><span class="mclose">)</span></span></span></span>. An exact hit therefore scores above a term accepted at the
maximum distance, so the accepted terms rank against each other instead of all
arriving with the same score.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="caveats">Caveats<a href="https://serenedb.com/blog/fuzzy-search-deep-dive#caveats" class="hash-link" aria-label="Direct link to Caveats" title="Direct link to Caveats">​</a></h3>
<p><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">Δ</mi></mrow><annotation encoding="application/x-tex">\Delta</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord">Δ</span></span></span></span> stores one transition per (parametric state, characteristic vector) pair,
so its size is the number of parametric states times <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mn>2</mn><mrow><mn>2</mn><mi>n</mi><mo>+</mo><mn>1</mn></mrow></msup></mrow><annotation encoding="application/x-tex">2^{2n+1}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8141em"></span><span class="mord"><span class="mord">2</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">2</span><span class="mord mathnormal mtight">n</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span></span></span></span></span></span></span></span></span>. The state count is
what explodes with <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mord mathnormal">n</span></span></span></span>:</p>
<ul>
<li><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi><mo>=</mo><mn>1</mn></mrow><annotation encoding="application/x-tex">n=1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:0.6444em"></span><span class="mord">1</span></span></span></span>: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>5</mn><mo>⋅</mo><msup><mn>2</mn><mn>3</mn></msup><mo>=</mo><mn>40</mn></mrow><annotation encoding="application/x-tex">5 \cdot 2^{3} = 40</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6444em"></span><span class="mord">5</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:0.8141em"></span><span class="mord"><span class="mord">2</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">3</span></span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:0.6444em"></span><span class="mord">40</span></span></span></span> transitions</li>
<li><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi><mo>=</mo><mn>2</mn></mrow><annotation encoding="application/x-tex">n=2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:0.6444em"></span><span class="mord">2</span></span></span></span>: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>30</mn><mo>⋅</mo><msup><mn>2</mn><mn>5</mn></msup><mo>=</mo><mn>960</mn></mrow><annotation encoding="application/x-tex">30 \cdot 2^{5} = 960</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6444em"></span><span class="mord">30</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:0.8141em"></span><span class="mord"><span class="mord">2</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">5</span></span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:0.6444em"></span><span class="mord">960</span></span></span></span></li>
<li><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi><mo>=</mo><mn>3</mn></mrow><annotation encoding="application/x-tex">n=3</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:0.6444em"></span><span class="mord">3</span></span></span></span>: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>196</mn><mo>⋅</mo><msup><mn>2</mn><mn>7</mn></msup><mo>=</mo><mn>25,088</mn></mrow><annotation encoding="application/x-tex">196 \cdot 2^{7} = 25{,}088</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6444em"></span><span class="mord">196</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:0.8141em"></span><span class="mord"><span class="mord">2</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">7</span></span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:0.8389em;vertical-align:-0.1944em"></span><span class="mord">25</span><span class="mord"><span class="mpunct">,</span></span><span class="mord">088</span></span></span></span></li>
<li><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi><mo>=</mo><mn>4</mn></mrow><annotation encoding="application/x-tex">n=4</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:0.6444em"></span><span class="mord">4</span></span></span></span>: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>1,353</mn><mo>⋅</mo><msup><mn>2</mn><mn>9</mn></msup><mo>=</mo><mn>692,736</mn></mrow><annotation encoding="application/x-tex">1{,}353 \cdot 2^{9} = 692{,}736</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8389em;vertical-align:-0.1944em"></span><span class="mord">1</span><span class="mord"><span class="mpunct">,</span></span><span class="mord">353</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:0.8141em"></span><span class="mord"><span class="mord">2</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">9</span></span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:0.8389em;vertical-align:-0.1944em"></span><span class="mord">692</span><span class="mord"><span class="mpunct">,</span></span><span class="mord">736</span></span></span></span></li>
</ul>
<p>That growth makes very large distances impractical, so SereneDB caps the edit
distance at <strong>4</strong> for Levenshtein and <strong>3</strong> for Damerau–Levenshtein. Beyond that
"fuzzy" stops meaning anything useful anyway.</p>
<p>There's also a lovely trick for squeezing one more unit of distance out of a
smaller automaton. The edit distance is invariant under reversal:
<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">lev</mi><mo>⁡</mo><mo stretchy="false">(</mo><mi>W</mi><mo separator="true">,</mo><mi>V</mi><mo stretchy="false">)</mo><mo>=</mo><mi mathvariant="normal">lev</mi><mo>⁡</mo><mo stretchy="false">(</mo><msup><mi>W</mi><mo mathvariant="normal" lspace="0em" rspace="0em">′</mo></msup><mo separator="true">,</mo><msup><mi>V</mi><mo mathvariant="normal" lspace="0em" rspace="0em">′</mo></msup><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">\operatorname{lev}(W, V) = \operatorname{lev}(W', V')</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mop"><span class="mord mathrm" style="margin-right:0.01389em">lev</span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.22222em">V</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1.0019em;vertical-align:-0.25em"></span><span class="mop"><span class="mord mathrm" style="margin-right:0.01389em">lev</span></span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7519em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">′</span></span></span></span></span></span></span></span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.22222em">V</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7519em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">′</span></span></span></span></span></span></span></span></span><span class="mclose">)</span></span></span></span> for the reversed strings.
So by keeping <strong>two</strong> term dictionaries, one forward and one reversed (a so-called
FB-trie), you can answer distance-<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">(</mo><mi>n</mi><mo lspace="0em" rspace="0em">+</mo><mn>1</mn><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">(n{+}1)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mopen">(</span><span class="mord mathnormal">n</span><span class="mord"><span class="mord">+</span></span><span class="mord">1</span><span class="mclose">)</span></span></span></span> queries using two distance-<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mord mathnormal">n</span></span></span></span>
automata. Given how fast the DFA grows with <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mord mathnormal">n</span></span></span></span>, that's a very good trade.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="trying-it-in-serenedb">Trying it in SereneDB<a href="https://serenedb.com/blog/fuzzy-search-deep-dive#trying-it-in-serenedb" class="hash-link" aria-label="Direct link to Trying it in SereneDB" title="Direct link to Trying it in SereneDB">​</a></h3>
<p>All of the above is behind a single SQL function,
<a href="https://serenedb.com/blog/docs/sql/functions/search/full-text#ts_levenshtein"><code>ts_levenshtein</code></a>. First, a
<a href="https://serenedb.com/blog/docs/sql/statements/create_text_search_dictionary/text"><code>text</code> dictionary</a> and an
<a href="https://serenedb.com/blog/docs/sql/indexes/inverted/full-text-search">inverted index</a> over some product
names:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TEXT</span><span class="token plain"> SEARCH DICTIONARY fuzzy_dict </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    template </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'text'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    locale </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'en_US.UTF-8'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'lower'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    stemming </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    accent </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TABLE</span><span class="token plain"> products </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id </span><span class="token keyword" style="color:#00009f">INTEGER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">PRIMARY</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">KEY</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> name </span><span class="token keyword" style="color:#00009f">VARCHAR</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> idx_products </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> products</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">USING</span><span class="token plain"> inverted </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> name fuzzy_dict</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">INSERT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INTO</span><span class="token plain"> products </span><span class="token keyword" style="color:#00009f">VALUES</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'cat'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'bat'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">3</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'car'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">4</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'dog'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">5</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'cats'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">6</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'act'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">VACUUM </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">REFRESH_TABLE</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> products</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Fuzzy matching is just the <code>@@</code> operator against a <code>ts_levenshtein</code> acceptor:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">-- Distance 1: everything one edit from 'cat'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> name </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> idx_products</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> name @@ ts_levenshtein</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'cat'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">--  1 cat | 2 bat | 3 car | 5 cats | 6 act</span><br></span></code></pre></div></div>
<p>If you drop the distance argument you get <strong>auto mode</strong> then. It picks the distance from the query
length: 0 for two characters or fewer, 1 for three to five, 2 from six up.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">-- No distance: 'cat' is 3 characters, so distance 1</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> name </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> idx_products</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> name @@ ts_levenshtein</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'cat'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">--  1 cat | 2 bat | 3 car | 5 cats | 6 act</span><br></span></code></pre></div></div>
<p>This is the form you want behind a search box, where the query grows one keystroke
at a time. A fixed distance of 2 is fine for <code>catalogue</code> and useless for <code>ct</code>, since
at two characters almost every short token in the dictionary is within two edits.
Auto mode gives <code>ct</code> distance 0 and <code>catalogue</code> distance 2 without you branching on
<code>length()</code> in SQL.</p>
<p><code>act</code> matches because a transposition counts as one edit. Transpositions are on by
default (Damerau–Levenshtein). Turn them off and <code>act</code> drops out:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">-- Strict Levenshtein: 'act' is now distance 2 from 'cat'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> name </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> idx_products</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> name @@ ts_levenshtein</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'cat'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">--  1 cat | 2 bat | 3 car | 5 cats</span><br></span></code></pre></div></div>
<p>You can also anchor a literal prefix and only fuzz the tail. That's cheap, because
the prefix walks the trie directly and the automaton only kicks in afterward:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">-- Must start with 'ca', fuzzy-match the rest within distance 1</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> name </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> idx_products</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> name @@ ts_levenshtein</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'t'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'ca'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">--  1 cat | 3 car | 5 cats</span><br></span></code></pre></div></div>
<p>Because it's an ordinary acceptor, it composes with the rest of SQL.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> name </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> idx_products</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> name @@ ts_levenshtein</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'cat'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">AND</span><span class="token plain"> id </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">4</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">--  1 cat | 2 bat | 3 car</span><br></span></code></pre></div></div>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="spell-correction">Spell correction<a href="https://serenedb.com/blog/fuzzy-search-deep-dive#spell-correction" class="hash-link" aria-label="Direct link to Spell correction" title="Direct link to Spell correction">​</a></h3>
<p>Being ordinary SQL also allows you to get the matched terms themselves. Point
<code>ts_levenshtein</code> at a query log and read the accepted dictionary entries back with
the <code>ts_dict</code> aggregates:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> unnest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ts_dict_agg</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">term</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">   </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> suggestion</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       unnest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ts_dict_score</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">term</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> similarity</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       unnest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ts_dict_count</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">term</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> searches</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> query_log_idx</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> term @@ ts_levenshtein</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'jaket'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> similarity </span><span class="token keyword" style="color:#00009f">DESC</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> searches </span><span class="token keyword" style="color:#00009f">DESC</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">--  jacket | 0.8 | 5</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">--  basket | 0.6 | 1</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">--  racket | 0.6 | 1</span><br></span></code></pre></div></div>
<p><code>ts_dict_agg</code> returns the terms the automaton accepted, <code>ts_dict_score</code> is the
<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>1</mn><mo>−</mo><mi>d</mi><mi mathvariant="normal">/</mi><mi>min</mi><mo>⁡</mo><mo stretchy="false">(</mo><mi mathvariant="normal">∣</mi><mi>V</mi><mi mathvariant="normal">∣</mi><mo separator="true">,</mo><mi mathvariant="normal">∣</mi><mi>W</mi><mi mathvariant="normal">∣</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">1 - d / \min(|V|, |W|)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7278em;vertical-align:-0.0833em"></span><span class="mord">1</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal">d</span><span class="mord">/</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mop">min</span><span class="mopen">(</span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.22222em">V</span><span class="mord">∣</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mord">∣</span><span class="mclose">)</span></span></span></span> similarity from the dictionary walk and <code>ts_dict_count</code> is
the indexed frequency. Sorting by similarity first and frequency second is what
turns three candidates at distance 2 into one correction: <code>jacket</code> was searched 5
times, the other two once each. Add <code>LIMIT 1</code> and you have a "did you mean".</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="why-another-kind-of-fuzziness">Why another kind of fuzziness?<a href="https://serenedb.com/blog/fuzzy-search-deep-dive#why-another-kind-of-fuzziness" class="hash-link" aria-label="Direct link to Why another kind of fuzziness?" title="Direct link to Why another kind of fuzziness?">​</a></h2>
<p>Raw edit distance isn't always the right lens. Two problems show up quickly.</p>
<p>First, <strong>length bias.</strong> A distance of 2 means something very different for a
4-letter word than for a 20-letter one. The obvious fix is to divide the distance
by the length and flip it into a similarity:</p>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi mathvariant="normal">levsim</mi><mo>⁡</mo><mo stretchy="false">(</mo><mi>W</mi><mo separator="true">,</mo><mi>V</mi><mo stretchy="false">)</mo><mo>=</mo><mn>1</mn><mo>−</mo><mfrac><mrow><mi mathvariant="normal">lev</mi><mo>⁡</mo><mo stretchy="false">(</mo><mi>W</mi><mo separator="true">,</mo><mi>V</mi><mo stretchy="false">)</mo></mrow><mrow><mi>max</mi><mo>⁡</mo><mo stretchy="false">(</mo><mi mathvariant="normal">∣</mi><mi>W</mi><mi mathvariant="normal">∣</mi><mo separator="true">,</mo><mi mathvariant="normal">∣</mi><mi>V</mi><mi mathvariant="normal">∣</mi><mo stretchy="false">)</mo></mrow></mfrac></mrow><annotation encoding="application/x-tex">\operatorname{levsim}(W, V) = 1 - \frac{\operatorname{lev}(W, V)}{\max(|W|, |V|)}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mop"><span class="mord mathrm">levsim</span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.22222em">V</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:0.7278em;vertical-align:-0.0833em"></span><span class="mord">1</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:2.363em;vertical-align:-0.936em"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.427em"><span style="top:-2.314em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mop">max</span><span class="mopen">(</span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mord">∣</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.22222em">V</span><span class="mord">∣</span><span class="mclose">)</span></span></span><span style="top:-3.23em"><span class="pstrut" style="height:3em"></span><span class="frac-line" style="border-bottom-width:0.04em"></span></span><span style="top:-3.677em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mop"><span class="mord mathrm" style="margin-right:0.01389em">lev</span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.22222em">V</span><span class="mclose">)</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.936em"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span>
<p><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>max</mi><mo>⁡</mo></mrow><annotation encoding="application/x-tex">\max</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mop">max</span></span></span></span> is what keeps the result inside <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">[</mo><mn>0</mn><mo separator="true">,</mo><mn>1</mn><mo stretchy="false">]</mo></mrow><annotation encoding="application/x-tex">[0,1]</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mopen">[</span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">1</span><span class="mclose">]</span></span></span></span>. Divide by <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>min</mi><mo>⁡</mo></mrow><annotation encoding="application/x-tex">\min</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6679em"></span><span class="mop">min</span></span></span></span> and the ratio can
exceed 1 (<code>a</code> against <code>xyz</code> gives <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>3</mn><mi mathvariant="normal">/</mi><mn>1</mn></mrow><annotation encoding="application/x-tex">3/1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord">3/1</span></span></span></span>), which is useless as a score. Even with
<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>max</mi><mo>⁡</mo></mrow><annotation encoding="application/x-tex">\max</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mop">max</span></span></span></span> you don't get much: the maximum edit distance we can afford is small, so
every long string that clears the cap lands a couple of percent below similarity 1
and the score has nothing left to discriminate with.</p>
<p>Second, <strong>phrases.</strong> Allowing one edit per word lets <code>quck brwn fx</code> match
<code>quick brown fox</code>. But it will never match <code>quick-witted brown fox</code>, because per-word edit distance has no notion of extra or missing words. For that we want a measure that tolerates extra and missing material in the indexed value.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="approximate-matching-based-on-n-gram-similarity">Approximate matching based on n-gram similarity<a href="https://serenedb.com/blog/fuzzy-search-deep-dive#approximate-matching-based-on-n-gram-similarity" class="hash-link" aria-label="Direct link to Approximate matching based on n-gram similarity" title="Direct link to Approximate matching based on n-gram similarity">​</a></h2>
<p>A different way to compare two strings is by their <strong>longest common subsequence</strong> (LCS) of characters: the longer the LCS, the more similar. On its own, character LCS is too context-free: <code>connection</code> and <code>fonetica</code> share a 5-character subsequence (<code>oneti</code>) despite meaning nothing alike.</p>
<p><a href="https://webdocs.cs.ualberta.ca/~kondrak/papers/spire05.pdf" target="_blank" rel="noopener noreferrer">Grzegorz Kondrak's fix</a>
is to run the LCS over <strong>n-grams</strong> instead of single
characters, so each unit carries a little local context. Compare the same pair as
3-grams:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">connection -&gt; con onn nne nec ect cti tio ion</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">fonetica   -&gt; fon one net eti tic ica</span><br></span></code></pre></div></div>
<p>Now they share <em>zero</em> trigrams and the spurious similarity is gone.</p>
<p>Formally, let <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>X</mi><mo>=</mo><mo stretchy="false">⟨</mo><msub><mi>x</mi><mn>1</mn></msub><mo>…</mo><msub><mi>x</mi><mi>k</mi></msub><mo stretchy="false">⟩</mo></mrow><annotation encoding="application/x-tex">X = \langle x_1 \dots x_k\rangle</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.07847em">X</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mopen">⟨</span><span class="mord"><span class="mord mathnormal">x</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em"></span><span class="minner">…</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord"><span class="mord mathnormal">x</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.03148em">k</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mclose">⟩</span></span></span></span> and <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>Y</mi><mo>=</mo><mo stretchy="false">⟨</mo><msub><mi>y</mi><mn>1</mn></msub><mo>…</mo><msub><mi>y</mi><mi>l</mi></msub><mo stretchy="false">⟩</mo></mrow><annotation encoding="application/x-tex">Y = \langle y_1 \dots y_l\rangle</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.22222em">Y</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mopen">⟨</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em">y</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em"></span><span class="minner">…</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em">y</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.01968em">l</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mclose">⟩</span></span></span></span>
be sequences over a finite alphabet. Write <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi mathvariant="normal">Γ</mi><mrow><mi>i</mi><mo separator="true">,</mo><mi>j</mi></mrow></msub></mrow><annotation encoding="application/x-tex">\Gamma_{i,j}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.9694em;vertical-align:-0.2861em"></span><span class="mord"><span class="mord">Γ</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">i</span><span class="mpunct mtight">,</span><span class="mord mathnormal mtight" style="margin-right:0.05724em">j</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em"><span></span></span></span></span></span></span></span></span></span> for a pair of prefixes,
<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msubsup><mi mathvariant="normal">Γ</mi><mrow><mi>i</mi><mo separator="true">,</mo><mi>j</mi></mrow><mo lspace="0em" rspace="0em">∗</mo></msubsup></mrow><annotation encoding="application/x-tex">\Gamma^{*}_{i,j}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.0835em;vertical-align:-0.3948em"></span><span class="mord"><span class="mord">Γ</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.6887em"><span style="top:-2.4413em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">i</span><span class="mpunct mtight">,</span><span class="mord mathnormal mtight" style="margin-right:0.05724em">j</span></span></span></span><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">∗</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.3948em"><span></span></span></span></span></span></span></span></span></span> for a pair of suffixes and <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msubsup><mi mathvariant="normal">Γ</mi><mrow><mi>i</mi><mo separator="true">,</mo><mi>j</mi></mrow><mi>n</mi></msubsup></mrow><annotation encoding="application/x-tex">\Gamma^{n}_{i,j}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.0781em;vertical-align:-0.3948em"></span><span class="mord"><span class="mord">Γ</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.6644em"><span style="top:-2.4413em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">i</span><span class="mpunct mtight">,</span><span class="mord mathnormal mtight" style="margin-right:0.05724em">j</span></span></span></span><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">n</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.3948em"><span></span></span></span></span></span></span></span></span></span> for a pair of
n-grams starting just after positions <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>i</mi></mrow><annotation encoding="application/x-tex">i</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6595em"></span><span class="mord mathnormal">i</span></span></span></span> and <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>j</mi></mrow><annotation encoding="application/x-tex">j</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.854em;vertical-align:-0.1944em"></span><span class="mord mathnormal" style="margin-right:0.05724em">j</span></span></span></span>. The base case, comparing two
single n-grams, is binary:</p>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>s</mi><mi>n</mi></msub><mo stretchy="false">(</mo><msubsup><mi mathvariant="normal">Γ</mi><mrow><mn>0</mn><mo separator="true">,</mo><mn>0</mn></mrow><mi>n</mi></msubsup><mo stretchy="false">)</mo><mo>=</mo><mrow><mo fence="true">{</mo><mtable rowspacing="0.36em" columnalign="left left" columnspacing="1em"><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mn>1</mn></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mrow><mtext>if&nbsp;</mtext><msub><mi>x</mi><mi>u</mi></msub><mo>=</mo><msub><mi>y</mi><mi>u</mi></msub><mtext>&nbsp;&nbsp;</mtext><mi mathvariant="normal">∀</mi><mtext> </mtext><mn>1</mn><mo>≤</mo><mi>u</mi><mo>≤</mo><mi>n</mi><mo separator="true">,</mo></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="false"><mn>0</mn></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="false"><mtext>otherwise.</mtext></mstyle></mtd></mtr></mtable></mrow></mrow><annotation encoding="application/x-tex">s_n(\Gamma^{n}_{0,0}) =
\begin{cases}
1 &amp; \text{if } x_u = y_u \ \ \forall\, 1 \le u \le n, \\
0 &amp; \text{otherwise.}
\end{cases}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.1331em;vertical-align:-0.3831em"></span><span class="mord"><span class="mord mathnormal">s</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">n</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord"><span class="mord">Γ</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.7144em"><span style="top:-2.453em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">0</span><span class="mpunct mtight">,</span><span class="mord mtight">0</span></span></span></span><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">n</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.3831em"><span></span></span></span></span></span></span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:3em;vertical-align:-1.25em"></span><span class="minner"><span class="mopen delimcenter" style="top:0em"><span class="delimsizing size4">{</span></span><span class="mord"><span class="mtable"><span class="col-align-l"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.69em"><span style="top:-3.69em"><span class="pstrut" style="height:3.008em"></span><span class="mord"><span class="mord">1</span></span></span><span style="top:-2.25em"><span class="pstrut" style="height:3.008em"></span><span class="mord"><span class="mord">0</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.19em"><span></span></span></span></span></span><span class="arraycolsep" style="width:1em"></span><span class="col-align-l"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.69em"><span style="top:-3.69em"><span class="pstrut" style="height:3.008em"></span><span class="mord"><span class="mord text"><span class="mord">if&nbsp;</span></span><span class="mord"><span class="mord mathnormal">x</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">u</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em">y</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">u</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mspace">&nbsp;</span><span class="mspace">&nbsp;</span><span class="mord">∀</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">1</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">≤</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mord mathnormal">u</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">≤</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mord mathnormal">n</span><span class="mpunct">,</span></span></span><span style="top:-2.25em"><span class="pstrut" style="height:3.008em"></span><span class="mord"><span class="mord text"><span class="mord">otherwise.</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.19em"><span></span></span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span>
<p>The similarity of the full sequences is then an LCS-style recurrence over n-grams:</p>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>s</mi><mo stretchy="false">(</mo><mi>X</mi><mo separator="true">,</mo><mi>Y</mi><mo stretchy="false">)</mo><mo>=</mo><msub><mi>s</mi><mi>n</mi></msub><mo stretchy="false">(</mo><msub><mi mathvariant="normal">Γ</mi><mrow><mi>k</mi><mo separator="true">,</mo><mi>l</mi></mrow></msub><mo stretchy="false">)</mo><mo>=</mo><munder><mrow><mi>max</mi><mo>⁡</mo></mrow><mrow><mi>i</mi><mo separator="true">,</mo><mi>j</mi></mrow></munder><mo fence="false" stretchy="true" minsize="1.8em" maxsize="1.8em">(</mo><mtext> </mtext><msub><mi>s</mi><mi>n</mi></msub><mo stretchy="false">(</mo><msubsup><mi mathvariant="normal">Γ</mi><mrow><mi>i</mi><mo>+</mo><mi>n</mi><mo>−</mo><mn>1</mn><mo separator="true">,</mo><mtext> </mtext><mi>j</mi><mo>+</mo><mi>n</mi><mo>−</mo><mn>1</mn></mrow><mi>n</mi></msubsup><mo stretchy="false">)</mo><mo>+</mo><msub><mi>s</mi><mi>n</mi></msub><mo stretchy="false">(</mo><msubsup><mi mathvariant="normal">Γ</mi><mrow><mi>i</mi><mo separator="true">,</mo><mi>j</mi></mrow><mo lspace="0em" rspace="0em">∗</mo></msubsup><mo stretchy="false">)</mo><mtext> </mtext><mo fence="false" stretchy="true" minsize="1.8em" maxsize="1.8em">)</mo></mrow><annotation encoding="application/x-tex">s(X, Y) = s_n(\Gamma_{k,l}) =
\max_{i,j}\Big(\, s_n(\Gamma^{n}_{i+n-1,\, j+n-1}) + s_n(\Gamma^{*}_{i,j}) \,\Big)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal">s</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.07847em">X</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.22222em">Y</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1.0361em;vertical-align:-0.2861em"></span><span class="mord"><span class="mord mathnormal">s</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">n</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord"><span class="mord">Γ</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.03148em">k</span><span class="mpunct mtight">,</span><span class="mord mathnormal mtight" style="margin-right:0.01968em">l</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em"><span></span></span></span></span></span></span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:2.0138em;vertical-align:-0.8638em"></span><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.4306em"><span style="top:-2.3723em;margin-left:0em"><span class="pstrut" style="height:3em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">i</span><span class="mpunct mtight">,</span><span class="mord mathnormal mtight" style="margin-right:0.05724em">j</span></span></span></span><span style="top:-3em"><span class="pstrut" style="height:3em"></span><span><span class="mop">max</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.8638em"><span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord"><span class="delimsizing size2">(</span></span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord"><span class="mord mathnormal">s</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">n</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord"><span class="mord">Γ</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.7144em"><span style="top:-2.453em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">i</span><span class="mbin mtight">+</span><span class="mord mathnormal mtight">n</span><span class="mbin mtight">−</span><span class="mord mtight">1</span><span class="mpunct mtight">,</span><span class="mspace mtight" style="margin-right:0.1952em"></span><span class="mord mathnormal mtight" style="margin-right:0.05724em">j</span><span class="mbin mtight">+</span><span class="mord mathnormal mtight">n</span><span class="mbin mtight">−</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">n</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.3831em"><span></span></span></span></span></span></span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:1.8em;vertical-align:-0.65em"></span><span class="mord"><span class="mord mathnormal">s</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">n</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord"><span class="mord">Γ</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.7387em"><span style="top:-2.453em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">i</span><span class="mpunct mtight">,</span><span class="mord mathnormal mtight" style="margin-right:0.05724em">j</span></span></span></span><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">∗</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.3831em"><span></span></span></span></span></span></span><span class="mclose">)</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord"><span class="delimsizing size2">)</span></span></span></span></span></span>
<p>Normalize by the longer string to land in <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">[</mo><mn>0</mn><mo separator="true">,</mo><mn>1</mn><mo stretchy="false">]</mo></mrow><annotation encoding="application/x-tex">[0, 1]</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mopen">[</span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">1</span><span class="mclose">]</span></span></span></span> and shed the length bias:</p>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>s</mi><mi>N</mi></msub><mo stretchy="false">(</mo><mi>X</mi><mo separator="true">,</mo><mi>Y</mi><mo stretchy="false">)</mo><mo>=</mo><mfrac><mrow><mi>s</mi><mo stretchy="false">(</mo><mi>X</mi><mo separator="true">,</mo><mi>Y</mi><mo stretchy="false">)</mo></mrow><mrow><mi>max</mi><mo>⁡</mo><mo stretchy="false">(</mo><mi mathvariant="normal">∣</mi><mi>X</mi><mi mathvariant="normal">∣</mi><mo separator="true">,</mo><mi mathvariant="normal">∣</mi><mi>Y</mi><mi mathvariant="normal">∣</mi><mo stretchy="false">)</mo></mrow></mfrac></mrow><annotation encoding="application/x-tex">s_N(X, Y) = \frac{s(X, Y)}{\max(|X|, |Y|)}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord"><span class="mord mathnormal">s</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.10903em">N</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.07847em">X</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.22222em">Y</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:2.363em;vertical-align:-0.936em"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.427em"><span style="top:-2.314em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mop">max</span><span class="mopen">(</span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.07847em">X</span><span class="mord">∣</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.22222em">Y</span><span class="mord">∣</span><span class="mclose">)</span></span></span><span style="top:-3.23em"><span class="pstrut" style="height:3em"></span><span class="frac-line" style="border-bottom-width:0.04em"></span></span><span style="top:-3.677em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord mathnormal">s</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.07847em">X</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.22222em">Y</span><span class="mclose">)</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.936em"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span>
<p>The practical beauty of this approach is that it needs <strong>no per-query automaton</strong>.
You split each indexed term into n-grams, store those n-grams as terms in the
dictionary and record their positions within each document. At query time you
split the input the same way, look up the posting list for each n-gram and for
every matched document use the recorded positions to compute the n-gram LCS and
finally <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>s</mi><mi>N</mi></msub><mo stretchy="false">(</mo><mi>X</mi><mo separator="true">,</mo><mi>Y</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">s_N(X, Y)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord"><span class="mord mathnormal">s</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.10903em">N</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.07847em">X</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.22222em">Y</span><span class="mclose">)</span></span></span></span>.</p>
<p>That two-step shape, a cheap posting-list lookup to gather candidates followed by an
exact positional check, is exactly the
<a href="https://serenedb.com/blog/iresearch-two-phase-queries">two-phase execution model</a>
IResearch uses across phrase, geo and nested queries. Phase 1 requires at least
<em>k</em> of the query's n-grams to be present, which is a
<a href="https://github.com/serenedb/serenedb/blob/main/libs/iresearch/include/iresearch/search/disjunction.hpp" target="_blank" rel="noopener noreferrer"><code>MinMatchDisjunction</code></a>
wrapped as <code>NGramApprox</code> in
<a href="https://github.com/serenedb/serenedb/blob/main/libs/iresearch/include/iresearch/search/ngram_similarity_query.cpp" target="_blank" rel="noopener noreferrer"><code>ngram_similarity_query.cpp</code></a>.
Phase 2 decodes positions to confirm they actually line up and it only runs for
documents phase 1 already accepted.</p>
<p>That <em>k</em> is where the implementation differs from Kondrak's <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>s</mi><mi>N</mi></msub></mrow><annotation encoding="application/x-tex">s_N</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.5806em;vertical-align:-0.15em"></span><span class="mord"><span class="mord mathnormal">s</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.10903em">N</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span></span>.
<a href="https://github.com/serenedb/serenedb/blob/main/libs/iresearch/include/iresearch/search/ngram_similarity_filter.cpp" target="_blank" rel="noopener noreferrer"><code>MinMatchCount</code></a>
derives it as <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>k</mi><mo>=</mo><mo stretchy="false">⌈</mo><mi mathvariant="normal">∣</mi><mi>X</mi><mi mathvariant="normal">∣</mi><mo>⋅</mo><mi>t</mi><mo stretchy="false">⌉</mo></mrow><annotation encoding="application/x-tex">k = \lceil |X| \cdot t \rceil</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em"></span><span class="mord mathnormal" style="margin-right:0.03148em">k</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mopen">⌈</span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.07847em">X</span><span class="mord">∣</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal">t</span><span class="mclose">⌉</span></span></span></span> for threshold <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>t</mi></mrow><annotation encoding="application/x-tex">t</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6151em"></span><span class="mord mathnormal">t</span></span></span></span> and query n-gram count
<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">∣</mi><mi>X</mi><mi mathvariant="normal">∣</mi></mrow><annotation encoding="application/x-tex">|X|</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.07847em">X</span><span class="mord">∣</span></span></span></span>, so the score is normalized by the query alone and <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">∣</mi><mi>Y</mi><mi mathvariant="normal">∣</mi></mrow><annotation encoding="application/x-tex">|Y|</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.22222em">Y</span><span class="mord">∣</span></span></span></span> never enters the
picture. Extra material in the indexed value is free. <code>ts_ngram('hello', 0.7)</code>
matches a title <code>helloworld</code> because all four query bigrams line up in order, while
<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>s</mi><mi>N</mi></msub></mrow><annotation encoding="application/x-tex">s_N</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.5806em;vertical-align:-0.15em"></span><span class="mord"><span class="mord mathnormal">s</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.10903em">N</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span></span> scores that pair <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>4</mn><mi mathvariant="normal">/</mi><mn>9</mn></mrow><annotation encoding="application/x-tex">4/9</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord">4/9</span></span></span></span> and rejects it. That asymmetry is what lets a short
query find a long field. It also gets you the phrase case from earlier: over a
character-bigram dictionary <code>ts_ngram('brwn fox')</code> matches <code>quick-witted brown fox</code>
at the default threshold, the leading extra word costing nothing. What you still pay
for is typos, since each one takes out the bigrams that straddle it. <code>quck brwn fx</code>
keeps 7 of its 11 bigrams against that title, so it needs the threshold down at
<code>0.6</code>.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="a-sharper-score">A sharper score<a href="https://serenedb.com/blog/fuzzy-search-deep-dive#a-sharper-score" class="hash-link" aria-label="Direct link to A sharper score" title="Direct link to A sharper score">​</a></h3>
<p>Kondrak also proposed a refinement: instead of the strictly-binary base case,
count how many <em>individual characters</em> line up inside a matched n-gram window:</p>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>s</mi><mi>n</mi></msub><mo stretchy="false">(</mo><msubsup><mi mathvariant="normal">Γ</mi><mrow><mi>i</mi><mo separator="true">,</mo><mi>j</mi></mrow><mi>n</mi></msubsup><mo stretchy="false">)</mo><mo>=</mo><mfrac><mn>1</mn><mi>n</mi></mfrac><munderover><mo>∑</mo><mrow><mi>u</mi><mo>=</mo><mn>1</mn></mrow><mi>n</mi></munderover><msub><mi>s</mi><mn>1</mn></msub><mo stretchy="false">(</mo><msub><mi>x</mi><mrow><mi>i</mi><mo>+</mo><mi>u</mi></mrow></msub><mo separator="true">,</mo><mtext> </mtext><msub><mi>y</mi><mrow><mi>j</mi><mo>+</mo><mi>u</mi></mrow></msub><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">s_n(\Gamma^{n}_{i,j}) = \frac{1}{n} \sum_{u=1}^{n} s_1(x_{i+u},\, y_{j+u})</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.1331em;vertical-align:-0.3831em"></span><span class="mord"><span class="mord mathnormal">s</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">n</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord"><span class="mord">Γ</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.7144em"><span style="top:-2.453em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">i</span><span class="mpunct mtight">,</span><span class="mord mathnormal mtight" style="margin-right:0.05724em">j</span></span></span></span><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">n</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.3831em"><span></span></span></span></span></span></span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:2.9185em;vertical-align:-1.2671em"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3214em"><span style="top:-2.314em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord mathnormal">n</span></span></span><span style="top:-3.23em"><span class="pstrut" style="height:3em"></span><span class="frac-line" style="border-bottom-width:0.04em"></span></span><span style="top:-3.677em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.686em"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.1667em"></span><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.6514em"><span style="top:-1.8829em;margin-left:0em"><span class="pstrut" style="height:3.05em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">u</span><span class="mrel mtight">=</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.05em"><span class="pstrut" style="height:3.05em"></span><span><span class="mop op-symbol large-op">∑</span></span></span><span style="top:-4.3em;margin-left:0em"><span class="pstrut" style="height:3.05em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">n</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.2671em"><span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord"><span class="mord mathnormal">s</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal">x</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">i</span><span class="mbin mtight">+</span><span class="mord mathnormal mtight">u</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2083em"><span></span></span></span></span></span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em">y</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.05724em">j</span><span class="mbin mtight">+</span><span class="mord mathnormal mtight">u</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em"><span></span></span></span></span></span></span><span class="mclose">)</span></span></span></span></span>
<p>Depending on the corpus this bought Kondrak 10–20% better accuracy. The catch is
that it's harder to evaluate. Here the two algorithms in this post meet:
split the query into n-gram tokens, build a Levenshtein automaton for each,
<strong>union them into one big automaton</strong> and use that to pull all the near-miss
n-grams out of the dictionary in one sweep. For each matched n-gram you then
measure the per-character similarity. Fuzzy matching all the way down.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="trying-it-in-serenedb-1">Trying it in SereneDB<a href="https://serenedb.com/blog/fuzzy-search-deep-dive#trying-it-in-serenedb-1" class="hash-link" aria-label="Direct link to Trying it in SereneDB" title="Direct link to Trying it in SereneDB">​</a></h3>
<p>N-gram similarity needs an
<a href="https://serenedb.com/blog/docs/sql/statements/create_text_search_dictionary/ngram"><code>ngram</code> dictionary</a> that
records both frequency and positions:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TEXT</span><span class="token plain"> SEARCH DICTIONARY bigram_dict </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    template </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'ngram'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    mingram </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    maxgram </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    frequency </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    position </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TABLE</span><span class="token plain"> articles </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id </span><span class="token keyword" style="color:#00009f">INTEGER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">PRIMARY</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">KEY</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> title </span><span class="token keyword" style="color:#00009f">VARCHAR</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> idx_articles </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> articles</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">USING</span><span class="token plain"> inverted </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> title bigram_dict</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">INSERT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INTO</span><span class="token plain"> articles </span><span class="token keyword" style="color:#00009f">VALUES</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'hello'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'help'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">3</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'world'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">4</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'held'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">5</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'hero'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">VACUUM </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">REFRESH_TABLE</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> articles</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Then <a href="https://serenedb.com/blog/docs/sql/functions/search/full-text#ts_ngram"><code>ts_ngram</code></a> takes a similarity
threshold in <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">[</mo><mn>0</mn><mo separator="true">,</mo><mn>1</mn><mo stretchy="false">]</mo></mrow><annotation encoding="application/x-tex">[0, 1]</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mopen">[</span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">1</span><span class="mclose">]</span></span></span></span> (default <code>0.7</code>), measured against the number of n-grams in the
query:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">-- Strict: only near-identical titles</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> title </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> idx_articles</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> title @@ ts_ngram</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'hello'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">--  1 hello</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">-- Loosen the threshold and neighbours appear</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> title </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> idx_articles</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> title @@ ts_ngram</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'hello'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.3</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">--  1 hello | 2 help | 4 held</span><br></span></code></pre></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="summary">Summary<a href="https://serenedb.com/blog/fuzzy-search-deep-dive#summary" class="hash-link" aria-label="Direct link to Summary" title="Direct link to Summary">​</a></h2>
<p>Neither algorithm wins outright. <strong>Levenshtein automata</strong> give you a
precise, bounded notion of "within <em>k</em> typos", built in linear time and pruned
against the term dictionary, which is perfect for autocorrect-style matching and
short terms. <strong>N-gram similarity</strong> gives you a normalized score that survives extra
and missing characters and rides the ordinary inverted index with no per-query
construction, which is better for longer strings and partial overlap. Most real
systems reach for both and sometimes combine them.</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="references">References<a href="https://serenedb.com/blog/fuzzy-search-deep-dive#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References">​</a></h4>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Wagner%E2%80%93Fischer_algorithm" target="_blank" rel="noopener noreferrer">Wagner–Fischer algorithm</a></li>
<li>Schulz &amp; Mihov, <a href="https://doi.org/10.1007/s10032-002-0082-8" target="_blank" rel="noopener noreferrer"><em>Fast string correction with Levenshtein automata</em></a>, IJDAR 5(1):67–85, 2002</li>
<li><a href="https://en.wikipedia.org/wiki/Trie" target="_blank" rel="noopener noreferrer">Trie</a> | <a href="https://en.wikipedia.org/wiki/Nondeterministic_finite_automaton" target="_blank" rel="noopener noreferrer">NFA</a> | <a href="https://en.wikipedia.org/wiki/Deterministic_finite_automaton" target="_blank" rel="noopener noreferrer">DFA</a></li>
<li><a href="https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance" target="_blank" rel="noopener noreferrer">Damerau–Levenshtein distance</a></li>
<li><a href="https://en.wikipedia.org/wiki/N-gram" target="_blank" rel="noopener noreferrer">N-gram</a> | <a href="https://en.wikipedia.org/wiki/Longest_common_subsequence_problem" target="_blank" rel="noopener noreferrer">Longest common subsequence</a></li>
<li>Kondrak, <a href="https://webdocs.cs.ualberta.ca/~kondrak/papers/spire05.pdf" target="_blank" rel="noopener noreferrer"><em>N-gram similarity and distance</em></a>, SPIRE 2005, LNCS 3772:115–126 (<a href="https://doi.org/10.1007/11575832_13" target="_blank" rel="noopener noreferrer">doi</a>)</li>
<li>IResearch source: <a href="https://github.com/serenedb/serenedb/blob/main/libs/iresearch/include/iresearch/utils/levenshtein_utils.hpp" target="_blank" rel="noopener noreferrer">levenshtein_utils</a> | <a href="https://github.com/serenedb/serenedb/blob/main/libs/iresearch/include/iresearch/utils/levenshtein_default_pdp.cpp" target="_blank" rel="noopener noreferrer">levenshtein_default_pdp</a> | <a href="https://github.com/serenedb/serenedb/blob/main/libs/iresearch/include/iresearch/search/levenshtein_filter.hpp" target="_blank" rel="noopener noreferrer">levenshtein_filter</a> | <a href="https://github.com/serenedb/serenedb/blob/main/libs/iresearch/include/iresearch/search/ngram_similarity_filter.cpp" target="_blank" rel="noopener noreferrer">ngram_similarity_filter</a> | <a href="https://github.com/serenedb/serenedb/blob/main/libs/iresearch/include/iresearch/search/ngram_similarity_query.cpp" target="_blank" rel="noopener noreferrer">ngram_similarity_query</a> | <a href="https://github.com/serenedb/serenedb/blob/main/libs/iresearch/include/iresearch/search/disjunction.hpp" target="_blank" rel="noopener noreferrer">disjunction</a></li>
</ul>
<hr>
<p style="font-size:14px;opacity:0.7">IResearch is open source (Apache 2.0) and available as part of <a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer">SereneDB</a>. If you find this work interesting, starring us on <a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer">GitHub</a> goes a long way for an early-stage project.</p></div>]]></content>
        <author>
            <name>Andrey Abramov</name>
            <uri>https://www.linkedin.com/in/andrey-abramov-57a16241/ru/?skipRedirect=true</uri>
        </author>
        <category label="Search" term="Search"/>
        <category label="FTS" term="FTS"/>
        <category label="Concepts" term="Concepts"/>
        <category label="Dev" term="Dev"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Serene Docs Search: a small, open-source alternative to Algolia DocSearch]]></title>
        <id>https://serenedb.com/blog/serene-docs-search</id>
        <link href="https://serenedb.com/blog/serene-docs-search"/>
        <updated>2026-08-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Serene Docs Search is a self-hosted, Apache 2.0-licensed documentation search stack with BM25, optional hybrid search, Ask AI and MCP, powered by SereneDB.]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="none" viewBox="0 0 28 28" class="logoAvatar_BINX"><path fill="#895AF8" d="M28 14c0-7.732-6.268-14-14-14S0 6.268 0 14s6.268 14 14 14 14-6.268 14-14"></path><path fill="#80BEFF" d="M9.345 18.655c4.556 4.556 11.416 5.084 15.32 1.178 3.906-3.905 3.379-10.764-1.178-15.32C18.931-.043 12.072-.571 8.167 3.334S4.789 14.1 9.345 18.654"></path><path fill="#fff" d="M15.649 12.35c2.278 2.278 5.676 2.574 7.59.66s1.617-5.311-.66-7.59c-2.279-2.277-5.677-2.573-7.59-.66-1.914 1.914-1.618 5.312.66 7.59"></path></svg></div></div><div class="copy_iHNG"><p class="names_ox2I">SereneDB Team</p><p class="meta_OgzX">Aug 4, 2026 · 10 minutes read</p></div></header><h2 class="title_RQ9v">Serene Docs Search: a small, open-source alternative to Algolia DocSearch</h2><p style="font-size:0.9rem;margin-top:-0.4rem">Point it at a website, repository, local folder or S3 bucket and get documentation search that you own</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg hideFirstHeading_VVMX" data-image-lightbox-root="true"><div style="position:relative;width:100%;aspect-ratio:16 / 9;margin-bottom:2rem"><iframe src="https://www.youtube.com/embed/hpxR1v2DXUA" title="Serene Docs Search" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" style="position:absolute;inset:0;width:100%;height:100%;border:0"></iframe></div>
<p>We needed search on our docs. We make a search database. You can see where this
went.</p>
<p><a href="https://docs.serenedb.com/" target="_blank" rel="noopener noreferrer">docs.serenedb.com</a> and this blog now run on
SereneDB-backed search. You can try it right here: press <code>Cmd+K</code> on macOS or
<code>Ctrl+K</code> elsewhere to open the search. Then we packaged the whole path from
"these are my docs" to "there is a search box on the site" into
<strong>Serene Docs Search</strong>.</p>
<p>The short version is: point it at a Git repository, a local folder, a live HTML
site or an S3-compatible bucket; choose full-text or hybrid search; download the
generated config and Compose file; add one React component or script tag to the
site. It keeps the index in sync after that. Ask AI can answer questions with
citations. An optional MCP server lets Codex, Claude and other agents search and
read the same index.</p>
<p>For straightforward documentation and website search, it is a small self-hosted
alternative to Algolia DocSearch. It is not trying to reproduce every feature of
a search SaaS. It covers the common case, keeps the stack and data under your
control and the application packages are licensed under Apache 2.0. Full-text
mode does not need an embeddings service or a model API, so the only bill is the
infrastructure you choose to run it on.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="from-source-to-search-box">From source to search box<a href="https://serenedb.com/blog/serene-docs-search#from-source-to-search-box" class="hash-link" aria-label="Direct link to From source to search box" title="Direct link to From source to search box">​</a></h2>
<p>Documentation does not have one standard home. It may be Markdown in the main
repository, generated HTML on a website, a directory mounted into a build server
or exported files in object storage. Serene Docs Search treats all four as
first-class sources:</p>
<ul>
<li><strong>Git:</strong> follow a branch, pin a commit and limit indexing to selected files or
subdirectories.</li>
<li><strong>Local folder:</strong> mount any directory read-only into the backend.</li>
<li><strong>Website:</strong> crawl from a URL, follow a sitemap and choose a maximum link
depth.</li>
<li><strong>S3:</strong> index a bucket or prefix, including S3-compatible services such as R2
and MinIO through a custom endpoint.</li>
</ul>
<p>The indexer understands Markdown, MDX, HTML, reStructuredText, plain text,
Jupyter notebooks and PDFs. Markdown can stay as one result per file or split at
headings so a result links directly to the relevant section. HTML extraction can
be scoped to <code>article</code>, <code>main</code> or your own CSS selectors, with navigation and
other boilerplate excluded before indexing.</p>
<p>Source paths and public URLs are often different, so URL mapping is part of the
configuration rather than application glue. You can strip source prefixes and
file extensions, map <code>README</code> and <code>index</code> files to directory URLs or route
different paths to different sites. One repository can serve a docs domain and a
blog domain without giving either one broken result links.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">Git / folder / website / S3  ── pull, parse, sync ──┐</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                                                    ▼</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">browser widget  ── /v1/search, /v1/ask ──&gt;  search backend  ──&gt;  SereneDB</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                                                    │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">AI agents      ── MCP tools ────────────────────────┘</span><br></span></code></pre></div></div>
<p>The setup wizard asks for the source, parsing rules, search mode and sync policy,
then generates <code>serene-search.config.json</code> and <code>docker-compose.yml</code> using the same
code as the backend packages. The standard stack is two containers: SereneDB and
the search backend. Ollama and the MCP service appear only when you enable them.</p>
<p>You can configure a real stack from this page now. For a walkthrough of every
option and deployment step, follow the
<a href="https://docs.serenedb.com/clients/serene-docs-search" target="_blank" rel="noopener noreferrer">step-by-step guide</a> in <strong>Apps and clients</strong>.
The wizard saves an unfinished draft in this browser, but it does not change the
search configuration of this site.</p>
<button type="button" class="navbar__button navbar__quick-start-button docs-search__configure-button" aria-haspopup="dialog">Configure now</button>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="search-first-ai-stays-optional">Search first; AI stays optional<a href="https://serenedb.com/blog/serene-docs-search#search-first-ai-stays-optional" class="hash-link" aria-label="Direct link to Search first; AI stays optional" title="Direct link to Search first; AI stays optional">​</a></h2>
<p>The default job of a docs search box is to find a page while somebody is still
typing. Serene Docs Search starts there.</p>
<img src="https://serenedb.com/blog/img/blog/serene-docs-search/im1-light.png" alt="Serene Docs Search with suggested queries and Search, Ask AI and MCP tabs" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%;display:block;margin:24px auto"><img src="https://serenedb.com/blog/img/blog/serene-docs-search/im1-dark.png" alt="Serene Docs Search with suggested queries and Search, Ask AI and MCP tabs" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%;display:block;margin:24px auto">
<p>Full-text mode uses BM25 with extra weight on titles. All complete query terms
must match, while the last term works as a prefix for search-as-you-type. The
analyzer lowercases and folds accents, removes stop words and stems terms, so
<code>run</code> can find <code>running</code>. You can add Solr-format synonyms such as
<code>db, database</code> or <code>k8s =&gt; kubernetes</code>.</p>
<p>Relevance gets a few deliberately practical passes on top:</p>
<ul>
<li>exact and prefix title matches stay above looser matches;</li>
<li>one long page cannot occupy the entire result list with its headings;</li>
<li>configured pages can be pinned for matching queries;</li>
<li>if a strict query returns nothing, a typo-tolerant pass tries again and can
show a <strong>Did you mean</strong> correction;</li>
<li>partial matches are used as a last fallback instead of returning an empty
modal;</li>
<li>every lexical hit includes a snippet with the matching terms highlighted.</li>
</ul>
<p>If keyword search is enough, stop there. There are no vectors to generate and no
model endpoint to operate.</p>
<p>Hybrid mode adds vector similarity over the same sections and merges the lexical
and semantic rankings with reciprocal rank fusion. The embeddings provider can be
OpenAI, Ollama or another OpenAI-compatible endpoint. The fusion weight, candidate
window and distance threshold are tunable per corpus.</p>
<p>Search results can also be grouped into sections such as <strong>Docs</strong>, <strong>Blog</strong> and
<strong>API reference</strong>. The group matching the page a visitor is currently reading
moves first, while relevance order stays unchanged inside every group. Empty
groups disappear. This is useful when one search backend serves several related
sites without making users choose a scope before they search.</p>
<p>When a user selects a result, the widget navigates to the heading anchor, flashes
the target heading and highlights the query terms on the destination page. Query
and click counters are recorded too, with an admin-only report for top queries,
zero-result queries and most-clicked pages.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="ask-the-same-index-a-question">Ask the same index a question<a href="https://serenedb.com/blog/serene-docs-search#ask-the-same-index-a-question" class="hash-link" aria-label="Direct link to Ask the same index a question" title="Direct link to Ask the same index a question">​</a></h2>
<p><strong>Ask AI</strong> is a separate switch, not a requirement for hybrid search. When it is
enabled, the model searches and reads sections through tools over the same index,
streams the answer into the modal and cites the pages it used. Multi-turn history
stays in the conversation. Providers without tool calling fall back to a single
retrieval-and-answer pass.</p>
<img src="https://serenedb.com/blog/img/blog/serene-docs-search/im3-light.png" alt="Ask AI answering a documentation question with cited sources" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%;display:block;margin:24px auto"><img src="https://serenedb.com/blog/img/blog/serene-docs-search/im3-dark.png" alt="Ask AI answering a documentation question with cited sources" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%;display:block;margin:24px auto">
<p>The answer model and embeddings model are configured independently. You can use
an OpenAI-compatible API for one, local Ollama for the other, use Ollama for both,
or leave Ask AI off completely. API keys stay in backend environment variables;
they are never shipped with the browser widget.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-docs-can-be-tools-for-agents-too">The docs can be tools for agents too<a href="https://serenedb.com/blog/serene-docs-search#the-docs-can-be-tools-for-agents-too" class="hash-link" aria-label="Direct link to The docs can be tools for agents too" title="Direct link to The docs can be tools for agents too">​</a></h2>
<p>A useful documentation index should not only serve humans. The optional
<code>@serenedb/docs-search-mcp</code> server exposes three tools:</p>
<ul>
<li><code>search_docs</code> finds relevant sections;</li>
<li><code>read_section</code> returns the full text behind one result;</li>
<li><code>docs_health</code> reports whether the corpus is indexed and current.</li>
</ul>
<img src="https://serenedb.com/blog/img/blog/serene-docs-search/im2-light.png" alt="MCP setup for using the SereneDB documentation from Codex or Claude" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%;display:block;margin:24px auto"><img src="https://serenedb.com/blog/img/blog/serene-docs-search/im2-dark.png" alt="MCP setup for using the SereneDB documentation from Codex or Claude" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%;display:block;margin:24px auto">
<p>It uses the same public HTTP API as the widget, so there is no second ingestion
pipeline or agent-specific copy of the docs. The modal can show a ready-to-copy
setup command for Codex or Claude. The configurator can also add the MCP container
to the generated Compose stack. Ask AI does not depend on MCP; the two features
remain independent opt-ins.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="keep-it-current-without-rebuilding-everything">Keep it current without rebuilding everything<a href="https://serenedb.com/blog/serene-docs-search#keep-it-current-without-rebuilding-everything" class="hash-link" aria-label="Direct link to Keep it current without rebuilding everything" title="Direct link to Keep it current without rebuilding everything">​</a></h2>
<p>The backend owns synchronization as well as queries. For Git it can cheaply watch
the branch for a new commit. Any source can be polled on an interval. CI can
instead call the reindex webhook after a deployment.</p>
<p>Snapshots hash every parsed section. Unchanged sections are skipped, changed ones
are replaced and sections deleted from the source are pruned from the index. The
setup flow shows fetch, parse, embed and index progress during the initial build,
and the health endpoint reports document counts and the last successful sync.</p>
<p>Administrative routes for configuration and manual sync are protected by a token
that the Compose generator creates for you. Search, health and Ask AI remain
public, so there is no admin secret in the website bundle.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="put-the-ui-where-you-need-it">Put the UI where you need it<a href="https://serenedb.com/blog/serene-docs-search#put-the-ui-where-you-need-it" class="hash-link" aria-label="Direct link to Put the UI where you need it" title="Direct link to Put the UI where you need it">​</a></h2>
<p>For React, the complete integration is a component and a stylesheet:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">npm install @serenedb/docs-search-react@latest</span><br></span></code></pre></div></div>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">SereneDocsSearch</span><span class="token imports"> </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@serenedb/docs-search-react"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@serenedb/docs-search-react/styles.css"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag class-name" style="color:#00009f">SereneDocsSearch</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">backendUrl</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">https://search.example.com</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"> </span><span class="token tag punctuation" style="color:#393A34">/&gt;</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>The component includes the trigger, modal, keyboard navigation, <code>Cmd/Ctrl+K</code>
hotkey, automatic light/dark theme, recent queries, connection status, Search,
Ask AI and MCP tabs. It can hand navigation to an SPA router, use a custom
trigger, run as a controlled modal or render into a custom portal.</p>
<p>If you already have a search interface, <code>useSereneDocsSearch()</code> exposes the same
connection, query, grouping, navigation and streamed-answer state without our UI.
If you do not use React, the embed package bundles the widget behind a stylesheet
and one script tag, with both explicit and zero-JavaScript initialization.</p>
<p>Underneath those integrations is a small HTTP API for health, search, streamed
answers, reading complete sections, synchronization and redacted configuration.
The widget is optional; the index is usable from any client that can make an HTTP
request.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-free-alternative-means">What “free alternative” means<a href="https://serenedb.com/blog/serene-docs-search#what-free-alternative-means" class="hash-link" aria-label="Direct link to What “free alternative” means" title="Direct link to What “free alternative” means">​</a></h2>
<p>Serene Docs Search is deliberately for the simple case: a site or docs corpus, a
search modal, a small backend and one database. If you need a global managed
search platform with a large operations team behind it, use one. If you need good
search over your own docs and would rather keep the source, index, relevance
configuration and deployment in your own repository, this is the case we built
for.</p>
<p>The software is open source and licensed under Apache 2.0. There is no per-search
fee and full-text mode has no model cost. Self-hosting still means providing the
machine, storage and operations. Optional model APIs charge whatever their
providers charge. The trade is explicit: less SaaS surface area, more ownership.</p>
<p>That is also why the configurator generates plain files instead of creating a
project in somebody else's control plane. You can inspect them, commit them,
change them and deploy them anywhere Docker Compose runs.</p>
<p>Try the <a href="https://docs.serenedb.com/" target="_blank" rel="noopener noreferrer">search on this site</a>, open the
<a href="https://docs.serenedb.com/clients/serene-docs-search" target="_blank" rel="noopener noreferrer">complete configuration guide</a>,
or generate your own stack with <strong>Configure now</strong> above.</p>
<hr>
<p>If Serene Docs Search is useful to you,
<a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer">star SereneDB on GitHub</a> and tell us what
you would like it to do next.</p></div>]]></content>
        <author>
            <name>SereneDB Team</name>
        </author>
        <category label="Search" term="Search"/>
        <category label="FTS" term="FTS"/>
        <category label="AI" term="AI"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The State of Serene, July 2026]]></title>
        <id>https://serenedb.com/blog/state-of-serene-2026-07</id>
        <link href="https://serenedb.com/blog/state-of-serene-2026-07"/>
        <updated>2026-08-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Issue #2 of The State of Serene: term-dictionary facets, IVF vector search, online CREATE INDEX, PostgreSQL RBAC, a ClickHouse connector and the SearchBench numbers against ArangoDB.]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><img class="avatarImage_s4Ar" src="https://serenedb.com/blog/img/abramov.jpeg" alt="Andrey Abramov" loading="lazy"></div></div><div class="copy_iHNG"><p class="names_ox2I">Andrey Abramov</p><p class="meta_OgzX">Aug 3, 2026 · 20 minutes read</p></div></header><h2 class="title_RQ9v">The State of Serene, July 2026</h2><p style="font-size:0.9rem;margin-top:-0.4rem">Issue #2: facets straight out of the index, vector search rebuilt on IVF, non-blocking CREATE INDEX, Postgres RBAC and the first open benchmark</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg hideFirstHeading_VVMX" data-image-lightbox-root="true"><img src="https://serenedb.com/blog/img/blog/state-of-serene-2026-07/preview.png" alt="Serene" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%;display:block;margin:0 auto"><img src="https://serenedb.com/blog/img/blog/state-of-serene-2026-07/preview.png" alt="Serene" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%;display:block;margin:0 auto">
<br>
<p>Welcome back to <strong>The State of Serene</strong>, our monthly note on what shipped and where
SereneDB is heading.</p>
<p>July was a big month. Plenty of new features: term-dictionary facets and
autocomplete, partial indexes, an online <code>CREATE INDEX</code> that doesn't block writes,
real Postgres roles and grants, <code>CREATE SERVER</code> with a ClickHouse connector behind
it and a docs search box that runs on SereneDB. Plenty of performance work too:
vector search rebuilt on IVF with RaBitQ queries up to 49x faster, filters pushed
down into the index scan and FSST+ compression taking 23.8% off URL columns on
disk. We also published SearchBench and came out 16.1x ahead of ArangoDB across 92
queries.</p>
<p>All of it is in
<a href="https://github.com/serenedb/serenedb/releases/tag/v26.07.5" target="_blank" rel="noopener noreferrer"><strong><code>v26.07.5</code></strong></a>, so
everything below is something you can pull and run today.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-happened-in-july">What happened in July<a href="https://serenedb.com/blog/state-of-serene-2026-07#what-happened-in-july" class="hash-link" aria-label="Direct link to What happened in July" title="Direct link to What happened in July">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="faceted-search-and-autocomplete-without-touching-a-document">Faceted search and autocomplete without touching a document<a href="https://serenedb.com/blog/state-of-serene-2026-07#faceted-search-and-autocomplete-without-touching-a-document" class="hash-link" aria-label="Direct link to Faceted search and autocomplete without touching a document" title="Direct link to Faceted search and autocomplete without touching a document">​</a></h3>
<p>Every search UI needs the same three things next to the results list: a facet
sidebar with counts, a type-ahead and a distinct-value list. On a normal database
all three mean scanning the matched documents and aggregating them, which is
exactly the work you already paid for once when you built the index.</p>
<p>The inverted index already knows every value of every field and how often each one
occurs. That's what a term dictionary is. SereneDB now makes it readable from SQL.
The <code>ts_dict_*</code> aggregates enumerate a field's dictionary straight off disk, with
no document scan and no postings. The list forms come out positionally aligned, so
<code>unnest</code> zips them into rows:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> unnest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ts_dict_agg</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">body</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">   </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> term</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       unnest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ts_dict_count</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">body</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> docs</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       unnest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ts_dict_freq</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">body</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">  </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> freq</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain">   docs_idx</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain">  </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> term</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Most of the time you don't have to spell any of that. On keyword columns the
optimizer rewrites ordinary SQL onto the same path: <code>count(DISTINCT col)</code>, <code>min</code>,
<code>max</code>, <code>array_agg(DISTINCT col)</code> and the plain <code>SELECT col, count(*) ... GROUP BY col</code>
facet shape. <code>EXPLAIN</code> prints <code>TsDict:</code> on the scan when it fires and the document
lookup disappears from the plan.</p>
<p>The sidebar case gets its own trick. A real facet panel counts four or five
dimensions at once and nobody wants five round trips for that, so <code>GROUPING SETS</code>
of single-column sets collapse into one dictionary pass:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> category</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> brand</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> price_band</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">count</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">*</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain">   products_idx</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain">  description @@ </span><span class="token string" style="color:#e3116c">'wireless'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">GROUP</span><span class="token plain">  </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> GROUPING SETS </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">category</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">brand</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">price_band</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>The plan tells you it worked. No document lookup, no scan node under the aggregate,
just the term dictionary being read three ways at once:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">╭─ IRESEARCH_SCAN ────────────────────╮</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│ Index: products_idx                 │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│ Index Filter:                       │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│ ╭─ Term ─────────────────────╮      │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│ │ Field: description(string) │      │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│ │ Value: wireless            │      │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│ ╰────────────────────────────╯      │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│ TsDict: category, brand, price_band │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│ ~7 rows                             │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">╰─────────────────────────────────────╯</span><br></span></code></pre></div></div>
<p>That <code>TsDict:</code> line is the tell. Projections trimmed for width, the rest is what
<code>EXPLAIN</code> prints.</p>
<p>Facets survive a <code>WHERE</code> on an <code>INCLUDE</code>'d column now too. All of it works the
same on view-backed indexes.</p>
<p>Docs: <a href="https://docs.serenedb.com/sql/functions/search/term-dictionary" target="_blank" rel="noopener noreferrer">term dictionary</a>
and <a href="https://docs.serenedb.com/sql/data_types/tsquery" target="_blank" rel="noopener noreferrer"><code>TSQUERY</code></a>, plus the
<a href="https://docs.serenedb.com/cookbook/search/faceted-search" target="_blank" rel="noopener noreferrer">faceted search</a>,
<a href="https://docs.serenedb.com/cookbook/search/autocomplete" target="_blank" rel="noopener noreferrer">autocomplete</a>,
<a href="https://docs.serenedb.com/cookbook/search/spell-correction" target="_blank" rel="noopener noreferrer">spell correction</a> and
<a href="https://docs.serenedb.com/cookbook/search/tag-cloud" target="_blank" rel="noopener noreferrer">tag cloud</a> recipes.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="vector-search-rebuilt-on-ivf">Vector search, rebuilt on IVF<a href="https://serenedb.com/blog/state-of-serene-2026-07#vector-search-rebuilt-on-ivf" class="hash-link" aria-label="Direct link to Vector search, rebuilt on IVF" title="Direct link to Vector search, rebuilt on IVF">​</a></h3>
<p>SereneDB replaced HNSW with a native IVF index built on its own columnstore. Three
reasons.</p>
<p><strong>It scales with disk.</strong> An HNSW graph wants to live in memory. Every hop is a
random access into the graph, so the moment it outgrows RAM your latency falls off
a cliff and index size turns into a hardware budget. IVF keeps the coarse centroids
resident and leaves everything else on disk, read lazily per probed cluster. The
dataset gets to be much bigger than the box.</p>
<p><strong>It's the same machinery as the rest of the index.</strong> A cluster is a term. Its
members are that term's postings list and the quantized codes ride along in the
postings payload stream. IVF isn't a second index bolted onto the side, it's the
inverted index doing its usual job with different keys, so it inherits the on-disk
format, the segment lifecycle, compaction and recovery instead of reimplementing
all four.</p>
<p><strong>Which is why it combines.</strong> A vector query is a disjunction over cluster
iterators, so it composes with full-text filters, boolean predicates and column
filters in the same scan. No candidate-generation step feeding a filtering step in
another system. That's what makes filtered kNN and hybrid search one query instead
of an integration.</p>
<p>Declare it inline on the column:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> idx </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> t</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">USING</span><span class="token plain"> inverted</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pk</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> emb ivf </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">metric </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'l2'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> quant </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'sq8'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p><code>metric</code> is <code>l2</code>, <code>l1</code>, <code>ip</code> or <code>cosine</code>, matched by the <code>&lt;-&gt;</code>, <code>&lt;#&gt;</code> and <code>&lt;=&gt;</code>
operators. <code>quant</code> is <code>sq8</code>, <code>sq4</code>, <code>pq</code>, <code>rabitq</code> or <code>none</code>, so you can trade
precision for a footprint that fits in memory. You don't size the cluster count by
hand any more. A multi-level centroid tree gets built from the row count and
<code>sdb_ivf_posting_size</code> moves the target leaf size if you want to push on it.</p>
<p>Recall then costs you two session settings at query time rather than a rebuild.
<code>sdb_nprobe</code> decides how many clusters a query scans and <code>sdb_rerank_factor</code> sizes
the pool that gets re-scored with exact distances, so the same index serves a cheap
approximate query and an expensive high-recall one. kNN is still plain SQL. An
<code>ORDER BY &lt;distance&gt; LIMIT k</code> gets you the nearest k and a
<code>WHERE &lt;distance&gt; &lt; threshold</code> gets you everything inside a radius.</p>
<p>Builds come out around 60x faster than the HNSW index they replace.</p>
<p>RaBitQ was the slowest quantizer. Its 1-bit default was the worst case: one scalar
distance call per vector per probed cluster. That's a single PQ4 fast-scan per
cluster now, at every bit width. Measured on dbpedia, 100K vectors at 1536
dimensions, l2, k=10:</p>
<table><thead><tr><th>config</th><th>before</th><th>after</th><th>speedup</th></tr></thead><tbody><tr><td>1-bit, nprobe=8, rerank=4</td><td>60.8 ms</td><td>5.5 ms</td><td>11.1x</td></tr><tr><td>1-bit, nprobe=32, rerank=4</td><td>226.5 ms</td><td>8.2 ms</td><td>27.6x</td></tr><tr><td>1-bit, nprobe=128, no rerank</td><td>867.5 ms</td><td>17.7 ms</td><td>49.0x</td></tr><tr><td>3-bit, nprobe=32</td><td>92.5 ms</td><td>26.0 ms</td><td>3.6x</td></tr><tr><td>5-bit, nprobe=32</td><td>89.5 ms</td><td>26.6 ms</td><td>3.4x</td></tr></tbody></table>
<p>Recall came out the same or better, build time and index size unchanged.</p>
<p>RaBitQ and PQ both rebuilt their entire scoring pipeline per probed cluster:
roughly 10 to 20 µs of setup against 0.07 µs per vector of scanning. The
code-dependent half is a lookup table built once per query now, so latency stays
nearly flat as <code>nprobe</code> grows. 20% faster at <code>nprobe=128</code>, 24% at 512.</p>
<p>Filtered kNN returns the right rows now. An <code>ORDER BY emb &lt;-&gt; $1 LIMIT k</code> with a
<code>WHERE</code> on an <code>INCLUDE</code>'d column pushes the filter into the scan instead of running
it above.</p>
<p>Those are SereneDB's own before-and-after numbers, which only tell you it got
faster than it was. For where SereneDB lands against somebody else, there are
preliminary recall-versus-QPS results against Qdrant at
<a href="https://serenedb.github.io/vector-search-benchmark/" target="_blank" rel="noopener noreferrer">serenedb.github.io/vector-search-benchmark</a>.
Treat them as early. Proper vector benchmarks are part of benchmark season and
they're coming.</p>
<p>One upgrade note, because it matters: the payload layout changed twice in July.
Vector indexes built before <code>v26.07.5</code> need a rebuild.</p>
<p>Docs: <a href="https://docs.serenedb.com/sql/indexes/inverted/vector-search" target="_blank" rel="noopener noreferrer">vector search</a>.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="create-index-stopped-blocking-your-writes">CREATE INDEX stopped blocking your writes<a href="https://serenedb.com/blog/state-of-serene-2026-07#create-index-stopped-blocking-your-writes" class="hash-link" aria-label="Direct link to CREATE INDEX stopped blocking your writes" title="Direct link to CREATE INDEX stopped blocking your writes">​</a></h3>
<p><code>CREATE INDEX ... USING inverted(...)</code> no longer blocks writes. Inserts, updates
and deletes run against the table for the whole build and the index that gets
published matches the table exactly. The backfill is parallel, so it gets faster
with more cores. Deletes that race the build are handled, so nothing removed
mid-build survives in the finished index.</p>
<p>Partial indexes handle the case where only some rows are worth indexing:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> recent_errors </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> logs </span><span class="token keyword" style="color:#00009f">USING</span><span class="token plain"> inverted</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">message log_dict</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">level</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'error'</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>PostgreSQL semantics all the way down, including <code>NULL</code> counting as non-matching.
Rows enter and leave the index as updates move them across the predicate boundary,
so membership stays correct without a reindex.</p>
<p>Index tuning is real DDL now:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> opt_tuned </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> opt_t </span><span class="token keyword" style="color:#00009f">USING</span><span class="token plain"> inverted</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">label</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">WITH</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">segment_memory_max </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">67108864</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> segment_docs_max </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        compaction_max_segments </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">4</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> compaction_floor_segment_bytes </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">4194304</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ALTER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> opt_tuned </span><span class="token keyword" style="color:#00009f">SET</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">segment_memory_max </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">33554432</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ALTER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> opt_tuned RESET </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">segment_memory_max</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p><code>ALTER INDEX SET</code> applies live to the running storage, writer limits and task
cadences both. <code>pg_class.reloptions</code> lists the persisted set and
<code>pg_index.indisvalid</code> / <code>indisready</code> are false while a build is in flight, so
anything already speaking Postgres can see what's going on.</p>
<p>Docs: <a href="https://docs.serenedb.com/sql/statements/create_index/inverted" target="_blank" rel="noopener noreferrer">inverted CREATE INDEX</a>.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="filters-push-all-the-way-into-the-scan">Filters push all the way into the scan<a href="https://serenedb.com/blog/state-of-serene-2026-07#filters-push-all-the-way-into-the-scan" class="hash-link" aria-label="Direct link to Filters push all the way into the scan" title="Direct link to Filters push all the way into the scan">​</a></h3>
<p>"Top 10 relevant errors from the frontend service in the last hour" is two problems
glued together: a search and a filter. If the filter runs above the scan you score
every match and then throw most of it away, which gets worse the more selective
your filter is. That's backwards.</p>
<p>Index scans accept pushed table filters now. Covered columns filter inside the
scan at codec level, with a row-group zonemap short-circuit and one decode pass
that doubles as materialization, so a filtered top-k only ever collects rows that
pass. New <code>BitpackingFilter</code> and <code>AlpFilter</code> skip whole codec groups whose header
bounds already refute the predicate, without decoding them at all.</p>
<p>There's one new knob and it's a good one: a static <code>score &gt; c</code> bound is consumed at
pushdown. The top-k collector starts at that floor, streaming WAND seeds its
threshold from it, the filter vanishes from the plan and <code>EXPLAIN</code> prints
<code>Min Score</code>.</p>
<p>The numbers, all measured on our side:</p>
<ul>
<li>dense lookups against a parquet-backed view: <strong>from 40-66x the cost of a native
scan down to 1.6-3.1x</strong></li>
<li><code>ORDER BY x ASC LIMIT 5</code> on 1M rows: <strong>0.219 ms to 0.128 ms</strong>, 42% off</li>
<li>a deliberately adversarial DESC TopN over insert-ordered data: <strong>12 ms to
0.6 ms</strong>, 8,480 rows scanned instead of 250,000</li>
<li>impossible filters fold to an empty result at plan time and never touch storage</li>
</ul>
<p>None of this is search-specific. Plain analytical scans over columnar storage get
the same treatment.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="postgres-roles-and-grants-for-real">Postgres roles and grants, for real<a href="https://serenedb.com/blog/state-of-serene-2026-07#postgres-roles-and-grants-for-real" class="hash-link" aria-label="Direct link to Postgres roles and grants, for real" title="Direct link to Postgres roles and grants, for real">​</a></h3>
<p>Up to now SereneDB was effectively single-user. Fine on a laptop, a non-starter the
moment two people or a BI tool share an instance. RBAC is the thing that lets you
put it in front of a team. It's PostgreSQL's actual model rather than a lookalike,
so <code>GRANT</code>, <code>REVOKE</code>, role membership and column-level privileges behave the way
your existing scripts already expect.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> ROLE analyst LOGIN PASSWORD </span><span class="token string" style="color:#e3116c">'secret'</span><span class="token plain"> VALID UNTIL </span><span class="token string" style="color:#e3116c">'2027-01-01 00:00:00+00'</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">GRANT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> title</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> created_at</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> docs </span><span class="token keyword" style="color:#00009f">TO</span><span class="token plain"> analyst</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">GRANT</span><span class="token plain"> analyst </span><span class="token keyword" style="color:#00009f">TO</span><span class="token plain"> junior_analyst</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Roles, nested membership through <code>GRANT role TO role</code>, column grants, <code>VALID UNTIL</code>
expiry, <code>SET ROLE</code> mid-session and SCRAM over the wire.</p>
<p>The defaults are the part worth reading. The whole policy lives in HBA now instead
of a pile of per-connection special cases:</p>
<ul>
<li><code>psql -h 127.0.0.1 -U postgres</code> works with no password on a fresh install and
over the unix socket too. First connect stays trivial.</li>
<li>Superuser over the network requires a password. It used to be silently trusted
on <code>0.0.0.0</code>, which is the kind of default that ends up in an incident report.</li>
<li>Every non-superuser needs a password, local or remote, same as Postgres.</li>
<li><code>SERENEDB_INITIAL_PASSWORD=secret</code> on first boot seeds the superuser password,
so a container comes up remote-reachable and authenticated in one step.</li>
<li>Three superuser loopback trust lines get force-prepended to every ruleset, so a
broken HBA config can't lock the admin out of the box.</li>
</ul>
<p>The pre-RBAC <code>--auth_password</code>, <code>--auth_method</code> and <code>--auth_user</code> flags are gone.
<code>--auth_password</code> used to shadow RBAC entirely by pinning the server to one
hard-coded user. Per-role catalog passwords are the only auth path now.</p>
<p>There's a whole security chapter now, where every example on the page is a query we
actually run: <a href="https://docs.serenedb.com/security/roles" target="_blank" rel="noopener noreferrer">roles</a>,
<a href="https://docs.serenedb.com/security/privileges" target="_blank" rel="noopener noreferrer">privileges</a>,
<a href="https://docs.serenedb.com/security/role_membership" target="_blank" rel="noopener noreferrer">role membership</a> and
<a href="https://docs.serenedb.com/security/client_authentication" target="_blank" rel="noopener noreferrer">client authentication</a>.
Statement pages too:
<a href="https://docs.serenedb.com/sql/statements/create_role" target="_blank" rel="noopener noreferrer"><code>CREATE ROLE</code></a>,
<a href="https://docs.serenedb.com/sql/statements/grant" target="_blank" rel="noopener noreferrer"><code>GRANT</code></a> and
<a href="https://docs.serenedb.com/sql/statements/revoke" target="_blank" rel="noopener noreferrer"><code>REVOKE</code></a>.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="index-clickhouse-and-postgres-with-create-server">Index ClickHouse and Postgres with CREATE SERVER<a href="https://serenedb.com/blog/state-of-serene-2026-07#index-clickhouse-and-postgres-with-create-server" class="hash-link" aria-label="Direct link to Index ClickHouse and Postgres with CREATE SERVER" title="Direct link to Index ClickHouse and Postgres with CREATE SERVER">​</a></h3>
<p>The data you want to search is usually already sitting in Postgres or ClickHouse
and nobody's excited about maintaining a second copy of it.</p>
<p><code>CREATE SERVER</code> and <code>CREATE USER MAPPING</code> make a remote system a catalog object
that survives restart, so it's infrastructure rather than an <code>ATTACH</code> you have to
remember to redo every session. There's a new ClickHouse connector behind it too,
speaking the native TCP protocol with columnar blocks and LZ4 or ZSTD, with
projection, filter and TopN pushdown, a shared connection pool and read plus write
support.</p>
<p>The part that makes this more than plumbing: you can build an inverted index over
an attached ClickHouse or Postgres table. Search runs here, matched rows get
re-fetched from the remote by its own primary key. Full-text and vector search over
a ClickHouse table without moving the ClickHouse table.</p>
<p>Credentials never enter SQL text and <code>pg_foreign_server</code> redacts secret values,
which is a deliberate divergence from Postgres.</p>
<p>Docs: <a href="https://docs.serenedb.com/sql/statements/create_server" target="_blank" rel="noopener noreferrer"><code>CREATE SERVER</code></a> and
<a href="https://docs.serenedb.com/sql/indexes/inverted/external-data" target="_blank" rel="noopener noreferrer">external data</a>.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="string-compression-fsst-and-bitpacked-rle">String compression: FSST+ and bitpacked RLE<a href="https://serenedb.com/blog/state-of-serene-2026-07#string-compression-fsst-and-bitpacked-rle" class="hash-link" aria-label="Direct link to String compression: FSST+ and bitpacked RLE" title="Direct link to String compression: FSST+ and bitpacked RLE">​</a></h3>
<p>On log and URL data the string columns are the storage bill.
<a href="https://www.vldb.org/pvldb/vol13/p2649-boncz.pdf" target="_blank" rel="noopener noreferrer">FSST</a> encodes each dictionary
entry whole, so the redundancy between neighbouring entries goes unused: a symbol
table is global while shared prefixes are local.
<a href="https://homepages.cwi.nl/~boncz/msc/2025-YanLannaAlexandre.pdf" target="_blank" rel="noopener noreferrer">FSST+</a> cleaves
each entry into a shared prefix plus a suffix and stores every distinct prefix
once, which is exactly the shape URLs, paths and stack traces have. On ClickBench
<code>hits_10pct</code>:</p>
<table><thead><tr><th>column</th><th>saved</th><th>note</th></tr></thead><tbody><tr><td>URL</td><td>23.8%</td><td>279.7 MB to 213.1 MB</td></tr><tr><td>Referer</td><td>17.0%</td><td></td></tr><tr><td>Title</td><td>16.3%</td><td></td></tr></tbody></table>
<p>Reads came out at parity, within 2%, because the prefixes get FSST-decoded once
when the segment initializes. Writes cost about 1.4x.</p>
<p>RLE now bitpacks both its streams, run values and run counts, which is 1.3 to 2.4x
smaller on run-heavy data, 19 to 34% cheaper on the write path and 12 to 23% faster
to read at short run lengths. It also declines outright when every run is a
singleton instead of pretending to help.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="deploy-it-on-kubernetes-build-it-on-macos">Deploy it on Kubernetes, build it on macOS<a href="https://serenedb.com/blog/state-of-serene-2026-07#deploy-it-on-kubernetes-build-it-on-macos" class="hash-link" aria-label="Direct link to Deploy it on Kubernetes, build it on macOS" title="Direct link to Deploy it on Kubernetes, build it on macOS">​</a></h3>
<p>There's a Helm chart now, published with every release, so getting SereneDB onto a
cluster is one command:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">helm install mydb \</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  https://github.com/serenedb/serenedb/releases/download/helm-chart-v0.0.5/serenedb-0.0.5.tgz</span><br></span></code></pre></div></div>
<p>That gives you a single-node StatefulSet on a persistent volume, client and
headless Services, generated superuser credentials and a NetworkPolicy. Upgrades
recreate the pod on the same PVC, so serened shuts down gracefully and the new
version opens the same datadir. <code>helm test mydb</code> runs a <code>SELECT 1</code> against the
service to confirm it came up.</p>
<p>There's deliberately no <code>replicaCount</code>. SereneDB is a single-machine database and
the chart gives you exactly one database pod rather than pretending otherwise.</p>
<p>macOS is a supported build target now, which came out of
<a href="https://github.com/serenedb/serenedb/issues/883" target="_blank" rel="noopener noreferrer">someone asking</a> whether it builds
on Apple silicon. Flex and bison became optional in the same stretch, falling back
to a pre-generated parser when they aren't installed, so there's less to set up
before building from source.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="benchmark-season-searchbench-and-the-arangodb-numbers">Benchmark season: SearchBench and the ArangoDB numbers<a href="https://serenedb.com/blog/state-of-serene-2026-07#benchmark-season-searchbench-and-the-arangodb-numbers" class="hash-link" aria-label="Direct link to Benchmark season: SearchBench and the ArangoDB numbers" title="Direct link to Benchmark season: SearchBench and the ArangoDB numbers">​</a></h3>
<p>We said last month we were calling it benchmark season. Here's the first one.</p>
<p><a href="https://github.com/serenedb/searchbench" target="_blank" rel="noopener noreferrer">SearchBench</a> is our open benchmark for
search and analytics: 92 queries over OpenTelemetry logs at 100M and 1B records,
measuring ingest time, index size and median latency, methodology copied fairly
directly from ClickBench. Every adapter is seven shell scripts, so adding an engine
is an afternoon.</p>
<p>The first head-to-head is
<a href="https://serenedb.com/blog/searchbench-arangodb">SereneDB against ArangoDB</a>. We
started there because ArangoSearch runs on a fork of IResearch, our own search
library, taken in 2023. It's the closest thing we'll ever get to measuring
ten years of work against its own starting point.</p>
<p>At 100M records on the same machine: ingest and index <strong>20.5x</strong> faster, index
<strong>5.3x</strong> smaller and <strong>16.1x</strong> at the median across all 92 queries, 17.2x
geometric mean. SereneDB wins 89 of 92. It also answered the full billion with
nothing capped and nothing failed, at 46 ms median. ArangoDB's Community Edition
caps at 100 GiB of total database size and its 100M index is already 55.3 GiB, so
the billion was never on the table for that side.</p>
<p>Which leaves one comparison worth making anyway. Put SereneDB at a billion next to
ArangoDB at 100M and SereneDB is still ahead on every task family while carrying
ten times the data. Counts land at 20.5 ms against 77 ms, log tailing at 10 ms
against 289.5 ms, group-by at 277.5 ms against 1.443 s and joins at 2.1 s against
32.5 s.</p>
<p>Everything is reproducible. Raw per-query results for every engine are at
<a href="https://serenedb.com/searchbench/" target="_blank" rel="noopener noreferrer">serenedb.com/searchbench</a>. If you think we
undertuned something, the adapters are Apache-2.0 and we'd rather take a pull
request than defend a guess.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="docs-search-powered-by-serenedb">Docs search, powered by SereneDB<a href="https://serenedb.com/blog/state-of-serene-2026-07#docs-search-powered-by-serenedb" class="hash-link" aria-label="Direct link to Docs search, powered by SereneDB" title="Direct link to Docs search, powered by SereneDB">​</a></h3>
<p>We needed search on our docs. We make a search database. You can see where this
went.</p>
<p><a href="https://docs.serenedb.com/" target="_blank" rel="noopener noreferrer">docs.serenedb.com</a> and this blog now run hybrid search,
BM25 plus vectors, on a SereneDB instance, with streamed AI answers that cite their
sources. There's also an MCP endpoint, so an agent can query the docs directly
instead of scraping the HTML and guessing.</p>
<p>Then we packaged it, because everyone shipping docs has this problem. Point it at a
git repo, a folder, a live website or an S3 bucket and it indexes and re-syncs on
its own:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">npm install @serenedb/docs-search-react@latest</span><br></span></code></pre></div></div>
<p>There's a script-tag embed if you're not on React, a <code>serenedb/docs-search-backend</code>
image and a configurator that generates the compose file for you. Run it full-text
only or turn on hybrid. AI answers and the MCP server are separate opt-ins.</p>
<p>Docs: <a href="https://docs.serenedb.com/clients/serene-docs-search" target="_blank" rel="noopener noreferrer">Serene Docs Search</a>.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="also-shipped">Also shipped<a href="https://serenedb.com/blog/state-of-serene-2026-07#also-shipped" class="hash-link" aria-label="Direct link to Also shipped" title="Direct link to Also shipped">​</a></h3>
<ul>
<li><strong>You can bind a search condition as a query parameter.</strong> <code>TSQUERY</code> is a real
value type now, so <code>WHERE col @@ $1</code> works and an app builds its search query
server-side instead of concatenating SQL strings. Prepared statements, arrays of
<code>TSQUERY</code>, the operators (<code>||</code>, <code>&amp;&amp;</code>, <code>!!</code>, <code>##</code>, <code>^</code>) and
<code>'text'::tokenize('dict')</code> casts all come with it.</li>
<li><strong>Postgres introspection tells the truth now.</strong> <code>pg_depend</code>, <code>pg_rewrite</code> and
<code>pg_attrdef</code> were empty stubs, so the ~15 <code>information_schema</code> views that join
them silently returned nothing. They're derived from the real dependency graph
now and validated byte-for-byte against PostgreSQL 18. Two of them were also
handing out oids outside the 32-bit range, which broke any client decoding them
as <code>int4</code>. The size functions got the same treatment: <code>pg_relation_size</code>,
<code>pg_table_size</code>, <code>pg_total_relation_size</code>, <code>pg_indexes_size</code>,
<code>pg_database_size</code> and <code>pg_schema_size</code> return real bytes, where
<code>pg_indexes_size</code> used to be hardcoded to zero.</li>
<li><code>UNION</code> type end to end: DDL, <code>union_value()</code>, COPY, WAL recovery and use inside
inverted indexes.</li>
<li>Temporal types promoted to first class: <code>TIMESTAMP_S/_MS/_NS</code>, <code>TIMESTAMPTZ_NS</code>,
<code>TIMETZ</code> and <code>TIME_NS</code> across the binary wire and index range queries.
<code>TIMESTAMPTZ</code> text output follows the session <code>TimeZone</code> the way PG 18 does.</li>
<li>WAL group commit, with the fsync batched and pipelined across concurrently
committing transactions. Commit semantics don't change, only the physical fsync
moves. New snapshots stay bounded below the not-yet-durable suffix, so nothing
can read a commit a crash could lose.</li>
<li><code>sdb_progress</code>, one row per connection with pipeline progress and exact
tuple and byte counters for every write statement. <code>pg_stat_activity</code> was an
always-empty stub and is now a projection of it, along with every
<code>pg_stat_progress_*</code> view. Any query is cancellable with
<code>pg_cancel_backend(pid)</code>.</li>
<li>Per-index metrics in <code>sdb_metrics</code>, keyed by <code>relation_id</code> so you can join
against <code>pg_class.oid</code>: live docs, segments, files, index size, average commit
and consolidation and cleanup times, failure counters.</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-to-expect-in-august">What to expect in August<a href="https://serenedb.com/blog/state-of-serene-2026-07#what-to-expect-in-august" class="hash-link" aria-label="Direct link to What to expect in August" title="Direct link to What to expect in August">​</a></h2>
<ul>
<li><strong>Sloppy phrase search.</strong> A phrase is exact today and the <code>##</code> gap form pins a
fixed distance between two terms. Sloppy phrase gives the whole phrase a slop
budget instead, so a query still matches when the text drops a word in the
middle.</li>
<li><strong>Auto-refreshable remote search indices.</strong> Today an index over Parquet or
Iceberg in a bucket is a snapshot of what was there when you built it. Next
month it keeps itself current.</li>
<li><strong>Logical Postgres replication and triggers.</strong> Publications and subscriptions,
so an <code>ATTACH</code>'d Postgres can stream changes into SereneDB instead of being
re-read.</li>
<li><strong>Azure.</strong> Object storage support beyond S3.</li>
<li><strong>Faster indexing.</strong> More write throughput on top of July's parallel backfill.</li>
<li><strong>Faster recovery.</strong> Less time between starting serened and answering queries
after an unclean shutdown.</li>
<li><strong>More benchmarks.</strong> ArangoDB is the first of several. Same rules every time:
open adapters, published methodology, raw numbers and the best configuration we
can build for every engine on the table.</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="kudos">Kudos<a href="https://serenedb.com/blog/state-of-serene-2026-07#kudos" class="hash-link" aria-label="Direct link to Kudos" title="Direct link to Kudos">​</a></h2>
<p>Some of the most useful input we got in July came from people running SereneDB on
their own data and telling us exactly what needs to be improved. A report with a
repro is worth as much as a patch and it's usually harder to write.</p>
<p>Thanks to <a href="https://github.com/rodion-m" target="_blank" rel="noopener noreferrer"><strong>rodion-m</strong></a> for putting inverted indexes
under a large code corpus and reporting everything that broke
(<a href="https://github.com/serenedb/serenedb/issues/970" target="_blank" rel="noopener noreferrer">#970</a>,
<a href="https://github.com/serenedb/serenedb/issues/971" target="_blank" rel="noopener noreferrer">#971</a>,
<a href="https://github.com/serenedb/serenedb/issues/972" target="_blank" rel="noopener noreferrer">#972</a>),
<a href="https://github.com/deadtrickster" target="_blank" rel="noopener noreferrer"><strong>deadtrickster</strong></a> for a sharp run of reports on
vector filtering and BM25 scoring
(<a href="https://github.com/serenedb/serenedb/issues/961" target="_blank" rel="noopener noreferrer">#961</a>,
<a href="https://github.com/serenedb/serenedb/issues/962" target="_blank" rel="noopener noreferrer">#962</a>,
<a href="https://github.com/serenedb/serenedb/issues/964" target="_blank" rel="noopener noreferrer">#964</a>),
<a href="https://github.com/a-soll" target="_blank" rel="noopener noreferrer"><strong>a-soll</strong></a> for asking about Apple silicon
(<a href="https://github.com/serenedb/serenedb/issues/883" target="_blank" rel="noopener noreferrer">#883</a>) before SereneDB could
build there and <a href="https://github.com/emarsden" target="_blank" rel="noopener noreferrer"><strong>emarsden</strong></a> for holding the SQL to
the standard (<a href="https://github.com/serenedb/serenedb/issues/934" target="_blank" rel="noopener noreferrer">#934</a>,
<a href="https://github.com/serenedb/serenedb/issues/935" target="_blank" rel="noopener noreferrer">#935</a>). Several of those are
fixed in this release. The rest are open with our names on them.</p>
<p>There's also good work in flight we owe review time to:
<a href="https://github.com/w3lld1" target="_blank" rel="noopener noreferrer"><strong>w3lld1</strong></a> on multi-column search highlights
(<a href="https://github.com/serenedb/serenedb/pull/932" target="_blank" rel="noopener noreferrer">#932</a>),
<a href="https://github.com/ivan-digital" target="_blank" rel="noopener noreferrer"><strong>ivan-digital</strong></a> on an embeddable
IResearch-only build target
(<a href="https://github.com/serenedb/serenedb/pull/976" target="_blank" rel="noopener noreferrer">#976</a>),
<a href="https://github.com/romanpovol" target="_blank" rel="noopener noreferrer"><strong>romanpovol</strong></a> on edge-ngram plus better stemming
and normalizing tokenizers
(<a href="https://github.com/serenedb/serenedb/pull/659" target="_blank" rel="noopener noreferrer">#659</a>) and
<a href="https://github.com/afigor2701" target="_blank" rel="noopener noreferrer"><strong>afigor2701</strong></a> on pfor encoding
(<a href="https://github.com/serenedb/serenedb/pull/780" target="_blank" rel="noopener noreferrer">#780</a>).</p>
<p>Want to be in the next one? We tag beginner-friendly work with
<a href="https://github.com/serenedb/serenedb/contribute" target="_blank" rel="noopener noreferrer"><code>good first issue</code></a>, so grab one,
ask questions in the issue and we'll get you going.</p>
<hr>
<p>Everything above is in
<a href="https://github.com/serenedb/serenedb/releases/tag/v26.07.5" target="_blank" rel="noopener noreferrer"><strong><code>v26.07.5</code></strong></a>. Grab
it and point it at something. If you'd rather look before you install, the
<a href="http://codesearch.serenedb.com/" target="_blank" rel="noopener noreferrer">code search demo</a> is live and <code>⌘K</code> on
<a href="https://docs.serenedb.com/" target="_blank" rel="noopener noreferrer">the docs</a> gets you SereneDB searching SereneDB's
documentation. Every raw benchmark result is at
<a href="https://serenedb.com/searchbench/" target="_blank" rel="noopener noreferrer">serenedb.com/searchbench</a>.</p>
<p>If you like what you see, ⭐ <a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer">star us on GitHub</a>.
Hit a rough edge? Open an issue. The Kudos section above is people who did exactly
that.</p>
<p>See you in the next State of Serene.</p></div>]]></content>
        <author>
            <name>Andrey Abramov</name>
            <uri>https://www.linkedin.com/in/andrey-abramov-57a16241/ru/?skipRedirect=true</uri>
        </author>
        <category label="News" term="News"/>
        <category label="Search" term="Search"/>
        <category label="Database" term="Database"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[SereneDB vs ArangoDB on search performance]]></title>
        <id>https://serenedb.com/blog/searchbench-arangodb</id>
        <link href="https://serenedb.com/blog/searchbench-arangodb"/>
        <updated>2026-07-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[One year after we started SereneDB we ran 92 search and analytics queries over 1B OpenTelemetry logs and put the numbers next to ArangoDB to see how far we have come.]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="none" viewBox="0 0 28 28" class="logoAvatar_BINX"><path fill="#895AF8" d="M28 14c0-7.732-6.268-14-14-14S0 6.268 0 14s6.268 14 14 14 14-6.268 14-14"></path><path fill="#80BEFF" d="M9.345 18.655c4.556 4.556 11.416 5.084 15.32 1.178 3.906-3.905 3.379-10.764-1.178-15.32C18.931-.043 12.072-.571 8.167 3.334S4.789 14.1 9.345 18.654"></path><path fill="#fff" d="M15.649 12.35c2.278 2.278 5.676 2.574 7.59.66s1.617-5.311-.66-7.59c-2.279-2.277-5.677-2.573-7.59-.66-1.914 1.914-1.618 5.312.66 7.59"></path></svg></div></div><div class="copy_iHNG"><p class="names_ox2I">SereneDB Team</p><p class="meta_OgzX">Jul 26, 2026 · 25 minutes read</p></div></header><h2 class="title_RQ9v">SereneDB vs ArangoDB on search performance</h2><p style="font-size:0.9rem;margin-top:-0.4rem">92 queries over 1B logs and a look at how IResearch has evolved since 2016</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg hideFirstHeading_VVMX" data-image-lightbox-root="true"><p>SereneDB is a database that runs search and analytics in the same query engine. The search half is a C++ library called <a href="https://github.com/serenedb/serenedb/tree/12896b8ed02b2c2664f840df9ed79578f504893e/libs/iresearch" target="_blank" rel="noopener noreferrer">IResearch</a> that we started in 2016 and still maintain and if that sounds familiar it is because it is what ArangoSearch, a search engine integrated into ArangoDB, runs on.</p>
<p>A billion OpenTelemetry logs, 92 queries and on the other side of the table a ten year old fork of our own code. ArangoDB still ships IResearch, wired into a real database and answering real queries, so for once we get to benchmark ourselves against ourselves. Back in March we published that IResearch <a href="https://serenedb.com/blog/search-benchmark-game-overview">won "Search Benchmark, The Game"</a>, but that was a microbenchmark and it said nothing about the database around it. Ten years of work either shows up in the numbers or it doesn't and this is how you find out.</p>
<p>Everything below is open so you can rerun it yourself. The head-to-head runs at 100M because that is as far as ArangoDB's Community Edition is licensed to go and past that it stops being a performance question and turns into a licensing one.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="tldr">TL;DR<a href="https://serenedb.com/blog/searchbench-arangodb#tldr" class="hash-link" aria-label="Direct link to TL;DR" title="Direct link to TL;DR">​</a></h2>
<p>If you only want the numbers.</p>
<div style="width:calc(100% + 32px);aspect-ratio:2984 / 1860;min-height:600px;margin:8px -16px;overflow:hidden"><iframe class="frame_pmeK lightFrame_C3KP" src="https://serenedb.com/searchbench/?s=eyJ2IjoxLCJkIjoib3RlbF9sb2dzXzEwMG0iLCJoIjoiRWxhc3RpY3NlYXJjaCxPcGVuU2VhcmNoLFBhcmFkZURCIiwidGgiOiJsaWdodCIsInAiOiJjYXRlZ29yaWVzLGNvbmZpZyxkYXRhc2V0LGV4cGFuZCxoZWFkZXIscGFydGljaXBhbnRzLHF1ZXJ5In0" title="Interactive SearchBench results — light theme" loading="eager" allowfullscreen="" sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox"></iframe><iframe class="frame_pmeK darkFrame_MH1O" src="https://serenedb.com/searchbench/?s=eyJ2IjoxLCJkIjoib3RlbF9sb2dzXzEwMG0iLCJoIjoiRWxhc3RpY3NlYXJjaCxPcGVuU2VhcmNoLFBhcmFkZURCIiwicCI6ImNhdGVnb3JpZXMsY29uZmlnLGRhdGFzZXQsZXhwYW5kLGhlYWRlcixwYXJ0aWNpcGFudHMscXVlcnkifQ" title="Interactive SearchBench results — dark theme" loading="eager" allowfullscreen="" sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox"></iframe></div>
<p>Results are available at <a href="https://serenedb.com/searchbench/" target="_blank" rel="noopener noreferrer">serenedb.com/searchbench</a> and the benchmark itself lives at <a href="https://github.com/serenedb/searchbench" target="_blank" rel="noopener noreferrer">github.com/serenedb/searchbench</a>.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-searchbench-is">What SearchBench is<a href="https://serenedb.com/blog/searchbench-arangodb#what-searchbench-is" class="hash-link" aria-label="Direct link to What SearchBench is" title="Direct link to What SearchBench is">​</a></h2>
<p>That March benchmark is the <a href="https://github.com/quickwit-oss/search-benchmark-game" target="_blank" rel="noopener noreferrer">search-benchmark-game</a>, built by the Tantivy team and <a href="https://github.com/serenedb/search-benchmark-game" target="_blank" rel="noopener noreferrer">forked</a> by us to add an IResearch engine. One process, one index, term and phrase and boolean queries, count and top-100 by score. It measures the search library on its own: decode the posting lists, intersect them, score the top 100. There is no SQL layer, no query planner, no columnar storage involved and no joins. So a win there tells you the postings format and the scoring loop are fast and almost nothing about what a database built on top of them does with a real query.</p>
<p>So we built <a href="https://github.com/serenedb/searchbench" target="_blank" rel="noopener noreferrer">SearchBench</a>, an open benchmark for search and analytics, which we <a href="https://www.youtube.com/watch?v=nj_Vgrbgtbk&amp;t=965s" target="_blank" rel="noopener noreferrer">announced at Berlin Buzzwords</a>. Same idea as Search Benchmark, The Game, moved end to end: how fast does the whole database answer a real question.</p>
<p>The methodology is heavily inspired by <a href="https://github.com/ClickHouse/ClickBench" target="_blank" rel="noopener noreferrer">ClickBench</a>. 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 on both engines. Each engine implements the same handful of shell scripts (<code>install</code>, <code>start</code>, <code>stop</code>, <code>check</code>, <code>load</code>, <code>query</code>, <code>data-size</code>) and a shared driver orchestrates them, so adding an engine is an afternoon.</p>
<p>One rule sits above all of that: every engine gets our best attempt at the best configuration we can build for it. Without that none of these numbers are worth publishing.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-data">The data<a href="https://serenedb.com/blog/searchbench-arangodb#the-data" class="hash-link" aria-label="Direct link to The data" title="Direct link to The data">​</a></h3>
<p>The corpus is generated OpenTelemetry logs. We use the set published by <a href="https://github.com/ClickHouse/TextBench" target="_blank" rel="noopener noreferrer">TextBench</a> instead of rolling our own. Every record has 15 columns describing one log message:</p>
<table><thead><tr><th></th><th></th></tr></thead><tbody><tr><td><code>Timestamp</code></td><td>when</td></tr><tr><td><code>TraceId</code>, <code>SpanId</code>, <code>TraceFlags</code></td><td>trace correlation</td></tr><tr><td><code>SeverityText</code>, <code>SeverityNumber</code></td><td>log level, as text and as a number</td></tr><tr><td><code>ServiceName</code></td><td>which service emitted it</td></tr><tr><td><code>Body</code></td><td>the message itself and the only full-text field</td></tr><tr><td><code>ResourceSchemaUrl</code>, <code>ResourceAttributes</code></td><td>what produced the log</td></tr><tr><td><code>ScopeSchemaUrl</code>, <code>ScopeName</code>, <code>ScopeVersion</code>, <code>ScopeAttributes</code></td><td>which instrumentation scope</td></tr><tr><td><code>LogAttributes</code></td><td>per-record key/values</td></tr></tbody></table>
<p>The three <code>*Attributes</code> columns are JSON maps. <strong>The database has to store all 15 columns</strong>, 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.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-queries">The queries<a href="https://serenedb.com/blog/searchbench-arangodb#the-queries" class="hash-link" aria-label="Direct link to The queries" title="Direct link to The queries">​</a></h3>
<p>92 of them, in five groups:</p>
<table><thead><tr><th>group</th><th>what it is</th></tr></thead><tbody><tr><td><code>count</code></td><td>how many logs match</td></tr><tr><td><code>top_k (bm25)</code></td><td><strong>top 100 ordered by relevance</strong>, BM25 scored</td></tr><tr><td><code>group_by</code></td><td>matches bucketed by service or by time</td></tr><tr><td><code>top_k (time)</code></td><td><strong>top 100 ordered by timestamp</strong>, newest first, which is log tailing</td></tr><tr><td><code>join</code></td><td>correlate two services through a shared trace id</td></tr></tbody></table>
<p>Two of those five are top-100 problems and they differ only in the ordering key: <code>top_k (bm25)</code> ranks by relevance, <code>top_k (time)</code> ranks by timestamp. Keeping them apart matters, because the two get answered by completely different machinery.</p>
<p>Each group 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% of your logs and a word in 0.001% of them are different problems.</p>
<p>That grid is what produces 92. Every query carries its tags in the result file, so you can slice by group or by filter or by term frequency instead of staring at one aggregate. In the raw files the two top-100 groups are tagged <code>top_k</code> and <code>recent</code>.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="why-arangodb-is-the-right-mirror">Why ArangoDB is the right mirror<a href="https://serenedb.com/blog/searchbench-arangodb#why-arangodb-is-the-right-mirror" class="hash-link" aria-label="Direct link to Why ArangoDB is the right mirror" title="Direct link to Why ArangoDB is the right mirror">​</a></h2>
<p>The comparison is worth running because of where the other side comes from. ArangoDB is where an older version of our own code still runs in production and that makes it the closest thing we have to a time machine.</p>
<p>And we do mean our own code. The lineage is traceable in public:</p>
<ul>
<li><strong>October 2016.</strong> We open <a href="https://github.com/iresearch-toolkit/iresearch" target="_blank" rel="noopener noreferrer"><code>iresearch-toolkit/iresearch</code></a>. That repo is archived today.</li>
<li><strong>December 2023.</strong> ArangoDB <a href="https://github.com/arangodb/iresearch" target="_blank" rel="noopener noreferrer">forks it into their own org</a>.</li>
<li><strong>January 2026.</strong> They move it out of <code>3rdParty</code> and <a href="https://github.com/arangodb/arangodb/commit/7f11dc39f5ad9928b2f748837616e72f2d2f7cb4" target="_blank" rel="noopener noreferrer">copy it into the main tree</a> at <code>lib/iresearch</code>. A little over a million lines in one commit.</li>
</ul>
<p>What happened in between is what makes this a fair mirror. 51 commits in the two and a half years since the fork and a few of those are <a href="https://github.com/arangodb/iresearch/commit/2fafdc48559a70471b52a9b58105b86625a2b66a" target="_blank" rel="noopener noreferrer">merges</a> pulling our upstream work back in. The rest is maintenance: <a href="https://github.com/arangodb/iresearch/commit/4a92edcf1dfc9630b1b4452773f17789c36cf70d" target="_blank" rel="noopener noreferrer">new consolidation defaults</a>, a <a href="https://github.com/arangodb/iresearch/commit/707a0919a1025e9c0bcba0fbc1a0a940f74df3ff" target="_blank" rel="noopener noreferrer">clang compatibility pass</a>, a <a href="https://github.com/arangodb/iresearch/commit/9a0ca084f89bbeeefffbebb65bee1b18ecb19b06" target="_blank" rel="noopener noreferrer">memory over-allocation fix</a>, an <a href="https://github.com/arangodb/iresearch/commit/a555e0a8b439d94be6059bd721ce89fa1f3dd873" target="_blank" rel="noopener noreferrer">iterator <code>seek</code> edge case</a>. The same codebase took 1,052 commits in 2017 alone.</p>
<p>So that is a stable dependency doing its job. It also means the engine on the other side of this benchmark is close to the one we handed over, which is what makes it a mirror. Most of what the numbers show is what changed on our side.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="how-we-set-it-up">How we set it up<a href="https://serenedb.com/blog/searchbench-arangodb#how-we-set-it-up" class="hash-link" aria-label="Direct link to How we set it up" title="Direct link to How we set it up">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-machine">The machine<a href="https://serenedb.com/blog/searchbench-arangodb#the-machine" class="hash-link" aria-label="Direct link to The machine" title="Direct link to The machine">​</a></h3>
<p>Both engines got 100M OpenTelemetry log records on the same machine: a single GCP <code>n2-standard-32</code>, which is 32 vCPUs of Intel Ice Lake at 2.6 GHz and 128 GB of RAM, running Ubuntu 24.04.4 LTS. Storage is a 3.9 TB <code>pd-ssd</code> Google Persistent Disk. One instance each, no cluster and no sharding on either side. SereneDB is <a href="https://github.com/serenedb/serenedb/releases/tag/v26.07.5" target="_blank" rel="noopener noreferrer"><code>v26.07.5</code></a> and ArangoDB is version <code>3.12.9-4</code>, Community Edition.</p>
<p>Community Edition needs a note, because "we benchmarked the free tier" usually means "we benchmarked a crippled build". Not here. Since 3.12.5 the Community Edition <a href="https://docs.arango.ai/arangodb/stable/release-notes/version-3.12/whats-new-in-3-12/#all-enterprise-edition-features-in-community-edition" target="_blank" rel="noopener noreferrer">ships every Enterprise feature</a> with no time limit. So feature for feature, this is the best ArangoDB there is. What it costs you is the right to run it commercially in production and a 100 GiB ceiling on total database size. That ceiling matters a lot later on.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-data-model">The data model<a href="https://serenedb.com/blog/searchbench-arangodb#the-data-model" class="hash-link" aria-label="Direct link to The data model" title="Direct link to The data model">​</a></h3>
<p>None of the numbers mean anything unless both indexes have the same information to work with. The ArangoSearch view below is the fastest one we managed to build for this workload and every field the queries touch is indexed the same way on both sides. Here is exactly what each engine got.</p>
<p><strong>Tokenization.</strong> SereneDB indexes the expression <code>ts_split_by_non_alpha(Body, true)</code>, which lowercases and splits on runs of non-alphanumeric characters, over a <code>keyword</code> dictionary with frequency, norm and position enabled. ArangoSearch gets a <code>segmentation</code> analyzer with <code>break=alpha</code> and <code>case=lower</code> and the same three features. Those two are the closest analogs the two engines have. They agree on ordinary log text and they can disagree at the margins, on things like <code>v1.2.3</code> or <code>user_id</code>, so the term dictionaries are not identical down to the byte. That difference is worth saying out loud and it is nowhere near the size of the gaps below.</p>
<p><strong>Stored columns.</strong> SereneDB's index carries all 15 OTel columns as <code>INCLUDE</code> columns, so retrieval, filtering and aggregation run index-only. The ArangoSearch view gets <code>storedValues</code> covering every column the queries return, which is the same deal. Neither engine has to go back to a row store to answer a query.</p>
<p><strong>Scored top-K</strong>, meaning the <code>top_k (bm25)</code> family and not <code>top_k (time)</code>. SereneDB's index is built with <code>optimize_top_k = 'bm25(1.2, 0.75)'</code>, our own WAND implementation inside IResearch. The ArangoSearch view is created with <code>optimizeTopK</code> enabled and the matching <a href="https://docs.arango.ai/arangodb/3.12/aql/functions/arangosearch/#bm25" target="_blank" rel="noopener noreferrer"><code>BM25</code></a> parameters. Both sides get to skip blocks they can prove cannot make the top of the list.</p>
<p><strong>Query translation.</strong> Every SereneDB query has a hand-written AQL twin with the same filters, the same windows, the same grouping, the same sort and the same limit. The mapping is mechanical and both sides are documented: <a href="https://docs.serenedb.com/sql/functions/search/full-text" target="_blank" rel="noopener noreferrer">SereneDB full-text functions</a> against <a href="https://docs.arango.ai/arangodb/3.12/aql/functions/arangosearch/" target="_blank" rel="noopener noreferrer">ArangoSearch AQL functions</a>.</p>
<table><thead><tr><th>SereneDB</th><th>ArangoSearch</th></tr></thead><tbody><tr><td><a href="https://docs.serenedb.com/sql/functions/search/full-text#match-operator" target="_blank" rel="noopener noreferrer"><code>@@ 'term'</code></a></td><td><a href="https://docs.arango.ai/arangodb/3.12/aql/functions/arangosearch/#analyzer" target="_blank" rel="noopener noreferrer"><code>ANALYZER(d.Body == 'term', 'seg')</code></a></td></tr><tr><td><a href="https://docs.serenedb.com/sql/functions/search/full-text#ts_all" target="_blank" rel="noopener noreferrer"><code>ts_all([...])</code></a></td><td><a href="https://docs.arango.ai/arangodb/3.12/aql/functions/arangosearch/#analyzer" target="_blank" rel="noopener noreferrer"><code>ANALYZER(d.Body == 'a' AND d.Body == 'b', 'seg')</code></a></td></tr><tr><td><a href="https://docs.serenedb.com/sql/functions/search/full-text#ts_any" target="_blank" rel="noopener noreferrer"><code>ts_any([...])</code></a></td><td><a href="https://docs.arango.ai/arangodb/3.12/aql/functions/arangosearch/#analyzer" target="_blank" rel="noopener noreferrer"><code>ANALYZER(d.Body IN ['a','b'], 'seg')</code></a></td></tr><tr><td><a href="https://docs.serenedb.com/sql/functions/search/full-text#ts_any" target="_blank" rel="noopener noreferrer"><code>ts_any([...], k)</code></a></td><td><a href="https://docs.arango.ai/arangodb/3.12/aql/functions/arangosearch/#min_match" target="_blank" rel="noopener noreferrer"><code>MIN_MATCH(c1, c2, ..., k)</code></a></td></tr><tr><td><a href="https://docs.serenedb.com/sql/functions/search/full-text#ts_phrase" target="_blank" rel="noopener noreferrer"><code>ts_phrase(...)</code></a></td><td><a href="https://docs.arango.ai/arangodb/3.12/aql/functions/arangosearch/#phrase" target="_blank" rel="noopener noreferrer"><code>PHRASE(d.Body, 'a b c', 'seg')</code></a></td></tr><tr><td><a href="https://docs.serenedb.com/sql/functions/search/full-text#a--b-phrase" target="_blank" rel="noopener noreferrer"><code>a ## b</code></a></td><td><a href="https://docs.arango.ai/arangodb/3.12/aql/functions/arangosearch/#phrase" target="_blank" rel="noopener noreferrer"><code>PHRASE(d.Body, 'a', N, 'b', 'seg')</code></a></td></tr><tr><td><a href="https://docs.serenedb.com/sql/functions/search/full-text#ts_starts_with" target="_blank" rel="noopener noreferrer"><code>ts_starts_with(...)</code></a></td><td><a href="https://docs.arango.ai/arangodb/3.12/aql/functions/arangosearch/#starts_with" target="_blank" rel="noopener noreferrer"><code>STARTS_WITH(d.Body, 'p')</code></a></td></tr><tr><td><a href="https://docs.serenedb.com/sql/functions/search/full-text#ts_like" target="_blank" rel="noopener noreferrer"><code>ts_like(...)</code></a></td><td><a href="https://docs.arango.ai/arangodb/3.12/aql/functions/arangosearch/#like" target="_blank" rel="noopener noreferrer"><code>LIKE(d.Body, 'conn%')</code></a></td></tr><tr><td><a href="https://docs.serenedb.com/sql/functions/search/full-text#ts_levenshtein" target="_blank" rel="noopener noreferrer"><code>ts_levenshtein(...)</code></a></td><td><a href="https://docs.arango.ai/arangodb/3.12/aql/functions/arangosearch/#levenshtein_match" target="_blank" rel="noopener noreferrer"><code>LEVENSHTEIN_MATCH(d.Body, 't', dist)</code></a></td></tr><tr><td><a href="https://docs.serenedb.com/sql/functions/search/full-text#ts_regexp" target="_blank" rel="noopener noreferrer"><code>ts_regexp(...)</code></a></td><td>no equivalent, see below</td></tr></tbody></table>
<p>That last row is the one place we had to improvise. ArangoDB has no regular-expression matcher inside <code>SEARCH</code> at all, so the nine queries that use one are hand-translated into whichever primitive comes closest: <code>STARTS_WITH(d.Body, 'charg')</code> where the pattern is a prefix followed by <code>.*</code>, <code>LIKE(d.Body, 'c_che')</code> where a <code>.</code> sits inside the word. We could have marked those queries unsupported and moved on. Giving ArangoDB the nearest primitive instead hands it an advantage, because <code>STARTS_WITH</code> walks one contiguous range of the term dictionary while the regex it stands in for compiles to an automaton that can walk all of it. Nine of the 92 queries are tilted ArangoDB's way and we would rather say so up front.</p>
<p>Both query files are in the repo. If a translation looks unfair the diff is right there and we want to hear about it.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="how-we-measure">How we measure<a href="https://serenedb.com/blog/searchbench-arangodb#how-we-measure" class="hash-link" aria-label="Direct link to How we measure" title="Direct link to How we measure">​</a></h3>
<p><strong>Timing.</strong> Three runs per query and we report the best of the last two. Between queries we stop the engine, drop the OS page cache, start it again and wait for it to report healthy. Each measurement is the client round-trip for that one query, taken from psql's <code>\timing</code> on the Postgres-wire side and curl's <code>%{time_total}</code> on the HTTP side, so the cost of spawning and connecting a client stays out of the number.</p>
<p><strong>The 60 second rule.</strong> Any single query gets 60 seconds. If it does not finish, we record that it did not finish. There are several of those below and they are all on the ArangoDB side, so we want to be precise about what it means: it is not a latency of 60 seconds, it is a query that never came back.</p>
<p><strong>What this does not measure.</strong> The driver runs one query at a time from a single client, so everything here is latency and none of it is throughput. There is no concurrent load, no ingest running alongside queries, no updates or deletes and nothing distributed. Memory footprint is not recorded either. Those are all fair questions about a log store and none of them are answered here.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="load-and-size">Load and size<a href="https://serenedb.com/blog/searchbench-arangodb#load-and-size" class="hash-link" aria-label="Direct link to Load and size" title="Direct link to Load and size">​</a></h2>
<p>Before any query runs, there is the part where you wait.</p>
<figure class="chart_Gfup"><div class="inner_O5vv" style="--benchmark-series-count:2"><div class="header_yZr_"><figcaption class="title_pL0r">Load and size, 100M logs</figcaption><div class="legend_BXPX" aria-label="Chart legend"><span><i aria-hidden="true" style="background-color:#895af8"></i>SereneDB</span><span><i aria-hidden="true" style="background-color:#20b89a"></i>ArangoDB</span></div></div><p class="scaleNote_An4h">Bars are normalized within each metric; labels show absolute values.</p><div class="plots_Hize" role="img" aria-label="Load and size, 100M logs. ingest + index: SereneDB: 54 s, ArangoDB: 18.6 min. index on disk: SereneDB: 10.5 GiB, ArangoDB: 55.3 GiB"><div class="row_YNFy"><div class="plot_VSLA"><div class="group_AVyR"><div class="bars_dL7M"><div class="column_HUZK" title="SereneDB: 54 s"><span class="value_n384" style="--benchmark-bar-height:4.064516129032258%">54 s</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#895af8;--benchmark-bar-height:4.064516129032258%"></span></div><div class="column_HUZK" title="ArangoDB: 18.6 min"><span class="value_n384" style="--benchmark-bar-height:84%">18.6 min</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#20b89a;--benchmark-bar-height:84%"></span></div></div><span class="category_d8eb">ingest + index</span></div><div class="group_AVyR"><div class="bars_dL7M"><div class="column_HUZK" title="SereneDB: 10.5 GiB"><span class="value_n384" style="--benchmark-bar-height:15.949367088607596%">10.5 GiB</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#895af8;--benchmark-bar-height:15.949367088607596%"></span></div><div class="column_HUZK" title="ArangoDB: 55.3 GiB"><span class="value_n384" style="--benchmark-bar-height:84%">55.3 GiB</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#20b89a;--benchmark-bar-height:84%"></span></div></div><span class="category_d8eb">index on disk</span></div></div></div></div></div></figure>
<p>Ingest is <strong>20.5x</strong> faster and the index is <strong>5.3x</strong> smaller. Both numbers cover the same work: read the corpus, tokenize the body, build the inverted index, store every column for retrieval.</p>
<p>The ingest number needs one note, because the two sides do not get the data exactly the same way. Both engines run in Docker with <code>--network host</code>. SereneDB builds the index straight off the parquet: the table is a view over <code>read_parquet</code> and <code>CREATE INDEX</code> consumes it in process, so nothing gets serialized and nothing crosses a socket. ArangoDB cannot read parquet at all, so the corpus goes through a throwaway serened container that emits NDJSON on stdout, <code>arangoimport</code> reads that over a pipe at its own default parallelism and inserts the documents and only then does the view link index them. Some of those 18.6 minutes is JSON serialization and JSONL parsing rather than index building. We report the pipeline because it is the pipeline you would actually have to run, but if you want the pure indexing cost on the ArangoDB side it might actually be lower.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="query-latency">Query latency<a href="https://serenedb.com/blog/searchbench-arangodb#query-latency" class="hash-link" aria-label="Direct link to Query latency" title="Direct link to Query latency">​</a></h2>
<p>Median query latency per task family.</p>
<figure class="chart_Gfup"><div class="inner_O5vv" style="--benchmark-series-count:2"><div class="header_yZr_"><figcaption class="title_pL0r">Median hot-query latency by task, 100M logs</figcaption><div class="legend_BXPX" aria-label="Chart legend"><span><i aria-hidden="true" style="background-color:#895af8"></i>SereneDB</span><span><i aria-hidden="true" style="background-color:#20b89a"></i>ArangoDB</span></div></div><p class="scaleNote_An4h">Bars are normalized within each metric; labels show absolute values.</p><div class="plots_Hize" role="img" aria-label="Median hot-query latency by task, 100M logs. count: SereneDB: 8 ms, ArangoDB: 77 ms. top-K BM25: SereneDB: 20 ms, ArangoDB: 97 ms. group by: SereneDB: 46.5 ms, ArangoDB: 1.443 s. top-K time: SereneDB: 8.5 ms, ArangoDB: 289.5 ms. join: SereneDB: 257 ms, ArangoDB: 32.48 s"><div class="row_YNFy"><div class="plot_VSLA"><div class="group_AVyR"><div class="bars_dL7M"><div class="column_HUZK" title="SereneDB: 8 ms"><span class="value_n384" style="--benchmark-bar-height:3%">8 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#895af8;--benchmark-bar-height:3%"></span></div><div class="column_HUZK" title="ArangoDB: 77 ms"><span class="value_n384" style="--benchmark-bar-height:6.468%">77 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#20b89a;--benchmark-bar-height:6.468%"></span></div></div><span class="category_d8eb">count</span></div><div class="group_AVyR"><div class="bars_dL7M"><div class="column_HUZK" title="SereneDB: 20 ms"><span class="value_n384" style="--benchmark-bar-height:3%">20 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#895af8;--benchmark-bar-height:3%"></span></div><div class="column_HUZK" title="ArangoDB: 97 ms"><span class="value_n384" style="--benchmark-bar-height:8.148%">97 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#20b89a;--benchmark-bar-height:8.148%"></span></div></div><span class="category_d8eb">top-K BM25</span></div><div class="group_AVyR"><div class="bars_dL7M"><div class="column_HUZK" title="SereneDB: 46.5 ms"><span class="value_n384" style="--benchmark-bar-height:3%">46.5 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#895af8;--benchmark-bar-height:3%"></span></div><div class="column_HUZK" title="ArangoDB: 1.443 s"><span class="value_n384" style="--benchmark-bar-height:84%">1.443 s</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#20b89a;--benchmark-bar-height:84%"></span></div></div><span class="category_d8eb">group by</span></div><div class="group_AVyR"><div class="bars_dL7M"><div class="column_HUZK" title="SereneDB: 8.5 ms"><span class="value_n384" style="--benchmark-bar-height:3%">8.5 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#895af8;--benchmark-bar-height:3%"></span></div><div class="column_HUZK" title="ArangoDB: 289.5 ms"><span class="value_n384" style="--benchmark-bar-height:24.317999999999998%">289.5 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#20b89a;--benchmark-bar-height:24.317999999999998%"></span></div></div><span class="category_d8eb">top-K time</span></div><div class="group_AVyR"><div class="bars_dL7M"><div class="column_HUZK" title="SereneDB: 257 ms"><span class="value_n384" style="--benchmark-bar-height:3%">257 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#895af8;--benchmark-bar-height:3%"></span></div><div class="column_HUZK" title="ArangoDB: 32.48 s"><span class="value_n384" style="--benchmark-bar-height:84%">32.48 s</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#20b89a;--benchmark-bar-height:84%"></span></div></div><span class="category_d8eb">join</span></div></div></div></div></div></figure>
<p><strong>2 of the 9 join queries never came back.</strong> They hit our 60 second ceiling and we record them as 60 seconds, which understates ArangoDB's real time and therefore understates the gap. Both sit above the join median of 32.5 s, so neither is what that bar is measuring. The other seven finish between 13 and 45 seconds. Drop the two capped queries and the median across the remaining 90 is 15.6x, against 16.1x for all 92.</p>
<p><strong>16.1x at the median, 17.2x geometric mean, across all 92 queries.</strong> Our best single query is 460x: a time-windowed top-100 that takes us 8 ms and ArangoDB 3.7 s. We quote the median instead because that is the number that tells you what the database feels like to use. It is also the number that survives an argument.</p>
<p>SereneDB is faster on 89 of the 92 queries. On the three it loses, both engines land somewhere between 1 ms and 15 ms, close enough to the floor that run-to-run variance decides the winner rather than anything about the engines.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-billion-logs-benchmark">The billion logs benchmark<a href="https://serenedb.com/blog/searchbench-arangodb#the-billion-logs-benchmark" class="hash-link" aria-label="Direct link to The billion logs benchmark" title="Direct link to The billion logs benchmark">​</a></h2>
<p>We ran the full billion on our side. SereneDB answers all 92 queries at that scale with nothing capped and nothing failed.</p>
<table><thead><tr><th>task</th><th>queries</th><th>total</th><th>median</th><th>slowest</th></tr></thead><tbody><tr><td><code>count</code></td><td>32</td><td>1.9 s</td><td>20.5 ms</td><td>368 ms</td></tr><tr><td><code>top_k (bm25)</code></td><td>21</td><td>1.3 s</td><td>58 ms</td><td>164 ms</td></tr><tr><td><code>group_by</code></td><td>14</td><td>5.1 s</td><td>277.5 ms</td><td>978 ms</td></tr><tr><td><code>top_k (time)</code></td><td>16</td><td>224 ms</td><td>10 ms</td><td>29 ms</td></tr><tr><td><code>join</code></td><td>9</td><td>21.1 s</td><td>2.1 s</td><td>3.6 s</td></tr></tbody></table>
<p>Ingest and index took 8.5 minutes and the index is 120 GiB on disk.</p>
<p>None of it scales linearly. Ten times the data costs a lot less than ten times as much:</p>
<figure class="chart_Gfup"><div class="inner_O5vv" style="--benchmark-series-count:2"><div class="header_yZr_"><figcaption class="title_pL0r">Scaling from 100M to 1B logs</figcaption><div class="legend_BXPX" aria-label="Chart legend"><span><i aria-hidden="true" style="background-color:#895af8"></i>100M</span><span><i aria-hidden="true" style="background-color:#b46cff"></i>1B</span></div></div><p class="scaleNote_An4h">Bars are normalized within each metric; labels show absolute values.</p><div class="plots_Hize" role="img" aria-label="Scaling from 100M to 1B logs. median query latency · 2.7× growth: 100M: 17 ms, 1B: 46 ms. ingest + index · 9.4× growth: 100M: 54 s, 1B: 8.5 min. index on disk · 11.5× growth: 100M: 10.5 GiB, 1B: 120 GiB"><div class="row_YNFy"><div class="plot_VSLA"><div class="group_AVyR"><div class="bars_dL7M"><div class="column_HUZK" title="100M: 17 ms"><span class="value_n384" style="--benchmark-bar-height:31.043478260869563%">17 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#895af8;--benchmark-bar-height:31.043478260869563%"></span></div><div class="column_HUZK" title="1B: 46 ms"><span class="value_n384" style="--benchmark-bar-height:84%">46 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#b46cff;--benchmark-bar-height:84%"></span></div></div><span class="category_d8eb">median query latency · 2.7× growth</span></div><div class="group_AVyR"><div class="bars_dL7M"><div class="column_HUZK" title="100M: 54 s"><span class="value_n384" style="--benchmark-bar-height:8.894117647058824%">54 s</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#895af8;--benchmark-bar-height:8.894117647058824%"></span></div><div class="column_HUZK" title="1B: 8.5 min"><span class="value_n384" style="--benchmark-bar-height:84%">8.5 min</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#b46cff;--benchmark-bar-height:84%"></span></div></div><span class="category_d8eb">ingest + index · 9.4× growth</span></div><div class="group_AVyR"><div class="bars_dL7M"><div class="column_HUZK" title="100M: 10.5 GiB"><span class="value_n384" style="--benchmark-bar-height:7.35%">10.5 GiB</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#895af8;--benchmark-bar-height:7.35%"></span></div><div class="column_HUZK" title="1B: 120 GiB"><span class="value_n384" style="--benchmark-bar-height:84%">120 GiB</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#b46cff;--benchmark-bar-height:84%"></span></div></div><span class="category_d8eb">index on disk · 11.5× growth</span></div></div></div></div></div></figure>
<p>Latency is the one that matters. Take each query on its own and the median slowdown is <strong>2.8x</strong> for ten times the corpus, with a geometric mean of 2.9x. Build time came in slightly under linear at 9.4x. Size went the other way at 11.5x, so the index gets proportionally fatter as the corpus grows. That one is moving against us and we would rather say so than let someone find it.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="why-we-cant-run-arangodb-at-a-billion">Why we can't run ArangoDB at a billion<a href="https://serenedb.com/blog/searchbench-arangodb#why-we-cant-run-arangodb-at-a-billion" class="hash-link" aria-label="Direct link to Why we can't run ArangoDB at a billion" title="Direct link to Why we can't run ArangoDB at a billion">​</a></h3>
<p>ArangoDB's Community Edition <a href="https://arangodb.com/3-12-ce-changes-faq/" target="_blank" rel="noopener noreferrer">enforces a 100 GiB total database size limit</a>. Go over it and you get warnings for two days, then the deployment drops into read-only for two days, then it shuts down. At 100M logs the ArangoDB instance is already 55.3 GiB, so the licensed room runs out somewhere around <strong>181M logs</strong>. A billion would have put us far past that, so we did not run it.</p>
<p>That leaves us extrapolating the two things that scale predictably. Straight-line from the measured 100M run, a 1B ArangoSearch index comes to roughly <strong>553 GiB</strong> and at least <strong>3.1 hours</strong> to build. Both are floors if ArangoSearch scales the way ours does: our own index grew 11.5x for ten times the data. Read the same figures as capacity and one 100 GiB instance holds around 181M logs on ArangoDB against around 830M on SereneDB. If you want measurements instead of our arithmetic, SearchBench is open and reproducible: the ArangoDB adapter is in the repo, so anyone holding a license that permits it can run the billion and publish what they get.</p>
<p>Query latency is the one thing we cannot extrapolate, so we did the next best thing and lined our 1B numbers up against ArangoDB's 100M ones, with only one side carrying ten times the data.</p>
<figure class="chart_Gfup"><div class="inner_O5vv" style="--benchmark-series-count:3"><div class="header_yZr_"><figcaption class="title_pL0r">100M vs 1B: load, size and latency</figcaption><div class="legend_BXPX" aria-label="Chart legend"><span><i aria-hidden="true" style="background-color:#895af8"></i>SereneDB 100M</span><span><i aria-hidden="true" style="background-color:#b46cff"></i>SereneDB 1B</span><span><i aria-hidden="true" style="background-color:#20b89a"></i>ArangoDB 100M</span></div></div><p class="scaleNote_An4h">Bars are normalized within each metric; labels show absolute values.</p><div class="plots_Hize multiRow_MpA1" role="img" aria-label="100M vs 1B: load, size and latency. ingest + index: SereneDB 100M: 54 s, SereneDB 1B: 8.5 min, ArangoDB 100M: 18.6 min. index on disk: SereneDB 100M: 10.5 GiB, SereneDB 1B: 120 GiB, ArangoDB 100M: 55.3 GiB. count: SereneDB 100M: 8 ms, SereneDB 1B: 20.5 ms, ArangoDB 100M: 77 ms. top-K BM25: SereneDB 100M: 20 ms, SereneDB 1B: 58 ms, ArangoDB 100M: 97 ms. group by: SereneDB 100M: 46.5 ms, SereneDB 1B: 277.5 ms, ArangoDB 100M: 1.443 s. top-K time: SereneDB 100M: 8.5 ms, SereneDB 1B: 10 ms, ArangoDB 100M: 289.5 ms. join: SereneDB 100M: 257 ms, SereneDB 1B: 2.121 s, ArangoDB 100M: 32.48 s"><div class="row_YNFy"><p class="rowLabel_Sc3F">Load and size</p><div class="plot_VSLA"><div class="group_AVyR"><div class="bars_dL7M"><div class="column_HUZK" title="SereneDB 100M: 54 s"><span class="value_n384" style="--benchmark-bar-height:4.064516129032258%">54 s</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#895af8;--benchmark-bar-height:4.064516129032258%"></span></div><div class="column_HUZK" title="SereneDB 1B: 8.5 min"><span class="value_n384" style="--benchmark-bar-height:38.387096774193544%">8.5 min</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#b46cff;--benchmark-bar-height:38.387096774193544%"></span></div><div class="column_HUZK" title="ArangoDB 100M: 18.6 min"><span class="value_n384" style="--benchmark-bar-height:84%">18.6 min</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#20b89a;--benchmark-bar-height:84%"></span></div></div><span class="category_d8eb">ingest + index</span></div><div class="group_AVyR"><div class="bars_dL7M"><div class="column_HUZK" title="SereneDB 100M: 10.5 GiB"><span class="value_n384" style="--benchmark-bar-height:7.35%">10.5 GiB</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#895af8;--benchmark-bar-height:7.35%"></span></div><div class="column_HUZK" title="SereneDB 1B: 120 GiB"><span class="value_n384" style="--benchmark-bar-height:84%">120 GiB</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#b46cff;--benchmark-bar-height:84%"></span></div><div class="column_HUZK" title="ArangoDB 100M: 55.3 GiB"><span class="value_n384" style="--benchmark-bar-height:38.71%">55.3 GiB</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#20b89a;--benchmark-bar-height:38.71%"></span></div></div><span class="category_d8eb">index on disk</span></div></div></div><div class="row_YNFy"><p class="rowLabel_Sc3F">Median hot-query latency by task</p><div class="plot_VSLA"><div class="group_AVyR"><div class="bars_dL7M"><div class="column_HUZK" title="SereneDB 100M: 8 ms"><span class="value_n384" style="--benchmark-bar-height:3%">8 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#895af8;--benchmark-bar-height:3%"></span></div><div class="column_HUZK" title="SereneDB 1B: 20.5 ms"><span class="value_n384" style="--benchmark-bar-height:3%">20.5 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#b46cff;--benchmark-bar-height:3%"></span></div><div class="column_HUZK" title="ArangoDB 100M: 77 ms"><span class="value_n384" style="--benchmark-bar-height:6.468%">77 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#20b89a;--benchmark-bar-height:6.468%"></span></div></div><span class="category_d8eb">count</span></div><div class="group_AVyR"><div class="bars_dL7M"><div class="column_HUZK" title="SereneDB 100M: 20 ms"><span class="value_n384" style="--benchmark-bar-height:3%">20 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#895af8;--benchmark-bar-height:3%"></span></div><div class="column_HUZK" title="SereneDB 1B: 58 ms"><span class="value_n384" style="--benchmark-bar-height:4.872%">58 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#b46cff;--benchmark-bar-height:4.872%"></span></div><div class="column_HUZK" title="ArangoDB 100M: 97 ms"><span class="value_n384" style="--benchmark-bar-height:8.148%">97 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#20b89a;--benchmark-bar-height:8.148%"></span></div></div><span class="category_d8eb">top-K BM25</span></div><div class="group_AVyR"><div class="bars_dL7M"><div class="column_HUZK" title="SereneDB 100M: 46.5 ms"><span class="value_n384" style="--benchmark-bar-height:3%">46.5 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#895af8;--benchmark-bar-height:3%"></span></div><div class="column_HUZK" title="SereneDB 1B: 277.5 ms"><span class="value_n384" style="--benchmark-bar-height:16.153846153846153%">277.5 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#b46cff;--benchmark-bar-height:16.153846153846153%"></span></div><div class="column_HUZK" title="ArangoDB 100M: 1.443 s"><span class="value_n384" style="--benchmark-bar-height:84%">1.443 s</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#20b89a;--benchmark-bar-height:84%"></span></div></div><span class="category_d8eb">group by</span></div><div class="group_AVyR"><div class="bars_dL7M"><div class="column_HUZK" title="SereneDB 100M: 8.5 ms"><span class="value_n384" style="--benchmark-bar-height:3%">8.5 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#895af8;--benchmark-bar-height:3%"></span></div><div class="column_HUZK" title="SereneDB 1B: 10 ms"><span class="value_n384" style="--benchmark-bar-height:3%">10 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#b46cff;--benchmark-bar-height:3%"></span></div><div class="column_HUZK" title="ArangoDB 100M: 289.5 ms"><span class="value_n384" style="--benchmark-bar-height:24.317999999999998%">289.5 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#20b89a;--benchmark-bar-height:24.317999999999998%"></span></div></div><span class="category_d8eb">top-K time</span></div><div class="group_AVyR"><div class="bars_dL7M"><div class="column_HUZK" title="SereneDB 100M: 257 ms"><span class="value_n384" style="--benchmark-bar-height:3%">257 ms</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#895af8;--benchmark-bar-height:3%"></span></div><div class="column_HUZK" title="SereneDB 1B: 2.121 s"><span class="value_n384" style="--benchmark-bar-height:5.485344827586208%">2.121 s</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#b46cff;--benchmark-bar-height:5.485344827586208%"></span></div><div class="column_HUZK" title="ArangoDB 100M: 32.48 s"><span class="value_n384" style="--benchmark-bar-height:84%">32.48 s</span><span aria-hidden="true" class="bar_MRLe" style="--benchmark-bar-color:#20b89a;--benchmark-bar-height:84%"></span></div></div><span class="category_d8eb">join</span></div></div></div></div></div></figure>
<p>So those are the numbers. The rest of this post is about where they come from.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="where-the-speed-comes-from">Where the speed comes from<a href="https://serenedb.com/blog/searchbench-arangodb#where-the-speed-comes-from" class="hash-link" aria-label="Direct link to Where the speed comes from" title="Direct link to Where the speed comes from">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="query-categories-explained">Query categories explained<a href="https://serenedb.com/blog/searchbench-arangodb#query-categories-explained" class="hash-link" aria-label="Direct link to Query categories explained" title="Direct link to Query categories explained">​</a></h3>
<table><thead><tr><th>family</th><th>use case and properties</th><th>what makes it fast</th></tr></thead><tbody><tr><td><code>count</code></td><td>how many logs match. No sorting, no aggregation, nothing materialized</td><td><code>count()</code> on every iterator plus an empty projection, so the column store is never opened. Otherwise pure search engine: block scoring, postings format, two-phase execution</td></tr><tr><td><code>top_k (bm25)</code></td><td>the most relevant hundred, ordered by score</td><td>WAND on both sides, which makes this the narrowest family. BlockMax MaxScore carries the disjunctions and an exact top-K threshold keeps the pruning bound tight</td></tr><tr><td><code>group_by</code></td><td>matches bucketed by service or by minute</td><td>vectorized aggregation over the columnar store against scalar</td></tr><tr><td><code>top_k (time)</code></td><td>log tailing: the newest hundred inside a time window. Ordered by a stored column, so no score pruning applies</td><td>walking a time range cheaply, then reading timestamps back out of the columnar store</td></tr><tr><td><code>join</code></td><td>correlate two services through a shared trace id</td><td>hash joins and a cost-based optimizer against a nested loop</td></tr></tbody></table>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="inside-the-search-engine">Inside the search engine<a href="https://serenedb.com/blog/searchbench-arangodb#inside-the-search-engine" class="hash-link" aria-label="Direct link to Inside the search engine" title="Direct link to Inside the search engine">​</a></h3>
<p>We reworked most of IResearch: the retrieval path, the scoring loop and the postings format itself. The details are published as the Search optimization journey series and each part maps onto something in the table above.</p>
<ul>
<li><strong><a href="https://serenedb.com/blog/search-optimization-1">Collecting top-K candidates</a></strong>, part 1. The pruning threshold has to be exact after every insert, because a stale bound makes every later block do more work than it needs to. Tantivy hit the same thing and <a href="https://github.com/quickwit-oss/tantivy/pull/2881" target="_blank" rel="noopener noreferrer">swapped in a binary heap</a> for exactly that reason.</li>
<li><strong><a href="https://serenedb.com/blog/search-optimization-2">Block scoring</a></strong>, part 2. Score a whole block of postings at a time instead of a document at a time. Tantivy is now <a href="https://github.com/quickwit-oss/tantivy/issues/2859" target="_blank" rel="noopener noreferrer">exploring the same approach</a>, citing that post.</li>
<li><strong><a href="https://serenedb.com/blog/norm-gathering">Optimize norm gathering</a></strong>, part 3. Norms sit on the hot path of every scored query, so how you fetch them shows up everywhere in <code>top_k (bm25)</code>.</li>
<li><strong><a href="https://serenedb.com/blog/iresearch-two-phase-queries">How to efficiently execute two phase queries</a></strong>, part 4. Matching separated from scoring, so the expensive half only runs on the documents that survive the cheap half.</li>
<li><strong><a href="https://serenedb.com/blog/search-optimization-5">Adaptive posting list format</a></strong>, part 5. The encoding adapts to the density of each term rather than forcing one layout on everything, which is most of why the index is 5.3x smaller.</li>
<li><strong>A new columnar store.</strong> This is the one that never got a post and it matters twice over. The store holds the <code>storedValues</code> that retrieval reads and the norms that BM25 scoring reads and in the version ArangoSearch runs it does no compression at all: norms are created with <a href="https://github.com/arangodb/arangodb/blob/b57bfd17cbd9663672b6d05ed380217673c51b55/arangod/IResearch/IResearchDataStore.cpp#L205-L216" target="_blank" rel="noopener noreferrer"><code>compression::none</code> hard-coded</a>. So the same bytes are bigger on disk and more expensive to pull through the scoring loop. Ours compresses, which is a good part of both the 5.3x size difference and the <code>top_k (bm25)</code> numbers.</li>
<li><strong>BlockMax MaxScore</strong>, not written up yet. For scored disjunctions, per-block score bounds let the iterator skip whole blocks that cannot reach the current top-K threshold. That is what carries the <code>or</code> entries in <code>top_k (bm25)</code>.</li>
<li><strong>A filter optimizer.</strong> Query rewriting used to happen inline while a filter was being prepared. It is now <a href="https://github.com/serenedb/serenedb/pull/771" target="_blank" rel="noopener noreferrer">a rule registry</a> with separate rule families for booleans, negation, ranges, terms, lowering and levenshtein prefixes and <code>prepare</code> does nothing but look up cookies. Wildcards and regexes lower to an <code>AutomatonFilter</code> and fuzzy matches to a <code>LevenshteinAutomatonFilter</code>, so <code>regexp</code>, <code>like</code> and <code>fuzzy</code> all end up as automaton intersections against the term dictionary instead of separate hand-written code paths.</li>
<li><strong>Parallel per-segment prepare.</strong> Preparing a filter is not free and for a fuzzy query it is most of the query: before anything can match, the engine has to walk the term dictionary of every segment collecting the terms within edit distance. That work now <a href="https://github.com/serenedb/serenedb/pull/767" target="_blank" rel="noopener noreferrer">runs per segment in parallel</a>.</li>
<li><strong>A dedicated path for counting.</strong> Counting is a first-class operation on the iterator tree rather than something layered on top. <a href="https://github.com/serenedb/serenedb/blob/9411d5732f2b060346dd0bac70a5759c7cf73605/libs/iresearch/include/iresearch/index/iterators.hpp#L306" target="_blank" rel="noopener noreferrer"><code>count()</code> is pure virtual on <code>DocIterator</code></a>, so every iterator implements it and a conjunction or a disjunction can report how many documents it matches without ever scoring or collecting them. Thirteen iterators implement it today. On top of that the planner pushes an empty projection into the scan for <code>count(*)</code> shapes, so the column store is never opened either. The plan says so out loud:</li>
</ul>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">╭─ IRESEARCH_SCAN ─────────────────────────────╮</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│ Index: logs_idx                              │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│ Index Filter:                                │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│ ╭─ Term ───────────────────────────────────╮ │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│ │ Field: ts_split_by_non_alpha(body, true) │ │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│ │ Value: payment                           │ │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│ ╰──────────────────────────────────────────╯ │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│ Output: row-count only                       │</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">╰──────────────────────────────────────────────╯</span><br></span></code></pre></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="around-the-search-engine">Around the search engine<a href="https://serenedb.com/blog/searchbench-arangodb#around-the-search-engine" class="hash-link" aria-label="Direct link to Around the search engine" title="Direct link to Around the search engine">​</a></h3>
<ul>
<li><strong>Parallel execution.</strong> ArangoSearch exposes a <a href="https://docs.arango.ai/arangodb/3.12/aql/high-level-operations/search/#parallelism" target="_blank" rel="noopener noreferrer"><code>parallelism</code> option</a> on <code>SEARCH</code> that spreads index segments across threads, so we tried it. The workload came out around 1.5x slower, so the published run leaves it at the default. On our side both halves run in parallel: preparation per segment inside the search engine, then execution out here in the query engine.</li>
<li><strong>Vectorized execution.</strong> Operators run over batches of values. Aggregating a few million matched rows by service or by minute is a columnar problem where the index plays no part and the <code>group_by</code> family is essentially a measurement of that.</li>
<li><strong>Join strategies with a cost-based optimizer.</strong> Correlating two services through a trace id is a join. For us it becomes a hash join inside the vectorized engine, running over columns the search index already holds, with a planner picking between several join algorithms. In ArangoDB the same question becomes a nested loop: for every log on the left, go look up the matching logs on the right. At 100M records that difference is architectural and no amount of postings tuning closes it.</li>
<li><strong>Regular expressions.</strong> ArangoSearch has no regex matcher inside <code>SEARCH</code> at all, so the nine <code>regexp</code> queries had to be hand-translated into <code>STARTS_WITH</code> and <code>LIKE</code>. We have one thanks to <a href="https://github.com/aksel2904" target="_blank" rel="noopener noreferrer">aksel2904</a>, who contributed the <a href="https://docs.serenedb.com/sql/indexes/inverted/full-text-search#prefix-wildcard-and-regex" target="_blank" rel="noopener noreferrer">regexp filter</a>.</li>
</ul>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="dont-trust-us-run-it-yourself">Don't trust us, run it yourself<a href="https://serenedb.com/blog/searchbench-arangodb#dont-trust-us-run-it-yourself" class="hash-link" aria-label="Direct link to Don't trust us, run it yourself" title="Direct link to Don't trust us, run it yourself">​</a></h2>
<p>Everything behind every number above is in <a href="https://github.com/serenedb/searchbench" target="_blank" rel="noopener noreferrer">github.com/serenedb/searchbench</a>: the adapters, the 92 tagged queries in each dialect, the shared driver and the raw result JSONs. Each engine directory has a README listing its env vars.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">git clone https://github.com/serenedb/searchbench</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">cd searchbench/arangodb   # or serenedb</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">SEARCHBENCH_DATASET=otel_logs_100m SEARCHBENCH_DATA_DIR=/data ./benchmark.sh --index</span><br></span></code></pre></div></div>
<p><code>--index</code> fetches the corpus, loads it and builds the index. Drop it to re-run queries against an engine that is already loaded. Omit <code>SEARCHBENCH_DATASET</code> and you get the billion. There is a 1M smoke scale that streams about 50 MB over HTTPS if you want to check an adapter before committing to a real run.</p>
<p>Full per-query results for every engine we have wired up, including the ones absent from this post, are on the <a href="https://serenedb.com/searchbench/" target="_blank" rel="noopener noreferrer">results page</a>.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="send-us-a-better-run">Send us a better run<a href="https://serenedb.com/blog/searchbench-arangodb#send-us-a-better-run" class="hash-link" aria-label="Direct link to Send us a better run" title="Direct link to Send us a better run">​</a></h2>
<p>If you think we got something wrong or that we left an engine undertuned, the fix is a pull request. We mean that literally. Every adapter in that repo is somebody's best attempt at configuring a database they did not write and ours are no exception. If you know ArangoDB better than we do, we would rather learn it from your PR than defend a number we got by guessing.</p>
<p>The same goes for the workload. If a translation looks unfair, if a query family is missing, if the corpus looks nothing like your logs, open an issue and say so. New engines are welcome too and cost about seven small scripts.</p>
<p>A benchmark is only worth anything when the people being measured can push back on it. That is why this one is Apache 2.0 with no conditions attached to publishing results and why the queries and the adapters and the raw JSONs are all sitting there in the open. Everyone in this field is guessing about everyone else's engine and the only way out of that is to compare notes properly. We would like SearchBench to be where that happens.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="conclusion">Conclusion<a href="https://serenedb.com/blog/searchbench-arangodb#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion">​</a></h2>
<p>Ten years on, the engine we started in 2016 as a pet project is unrecognizable from the one still shipping inside ArangoDB and this is the first time we have put numbers on that.</p>
<p>Here is what a billion OpenTelemetry logs look like on a single machine. Ingest and index takes <strong>eight and a half minutes</strong> and lands at <strong>120 GiB</strong> on disk. Of the 92 queries, the 83 non-join ones come back at a <strong>median of 29 ms</strong>, with just over seven in ten under 100 ms. The nine expensive joins that correlate two services through a trace id take just a few seconds.</p>
<p>If you are running search workloads on something that makes you choose between full-text and analytics, SereneDB is worth twenty minutes of your evening. It speaks the Postgres wire protocol, so whatever client you already have will do and every number in this post is a <code>CREATE INDEX</code> away from being reproducible on your own dataset.</p>
<p>This is the first of these and not the last. Several engines are already wired up with results in the repo at both scales, so the next comparisons are mostly a matter of writing them up.</p>
<hr>
<p>If you find this interesting, we'd be grateful if you support SereneDB with a star on <a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer">GitHub</a>. For an early-stage project, it means more than you might think.</p></div>]]></content>
        <author>
            <name>SereneDB Team</name>
        </author>
        <category label="Search" term="Search"/>
        <category label="Database" term="Database"/>
        <category label="Search optimization journey" term="Search optimization journey"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The State of Serene, June 2026]]></title>
        <id>https://serenedb.com/blog/state-of-serene-2026-06</id>
        <link href="https://serenedb.com/blog/state-of-serene-2026-06"/>
        <updated>2026-07-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Issue #1 of The State of Serene: what SereneDB is — lakehouse search and analytics for your agents — what it does today, and everything we shipped in June 2026.]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><img class="avatarImage_s4Ar" src="https://serenedb.com/blog/img/abramov.jpeg" alt="Andrey Abramov" loading="lazy"></div></div><div class="copy_iHNG"><p class="names_ox2I">Andrey Abramov</p><p class="meta_OgzX">Jul 3, 2026 · 9 minutes read</p></div></header><h2 class="title_RQ9v">The State of Serene, June 2026</h2><p style="font-size:0.9rem;margin-top:-0.4rem">Issue #1: lakehouse search and analytics for your agents. What it does today, plus everything we shipped in June</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg hideFirstHeading_VVMX" data-image-lightbox-root="true"><img src="https://serenedb.com/blog/img/blog/state-of-serene-2026-06/preview.png" alt="Serene" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%;display:block;margin:0 auto"><img src="https://serenedb.com/blog/img/blog/state-of-serene-2026-06/preview.png" alt="Serene" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%;display:block;margin:0 auto">
<br>
<p>Welcome to <strong>The State of Serene</strong>, our monthly note on what we shipped, what we
learned and where SereneDB is heading. This is the first issue, so before we get
to June, let's answer the obvious question: what is SereneDB and what can you
actually do with it today?</p>
<p>Instead of moving your data to the model, point the model at the data. Imagine an
agent gets asked: "Which enterprise accounts are unhappy about latency and how much
revenue is at risk?" It can't grep 100GB of tickets and it won't fit them in a
context window, so it asks SereneDB instead. Search pulls the tickets that are
really about slowness, even the ones that never use the word "latency". The same
query joins them to accounts and sums the revenue at risk by plan. The agent gets
back a handful of numbers it can act on, not a stack of tickets to read.</p>
<p>So an agent needs two things: the most relevant data and the statistics over it.
Hybrid search gives the relevant part. Analytics gives the numbers on top. SereneDB
does both in one query and hands back a small answer, so the agent spends its
tokens on the decision, not on scanning files.</p>
<p>That's SereneDB: lakehouse search and analytics for your agents. One Apache-2.0
database that speaks Postgres and reads your data where it already lives, whether
that's Parquet and Iceberg in the lake, your Postgres or your warehouse. It indexes
that data in place and hands back the slice that matters, so nothing gets copied
into another system and nothing goes stale. Think of it as a compute and delivery
machine for your agents.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-you-can-do-today">What you can do today<a href="https://serenedb.com/blog/state-of-serene-2026-06#what-you-can-do-today" class="hash-link" aria-label="Direct link to What you can do today" title="Direct link to What you can do today">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="its-postgres-compatible">It's Postgres-compatible<a href="https://serenedb.com/blog/state-of-serene-2026-06#its-postgres-compatible" class="hash-link" aria-label="Direct link to It's Postgres-compatible" title="Direct link to It's Postgres-compatible">​</a></h3>
<p>Compatible on two levels, the wire protocol and the SQL dialect, so most of what
you already use just works.</p>
<ul>
<li>Connect with <code>psql</code>, <a href="https://docs.serenedb.com/clients" target="_blank" rel="noopener noreferrer">any Postgres driver</a> or
your favorite ORM. Nothing new to learn.</li>
<li>Standard types plus <a href="https://docs.serenedb.com/compatibility/system-table-compatibility" target="_blank" rel="noopener noreferrer"><code>pg_catalog</code> and system tables</a>,
so BI tools and ORMs introspect it like any Postgres.</li>
<li>See what's supported and the few quirks in the
<a href="https://docs.serenedb.com/compatibility" target="_blank" rel="noopener noreferrer">compatibility docs</a>.</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="search-and-analytics-in-one-engine">Search and analytics in one engine<a href="https://serenedb.com/blog/state-of-serene-2026-06#search-and-analytics-in-one-engine" class="hash-link" aria-label="Direct link to Search and analytics in one engine" title="Direct link to Search and analytics in one engine">​</a></h3>
<p>One inverted index holds full-text and vectors, sitting right next to columnar,
vectorized analytics. Same data, same query.</p>
<ul>
<li><a href="https://docs.serenedb.com/sql/indexes/inverted/full-text-search" target="_blank" rel="noopener noreferrer">Full-text search</a>:
BM25, phrase, fuzzy, wildcard and regex, plus a stack of tokenizers.</li>
<li><a href="https://docs.serenedb.com/sql/indexes/inverted/vector-search" target="_blank" rel="noopener noreferrer">Vector search</a>
and <a href="https://docs.serenedb.com/sql/indexes/inverted/hybrid-search" target="_blank" rel="noopener noreferrer">hybrid</a>
that fuses both with tunable <a href="https://docs.serenedb.com/sql/indexes/inverted/ranking" target="_blank" rel="noopener noreferrer">ranking</a>.</li>
<li><a href="https://docs.serenedb.com/sql/indexes/inverted/geospatial-search" target="_blank" rel="noopener noreferrer">Geospatial search</a>
in the same index.</li>
<li>Real analytical SQL right beside it: <a href="https://docs.serenedb.com/sql/query_syntax/from_and_join" target="_blank" rel="noopener noreferrer">joins</a>,
<a href="https://docs.serenedb.com/sql/query_syntax/with" target="_blank" rel="noopener noreferrer">CTEs</a>,
<a href="https://docs.serenedb.com/sql/query_syntax/window" target="_blank" rel="noopener noreferrer">window functions</a> and
<a href="https://docs.serenedb.com/sql/query_syntax/groupby" target="_blank" rel="noopener noreferrer"><code>GROUP BY</code></a> over the same rows.
Filter by relevance, then aggregate, in one query.</li>
<li>It all runs on <a href="https://serenedb.com/blog/search-benchmark-game-overview">iresearch</a>,
our own open-source engine and a fast one.</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="index-your-data-where-it-lives">Index your data where it lives<a href="https://serenedb.com/blog/state-of-serene-2026-06#index-your-data-where-it-lives" class="hash-link" aria-label="Direct link to Index your data where it lives" title="Direct link to Index your data where it lives">​</a></h3>
<p>Point SereneDB at your data and query it like a normal table, no ETL and no copies.</p>
<ul>
<li>Remote files:
<a href="https://docs.serenedb.com/sql/indexes/inverted/external-data" target="_blank" rel="noopener noreferrer">Parquet, JSON, CSV and Iceberg</a> on S3, HTTP or Hugging Face.</li>
<li><a href="https://docs.serenedb.com/sql/statements/attach/postgres" target="_blank" rel="noopener noreferrer"><code>ATTACH</code></a> a live
Postgres database and join it against your indexed data.</li>
<li>Bring it in for one source of truth or leave it where it is and index it in
place. Same SQL either way.</li>
</ul>
<p>See it end to end in the <a href="http://codesearch.serenedb.com/" target="_blank" rel="noopener noreferrer">code search demo</a>:
hybrid search over 11.4M rows read straight from Hugging Face. More on that below.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-happened-in-june">What happened in June<a href="https://serenedb.com/blog/state-of-serene-2026-06#what-happened-in-june" class="hash-link" aria-label="Direct link to What happened in June" title="Direct link to What happened in June">​</a></h2>
<p>A few of June's highlights worth your time.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="search-queries-prepare-in-parallel">Search queries prepare in parallel<a href="https://serenedb.com/blog/state-of-serene-2026-06#search-queries-prepare-in-parallel" class="hash-link" aria-label="Direct link to Search queries prepare in parallel" title="Direct link to Search queries prepare in parallel">​</a></h3>
<p>Setting up a search query used to walk the index segments one at a time. Now the
prepare phase and the scan both fan out across segments, so query setup scales with
your cores instead of pinning a single one. Ranked queries also skip documents that
can't reach the top n. Under real concurrency it adds up. Here are Top-K queries at
32 threads, before and after this month's search work:</p>
<table><thead><tr><th>filter</th><th>before</th><th>after</th><th>speedup</th></tr></thead><tbody><tr><td>ngram</td><td>162 ms</td><td>65 ms</td><td>2.48x</td></tr><tr><td>and_not</td><td>325 ms</td><td>177 ms</td><td>1.83x</td></tr><tr><td>any_minmatch</td><td>871 ms</td><td>518 ms</td><td>1.68x</td></tr><tr><td>like</td><td>7,421 ms</td><td>4,831 ms</td><td>1.54x</td></tr><tr><td>phrase</td><td>1,171 ms</td><td>964 ms</td><td>1.22x</td></tr><tr><td>regexp</td><td>5,467 ms</td><td>4,887 ms</td><td>1.12x</td></tr></tbody></table>
<p>Every one of the 26 filter types came out ahead, from 1.06x (gt) to 2.48x (ngram).</p>
<p>And it scales with cores. BM25 scorer streaming runs 1.90x faster at 32 threads and
2.08x at 64. Top-K gets most of its win by 32 threads.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="smarter-search-filters">Smarter search filters<a href="https://serenedb.com/blog/state-of-serene-2026-06#smarter-search-filters" class="hash-link" aria-label="Direct link to Smarter search filters" title="Direct link to Smarter search filters">​</a></h3>
<p>Search filters now run through a set of optimizer rules that lower each one to the
cheapest form that still matches, done once up front instead of ad-hoc code in the
prepare path. Some of the rewrites:</p>
<table><thead><tr><th>you write</th><th>it becomes</th></tr></thead><tbody><tr><td><code>ts_starts_with('abc') AND ts_levenshtein('abcdef')</code></td><td>one fuzzy match with the <code>abc</code> prefix fused into its args</td></tr><tr><td>nested booleans, <code>a OR (b OR (c OR d))</code></td><td>one flat <code>a OR b OR c OR d</code></td></tr><tr><td>nested negations, <code>NOT (NOT a)</code></td><td>one flat <code>a</code></td></tr><tr><td><code>NOT (a OR b)</code></td><td><code>NOT a AND NOT b</code></td></tr></tbody></table>
<p>Cheaper filters and the lowering runs once at optimize time instead of per query.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="a-rebuilt-server-that-speaks-two-protocols">A rebuilt server that speaks two protocols<a href="https://serenedb.com/blog/state-of-serene-2026-06#a-rebuilt-server-that-speaks-two-protocols" class="hash-link" aria-label="Direct link to A rebuilt server that speaks two protocols" title="Direct link to A rebuilt server that speaks two protocols">​</a></h3>
<p>We rebuilt the server from the ground up as a sans-io network server with
zero-copy framing. Two protocols talk to it:</p>
<ul>
<li><strong>Postgres wire protocol</strong>: a clean implementation with auth, a
<a href="https://docs.serenedb.com/clients/serened-shell" target="_blank" rel="noopener noreferrer">real CLI</a> and a
<a href="https://docs.serenedb.com/clients/serened-psql" target="_blank" rel="noopener noreferrer">psql-style shell</a> out of the
box, plus background schedulers for maintenance.</li>
<li><strong>Elasticsearch HTTP API</strong>: a drop-in <code>_bulk</code>, <code>_search</code> and scroll, so existing
Elasticsearch clients point straight at it. There's a
<a href="https://docs.serenedb.com/sql/indexes/inverted/migrating-from-elasticsearch" target="_blank" rel="noopener noreferrer">migration guide</a>
for when you want to move queries to SQL.</li>
</ul>
<p>Underneath, we also started collapsing the storage layer, so fewer moving parts
and a simpler single binary.</p>
<p>And it's faster. A wire bench (trivial queries, pgbench-style, 16 clients), so it
measures the server and protocol layer, not query execution, since the engine
underneath is unchanged. <code>select1</code> is a bare <code>SELECT 1</code> (pure transport cost),
<code>wide</code> is a
parse-heavy <code>SELECT 1 WHERE 1=1 AND ... AND 10=10</code> (isolates parse cost) and
<code>simple</code>/<code>extended</code>/<code>prepared</code> are the Postgres protocol modes. New server against
the old one:</p>
<div data-email-img="/img/email/mermaid/2026-07-03-state-of-serene-2026-06-1.png"></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-demo-that-ties-it-together">The demo that ties it together<a href="https://serenedb.com/blog/state-of-serene-2026-06#the-demo-that-ties-it-together" class="hash-link" aria-label="Direct link to The demo that ties it together" title="Direct link to The demo that ties it together">​</a></h3>
<p>We put the whole thing in a live demo: code search over ~11.4M Codeforces
solutions and their problem statements, read straight from Hugging Face with no
ingestion job. Solutions get GitHub-style code search (exact and fuzzy), matched
with a new <a href="https://docs.serenedb.com/sql/statements/create_text_search_dictionary/sparse-ngram" target="_blank" rel="noopener noreferrer">sparse ngram tokenizer</a>.
Tasks get hybrid keyword plus semantic. And there's an MCP endpoint so an agent can
search it directly.</p>
<p>The fun part: the backend took about 15 minutes. We handed Claude our
<a href="https://docs.serenedb.com/" target="_blank" rel="noopener noreferrer">docs</a> and it wrote the whole thing, pointing the
database at the remote data, building the indexes and writing the queries.</p>
<p>Go try it: <a href="http://codesearch.serenedb.com/" target="_blank" rel="noopener noreferrer">codesearch.serenedb.com</a> ·
repo: <a href="https://github.com/serenedb/codesearch" target="_blank" rel="noopener noreferrer">github.com/serenedb/codesearch</a> ·
writeup: <a href="https://serenedb.com/blog/code-search-system">Code search over a data lake</a>.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="also-shipped">Also shipped<a href="https://serenedb.com/blog/state-of-serene-2026-06#also-shipped" class="hash-link" aria-label="Direct link to Also shipped" title="Direct link to Also shipped">​</a></h3>
<ul>
<li>a new on-disk index format, the base layer a lot of the above sits on</li>
<li>column statistics from the index, so aggregates like <code>SUM</code> hit faster paths (6-7 ms, was 23-27 on 25M rows)</li>
<li><code>VARIANT</code> support (store, load, shredding) and array indexing for semi-structured
data</li>
<li>the <code>inet</code> type</li>
<li>nanosecond <code>timestamptz</code></li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-to-expect-in-july">What to expect in July<a href="https://serenedb.com/blog/state-of-serene-2026-06#what-to-expect-in-july" class="hash-link" aria-label="Direct link to What to expect in July" title="Direct link to What to expect in July">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="on-the-engineering-side">On the engineering side<a href="https://serenedb.com/blog/state-of-serene-2026-06#on-the-engineering-side" class="hash-link" aria-label="Direct link to On the engineering side" title="Direct link to On the engineering side">​</a></h3>
<ul>
<li>landing <code>ts_dict</code> (analytics from the search index)</li>
<li>a new vector-search design we've been cooking</li>
<li>more efficient columnar storage and execution, with dynamic filters</li>
<li>indexing of complex types, including schema-agnostic indexing</li>
<li>foreign data sources via <code>CREATE SERVER</code>, including a ClickHouse connector</li>
<li>ongoing work on logical replication (publications and subscriptions)</li>
<li>PG-compatible authorization and RBAC</li>
</ul>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="benchmark-season">Benchmark season<a href="https://serenedb.com/blog/state-of-serene-2026-06#benchmark-season" class="hash-link" aria-label="Direct link to Benchmark season" title="Direct link to Benchmark season">​</a></h3>
<p>Summer's here, so we're calling it <strong>benchmark season</strong>. Over the coming weeks
we're putting SereneDB up against the best-in-class databases in the open, with
methodology and raw numbers published like we always do. First up is a
<strong>billion-record</strong> benchmark on search-heavy workloads, our biggest test so far.
Stay tuned.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="kudos">Kudos<a href="https://serenedb.com/blog/state-of-serene-2026-06#kudos" class="hash-link" aria-label="Direct link to Kudos" title="Direct link to Kudos">​</a></h2>
<p>A lot of SereneDB's search surface comes from external contributors. A few we
want to shout out:</p>
<ul>
<li><a href="https://github.com/romanpovol" target="_blank" rel="noopener noreferrer"><strong>romanpovol</strong></a> built the <a href="https://docs.serenedb.com/sql/statements/create_text_search_dictionary/path-hierarchy" target="_blank" rel="noopener noreferrer">path-hierarchy</a> and <a href="https://docs.serenedb.com/sql/statements/create_text_search_dictionary/pattern" target="_blank" rel="noopener noreferrer">pattern</a> tokenizers</li>
<li><a href="https://github.com/afigor2701" target="_blank" rel="noopener noreferrer"><strong>afigor2701</strong></a> worked on inverted-index internals (WAND data handling and integer encoding)</li>
<li><a href="https://github.com/aksel2904" target="_blank" rel="noopener noreferrer"><strong>aksel2904</strong></a> added the <a href="https://docs.serenedb.com/sql/indexes/inverted/full-text-search#prefix-wildcard-and-regex" target="_blank" rel="noopener noreferrer">regexp filter</a></li>
<li><a href="https://github.com/abhishek593" target="_blank" rel="noopener noreferrer"><strong>abhishek593</strong></a> added the <a href="https://docs.serenedb.com/sql/statements/create_text_search_dictionary/union" target="_blank" rel="noopener noreferrer">union tokenizer</a></li>
<li><a href="https://github.com/k-morozov" target="_blank" rel="noopener noreferrer"><strong>k-morozov</strong></a> did the <a href="https://docs.serenedb.com/sql/statements/create_text_search_dictionary/wordnet-synonyms" target="_blank" rel="noopener noreferrer">synonym tokenizer with WordNet support</a>, plus the <code>CREATE PUBLICATION</code> / <code>CREATE SUBSCRIPTION</code> grammar</li>
<li><a href="https://github.com/hinata34" target="_blank" rel="noopener noreferrer"><strong>hinata34</strong></a> co-authored the filter optimizer</li>
</ul>
<p>Thanks folks, keep the PRs coming.</p>
<p>Want to be in the next one? We love outside contributions and we're happy to help
you land your first PR. We tag beginner-friendly work with
<a href="https://github.com/serenedb/serenedb/contribute" target="_blank" rel="noopener noreferrer"><code>good first issue</code></a>, so grab one,
ask questions in the issue and we'll get you going. There's plenty of things to dig into.</p>
<hr>
<p>If "search and analytics on the data where it lives" sounds useful, the fastest
path is the <a href="http://codesearch.serenedb.com/" target="_blank" rel="noopener noreferrer">code search demo</a>. And if you like
what you see, ⭐ <a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer">star us on GitHub</a>. Hit a
rough edge or have a use case? We'd love to hear it: open an issue or just say hi.</p>
<p>See you in the next State of Serene.</p></div>]]></content>
        <author>
            <name>Andrey Abramov</name>
            <uri>https://www.linkedin.com/in/andrey-abramov-57a16241/ru/?skipRedirect=true</uri>
        </author>
        <category label="News" term="News"/>
        <category label="Search" term="Search"/>
        <category label="Database" term="Database"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Code search over a data lake, without moving the data]]></title>
        <id>https://serenedb.com/blog/code-search-system</id>
        <link href="https://serenedb.com/blog/code-search-system"/>
        <updated>2026-06-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Building a code-search system over 14 GB of programming solutions in a data lake — searching Parquet in place, without moving the data.]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><img class="avatarImage_s4Ar" src="https://serenedb.com/blog/img/ivanov.jpg" alt="Pavel Ivanov" loading="lazy"></div></div><div class="copy_iHNG"><p class="names_ox2I">Pavel Ivanov</p><p class="meta_OgzX">Jun 24, 2026 · 8 minutes read</p></div></header><h2 class="title_RQ9v">Code search over a data lake, without moving the data</h2><p style="font-size:0.9rem;margin-top:-0.4rem">Code search over remote parquet</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg hideFirstHeading_VVMX" data-image-lightbox-root="true"><p>Most data never moves. It just sits as files in object storage: Parquet and JSON
in S3, datasets on Hugging Face. A lot of it, in open formats. That's a data
lake: cheap to store, but you can't search it on its own.</p>
<p>Search and analytics tools can't read the lake directly; they need their own
copy of the data. So you set up a separate system, write code to copy the data
into it, keep that copy up to date when the source changes and add a warehouse
on the side for the numbers. That's a lot of extra work, just to move data you'd
rather leave where it is.</p>
<p>SereneDB lets you skip that. You point it at the files where they are, build an
index and search them in place. Because the index also holds your columns, one
query can both search and compute statistics, and the data never leaves storage.
And it isn't only for code search: the same view-and-index steps work on any
Parquet in any bucket.</p>
<p>To show what that looks like we built a <a href="https://codesearch.serenedb.com/" target="_blank" rel="noopener noreferrer">code-search system</a>
on it, in about 15 minutes: roughly 14,000 programming problems and 11.5
million accepted solutions, about 14 GB of text in all, searchable by keyword,
by code substring and by meaning, with live statistics over whatever you
searched for. The data is Parquet on Hugging Face; we put SQL views over the
files and built indexes on the views. SereneDB speaks the Postgres wire
protocol, so <code>psql</code> and any Postgres client just work.</p>
<div data-email-img="/img/email/mermaid/2026-06-24-code-search-system-1.png"></div>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="index-remote-sources-store-only-the-index">Index remote sources, store only the index<a href="https://serenedb.com/blog/code-search-system#index-remote-sources-store-only-the-index" class="hash-link" aria-label="Direct link to Index remote sources, store only the index" title="Direct link to Index remote sources, store only the index">​</a></h2>
<p>A view is just a SQL definition. Ours read Parquet straight off Hugging Face,
reshaping each source into a common shape and gluing different sources together
with <code>UNION ALL</code>:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">VIEW</span><span class="token plain"> solutions_v </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">-- Codeforces: ~11.4M accepted submissions, with a natural numeric id</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> submission_id::</span><span class="token keyword" style="color:#00009f">BIGINT</span><span class="token plain">      </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> problem_id </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> task_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> source </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> code</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> read_parquet</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'hf://datasets/open-r1/codeforces-submissions/...'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">UNION</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">ALL</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">-- MBPP reference solutions (no numeric id -&gt; hash the natural key)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">hash</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'mbpp/'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> task_id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">   </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'mbpp/'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> task_id </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> task_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> code</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> read_parquet</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'hf://datasets/google-research-datasets/mbpp/...'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">UNION</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">ALL</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">-- HumanEval reference solutions</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">hash</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'he/'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> task_id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">     </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> task_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> prompt </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> canonical_solution </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> code</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> read_parquet</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'hf://datasets/openai/openai_humaneval/...'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">UNION</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">ALL</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">-- Rosetta Code: per-language source</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">hash</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'rc/'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> task_name</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">   </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> task_name </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> task_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> code</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> read_parquet</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'hf://datasets/christopher/rosetta-code/...'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> solutions_idx </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> solutions_v</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">USING</span><span class="token plain"> inverted</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> task_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> lang</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> code code_grams</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p><code>solutions_v</code> unions four sources this way; <code>tasks_v</code> unions six. That's seven
distinct Hugging Face datasets across the two indexes, each with its own schema,
flattened into one searchable relation.</p>
<p>That's the whole ingest. No table is created and nothing is downloaded — there's
no <code>wget</code>, no <code>COPY</code>, no "first pull it onto a box" step. The view reads the
remote Parquet in place (pulling only the columns and rows it touches), and the
index build streams from there. The single thing we store is the index. Adding
a source is one more <code>SELECT</code>, and keeping the index current as those files
change is what we're building next.</p>
<p>So there's no search stack to stand up: no Elasticsearch, no ETL job, no copy to
keep in sync. You point SereneDB at data already sitting in S3, write
<code>CREATE INDEX</code>, and you have search.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="keep-columns-in-the-index-with-include">Keep columns in the index with INCLUDE<a href="https://serenedb.com/blog/code-search-system#keep-columns-in-the-index-with-include" class="hash-link" aria-label="Direct link to Keep columns in the index with INCLUDE" title="Direct link to Keep columns in the index with INCLUDE">​</a></h2>
<p>An inverted index maps each term to the documents that contain it: a posting
list of doc ids. By default a match hands you those ids, and you'd look the rest
of each row up in the source. <code>INCLUDE(...)</code> changes that: the listed columns
are stored in the index, right next to the postings, so a result comes back
whole, with no lookup into the source.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> solutions_idx </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> solutions_v</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">USING</span><span class="token plain"> inverted</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> task_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> lang</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> code code_grams</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  INCLUDE </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> task_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> code</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> code_len</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> lang</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> time_ms</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> memory_kb</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<div data-email-img="/img/email/mermaid/2026-06-24-code-search-system-2.png"></div>
<p>So the code, the language and the judge's <code>time_ms</code> / <code>memory_kb</code> measurements
all ride along in the index. <code>EXPLAIN</code> shows it: a single <code>IRESEARCH_SCAN</code>, no
<code>TABLE_SCAN</code>, with the INCLUDE columns projected straight from the index:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">EXPLAIN</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> task_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> code_len</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> time_ms</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> memory_kb</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> solutions_idx</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> code @@ ts_all</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ts_tokenize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ARRAY</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">'priority_queue'</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'code_grams_q'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<style>
.docusaurus-mermaid-container svg g[id*="flowchart-plan_include"],
.docusaurus-mermaid-container svg path[id*="L_plan_include_scan"] {
translate: 0 68px;
}
</style>
<div data-email-img="/img/email/mermaid/2026-06-24-code-search-system-3.png"></div>
<p>No <code>TABLE_SCAN</code> anywhere. Once built, the index never reads the Parquet again.
The corpus keeps serving with the network unplugged, and statistics over
<code>time_ms</code> / <code>memory_kb</code> are pure index reads.</p>
<p>Because those measurements ride in the index, a query can search and aggregate
in one pass. The next section does exactly that.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="tokenizers-turning-text-into-terms">Tokenizers: turning text into terms<a href="https://serenedb.com/blog/code-search-system#tokenizers-turning-text-into-terms" class="hash-link" aria-label="Direct link to Tokenizers: turning text into terms" title="Direct link to Tokenizers: turning text into terms">​</a></h2>
<p>The inverted index is generic; a tokenizer decides what counts as a term. For
problem statements that's a normal language analyzer (lowercasing, stemming,
stop-words, BM25). For source code we use a sparse n-gram tokenizer, which is
what lets you grep code the way GitHub code search does.</p>
<p>It runs a monotonic stack over bigram hashes and picks a sparse set of
variable-length n-grams (3 bytes and up) from each line. Indexing emits grams
that cover every substring; a query only needs a short covering chain. So a
substring search turns into a conjunction: tokenize the query and require every
gram to be present.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">  "for (int i"  →  [ "for ", "r (in", "(int", "nt i" ]</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  WHERE code @@ ts_all(ts_tokenize(ARRAY['for (int i'], 'code_grams_q'))</span><br></span></code></pre></div></div>
<p>The index cuts millions of rows down to a few candidates; a <code>LIKE</code> checks them
for an exact match. Replace <code>ts_all</code> with <code>ts_any(..., k)</code> and you get fuzzy
"roughly this shape" search instead, ranked by BM25.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="queries-you-can-run">Queries you can run<a href="https://serenedb.com/blog/code-search-system#queries-you-can-run" class="hash-link" aria-label="Direct link to Queries you can run" title="Direct link to Queries you can run">​</a></h2>
<p>Five ways to ask the same two indexes (<code>solutions_idx</code>, <code>tasks_idx</code>) a question.
This is the real SQL behind the search on the site.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="substring-grep">Substring grep<a href="https://serenedb.com/blog/code-search-system#substring-grep" class="hash-link" aria-label="Direct link to Substring grep" title="Direct link to Substring grep">​</a></h3>
<p>Find accepted solutions that literally contain a code fragment. The sparse-ngram
tokenizer turns the fragment into grams, the index returns the rows that contain
all of them, ranked by BM25.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> task_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> lang</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> code_len</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> solutions_idx</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> code @@ ts_all</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ts_tokenize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ARRAY</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">'priority_queue'</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'code_grams_q'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> bm25</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">solutions_idx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">tableoid</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">DESC</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">LIMIT</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">20</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="analytics-over-the-matches">Analytics over the matches<a href="https://serenedb.com/blog/code-search-system#analytics-over-the-matches" class="hash-link" aria-label="Direct link to Analytics over the matches" title="Direct link to Analytics over the matches">​</a></h3>
<p>SereneDB does analytics too, over the rows you just searched. Because <code>time_ms</code>
and <code>memory_kb</code> live in the index, the matches get aggregated in the same query:
p50/p95 run time and memory over the full match set. Search and analytics in one
shot, no export to a notebook.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">count</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">*</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">                              </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> matches</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       </span><span class="token function" style="color:#d73a49">round</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">approx_quantile</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">time_ms</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.5</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">  </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> p50_ms</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       </span><span class="token function" style="color:#d73a49">round</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">approx_quantile</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">time_ms</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.95</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> p95_ms</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       </span><span class="token function" style="color:#d73a49">round</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">approx_quantile</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">memory_kb</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.5</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> p50_kb</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> solutions_idx</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> code @@ ts_all</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ts_tokenize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ARRAY</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">'from functools import lru_cache'</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'code_grams_q'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="keyword-relevance-bm25">Keyword relevance (BM25)<a href="https://serenedb.com/blog/code-search-system#keyword-relevance-bm25" class="hash-link" aria-label="Direct link to Keyword relevance (BM25)" title="Direct link to Keyword relevance (BM25)">​</a></h3>
<p>Classic full-text ranking over problem statements: the scoring behind most
search boxes.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> title</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">round</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">bm25</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">tasks_idx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">tableoid</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">::</span><span class="token keyword" style="color:#00009f">numeric</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> score</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> tasks_idx</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> statement @@ ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'shortest path'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> bm25</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">tasks_idx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">tableoid</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">DESC</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">LIMIT</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="semantic-search">Semantic search<a href="https://serenedb.com/blog/code-search-system#semantic-search" class="hash-link" aria-label="Direct link to Semantic search" title="Direct link to Semantic search">​</a></h3>
<p>Match by meaning instead of words: nearest-neighbour over the embedding vectors
(HNSW), so "dsu" finds "disjoint set union" with no word in common.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> task_vec</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> embedding </span><span class="token operator" style="color:#393A34">&lt;=&gt;</span><span class="token plain"> $query_embedding</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">LIMIT</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="hybrid-reciprocal-rank-fusion">Hybrid (reciprocal rank fusion)<a href="https://serenedb.com/blog/code-search-system#hybrid-reciprocal-rank-fusion" class="hash-link" aria-label="Direct link to Hybrid (reciprocal rank fusion)" title="Direct link to Hybrid (reciprocal rank fusion)">​</a></h3>
<p>BM25 matches on words and vectors match on meaning; they disagree often enough
that you want both. RRF merges two rankings without needing their
scores to be comparable: keep each result's rank in each list and add up
<code>1 / (60 + rank)</code>, so anything ranked high in either list bubbles up. It's how
the site's Explore blends keyword and semantic search behind one slider.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">WITH</span><span class="token plain"> kw </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> row_number</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">OVER</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> bm25</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">tasks_idx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">tableoid</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">DESC</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> rank</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> tasks_idx </span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> statement @@ ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'disjoint set'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">LIMIT</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">sem </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> row_number</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">OVER</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> embedding </span><span class="token operator" style="color:#393A34">&lt;=&gt;</span><span class="token plain"> $query_embedding</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> rank</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> task_vec </span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> embedding </span><span class="token operator" style="color:#393A34">&lt;=&gt;</span><span class="token plain"> $query_embedding </span><span class="token keyword" style="color:#00009f">LIMIT</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">sum</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1.0</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">60</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> rank</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> score</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> kw </span><span class="token keyword" style="color:#00009f">UNION</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">ALL</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> sem</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">GROUP</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> id </span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> score </span><span class="token keyword" style="color:#00009f">DESC</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">LIMIT</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="try-it-yourself">Try it yourself<a href="https://serenedb.com/blog/code-search-system#try-it-yourself" class="hash-link" aria-label="Direct link to Try it yourself" title="Direct link to Try it yourself">​</a></h2>
<p><a href="https://codesearch.serenedb.com/" target="_blank" rel="noopener noreferrer">Open the web UI</a> and search the corpus
directly, or point your Claude at the hosted MCP endpoint and let it run the
searches as tools:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">claude mcp add codesearch --transport http https://codesearch.serenedb.com/mcp</span><br></span></code></pre></div></div>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">codex mcp add codesearch --url https://codesearch.serenedb.com/mcp</span><br></span></code></pre></div></div>
<p>Then ask it something like "find accepted solutions using a monotonic stack and
chart their run times" and it calls <code>search_code</code>, <code>code_analytics</code>, <code>explore</code>
and a few others for you, all backed by the same indexes.</p></div>]]></content>
        <author>
            <name>Pavel Ivanov</name>
        </author>
        <category label="Search" term="Search"/>
        <category label="Database" term="Database"/>
        <category label="Demos" term="Demos"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Why SQL Is the Right Language for Search-OLAP]]></title>
        <id>https://serenedb.com/blog/sql-for-search</id>
        <link href="https://serenedb.com/blog/sql-for-search"/>
        <updated>2026-05-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Why SQL is the right language for Search-OLAP: BM25 scores and vector distances belong in the same query plan as your joins and aggregations.]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><img class="avatarImage_s4Ar" src="https://serenedb.com/blog/img/malandin.jpeg" alt="Alexander Malandin" loading="lazy"></div></div><div class="copy_iHNG"><p class="names_ox2I">Alexander Malandin</p><p class="meta_OgzX">May 25, 2026 · 9 minutes read</p></div></header><h2 class="title_RQ9v">Why SQL Is the Right Language for Search-OLAP</h2><p style="font-size:0.9rem;margin-top:-0.4rem">BM25 scores and vector distances belong in the same query plan as your joins and aggregations</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg" data-image-lightbox-root="true"><h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-retrievalanalytics-divide">The retrieval/analytics divide<a href="https://serenedb.com/blog/sql-for-search#the-retrievalanalytics-divide" class="hash-link" aria-label="Direct link to The retrieval/analytics divide" title="Direct link to The retrieval/analytics divide">​</a></h2>
<p>Search and analytics are usually split for good reasons. Search engines are built around relevance: matching, scoring, ranking and returning useful results fast. Analytical systems are built around scans, joins, grouping and historical reporting. In production, these become separate projections of the same business reality, kept in sync by pipelines and stitched together by application code.</p>
<p>The trouble starts when the product asks an analytical question about a relevance-defined set: “among the best-matching documents, count by category,” “among the documents nearest to a query embedding, count by category,” or “for users matching this query, show revenue by region.” Now relevance lives in one system and aggregation context lives in another. The query crosses the seam: ask search what matches, translate that into IDs or filters, query the analytical copy, then reconcile the answers. The result is added latency, freshness ambiguity, semantic drift and glue code no one wants to own.</p>
<img src="https://serenedb.com/blog/img/blog/sql-for-search/split-search-analytics.svg" alt="Before Search-OLAP: split search and analytics with separate projections" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%;max-width:480px;display:block;margin:0 auto"><img src="https://serenedb.com/blog/img/blog/sql-for-search/split-search-analytics.svg" alt="Before Search-OLAP: split search and analytics with separate projections" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%;max-width:480px;display:block;margin:0 auto">
<br>
<p>We built SereneDB around a different premise: retrieval and analytics in the same engine, behind the same query language, over the same data. We're calling this <em>search-OLAP</em> — analytical SQL queries (joins, aggregations, ranking) over candidate sets selected by full-text or vector predicates. It's an emerging term and we hope it catches on, because we think the workload is becoming common enough to deserve one.</p>
<img src="https://serenedb.com/blog/img/blog/sql-for-search/search-olap-one-engine.svg" alt="After Search-OLAP: SereneDB handles search and analytics in one SQL query plan" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%;max-width:480px;display:block;margin:0 auto"><img src="https://serenedb.com/blog/img/blog/sql-for-search/search-olap-one-engine.svg" alt="After Search-OLAP: SereneDB handles search and analytics in one SQL query plan" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%;max-width:480px;display:block;margin:0 auto">
<br>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="why-sql-is-the-right-language-for-it">Why SQL is the right language for it<a href="https://serenedb.com/blog/sql-for-search#why-sql-is-the-right-language-for-it" class="hash-link" aria-label="Direct link to Why SQL is the right language for it" title="Direct link to Why SQL is the right language for it">​</a></h2>
<p>Search-OLAP queries need to do two things at once: select a candidate set with a search predicate and then run real analytical work over that set. Search DSLs excel at the first, SQL at the second but few languages do both.</p>
<p>Search engines excel at ranking denormalized documents. Where they get awkward is the relational half: arbitrary joins, window functions, <code>QUALIFY</code>, correlated subqueries, set operations, recursive queries. Those aren't search primitives. The engines that excel at relational work speak SQL.</p>
<p>That ecosystem advantage isn't incidental. SQL has the relational algebra surface, the optimizer literature, the tooling and the practitioner base. The modern lakehouse (Iceberg, Parquet, Delta) represents structured, columnar data that SQL's optimizer understands. Most analytical data lives in these formats and participates in the query plans DuckDB, ClickHouse, Trino and Snowflake produce. Any language for search-OLAP needs to be fluent in the data that's already there.</p>
<p>Once you accept that retrieval and analytics belong in one engine, the choice of query language is clear. SQL is the only option with both halves <em>and</em> it has a clean predicate slot for search to drop into, courtesy of Postgres's <code>@@</code>. We didn't have to invent a syntax. We had to make the right-hand side of <code>@@</code> a first-class expression language and make the engine underneath capable of treating search as a peer of every other physical operator.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-this-looks-like-in-practice">What this looks like in practice<a href="https://serenedb.com/blog/sql-for-search#what-this-looks-like-in-practice" class="hash-link" aria-label="Direct link to What this looks like in practice" title="Direct link to What this looks like in practice">​</a></h2>
<p><strong>Weekly volume and top relevance, by author.</strong> A content team wants to see how often articles about distributed systems are appearing, who's writing them and the strongest match per author each week:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    date_trunc</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'week'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> published_at</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> week</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    author_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">COUNT</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">*</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">      </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> matches</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">MIN</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">distance</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> closest_match</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> author_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">           published_at</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">           body_embedding </span><span class="token operator" style="color:#393A34">&lt;=&gt;</span><span class="token plain"> ai_embed</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">             </span><span class="token string" style="color:#e3116c">'consensus algorithms and fault tolerance in databases'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">             </span><span class="token string" style="color:#e3116c">'text-embedding-3-small'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">             </span><span class="token string" style="color:#e3116c">'openai'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">::</span><span class="token keyword" style="color:#00009f">FLOAT</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">1536</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> distance</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain">   articles_idx</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain">  body @@ </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'distributed systems'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'blockchain'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token operator" style="color:#393A34">AND</span><span class="token plain">  published_at </span><span class="token operator" style="color:#393A34">&gt;=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">DATE</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'2025-01-01'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> t</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">GROUP</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> week</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> author_id</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> week</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> closest_match</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>A lexical filter generates the candidate set; the vector distance to the query embedding becomes a first-class column. <code>GROUP BY</code> rolls it up by week and author and <code>MIN(distance)</code> ranks within each bucket.</p>
<p><strong>Search joined with normalized data.</strong> Products matching a search, joined to inventory and warehouse, aggregated by region:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    region</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">COUNT</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">*</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">                     </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> matching_products</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">SUM</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">units_available</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">         </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> total_stock</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    quantile_cont</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">distance</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.5</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> median_distance</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> w</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">region</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">           i</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">units_available</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">           p</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">description_embedding </span><span class="token operator" style="color:#393A34">&lt;=&gt;</span><span class="token plain"> ai_embed</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">             </span><span class="token string" style="color:#e3116c">'wireless noise-cancelling headphones with long battery life'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">             </span><span class="token string" style="color:#e3116c">'text-embedding-3-small'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">             </span><span class="token string" style="color:#e3116c">'openai'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">::</span><span class="token keyword" style="color:#00009f">FLOAT</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">1536</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> distance</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain">   products_idx </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> p</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">JOIN</span><span class="token plain">   inventory    </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> i </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> i</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">product_id </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> p</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">JOIN</span><span class="token plain">   warehouses   </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> w </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> w</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id         </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> i</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">warehouse_id</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain">  p</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">description @@ ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'headphones'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> t</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">GROUP</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> region</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> matching_products </span><span class="token keyword" style="color:#00009f">DESC</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Three tables, a hybrid lexical + vector search predicate, two joins, a <code>GROUP BY</code> and a continuous-quantile aggregation over vector distances. One query plan. More than anything else, this single query plan — with the search predicate as one of its nodes — demonstrates the unique optimization of search-OLAP.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="search-olap-over-the-data-lake">Search-OLAP over the data lake<a href="https://serenedb.com/blog/sql-for-search#search-olap-over-the-data-lake" class="hash-link" aria-label="Direct link to Search-OLAP over the data lake" title="Direct link to Search-OLAP over the data lake">​</a></h2>
<p>The examples above run over local tables. Most analytical data lives in object storage — Iceberg, Delta, or raw Parquet — in data lakes, not warehouses. DuckDB and ClickHouse made it normal to query that data where it sits: no ingestion, no copy, no round-trip. SereneDB extends that model to search: index and query remote data with the same SQL you'd use against a local table.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">VIEW</span><span class="token plain"> events </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> iceberg_scan</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'s3://warehouse/events/'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> allow_moved_paths</span><span class="token operator" style="color:#393A34">=</span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> events_idx </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> events</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">USING</span><span class="token plain"> inverted</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    message    en_dict</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    embedding  ivf </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">metric </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'cosine'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> INCLUDE </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event_time</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> service</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> severity</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">hour</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    service</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">COUNT</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">*</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">                      </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> matching_errors</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    quantile_cont</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">distance</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.95</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> p95_distance</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> date_trunc</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'hour'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">event_time</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">hour</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">           e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">service</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">           e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">embedding </span><span class="token operator" style="color:#393A34">&lt;=&gt;</span><span class="token plain"> ai_embed</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">             </span><span class="token string" style="color:#e3116c">'upstream service unreachable, TCP connection refused'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">             </span><span class="token string" style="color:#e3116c">'text-embedding-3-small'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">             </span><span class="token string" style="color:#e3116c">'openai'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">::</span><span class="token keyword" style="color:#00009f">FLOAT</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">1536</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> distance</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain">   events_idx </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> e</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain">  e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">message @@ ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'connection refused'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token operator" style="color:#393A34">AND</span><span class="token plain">  e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">severity   </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'error'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token operator" style="color:#393A34">AND</span><span class="token plain">  e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">event_time </span><span class="token operator" style="color:#393A34">&gt;=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">NOW</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INTERVAL</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'7 days'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> t</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">GROUP</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">hour</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> service</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> matching_errors </span><span class="token keyword" style="color:#00009f">DESC</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">hour</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>A logs/events lake on S3, in Iceberg, with a hybrid lexical + vector search predicate, with structured filters on severity and time, with a quantile over vector distances grouped by hour and service.</p>
<p style="text-align:center">No copy into a search engine. No ETL. No second system to keep in sync.</p>
<p>SQL is the analytical language. SQL is the language with a slot for search. SQL is what the lakehouse speaks. One engine handling all three means one query plan, no ETL seams (neither between search and analytics nor between local and remote data).</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="one-engine-not-two">One engine, not two<a href="https://serenedb.com/blog/sql-for-search#one-engine-not-two" class="hash-link" aria-label="Direct link to One engine, not two" title="Direct link to One engine, not two">​</a></h2>
<p>The reason these queries run well isn't only the syntax. It's the engine.</p>
<p>SereneDB's core contribution is fusing a search engine with a vectorized analytical executor at the <em>physical-operator</em> level. A search predicate isn't a black box queried over a network. It's a real operator inside the query plan, with cost statistics, selectivity estimates and a row-id-plus-score output that the optimizer reorders against joins, scans and aggregations. That's why BM25 scores and vector distances both work inside window functions, why a remote Iceberg scan and a local indexed table participate in the same join and why the planner (not the application) decides which side of the search/structured boundary drives the query.</p>
<p>The engine builds on top of IResearch — our own search engine, benchmarked ahead of Lucene and Tantivy in the benchmark called Search Benchmark, The Game — plugged in as that physical operator. On the analytical side, we used the vectorized execution and an optimizer from DuckDB. This was a deliberate decision; reinventing analytical execution alongside our search work would have produced something worse. The fusion between the two (the cost model, the index integration, the SQL surface for search expressions) is what makes SereneDB and it's what makes one system instead of two systems sharing a process.</p>
<p>Search-in-SQL itself isn't new. Postgres has had <code>@@</code> for nearly two decades and the operator is good enough that we kept it. ParadeDB and others continue to push modern relevance into the Postgres world; that work is real. What's different about SereneDB is the <em>depth</em> of the integration: a search predicate that's a first-class physical operator inside a vectorized analytical executor, reading both local and remote data natively. That's the point on the design surface SereneDB occupies.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="a-complete-search-engine-plus-an-analytical-executor">A complete search engine, plus an analytical executor<a href="https://serenedb.com/blog/sql-for-search#a-complete-search-engine-plus-an-analytical-executor" class="hash-link" aria-label="Direct link to A complete search engine, plus an analytical executor" title="Direct link to A complete search engine, plus an analytical executor">​</a></h2>
<p>A JSON search DSL has separate query types per mode (match, knn, geo_distance, fuzzy) and combining them means nesting clauses. SQL doesn't have separate query types. It has expressions. SereneDB exposes all of the search capabilities as composable expressions inside @@: full-text and phrase search, n-grams, Levenshtein-based fuzzy matching, vector search, geospatial search and hybrid lexical-vector ranking, freely combined and freely consumed by the analytical SQL around them.</p>
<p>Pair that with an analytical executor of equal weight — vectorized, cost-based, the full surface of analytical SQL — and you have one system carrying both sides at full strength.</p>
<p>One honest caveat though, at the time of writing this runs on a single node. Clustering is on the roadmap but a single SereneDB node is already powerful enough for most workloads (very large deployments are where we're heading next).</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="closing">Closing<a href="https://serenedb.com/blog/sql-for-search#closing" class="hash-link" aria-label="Direct link to Closing" title="Direct link to Closing">​</a></h2>
<p>The next generation of search-aware applications will be built by teams who never had to learn the difference between their search system and their warehouse, who joined search results to relational data without writing glue code and who treated relevance (lexical or semantic) as just another column in analytical SQL. SereneDB is built for those teams! One complete search engine, one vectorized analytical executor, one query language for both.</p></div>]]></content>
        <author>
            <name>Alexander Malandin</name>
            <uri>https://de.linkedin.com/in/malandin/ru?trk=org-employees</uri>
        </author>
        <category label="Search" term="Search"/>
        <category label="Database" term="Database"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[A demo trilogy: Vectorizer and Hybrid search]]></title>
        <id>https://serenedb.com/blog/hybrid-search</id>
        <link href="https://serenedb.com/blog/hybrid-search"/>
        <updated>2026-05-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Keep embeddings next to your data: how SereneDB runs vector similarity and hybrid search and manages embeddings under one index, with no separate vector database to sync.]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="none" viewBox="0 0 28 28" class="logoAvatar_BINX"><path fill="#895AF8" d="M28 14c0-7.732-6.268-14-14-14S0 6.268 0 14s6.268 14 14 14 14-6.268 14-14"></path><path fill="#80BEFF" d="M9.345 18.655c4.556 4.556 11.416 5.084 15.32 1.178 3.906-3.905 3.379-10.764-1.178-15.32C18.931-.043 12.072-.571 8.167 3.334S4.789 14.1 9.345 18.654"></path><path fill="#fff" d="M15.649 12.35c2.278 2.278 5.676 2.574 7.59.66s1.617-5.311-.66-7.59c-2.279-2.277-5.677-2.573-7.59-.66-1.914 1.914-1.618 5.312.66 7.59"></path></svg></div></div><div class="copy_iHNG"><p class="names_ox2I">SereneDB Team</p><p class="meta_OgzX">May 19, 2026 · 9 minutes read</p></div></header><h2 class="title_RQ9v">A demo trilogy: Vectorizer and Hybrid search</h2><p style="font-size:0.9rem;margin-top:-0.4rem">How to use similarity search and manage embeddings in SereneDB?</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg hideFirstHeading_VVMX" data-image-lightbox-root="true"><img src="https://serenedb.com/blog/img/blog/ann-search/serenedb-hybrid-index.svg" alt="SereneDB hybrid index: BM25 over text and HNSW over embedding under one inverted index" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%"><img src="https://serenedb.com/blog/img/blog/ann-search/serenedb-hybrid-index-dark.svg" alt="SereneDB hybrid index: BM25 over text and HNSW over embedding under one inverted index" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%">
<br>
<br>
<p>There is a lot of interest in processing vectors lately, and the usual answer is to set up a separate vector database next to the one that already holds your data.
We think it's the wrong abstraction, and we are not alone in this — see <a href="https://www.tigerdata.com/blog/vector-databases-are-the-wrong-abstraction" target="_blank" rel="noopener noreferrer">Vector Databases Are the Wrong Abstraction</a> from TigerData for the same argument.
Embeddings are not independent data, they are derived from the source text, very much like a regular index.
The moment you put them in a separate system, you have to manage a second database and a sync pipeline between the two, which is quite a hard SRE and DevOps task on its own.
The cleaner approach is to keep embeddings next to the data they describe, and let the database manage them as it already manages indexes.
That is exactly what this post is about.</p>
<p>The <a href="https://serenedb.com/blog/one-sql-three-access-modes">previous post</a> explored feature-rich access patterns in SereneDB.
I highly recommend reading it before this post, so you can understand the full potential of remote indexing.</p>
<p>After reading about remote text indexing in SereneDB, you might have had a desire to use not only full-text indexes, but also similarity search.
Well, lucky for you, we've created a demo that covers search over text embeddings with full-text filters.
Moreover, since you may need to transform text into embeddings yourself, we've added a special function which allows you to trigger your favourite API (OpenAI, Voyage AI, Gemini, Ollama) and transform text into a vector. The second demo describes these cases more deeply.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="demo1-ann-and-hybrid-search">Demo1: ANN and Hybrid search<a href="https://serenedb.com/blog/hybrid-search#demo1-ann-and-hybrid-search" class="hash-link" aria-label="Direct link to Demo1: ANN and Hybrid search" title="Direct link to Demo1: ANN and Hybrid search">​</a></h2>
<p>For the first demo, we took a part of the Wikipedia dataset that has already been preprocessed with openai-3-small-1536.
There are two scenarios of querying: from a table and from a view.
In the first one we insert data from Parquet files into the table and then build an index over the table.
By storing all the data on disk, the user is able to minimize the latency of lookups.
In the second scenario the view is created over a Parquet file and the index is built over the view object.
This allows the user to save disk space at the cost of additional latency from a remote request on each read.</p>
<p>To start, we create a hybrid index:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> dbpedia_idx </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> dbpedia </span><span class="token keyword" style="color:#00009f">USING</span><span class="token plain"> inverted</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">text</span><span class="token plain">       dbpedia_en</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  embedding  ivf </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">metric </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'cosine'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="q1">Q1<a href="https://serenedb.com/blog/hybrid-search#q1" class="hash-link" aria-label="Direct link to Q1" title="Direct link to Q1">​</a></h3>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> title</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain">   dbpedia_idx d</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> d</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">embedding </span><span class="token operator" style="color:#393A34">&lt;=&gt;</span><span class="token plain"> $</span><span class="token number" style="color:#36acaa">1</span><span class="token plain">::</span><span class="token keyword" style="color:#00009f">FLOAT</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">1536</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">LIMIT</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>It is a basic nearest-neighbors query that finds the 5 closest vectors to the given one (<code>$1</code>).
Worth mentioning, the distance metric in the query should match the metric in the index definition.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="q2">Q2<a href="https://serenedb.com/blog/hybrid-search#q2" class="hash-link" aria-label="Direct link to Q2" title="Direct link to Q2">​</a></h3>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> d</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">title</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain">   dbpedia_idx d</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain">  d</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">embedding </span><span class="token operator" style="color:#393A34">&lt;=&gt;</span><span class="token plain"> $</span><span class="token number" style="color:#36acaa">1</span><span class="token plain">::</span><span class="token keyword" style="color:#00009f">FLOAT</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">1536</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.3</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">LIMIT</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>In the next query we find all the fields that are inside a circle of radius 0.3 around the query vector <code>$1</code>.
This is useful when you don't really care about the top-K, but rather about <em>every</em> item that is close enough.
The <code>LIMIT&nbsp;10</code> is just a safety cap on the result size: the radius does the actual filtering.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="q3">Q3<a href="https://serenedb.com/blog/hybrid-search#q3" class="hash-link" aria-label="Direct link to Q3" title="Direct link to Q3">​</a></h3>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> title</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       </span><span class="token keyword" style="color:#00009f">left</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">text</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">80</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> snippet</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain">   dbpedia_idx d</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain">  </span><span class="token keyword" style="color:#00009f">text</span><span class="token plain"> @@ </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'physicist'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'philosophy'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'quantum mechanics'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'general relativity'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> d</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">embedding </span><span class="token operator" style="color:#393A34">&lt;=&gt;</span><span class="token plain"> $</span><span class="token number" style="color:#36acaa">1</span><span class="token plain">::</span><span class="token keyword" style="color:#00009f">FLOAT</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">1536</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">LIMIT</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Next, we add a full-text filter to the <code>Q1</code> query.
The abstract must mention "physicist", must not mention "philosophy", and must contain "quantum mechanics" or "general relativity".
This BM25 filter and the ANN traversal are applied on the fly against the same inverted index, instead of running as two separate stages.
Since the <code>text</code> field is part of the index, the filter is served by the index itself, not by a table lookup, and the table is touched only for the final five rows.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="q4">Q4<a href="https://serenedb.com/blog/hybrid-search#q4" class="hash-link" aria-label="Direct link to Q4" title="Direct link to Q4">​</a></h3>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> title</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       </span><span class="token keyword" style="color:#00009f">left</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">text</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">80</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> snippet</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain">   dbpedia_idx d</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain">  </span><span class="token keyword" style="color:#00009f">text</span><span class="token plain"> @@ </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'quantum'</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">## 'mechanics')</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> ts_levenshtein</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'Schrodinger'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> ts_regexp</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'heisen[bu]+rg'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> d</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">embedding </span><span class="token operator" style="color:#393A34">&lt;=&gt;</span><span class="token plain"> $</span><span class="token number" style="color:#36acaa">1</span><span class="token plain">::</span><span class="token keyword" style="color:#00009f">FLOAT</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">1536</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">LIMIT</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>The full-text side also supports proximity, fuzzy and regex matching.
<code>##</code> matches "quantum" immediately next to "mechanics", <code>ts_levenshtein</code> catches "Schrodinger" / "Schroedinger" / "Schrödinger" with edit distance 2, and <code>ts_regexp</code> restricts to "Heisenberg" / "Heisenburg".
All of these compose with the ANN search in the same query plan.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="demo2-generate-embeddings-yourself">Demo2: Generate embeddings yourself!<a href="https://serenedb.com/blog/hybrid-search#demo2-generate-embeddings-yourself" class="hash-link" aria-label="Direct link to Demo2: Generate embeddings yourself!" title="Direct link to Demo2: Generate embeddings yourself!">​</a></h2>
<p>In case you want your own embeddings, we created the <code>ai_embed</code> function!
<code>ai_embed</code> is the first step towards AI and RAG workloads in SereneDB.
It accesses your custom API endpoint and retrieves embeddings from there:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">-- OpenAI embeddings</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> SECRET openai </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">TYPE</span><span class="token plain">    openai</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  api_key </span><span class="token string" style="color:#e3116c">'API_KEY'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> ai_embed</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'hello world'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'text-embedding-3-small'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'openai'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">-- Gemini embeddings through the OpenAI-compatible API</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> SECRET gemini </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">TYPE</span><span class="token plain">             openai</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  api_key          </span><span class="token string" style="color:#e3116c">'API_KEY'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  base_url         </span><span class="token string" style="color:#e3116c">'https://generativelanguage.googleapis.com'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  embeddings_path  </span><span class="token string" style="color:#e3116c">'/v1beta/openai/embeddings'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> ai_embed</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'hello world'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'gemini-embedding-001'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'gemini'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">-- local Ollama</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> SECRET ollama </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">TYPE</span><span class="token plain">             openai</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  base_url         </span><span class="token string" style="color:#e3116c">'http://&lt;host&gt;:&lt;port&gt;'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  embeddings_path  </span><span class="token string" style="color:#e3116c">'/v1/embeddings'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> ai_embed</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'hello world'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'nomic-embed-text'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'ollama'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>This way, you are able to take control over text transformation right in SereneDB with just 2 statements:</p>
<ol>
<li>Create a secret object with an API key and a custom URL, if needed.</li>
<li>Transform text with any model using the <code>ai_embed</code> function.</li>
</ol>
<p>There are no third-party ETL services nor preprocessed embeddings datasets, just the SereneDB server.
And the second demo will show you how easy it is.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="self-embedding-semantic-search-over-arxiv-abstracts">Self-embedding semantic search over arXiv abstracts<a href="https://serenedb.com/blog/hybrid-search#self-embedding-semantic-search-over-arxiv-abstracts" class="hash-link" aria-label="Direct link to Self-embedding semantic search over arXiv abstracts" title="Direct link to Self-embedding semantic search over arXiv abstracts">​</a></h2>
<p>For the second demo we use a slice of <a href="https://huggingface.co/datasets/neuralwork/arxiver" target="_blank" rel="noopener noreferrer">neuralwork/arxiver</a>
and compute the embeddings on the fly via <code>ai_embed(...)</code> against Google's Gemini API through its
OpenAI-compatible endpoint. The exact same function is reused at query time, so a natural-language
prompt becomes a vector in-line inside the <code>ORDER BY</code>:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> SECRET gemini </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">TYPE</span><span class="token plain">             openai</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  api_key          </span><span class="token string" style="color:#e3116c">'API_KEY'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  base_url         </span><span class="token string" style="color:#e3116c">'https://generativelanguage.googleapis.com'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  embeddings_path  </span><span class="token string" style="color:#e3116c">'/v1beta/openai/embeddings'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TABLE</span><span class="token plain"> arxiv </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  id              </span><span class="token keyword" style="color:#00009f">VARCHAR</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  title           </span><span class="token keyword" style="color:#00009f">VARCHAR</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  abstract        </span><span class="token keyword" style="color:#00009f">VARCHAR</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  authors         </span><span class="token keyword" style="color:#00009f">VARCHAR</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  published_date  </span><span class="token keyword" style="color:#00009f">TIMESTAMP</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  embedding       </span><span class="token keyword" style="color:#00009f">FLOAT</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">3072</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">INSERT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INTO</span><span class="token plain"> arxiv</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       title</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       abstract</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       authors</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       published_date</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       ai_embed</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">abstract</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'gemini-embedding-001'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'gemini'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">::</span><span class="token keyword" style="color:#00009f">FLOAT</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">3072</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">         title</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">         abstract</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">         authors</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">         strptime</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">published_date</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'%Y-%m-%dT%H:%M:%SZ'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> published_date</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain">   read_parquet</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">           </span><span class="token string" style="color:#e3116c">'https://huggingface.co/datasets/neuralwork/arxiver/resolve/main/data/train.parquet'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> src</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> arxiv_idx </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> arxiv </span><span class="token keyword" style="color:#00009f">USING</span><span class="token plain"> inverted</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  abstract   arxiv_en</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  embedding  ivf </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">metric </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'cosine'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>As before we created one hybrid index over two fields: abstract (<code>VARCHAR</code>) and embedding (<code>FLOAT[3072]</code>).</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="q1--pure-semantic-search">Q1 — pure semantic search<a href="https://serenedb.com/blog/hybrid-search#q1--pure-semantic-search" class="hash-link" aria-label="Direct link to Q1 — pure semantic search" title="Direct link to Q1 — pure semantic search">​</a></h3>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> title</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain">   arxiv_idx a</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> a</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">embedding </span><span class="token operator" style="color:#393A34">&lt;=&gt;</span><span class="token plain"> ai_embed</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">           </span><span class="token string" style="color:#e3116c">'Compaction in LLM'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">           </span><span class="token string" style="color:#e3116c">'gemini-embedding-001'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">           </span><span class="token string" style="color:#e3116c">'gemini'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">::</span><span class="token keyword" style="color:#00009f">FLOAT</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">3072</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">LIMIT</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Often a user is after the <em>meaning</em> of a query rather than the exact words it contains.
Here is an example of such a query, where the result contains information about papers somehow linked to "Compaction in LLM".
Firstly, the text is transformed using Gemini into a vector, and then (like in <code>Q1</code> of demo 1) the search is run on <code>arxiv_idx</code>.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="q2--hybrid-lexical-scope--semantic-rerank">Q2 — hybrid: lexical scope + semantic rerank<a href="https://serenedb.com/blog/hybrid-search#q2--hybrid-lexical-scope--semantic-rerank" class="hash-link" aria-label="Direct link to Q2 — hybrid: lexical scope + semantic rerank" title="Direct link to Q2 — hybrid: lexical scope + semantic rerank">​</a></h3>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> title</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain">   arxiv_idx a</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain">  abstract @@ </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'OpenAI'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'survey'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ts_starts_with</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'gpt'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> ts_starts_with</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'gemini'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> a</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">embedding </span><span class="token operator" style="color:#393A34">&lt;=&gt;</span><span class="token plain"> ai_embed</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">           </span><span class="token string" style="color:#e3116c">'evaluating frontier model limits'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">           </span><span class="token string" style="color:#e3116c">'gemini-embedding-001'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">           </span><span class="token string" style="color:#e3116c">'gemini'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">::</span><span class="token keyword" style="color:#00009f">FLOAT</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">3072</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">LIMIT</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>The next enhancement is the unification of full-text and ANN searches.
The boolean filter (must mention "OpenAI", must not mention "survey", and must mention GPT or Gemini) and the ANN scoring against "evaluating frontier model limits" run together in the same index pass, so only abstracts that satisfy the filter ever reach the top-5.
This pattern is handy for things like company, author or model names, words that can't be rephrased and have to be present in the text.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="q3--hybrid-search-with-generic-filter">Q3 — Hybrid search with generic filter<a href="https://serenedb.com/blog/hybrid-search#q3--hybrid-search-with-generic-filter" class="hash-link" aria-label="Direct link to Q3 — Hybrid search with generic filter" title="Direct link to Q3 — Hybrid search with generic filter">​</a></h3>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> published_date::</span><span class="token keyword" style="color:#00009f">DATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> published</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       title</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain">   arxiv_idx a</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain">  published_date </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TIMESTAMP</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'2024-01-01'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token operator" style="color:#393A34">AND</span><span class="token plain">  abstract @@ </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ts_starts_with</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'agent'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> ts_starts_with</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'tool'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'robotic'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'chain of thought'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> a</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">embedding </span><span class="token operator" style="color:#393A34">&lt;=&gt;</span><span class="token plain"> ai_embed</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">           </span><span class="token string" style="color:#e3116c">'LLM agents using tools'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">           </span><span class="token string" style="color:#e3116c">'gemini-embedding-001'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">           </span><span class="token string" style="color:#e3116c">'gemini'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">::</span><span class="token keyword" style="color:#00009f">FLOAT</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">3072</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">LIMIT</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>The filter does not have to be FTS, any SQL predicate works.
Here a date predicate keeps only pre-2024 papers, while the boolean BM25 filter (agent/tool terminology, no robotics, and a chain-of-thought hint) and the ANN scoring against "LLM agents using tools" run together over the same index, so the agent/tool filter and the semantic ordering are applied in one pass instead of two.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="try-it-yourself">Try it yourself<a href="https://serenedb.com/blog/hybrid-search#try-it-yourself" class="hash-link" aria-label="Direct link to Try it yourself" title="Direct link to Try it yourself">​</a></h2>
<p>Everything in this article lives in the <a href="https://github.com/serenedb/serenedb/tree/main/examples" target="_blank" rel="noopener noreferrer"><code>examples/</code></a> folder of the SereneDB repo on GitHub:</p>
<ul>
<li><a href="https://github.com/serenedb/serenedb/tree/main/examples/demo4" target="_blank" rel="noopener noreferrer"><code>examples/demo4</code></a>: hybrid vector + full-text search over DBpedia abstracts (the first demo, both native-table and remote-Parquet-view variants)</li>
<li><a href="https://github.com/serenedb/serenedb/tree/main/examples/demo5" target="_blank" rel="noopener noreferrer"><code>examples/demo5</code></a>: self-embedding semantic search over arXiv abstracts via <code>ai_embed</code> (the second demo)</li>
</ul>
<p>Each demo has a runnable <code>demo.sql</code> and a <code>bootstrap.sql</code> file, which sets up the environment (downloads dataset or links view to some endpoint).
To run any of them locally, grab a build from the <a href="https://serenedb.com/download" target="_blank" rel="noopener noreferrer">downloads page</a>.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="whats-next">What's next?<a href="https://serenedb.com/blog/hybrid-search#whats-next" class="hash-link" aria-label="Direct link to What's next?" title="Direct link to What's next?">​</a></h2>
<p>We're actively developing vector search features in SereneDB, so there will be a lot more in the future.
If you don't want to miss it, <a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer"><strong>star us on GitHub</strong></a>.
It genuinely helps us reach more people.</p></div>]]></content>
        <author>
            <name>SereneDB Team</name>
        </author>
        <category label="Search" term="Search"/>
        <category label="Demos" term="Demos"/>
        <category label="ANN" term="ANN"/>
        <category label="AI" term="AI"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[A demo trilogy: one SQL, three access modes]]></title>
        <id>https://serenedb.com/blog/one-sql-three-access-modes</id>
        <link href="https://serenedb.com/blog/one-sql-three-access-modes"/>
        <updated>2026-05-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Three demos run the same SQL — Elastic-grade ranked search plus relational analytics — over remote Parquet, local files and a native SereneDB table, without changing the query.]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><img class="avatarImage_s4Ar" src="https://serenedb.com/blog/img/abramov.jpeg" alt="Andrey Abramov" loading="lazy"></div></div><div class="copy_iHNG"><p class="names_ox2I">Andrey Abramov</p><p class="meta_OgzX">May 7, 2026 · 14 minutes read</p></div></header><h2 class="title_RQ9v">A demo trilogy: one SQL, three access modes</h2><p style="font-size:0.9rem;margin-top:-0.4rem">Elastic-grade search and analytics over remote Parquet, local files and a native table without changing a query</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg hideFirstHeading_VVMX" data-image-lightbox-root="true"><p>The <a href="https://serenedb.com/blog/search-where-your-data-lives">previous post</a> made the case for a search engine that indexes data where it lives, with a SQL surface that lets full-text predicates compose with <code>count</code>, <code>avg</code>, <code>GROUP BY</code> and <code>JOIN</code> in one query. This post is the walkthrough.</p>
<p>We built three small demos that share a <a href="https://huggingface.co/datasets/stanfordnlp/imdb" target="_blank" rel="noopener noreferrer">IMDb reviews on Hugging Face</a> dataset and four queries. Mode 1 indexes the dataset directly from Hugging Face. Mode 2 stages the Parquet locally and points a view at it. Mode 3 ingests the rows into a native SereneDB table. The queries don't change; only the view or table source does.</p>
<p>The point of this article is to show that Elastic-grade ranked search combined with relational analytics over the same dataset works in every mode, expressed as the same SQL. Pick the access mode that fits your operational needs; you don't have to pick a <em>language</em>.</p>
<p>The full demos (DDL, queries, expected output) live in the repo at</p>
<ul>
<li><a href="https://github.com/serenedb/serenedb/tree/main/examples/demo0" target="_blank" rel="noopener noreferrer"><code>examples/demo0</code></a>,</li>
<li><a href="https://github.com/serenedb/serenedb/tree/main/examples/demo1" target="_blank" rel="noopener noreferrer"><code>examples/demo1</code></a></li>
<li><a href="https://github.com/serenedb/serenedb/tree/main/examples/demo2" target="_blank" rel="noopener noreferrer"><code>examples/demo2</code></a>.</li>
</ul>
<p>For a guided tour of the broader FTS surface (phrase with slop, regex, fuzzy, ngram, multiple scoring models, highlights), see <a href="https://github.com/serenedb/serenedb/tree/main/examples/demo3" target="_blank" rel="noopener noreferrer"><code>examples/demo3</code></a>.</p>
<p>Clone the repo and run any of them locally, this article gives you the tour.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-dataset-and-the-four-queries">The dataset and the four queries<a href="https://serenedb.com/blog/one-sql-three-access-modes#the-dataset-and-the-four-queries" class="hash-link" aria-label="Direct link to The dataset and the four queries" title="Direct link to The dataset and the four queries">​</a></h2>
<p>The dataset is the <a href="https://huggingface.co/datasets/stanfordnlp/imdb" target="_blank" rel="noopener noreferrer">IMDb sentiment corpus</a>, 100k movie reviews split across three Parquet shards (train / test / unsupervised) on Hugging Face. Each row has a <code>text</code> column (the review body) and a <code>label</code> column (<code>-1</code> for unsupervised, <code>0</code> for negative, <code>1</code> for positive). 100k reviews is small on purpose, small enough to clone and reproduce on a laptop but big enough that everything that matters in the article shows up clearly.</p>
<p>The same four queries run in every mode. I picked them to cover the things people actually want from search: a pure count, ranked top-K, an analytics aggregate over an FTS-filtered subset and a JOIN with a relational table.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="q1-phrase-count">Q1: phrase count<a href="https://serenedb.com/blog/one-sql-three-access-modes#q1-phrase-count" class="hash-link" aria-label="Direct link to Q1: phrase count" title="Direct link to Q1: phrase count">​</a></h3>
<p>The lightest kind of query you can run. Count matches without ever pulling rows back. Useful for "how many reviews mention X?", faceting and sanity-checking.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">count</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">*</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> hits </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> imdb_idx</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">text</span><span class="token plain"> @@ ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'breathtaking cinematography'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="q2-top-k-with-bm25">Q2: top-K with BM25<a href="https://serenedb.com/blog/one-sql-three-access-modes#q2-top-k-with-bm25" class="hash-link" aria-label="Direct link to Q2: top-K with BM25" title="Direct link to Q2: top-K with BM25">​</a></h3>
<p>The search-engine staple. Phrase, boolean, score boost, ranked top-K. Reads as "find reviews about <code>plot twist</code> (3x boost) OR <code>surprise ending</code>, ranked by BM25, top 5."</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> label</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> BM25</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">imdb_idx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">tableoid</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> score</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">text</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> imdb_idx</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">text</span><span class="token plain"> @@ </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'plot twist'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">^</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">3</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'surprise ending'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ORDER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> BM25</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">imdb_idx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">tableoid</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">DESC</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">LIMIT</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="q3-hybrid-analytics">Q3: hybrid analytics<a href="https://serenedb.com/blog/one-sql-three-access-modes#q3-hybrid-analytics" class="hash-link" aria-label="Direct link to Q3: hybrid analytics" title="Direct link to Q3: hybrid analytics">​</a></h3>
<p>Now the <em>and analytics</em> part. A count and an average label over an FTS-filtered subset. The full-text predicate is a normal <code>WHERE</code> clause and <code>avg(label::float)</code> is a normal aggregate; both live in one query.</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">count</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">*</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">avg</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">label::</span><span class="token keyword" style="color:#00009f">float</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> imdb_idx</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">text</span><span class="token plain"> @@ ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'worst movie ever'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">AND</span><span class="token plain"> label </span><span class="token operator" style="color:#393A34">&gt;=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="q4-join-with-a-labels-lookup">Q4: JOIN with a labels lookup<a href="https://serenedb.com/blog/one-sql-three-access-modes#q4-join-with-a-labels-lookup" class="hash-link" aria-label="Direct link to Q4: JOIN with a labels lookup" title="Direct link to Q4: JOIN with a labels lookup">​</a></h3>
<p>The FTS predicate inside a relational JOIN. Find the reviews matching Q2's compound query, join to a labels lookup, group by sentiment, average BM25 relevance per group. The <code>labels</code> CTE here is a <code>VALUES</code> literal only to keep the example self-contained. In real use you'd be joining to whatever table makes sense (a products table, a users table, an organisations table, anything indexed by the join key).</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">WITH</span><span class="token plain"> labels</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">label</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> name</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">VALUES</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">-</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'unsupervised'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'negative'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'positive'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> l</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">name</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">count</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">*</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">round</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">avg</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">BM25</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">imdb_idx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">tableoid</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">::</span><span class="token keyword" style="color:#00009f">numeric</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> imdb_idx </span><span class="token keyword" style="color:#00009f">JOIN</span><span class="token plain"> labels l </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> imdb_idx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">label </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> l</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">label</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">WHERE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">text</span><span class="token plain"> @@ </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ts_phrase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'plot twist'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">^</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">3</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'surprise ending'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">GROUP</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">BY</span><span class="token plain"> l</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">name</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>From here on, the four queries are identical in every mode.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-the-surface-looks-like">What the surface looks like<a href="https://serenedb.com/blog/one-sql-three-access-modes#what-the-surface-looks-like" class="hash-link" aria-label="Direct link to What the surface looks like" title="Direct link to What the surface looks like">​</a></h2>
<p>Two DDL statements set up everything in every mode. A <strong>text search dictionary</strong> is the analyzer config (locale, case folding, stemming, normalization):</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TEXT</span><span class="token plain"> SEARCH DICTIONARY imdb_en </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  template </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'text'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  locale </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'en_US.UTF-8'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'lower'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  frequency </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  position </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  norm </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>The <strong>inverted index</strong> sits next to the data like any other secondary index:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> imdb_idx </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">source</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">USING</span><span class="token plain"> inverted</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">text</span><span class="token plain"> imdb_en</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> label</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>The <code>@@</code> operator runs the search inside <code>WHERE</code>. Phrase (<code>ts_phrase('a b')</code>), boolean (<code>||</code> / <code>&amp;&amp;</code> / <code>!!</code>), score boost (<code>^ N</code>), prefix, wildcard, regex and fuzzy are all first-class SQL and they compose with each other and with the rest of the query.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="mode-1-point-at-a-url-search-it">Mode 1: point at a URL, search it<a href="https://serenedb.com/blog/one-sql-three-access-modes#mode-1-point-at-a-url-search-it" class="hash-link" aria-label="Direct link to Mode 1: point at a URL, search it" title="Direct link to Mode 1: point at a URL, search it">​</a></h2>
<p>This was the demo I was most excited to build, because it shouldn't quite be possible. The full setup is three statements (<a href="https://github.com/serenedb/serenedb/blob/main/examples/demo0/demo.sql" target="_blank" rel="noopener noreferrer">demo0/demo.sql</a>):</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TEXT</span><span class="token plain"> SEARCH DICTIONARY imdb_en </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  template </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'text'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  locale </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'en_US.UTF-8'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'lower'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  frequency </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  position </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  norm </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">VIEW</span><span class="token plain"> imdb_v </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> read_parquet</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token string" style="color:#e3116c">'hf://datasets/stanfordnlp/imdb@~parquet/plain_text/**/*.parquet'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> imdb_idx </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> imdb_v </span><span class="token keyword" style="color:#00009f">USING</span><span class="token plain"> inverted</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">text</span><span class="token plain"> imdb_en</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> label</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>That's it. <code>hf://</code> is Hugging Face's storage scheme; <code>@~parquet</code> is shorthand for the auto-converted parquet branch every HF dataset gets; <code>**/*.parquet</code> expands across the train, test and unsupervised shards. The index lives next to your local SereneDB instance, the data stays on Hugging Face's CDN. No copy of the rows lives anywhere in SereneDB; the index just records where each row is.</p>
<p>All four queries run end-to-end against this view. Q1's phrase count, Q2's BM25-ranked top-K with materialised review text, Q3's count + average over an FTS-filtered subset, Q4's JOIN with the labels lookup grouped by sentiment.</p>
<p>You don't need anything except this. Two DDL statements, one URL, ranked search and SQL analytics over a public dataset. When the server restarts the index is still on disk; only the row content has to be re-fetched from Hugging Face on demand.</p>
<p>This mode is useful when you want to work with data wherever it actually sits. Point at a public dataset on Hugging Face, a Kaggle dump or a bucket on S3 and run real ranked queries. Agents fall naturally into the same pattern. A user-turn might need to search a corpus the agent doesn't own, where no pre-built ingest pipeline can keep up because the next user-turn might want a different corpus. Cross-team data sharing inside a single organisation gets simpler too. One team's bucket becomes searchable from another team's engine, no fan-out copy required.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="mode-2-same-sql-stage-parquet-locally">Mode 2: same SQL, stage Parquet locally<a href="https://serenedb.com/blog/one-sql-three-access-modes#mode-2-same-sql-stage-parquet-locally" class="hash-link" aria-label="Direct link to Mode 2: same SQL, stage Parquet locally" title="Direct link to Mode 2: same SQL, stage Parquet locally">​</a></h2>
<p>The natural next thing to try is to stage the same Parquet locally and run the same queries against it. The bootstrap is one statement, a server-side <code>COPY</code> that pulls the source from Hugging Face once and writes to local disk (<a href="https://github.com/serenedb/serenedb/blob/main/examples/demo1/bootstrap.sql" target="_blank" rel="noopener noreferrer">demo1/bootstrap.sql</a>):</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">COPY </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> read_parquet</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">'hf://datasets/stanfordnlp/imdb@~parquet/plain_text/train/0000.parquet'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TO</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'/tmp/imdb_train.parquet'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">FORMAT PARQUET</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>From there the view points at the local glob instead of the <code>hf://</code> URL (<a href="https://github.com/serenedb/serenedb/blob/main/examples/demo1/demo.sql" target="_blank" rel="noopener noreferrer">demo1/demo.sql</a>):</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">VIEW</span><span class="token plain"> imdb_v </span><span class="token keyword" style="color:#00009f">AS</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> read_parquet</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'/tmp/imdb_*.parquet'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> imdb_idx </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> imdb_v </span><span class="token keyword" style="color:#00009f">USING</span><span class="token plain"> inverted</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">text</span><span class="token plain"> imdb_en</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> label</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>The dictionary DDL, the index DDL and the four queries are byte-for-byte identical to Mode 1. The only thing that changed is one URL string in the view body. Same SQL, same result rows.</p>
<p>This mode is useful for datasets you want a local copy of, without writing them into a database. The local Parquet <em>is</em> your copy; the index lives alongside it. Training corpora that update by file replacement (model checkpoints, archive dumps, snapshot deliveries) fit cleanly. Re-stage the file when it changes, queries don't. Reference data published as Parquet by an upstream you don't control (regulatory feeds, partner-published catalogs) works the same way. And in air-gapped or low-egress environments where remote URLs aren't an option but the data file is, this is what you reach for. The local copy is one <code>COPY</code> statement; the queries against it are the queries you already have.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="mode-3-ingest-once-queries-are-local-from-then-on">Mode 3: ingest once, queries are local from then on<a href="https://serenedb.com/blog/one-sql-three-access-modes#mode-3-ingest-once-queries-are-local-from-then-on" class="hash-link" aria-label="Direct link to Mode 3: ingest once, queries are local from then on" title="Direct link to Mode 3: ingest once, queries are local from then on">​</a></h2>
<p>The third mode is the one that looks most like a traditional database. Instead of a view, the data lives in a regular SereneDB table; the inverted index sits next to the rows in SereneDB's native storage. The bootstrap is a one-shot ingest (<a href="https://github.com/serenedb/serenedb/blob/main/examples/demo2/bootstrap.sql" target="_blank" rel="noopener noreferrer">demo2/bootstrap.sql</a>):</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TABLE</span><span class="token plain"> imdb </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id </span><span class="token keyword" style="color:#00009f">INTEGER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">PRIMARY</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">KEY</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">text</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TEXT</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> label </span><span class="token keyword" style="color:#00009f">INTEGER</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">INSERT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INTO</span><span class="token plain"> imdb</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">SELECT</span><span class="token plain"> row_number</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">OVER</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">::</span><span class="token keyword" style="color:#00009f">INTEGER</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">text</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> label::</span><span class="token keyword" style="color:#00009f">INTEGER</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">FROM</span><span class="token plain"> read_parquet</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">'hf://datasets/stanfordnlp/imdb@~parquet/plain_text/**/*.parquet'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">INDEX</span><span class="token plain"> imdb_idx </span><span class="token keyword" style="color:#00009f">ON</span><span class="token plain"> imdb </span><span class="token keyword" style="color:#00009f">USING</span><span class="token plain"> inverted</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">text</span><span class="token plain"> imdb_en</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> label</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>One network fetch and the data is in your database. The queries themselves (<a href="https://github.com/serenedb/serenedb/blob/main/examples/demo2/demo.sql" target="_blank" rel="noopener noreferrer">demo2/demo.sql</a>) are byte-for-byte identical to Modes 1 and 2.</p>
<p>There's a reason this mode reads differently from Mode 2. Parquet is a columnar format optimised for full-scan analytics (reading whole columns, aggregating, filtering large ranges). It carries a noticeable overhead for <em>random</em> row lookups, even on a local file, because the format isn't designed for that access pattern. Every materialised row in Modes 1 and 2 pays it. Native storage uses row-oriented point lookups that match per-row materialisation directly, so that overhead goes away.</p>
<p>The bigger thing this mode brings is <strong>production behaviour</strong>. <code>INSERT</code>, <code>UPDATE</code> and <code>DELETE</code> against the <code>imdb</code> table update the inverted index transactionally, in the same transaction as the row write. Crash recovery uses the WAL the same way it does for any other SereneDB table. Backup, restore and replication all just work, with the FTS index along for the ride like any other secondary index. Search isn't a separate cluster you keep in sync; it's a property of the table.</p>
<p>This mode is useful for production workloads. Live application data with mutations (user-generated content, product catalogs, message threads) flows through <code>INSERT</code>, <code>UPDATE</code> and <code>DELETE</code> on the table. The inverted index updates in the same transaction. Multi-tenant systems benefit from having the FTS index inherit the same backup, restore and replication story as the rows themselves. And anywhere queries need to feel instant (interactive UIs, autocomplete, type-as-you-search, suggested results) belongs here.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-changed-across-modes">What changed across modes<a href="https://serenedb.com/blog/one-sql-three-access-modes#what-changed-across-modes" class="hash-link" aria-label="Direct link to What changed across modes" title="Direct link to What changed across modes">​</a></h2>
<img src="https://serenedb.com/blog/img/blog/one-sql-three-access-modes/three-modes-light.svg" alt="One SQL, three access modes" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%"><img src="https://serenedb.com/blog/img/blog/one-sql-three-access-modes/three-modes-dark.svg" alt="One SQL, three access modes" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%">
<p>The query surface is identical across all three modes. The diagram above is the proof. What changes is the operational shape around it:</p>
<table><thead><tr><th></th><th>Mode 1 (remote)</th><th>Mode 2 (local)</th><th>Mode 3 (native)</th></tr></thead><tbody><tr><td>Where the data lives</td><td>wherever it sits (<code>hf://</code>, S3, ...)</td><td>local file (<code>/tmp/...parquet</code>)</td><td>inside the database (<code>imdb</code> table)</td></tr><tr><td>Data duplicated?</td><td>No, index references the source</td><td>One local Parquet mirror</td><td>Yes, ingested as rows</td></tr><tr><td>Setup</td><td>one view DDL</td><td>one <code>COPY</code> + one view DDL</td><td><code>CREATE TABLE</code> + <code>INSERT</code> + index DDL</td></tr><tr><td>Updates</td><td>re-read source on demand</td><td>re-stage when source changes</td><td><code>INSERT</code> / <code>UPDATE</code> / <code>DELETE</code></td></tr><tr><td>Durability</td><td>owned upstream</td><td>owned upstream</td><td>WAL, transactional</td></tr></tbody></table>
<p>As you move right, you take on more setup, more control and more responsibility for the data. As you move left, less setup and less ownership; you read whatever the upstream gives you. The query layer stays the same either way. You moved a workload from "look at this remote dataset" to "host it locally" to "ingest into production" by changing one URL or one DDL statement.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="why-this-matters">Why this matters<a href="https://serenedb.com/blog/one-sql-three-access-modes#why-this-matters" class="hash-link" aria-label="Direct link to Why this matters" title="Direct link to Why this matters">​</a></h2>
<p>The point isn't that one mode is faster than another. The point is that <strong>ranked full-text search combined with relational analytics works against data wherever it lives</strong>, with the same SQL.</p>
<p>Three concrete things come out of that:</p>
<p><strong>Search on data you don't own.</strong> A <code>hf://</code> URL or a Parquet path on S3 is a complete recipe. No infrastructure, no copy, no schema mapping. A researcher, a data scientist or an agent answering a per-question retrieval can have ranked search over a public dataset in seconds.</p>
<p><strong>Search and analytics in one query.</strong> <code>text @@ ts_phrase('...')</code> is a predicate like any other. You compose it with <code>count</code>, <code>avg</code>, <code>GROUP BY</code>, <code>JOIN</code>. Q3 and Q4 in this article are the small version of the pattern, asking things like "how often does this phrase show up, broken down by sentiment" or "what's the average BM25 relevance per group, joined to a metadata table". This is hard to express when search is a separate cluster speaking JSON. It's a normal SQL query when it isn't.</p>
<p><strong>One surface, three operational shapes.</strong> A team can prototype against a remote URL, stage a local mirror for read-only workloads and ingest into a native table for production, without rewriting any of the queries that drive the application. That's a property worth having.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="beyond-this-article">Beyond this article<a href="https://serenedb.com/blog/one-sql-three-access-modes#beyond-this-article" class="hash-link" aria-label="Direct link to Beyond this article" title="Direct link to Beyond this article">​</a></h2>
<p>This is one chapter of a longer story. Each of the next pieces gets its own post.</p>
<p><strong>Multi-source queries.</strong> Indexes built on different sources can be <code>JOIN</code>-ed or <code>UNION</code>-ed in the same query. A support-ticket search index on a native SereneDB table can be unioned with a product-docs index on remote Parquet and the merged result ranked by BM25 across both. Already supported; deserves a dedicated post given the scope.</p>
<p><strong>Iceberg tables as a first-class FTS source.</strong> The same view-backed indexing pattern in this article works against Iceberg as it does against Parquet. Already shipping; showcased separately in an upcoming post.</p>
<p><strong>Vector index with hybrid search.</strong> Same <code>CREATE INDEX</code> shape, vector method instead of inverted. The interesting part is composability. Vector similarity, BM25 ranking and a relational <code>WHERE</code> filter all compose in one SQL query. Hybrid retrieval (the inverted index narrows the candidate set with arbitrarily complex FTS conditions such as phrase, boolean, regex, fuzzy and ngram, then the vector index re-ranks within by similarity; or the reverse) is the natural pattern. Demo and post coming.</p>
<p><strong>More analyzers and language packs.</strong> English is what we use here; more locales and stemming variants are available. The dictionary template system is open enough to plug in new ones.</p>
<p><strong>The FTS breadth tour.</strong> Phrase + slop, regex, fuzzy, ngram, four scoring models (BM25 / TFIDF / LM-Dirichlet / DFI) and highlights. All the parts that didn't fit in this article. The guided tour is already in the repo at <a href="https://github.com/serenedb/serenedb/tree/main/examples/demo3" target="_blank" rel="noopener noreferrer"><code>examples/demo3</code></a>; the dedicated post lands soon.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="try-it-yourself">Try it yourself<a href="https://serenedb.com/blog/one-sql-three-access-modes#try-it-yourself" class="hash-link" aria-label="Direct link to Try it yourself" title="Direct link to Try it yourself">​</a></h2>
<p>Everything in this article lives in the <a href="https://github.com/serenedb/serenedb/tree/main/examples" target="_blank" rel="noopener noreferrer"><code>examples/</code></a> folder of the SereneDB repo on GitHub:</p>
<ul>
<li><a href="https://github.com/serenedb/serenedb/tree/main/examples/demo0" target="_blank" rel="noopener noreferrer"><code>examples/demo0</code></a>: Mode 1, remote <code>hf://</code> view</li>
<li><a href="https://github.com/serenedb/serenedb/tree/main/examples/demo1" target="_blank" rel="noopener noreferrer"><code>examples/demo1</code></a>: Mode 2, local Parquet view</li>
<li><a href="https://github.com/serenedb/serenedb/tree/main/examples/demo2" target="_blank" rel="noopener noreferrer"><code>examples/demo2</code></a>: Mode 3, native SereneDB table</li>
<li><a href="https://github.com/serenedb/serenedb/tree/main/examples/demo3" target="_blank" rel="noopener noreferrer"><code>examples/demo3</code></a>: the FTS breadth tour (for the follow-up post)</li>
</ul>
<p>Each demo has a runnable <code>demo.sql</code>, a <code>README</code> with expected output and (for Modes 2 and 3) a <code>bootstrap.sql</code>. To run any of them locally, grab a build from the <a href="https://serenedb.com/download" target="_blank" rel="noopener noreferrer">downloads page</a>. Tarballs, deb and platform binaries are also on the <a href="https://github.com/serenedb/serenedb/releases" target="_blank" rel="noopener noreferrer">GitHub releases page</a>.</p>
<p>If you find a sharp edge or a feature you wish existed, file an <a href="https://github.com/serenedb/serenedb/issues" target="_blank" rel="noopener noreferrer">issue</a>. Engineering reads them and prioritises directly.</p>
<p>If you like what you see, <a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer"><strong>star us on GitHub</strong></a>. It genuinely helps us reach more people.</p></div>]]></content>
        <author>
            <name>Andrey Abramov</name>
            <uri>https://www.linkedin.com/in/andrey-abramov-57a16241/ru/?skipRedirect=true</uri>
        </author>
        <category label="Search" term="Search"/>
        <category label="FTS" term="FTS"/>
        <category label="Demos" term="Demos"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Search where your data lives]]></title>
        <id>https://serenedb.com/blog/search-where-your-data-lives</id>
        <link href="https://serenedb.com/blog/search-where-your-data-lives"/>
        <updated>2026-05-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Elasticsearch shipped in 2012 and the deal was simple: your data lived in Postgres or MySQL; you copied it into Lucene; you got ranked search. The duplication tax was the price of admission. For most of the years since, that's been the pattern - ETL into a search engine, run it on a separate cluster, keep the two halves in sync.]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><img class="avatarImage_s4Ar" src="https://serenedb.com/blog/img/abramov.jpeg" alt="Andrey Abramov" loading="lazy"></div></div><div class="copy_iHNG"><p class="names_ox2I">Andrey Abramov</p><p class="meta_OgzX">May 6, 2026 · 6 minutes read</p></div></header><h2 class="title_RQ9v">Search where your data lives</h2><p style="font-size:0.9rem;margin-top:-0.4rem">Why search engines haven't followed the data into the lake and why agents need them to</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg hideFirstHeading_VVMX" data-image-lightbox-root="true"><p>Elasticsearch shipped in 2012 and the deal was simple: your data lived in Postgres or MySQL; you copied it into Lucene; you got ranked search. The duplication tax was the price of admission. For most of the years since, that's been the pattern - ETL into a search engine, run it on a separate cluster, keep the two halves in sync.</p>
<p>The analytical landscape has moved since. Operational data still lives in row-stores, but most <em>analytical</em> data now sits across a wider list of sources, such as Parquet files on S3, Iceberg tables in a lakehouse, JSON or CSV files dropped into a bucket and increasingly data hubs like Hugging Face that publish datasets directly over HTTPS. The volumes have grown to the point where moving data is the expensive operation. You can't pre-ingest what you don't know you'll need.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="where-agent-context-comes-from">Where agent context comes from<a href="https://serenedb.com/blog/search-where-your-data-lives#where-agent-context-comes-from" class="hash-link" aria-label="Direct link to Where agent context comes from" title="Direct link to Where agent context comes from">​</a></h2>
<p>The agents case is where this shows up most clearly. Watch what an agent actually reaches for to answer one real question.</p>
<p>A research agent answering "find me recent papers on quantum error correction" wants arXiv, plus a colleague's S3 bucket of pre-prints, plus your own local notes. A support copilot wants the ticket history, the product docs, the customer's Slack thread and yesterday's release notes. Each agent question pulls from a different mix of stores and the next question may pull from a different mix again.</p>
<p>You can't make this work by ingesting everything into a single search cluster ahead of time. The volume is too big, the staleness window is too short and the relevant subset is decided <em>per question</em>, not per pipeline. What you actually want is to let the agent navigate the data where it lives. Search and analytics are the right tools for that and they always have been. What's new is that the data is no longer in one place, so the engine has to be able to read across all of it.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="federation-is-plumbing---its-not-enough-on-its-own">Federation is plumbing - it's not enough on its own<a href="https://serenedb.com/blog/search-where-your-data-lives#federation-is-plumbing---its-not-enough-on-its-own" class="hash-link" aria-label="Direct link to Federation is plumbing - it's not enough on its own" title="Direct link to Federation is plumbing - it's not enough on its own">​</a></h2>
<p>"Federated" means: query the Parquet on S3, the Iceberg table in your warehouse, the JSON in a colleague's bucket and the rows in your local database, from the same engine, without a copy step. Necessary, but not the whole story. If you can read distributed bytes but can't <em>find</em> what you need in them, you've solved connectivity and nothing else. Search and analytics are what turn distributed bytes into answers.</p>
<p>Analytical engines have largely figured federation out. You point a modern analytical engine at a Parquet file and you get aggregates - no ingest required. That's now the dominant pattern. Search engines, though, are still ingest-first. The typical pattern is: copy your data into a separate cluster, in the cluster's own format and keep the two halves in sync.</p>
<img src="https://serenedb.com/blog/img/blog/search-where-your-data-lives/etl-vs-federated-light.svg" alt="From ETL to federated search" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%"><img src="https://serenedb.com/blog/img/blog/search-where-your-data-lives/etl-vs-federated-dark.svg" alt="From ETL to federated search" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%">
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-approach-index-where-the-engine-is-data-where-the-data-is">The approach: index where the engine is, data where the data is<a href="https://serenedb.com/blog/search-where-your-data-lives#the-approach-index-where-the-engine-is-data-where-the-data-is" class="hash-link" aria-label="Direct link to The approach: index where the engine is, data where the data is" title="Direct link to The approach: index where the engine is, data where the data is">​</a></h2>
<p>The fix is straightforward to describe. A search index <em>references</em> the data instead of duplicating it. The engine reads the source (a Parquet file on S3, an Iceberg table, a JSON dump on Hugging Face or rows in your local table) and records which rows contain which terms. At query time, the index tells you which rows match. Reading the underlying row content - what databases call <em>materialization</em> - is deferred until a query actually needs it. A <code>COUNT(*)</code>, a <code>GROUP BY</code> on an indexed facet or a top-K with <code>LIMIT</code> pay that cost for a handful of rows or none at all; the rest of the data lake stays on the lake.</p>
<p>A few practical consequences:</p>
<ul>
<li><strong>Zero ETL.</strong> The data lake stays the source of truth. No re-staging, no syncing, no separate cluster to maintain.</li>
<li><strong>One SQL surface, no matter where the rows live.</strong> Whether you're hitting a <code>hf://</code> URL, a local Parquet file or a regular database table, the queries are the same. Only the view or table source changes.</li>
<li><strong>Search and analytics in one query.</strong> Full-text predicates are first-class SQL expressions, so they feed <code>COUNT</code>, <code>AVG</code>, <code>GROUP BY</code> and <code>JOIN</code> like any other column. No round-trips between a search cluster and an analytical engine to stitch results together at the application layer.</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-this-looks-like-in-serenedb">What this looks like in SereneDB<a href="https://serenedb.com/blog/search-where-your-data-lives#what-this-looks-like-in-serenedb" class="hash-link" aria-label="Direct link to What this looks like in SereneDB" title="Direct link to What this looks like in SereneDB">​</a></h2>
<p>SereneDB lets you search and analyse data where it lives. It's a Postgres-compatible engine: drop into <code>psql</code>, use your existing drivers, no extension to install. Inside, an inverted index (powered by <a href="https://blog.serenedb.com/search-benchmark-game-overview" target="_blank" rel="noopener noreferrer">IResearch</a>, the fastest C++ search engine, written by SereneDB team) sits alongside a columnar store with a vectorized analytical executor and a planner that combines inverted-index lookups with columnar scans in a single plan.</p>
<p>The index doesn't own the data. It points at it. The same SQL runs over a <code>hf://</code> URL, a Parquet file on S3, an Iceberg table in your warehouse or rows in a native SereneDB table. The data lake stays the source of truth and the engine reads through to it.</p>
<p>One query, one round-trip. A coding agent asks for "the top 5 hits, grouped by repo, with average relevance per group." A research workflow asks for "matching papers, joined to a metadata table, filtered by year." Both are plain SQL queries. Full-text search is just part of the language, not a separate API you have to learn alongside it. Anything that already speaks Postgres (your driver, your BI tool, your LLM agent) already speaks SereneDB. What used to take three systems (search cluster, analytical engine, app-layer glue) becomes a single SQL query.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="coming-next">Coming next<a href="https://serenedb.com/blog/search-where-your-data-lives#coming-next" class="hash-link" aria-label="Direct link to Coming next" title="Direct link to Coming next">​</a></h2>
<p>The next post walks through three demos that share a dataset (<a href="https://huggingface.co/datasets/stanfordnlp/imdb" target="_blank" rel="noopener noreferrer">IMDb reviews</a>) and the same set of queries, run against remote Parquet on Hugging Face, against local Parquet on disk and against a native SereneDB table. You'll see phrase queries, BM25 ranking, hybrid analytics and JOINs against full-text predicates - with no ingest pipeline in sight.</p>
<p><strong>Next:</strong> <a href="https://serenedb.com/blog/one-sql-three-access-modes">One SQL, three access modes</a> - the demo walkthrough.</p>
<p>If you'd rather skip ahead and play with the demos directly, everything is in the <a href="https://github.com/serenedb/serenedb/tree/main/examples" target="_blank" rel="noopener noreferrer">examples folder</a> of the repo, including install instructions and demo SQL.</p>
<p><a href="https://serenedb.com/download" target="_blank" rel="noopener noreferrer">Downloads page</a><br>
<a href="https://github.com/serenedb/serenedb/releases" target="_blank" rel="noopener noreferrer">GitHub releases</a><br>
<a href="https://github.com/serenedb/serenedb/issues" target="_blank" rel="noopener noreferrer">Issues and feature requests welcome on GitHub</a><br></p>
<p>And if you like what you see, <a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer"><strong>star us on GitHub</strong></a> - it genuinely helps us reach more people.</p></div>]]></content>
        <author>
            <name>Andrey Abramov</name>
            <uri>https://www.linkedin.com/in/andrey-abramov-57a16241/ru/?skipRedirect=true</uri>
        </author>
        <category label="Search" term="Search"/>
        <category label="FTS" term="FTS"/>
        <category label="Concepts" term="Concepts"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[SereneUI: Evolving Through Feedback]]></title>
        <id>https://serenedb.com/blog/serene-ui-evolving</id>
        <link href="https://serenedb.com/blog/serene-ui-evolving"/>
        <updated>2026-05-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[SereneUI: Evolving Through Feedback]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="none" viewBox="0 0 28 28" class="logoAvatar_BINX"><path fill="#895AF8" d="M28 14c0-7.732-6.268-14-14-14S0 6.268 0 14s6.268 14 14 14 14-6.268 14-14"></path><path fill="#80BEFF" d="M9.345 18.655c4.556 4.556 11.416 5.084 15.32 1.178 3.906-3.905 3.379-10.764-1.178-15.32C18.931-.043 12.072-.571 8.167 3.334S4.789 14.1 9.345 18.654"></path><path fill="#fff" d="M15.649 12.35c2.278 2.278 5.676 2.574 7.59.66s1.617-5.311-.66-7.59c-2.279-2.277-5.677-2.573-7.59-.66-1.914 1.914-1.618 5.312.66 7.59"></path></svg></div></div><div class="copy_iHNG"><p class="names_ox2I">SereneDB Team</p><p class="meta_OgzX">May 5, 2026 · 6 minutes read</p></div></header><h2 class="title_RQ9v">SereneUI: Evolving Through Feedback</h2><p style="font-size:0.9rem;margin-top:-0.4rem">How community shapes our products</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg hideFirstHeading_VVMX" data-image-lightbox-root="true"><h2 class="anchor anchorWithStickyNavbar_LWe7" id="sereneui-evolving-through-feedback"><strong>SereneUI: Evolving Through Feedback</strong><a href="https://serenedb.com/blog/serene-ui-evolving#sereneui-evolving-through-feedback" class="hash-link" aria-label="Direct link to sereneui-evolving-through-feedback" title="Direct link to sereneui-evolving-through-feedback">​</a></h2>
<p>Since the first release of SereneUI earlier this year, one thing has been consistent: you told us what wasn't working and we fixed it. SereneUI is a database UI that works with both SereneDB and Postgres and because SereneDB is built for Postgres compatibility, the experience should feel equally at home with either. In this blog post we are telling what changed and why.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="improvements">Improvements<a href="https://serenedb.com/blog/serene-ui-evolving#improvements" class="hash-link" aria-label="Direct link to Improvements" title="Direct link to Improvements">​</a></h3>
<p>Here are the changes that made the biggest impact, focusing on the areas that received the most consistent feedback:</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="1-reworked-ui">1. Reworked UI<a href="https://serenedb.com/blog/serene-ui-evolving#1-reworked-ui" class="hash-link" aria-label="Direct link to 1. Reworked UI" title="Direct link to 1. Reworked UI">​</a></h4>
<blockquote>
<p><em>"Feels pretty good overall, but the contrast is a bit harsh, especially on higher brightness."</em></p>
<p><em>"A light theme would really help. It's hard to use during the day."</em></p>
<p><em>"Love the app, but I keep adjusting my screen brightness every time I switch back and forth because of the dark UI."</em></p>
</blockquote>
<video autoplay="" loop="" muted="" playsinline="" preload="metadata" poster="/blog/assets/images/theme-d2a840edf0f3c3b8d0be45c40648a534.gif"><source src="/blog/assets/medias/theme-0a937d29e3d1f494736e1f10d152c598.webm" type="video/webm"></video>
<br>
<p>The dark theme worked well in isolation but real workflows don't happen in isolation. Switching between apps, adjusting brightness, working in different lighting throughout the day: the UI had to hold up across all of that.</p>
<p>So we revisited the entire UI, rethinking contrast, balance and adaptability, and introduced a light theme alongside an improved dark one. The result is more comfortable across different lighting conditions, more consistent across contexts and better suited for long sessions.</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="2-new-tab-system">2. New tab system<a href="https://serenedb.com/blog/serene-ui-evolving#2-new-tab-system" class="hash-link" aria-label="Direct link to 2. New tab system" title="Direct link to 2. New tab system">​</a></h4>
<blockquote>
<p><em>"Would be great if I could split the editor and run queries side by side."</em></p>
<p><em>"Switching between tabs all the time is a bit annoying. I just want to see multiple results at once."</em></p>
<p><em>"Working with one panel feels limiting when you're comparing queries."</em></p>
</blockquote>
<video autoplay="" loop="" muted="" playsinline="" preload="metadata" poster="/blog/assets/images/panels-d8002f20daec89f610ab8e5178a73e1a.gif"><source src="/blog/assets/medias/panels-1a0eb72f5353f8823bd4f30dabb928a5.webm" type="video/webm"></video>
<br>
<p>One panel is fine until it isn't. When you're comparing results or running queries in parallel, constantly switching tabs breaks the flow more than it helps.</p>
<p>So we introduced a new editor-like tab system, inspired by the way developers work in modern IDEs. Tabs are no longer fixed to a single layout. You can freely move them between panes, split your workspace vertically or horizontally and organize everything the way it fits your workflow. Whether you're comparing results, running multiple queries or exploring different datasets, the interface adapts to you and not the other way around.</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="3-unified-sidebar">3. Unified Sidebar<a href="https://serenedb.com/blog/serene-ui-evolving#3-unified-sidebar" class="hash-link" aria-label="Direct link to 3. Unified Sidebar" title="Direct link to 3. Unified Sidebar">​</a></h4>
<blockquote>
<p><em>"Switching between saved queries and entities feels a bit clunky."</em></p>
<p><em>"I keep losing context when navigating between sections."</em></p>
<p><em>"Would be great to pin frequently used queries somewhere."</em></p>
<p><em>"Changing connections is not very convenient, takes too many steps."</em></p>
</blockquote>
<video autoplay="" loop="" muted="" playsinline="" preload="metadata" poster="/blog/assets/images/sidebar-9084e31f9dbb8811476546579b4581d0.gif"><source src="/blog/assets/medias/sidebar-ae51c2efea95c22267b8d6c6e6e8fb0e.webm" type="video/webm"></video>
<br>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<p>Navigation should feel invisible. But when switching between saved queries and entities costs you context and changing a connection takes too many steps, it starts to get in the way.</p>
<p>The sidebar has been completely rethought into a single structured space. Instead of jumping between separate views, everything now lives in one place organized through collapsible sections, making navigation faster and more predictable. We also introduced pinned items so frequently used entities and saved queries are always within reach.</p>
<p>Connection management has been simplified too. Moving it to the header makes the active context always visible, while quick switching via keyboard lets you change connections instantly without breaking your flow.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="new-features">New Features<a href="https://serenedb.com/blog/serene-ui-evolving#new-features" class="hash-link" aria-label="Direct link to New Features" title="Direct link to New Features">​</a></h3>
<p>While much of our work was driven by feedback, we also continued building towards our core roadmap and introducing new functionality along the way.</p>
<p>Here are the most important additions:</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="1-dashboards">1. Dashboards<a href="https://serenedb.com/blog/serene-ui-evolving#1-dashboards" class="hash-link" aria-label="Direct link to 1. Dashboards" title="Direct link to 1. Dashboards">​</a></h4>
<img src="https://serenedb.com/blog/assets/images/dashboards-100161c0d73f16ac6ed6a6a0f632ef00.png">
<br>
<p>Dashboards started from a simple idea: making it easier to move from querying data to actually understanding it. Instead of exporting results or switching tools, you can now take a query, select the columns you care about, choose a chart template and turn it into a clear structured visualization in seconds.</p>
<p>From there, dashboards grow naturally around your workflow. You can create as many as you need for different use cases, combine data from multiple databases within a single dashboard and keep everything up to date with manual or automatic refresh. What used to be a separate step is now part of the same flow.</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="2-sequential-execution">2. Sequential execution<a href="https://serenedb.com/blog/serene-ui-evolving#2-sequential-execution" class="hash-link" aria-label="Direct link to 2. Sequential execution" title="Direct link to 2. Sequential execution">​</a></h4>
<img src="https://serenedb.com/blog/assets/images/sequential-1001b5da258317d0e2e4dde9888748c0.png">
<br>
<p>By default, queries in SereneUI run as a transaction ensuring consistency and predictable results. But there are cases where you need more control, especially when working with multiple statements that should execute step by step.</p>
<p>With sequential execution you can now switch to that mode when needed. Simply press run, choose Execute sequentially and watch each statement execute one by one, highlighted directly in the editor as it progresses. All results are collected in a single tab, making it easy to navigate between them without losing context or switching views.</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="3-improved-autocompletion">3. Improved Autocompletion<a href="https://serenedb.com/blog/serene-ui-evolving#3-improved-autocompletion" class="hash-link" aria-label="Direct link to 3. Improved Autocompletion" title="Direct link to 3. Improved Autocompletion">​</a></h4>
<img src="https://serenedb.com/blog/assets/images/autocomplition-702921607292f494a9379b5450ed71dc.png">
<br>
<p>Autocompletion has been reworked to better understand context. Instead of generic suggestions, it now adapts to where you are in the query: suggesting tables when you need tables and columns when you need columns.</p>
<p>We also split autocompletion into two modes: standard and inline. Standard autocomplete helps you build queries in real time, while inline autocomplete surfaces suggestions from your query history and saved queries directly as you type. If you prefer a simpler experience, inline suggestions can be disabled in the settings.</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="4-desktop-clients">4. Desktop Clients<a href="https://serenedb.com/blog/serene-ui-evolving#4-desktop-clients" class="hash-link" aria-label="Direct link to 4. Desktop Clients" title="Direct link to 4. Desktop Clients">​</a></h4>
<p>SereneUI is now available across platforms and no longer limited to running through Docker. We introduced desktop clients for each platform to make installation and usage simpler. Setup is faster and getting started doesn't require any additional configuration.</p>
<p>Here are the downloads:</p>
<div class="downloadGrid_NQi0" aria-label="SereneUI downloads"><a class="downloadButton_KsPC" href="https://github.com/serenedb/serenedb/releases/download/v26.04.1-ui/serene-ui-electron_26.04.1_amd64.deb" aria-label="Linux DEB"><span class="iconWrap_YaLe" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="27.77" height="32" viewBox="0 0 256 295" class="icon_oBrO"><defs><linearGradient id="b" x1="48.548%" x2="51.047%" y1="115.276%" y2="41.364%"><stop offset="0%" stop-color="#FFEED7"></stop><stop offset="100%" stop-color="#BDBFC2"></stop></linearGradient><linearGradient id="c" x1="54.407%" x2="46.175%" y1="2.404%" y2="90.542%"><stop offset="0%" stop-color="#FFF" stop-opacity="0.8"></stop><stop offset="100%" stop-color="#FFF" stop-opacity="0"></stop></linearGradient><linearGradient id="d" x1="51.86%" x2="47.947%" y1="88.248%" y2="9.748%"><stop offset="0%" stop-color="#FFEED7"></stop><stop offset="100%" stop-color="#BDBFC2"></stop></linearGradient><linearGradient id="e" x1="49.925%" x2="49.924%" y1="85.49%" y2="13.811%"><stop offset="0%" stop-color="#FFEED7"></stop><stop offset="100%" stop-color="#BDBFC2"></stop></linearGradient><linearGradient id="f" x1="53.901%" x2="45.956%" y1="3.102%" y2="93.895%"><stop offset="0%" stop-color="#FFF" stop-opacity="0.65"></stop><stop offset="100%" stop-color="#FFF" stop-opacity="0"></stop></linearGradient><linearGradient id="g" x1="45.593%" x2="54.811%" y1="5.475%" y2="93.524%"><stop offset="0%" stop-color="#FFF" stop-opacity="0.65"></stop><stop offset="100%" stop-color="#FFF" stop-opacity="0"></stop></linearGradient><linearGradient id="h" x1="49.984%" x2="49.984%" y1="89.845%" y2="40.632%"><stop offset="0%" stop-color="#FFEED7"></stop><stop offset="100%" stop-color="#BDBFC2"></stop></linearGradient><linearGradient id="i" x1="53.505%" x2="42.746%" y1="99.975%" y2="23.545%"><stop offset="0%" stop-color="#FFEED7"></stop><stop offset="100%" stop-color="#BDBFC2"></stop></linearGradient><linearGradient id="j" x1="49.841%" x2="50.241%" y1="13.229%" y2="94.673%"><stop offset="0%" stop-color="#FFF" stop-opacity="0.8"></stop><stop offset="100%" stop-color="#FFF" stop-opacity="0"></stop></linearGradient><linearGradient id="k" x1="49.927%" x2="50.727%" y1="37.327%" y2="92.782%"><stop offset="0%" stop-color="#FFF" stop-opacity="0.65"></stop><stop offset="100%" stop-color="#FFF" stop-opacity="0"></stop></linearGradient><linearGradient id="l" x1="49.876%" x2="49.876%" y1="2.299%" y2="81.204%"><stop offset="0%" stop-color="#FFF" stop-opacity="0.65"></stop><stop offset="100%" stop-color="#FFF" stop-opacity="0"></stop></linearGradient><linearGradient id="m" x1="49.833%" x2="49.824%" y1="2.272%" y2="71.799%"><stop offset="0%" stop-color="#FFF" stop-opacity="0.65"></stop><stop offset="100%" stop-color="#FFF" stop-opacity="0"></stop></linearGradient><linearGradient id="n" x1="53.467%" x2="38.949%" y1="48.921%" y2="98.1%"><stop offset="0%" stop-color="#FFA63F"></stop><stop offset="100%" stop-color="#FF0"></stop></linearGradient><linearGradient id="o" x1="52.373%" x2="47.579%" y1="143.009%" y2="-64.622%"><stop offset="0%" stop-color="#FFEED7"></stop><stop offset="100%" stop-color="#BDBFC2"></stop></linearGradient><linearGradient id="p" x1="30.581%" x2="65.887%" y1="34.024%" y2="89.175%"><stop offset="0%" stop-color="#FFA63F"></stop><stop offset="100%" stop-color="#FF0"></stop></linearGradient><linearGradient id="q" x1="59.572%" x2="48.361%" y1="-17.216%" y2="66.118%"><stop offset="0%" stop-color="#FFF" stop-opacity="0.65"></stop><stop offset="100%" stop-color="#FFF" stop-opacity="0"></stop></linearGradient><linearGradient id="r" x1="47.769%" x2="51.373%" y1="1.565%" y2="104.313%"><stop offset="0%" stop-color="#FFF" stop-opacity="0.65"></stop><stop offset="100%" stop-color="#FFF" stop-opacity="0"></stop></linearGradient><linearGradient id="s" x1="43.55%" x2="57.114%" y1="4.533%" y2="92.827%"><stop offset="0%" stop-color="#FFF" stop-opacity="0.65"></stop><stop offset="100%" stop-color="#FFF" stop-opacity="0"></stop></linearGradient><linearGradient id="t" x1="49.733%" x2="50.558%" y1="17.609%" y2="99.385%"><stop offset="0%" stop-color="#FFA63F"></stop><stop offset="100%" stop-color="#FF0"></stop></linearGradient><linearGradient id="u" x1="50.17%" x2="49.68%" y1="2.89%" y2="94.17%"><stop offset="0%" stop-color="#FFF" stop-opacity="0.65"></stop><stop offset="100%" stop-color="#FFF" stop-opacity="0"></stop></linearGradient><filter id="a" width="200%" height="200%" x="-50%" y="-50%" filterUnits="objectBoundingBox"><feOffset in="SourceAlpha" result="shadowOffsetOuter1"></feOffset><feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation="6.5"></feGaussianBlur></filter></defs><g fill="none"><path fill="#000" fill-opacity="0.2" d="M235.125 249.359c0 17.355-52.617 31.497-117.54 31.497S.044 266.806.044 249.359c0-17.356 52.618-31.498 117.54-31.498 64.924 0 117.45 14.142 117.541 31.498" filter="url(#a)" transform="translate(10)"></path><path fill="#000" d="M63.213 215.474c-11.387-16.346-13.591-69.606 12.947-102.39C89.292 97.383 92.69 86.455 93.7 71.67c.734-16.805-11.846-66.851 35.537-70.616 48.027-3.857 45.364 43.526 45.088 68.596-.183 21.12 15.52 33.15 26.355 49.68 19.927 30.303 18.274 82.461-3.765 110.745-27.916 35.354-51.791 20.018-67.678 21.304-29.752 1.745-30.762 17.54-66.024-35.905"></path><path fill="url(#b)" d="M169.1 122.451c8.265 7.622 29.661 41.69-4.224 62.995-11.937 7.438 10.653 35.721 21.488 22.039 19.193-24.61 6.98-63.913-4.591-77.963-7.714-9.917-19.651-13.774-12.672-7.07" transform="translate(10)"></path><path fill="#000" stroke="#000" stroke-width="0.977" d="M176.805 117.86c13.59 11.02 38.292 49.587 2.204 74.748-11.846 7.806 10.468 32.508 23.049 19.927 43.618-43.894-1.102-94.308-16.53-111.664-13.774-15.151-25.987 3.49-8.723 16.989z"></path><path fill="url(#c)" d="M147.245 25.02c-.459 12.581-14.325 23.51-30.946 24.52S86.639 41 87.097 28.419c.46-12.581 14.326-23.509 30.947-24.519 16.62-.918 29.66 8.54 29.201 21.12" transform="translate(10)"></path><path fill="url(#d)" d="M107.483 54.957c.46 8.173-3.397 15.06-8.723 15.335s-10.01-6.06-10.469-14.233 3.398-15.06 8.724-15.335 10.01 6.06 10.468 14.233" transform="translate(10)"></path><path fill="url(#e)" d="M117.125 55.6c.184 9.458 6.337 16.988 13.683 16.805 7.346-.184 13.131-7.99 12.948-17.54-.184-9.458-6.336-16.988-13.683-16.804-7.346.183-13.223 8.08-12.948 17.539" transform="translate(10)"></path><path fill="#000" d="M133.186 57.712c-.092 5.234 2.48 9.458 5.877 9.458 3.306 0 6.153-4.224 6.245-9.366.091-5.234-2.48-9.459-5.878-9.459-3.397 0-6.152 4.225-6.244 9.367m-21.212.092c.459 4.316-1.194 7.989-3.582 8.356-2.387.276-4.683-2.938-5.142-7.254s1.194-7.99 3.581-8.357c2.388-.275 4.684 2.939 5.143 7.255"></path><path fill="url(#f)" d="M124.564 54.773c-.276 2.939 1.102 5.326 3.03 5.51s3.765-2.112 4.04-4.959c.276-2.938-1.102-5.326-3.03-5.51-1.928-.183-3.765 2.113-4.04 4.96" transform="translate(10)"></path><path fill="url(#g)" d="M99.953 55.508c.276 2.388-.734 4.5-2.203 4.683-1.47.184-2.847-1.653-3.123-4.132-.275-2.388.735-4.5 2.204-4.683 1.47-.184 2.847 1.744 3.122 4.132" transform="translate(10)"></path><path fill="url(#h)" d="M71.027 145.684c6.52-14.785 20.386-40.772 20.662-60.883 0-15.978 47.843-19.835 51.7-3.856 3.856 15.978 13.59 39.853 19.834 51.424 6.245 11.478 24.335 48.118 5.051 80.074-17.356 28.284-69.973 50.69-98.073-3.856-9.55-18.917-7.806-42.333.826-62.903" transform="translate(10)"></path><path fill="url(#i)" d="M65.15 134.664c-5.601 10.56-17.172 38.293 11.112 53.445 30.395 16.162 30.303 49.312-6.245 33.517-33.425-14.233-18.641-71.902-9.274-85.676 6.06-9.642 15.243-21.488 4.407-1.286" transform="translate(10)"></path><path fill="#000" stroke="#000" stroke-width="1.25" d="M79.925 122.727c-8.907 14.509-30.211 48.669-1.652 66.484 38.384 23.6 27.548 47.108-7.53 25.895-49.404-29.568-5.97-89.257 13.774-112.03 22.59-25.529 4.316 4.683-4.592 19.65z"></path><path fill="url(#j)" d="M156.428 151.285c0 16.162-15.519 37.1-42.15 36.916-27.456.183-39.118-20.754-39.118-36.916s18.182-29.293 40.588-29.293c22.498.092 40.68 13.132 40.68 29.293" transform="translate(10)"></path><path fill="url(#k)" d="M141.92 100.504c-.276 16.713-11.204 20.662-24.978 20.662s-23.784-2.48-24.978-20.662c0-11.387 11.203-17.998 24.978-17.998 13.774-.092 24.977 6.52 24.977 17.998" transform="translate(10)"></path><path fill="url(#l)" d="M58.63 126.216c9-13.682 28.008-34.711 3.582 2.939-19.835 31.038-7.346 50.965-.918 56.474 18.549 16.53 17.814 27.64 3.214 18.917-31.314-18.641-24.794-50.047-5.878-78.33" transform="translate(10)"></path><path fill="url(#m)" d="M188.936 131.818c-7.806-16.07-32.6-56.842 1.193-9.459 30.763 42.884 9.183 72.729 5.326 75.667-3.856 2.939-16.804 8.908-13.04-1.469 3.858-10.377 22.958-30.028 6.52-64.74" transform="translate(10)"></path><path fill="url(#n)" stroke="#E68C3F" stroke-width="6.25" d="M51.835 258.542c-20.57-10.928-50.414 2.112-39.578-27.457 2.204-6.704-3.214-16.805.275-23.325 4.133-7.989 13.04-6.244 18.366-11.57 5.234-5.51 8.54-15.06 18.366-13.59 9.734 1.468 16.254 13.406 23.049 28.099 5.05 10.468 22.865 25.253 21.672 37.007-1.47 17.998-21.948 21.396-42.15 10.836z" transform="translate(10)"></path><path fill="url(#o)" d="M201.608 189.119c-3.122 5.877-16.162 15.335-24.886 12.856-8.815-2.388-12.856-15.795-11.111-25.988 1.653-11.386 11.111-12.03 23.05-6.336 12.855 6.336 16.712 11.662 12.947 19.468" transform="translate(10)"></path><path fill="url(#p)" stroke="#E68C3F" stroke-width="6.251" d="M194.445 253.49c15.06-18.273 48.578-14.508 25.988-39.577-4.775-5.418-3.306-16.989-9.183-21.947-6.887-6.061-14.509-1.102-21.488-4.224-6.979-3.398-14.325-9.918-22.865-5.327-8.54 4.684-9.459 16.805-10.285 32.783-.735 11.479-11.203 30.671-5.602 41.231 8.081 16.346 29.11 14.142 43.435-2.938z" transform="translate(10)"></path><path fill="url(#q)" d="M187.925 229.064c23.325-34.435 5.97-34.16.092-36.823-5.877-2.755-12.03-8.173-18.916-4.408-6.888 3.857-7.255 13.775-7.439 26.814-.275 9.367-8.08 25.07-3.397 33.793 5.693 10.193 19.467-4.591 29.66-19.376" transform="translate(10)"></path><path fill="url(#r)" d="M47.06 234.023c-34.895-22.59-18.55-30.303-13.315-33.885 6.336-4.591 6.428-13.407 14.233-12.58 7.806.826 12.397 10.468 17.631 22.406 3.857 8.54 17.264 19.927 16.254 29.753-1.285 11.57-19.743 3.948-34.803-5.694" transform="translate(10)"></path><path fill="#000" d="M209.588 188.843c-2.755 4.776-13.958 12.306-21.396 10.285-7.622-1.928-11.112-12.672-9.55-20.753 1.377-9.183 9.55-9.642 19.834-5.05 10.928 4.958 14.326 9.182 11.112 15.518"></path><path fill="url(#s)" d="M192.058 186.18c-1.745 3.306-9.091 8.54-14.234 7.163-5.142-1.377-7.713-8.815-6.887-14.417.735-6.336 6.244-6.704 13.223-3.581 7.53 3.49 9.918 6.428 7.898 10.835" transform="translate(10)"></path><path fill="url(#t)" stroke="#E68C3F" stroke-width="3.75" d="M97.107 66.344c3.673-3.398 12.58-13.774 29.477-2.939 3.122 2.02 5.693 2.204 11.662 4.775 12.03 4.96 6.336 16.897-6.52 20.937-5.51 1.745-10.468 8.449-20.386 7.806-8.54-.46-10.744-6.06-15.978-9.091-9.275-5.234-10.652-12.305-5.602-16.07 5.051-3.765 6.98-5.143 7.347-5.418z" transform="translate(10)"></path><path stroke="#E68C3F" stroke-width="2.5" d="M148.43 75.986c-5.05.275-15.979 11.203-27.457 11.203s-18.366-10.652-20.11-10.652"></path><path fill="url(#u)" d="M102.8 65.426c1.837-1.653 7.622-6.153 15.244-1.562 1.653.919 3.306 1.929 5.693 3.306 4.867 2.847 2.48 6.98-3.398 9.55-2.663 1.102-7.07 3.49-10.376 3.306-3.673-.367-6.153-2.755-8.54-4.316-4.5-2.938-4.224-5.418-2.112-7.346 1.56-1.47 3.305-2.847 3.49-2.938" transform="translate(10)"></path></g></svg></span><span class="buttonCopy_oYqU"><span class="buttonLabel_XksF">Linux</span><span class="buttonMeta_O7ZT">DEB</span></span></a><a class="downloadButton_KsPC" href="https://github.com/serenedb/serenedb/releases/download/v26.04.1-ui/SereneUI-26.04.1-arm64.dmg" aria-label="macOS DMG"><span class="iconWrap_YaLe" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="209" height="256" viewBox="0 0 814 1000" class="icon_oBrO"><path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2m-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3"></path></svg></span><span class="buttonCopy_oYqU"><span class="buttonLabel_XksF">macOS</span><span class="buttonMeta_O7ZT">DMG</span></span></a><a class="downloadButton_KsPC" href="https://github.com/serenedb/serenedb/releases/download/v26.04.1-ui/SereneUI-26.04.1.Setup.exe" aria-label="Windows EXE"><span class="iconWrap_YaLe" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="88" height="88" viewBox="0 0 88 88" class="icon_oBrO"><path fill="#00adef" d="m0 12.402 35.687-4.86.016 34.423-35.67.203zm35.67 33.529.028 34.453L.028 75.48.026 45.7zm4.326-39.025L87.314 0v41.527l-47.318.376zm47.329 39.349-.011 41.34-47.318-6.678-.066-34.739z"></path></svg></span><span class="buttonCopy_oYqU"><span class="buttonLabel_XksF">Windows</span><span class="buttonMeta_O7ZT">EXE</span></span></a><a class="downloadButton_KsPC" href="https://serenedb.com/blog/serene-ui-evolving#try-it-yourself" aria-label="Docker Quickstart"><span class="iconWrap_YaLe" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" fill="#008fe2" viewBox="0 0 24 24" class="icon_oBrO"><path d="M13.98 11.08h2.12a.19.19 0 0 0 .19-.19V9.01a.19.19 0 0 0-.19-.19h-2.12a.18.18 0 0 0-.18.18v1.9c0 .1.08.18.18.18m-2.95-5.43h2.12a.19.19 0 0 0 .18-.19V3.57a.19.19 0 0 0-.18-.18h-2.12a.18.18 0 0 0-.19.18v1.9c0 .1.09.18.19.18m0 2.71h2.12a.19.19 0 0 0 .18-.18V6.29a.19.19 0 0 0-.18-.18h-2.12a.18.18 0 0 0-.19.18v1.89c0 .1.09.18.19.18m-2.93 0h2.12a.19.19 0 0 0 .18-.18V6.29a.18.18 0 0 0-.18-.18H8.1a.18.18 0 0 0-.18.18v1.89c0 .1.08.18.18.18m-2.96 0h2.11a.19.19 0 0 0 .19-.18V6.29a.18.18 0 0 0-.19-.18H5.14a.19.19 0 0 0-.19.18v1.89c0 .1.08.18.19.18m5.89 2.72h2.12a.19.19 0 0 0 .18-.19V9.01a.19.19 0 0 0-.18-.19h-2.12a.18.18 0 0 0-.19.18v1.9c0 .1.09.18.19.18m-2.93 0h2.12a.18.18 0 0 0 .18-.19V9.01a.18.18 0 0 0-.18-.19H8.1a.18.18 0 0 0-.18.18v1.9c0 .1.08.18.18.18m-2.96 0h2.11a.18.18 0 0 0 .19-.19V9.01a.18.18 0 0 0-.18-.19H5.14a.19.19 0 0 0-.19.19v1.88c0 .1.08.19.19.19m-2.92 0h2.12a.18.18 0 0 0 .18-.19V9.01a.18.18 0 0 0-.18-.19H2.22a.18.18 0 0 0-.19.18v1.9c0 .1.08.18.19.18m21.54-1.19c-.06-.05-.67-.51-1.95-.51-.34 0-.68.03-1.01.09a3.77 3.77 0 0 0-1.72-2.57l-.34-.2-.23.33a4.6 4.6 0 0 0-.6 1.43c-.24.97-.1 1.88.4 2.66a4.7 4.7 0 0 1-1.75.42H.76a.75.75 0 0 0-.76.75 11.4 11.4 0 0 0 .7 4.06 6.03 6.03 0 0 0 2.4 3.12c1.18.73 3.1 1.14 5.28 1.14.98 0 1.96-.08 2.93-.26a12.3 12.3 0 0 0 3.82-1.4 10.5 10.5 0 0 0 2.61-2.13c1.25-1.42 2-3 2.55-4.4h.23c1.37 0 2.21-.55 2.68-1 .3-.3.55-.66.7-1.06l.1-.28Z"></path></svg></span><span class="buttonCopy_oYqU"><span class="buttonLabel_XksF">Docker</span><span class="buttonMeta_O7ZT">Quickstart</span></span></a></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="built-with-you">Built with You<a href="https://serenedb.com/blog/serene-ui-evolving#built-with-you" class="hash-link" aria-label="Direct link to Built with You" title="Direct link to Built with You">​</a></h3>
<p>A lot of what you've seen here comes directly from feedback. It helped us spot friction, rethink decisions and focus on what actually matters in everyday use.</p>
<p>We use it as a signal: to prioritize, to adjust and to move faster in the right direction.</p>
<p>Thanks for taking the time to share it. We'll keep listening and improving.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="try-it-yourself">Try it yourself<a href="https://serenedb.com/blog/serene-ui-evolving#try-it-yourself" class="hash-link" aria-label="Direct link to Try it yourself" title="Direct link to Try it yourself">​</a></h3>
<p>If you are already running Postgres, SereneUI works with your existing setup with no changes needed. Just point it at your database and see how it feels.</p>
<p>Don’t have a Postgres instance at hand? We put together a quickstart script that runs SereneUI and SereneDB together in Docker. No setup, no strings attached, just a quick way to see what it's like.</p></div>]]></content>
        <author>
            <name>SereneDB Team</name>
        </author>
        <category label="UI" term="UI"/>
        <category label="Dev" term="Dev"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Adaptive posting list format]]></title>
        <id>https://serenedb.com/blog/search-optimization-5</id>
        <link href="https://serenedb.com/blog/search-optimization-5"/>
        <updated>2026-04-30T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Search optimization journey 5: Adaptive posting list format]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><img class="avatarImage_s4Ar" src="https://serenedb.com/blog/img/abramov.jpeg" alt="Andrey Abramov" loading="lazy"></div></div><div class="copy_iHNG"><p class="names_ox2I">Andrey Abramov</p><p class="meta_OgzX">Apr 30, 2026 · 18 minutes read</p></div></header><h2 class="title_RQ9v">Adaptive posting list format</h2><p style="font-size:0.9rem;margin-top:-0.4rem">Search optimization journey 5</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg hideFirstHeading_VVMX" data-image-lightbox-root="true"><h2 class="anchor anchorWithStickyNavbar_LWe7" id="search-optimization-journey-5-adaptive-posting-list-format"><strong>Search optimization journey 5: Adaptive posting list format</strong><a href="https://serenedb.com/blog/search-optimization-5#search-optimization-journey-5-adaptive-posting-list-format" class="hash-link" aria-label="Direct link to search-optimization-journey-5-adaptive-posting-list-format" title="Direct link to search-optimization-journey-5-adaptive-posting-list-format">​</a></h2>
<p>In a <a href="https://serenedb.com/blog/search-benchmark-game-overview">recent benchmark</a>, SereneDB's search engine <a href="https://github.com/serenedb/serenedb/tree/main/libs/iresearch" target="_blank" rel="noopener noreferrer">IResearch</a> outperformed established search engines across query types and collection modes. This post is part of the <strong>Search optimization journey</strong> series, a set of technical reports examining the implementation decisions behind those results.</p>
<p>The previous four posts covered query execution: <a href="https://serenedb.com/blog/iresearch-two-phase-queries">lazy two-phase iterators</a>, <a href="https://serenedb.com/blog/search-optimization-2">block-at-a-time SIMD scoring</a>, <a href="https://serenedb.com/blog/search-optimization-1">nth_element top-K collection</a> and <a href="https://serenedb.com/blog/norm-gathering">contiguous-block norm gathering</a>. This post goes one level deeper - to the data the iterators read.</p>
<p>Before any iterator runs, a block of 128 document IDs was compressed to disk. The choices made at that layer have a direct multiplier on everything above: a poorly compressed block takes longer to decode and since posting list decoding sits on the critical path of every single query type, that cost compounds across the entire workload.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-problem-with-a-fixed-posting-list-format">The problem with a fixed posting list format<a href="https://serenedb.com/blog/search-optimization-5#the-problem-with-a-fixed-posting-list-format" class="hash-link" aria-label="Direct link to The problem with a fixed posting list format" title="Direct link to The problem with a fixed posting list format">​</a></h3>
<p>Classical posting list formats commit to one codec per posting list. Lucene's original design, introduced around 2012, used FOR-delta (Frame of Reference with delta coding and bitpacking) for full 128-element blocks, with a VInt-encoded tail for the last partial block. The tail is a special case: it has its own code path and none of the compression options available to full blocks. Tantivy follows a similar model. The tail block is encoded differently from the main blocks and the encoding choice is made once per posting list rather than per block.</p>
<p><img decoding="async" loading="lazy" alt="Fig. 0 - The old fixed format: every full block uses FOR-delta regardless of local data shape; the tail always falls back to VInt with no access to the compression options available to full blocks." src="https://serenedb.com/blog/assets/images/figure-00-fixed-format-34a047c3bb73e4fe109cedd3dabb625b.svg" width="1100" height="467" class="img_ev3q"></p>
<p style="font-size:12px;opacity:0.7"><em>Fig. 0 - The old fixed format: every full block uses FOR-delta regardless of local data shape; the tail always falls back to VInt with no access to the compression options available to full blocks.</em></p>
<p>The fundamental problem is that the value distribution within a block changes throughout a posting list. For example, a fresh segment for a common term like <code>"the"</code> has long runs of nearly-consecutive document IDs efficiently compressible with a bitset. Later blocks, after many small commits and partial merges, may have large irregular gaps between the same term's occurrences. A single encoding scheme cannot be optimal for both shapes. Whichever format you choose, there will be blocks where a different choice would have been two or three times smaller.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-lineage-columnar-compression-research-and-adaptation-in-infromation-retrieval">The lineage: columnar compression research and adaptation in Infromation Retrieval<a href="https://serenedb.com/blog/search-optimization-5#the-lineage-columnar-compression-research-and-adaptation-in-infromation-retrieval" class="hash-link" aria-label="Direct link to The lineage: columnar compression research and adaptation in Infromation Retrieval" title="Direct link to The lineage: columnar compression research and adaptation in Infromation Retrieval">​</a></h3>
<p>The problem of choosing the best encoding per block is not unique to search engines. Columnar data formats face the same challenge: given a block of values, what is the most compact representation that also decodes fast? The answer depends on the data type, the value range and the actual distribution of values in that specific block, which can vary dramatically across blocks in the same column.</p>
<p>Several research systems have tackled this directly. <a href="https://www.cs.cit.tum.de/fileadmin/w00cfj/dis/papers/btrblocks.pdf" target="_blank" rel="noopener noreferrer"><strong>BtrBlocks</strong> (SIGMOD 2023)</a> divides each column into fixed-size blocks and runs an encoding competition per block - FOR, RLE, frequency encoding, dictionary - picking the smallest result. Its central finding is that per-block selection consistently beats per-column selection because local distributions vary dramatically even within a single column. <a href="https://ir.cwi.nl/pub/35881/35881.pdf" target="_blank" rel="noopener noreferrer"><strong>FastLanes</strong> (VLDB 2023)</a> takes a complementary angle: redesigning the bit-unpacking layout to target a virtual 1024-bit register so the same code auto-vectorizes across SSE4.2, AVX2, ARM NEON and SVE without platform-specific intrinsics. [<strong>DuckDB</strong>]*<a href="https://duckdb.org/2022/10/28/lightweight-compression" target="_blank" rel="noopener noreferrer">https://duckdb.org/2022/10/28/lightweight-compression</a>) applies adaptive compression in its storage layer, selecting from a menu of encodings per data segment based on measured compression ratio and decode cost, making the same per-block competition a production reality in a widely-used analytical engine.</p>
<p>Posting lists are a special case of this general problem. The values are always 32-bit unsigned integers, always monotonically increasing and always delta-encoded before compression. This constraint narrows the encoding menu considerably compared to general columnar data, but it also enables encodings that would not make sense otherwise. A bitset over a contiguous range of document IDs is only meaningful for sorted unique integers. An all-same encoding for uniform gaps is only meaningful after delta coding. The IR-specific structure of the data is what makes these encodings possible and what makes the competition tractable.</p>
<p>With that in mind, we designed a new adaptive posting list format that follows the same per-block competition principle, but with an encoding menu tuned specifically for sorted 32-bit integer sequences in an inverted index.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="iresearch-adaptive-postings-format">IResearch adaptive postings format<a href="https://serenedb.com/blog/search-optimization-5#iresearch-adaptive-postings-format" class="hash-link" aria-label="Direct link to IResearch adaptive postings format" title="Direct link to IResearch adaptive postings format">​</a></h3>
<p>Each block in the posting list is prefixed by a single selector byte. At decode time a <code>switch</code> on that byte picks the path without a special-case for tail block. The physical layout is a sequence of self-describing blocks, each compressed with whatever encoding was smallest at write time.</p>
<p><img decoding="async" loading="lazy" alt="Fig. 1 - A posting list is a sequence of self-describing blocks. Encoding is chosen per-block; the tail uses the same competition as full blocks." src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTEwMCIgaGVpZ2h0PSI0NjciIHZpZXdCb3g9IjAgMCAxMTAwIDQ2NyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHRpdGxlPlBvc3RpbmcgbGlzdCBwaHlzaWNhbCBsYXlvdXQgb24gZGlzazwvdGl0bGU+CjxkZXNjPkEgcG9zdGluZyBsaXN0IGFzIGEgc2VxdWVuY2Ugb2Ygc2VsZi1kZXNjcmliaW5nIGJsb2Nrcy4gRWFjaCBibG9jayBoYXMgYSAxLWJ5dGUgZW5jb2Rpbmcgc2VsZWN0b3IgZm9sbG93ZWQgYnkgYSB2YXJpYWJsZS1sZW5ndGggcGF5bG9hZC4gRGlmZmVyZW50IGJsb2NrcyB1c2UgZGlmZmVyZW50IGVuY29kaW5ncy4gVGhlIHRhaWwgdXNlcyB0aGUgc2FtZSBmb3JtYXQuPC9kZXNjPgo8c3R5bGU+PCFbQ0RBVEFbCi5sYWJlbCB7IGZvbnQ6IDQwMCAxNHB4IC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgIlNlZ29lIFVJIiwgc2Fucy1zZXJpZjsgZmlsbDogIzExMTIxRDsgfQoubXV0ZWQgeyBmb250OiA0MDAgMTNweCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICJTZWdvZSBVSSIsIHNhbnMtc2VyaWY7IGZpbGw6ICM1MDYxODI7IH0KLnNtYWxsIHsgZm9udDogNDAwIDEycHggLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAiU2Vnb2UgVUkiLCBzYW5zLXNlcmlmOyBmaWxsOiAjNTA2MTgyOyB9Ci50aW55IHsgZm9udDogNDAwIDExcHggLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAiU2Vnb2UgVUkiLCBzYW5zLXNlcmlmOyBmaWxsOiAjNTA2MTgyOyB9Ci5oZWFkIHsgZm9udDogNjAwIDE0cHggLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAiU2Vnb2UgVUkiLCBzYW5zLXNlcmlmOyBmaWxsOiAjNEE3QUFEOyB9Ci5hY2NlbnQgeyBmaWxsOiAjODk1QUY4OyB9Ci5hY2NlbnRUZXh0IHsgZmlsbDogIzg5NUFGODsgfQouc29mdFRleHQgeyBmaWxsOiAjNTA2MTgyOyBvcGFjaXR5OiAwLjc7IH0KLmJveCB7IGZpbGw6IHdoaXRlOyBzdHJva2U6ICNFN0U3RTc7IHN0cm9rZS13aWR0aDogMC41OyB9Ci5wYXlsb2FkIHsgZmlsbDogd2hpdGU7IHN0cm9rZTogI0U3RTdFNzsgc3Ryb2tlLXdpZHRoOiAwLjU7IH0KLnNlbGVjdG9yIHsgZmlsbDogIzg5NUFGODsgZmlsbC1vcGFjaXR5OiAwLjE7IHN0cm9rZTogIzg5NUFGODsgc3Ryb2tlLW9wYWNpdHk6IDAuMzsgc3Ryb2tlLXdpZHRoOiAwLjU7IH0KLnRhaWwgeyBmaWxsOiAjODk1QUY4OyBmaWxsLW9wYWNpdHk6IDAuMTsgc3Ryb2tlOiAjODk1QUY4OyBzdHJva2Utb3BhY2l0eTogMC4zOyBzdHJva2Utd2lkdGg6IDAuNTsgfQouZmFkZUJveCB7IGZpbGw6ICNGMEYyRjU7IHN0cm9rZTogI0U3RTdFNzsgc3Ryb2tlLXdpZHRoOiAwLjU7IG9wYWNpdHk6IDAuNjU7IH0KLmd1aWRlIHsgc3Ryb2tlOiAjREREREREOyBzdHJva2Utd2lkdGg6IDAuNzsgfQoudGhpbiB7IHN0cm9rZTogI0U3RTdFNzsgc3Ryb2tlLXdpZHRoOiAwLjU7IH0KLmFjY2VudExpbmUgeyBzdHJva2U6ICM4OTVBRjg7IHN0cm9rZS1vcGFjaXR5OiAwLjQ1OyBzdHJva2Utd2lkdGg6IDAuNzsgfQpdXT48L3N0eWxlPgoKPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzExNjJfNjAyKSI+CjxwYXRoIGQ9Ik0xMDk5LjA0IDAuMzIwMzEySDAuMzIwMzEyVjQ2Ni4wNDFIMTA5OS4wNFYwLjMyMDMxMloiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xMDk5LjA0IDAuMzIwMzEySDAuMzIwMzEyVjMwOS45NDhIMTA5OS4wNFYwLjMyMDMxMloiIGZpbGw9IiNGNUY1RjUiIHN0cm9rZT0iI0U3RTdFNyIgc3Ryb2tlLXdpZHRoPSIwLjUiLz4KCjx0ZXh0IGNsYXNzPSJsYWJlbCIgeD0iNDYuMzEyNSIgeT0iNTYiIG9wYWNpdHk9IjAuNSI+cG9zdGluZyBsaXN0PC90ZXh0Pgo8dGV4dCBjbGFzcz0ibGFiZWwiIHg9IjE0NyIgeT0iNTYiPi0mZ3Q7IGFkYXB0aXZlIHNlbGYtZGVzY3JpYmluZyBibG9ja3MuPC90ZXh0Pgo8dGV4dCBjbGFzcz0ic21hbGwgc29mdFRleHQiIHg9IjEwNTMiIHk9IjU2IiB0ZXh0LWFuY2hvcj0iZW5kIj50ZXJtICJ0aGUiIC0gNC41TSBkb2NzLCAzNSwwMDArIGJsb2NrczwvdGV4dD4KPHBhdGggZD0iTTQ2LjMxMjUgNzguMzY2MkgxMDUzLjA1IiBzdHJva2U9IiNEREREREQiIHN0cm9rZS13aWR0aD0iMC43IiBzdHJva2UtZGFzaGFycmF5PSI0IDMiLz4KCjx0ZXh0IGNsYXNzPSJzbWFsbCIgeD0iMTg0IiB5PSIxMDEiIHRleHQtYW5jaG9yPSJtaWRkbGUiPmZ1bGwgYmxvY2sgMDwvdGV4dD4KPHRleHQgY2xhc3M9InNtYWxsIiB4PSIzOTMiIHk9IjEwMSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+ZnVsbCBibG9jayAxPC90ZXh0Pgo8dGV4dCBjbGFzcz0ic21hbGwiIHg9IjYwMiIgeT0iMTAxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5mdWxsIGJsb2NrIDI8L3RleHQ+Cjx0ZXh0IGNsYXNzPSJzbWFsbCBhY2NlbnRUZXh0IiB4PSI5MTAiIHk9IjEwMSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+dGFpbCBibG9jazwvdGV4dD4KCjxwYXRoIGQ9Ik02MSAxMTQuMTkxSDMwNyIgY2xhc3M9InRoaW4iLz4KPHBhdGggZD0iTTYxIDExNC4xOTFWMTIxLjg2OCIgY2xhc3M9InRoaW4iLz4KPHBhdGggZD0iTTMwNyAxMTQuMTkxVjEyMS44NjgiIGNsYXNzPSJ0aGluIi8+CjxwYXRoIGQ9Ik0zMzQgMTE0LjE5MUg0NTIiIGNsYXNzPSJ0aGluIi8+CjxwYXRoIGQ9Ik0zMzQgMTE0LjE5MVYxMjEuODY4IiBjbGFzcz0idGhpbiIvPgo8cGF0aCBkPSJNNDUyIDExNC4xOTFWMTIxLjg2OCIgY2xhc3M9InRoaW4iLz4KPHBhdGggZD0iTTQ3OSAxMTQuMTkxSDcyNSIgY2xhc3M9InRoaW4iLz4KPHBhdGggZD0iTTQ3OSAxMTQuMTkxVjEyMS44NjgiIGNsYXNzPSJ0aGluIi8+CjxwYXRoIGQ9Ik03MjUgMTE0LjE5MVYxMjEuODY4IiBjbGFzcz0idGhpbiIvPgo8cGF0aCBkPSJNNzc1IDExNC4xOTFIMTA0MSIgY2xhc3M9ImFjY2VudExpbmUiLz4KPHBhdGggZD0iTTc3NSAxMTQuMTkxVjEyMS44NjgiIGNsYXNzPSJhY2NlbnRMaW5lIi8+CjxwYXRoIGQ9Ik0xMDQxIDExNC4xOTFWMTIxLjg2OCIgY2xhc3M9ImFjY2VudExpbmUiLz4KCjxnPgo8cmVjdCBjbGFzcz0ic2VsZWN0b3IiIHg9IjYxIiB5PSIxMjEuODY4IiB3aWR0aD0iMzQiIGhlaWdodD0iOTcuMjM5IiByeD0iMCIvPgo8cmVjdCBjbGFzcz0icGF5bG9hZCIgeD0iOTUiIHk9IjEyMS44NjgiIHdpZHRoPSIyMTIiIGhlaWdodD0iOTcuMjM5IiByeD0iMCIvPgo8dGV4dCBjbGFzcz0iaGVhZCIgeD0iMjAxIiB5PSIxNDYiIHRleHQtYW5jaG9yPSJtaWRkbGUiPmJpdHBhY2s8L3RleHQ+Cjx0ZXh0IGNsYXNzPSJtdXRlZCIgeD0iMjAxIiB5PSIxNzIiIHRleHQtYW5jaG9yPSJtaWRkbGUiPnBheWxvYWQ6IDE2IGJ5dGVzPC90ZXh0Pgo8dGV4dCBjbGFzcz0ic21hbGwgc29mdFRleHQiIHg9IjIwMSIgeT0iMTkzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5zZWxlY3RvciBieXRlIHBpY2tzIGRlY29kZXI8L3RleHQ+Cjx0ZXh0IGNsYXNzPSJsYWJlbCBhY2NlbnRUZXh0IiB4PSI3OCIgeT0iMTc0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5FPC90ZXh0Pgo8L2c+Cgo8Zz4KPHJlY3QgY2xhc3M9InNlbGVjdG9yIiB4PSIzMzQiIHk9IjEyMS44NjgiIHdpZHRoPSIzNCIgaGVpZ2h0PSI5Ny4yMzkiIHJ4PSIwIi8+CjxyZWN0IGNsYXNzPSJwYXlsb2FkIiB4PSIzNjgiIHk9IjEyMS44NjgiIHdpZHRoPSI4NCIgaGVpZ2h0PSI5Ny4yMzkiIHJ4PSIwIi8+Cjx0ZXh0IGNsYXNzPSJoZWFkIiB4PSI0MTAiIHk9IjE0NiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+Yml0c2V0PC90ZXh0Pgo8dGV4dCBjbGFzcz0ibXV0ZWQiIHg9IjQxMCIgeT0iMTcyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj4xNiBieXRlczwvdGV4dD4KPHRleHQgY2xhc3M9InNtYWxsIHNvZnRUZXh0IiB4PSI0MTAiIHk9IjE5MyIgdGV4dC1hbmNob3I9Im1pZGRsZSI+ZGVuc2UgcmFuZ2U8L3RleHQ+Cjx0ZXh0IGNsYXNzPSJsYWJlbCBhY2NlbnRUZXh0IiB4PSIzNTEiIHk9IjE3NCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+RTwvdGV4dD4KPC9nPgoKPGc+CjxyZWN0IGNsYXNzPSJzZWxlY3RvciIgeD0iNDc5IiB5PSIxMjEuODY4IiB3aWR0aD0iMzQiIGhlaWdodD0iOTcuMjM5IiByeD0iMCIvPgo8cmVjdCBjbGFzcz0icGF5bG9hZCIgeD0iNTEzIiB5PSIxMjEuODY4IiB3aWR0aD0iMjEyIiBoZWlnaHQ9Ijk3LjIzOSIgcng9IjAiLz4KPHRleHQgY2xhc3M9ImhlYWQiIHg9IjYxOSIgeT0iMTQ2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5iaXRwYWNrPC90ZXh0Pgo8dGV4dCBjbGFzcz0ibXV0ZWQiIHg9IjYxOSIgeT0iMTcyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5wYXlsb2FkOiAzMiBieXRlczwvdGV4dD4KPHRleHQgY2xhc3M9InNtYWxsIHNvZnRUZXh0IiB4PSI2MTkiIHk9IjE5MyIgdGV4dC1hbmNob3I9Im1pZGRsZSI+d2lkZXIgbG9jYWwgZ2FwczwvdGV4dD4KPHRleHQgY2xhc3M9ImxhYmVsIGFjY2VudFRleHQiIHg9IjQ5NiIgeT0iMTc0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5FPC90ZXh0Pgo8L2c+Cgo8dGV4dCBjbGFzcz0ibGFiZWwgc29mdFRleHQiIHg9Ijc1MCIgeT0iMTczIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj4uLi48L3RleHQ+Cgo8Zz4KPHJlY3QgY2xhc3M9InRhaWwiIHg9Ijc3NSIgeT0iMTIxLjg2OCIgd2lkdGg9IjQ2IiBoZWlnaHQ9Ijk3LjIzOSIgcng9IjAiLz4KPHJlY3QgY2xhc3M9InRhaWwiIHg9IjgyMSIgeT0iMTIxLjg2OCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSI5Ny4yMzkiIHJ4PSIwIi8+Cjx0ZXh0IGNsYXNzPSJsYWJlbCBhY2NlbnRUZXh0IiB4PSI5MDYiIHk9IjE0NiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+c3RyZWFtdmJ5dGU8L3RleHQ+Cjx0ZXh0IGNsYXNzPSJtdXRlZCBhY2NlbnRUZXh0IiB4PSI5MDYiIHk9IjE3MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgb3BhY2l0eT0iMC43NSI+dGFpbCAtIDQ3IGRvY3M8L3RleHQ+Cjx0ZXh0IGNsYXNzPSJzbWFsbCBhY2NlbnRUZXh0IiB4PSI5MDYiIHk9IjE5MyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgb3BhY2l0eT0iMC42NSI+c2FtZSAxLWJ5dGUgcHJlZml4PC90ZXh0Pgo8dGV4dCBjbGFzcz0ibGFiZWwgYWNjZW50VGV4dCIgeD0iNzk4IiB5PSIxNzQiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkU8L3RleHQ+CjwvZz4KCjxyZWN0IGNsYXNzPSJmYWRlQm94IiB4PSIxMDA2IiB5PSIxMjEuODY4IiB3aWR0aD0iMzUiIGhlaWdodD0iOTcuMjM5IiByeD0iMCIvPgo8dGV4dCBjbGFzcz0ic21hbGwgc29mdFRleHQiIHg9IjEwMjMuNSIgeT0iMTczIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBvcGFjaXR5PSIwLjQ1Ij5mcmVxczwvdGV4dD4KCjxwYXRoIGQ9Ik03OCAyMTkuMTA3VjIzOSIgc3Ryb2tlPSIjODk1QUY4IiBzdHJva2Utb3BhY2l0eT0iMC40NSIgc3Ryb2tlLXdpZHRoPSIwLjgiIHN0cm9rZS1kYXNoYXJyYXk9IjQgMyIvPgo8dGV4dCBjbGFzcz0idGlueSBhY2NlbnRUZXh0IiB4PSI3OCIgeT0iMjU1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5iaXRwYWNrXzAyPC90ZXh0Pgo8cGF0aCBkPSJNMzUxIDIxOS4xMDdWMjM5IiBzdHJva2U9IiM4OTVBRjgiIHN0cm9rZS1vcGFjaXR5PSIwLjQ1IiBzdHJva2Utd2lkdGg9IjAuOCIgc3Ryb2tlLWRhc2hhcnJheT0iNCAzIi8+Cjx0ZXh0IGNsYXNzPSJ0aW55IGFjY2VudFRleHQiIHg9IjM1MSIgeT0iMjU1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5iaXRzZXQ8L3RleHQ+CjxwYXRoIGQ9Ik00OTYgMjE5LjEwN1YyMzkiIHN0cm9rZT0iIzg5NUFGOCIgc3Ryb2tlLW9wYWNpdHk9IjAuNDUiIHN0cm9rZS13aWR0aD0iMC44IiBzdHJva2UtZGFzaGFycmF5PSI0IDMiLz4KPHRleHQgY2xhc3M9InRpbnkgYWNjZW50VGV4dCIgeD0iNDk2IiB5PSIyNTUiIHRleHQtYW5jaG9yPSJtaWRkbGUiPmJpdHBhY2tfMDQ8L3RleHQ+CjxwYXRoIGQ9Ik03OTggMjE5LjEwN1YyMzkiIHN0cm9rZT0iIzg5NUFGOCIgc3Ryb2tlLW9wYWNpdHk9IjAuNDUiIHN0cm9rZS13aWR0aD0iMC44IiBzdHJva2UtZGFzaGFycmF5PSI0IDMiLz4KPHRleHQgY2xhc3M9InRpbnkgYWNjZW50VGV4dCIgeD0iNzk4IiB5PSIyNTUiIHRleHQtYW5jaG9yPSJtaWRkbGUiPnN0cmVhbXZieXRlPC90ZXh0PgoKPHJlY3QgY2xhc3M9ImJveCIgeD0iNDYuMzEyNSIgeT0iMjc4IiB3aWR0aD0iOTk1IiBoZWlnaHQ9IjI4IiByeD0iMCIvPgo8cmVjdCBjbGFzcz0ic2VsZWN0b3IiIHg9IjQ2LjMxMjUiIHk9IjI3OCIgd2lkdGg9IjExMiIgaGVpZ2h0PSIyOCIgcng9IjAiLz4KPHRleHQgY2xhc3M9InNtYWxsIGFjY2VudFRleHQiIHg9IjEwMi4zMTI1IiB5PSIyOTYiIHRleHQtYW5jaG9yPSJtaWRkbGUiPnNlbGVjdG9yIGJ5dGU8L3RleHQ+Cjx0ZXh0IGNsYXNzPSJzbWFsbCIgeD0iMTc0IiB5PSIyOTYiPnBheWxvYWQgZm9sbG93cyBpbW1lZGlhdGVseSAtIG5vIGdsb2JhbCBjb2RlYywgbm8gc2VwYXJhdGUgdGFpbCBsYXlvdXQ8L3RleHQ+Cgo8cGF0aCBkPSJNMTA5OS4wNCAzMDkuOTQ3SDAuMzIwMzEyVjQ2Ni4wNEgxMDk5LjA0VjMwOS45NDdaIiBmaWxsPSJ3aGl0ZSIvPgo8dGV4dCBjbGFzcz0iaGVhZCIgeD0iNDYuMzEyNSIgeT0iMzQ0Ij5XaGF0IGNoYW5nZXMgb24gZGlzazwvdGV4dD4KCjx0ZXh0IGNsYXNzPSJtdXRlZCIgeD0iNDYuMzEyNSIgeT0iMzc2Ij4KPHRzcGFuIGNsYXNzPSJhY2NlbnRUZXh0Ij4xLjwvdHNwYW4+Cjx0c3Bhbj4gRXZlcnkgYmxvY2sgY2FycmllcyB0aGUgYnl0ZSB0aGF0IHNlbGVjdHMgaXRzIGRlY29kZXIuPC90c3Bhbj4KPC90ZXh0Pgo8dGV4dCBjbGFzcz0ibXV0ZWQiIHg9IjQ2LjMxMjUiIHk9IjQwMiI+Cjx0c3BhbiBjbGFzcz0iYWNjZW50VGV4dCI+Mi48L3RzcGFuPgo8dHNwYW4+IEVuY29kaW5nIHZhcmllcyBibG9jay1ieS1ibG9jazogYml0cGFja2luZywgYml0c2V0LCBjb25zdGFudCBkZWx0YSwgc3RyZWFtdmJ5dGUsIG9yIHJhdy48L3RzcGFuPgo8L3RleHQ+Cjx0ZXh0IGNsYXNzPSJtdXRlZCIgeD0iNDYuMzEyNSIgeT0iNDI4Ij4KPHRzcGFuIGNsYXNzPSJhY2NlbnRUZXh0Ij4zLjwvdHNwYW4+Cjx0c3Bhbj4gVGhlIHRhaWwgaXMgbm90IHNwZWNpYWwgYW55bW9yZTogaXQgdXNlcyB0aGUgc2FtZSBzZWxlY3RvciArIHBheWxvYWQgc2hhcGUuPC90c3Bhbj4KPC90ZXh0PgoKPHJlY3QgY2xhc3M9InRhaWwiIHg9IjczNC45MyIgeT0iMzM1IiB3aWR0aD0iMzA2LjEyIiBoZWlnaHQ9IjkzIiByeD0iMCIvPgo8dGV4dCBjbGFzcz0ibGFiZWwgYWNjZW50VGV4dCIgeD0iNzUyIiB5PSIzNjUiPkRlY29kZXIgcGF0aCBpcyBsb2NhbDwvdGV4dD4KPHRleHQgY2xhc3M9InNtYWxsIGFjY2VudFRleHQiIHg9Ijc1MiIgeT0iMzkyIiBvcGFjaXR5PSIwLjc1Ij5yZWFkIEUgLSZndDsgc3dpdGNoKEUpIC0mZ3Q7IGRlY29kZSBwYXlsb2FkPC90ZXh0Pgo8dGV4dCBjbGFzcz0ic21hbGwgYWNjZW50VGV4dCIgeD0iNzUyIiB5PSI0MTQiIG9wYWNpdHk9IjAuNzUiPm5ldyBlbmNvZGluZ3MgYmVjb21lIG5ldyBzZWxlY3RvciB2YWx1ZXM8L3RleHQ+Cgo8cGF0aCBkPSJNMC4zMjAzMTIgNDY1LjcyMUgxMDk5LjA0IiBzdHJva2U9IiNFN0U3RTciIHN0cm9rZS13aWR0aD0iMC41Ii8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfMTE2Ml82MDIiPgo8cmVjdCB3aWR0aD0iMTEwMCIgaGVpZ2h0PSI0NjciIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==" width="1100" height="467" class="img_ev3q"></p>
<p style="font-size:12px;opacity:0.7"><em>Fig. 1 - A posting list is a sequence of self-describing blocks. Encoding is chosen per-block; the tail uses the same competition as full blocks.</em></p>
<p>The format uses two separate encoding families, because doc IDs and payload values have different structure. Doc IDs are sorted unique integers, so delta coding is always applied first - only the gaps between consecutive IDs are stored. On the other hand, frequencies are unsigned integers with no monotonicity guarantee, so delta coding is sub-optimal.</p>
<p><strong>Doc ID encodings</strong> (after delta coding):</p>
<table><thead><tr><th>Encoding</th><th>Description</th><th>Block size</th><th>When it wins</th></tr></thead><tbody><tr><td>SIMD bitpacking</td><td>All gaps packed at <code>N</code> bits each using Lemire's <code>simdpackwithoutmaskd1</code>. <code>N = bit_width(max_delta)</code>.</td><td><code>N × 16 bytes</code></td><td>Most natural language text - gaps small and fairly uniform</td></tr><tr><td>FOR bitset</td><td>Frame of Reference bitmask: a bit per position in <code>[prev, prev+range]</code>. Set bit = doc present.</td><td><code>ceil(range / 64) × 8 bytes</code></td><td>Dense blocks - many docs in a narrow ID range</td></tr><tr><td>Constant delta</td><td>All gaps are equal. Store the single gap value (1, 2 or 4 bytes).</td><td><code>1-4 bytes</code></td><td>Structured data with regular ID spacing</td></tr><tr><td><a href="https://github.com/lemire/streamvbyte" target="_blank" rel="noopener noreferrer">StreamVByte</a></td><td>Variable-length byte encoding. Each value takes 1-4 bytes based on magnitude.</td><td>varies</td><td>Tail blocks with highly variable gaps</td></tr><tr><td>Raw values</td><td>Uncompressed 32-bit values. Fallback when nothing else wins.</td><td><code>128 × 4 bytes</code></td><td>Pathological distributions</td></tr></tbody></table>
<p><strong>Frequency and position encodings</strong>:</p>
<table><thead><tr><th>Encoding</th><th>Description</th><th>Block size</th><th>When it wins</th></tr></thead><tbody><tr><td>SIMD bitpacking</td><td>Values packed at <code>N</code> bits using Lemire's <code>simdpackwithoutmask</code>. No delta coding.</td><td><code>N × 16 bytes</code></td><td>General case for frequencies</td></tr><tr><td>Constant value</td><td>All values in the block are equal. Store one value (1, 2 or 4 bytes).</td><td><code>1-4 bytes</code></td><td><code>tf=1</code> throughout - the majority of frequency blocks</td></tr><tr><td>StreamVByte</td><td>Variable-length byte encoding. No delta coding.</td><td>varies</td><td>Tail blocks with variable values</td></tr><tr><td>Raw values</td><td>Uncompressed 32-bit values.</td><td><code>128 × 4 bytes</code></td><td>Fallback</td></tr></tbody></table>
<p>Encoding selection runs in a single pass over the block. Three quantities are tracked simultaneously: the maximum delta value (which determines the bitpack width), a flag for whether all deltas are equal and a running StreamVByte size estimate. The bitset cost is computed analytically from <code>max_doc - prev</code>. At the end of the pass, the byte cost of each candidate is known and the winner is written as a single byte prefix followed by its payload.</p>
<p><img decoding="async" loading="lazy" alt="Fig. 2 - Encoding selection runs in a single pass. Four candidates are costed simultaneously; the smallest wins." src="data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNjgwIDM0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgZGF0YS1maWd1cmUtYmFja2dyb3VuZD0id2hpdGUiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9IiNmZmYiLz4KPHN0eWxlPjwhW0NEQVRBWwoudCB7IGZvbnQ6IDQwMCAxNHB4IC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgJ1NlZ29lIFVJJywgc2Fucy1zZXJpZjsgZmlsbDogIzFhMWExYTsgfQoudHMgeyBmb250OiA0MDAgMTJweCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICdTZWdvZSBVSScsIHNhbnMtc2VyaWY7IGZpbGw6ICM1NTU7IH0KLnRoIHsgZm9udDogNjAwIDE0cHggLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAnU2Vnb2UgVUknLCBzYW5zLXNlcmlmOyBmaWxsOiAjMWExYTFhOyB9Ci5jLWdyYXkgPiByZWN0IHsgZmlsbDogI2YxZWZlODsgc3Ryb2tlOiAjODg4NzgwOyBzdHJva2Utd2lkdGg6IDAuNTsgfQouYy1ncmF5ID4gdGV4dCB7IGZpbGw6ICM0NDQ0NDE7IH0KLmMtdGVhbCA+IHJlY3QgeyBmaWxsOiAjZTFmNWVlOyBzdHJva2U6ICMxZDllNzU7IHN0cm9rZS13aWR0aDogMC41OyB9Ci5jLXRlYWwgPiB0ZXh0IHsgZmlsbDogIzA4NTA0MTsgfQouYy1jb3JhbCA+IHJlY3QgeyBmaWxsOiAjZmFlY2U3OyBzdHJva2U6ICNkODVhMzA7IHN0cm9rZS13aWR0aDogMC41OyB9Ci5jLWNvcmFsID4gdGV4dCB7IGZpbGw6ICM3MTJiMTM7IH0KLmMtcHVycGxlID4gcmVjdCB7IGZpbGw6ICNlZWVkZmU7IHN0cm9rZTogIzdmNzdkZDsgc3Ryb2tlLXdpZHRoOiAwLjU7IH0KLmMtcHVycGxlID4gdGV4dCB7IGZpbGw6ICMzYzM0ODk7IH0KLmMtYW1iZXIgPiByZWN0IHsgZmlsbDogI2ZhZWVkYTsgc3Ryb2tlOiAjYmE3NTE3OyBzdHJva2Utd2lkdGg6IDAuNTsgfQouYy1hbWJlciA+IHRleHQgeyBmaWxsOiAjNjMzODA2OyB9CnJlY3Quc2VjdGlvbi1iZyB7IGZpbGw6ICNmNmY1ZjA7IHN0cm9rZTogI2RkZDsgc3Ryb2tlLXdpZHRoOiAwLjU7IH0KcmVjdC5pbnNpZ2h0LWJnIHsgZmlsbDogI2YwZWRlNDsgc3Ryb2tlOiAjY2NjOyBzdHJva2Utd2lkdGg6IDAuNTsgfQpdXT48L3N0eWxlPgo8dGl0bGU+RW5jb2RpbmcgY29tcGV0aXRpb246IHNpbmdsZSBwYXNzLCBwaWNrIHRoZSBzbWFsbGVzdDwvdGl0bGU+CjxkZXNjPkEgc2luZ2xlIHBhc3Mgb3ZlciAxMjggZGVsdGEgdmFsdWVzIHRyYWNrcyB0aHJlZSBxdWFudGl0aWVzIHNpbXVsdGFuZW91c2x5LiBBdCB0aGUgZW5kLCBmb3VyIGNhbmRpZGF0ZSBjb3N0cyBhcmUgY29tcGFyZWQgYW5kIHRoZSBzbWFsbGVzdCB3aW5zLjwvZGVzYz4KPGRlZnM+PG1hcmtlciBpZD0iYTIiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iOCIgcmVmWT0iNSIgbWFya2VyV2lkdGg9IjYiIG1hcmtlckhlaWdodD0iNiIgb3JpZW50PSJhdXRvLXN0YXJ0LXJldmVyc2UiPjxwYXRoIGQ9Ik0yIDFMOCA1TDIgOSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJjb250ZXh0LXN0cm9rZSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvbWFya2VyPjwvZGVmcz4KCjxnIGNsYXNzPSJjLWdyYXkiPjxyZWN0IHg9IjI0MCIgeT0iMjAiIHdpZHRoPSIyMDAiIGhlaWdodD0iNDQiIHJ4PSI2Ii8+PHRleHQgY2xhc3M9InRoIiB4PSIzNDAiIHk9IjM4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+MTI4IGRlbHRhIHZhbHVlczwvdGV4dD48dGV4dCBjbGFzcz0idHMiIHg9IjM0MCIgeT0iNTQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5vbmUgcGFzcywgZm91ciBjYW5kaWRhdGVzIGNvc3RlZDwvdGV4dD48L2c+Cgo8bGluZSB4MT0iMjkwIiB5MT0iNjQiIHgyPSIxMDAiIHkyPSIxMDAiIHN0cm9rZT0iI2RkZCIgc3Ryb2tlLXdpZHRoPSIwLjgiIG1hcmtlci1lbmQ9InVybCgjYTIpIi8+CjxsaW5lIHgxPSIzMjAiIHkxPSI2NCIgeDI9IjI2MCIgeTI9IjEwMCIgc3Ryb2tlPSIjZGRkIiBzdHJva2Utd2lkdGg9IjAuOCIgbWFya2VyLWVuZD0idXJsKCNhMikiLz4KPGxpbmUgeDE9IjM2MCIgeTE9IjY0IiB4Mj0iNDIwIiB5Mj0iMTAwIiBzdHJva2U9IiNkZGQiIHN0cm9rZS13aWR0aD0iMC44IiBtYXJrZXItZW5kPSJ1cmwoI2EyKSIvPgo8bGluZSB4MT0iMzkwIiB5MT0iNjQiIHgyPSI1ODAiIHkyPSIxMDAiIHN0cm9rZT0iI2RkZCIgc3Ryb2tlLXdpZHRoPSIwLjgiIG1hcmtlci1lbmQ9InVybCgjYTIpIi8+Cgo8ZyBjbGFzcz0iYy1wdXJwbGUiPjxyZWN0IHg9IjQ0IiB5PSIxMDAiIHdpZHRoPSIxNDgiIGhlaWdodD0iNTYiIHJ4PSI2Ii8+PHRleHQgY2xhc3M9InRoIiB4PSIxMTgiIHk9IjEyMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPmJpdHBhY2s8L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSIxMTgiIHk9IjEzOCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPk4gw5cgMTYgYnl0ZXM8L3RleHQ+PC9nPgo8ZyBjbGFzcz0iYy10ZWFsIj48cmVjdCB4PSIyMDgiIHk9IjEwMCIgd2lkdGg9IjE0OCIgaGVpZ2h0PSI1NiIgcng9IjYiLz48dGV4dCBjbGFzcz0idGgiIHg9IjI4MiIgeT0iMTIwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+YWxsX3NhbWU8L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSIyODIiIHk9IjEzOCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPjEtNCBieXRlczwvdGV4dD48L2c+CjxnIGNsYXNzPSJjLWdyYXkiPjxyZWN0IHg9IjM3MiIgeT0iMTAwIiB3aWR0aD0iMTQ4IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjx0ZXh0IGNsYXNzPSJ0aCIgeD0iNDQ2IiB5PSIxMjAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5iaXRzZXQ8L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSI0NDYiIHk9IjEzOCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPmRlbnNpdHkgY2hlY2s8L3RleHQ+PC9nPgo8ZyBjbGFzcz0iYy1hbWJlciI+PHJlY3QgeD0iNTM2IiB5PSIxMDAiIHdpZHRoPSIxMDAiIGhlaWdodD0iNTYiIHJ4PSI2Ii8+PHRleHQgY2xhc3M9InRoIiB4PSI1ODYiIHk9IjEyMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPnN2Ynl0ZTwvdGV4dD48dGV4dCBjbGFzcz0idHMiIHg9IjU4NiIgeT0iMTM4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+dGFpbCBvbmx5PC90ZXh0PjwvZz4KCjxsaW5lIHgxPSIxMTgiIHkxPSIxNTYiIHgyPSIyOTAiIHkyPSIyMDAiIHN0cm9rZT0iI2RkZCIgc3Ryb2tlLXdpZHRoPSIwLjgiIG1hcmtlci1lbmQ9InVybCgjYTIpIi8+CjxsaW5lIHgxPSIyODIiIHkxPSIxNTYiIHgyPSIzMTYiIHkyPSIyMDAiIHN0cm9rZT0iI2RkZCIgc3Ryb2tlLXdpZHRoPSIwLjgiIG1hcmtlci1lbmQ9InVybCgjYTIpIi8+CjxsaW5lIHgxPSI0NDYiIHkxPSIxNTYiIHgyPSIzNjQiIHkyPSIyMDAiIHN0cm9rZT0iI2RkZCIgc3Ryb2tlLXdpZHRoPSIwLjgiIG1hcmtlci1lbmQ9InVybCgjYTIpIi8+CjxsaW5lIHgxPSI1ODYiIHkxPSIxNTYiIHgyPSIzOTAiIHkyPSIyMDAiIHN0cm9rZT0iI2RkZCIgc3Ryb2tlLXdpZHRoPSIwLjgiIG1hcmtlci1lbmQ9InVybCgjYTIpIi8+Cgo8ZyBjbGFzcz0iYy1ncmF5Ij48cmVjdCB4PSIyMDAiIHk9IjIwMCIgd2lkdGg9IjI4MCIgaGVpZ2h0PSI0NCIgcng9IjgiLz48dGV4dCBjbGFzcz0idGgiIHg9IjM0MCIgeT0iMjE4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+cGljayBzbWFsbGVzdCBieXRlIGNvc3Q8L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSIzNDAiIHk9IjIzNCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPndyaXRlIDEtYnl0ZSBzZWxlY3RvciArIHBheWxvYWQ8L3RleHQ+PC9nPgoKPHJlY3QgeD0iNDQiIHk9IjI2MiIgd2lkdGg9IjU5MiIgaGVpZ2h0PSI1OCIgcng9IjYiIGNsYXNzPSJzZWN0aW9uLWJnIi8+Cjx0ZXh0IGNsYXNzPSJ0cyIgeD0iMzQwIiB5PSIyODIiIHRleHQtYW5jaG9yPSJtaWRkbGUiPmJpdHNldCBjb3N0ID0gMSArIGNlaWwoKG1heF9kb2MgLSBwcmV2ICsgMSkgLyA2NCkgw5cgOCBieXRlczwvdGV4dD4KPHRleHQgY2xhc3M9InRzIiB4PSIzNDAiIHk9IjMwMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgb3BhY2l0eT0iMC41NSI+c3ZieXRlIGF2YWlsYWJsZSBmb3IgdGFpbHMgb25seSAtIGJpdHBhY2sgZXhjbHVkZWQgZm9yIHRhaWxzIChuZWVkcyBleGFjdGx5IDEyOCBlbGVtZW50cyk8L3RleHQ+Cjwvc3ZnPgo=" width="680" height="340" class="img_ev3q"></p>
<p style="font-size:12px;opacity:0.7"><em>Fig. 2 - Encoding selection runs in a single pass. Four candidates are costed simultaneously; the smallest wins.</em></p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="tail-unification">Tail unification<a href="https://serenedb.com/blog/search-optimization-5#tail-unification" class="hash-link" aria-label="Direct link to Tail unification" title="Direct link to Tail unification">​</a></h4>
<p>Both Lucene and Tantivy encode the tail block, the last partial block with fewer than 128 documents, using VInt: each doc ID is stored as a variable-length integer individually, with no block-level compression. This is a separate code path from the main blocks and it means the tail gets none of the compression options available to full blocks.</p>
<p>The current format eliminates the distinction. The tail runs through the same encoding competition as full blocks. Bitpacking is excluded for tails since it requires exactly 128 elements to operate, but all other encodings are available - constant delta, bitset, StreamVByte and raw values. StreamVByte is available <strong>only</strong> for tails: full blocks must produce a fixed-stride layout that the downstream SIMD scoring pipeline relies on, while tails have no such constraint.</p>
<table><thead><tr><th>Term</th><th>Tail docs</th><th>VInt tail</th><th>New format</th></tr></thead><tbody><tr><td>Common term, dense tail</td><td>96, range of 128</td><td><code>~192 bytes</code> (2 bytes/gap avg)</td><td><code>16 bytes</code> (bitset)</td></tr><tr><td>Mid-frequency term</td><td>64, 12-bit gaps</td><td><code>~192 bytes</code> (3 bytes/gap avg)</td><td><code>96 bytes</code> (bitpacking)</td></tr><tr><td>Structured data, uniform gaps</td><td>80, equal gaps</td><td><code>~160 bytes</code> (2 bytes/gap avg)</td><td><code>2 bytes</code> (constant delta)</td></tr></tbody></table>
<p>This matters most for rare terms - those with fewer than 128 documents - which have exactly one tail block and no full blocks at all. They outnumber common terms by orders of magnitude in any natural language vocabulary.</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="extensibility">Extensibility<a href="https://serenedb.com/blog/search-optimization-5#extensibility" class="hash-link" aria-label="Direct link to Extensibility" title="Direct link to Extensibility">​</a></h4>
<p>Because the selector byte fully describes each block's encoding, adding a new encoding to the format requires touching only two places: a new entry in the encoder and a corresponding entry in the decoder. The per-block competition picks it up automatically - if the new encoding produces a smaller result than existing options on a given block, it wins; otherwise it loses silently and nothing else changes.</p>
<p>For example, a Frame of Reference combined with StreamVByte is already planned as a future encoding. When it lands, it becomes a live competitor across the entire corpus with minimal code changes. Whether it wins often enough to justify the dependency is a question the competition answers empirically, not by intuition.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<p>The same property absorbs larger structural changes. Experimenting with a 256-element block size, which would enable wider SIMD bitpacking and lower per-block header overhead, would change the block size constant and the bitpacking entries, while the competition logic, tail handling and selector byte format all carry over unchanged.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="when-each-encoding-wins">When each encoding wins<a href="https://serenedb.com/blog/search-optimization-5#when-each-encoding-wins" class="hash-link" aria-label="Direct link to When each encoding wins" title="Direct link to When each encoding wins">​</a></h3>
<table><thead><tr><th>Encoding</th><th>Wins when</th><th>Typical scenario</th><th>Size for 128 docs</th></tr></thead><tbody><tr><td>Constant delta</td><td>All inter-doc gaps identical</td><td>Structured data with regular ID spacing</td><td><code>1-4 bytes</code></td></tr><tr><td>Bitset</td><td>High density, narrow ID range</td><td>Common terms in fresh segments</td><td><code>16-64 bytes</code></td></tr><tr><td>Bitpacking</td><td>Gaps small and fairly uniform</td><td>Most natural language text</td><td><code>N × 16 bytes</code></td></tr><tr><td>StreamVByte</td><td>Gaps variable in magnitude</td><td>Rare terms, short tail blocks</td><td>varies</td></tr><tr><td>Constant value (freqs)</td><td>All frequency values equal</td><td><code>tf=1</code> for all docs in block</td><td><code>2 bytes</code></td></tr></tbody></table>
<p><strong>Constant delta</strong> is the most extreme compression: when every gap between consecutive doc IDs is identical, the entire block, regardless of how many documents it contains, compresses to a single value. That's 1-4 bytes for 128 documents. It sounds like a special case, but it occurs naturally in structured data where IDs are assigned at regular intervals: partitioned datasets, range filters over numeric fields, evenly-distributed sharded collections.</p>
<p><strong>Bitset</strong> wins when many doc IDs fall in a narrow range. Instead of storing gaps, a single bit is set for each ID present in the range. For 120 documents spanning IDs 1000-1127, the bitset is 17 bytes. The same block with 1-bit gap bitpacking is 16 bytes - essentially the same. But once the range widens, bitpacking pulls ahead; the competition always picks correctly without manual tuning.</p>
<p><strong>Bitpacking</strong> is the workhorse for most natural language text. Gaps between occurrences of a mid-frequency term typically fit in 10-14 bits. A block at 12 bits per gap costs 192 bytes, vs 512 bytes uncompressed - a 2.7× reduction. The bit width is chosen per block based on the largest gap present, so a block with small uniform gaps compresses tightly even if a neighbouring block needs more bits.</p>
<p><strong>Constant value for frequencies</strong> is worth calling out specifically. Most terms appear exactly once per document - <code>tf=1</code> throughout the corpus. For those terms, an entire block of 128 frequencies compresses to 2 bytes: 1 selector + 1 value. This fell out naturally from the all-same check rather than requiring dedicated engineering and it applies to the large majority of all frequency blocks in a real index.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-bitset-encoding-in-practice">The bitset encoding in practice<a href="https://serenedb.com/blog/search-optimization-5#the-bitset-encoding-in-practice" class="hash-link" aria-label="Direct link to The bitset encoding in practice" title="Direct link to The bitset encoding in practice">​</a></h3>
<p>The bitset encoding stores a bitmask of which doc IDs are present within a range. Beyond its compression benefits, it is naturally fast for seek operations: checking whether a specific doc ID is present is a single bit test, with no decoding required. This makes it particularly effective for counting queries, where the engine traverses the entire posting list checking membership - bitset seeks are as cheap as any format can offer and the compact representation reduces I/O.</p>
<p>In many other cases, however, the engine needs to <em>materialize</em> doc IDs into a flat array - for example, when gathering norms from columnar storage during scoring or when returning all matched doc IDs directly to the caller. In these cases, materialization becomes the bottleneck. A bitset that is cheap to seek can still be expensive to fully decode if the conversion is done naively.</p>
<p>Even <a href="https://lemire.me/blog/2018/02/21/iterating-over-set-bits-quickly/" target="_blank" rel="noopener noreferrer">Lemire's optimized bit iteration</a> - repeatedly extracting the lowest set bit with <code>std::countr_zero</code>, recording the position and clearing the bit - runs at around 45 ns per 128-element block. By comparison, his SIMD bitpack decoder (<code>simdunpackd1</code>) decodes a bitpacked block in <code>~15 ns</code>, about 3× faster. Using bit iteration for materialization would make the bitset encoding a net loss for any query that requires a flat array of doc IDs.</p>
<p>To address this, we implemented a SIMD materialization path using a 256-entry lookup table built at compile time. Each possible byte value maps to its popcount and the positions of its set bits. For each byte in the bitset, five AVX2 instructions produce up to 8 doc IDs: broadcast the base offset, load positions from the table, sign-extend, add and store. The result is <code>~15-20 ns</code> per block depending on density at parity with <code>simdunpackd1</code>. The bitset encoding is now a strict win on dense blocks: better compression and equally fast to decode.</p>
<p><img decoding="async" loading="lazy" alt="Fig. 3 - 120 doc IDs compress from 480 bytes to 17 bytes. The AVX2 lookup table matches simdunpackd1 throughput; scalar bit iteration is 3× slower and would negate the compression benefit." src="data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNjgwIDM4MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgZGF0YS1maWd1cmUtYmFja2dyb3VuZD0id2hpdGUiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9IiNmZmYiLz4KPHN0eWxlPjwhW0NEQVRBWwoudCB7IGZvbnQ6IDQwMCAxNHB4IC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgJ1NlZ29lIFVJJywgc2Fucy1zZXJpZjsgZmlsbDogIzFhMWExYTsgfQoudHMgeyBmb250OiA0MDAgMTJweCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICdTZWdvZSBVSScsIHNhbnMtc2VyaWY7IGZpbGw6ICM1NTU7IH0KLnRoIHsgZm9udDogNjAwIDE0cHggLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAnU2Vnb2UgVUknLCBzYW5zLXNlcmlmOyBmaWxsOiAjMWExYTFhOyB9Ci5jLWdyYXkgPiByZWN0IHsgZmlsbDogI2YxZWZlODsgc3Ryb2tlOiAjODg4NzgwOyBzdHJva2Utd2lkdGg6IDAuNTsgfQouYy1ncmF5ID4gdGV4dCB7IGZpbGw6ICM0NDQ0NDE7IH0KLmMtdGVhbCA+IHJlY3QgeyBmaWxsOiAjZTFmNWVlOyBzdHJva2U6ICMxZDllNzU7IHN0cm9rZS13aWR0aDogMC41OyB9Ci5jLXRlYWwgPiB0ZXh0IHsgZmlsbDogIzA4NTA0MTsgfQouYy1jb3JhbCA+IHJlY3QgeyBmaWxsOiAjZmFlY2U3OyBzdHJva2U6ICNkODVhMzA7IHN0cm9rZS13aWR0aDogMC41OyB9Ci5jLWNvcmFsID4gdGV4dCB7IGZpbGw6ICM3MTJiMTM7IH0KLmMtcHVycGxlID4gcmVjdCB7IGZpbGw6ICNlZWVkZmU7IHN0cm9rZTogIzdmNzdkZDsgc3Ryb2tlLXdpZHRoOiAwLjU7IH0KLmMtcHVycGxlID4gdGV4dCB7IGZpbGw6ICMzYzM0ODk7IH0KLmMtYW1iZXIgPiByZWN0IHsgZmlsbDogI2ZhZWVkYTsgc3Ryb2tlOiAjYmE3NTE3OyBzdHJva2Utd2lkdGg6IDAuNTsgfQouYy1hbWJlciA+IHRleHQgeyBmaWxsOiAjNjMzODA2OyB9CnJlY3Quc2VjdGlvbi1iZyB7IGZpbGw6ICNmNmY1ZjA7IHN0cm9rZTogI2RkZDsgc3Ryb2tlLXdpZHRoOiAwLjU7IH0KcmVjdC5pbnNpZ2h0LWJnIHsgZmlsbDogI2YwZWRlNDsgc3Ryb2tlOiAjY2NjOyBzdHJva2Utd2lkdGg6IDAuNTsgfQpdXT48L3N0eWxlPgo8dGl0bGU+Qml0c2V0IGVuY29kaW5nOiBkZW5zZSBibG9jayBjb21wcmVzc2VkIHRvIDE3IGJ5dGVzLCBhbmQgdGhlIG1hdGVyaWFsaXphdGlvbiBjaGFsbGVuZ2U8L3RpdGxlPgo8ZGVzYz5Ub3A6IDEyMCBkb2MgSURzIGluIGEgMTI4LUlEIHJhbmdlIHN0b3JlZCBhcyAxNyBieXRlcyBpbnN0ZWFkIG9mIDQ4MCBieXRlcy4gQm90dG9tIGxlZnQ6IHNjYWxhciBleHRyYWN0aW9uIGFyb3VuZCA0NW5zLiBCb3R0b20gcmlnaHQ6IEFWWDIgbG9va3VwIHRhYmxlIGFyb3VuZCAxNSB0byAyMG5zLCBhdCBwYXJpdHkgd2l0aCBzaW1kdW5wYWNrZDEuPC9kZXNjPgo8ZGVmcz48bWFya2VyIGlkPSJhMyIgdmlld0JveD0iMCAwIDEwIDEwIiByZWZYPSI4IiByZWZZPSI1IiBtYXJrZXJXaWR0aD0iNiIgbWFya2VySGVpZ2h0PSI2IiBvcmllbnQ9ImF1dG8tc3RhcnQtcmV2ZXJzZSI+PHBhdGggZD0iTTIgMUw4IDVMMiA5IiBmaWxsPSJub25lIiBzdHJva2U9ImNvbnRleHQtc3Ryb2tlIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9tYXJrZXI+PC9kZWZzPgoKPHRleHQgY2xhc3M9InRzIiB4PSI0NCIgeT0iMTgiIG9wYWNpdHk9IjAuNSI+cmF3OiA0ODAgYnl0ZXMgKDEyMCBkb2NzIMOXIDQgYnl0ZXMpPC90ZXh0Pgo8dGV4dCBjbGFzcz0idHMiIHg9IjQ0MCIgeT0iMTgiIG9wYWNpdHk9IjAuNSI+Yml0c2V0OiAxNyBieXRlcyAoMSBzZWxlY3RvciArIDE2IHBheWxvYWQpPC90ZXh0PgoKPGcgY2xhc3M9ImMtZ3JheSI+PHJlY3QgeD0iNDQiIHk9IjI2IiB3aWR0aD0iNDYiIGhlaWdodD0iMjgiIHJ4PSI0Ii8+PHRleHQgY2xhc3M9InRzIiB4PSI2NyIgeT0iNDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj4xMDAwPC90ZXh0PjwvZz4KPGcgY2xhc3M9ImMtZ3JheSI+PHJlY3QgeD0iOTYiIHk9IjI2IiB3aWR0aD0iNDYiIGhlaWdodD0iMjgiIHJ4PSI0Ii8+PHRleHQgY2xhc3M9InRzIiB4PSIxMTkiIHk9IjQwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+MTAwMTwvdGV4dD48L2c+CjxnIGNsYXNzPSJjLWdyYXkiPjxyZWN0IHg9IjE0OCIgeT0iMjYiIHdpZHRoPSI0NiIgaGVpZ2h0PSIyOCIgcng9IjQiLz48dGV4dCBjbGFzcz0idHMiIHg9IjE3MSIgeT0iNDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj4xMDAyPC90ZXh0PjwvZz4KPHRleHQgY2xhc3M9InRzIiB4PSIyMzAiIHk9IjQwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCIgb3BhY2l0eT0iMC40Ij7CtyDCtyDCtzwvdGV4dD4KPGcgY2xhc3M9ImMtZ3JheSI+PHJlY3QgeD0iMjcwIiB5PSIyNiIgd2lkdGg9IjQ2IiBoZWlnaHQ9IjI4IiByeD0iNCIvPjx0ZXh0IGNsYXNzPSJ0cyIgeD0iMjkzIiB5PSI0MCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPjExMTk8L3RleHQ+PC9nPgoKPHBhdGggZD0iTTM2MCA0MCBMNDIwIDQwIiBmaWxsPSJub25lIiBzdHJva2U9IiMxZDllNzUiIHN0cm9rZS13aWR0aD0iMS4yIiBtYXJrZXItZW5kPSJ1cmwoI2EzKSIvPgo8dGV4dCBjbGFzcz0idHMiIHg9IjM5MCIgeT0iMzQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMwODUwNDEiPjI4w5c8L3RleHQ+Cgo8ZyBjbGFzcz0iYy10ZWFsIj48cmVjdCB4PSI0MzAiIHk9IjI2IiB3aWR0aD0iMjIiIGhlaWdodD0iMjgiIHJ4PSIzIi8+PHRleHQgY2xhc3M9InRzIiB4PSI0NDEiIHk9IjQwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+ZmY8L3RleHQ+PC9nPgo8ZyBjbGFzcz0iYy10ZWFsIj48cmVjdCB4PSI0NTYiIHk9IjI2IiB3aWR0aD0iMjIiIGhlaWdodD0iMjgiIHJ4PSIzIi8+PHRleHQgY2xhc3M9InRzIiB4PSI0NjciIHk9IjQwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+ZmY8L3RleHQ+PC9nPgo8ZyBjbGFzcz0iYy10ZWFsIj48cmVjdCB4PSI0ODIiIHk9IjI2IiB3aWR0aD0iMjIiIGhlaWdodD0iMjgiIHJ4PSIzIi8+PHRleHQgY2xhc3M9InRzIiB4PSI0OTMiIHk9IjQwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+ZmY8L3RleHQ+PC9nPgo8ZyBjbGFzcz0iYy10ZWFsIj48cmVjdCB4PSI1MDgiIHk9IjI2IiB3aWR0aD0iMjIiIGhlaWdodD0iMjgiIHJ4PSIzIi8+PHRleHQgY2xhc3M9InRzIiB4PSI1MTkiIHk9IjQwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+ZmY8L3RleHQ+PC9nPgo8ZyBjbGFzcz0iYy10ZWFsIj48cmVjdCB4PSI1MzQiIHk9IjI2IiB3aWR0aD0iMjIiIGhlaWdodD0iMjgiIHJ4PSIzIi8+PHRleHQgY2xhc3M9InRzIiB4PSI1NDUiIHk9IjQwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+ZmY8L3RleHQ+PC9nPgo8ZyBjbGFzcz0iYy10ZWFsIj48cmVjdCB4PSI1NjAiIHk9IjI2IiB3aWR0aD0iMjIiIGhlaWdodD0iMjgiIHJ4PSIzIi8+PHRleHQgY2xhc3M9InRzIiB4PSI1NzEiIHk9IjQwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+ZmY8L3RleHQ+PC9nPgo8ZyBjbGFzcz0iYy1ncmF5Ij48cmVjdCB4PSI1ODYiIHk9IjI2IiB3aWR0aD0iMjIiIGhlaWdodD0iMjgiIHJ4PSIzIi8+PHRleHQgY2xhc3M9InRzIiB4PSI1OTciIHk9IjQwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+ZjA8L3RleHQ+PC9nPgo8dGV4dCBjbGFzcz0idHMiIHg9IjYyNCIgeT0iNDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIiBvcGFjaXR5PSIwLjQ1Ij4rOTwvdGV4dD4KCjxsaW5lIHgxPSI0NCIgeTE9IjcyIiB4Mj0iNjM2IiB5Mj0iNzIiIHN0cm9rZT0iI2RkZCIgc3Ryb2tlLXdpZHRoPSIwLjUiLz4KPHRleHQgY2xhc3M9InRzIiB4PSIzNDAiIHk9IjkwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBvcGFjaXR5PSIwLjUiPnF1ZXJpZXMgbmVlZGluZyBhIGZsYXQgdWludDMyX3QgYXJyYXkgbXVzdCBtYXRlcmlhbGl6ZSAtIGhvdyBmYXN0IG1hdHRlcnM8L3RleHQ+CjxsaW5lIHgxPSI0NCIgeTE9IjEwMCIgeDI9IjYzNiIgeTI9IjEwMCIgc3Ryb2tlPSIjZGRkIiBzdHJva2Utd2lkdGg9IjAuNSIvPgoKPHRleHQgY2xhc3M9InRoIiB4PSIxNjAiIHk9IjEyMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzk5M2MxZCI+c2NhbGFyIH40NSBuczwvdGV4dD4KPHJlY3QgeD0iNDQiIHk9IjEzMCIgd2lkdGg9IjIzMiIgaGVpZ2h0PSI3NiIgcng9IjYiIGNsYXNzPSJzZWN0aW9uLWJnIi8+Cjx0ZXh0IGNsYXNzPSJ0cyIgeD0iNTYiIHk9IjE1MCIgb3BhY2l0eT0iMC43Ij5jb3VudHJfemVybyh3b3JkKSDihpIgYml0IHBvc2l0aW9uPC90ZXh0Pgo8dGV4dCBjbGFzcz0idHMiIHg9IjU2IiB5PSIxNjgiIG9wYWNpdHk9IjAuNyI+UG9wQml0KHdvcmQpIOKGkiBjbGVhciBpdDwvdGV4dD4KPHRleHQgY2xhc3M9InRzIiB4PSI1NiIgeT0iMTg2IiBmaWxsPSIjOTkzYzFkIj5lYWNoIHN0ZXAgZGVwZW5kcyBvbiBwcmV2aW91czwvdGV4dD4KCjx0ZXh0IGNsYXNzPSJ0aCIgeD0iNTAwIiB5PSIxMjAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMwODUwNDEiPmxvb2t1cCB0YWJsZSB+MTUtMjAgbnM8L3RleHQ+CjxyZWN0IHg9IjQwNCIgeT0iMTMwIiB3aWR0aD0iMjMyIiBoZWlnaHQ9Ijc2IiByeD0iNiIgY2xhc3M9InNlY3Rpb24tYmciLz4KPHRleHQgY2xhc3M9InRzIiB4PSI0MTYiIHk9IjE1MCIgb3BhY2l0eT0iMC43Ij5lID0ga0JpdHNldEJ5dGVUYWJsZVtieXRlXTwvdGV4dD4KPHRleHQgY2xhc3M9InRzIiB4PSI0MTYiIHk9IjE2OCIgb3BhY2l0eT0iMC43Ij5icm9hZGNhc3QgKyBhZGQg4oaSIDggZG9jIElEczwvdGV4dD4KPHRleHQgY2xhc3M9InRzIiB4PSI0MTYiIHk9IjE4NiIgZmlsbD0iIzA4NTA0MSI+YWxsIGl0ZXJhdGlvbnMgaW5kZXBlbmRlbnQ8L3RleHQ+Cgo8bGluZSB4MT0iMTYwIiB5MT0iMjA2IiB4Mj0iMTYwIiB5Mj0iMjI4IiBzdHJva2U9IiNkODVhMzAiIHN0cm9rZS13aWR0aD0iMC44IiBtYXJrZXItZW5kPSJ1cmwoI2EzKSIvPgo8bGluZSB4MT0iNTIwIiB5MT0iMjA2IiB4Mj0iNTIwIiB5Mj0iMjI4IiBzdHJva2U9IiMxZDllNzUiIHN0cm9rZS13aWR0aD0iMC44IiBtYXJrZXItZW5kPSJ1cmwoI2EzKSIvPgoKPGcgY2xhc3M9ImMtY29yYWwiPjxyZWN0IHg9IjQ0IiB5PSIyMjgiIHdpZHRoPSIyMzIiIGhlaWdodD0iNDAiIHJ4PSI2Ii8+PHRleHQgY2xhc3M9InRoIiB4PSIxNjAiIHk9IjI0NCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPjPDlyBzbG93ZXIgdGhhbiBzaW1kdW5wYWNrZDE8L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSIxNjAiIHk9IjI2MCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPm1ha2VzIGJpdHNldCBhIGJhZCB0cmFkZW9mZjwvdGV4dD48L2c+CjxnIGNsYXNzPSJjLXRlYWwiPjxyZWN0IHg9IjQwNCIgeT0iMjI4IiB3aWR0aD0iMjMyIiBoZWlnaHQ9IjQwIiByeD0iNiIvPjx0ZXh0IGNsYXNzPSJ0aCIgeD0iNTIwIiB5PSIyNDQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5hdCBwYXJpdHkgd2l0aCBzaW1kdW5wYWNrZDE8L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSI1MjAiIHk9IjI2MCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPmJpdHNldCBpcyBhIHN0cmljdCB3aW48L3RleHQ+PC9nPgoKPHJlY3QgeD0iNDQiIHk9IjI4NiIgd2lkdGg9IjU5MiIgaGVpZ2h0PSI3NCIgcng9IjYiIGNsYXNzPSJpbnNpZ2h0LWJnIi8+Cjx0ZXh0IGNsYXNzPSJ0aCIgeD0iMzQwIiB5PSIzMDgiIHRleHQtYW5jaG9yPSJtaWRkbGUiPnNpbWR1bnBhY2tkMSB+MTUgbnMgwrcgc2NhbGFyIGJpdCBpdGVyYXRpb24gfjQ1IG5zIMK3IE1hdGVyaWFsaXplQml0c2V0IH4xNS0yMCBuczwvdGV4dD4KPHRleHQgY2xhc3M9InRzIiB4PSIzNDAiIHk9IjMyOCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+NCBzZXQgd29yZHMgKDUwJSBkZW5zaXR5KTogfjE1IG5zIMK3IDggc2V0IHdvcmRzIChmdWxsIGRlbnNpdHkpOiB+MjAgbnM8L3RleHQ+Cjx0ZXh0IGNsYXNzPSJ0cyIgeD0iMzQwIiB5PSIzNDgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIG9wYWNpdHk9IjAuNTUiPkZ1dHVyZTogbmF0aXZlIGJpdHNldCBpdGVyYXRpb24g4oaSIExhenlTZWVrID0gc2luZ2xlIGJpdCB0ZXN0LCBubyBtYXRlcmlhbGl6YXRpb24gb24gbWlzc2VzPC90ZXh0Pgo8L3N2Zz4K" width="680" height="380" class="img_ev3q"></p>
<p style="font-size:12px;opacity:0.7"><em>Fig. 3 - 120 doc IDs compress from 480 bytes to 17 bytes. The AVX2 lookup table matches simdunpackd1 throughput; scalar bit iteration is 3× slower and would negate the compression benefit.</em></p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="simd-materialization-implementation">SIMD materialization: implementation<a href="https://serenedb.com/blog/search-optimization-5#simd-materialization-implementation" class="hash-link" aria-label="Direct link to SIMD materialization: implementation" title="Direct link to SIMD materialization: implementation">​</a></h3>
<p>The solution is a 256-entry lookup table built entirely at compile time with zero runtime cost. Each possible byte value maps to the count of its set bits and their positions within the byte. For a bitset block, the outer loop walks 64-bit words; the inner loop processes one byte at a time. For each byte, five AVX2 instructions produce up to 8 doc IDs: broadcast the base offset for that byte's position in the range, load the precomputed bit positions from the table, sign-extend them to 32-bit integers, add the base and store 8 values to the output. The write pointer advances by the actual count, not by 8 - the store always writes a full 32 bytes and any overshoot is overwritten in the next iteration, which is safe as long as the output buffer has a small headroom past the end.</p>
<div class="language-cpp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cpp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">struct</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">alignas</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">16</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> BitsetByteEntry </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">uint8_t</span><span class="token plain"> count</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">uint8_t</span><span class="token plain"> positions</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">8</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">constexpr</span><span class="token plain"> std</span><span class="token double-colon punctuation" style="color:#393A34">::</span><span class="token plain">array</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">BitsetByteEntry</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">256</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> kBitsetByteTable </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  std</span><span class="token double-colon punctuation" style="color:#393A34">::</span><span class="token plain">array</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">BitsetByteEntry</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">256</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> t</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">uint32_t</span><span class="token plain"> b </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> b </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">256</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">++</span><span class="token plain">b</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    t</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">b</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">count </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">uint32_t</span><span class="token plain"> i </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> i </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">8</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">++</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">b </span><span class="token operator" style="color:#393A34">&amp;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&lt;&lt;</span><span class="token plain"> i</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> t</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">b</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">positions</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">t</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">b</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">count</span><span class="token operator" style="color:#393A34">++</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> i</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> t</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>For each 64-bit word in the bitset, the AVX2 path processes 8 bytes. For each byte, five instructions produce up to 8 doc IDs:</p>
<div class="language-cpp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cpp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> __m256i base_vec </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">_mm256_set1_epi32</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">prev </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> i </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">64</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> b </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">8</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic">// broadcast base</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> __m128i pos8 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">_mm_loadl_epi64</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">                              </span><span class="token comment" style="color:#999988;font-style:italic">// load 8 byte positions</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token generic-function function" style="color:#d73a49">reinterpret_cast</span><span class="token generic-function generic class-name operator" style="color:#393A34">&lt;</span><span class="token generic-function generic class-name keyword" style="color:#00009f">const</span><span class="token generic-function generic class-name"> __m128i</span><span class="token generic-function generic class-name operator" style="color:#393A34">*</span><span class="token generic-function generic class-name operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">positions</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> __m256i result </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">_mm256_add_epi32</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">                             </span><span class="token comment" style="color:#999988;font-style:italic">// base + each position</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    base_vec</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_cvtepi8_epi32</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pos8</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">_mm256_storeu_si256</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">                            </span><span class="token comment" style="color:#999988;font-style:italic">// store 8 doc IDs</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token generic-function function" style="color:#d73a49">reinterpret_cast</span><span class="token generic-function generic class-name operator" style="color:#393A34">&lt;</span><span class="token generic-function generic class-name">__m256i</span><span class="token generic-function generic class-name operator" style="color:#393A34">*</span><span class="token generic-function generic class-name operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">end</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> result</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">end </span><span class="token operator" style="color:#393A34">+=</span><span class="token plain"> e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">count</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">                                 </span><span class="token comment" style="color:#999988;font-style:italic">// advance by actual count</span><br></span></code></pre></div></div>
<p><code>_mm256_storeu_si256</code> always writes 32 bytes, but <code>end</code> only advances by <code>e.count</code>. The extra positions beyond <code>count</code> are simply overwritten in the next iteration, it's safe as long as the output buffer has 8 entries of headroom past the actual output length.</p>
<p><img decoding="async" loading="lazy" alt="Fig. 4 - Five AVX2 instructions per byte: broadcast base, load positions, sign-extend, add, store. Write pointer advances by count, not 8 - safe overshoot by design." src="data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNjgwIDM0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgZGF0YS1maWd1cmUtYmFja2dyb3VuZD0id2hpdGUiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9IiNmZmYiLz4KPHN0eWxlPjwhW0NEQVRBWwoudCB7IGZvbnQ6IDQwMCAxNHB4IC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgJ1NlZ29lIFVJJywgc2Fucy1zZXJpZjsgZmlsbDogIzFhMWExYTsgfQoudHMgeyBmb250OiA0MDAgMTJweCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICdTZWdvZSBVSScsIHNhbnMtc2VyaWY7IGZpbGw6ICM1NTU7IH0KLnRoIHsgZm9udDogNjAwIDE0cHggLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAnU2Vnb2UgVUknLCBzYW5zLXNlcmlmOyBmaWxsOiAjMWExYTFhOyB9Ci5jLWdyYXkgPiByZWN0IHsgZmlsbDogI2YxZWZlODsgc3Ryb2tlOiAjODg4NzgwOyBzdHJva2Utd2lkdGg6IDAuNTsgfQouYy1ncmF5ID4gdGV4dCB7IGZpbGw6ICM0NDQ0NDE7IH0KLmMtdGVhbCA+IHJlY3QgeyBmaWxsOiAjZTFmNWVlOyBzdHJva2U6ICMxZDllNzU7IHN0cm9rZS13aWR0aDogMC41OyB9Ci5jLXRlYWwgPiB0ZXh0IHsgZmlsbDogIzA4NTA0MTsgfQouYy1jb3JhbCA+IHJlY3QgeyBmaWxsOiAjZmFlY2U3OyBzdHJva2U6ICNkODVhMzA7IHN0cm9rZS13aWR0aDogMC41OyB9Ci5jLWNvcmFsID4gdGV4dCB7IGZpbGw6ICM3MTJiMTM7IH0KLmMtcHVycGxlID4gcmVjdCB7IGZpbGw6ICNlZWVkZmU7IHN0cm9rZTogIzdmNzdkZDsgc3Ryb2tlLXdpZHRoOiAwLjU7IH0KLmMtcHVycGxlID4gdGV4dCB7IGZpbGw6ICMzYzM0ODk7IH0KLmMtYW1iZXIgPiByZWN0IHsgZmlsbDogI2ZhZWVkYTsgc3Ryb2tlOiAjYmE3NTE3OyBzdHJva2Utd2lkdGg6IDAuNTsgfQouYy1hbWJlciA+IHRleHQgeyBmaWxsOiAjNjMzODA2OyB9CnJlY3Quc2VjdGlvbi1iZyB7IGZpbGw6ICNmNmY1ZjA7IHN0cm9rZTogI2RkZDsgc3Ryb2tlLXdpZHRoOiAwLjU7IH0KcmVjdC5pbnNpZ2h0LWJnIHsgZmlsbDogI2YwZWRlNDsgc3Ryb2tlOiAjY2NjOyBzdHJva2Utd2lkdGg6IDAuNTsgfQpdXT48L3N0eWxlPgo8dGl0bGU+QVZYMiBiaXRzZXQgbWF0ZXJpYWxpemF0aW9uOiBvbmUgYnl0ZSB0byA4IGRvYyBJRHMgaW4gNSBpbnN0cnVjdGlvbnM8L3RpdGxlPgo8ZGVzYz5Gb3IgZWFjaCBieXRlIGluIGEgNjQtYml0IHdvcmQsIHRoZSBsb29rdXAgdGFibGUgZ2l2ZXMgY291bnQgYW5kIHBvc2l0aW9ucy4gRml2ZSBBVlgyIGluc3RydWN0aW9ucyBwcm9kdWNlIHVwIHRvIDggZG9jIElEcy4gVGhlIHdyaXRlIHBvaW50ZXIgYWR2YW5jZXMgYnkgY291bnQsIG5vdCA4LjwvZGVzYz4KPGRlZnM+PG1hcmtlciBpZD0iYTQiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iOCIgcmVmWT0iNSIgbWFya2VyV2lkdGg9IjYiIG1hcmtlckhlaWdodD0iNiIgb3JpZW50PSJhdXRvLXN0YXJ0LXJldmVyc2UiPjxwYXRoIGQ9Ik0yIDFMOCA1TDIgOSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJjb250ZXh0LXN0cm9rZSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvbWFya2VyPjwvZGVmcz4KCjx0ZXh0IGNsYXNzPSJ0cyIgeD0iMzQwIiB5PSIxNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgb3BhY2l0eT0iMC40NSI+Ynl0ZSA9IDB4YjUgwrcgYml0cyAwLDIsNCw1LDcgc2V0IMK3IGJhc2UgPSAxMDAwPC90ZXh0PgoKPGcgY2xhc3M9ImMtZ3JheSI+PHJlY3QgeD0iNDQiIHk9IjI2IiB3aWR0aD0iMTY4IiBoZWlnaHQ9IjQwIiByeD0iNiIvPjx0ZXh0IGNsYXNzPSJ0aCIgeD0iMTI4IiB5PSI0MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPmJ5dGU6IDB4YjU8L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSIxMjgiIHk9IjU3IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+NSBiaXRzIHNldDwvdGV4dD48L2c+CjxsaW5lIHgxPSIyMTIiIHkxPSI0NiIgeDI9IjI0OCIgeTI9IjQ2IiBzdHJva2U9IiNkZGQiIHN0cm9rZS13aWR0aD0iMC44IiBtYXJrZXItZW5kPSJ1cmwoI2E0KSIvPgo8ZyBjbGFzcz0iYy1wdXJwbGUiPjxyZWN0IHg9IjI0OCIgeT0iMjYiIHdpZHRoPSIyODAiIGhlaWdodD0iNDAiIHJ4PSI2Ii8+PHRleHQgY2xhc3M9InRoIiB4PSIzODgiIHk9IjQyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+a0JpdHNldEJ5dGVUYWJsZVsweGI1XTwvdGV4dD48dGV4dCBjbGFzcz0idHMiIHg9IjM4OCIgeT0iNTciIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5jb3VudD01IMK3IHBvc2l0aW9ucz1bMCwyLDQsNSw3LF8sXyxfXTwvdGV4dD48L2c+Cgo8bGluZSB4MT0iMzg4IiB5MT0iNjYiIHgyPSIzODgiIHkyPSI5MCIgc3Ryb2tlPSIjZGRkIiBzdHJva2Utd2lkdGg9IjAuOCIgbWFya2VyLWVuZD0idXJsKCNhNCkiLz4KCjxnIGNsYXNzPSJjLXB1cnBsZSI+PHJlY3QgeD0iNDQiIHk9IjkwIiB3aWR0aD0iMjAwIiBoZWlnaHQ9IjQwIiByeD0iNiIvPjx0ZXh0IGNsYXNzPSJ0aCIgeD0iMTQ0IiB5PSIxMDYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5zZXQxX2VwaTMyKDEwMDApPC90ZXh0Pjx0ZXh0IGNsYXNzPSJ0cyIgeD0iMTQ0IiB5PSIxMjEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5icm9hZGNhc3QgYmFzZSDihpIgOCBsYW5lczwvdGV4dD48L2c+CjxnIGNsYXNzPSJjLXB1cnBsZSI+PHJlY3QgeD0iMjYwIiB5PSI5MCIgd2lkdGg9IjI2OCIgaGVpZ2h0PSI0MCIgcng9IjYiLz48dGV4dCBjbGFzcz0idGgiIHg9IjM5NCIgeT0iMTA2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+bG9hZGxfZXBpNjQocG9zaXRpb25zKTwvdGV4dD48dGV4dCBjbGFzcz0idHMiIHg9IjM5NCIgeT0iMTIxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+bG9hZCA4IHBvc2l0aW9ucyDihpIgX19tMTI4aTwvdGV4dD48L2c+Cgo8bGluZSB4MT0iMTQ0IiB5MT0iMTMwIiB4Mj0iMjMwIiB5Mj0iMTU4IiBzdHJva2U9IiNkZGQiIHN0cm9rZS13aWR0aD0iMC44IiBtYXJrZXItZW5kPSJ1cmwoI2E0KSIvPgo8bGluZSB4MT0iMzk0IiB5MT0iMTMwIiB4Mj0iMzA4IiB5Mj0iMTU4IiBzdHJva2U9IiNkZGQiIHN0cm9rZS13aWR0aD0iMC44IiBtYXJrZXItZW5kPSJ1cmwoI2E0KSIvPgoKPGcgY2xhc3M9ImMtcHVycGxlIj48cmVjdCB4PSIxOTYiIHk9IjE1OCIgd2lkdGg9IjI4OCIgaGVpZ2h0PSI0MCIgcng9IjYiLz48dGV4dCBjbGFzcz0idGgiIHg9IjM0MCIgeT0iMTc0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+Y3Z0ZXBpOF9lcGkzMihwb3M4KTwvdGV4dD48dGV4dCBjbGFzcz0idHMiIHg9IjM0MCIgeT0iMTg5IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+c2lnbi1leHRlbmQgOCBieXRlcyDihpIgOCBpbnQzMnM8L3RleHQ+PC9nPgoKPGxpbmUgeDE9IjM0MCIgeTE9IjE5OCIgeDI9IjM0MCIgeTI9IjIyMiIgc3Ryb2tlPSIjZGRkIiBzdHJva2Utd2lkdGg9IjAuOCIgbWFya2VyLWVuZD0idXJsKCNhNCkiLz4KCjxnIGNsYXNzPSJjLXRlYWwiPjxyZWN0IHg9IjE5NiIgeT0iMjIyIiB3aWR0aD0iMjg4IiBoZWlnaHQ9IjQwIiByeD0iNiIvPjx0ZXh0IGNsYXNzPSJ0aCIgeD0iMzQwIiB5PSIyMzgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5hZGRfZXBpMzIoYmFzZSwgb2Zmc2V0cyk8L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSIzNDAiIHk9IjI1MyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPlsxMDAwLCAxMDAyLCAxMDA0LCAxMDA1LCAxMDA3LCBfLCBfLCBfXTwvdGV4dD48L2c+Cgo8bGluZSB4MT0iMzQwIiB5MT0iMjYyIiB4Mj0iMzQwIiB5Mj0iMjg2IiBzdHJva2U9IiNkZGQiIHN0cm9rZS13aWR0aD0iMC44IiBtYXJrZXItZW5kPSJ1cmwoI2E0KSIvPgoKPGcgY2xhc3M9ImMtdGVhbCI+PHJlY3QgeD0iMTk2IiB5PSIyODYiIHdpZHRoPSIyODgiIGhlaWdodD0iNDAiIHJ4PSI2Ii8+PHRleHQgY2xhc3M9InRoIiB4PSIzNDAiIHk9IjMwMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPnN0b3JldV9zaTI1NihlbmQsIHJlc3VsdCk8L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSIzNDAiIHk9IjMxNyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPndyaXRlIDMyIGJ5dGVzIMK3IGVuZCArPSA1IChjb3VudCwgbm90IDgpPC90ZXh0PjwvZz4KPC9zdmc+Cg==" width="680" height="340" class="img_ev3q"></p>
<p style="font-size:12px;opacity:0.7"><em>Fig. 4 - Five AVX2 instructions per byte: broadcast base, load positions, sign-extend, add, store. Write pointer advances by count, not 8 - safe overshoot by design.</em></p>
<p>Total work per 128-doc block: 16 words × 8 bytes, each producing up to 8 doc IDs with early-out for empty words and no branch per doc ID. In practice this reaches <code>~15 ns</code> for a 50%-dense block and <code>~20 ns</code> for a fully-dense block - at parity with SIMD bitpack decode.</p>
<p>On aarch64, where NEON lacks the gather instructions needed, a scalar fallback is used - correct everywhere, with AVX2 preferred where available. This mirrors the pattern from the <a href="https://serenedb.com/blog/norm-gathering">norm gathering post</a>: a fast architecture-specific path and a portable fallback, without intrinsics that complicate cross-platform builds.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="future-work">Future work<a href="https://serenedb.com/blog/search-optimization-5#future-work" class="hash-link" aria-label="Direct link to Future work" title="Direct link to Future work">​</a></h3>
<p>The SIMD materialization path closes the decode throughput gap, but materialization still happens even when the caller only needs to know whether a document is present. For seek-only operations, checking membership in a bitset is already a single bit test. Implementing the iterator's seek and advance operations directly over the bitset representation, without converting to a flat array first, would eliminate this conversion entirely on paths where it is not needed. The payoff is largest for top-K queries with pruning (e.g. BlockMax Score).</p>
<p>On the hardware side, AVX-512 would allow processing more bits per instruction in the materialization path and a 256-element block size would enable wider SIMD bitpacking and reduce per-block overhead. We're exploring both areas.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="conclusion">Conclusion<a href="https://serenedb.com/blog/search-optimization-5#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion">​</a></h3>
<p>A posting list format that looks simple from the outside - one byte, then a payload - hides a lot of design work in that one byte. Getting it right means every query that runs on top benefits: dense blocks compress smaller with the bitset encoding, structured data compresses to near-nothing with constant delta and rare terms stop paying the overhead of a VInt tail. Each encoding fits naturally into the same competition and adding a new one touches only the encoder and decoder.</p>
<p>The results are available on the <a href="https://serenedb.com/search-benchmark-game" target="_blank" rel="noopener noreferrer">official benchmark</a>, accepted and published by the Tantivy maintainers. IResearch leads across all query types and categories. All values are in microseconds:</p>
<div class="sd-heatmap-root"><style>
.sd-heatmap-root,
.sd-heatmap-root * { box-sizing: border-box; }

.sd-heatmap-root {
  background: transparent;
  width: 100%;
  font-family: var(--ifm-font-family-base);
  padding-top: 20px;
  padding-right: 20px;
  border-radius: 10px
}

.sd-heatmap-layout {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.sd-heatmap-scroll {
  position: relative;
  width: 100%;
  overflow: visible;
}

.sd-heatmap-lines {
  display:none;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.sd-heatmap-grid {
  display: grid;
  min-width: 780px;
  grid-template-columns: 88px repeat(3, minmax(0, 1fr));
  gap: 20px;
}

.sd-heatmap-grid-desktop {
  display: none;
}

.sd-heatmap-grid-mobile {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.sd-heatmap-row-mobile {
  display: grid;
  grid-template-columns: 88px minmax(0, 1fr);
  gap: 12px;
  align-items: start;
}

.sd-heatmap-axis {
  display: grid;
  grid-template-rows: 20px repeat(4, minmax(56px, 1fr));
  gap: 8px;
  padding-bottom: 28px;
}

.sd-heatmap-axis-spacer { width: 1px; }

.sd-heatmap-axis-label {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding-right: 4px;
  font-size: 8px;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(71, 85, 105, 0.75);
}

@media (max-width: 767px) {
  .sd-heatmap-axis {
    grid-template-rows: 14px repeat(4, 40px);
    gap: 4px;
    padding-bottom: 12px;
  }

  .sd-heatmap-axis-label {
    font-size: 7px;
    padding-right: 2px;
  }

  .sd-heatmap-collection-label {
    font-size: 7px;
    line-height: 14px;
  }

  .sd-heatmap-cell {
    min-height: 40px;
    padding: 6px;
  }
}

.sd-heatmap-card {
  position: relative;
  border-radius: 12px;
  backdrop-filter: blur(4px);
}



.sd-heatmap-card-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
}

.sd-heatmap-collection-label {
  padding: 0 4px;
  text-align: center;
  font-size: 8px;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(71, 85, 105, 0.75);
  word-break: break-word;
}

.sd-heatmap-engine-label {
  margin-top: 16px;
  border: 1px solid rgba(148, 163, 184, 0.35);
  border-radius: 8px;
  padding: 8px 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-size: 11px;
  font-weight: 800;
  color: var(--ifm-color-content);
  text-transform: uppercase;
}

.sd-heatmap-engine-badge {
  font-size: 8px;
  line-height: 1;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 999px;
  color: #ffffff;
  background: var(--ifm-serene-blue);
  text-transform: none;
}

.sd-heatmap-cell {
  position: relative;
  display: flex;
  aspect-ratio: 1 / 1;
  min-height: 56px;
  flex-direction: column;
  justify-content: space-between;
  border-radius: 10px;
  border-width: 1px;
  border-style: solid;
  padding: 8px;
  text-align: left;
  cursor: pointer;
  transition: opacity 0.2s ease, filter 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
}

.sd-heatmap-cell:hover { transform: translateY(-1px); }
.sd-heatmap-cell:focus-visible {
  outline: 2px solid var(--ifm-serene-blue);
  outline-offset: 1px;
}

.sd-heatmap-cell--dimmed {
  opacity: 0.3;
  filter: saturate(0.5);
}

.sd-heatmap-cell--active {
  border-style: dashed;
  border-color: var(--ifm-serene-blue) !important;
  box-shadow: none;
}

.sd-heatmap-cell--good { color: #064e3b; }
.sd-heatmap-cell--mid { color: #78350f; }
.sd-heatmap-cell--bad { color: #7f1d1d; }

[data-theme='dark'] .sd-heatmap-axis-label,
[data-theme='dark'] .sd-heatmap-collection-label {
  color: rgba(203, 213, 225, 0.82);
}

[data-theme='dark'] .sd-heatmap-engine-label { color: #e2e8f0; }
[data-theme='dark'] .sd-heatmap-engine-badge {
  color: #ffffff;
  background: var(--ifm-serene-blue);
}
[data-theme='dark'] .sd-heatmap-cell--good { color: #d1fae5; }
[data-theme='dark'] .sd-heatmap-cell--mid { color: #fef3c7; }
[data-theme='dark'] .sd-heatmap-cell--bad { color: #fee2e2; }

.sd-heatmap-cell-value {
  font-size: 15px;
  line-height: 1;
  font-weight: 800;
}

.sd-heatmap-cell-delta {
  font-size: 8px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.sd-heatmap-footnote {
  border: 1px solid rgba(148, 163, 184, 0.25);
  background: rgba(148, 163, 184, 0.08);
  border-radius: 8px;
  padding: 8px;
}

.sd-heatmap-footnote p {
  margin: 0;
  text-align: center;
  font-size: 12px;
  color: rgba(71, 85, 105, 0.8);
}

@media (min-width: 1024px) {
  .sd-heatmap-scroll {
    overflow-x: auto;
  }

  .sd-heatmap-grid-desktop {
    display: grid;
  }

  .sd-heatmap-grid-mobile {
    display: none;
  }
}
.sd-heatmap-footnote{
    margin-left: 107px;
   }


@media (min-width: 1280px) {
  .sd-heatmap-footnote{
    margin-left: 107px;
  }
  .sd-heatmap-layout {
  }
}

@media (max-width: 1024px) {
     .sd-heatmap-layout{
     margin-left: auto;
     margin-right:auto;
     padding-right: 30px;
     max-width: 400px;
   }
}

@media (max-width: 768px) {
   .sd-heatmap-layout {
     padding-right: 70px;
     max-width: 400px;
   }
   .sd-heatmap-footnote {
     margin-left: 100px;
   }
}

@media (max-width: 500px) {
   .sd-heatmap-layout {
     padding-right: 0px;
     margin-left: -70px;
     max-width: 400px;
   }
   .sd-heatmap-footnote {
     margin-left: 100px;
   }
}
</style><div class="sd-heatmap-layout"><div class="sd-heatmap-scroll"><svg aria-hidden="true" class="sd-heatmap-lines"></svg><div class="sd-heatmap-grid sd-heatmap-grid-desktop"><div class="sd-heatmap-axis"><div class="sd-heatmap-axis-spacer"></div><div class="sd-heatmap-axis-label">OR</div><div class="sd-heatmap-axis-label">AND</div><div class="sd-heatmap-axis-label">phrase</div><div class="sd-heatmap-axis-label">other</div></div><div class="sd-heatmap-card"><div class="sd-heatmap-card-grid"><div class="sd-heatmap-collection-label">count</div><div class="sd-heatmap-collection-label">top_100</div><div class="sd-heatmap-collection-label">top_100 count</div><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="union" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">253</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="union" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">651</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="union" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">1374</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="intersection" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">236</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="intersection" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">475</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="intersection" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">474</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="phrase" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">796</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="phrase" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">800</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="phrase" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">804</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="other" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">905</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="other" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">982</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="other" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">1115</span><span class="sd-heatmap-cell-delta">best</span></button></div><div class="sd-heatmap-engine-label"><span>iresearch</span><span class="sd-heatmap-engine-badge">by SereneDB</span></div></div><div class="sd-heatmap-card sd-heatmap-card--with-separator"><div class="sd-heatmap-card-grid"><div class="sd-heatmap-collection-label">count</div><div class="sd-heatmap-collection-label">top_100</div><div class="sd-heatmap-collection-label">top_100 count</div><button type="button" data-heatmap-engine="lucene" data-heatmap-query="union" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.29524684067549517);border-color:rgba(217, 119, 6, 0.396807346167416)"><span class="sd-heatmap-cell-value">379</span><span class="sd-heatmap-cell-delta">+49.8%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="union" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.23171923175992337);border-color:rgba(217, 119, 6, 0.34344415467833567)"><span class="sd-heatmap-cell-value">797</span><span class="sd-heatmap-cell-delta">+22.3%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="union" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.5969958738195035);border-color:rgba(153, 27, 27, 0.7455795510012115)"><span class="sd-heatmap-cell-value">4648</span><span class="sd-heatmap-cell-delta">+238.3%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="intersection" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.23700516344900746);border-color:rgba(217, 119, 6, 0.3478843372971663)"><span class="sd-heatmap-cell-value">294</span><span class="sd-heatmap-cell-delta">+24.6%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="intersection" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.2156759746979655);border-color:rgba(217, 119, 6, 0.329967818746291)"><span class="sd-heatmap-cell-value">548</span><span class="sd-heatmap-cell-delta">+15.4%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="intersection" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.3017667307227304);border-color:rgba(217, 119, 6, 0.40228405380709353)"><span class="sd-heatmap-cell-value">723</span><span class="sd-heatmap-cell-delta">+52.6%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="phrase" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.4284455302636617);border-color:rgba(153, 27, 27, 0.5910750694083566)"><span class="sd-heatmap-cell-value">1542</span><span class="sd-heatmap-cell-delta">+93.8%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="phrase" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.3754428298316831);border-color:rgba(153, 27, 27, 0.5424892606790428)"><span class="sd-heatmap-cell-value">1359</span><span class="sd-heatmap-cell-delta">+69.9%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="phrase" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.4470104997215848);border-color:rgba(153, 27, 27, 0.6080929580781195)"><span class="sd-heatmap-cell-value">1625</span><span class="sd-heatmap-cell-delta">+102.1%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="other" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.2984640738999065);border-color:rgba(217, 119, 6, 0.3995098220759214)"><span class="sd-heatmap-cell-value">1367</span><span class="sd-heatmap-cell-delta">+51.2%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="other" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.33250223876162577);border-color:rgba(217, 119, 6, 0.42810188055976567)"><span class="sd-heatmap-cell-value">1628</span><span class="sd-heatmap-cell-delta">+65.9%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="other" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.43602529419523695);border-color:rgba(217, 119, 6, 0.515061247123999)"><span class="sd-heatmap-cell-value">2347</span><span class="sd-heatmap-cell-delta">+110.6%</span></button></div><div class="sd-heatmap-engine-label"><span>lucene</span></div></div><div class="sd-heatmap-card sd-heatmap-card--with-separator"><div class="sd-heatmap-card-grid"><div class="sd-heatmap-collection-label">count</div><div class="sd-heatmap-collection-label">top_100</div><div class="sd-heatmap-collection-label">top_100 count</div><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="union" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.5926622405992304);border-color:rgba(153, 27, 27, 0.7416070538826279)"><span class="sd-heatmap-cell-value">817</span><span class="sd-heatmap-cell-delta">+222.5%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="union" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.4570090985760763);border-color:rgba(153, 27, 27, 0.6172583403614034)"><span class="sd-heatmap-cell-value">1346</span><span class="sd-heatmap-cell-delta">+106.6%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="union" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.4934149998976522);border-color:rgba(217, 119, 6, 0.5632685999140279)"><span class="sd-heatmap-cell-value">3233</span><span class="sd-heatmap-cell-delta">+135.4%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="intersection" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.4015821993520682);border-color:rgba(153, 27, 27, 0.5664503494060625)"><span class="sd-heatmap-cell-value">428</span><span class="sd-heatmap-cell-delta">+81.7%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="intersection" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.35160597530080806);border-color:rgba(153, 27, 27, 0.5206388106924075)"><span class="sd-heatmap-cell-value">756</span><span class="sd-heatmap-cell-delta">+59.2%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="intersection" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.3615481599204989);border-color:rgba(153, 27, 27, 0.529752479927124)"><span class="sd-heatmap-cell-value">776</span><span class="sd-heatmap-cell-delta">+63.7%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="phrase" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.2460016779038526);border-color:rgba(217, 119, 6, 0.3554414094392362)"><span class="sd-heatmap-cell-value">1023</span><span class="sd-heatmap-cell-delta">+28.5%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="phrase" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.2517326510031023);border-color:rgba(217, 119, 6, 0.3602554268426059)"><span class="sd-heatmap-cell-value">1048</span><span class="sd-heatmap-cell-delta">+31.0%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="phrase" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.25212343760925354);border-color:rgba(217, 119, 6, 0.36058368759177295)"><span class="sd-heatmap-cell-value">1054</span><span class="sd-heatmap-cell-delta">+31.2%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="other" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.7);border-color:rgba(153, 27, 27, 0.8400000000000001)"><span class="sd-heatmap-cell-value">6477</span><span class="sd-heatmap-cell-delta">+616.1%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="other" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.6903374694085057);border-color:rgba(153, 27, 27, 0.8311426802911304)"><span class="sd-heatmap-cell-value">6681</span><span class="sd-heatmap-cell-delta">+580.6%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="other" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.6758882674632718);border-color:rgba(153, 27, 27, 0.8178975785079992)"><span class="sd-heatmap-cell-value">6996</span><span class="sd-heatmap-cell-delta">+527.6%</span></button></div><div class="sd-heatmap-engine-label"><span>tantivy</span></div></div></div><div class="sd-heatmap-grid-mobile"><div class="sd-heatmap-row-mobile"><div class="sd-heatmap-axis"><div class="sd-heatmap-axis-spacer"></div><div class="sd-heatmap-axis-label">OR</div><div class="sd-heatmap-axis-label">AND</div><div class="sd-heatmap-axis-label">phrase</div><div class="sd-heatmap-axis-label">other</div></div><div class="sd-heatmap-card"><div class="sd-heatmap-card-grid"><div class="sd-heatmap-collection-label">count</div><div class="sd-heatmap-collection-label">top_100</div><div class="sd-heatmap-collection-label">top_100 count</div><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="union" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">253</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="union" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">651</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="union" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">1374</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="intersection" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">236</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="intersection" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">475</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="intersection" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">474</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="phrase" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">796</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="phrase" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">800</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="phrase" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">804</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="other" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">905</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="other" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">982</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="other" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">1115</span><span class="sd-heatmap-cell-delta">best</span></button></div><div class="sd-heatmap-engine-label"><span>iresearch</span><span class="sd-heatmap-engine-badge">by SereneDB</span></div></div></div><div class="sd-heatmap-row-mobile"><div class="sd-heatmap-axis"><div class="sd-heatmap-axis-spacer"></div><div class="sd-heatmap-axis-label">OR</div><div class="sd-heatmap-axis-label">AND</div><div class="sd-heatmap-axis-label">phrase</div><div class="sd-heatmap-axis-label">other</div></div><div class="sd-heatmap-card sd-heatmap-card--with-separator"><div class="sd-heatmap-card-grid"><div class="sd-heatmap-collection-label">count</div><div class="sd-heatmap-collection-label">top_100</div><div class="sd-heatmap-collection-label">top_100 count</div><button type="button" data-heatmap-engine="lucene" data-heatmap-query="union" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.29524684067549517);border-color:rgba(217, 119, 6, 0.396807346167416)"><span class="sd-heatmap-cell-value">379</span><span class="sd-heatmap-cell-delta">+49.8%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="union" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.23171923175992337);border-color:rgba(217, 119, 6, 0.34344415467833567)"><span class="sd-heatmap-cell-value">797</span><span class="sd-heatmap-cell-delta">+22.3%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="union" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.5969958738195035);border-color:rgba(153, 27, 27, 0.7455795510012115)"><span class="sd-heatmap-cell-value">4648</span><span class="sd-heatmap-cell-delta">+238.3%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="intersection" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.23700516344900746);border-color:rgba(217, 119, 6, 0.3478843372971663)"><span class="sd-heatmap-cell-value">294</span><span class="sd-heatmap-cell-delta">+24.6%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="intersection" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.2156759746979655);border-color:rgba(217, 119, 6, 0.329967818746291)"><span class="sd-heatmap-cell-value">548</span><span class="sd-heatmap-cell-delta">+15.4%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="intersection" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.3017667307227304);border-color:rgba(217, 119, 6, 0.40228405380709353)"><span class="sd-heatmap-cell-value">723</span><span class="sd-heatmap-cell-delta">+52.6%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="phrase" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.4284455302636617);border-color:rgba(153, 27, 27, 0.5910750694083566)"><span class="sd-heatmap-cell-value">1542</span><span class="sd-heatmap-cell-delta">+93.8%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="phrase" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.3754428298316831);border-color:rgba(153, 27, 27, 0.5424892606790428)"><span class="sd-heatmap-cell-value">1359</span><span class="sd-heatmap-cell-delta">+69.9%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="phrase" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.4470104997215848);border-color:rgba(153, 27, 27, 0.6080929580781195)"><span class="sd-heatmap-cell-value">1625</span><span class="sd-heatmap-cell-delta">+102.1%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="other" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.2984640738999065);border-color:rgba(217, 119, 6, 0.3995098220759214)"><span class="sd-heatmap-cell-value">1367</span><span class="sd-heatmap-cell-delta">+51.2%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="other" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.33250223876162577);border-color:rgba(217, 119, 6, 0.42810188055976567)"><span class="sd-heatmap-cell-value">1628</span><span class="sd-heatmap-cell-delta">+65.9%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="other" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.43602529419523695);border-color:rgba(217, 119, 6, 0.515061247123999)"><span class="sd-heatmap-cell-value">2347</span><span class="sd-heatmap-cell-delta">+110.6%</span></button></div><div class="sd-heatmap-engine-label"><span>lucene</span></div></div></div><div class="sd-heatmap-row-mobile"><div class="sd-heatmap-axis"><div class="sd-heatmap-axis-spacer"></div><div class="sd-heatmap-axis-label">OR</div><div class="sd-heatmap-axis-label">AND</div><div class="sd-heatmap-axis-label">phrase</div><div class="sd-heatmap-axis-label">other</div></div><div class="sd-heatmap-card sd-heatmap-card--with-separator"><div class="sd-heatmap-card-grid"><div class="sd-heatmap-collection-label">count</div><div class="sd-heatmap-collection-label">top_100</div><div class="sd-heatmap-collection-label">top_100 count</div><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="union" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.5926622405992304);border-color:rgba(153, 27, 27, 0.7416070538826279)"><span class="sd-heatmap-cell-value">817</span><span class="sd-heatmap-cell-delta">+222.5%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="union" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.4570090985760763);border-color:rgba(153, 27, 27, 0.6172583403614034)"><span class="sd-heatmap-cell-value">1346</span><span class="sd-heatmap-cell-delta">+106.6%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="union" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.4934149998976522);border-color:rgba(217, 119, 6, 0.5632685999140279)"><span class="sd-heatmap-cell-value">3233</span><span class="sd-heatmap-cell-delta">+135.4%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="intersection" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.4015821993520682);border-color:rgba(153, 27, 27, 0.5664503494060625)"><span class="sd-heatmap-cell-value">428</span><span class="sd-heatmap-cell-delta">+81.7%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="intersection" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.35160597530080806);border-color:rgba(153, 27, 27, 0.5206388106924075)"><span class="sd-heatmap-cell-value">756</span><span class="sd-heatmap-cell-delta">+59.2%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="intersection" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.3615481599204989);border-color:rgba(153, 27, 27, 0.529752479927124)"><span class="sd-heatmap-cell-value">776</span><span class="sd-heatmap-cell-delta">+63.7%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="phrase" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.2460016779038526);border-color:rgba(217, 119, 6, 0.3554414094392362)"><span class="sd-heatmap-cell-value">1023</span><span class="sd-heatmap-cell-delta">+28.5%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="phrase" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.2517326510031023);border-color:rgba(217, 119, 6, 0.3602554268426059)"><span class="sd-heatmap-cell-value">1048</span><span class="sd-heatmap-cell-delta">+31.0%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="phrase" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.25212343760925354);border-color:rgba(217, 119, 6, 0.36058368759177295)"><span class="sd-heatmap-cell-value">1054</span><span class="sd-heatmap-cell-delta">+31.2%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="other" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.7);border-color:rgba(153, 27, 27, 0.8400000000000001)"><span class="sd-heatmap-cell-value">6477</span><span class="sd-heatmap-cell-delta">+616.1%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="other" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.6903374694085057);border-color:rgba(153, 27, 27, 0.8311426802911304)"><span class="sd-heatmap-cell-value">6681</span><span class="sd-heatmap-cell-delta">+580.6%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="other" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.6758882674632718);border-color:rgba(153, 27, 27, 0.8178975785079992)"><span class="sd-heatmap-cell-value">6996</span><span class="sd-heatmap-cell-delta">+527.6%</span></button></div><div class="sd-heatmap-engine-label"><span>tantivy</span></div></div></div></div></div><div class="sd-heatmap-footnote"><p>All values have been measured in microseconds</p></div></div></div>
<br>
<p>These are still sort of "microbenchmark" results on a single-node search library. Real-world performance is shaped by many more factors like network, storage, query patterns, indexing load and the full system stack around the search engine. Translating library-level wins into end-to-end product performance is the next challenge for SereneDB and it's the one we are working on. We will be back with broader benchmarks soon. Stay tuned!</p>
<p>In the meantime, if you found this series useful, a star on <a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer">GitHub</a> is the best way to show it!</p>
<p><strong>Other articles in the Search optimization journey series:</strong></p>
<ul>
<li><a href="https://serenedb.com/blog/search-benchmark-game-overview">Benchmark overview</a></li>
<li><a href="https://serenedb.com/blog/search-optimization-1">Search optimization journey 1: Collecting top-K candidates</a></li>
<li><a href="https://serenedb.com/blog/search-optimization-2">Search optimization journey 2: Block scoring</a></li>
<li><a href="https://serenedb.com/blog/norm-gathering">Search optimization journey 3: Optimize norm gathering</a></li>
<li><a href="https://serenedb.com/blog/iresearch-two-phase-queries">Search optimization journey 4: How To Efficiently Execute Two Phase Queries</a></li>
</ul></div>]]></content>
        <author>
            <name>Andrey Abramov</name>
            <uri>https://www.linkedin.com/in/andrey-abramov-57a16241/ru/?skipRedirect=true</uri>
        </author>
        <category label="Search" term="Search"/>
        <category label="Dev" term="Dev"/>
        <category label="Search optimization journey" term="Search optimization journey"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[How To Efficiently Execute Two Phase Queries]]></title>
        <id>https://serenedb.com/blog/iresearch-two-phase-queries</id>
        <link href="https://serenedb.com/blog/iresearch-two-phase-queries"/>
        <updated>2026-04-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How To Efficiently Execute Two Phase Queries]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><img class="avatarImage_s4Ar" src="https://serenedb.com/blog/img/abramov.jpeg" alt="Andrey Abramov" loading="lazy"></div></div><div class="copy_iHNG"><p class="names_ox2I">Andrey Abramov</p><p class="meta_OgzX">Apr 22, 2026 · 11 minutes read</p></div></header><h2 class="title_RQ9v">How To Efficiently Execute Two Phase Queries</h2><p style="font-size:0.9rem;margin-top:-0.4rem">Search optimization journey 4</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg hideFirstHeading_VVMX" data-image-lightbox-root="true"><h2 class="anchor anchorWithStickyNavbar_LWe7" id="how-to-efficiently-execute-two-phase-queries"><strong>How To Efficiently Execute Two Phase Queries</strong><a href="https://serenedb.com/blog/iresearch-two-phase-queries#how-to-efficiently-execute-two-phase-queries" class="hash-link" aria-label="Direct link to how-to-efficiently-execute-two-phase-queries" title="Direct link to how-to-efficiently-execute-two-phase-queries">​</a></h2>
<p>In a <a href="https://serenedb.com/blog/search-benchmark-game-overview">recent benchmark</a>, SereneDB's search engine <a href="https://github.com/serenedb/serenedb/tree/main/libs/iresearch" target="_blank" rel="noopener noreferrer">IResearch</a> outperformed established search engines across query types and collection modes. This post is part of the <strong>Search optimization journey</strong> series, a set of technical reports examining the implementation decisions behind those results.</p>
<p>This articles focuses on lazy evaluation in the query execution layer. Search engines are machines for merging sorted streams of document IDs and nearly every interesting query like conjunctions, exclusions, phrase queries, geo filters, fuzzy matching, nested document joins requires iterators to cooperate and avoid doing expensive work on documents that will ultimately be rejected.</p>
<p>The mechanism <a href="https://github.com/serenedb/serenedb/tree/main/libs/iresearch" target="_blank" rel="noopener noreferrer">IResearch</a> uses for this is a two-contract seek API:</p>
<ul>
<li>a strict <code>Seek</code> for when you need the next document definitively;</li>
<li>a weaker <code>LazySeek</code> that only answers "are you at this exact document?" and skips all the expensive machinery when the answer is no.</li>
</ul>
<p>We'll follow this idea from the single posting list block all the way up through the full query tree and then show how the same principle extends to position-level intersection for phrase queries.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-problem-not-all-seeks-are-equal">The Problem: Not All Seeks Are Equal<a href="https://serenedb.com/blog/iresearch-two-phase-queries#the-problem-not-all-seeks-are-equal" class="hash-link" aria-label="Direct link to The Problem: Not All Seeks Are Equal" title="Direct link to The Problem: Not All Seeks Are Equal">​</a></h3>
<p>Every search engine built on inverted indexes is, at its core, a machine for merging sorted streams of document IDs.</p>
<ul>
<li>Lucene calls it <a href="https://github.com/apache/lucene/blob/main/lucene/core/src/java/org/apache/lucene/search/DocIdSetIterator.java" target="_blank" rel="noopener noreferrer"><code>DocIdSetIterator</code></a></li>
<li>Tantivy calls it <a href="https://github.com/quickwit-oss/tantivy/blob/main/src/docset.rs" target="_blank" rel="noopener noreferrer"><code>DocSet</code></a></li>
<li>In IResearch we call it <a href="https://github.com/serenedb/serenedb/blob/b0faf7d5ae786184f90c1af35ab93f646c67762e/libs/iresearch/include/iresearch/index/iterators.hpp#L274" target="_blank" rel="noopener noreferrer"><code>DocIterator</code></a></li>
</ul>
<p>The interface is the same: advance through documents in sorted order and optionally jump ahead to a target document ID via <code>Seek</code>.</p>
<p>But not all seeks are created equal. Some callers need the next document at or after a target. Others only need to know whether the target itself is present — and if it isn't, they don't care where the iterator ended up.</p>
<p>Two examples make this concrete:</p>
<ul>
<li><strong>Phrase query</strong>: "quick brown fox": first confirm all three terms appear in the document (cheap, posting list check), then verify they appear consecutively (expensive, decode positions). If the first check fails, there is no reason to touch positions at all.</li>
<li><strong>Geo filter</strong>: "find restaurants within 2km": first confirm the document's indexed S2 cell overlaps the query region (cheap, posting list check), then load the stored coordinates and run the precise distance calculation (expensive, column read + math). If the cell check fails, the geometry is never loaded.</li>
</ul>
<p>In both cases the second phase only runs when the first has already confirmed the document is a plausible candidate. <code>LazySeek</code> is the mechanism that makes this gating work across the entire query tree.</p>
<p>This distinction is the seed from which IResearch's lazy execution model grows.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-two-seek-contracts">The Two Seek Contracts<a href="https://serenedb.com/blog/iresearch-two-phase-queries#the-two-seek-contracts" class="hash-link" aria-label="Direct link to The Two Seek Contracts" title="Direct link to The Two Seek Contracts">​</a></h3>
<p><code>Seek</code> always positions the iterator at the smallest document ≥ target and returns it definitively. <code>LazySeek</code> has a weaker, three-outcome contract: given a target, it may stay on the current document (if it already equals target), jump exactly to target or jump to some document larger than target. All three outcomes are valid and the result can be freely mixed with all other iterator API calls, there is no indeterminate state to reason about.</p>
<p style="text-align:center"><code>value() ≤ LazySeek(target) ≤ Seek(target)</code></p>
<p>The key difference from <code>Seek</code> is that when <code>LazySeek</code> returns a value greater than target, it may have taken a shortcut — returning the first convenient value ≥ target rather than the precise next document. This is valid for callers who only need a confirmation ("are you at this exact document?") rather than a definitive forward advance.</p>
<p>The default implementation of <code>LazySeek</code> simply calls <code>Seek</code>. The interesting behavior emerges in the overrides and in how composite iterators exploit the weaker contract to avoid work entirely.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="why-this-matters-four-query-types-that-exploit-laziness">Why This Matters: Four Query Types That Exploit Laziness<a href="https://serenedb.com/blog/iresearch-two-phase-queries#why-this-matters-four-query-types-that-exploit-laziness" class="hash-link" aria-label="Direct link to Why This Matters: Four Query Types That Exploit Laziness" title="Direct link to Why This Matters: Four Query Types That Exploit Laziness">​</a></h3>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="1-two-phase-queries-phrase-geo-n-gram-nested">1. Two-phase queries (Phrase, Geo, N-gram, Nested)<a href="https://serenedb.com/blog/iresearch-two-phase-queries#1-two-phase-queries-phrase-geo-n-gram-nested" class="hash-link" aria-label="Direct link to 1. Two-phase queries (Phrase, Geo, N-gram, Nested)" title="Direct link to 1. Two-phase queries (Phrase, Geo, N-gram, Nested)">​</a></h4>
<p>Two-phase execution is very intuitive pattern. Every query that has a cheap approximation and an expensive exact check benefits from <code>LazySeek</code>. The approximation runs first using <code>LazySeek</code> and if it misses, the exact check is skipped entirely. The exact check only fires when the approximation confirms the document is a plausible candidate.</p>
<p>This matters because the exact check is expensive — decoding positions, deserializing geometry, checking sequence continuity, scanning child documents. <code>LazySeek</code> on the approximation means that cost is never paid for documents that would fail it anyway.</p>
<p>The two-phase pattern is universal.</p>
<p><strong>Phrase queries</strong> are the most familiar example, but the same two-phase structure appears across every non-trivial query type in IResearch. The approximation is always a cheap posting-list operation; the exact check is whatever domain-specific work cannot be avoided once a candidate document is confirmed.</p>
<p><img decoding="async" loading="lazy" alt="Fig. 1 — Positions are decoded only when the conjunction confirms all terms are present. A missed document in phase 1 costs nothing in phase 2." src="data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNjgwIDQwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgZGF0YS1maWd1cmUtYmFja2dyb3VuZD0id2hpdGUiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9IiNmZmYiLz4KPHN0eWxlPjwhW0NEQVRBWwoudCB7IGZvbnQ6IDQwMCAxNHB4IC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgJ1NlZ29lIFVJJywgc2Fucy1zZXJpZjsgZmlsbDogIzFhMWExYTsgfQoudHMgeyBmb250OiA0MDAgMTJweCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICdTZWdvZSBVSScsIHNhbnMtc2VyaWY7IGZpbGw6ICM1NTU7IH0KLnRoIHsgZm9udDogNjAwIDE0cHggLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAnU2Vnb2UgVUknLCBzYW5zLXNlcmlmOyBmaWxsOiAjMWExYTFhOyB9Ci5jLWdyYXkgPiByZWN0IHsgZmlsbDogI2YxZWZlODsgc3Ryb2tlOiAjODg4NzgwOyBzdHJva2Utd2lkdGg6IDAuNTsgfQouYy1ncmF5ID4gdGV4dCB7IGZpbGw6ICM0NDQ0NDE7IH0KLmMtYmx1ZSA+IHJlY3QgeyBmaWxsOiAjZTZmMWZiOyBzdHJva2U6ICMzNzhhZGQ7IHN0cm9rZS13aWR0aDogMC41OyB9Ci5jLWJsdWUgPiB0ZXh0IHsgZmlsbDogIzBjNDQ3YzsgfQouYy10ZWFsID4gcmVjdCB7IGZpbGw6ICNlMWY1ZWU7IHN0cm9rZTogIzFkOWU3NTsgc3Ryb2tlLXdpZHRoOiAwLjU7IH0KLmMtdGVhbCA+IHRleHQgeyBmaWxsOiAjMDg1MDQxOyB9Ci5jLWNvcmFsID4gcmVjdCB7IGZpbGw6ICNmYWVjZTc7IHN0cm9rZTogI2Q4NWEzMDsgc3Ryb2tlLXdpZHRoOiAwLjU7IH0KLmMtY29yYWwgPiB0ZXh0IHsgZmlsbDogIzcxMmIxMzsgfQouYy1wdXJwbGUgPiByZWN0IHsgZmlsbDogI2VlZWRmZTsgc3Ryb2tlOiAjN2Y3N2RkOyBzdHJva2Utd2lkdGg6IDAuNTsgfQouYy1wdXJwbGUgPiB0ZXh0IHsgZmlsbDogIzNjMzQ4OTsgfQouYy1hbWJlciA+IHJlY3QgeyBmaWxsOiAjZmFlZWRhOyBzdHJva2U6ICNiYTc1MTc7IHN0cm9rZS13aWR0aDogMC41OyB9Ci5jLWFtYmVyID4gdGV4dCB7IGZpbGw6ICM2MzM4MDY7IH0KcmVjdC5zZWN0aW9uLWJnIHsgZmlsbDogI2Y2ZjVmMDsgc3Ryb2tlOiAjZGRkOyBzdHJva2Utd2lkdGg6IDAuNTsgfQpyZWN0Lmluc2lnaHQtYmcgeyBmaWxsOiAjZjBlZGU0OyBzdHJva2U6ICNjY2M7IHN0cm9rZS13aWR0aDogMC41OyB9Cl1dPjwvc3R5bGU+CiAgPGRlZnM+PG1hcmtlciBpZD0iYTQiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iOCIgcmVmWT0iNSIgbWFya2VyV2lkdGg9IjYiIG1hcmtlckhlaWdodD0iNiIgb3JpZW50PSJhdXRvLXN0YXJ0LXJldmVyc2UiPjxwYXRoIGQ9Ik0yIDFMOCA1TDIgOSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJjb250ZXh0LXN0cm9rZSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvbWFya2VyPjwvZGVmcz4KICA8dGV4dCBjbGFzcz0idGgiIHg9IjM0MCIgeT0iMjgiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlR3by1waGFzZSBleGVjdXRpb246IGNvbmp1bmN0aW9uIGdhdGVzIHBvc2l0aW9uIGRlY29kaW5nPC90ZXh0PgogIDx0ZXh0IGNsYXNzPSJ0cyIgeD0iMTc2IiB5PSI1MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgb3BhY2l0eT0iMC41NSI+cGhhc2UgMSDigJQgY2hlYXA8L3RleHQ+CiAgPHRleHQgY2xhc3M9InRzIiB4PSI1MTYiIHk9IjUyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBvcGFjaXR5PSIwLjU1Ij5waGFzZSAyIOKAlCBleHBlbnNpdmU8L3RleHQ+CiAgPGxpbmUgeDE9IjM0NiIgeTE9IjQ0IiB4Mj0iMzQ2IiB5Mj0iMzQ2IiBzdHJva2U9IiNkZGQiIHN0cm9rZS13aWR0aD0iMC41IiBzdHJva2UtZGFzaGFycmF5PSI0IDMiLz4KICA8cmVjdCB4PSI0NCIgeT0iNjAiIHdpZHRoPSIyNjQiIGhlaWdodD0iNjYiIHJ4PSI4IiBjbGFzcz0ic2VjdGlvbi1iZyIvPgogIDx0ZXh0IGNsYXNzPSJ0aCIgeD0iMTc2IiB5PSI4MCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+Q29uanVuY3Rpb24gKGFwcHJveCk8L3RleHQ+CiAgPHRleHQgY2xhc3M9InRzIiB4PSIxNzYiIHk9Ijk4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5hbGwgdGVybXMgcHJlc2VudCBpbiBkb2M/PC90ZXh0PgogIDx0ZXh0IGNsYXNzPSJ0cyIgeD0iMTc2IiB5PSIxMTQiIHRleHQtYW5jaG9yPSJtaWRkbGUiPnVzZXMgTGF6eVNlZWsg4oCUIG5vIHBvc2l0aW9ucyBsb2FkZWQ8L3RleHQ+CiAgPHJlY3QgeD0iMzY0IiB5PSI2MCIgd2lkdGg9IjI3MiIgaGVpZ2h0PSI2NiIgcng9IjgiIGNsYXNzPSJzZWN0aW9uLWJnIi8+CiAgPHRleHQgY2xhc3M9InRoIiB4PSI1MDAiIHk9IjgwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5Qb3NpdGlvbiBjaGVjayAoZXhhY3QpPC90ZXh0PgogIDx0ZXh0IGNsYXNzPSJ0cyIgeD0iNTAwIiB5PSI5OCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+dGVybXMgYXQgY29ycmVjdCBwb3NpdGlvbnM/PC90ZXh0PgogIDx0ZXh0IGNsYXNzPSJ0cyIgeD0iNTAwIiB5PSIxMTQiIHRleHQtYW5jaG9yPSJtaWRkbGUiPmRlY29kZSBwb3NpdGlvbnMgb25seSBpZiBwaGFzZSAxIHBhc3NlczwvdGV4dD4KICA8cmVjdCB4PSI0NCIgeT0iMTQ4IiB3aWR0aD0iNTkyIiBoZWlnaHQ9Ijg4IiByeD0iOCIgY2xhc3M9InNlY3Rpb24tYmciLz4KICA8dGV4dCBjbGFzcz0idHMiIHg9IjYwIiB5PSIxNjYiIG9wYWNpdHk9IjAuNSI+Y2FzZSBBIOKAlCBjb25qdW5jdGlvbiBtaXNzOiBwb3NpdGlvbnMgbmV2ZXIgZGVjb2RlZDwvdGV4dD4KICA8ZyBjbGFzcz0iYy1jb3JhbCI+PHJlY3QgeD0iNjAiIHk9IjE3NCIgd2lkdGg9IjI2NCIgaGVpZ2h0PSI0NCIgcng9IjYiLz48dGV4dCBjbGFzcz0idGgiIHg9IjE5MiIgeT0iMTkyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+YXBwcm94LkxhenlTZWVrKDUwKSDihpIgNjM8L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSIxOTIiIHk9IjIwOCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPm1pc3Mg4oCUIDYzIOKJoCA1MCwgcmV0dXJuIDYzPC90ZXh0PjwvZz4KICA8cGF0aCBkPSJNMzI0IDE5NiBMMzQ2IDE5NiBMMzY0IDE5NiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRDg1QTMwIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1kYXNoYXJyYXk9IjQgMyIgbWFya2VyLWVuZD0idXJsKCNhNCkiLz4KICA8cmVjdCB4PSIzNjQiIHk9IjE3NCIgd2lkdGg9IjI3MiIgaGVpZ2h0PSI0NCIgcng9IjYiIGZpbGw9IiNmNmY1ZjAiIHN0cm9rZT0iI2RkZCIgc3Ryb2tlLXdpZHRoPSIwLjUiIG9wYWNpdHk9IjAuMzUiLz4KICA8dGV4dCBjbGFzcz0idHMiIHg9IjUwMCIgeT0iMTk2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCIgb3BhY2l0eT0iMC4zIj5wb3NpdGlvbiBkZWNvZGU8L3RleHQ+CiAgPGxpbmUgeDE9IjM2NCIgeTE9IjE3NCIgeDI9IjYzNiIgeTI9IjIxOCIgc3Ryb2tlPSIjY2NjIiBzdHJva2Utd2lkdGg9IjEuMiIgb3BhY2l0eT0iMC40Ii8+CiAgPGxpbmUgeDE9IjYzNiIgeTE9IjE3NCIgeDI9IjM2NCIgeTI9IjIxOCIgc3Ryb2tlPSIjY2NjIiBzdHJva2Utd2lkdGg9IjEuMiIgb3BhY2l0eT0iMC40Ii8+CiAgPHRleHQgY2xhc3M9InRzIiB4PSI1MDAiIHk9IjIzMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgb3BhY2l0eT0iMC40Ij5uZXZlciBjYWxsZWQ8L3RleHQ+CiAgPHJlY3QgeD0iNDQiIHk9IjI1NiIgd2lkdGg9IjU5MiIgaGVpZ2h0PSI4OCIgcng9IjgiIGNsYXNzPSJzZWN0aW9uLWJnIi8+CiAgPHRleHQgY2xhc3M9InRzIiB4PSI2MCIgeT0iMjc0IiBvcGFjaXR5PSIwLjUiPmNhc2UgQiDigJQgY29uanVuY3Rpb24gY29uZmlybXM6IHBvc2l0aW9ucyBkZWNvZGVkIGFuZCBjaGVja2VkPC90ZXh0PgogIDxnIGNsYXNzPSJjLXRlYWwiPjxyZWN0IHg9IjYwIiB5PSIyODIiIHdpZHRoPSIyNjQiIGhlaWdodD0iNDQiIHJ4PSI2Ii8+PHRleHQgY2xhc3M9InRoIiB4PSIxOTIiIHk9IjMwMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPmFwcHJveC5MYXp5U2Vlayg2Mykg4oaSIDYzPC90ZXh0Pjx0ZXh0IGNsYXNzPSJ0cyIgeD0iMTkyIiB5PSIzMTYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5hbGwgdGVybXMgcHJlc2VudCDinJM8L3RleHQ+PC9nPgogIDxwYXRoIGQ9Ik0zMjQgMzA0IEwzNjQgMzA0IiBmaWxsPSJub25lIiBzdHJva2U9IiMwRjZFNTYiIHN0cm9rZS13aWR0aD0iMS4yIiBtYXJrZXItZW5kPSJ1cmwoI2E0KSIvPgogIDxnIGNsYXNzPSJjLXRlYWwiPjxyZWN0IHg9IjM2NCIgeT0iMjgyIiB3aWR0aD0iMjcyIiBoZWlnaHQ9IjQ0IiByeD0iNiIvPjx0ZXh0IGNsYXNzPSJ0aCIgeD0iNTAwIiB5PSIzMDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5mcmVxLk1hdGNoKCkg4oaSIHRydWU8L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSI1MDAiIHk9IjMxNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPnBvc2l0aW9ucyBpbnRlcnNlY3Qg4oaSIHBocmFzZSBmb3VuZCDinJM8L3RleHQ+PC9nPgogIDxnIGNsYXNzPSJjLXRlYWwiPjxyZWN0IHg9IjI0MCIgeT0iMzU4IiB3aWR0aD0iMjAwIiBoZWlnaHQ9IjM0IiByeD0iOCIvPjx0ZXh0IGNsYXNzPSJ0aCIgeD0iMzQwIiB5PSIzNzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5QaHJhc2VJdGVyYXRvciDihpIgZG9jIDYzPC90ZXh0PjwvZz4KICA8L3N2Zz4K" width="680" height="400" class="img_ev3q"></p>
<p style="font-size:12px;opacity:0.7"><em>Fig. 1 — Positions are decoded only when the conjunction confirms all terms are present. A missed document in phase 1 costs nothing in phase 2.</em></p>
<p><strong>Geo filter</strong> — the query region is decomposed into a covering of S2 cells, each indexed as a term. The approximation is a disjunction over those cell posting lists: if the document's indexed cells don't overlap the query region, <code>LazySeek</code> returns immediately and the stored geometry is never read. Only when the approximation confirms the document does the exact check deserialize the geometry from columnar storage and run the spatial predicate.</p>
<p><strong>N-gram similarity</strong> — a fuzzy text query like "quikc" is split into character n-grams ("quik", "uikc") and matched using a <code>MinMatchDisjunction</code> requiring at least <em>k</em> n-grams to be present. This is phase 1. Phase 2 then verifies that the matched n-grams form a contiguous sequence in the document confirming that. This is a genuine fuzzy match rather than an accidental overlap. Position decoding only happens in phase 2, and only when phase 1 confirms enough n-grams are present.</p>
<p><strong>Nested filter (child-to-parent join)</strong> — in a document model with nested objects (e.g. an ORDER containing LINE ITEMS), a query might ask for orders where <em>any</em> line item matches a predicate. The parent iterator confirms the parent document with <code>LazySeek</code>. Only then does the child iterator <code>Seek</code> through the range of child doc IDs belonging to that parent. If no child satisfies the child filter, the parent is rejected and the search continues without having paid for a full child scan on every candidate parent.</p>
<p><img decoding="async" loading="lazy" alt="Fig. 2 — The exact check differs per query type, but the gating is always the same: a cheap LazySeek confirms the document before any expensive I/O is attempted." src="data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNjgwIDU4MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgZGF0YS1maWd1cmUtYmFja2dyb3VuZD0id2hpdGUiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9IiNmZmYiLz4KPHN0eWxlPjwhW0NEQVRBWwoudCB7IGZvbnQ6IDQwMCAxNHB4IC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgJ1NlZ29lIFVJJywgc2Fucy1zZXJpZjsgZmlsbDogIzFhMWExYTsgfQoudHMgeyBmb250OiA0MDAgMTJweCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICdTZWdvZSBVSScsIHNhbnMtc2VyaWY7IGZpbGw6ICM1NTU7IH0KLnRoIHsgZm9udDogNjAwIDE0cHggLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAnU2Vnb2UgVUknLCBzYW5zLXNlcmlmOyBmaWxsOiAjMWExYTFhOyB9Ci5jLWdyYXkgPiByZWN0IHsgZmlsbDogI2YxZWZlODsgc3Ryb2tlOiAjODg4NzgwOyBzdHJva2Utd2lkdGg6IDAuNTsgfQouYy1ncmF5ID4gdGV4dCB7IGZpbGw6ICM0NDQ0NDE7IH0KLmMtYmx1ZSA+IHJlY3QgeyBmaWxsOiAjZTZmMWZiOyBzdHJva2U6ICMzNzhhZGQ7IHN0cm9rZS13aWR0aDogMC41OyB9Ci5jLWJsdWUgPiB0ZXh0IHsgZmlsbDogIzBjNDQ3YzsgfQouYy10ZWFsID4gcmVjdCB7IGZpbGw6ICNlMWY1ZWU7IHN0cm9rZTogIzFkOWU3NTsgc3Ryb2tlLXdpZHRoOiAwLjU7IH0KLmMtdGVhbCA+IHRleHQgeyBmaWxsOiAjMDg1MDQxOyB9Ci5jLWNvcmFsID4gcmVjdCB7IGZpbGw6ICNmYWVjZTc7IHN0cm9rZTogI2Q4NWEzMDsgc3Ryb2tlLXdpZHRoOiAwLjU7IH0KLmMtY29yYWwgPiB0ZXh0IHsgZmlsbDogIzcxMmIxMzsgfQouYy1wdXJwbGUgPiByZWN0IHsgZmlsbDogI2VlZWRmZTsgc3Ryb2tlOiAjN2Y3N2RkOyBzdHJva2Utd2lkdGg6IDAuNTsgfQouYy1wdXJwbGUgPiB0ZXh0IHsgZmlsbDogIzNjMzQ4OTsgfQouYy1hbWJlciA+IHJlY3QgeyBmaWxsOiAjZmFlZWRhOyBzdHJva2U6ICNiYTc1MTc7IHN0cm9rZS13aWR0aDogMC41OyB9Ci5jLWFtYmVyID4gdGV4dCB7IGZpbGw6ICM2MzM4MDY7IH0KcmVjdC5zZWN0aW9uLWJnIHsgZmlsbDogI2Y2ZjVmMDsgc3Ryb2tlOiAjZGRkOyBzdHJva2Utd2lkdGg6IDAuNTsgfQpyZWN0Lmluc2lnaHQtYmcgeyBmaWxsOiAjZjBlZGU0OyBzdHJva2U6ICNjY2M7IHN0cm9rZS13aWR0aDogMC41OyB9Cl1dPjwvc3R5bGU+CiAgPGRlZnM+PG1hcmtlciBpZD0iYTgiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iOCIgcmVmWT0iNSIgbWFya2VyV2lkdGg9IjYiIG1hcmtlckhlaWdodD0iNiIgb3JpZW50PSJhdXRvLXN0YXJ0LXJldmVyc2UiPjxwYXRoIGQ9Ik0yIDFMOCA1TDIgOSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJjb250ZXh0LXN0cm9rZSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvbWFya2VyPjwvZGVmcz4KICA8dGV4dCBjbGFzcz0idGgiIHg9IjM0MCIgeT0iMjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiPldoYXQgZWFjaCBwaGFzZSBhY3R1YWxseSBkb2VzPC90ZXh0PgogIDx0ZXh0IGNsYXNzPSJ0aCIgeD0iMzQwIiB5PSI1NiIgZmlsbD0iIzUzNEFCNyI+R2VvIGZpbHRlcjwvdGV4dD4KICA8bGluZSB4MT0iNDQiIHkxPSI2NCIgeDI9IjYzNiIgeTI9IjY0IiBzdHJva2U9IiM3Rjc3REQiIHN0cm9rZS13aWR0aD0iMC41IiBvcGFjaXR5PSIwLjUiLz4KICA8ZyBjbGFzcz0iYy1wdXJwbGUiPjxyZWN0IHg9IjQ0IiB5PSI3MiIgd2lkdGg9IjI3MCIgaGVpZ2h0PSI3OCIgcng9IjYiLz48dGV4dCBjbGFzcz0idGgiIHg9IjE3OSIgeT0iOTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5QaGFzZSAxOiBTMiBjZWxsIGFwcHJveGltYXRpb248L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSIxNzkiIHk9IjExMiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+UXVlcnkgcmVnaW9uIGRlY29tcG9zZWQgaW50byBTMiBjZWxscy48L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSIxNzkiIHk9IjEyOCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+Q2VsbCBJRHMgaW5kZXhlZCBhcyB0ZXJtcy4gRGlzanVuY3Rpb248L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSIxNzkiIHk9IjE0NCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+b3ZlciBjZWxsIHBvc3RpbmcgbGlzdHMuIExhenlTZWVrIGNoZWFwLjwvdGV4dD48L2c+CiAgPHBhdGggZD0iTTMxNCAxMTEgTDM1NiAxMTEiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzUzNEFCNyIgc3Ryb2tlLXdpZHRoPSIxLjIiIG1hcmtlci1lbmQ9InVybCgjYTgpIi8+CiAgPGcgY2xhc3M9ImMtcHVycGxlIj48cmVjdCB4PSIzNTYiIHk9IjcyIiB3aWR0aD0iMjgwIiBoZWlnaHQ9Ijc4IiByeD0iNiIvPjx0ZXh0IGNsYXNzPSJ0aCIgeD0iNDk2IiB5PSI5MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPlBoYXNlIDI6IGdlb21ldHJ5IHByZWRpY2F0ZTwvdGV4dD48dGV4dCBjbGFzcz0idHMiIHg9IjQ5NiIgeT0iMTEyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5GZXRjaCBzdG9yZWQgZ2VvbWV0cnkgYnl0ZXMgZnJvbTwvdGV4dD48dGV4dCBjbGFzcz0idHMiIHg9IjQ5NiIgeT0iMTI4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5jb2x1bW5hciBzdG9yYWdlIHZpYSBMYXp5U2Vlay48L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSI0OTYiIHk9IjE0NCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+UGFyc2Ugc2hhcGUsIHJ1biBzcGF0aWFsIGFjY2VwdG9yLjwvdGV4dD48L2c+CiAgPHRleHQgY2xhc3M9InRoIiB4PSIzNDAiIHk9IjE3OCIgZmlsbD0iI0JBNzUxNyI+Ti1ncmFtIHNpbWlsYXJpdHk8L3RleHQ+CiAgPGxpbmUgeDE9IjQ0IiB5MT0iMTg2IiB4Mj0iNjM2IiB5Mj0iMTg2IiBzdHJva2U9IiNCQTc1MTciIHN0cm9rZS13aWR0aD0iMC41IiBvcGFjaXR5PSIwLjUiLz4KICA8ZyBjbGFzcz0iYy1hbWJlciI+PHJlY3QgeD0iNDQiIHk9IjE5NCIgd2lkdGg9IjI3MCIgaGVpZ2h0PSI5MCIgcng9IjYiLz48dGV4dCBjbGFzcz0idGgiIHg9IjE3OSIgeT0iMjE0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+UGhhc2UgMTogbWluLW1hdGNoIGRpc2p1bmN0aW9uPC90ZXh0Pjx0ZXh0IGNsYXNzPSJ0cyIgeD0iMTc5IiB5PSIyMzQiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlF1ZXJ5IHNwbGl0IGludG8gbi1ncmFtcyAoInF1aWMiLDwvdGV4dD48dGV4dCBjbGFzcz0idHMiIHg9IjE3OSIgeT0iMjUwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj4idWljayIsICJpY2siKS4gUmVxdWlyZSDiiaVrIHRvIG1hdGNoLjwvdGV4dD48dGV4dCBjbGFzcz0idHMiIHg9IjE3OSIgeT0iMjY2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5MYXp5U2VlayBvbiBkaXNqdW5jdGlvbi4gQ291bnQgY2hlYXAuPC90ZXh0PjwvZz4KICA8cGF0aCBkPSJNMzE0IDIzOSBMMzU2IDIzOSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjQkE3NTE3IiBzdHJva2Utd2lkdGg9IjEuMiIgbWFya2VyLWVuZD0idXJsKCNhOCkiLz4KICA8ZyBjbGFzcz0iYy1hbWJlciI+PHJlY3QgeD0iMzU2IiB5PSIxOTQiIHdpZHRoPSIyODAiIGhlaWdodD0iOTAiIHJ4PSI2Ii8+PHRleHQgY2xhc3M9InRoIiB4PSI0OTYiIHk9IjIxNCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPlBoYXNlIDI6IHNlcXVlbmNlIGNoZWNrZXI8L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSI0OTYiIHk9IjIzNCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+V2FsayBwb3NpdGlvbnMgb2YgbWF0Y2hlZCBuLWdyYW1zLjwvdGV4dD48dGV4dCBjbGFzcz0idHMiIHg9IjQ5NiIgeT0iMjUwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5WZXJpZnkgdGhleSBmb3JtIGEgY29udGlndW91cyBydW4uPC90ZXh0Pjx0ZXh0IGNsYXNzPSJ0cyIgeD0iNDk2IiB5PSIyNjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkNvbXB1dGUgc2ltaWxhcml0eSBzY29yZSBmcm9tIHJ1biBsZW5ndGguPC90ZXh0PjwvZz4KICA8dGV4dCBjbGFzcz0idGgiIHg9IjM0MCIgeT0iMzE0IiBmaWxsPSIjOTkzQzFEIj5OZXN0ZWQgZmlsdGVyIChjaGlsZC10by1wYXJlbnQgam9pbik8L3RleHQ+CiAgPGxpbmUgeDE9IjQ0IiB5MT0iMzIyIiB4Mj0iNjM2IiB5Mj0iMzIyIiBzdHJva2U9IiNEODVBMzAiIHN0cm9rZS13aWR0aD0iMC41IiBvcGFjaXR5PSIwLjUiLz4KICA8ZyBjbGFzcz0iYy1jb3JhbCI+PHJlY3QgeD0iNDQiIHk9IjMzMCIgd2lkdGg9IjI3MCIgaGVpZ2h0PSI5MCIgcng9IjYiLz48dGV4dCBjbGFzcz0idGgiIHg9IjE3OSIgeT0iMzUwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+UGhhc2UgMTogcGFyZW50IGFwcHJveGltYXRpb248L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSIxNzkiIHk9IjM3MCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+UGFyZW50IGRvYyBpdGVyYXRvciAoZS5nLiBPUkRFUiBkb2NzKS48L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSIxNzkiIHk9IjM4NiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+TGF6eVNlZWsgY29uZmlybXMgcGFyZW50IGNoZWFwbHkuPC90ZXh0Pjx0ZXh0IGNsYXNzPSJ0cyIgeD0iMTc5IiB5PSI0MDIiIHRleHQtYW5jaG9yPSJtaWRkbGUiPk5vIGNoaWxkIEkvTyB1bnRpbCBwYXJlbnQgY29uZmlybWVkLjwvdGV4dD48L2c+CiAgPHBhdGggZD0iTTMxNCAzNzUgTDM1NiAzNzUiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzk5M0MxRCIgc3Ryb2tlLXdpZHRoPSIxLjIiIG1hcmtlci1lbmQ9InVybCgjYTgpIi8+CiAgPGcgY2xhc3M9ImMtY29yYWwiPjxyZWN0IHg9IjM1NiIgeT0iMzMwIiB3aWR0aD0iMjgwIiBoZWlnaHQ9IjkwIiByeD0iNiIvPjx0ZXh0IGNsYXNzPSJ0aCIgeD0iNDk2IiB5PSIzNTAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5QaGFzZSAyOiBjaGlsZCBTZWVrPC90ZXh0Pjx0ZXh0IGNsYXNzPSJ0cyIgeD0iNDk2IiB5PSIzNzAiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlNlZWsgY2hpbGQgaW50byBbcHJldl9wYXJlbnQrMSwgcGFyZW50KS48L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSI0OTYiIHk9IjM4NiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+Q2hpbGQgbXVzdCBzYXRpc2Z5IGl0cyBvd24gZmlsdGVyLjwvdGV4dD48dGV4dCBjbGFzcz0idHMiIHg9IjQ5NiIgeT0iNDAyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5JZiBubyBjaGlsZCBtYXRjaGVzLCBhZHZhbmNlIHBhcmVudC48L3RleHQ+PC9nPgogIDxyZWN0IHg9IjQ0IiB5PSI0MzgiIHdpZHRoPSI1OTIiIGhlaWdodD0iMTIyIiByeD0iOCIgY2xhc3M9Imluc2lnaHQtYmciLz4KICA8dGV4dCBjbGFzcz0idGgiIHg9IjM0MCIgeT0iNDYwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5Db3N0IGFzeW1tZXRyeSDigJQgd2h5IHRoZSBnYXRpbmcgbWF0dGVyczwvdGV4dD4KICA8dGV4dCBjbGFzcz0idHMiIHg9IjYwIiB5PSI0ODQiPjx0c3BhbiBmb250LXdlaWdodD0iNjAwIj5HZW88L3RzcGFuPiAgICBwaGFzZSAxOiBjaGVjayBhIHBvc3RpbmcgbGlzdCBiaXQgwrcgcGhhc2UgMjogZGVzZXJpYWxpemUgZ2VvbWV0cnkgKyBydW4gUzIgcHJlZGljYXRlPC90ZXh0PgogIDx0ZXh0IGNsYXNzPSJ0cyIgeD0iNjAiIHk9IjUwNCI+PHRzcGFuIGZvbnQtd2VpZ2h0PSI2MDAiPk4tZ3JhbTwvdHNwYW4+ICBwaGFzZSAxOiBjb3VudCB0ZXJtIGhpdHMgwrcgcGhhc2UgMjogZGVjb2RlIHBvc2l0aW9uIGxpc3RzICsgd2FsayBzZXF1ZW5jZTwvdGV4dD4KICA8dGV4dCBjbGFzcz0idHMiIHg9IjYwIiB5PSI1MjQiPjx0c3BhbiBmb250LXdlaWdodD0iNjAwIj5OZXN0ZWQ8L3RzcGFuPiAgcGhhc2UgMTogY2hlY2sgcGFyZW50IGV4aXN0cyDCtyBwaGFzZSAyOiBTZWVrIHRocm91Z2ggcG90ZW50aWFsbHkgbWFueSBjaGlsZCBkb2NzPC90ZXh0PgogIDx0ZXh0IGNsYXNzPSJ0cyIgeD0iMzQwIiB5PSI1NDgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIG9wYWNpdHk9IjAuNTUiPkxhenlTZWVrIG9uIHBoYXNlIDEgbWVhbnMgcGhhc2UgMiBuZXZlciBmaXJlcyBmb3Igbm9uLW1hdGNoaW5nIGRvY3VtZW50cy48L3RleHQ+CiAgPC9zdmc+Cg==" width="680" height="580" class="img_ev3q"></p>
<p style="font-size:12px;opacity:0.7"><em>Fig. 2 — The exact check differs per query type, but the gating is always the same: a cheap LazySeek confirms the document before any expensive I/O is attempted.</em></p>
<p>The cost asymmetry is what makes the pattern so valuable. A posting list bit check costs nanoseconds. Deserializing a stored geometry, decoding a position stream, or scanning a range of child documents can cost microseconds or more. Across millions of candidate documents the difference compounds dramatically.</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="2-conjunction-and--intersection">2. Conjunction (AND / Intersection)<a href="https://serenedb.com/blog/iresearch-two-phase-queries#2-conjunction-and--intersection" class="hash-link" aria-label="Direct link to 2. Conjunction (AND / Intersection)" title="Direct link to 2. Conjunction (AND / Intersection)">​</a></h4>
<p>Conjunction is where <code>LazySeek</code> earns most of its keep. The lead iterator (sorted by cost, cheapest first) drives forward with full <code>Seek</code> calls. All other iterators only need to confirm they are at the same document. If any follower returns a value greater than the target, the conjunction immediately short-circuits and the lead jumps forward without checking any remaining followers.</p>
<p><img decoding="async" loading="lazy" alt="Fig. 3 — In step 1, follower B misses and short-circuits the loop. The lead jumps to 31. In step 2, both followers confirm. Total follower work: two LazySeek calls, zero forward scans." src="data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNjgwIDQzMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgZGF0YS1maWd1cmUtYmFja2dyb3VuZD0id2hpdGUiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9IiNmZmYiLz4KPHN0eWxlPjwhW0NEQVRBWwoudCB7IGZvbnQ6IDQwMCAxNHB4IC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgJ1NlZ29lIFVJJywgc2Fucy1zZXJpZjsgZmlsbDogIzFhMWExYTsgfQoudHMgeyBmb250OiA0MDAgMTJweCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICdTZWdvZSBVSScsIHNhbnMtc2VyaWY7IGZpbGw6ICM1NTU7IH0KLnRoIHsgZm9udDogNjAwIDE0cHggLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAnU2Vnb2UgVUknLCBzYW5zLXNlcmlmOyBmaWxsOiAjMWExYTFhOyB9Ci5jLWdyYXkgPiByZWN0IHsgZmlsbDogI2YxZWZlODsgc3Ryb2tlOiAjODg4NzgwOyBzdHJva2Utd2lkdGg6IDAuNTsgfQouYy1ncmF5ID4gdGV4dCB7IGZpbGw6ICM0NDQ0NDE7IH0KLmMtYmx1ZSA+IHJlY3QgeyBmaWxsOiAjZTZmMWZiOyBzdHJva2U6ICMzNzhhZGQ7IHN0cm9rZS13aWR0aDogMC41OyB9Ci5jLWJsdWUgPiB0ZXh0IHsgZmlsbDogIzBjNDQ3YzsgfQouYy10ZWFsID4gcmVjdCB7IGZpbGw6ICNlMWY1ZWU7IHN0cm9rZTogIzFkOWU3NTsgc3Ryb2tlLXdpZHRoOiAwLjU7IH0KLmMtdGVhbCA+IHRleHQgeyBmaWxsOiAjMDg1MDQxOyB9Ci5jLWNvcmFsID4gcmVjdCB7IGZpbGw6ICNmYWVjZTc7IHN0cm9rZTogI2Q4NWEzMDsgc3Ryb2tlLXdpZHRoOiAwLjU7IH0KLmMtY29yYWwgPiB0ZXh0IHsgZmlsbDogIzcxMmIxMzsgfQouYy1wdXJwbGUgPiByZWN0IHsgZmlsbDogI2VlZWRmZTsgc3Ryb2tlOiAjN2Y3N2RkOyBzdHJva2Utd2lkdGg6IDAuNTsgfQouYy1wdXJwbGUgPiB0ZXh0IHsgZmlsbDogIzNjMzQ4OTsgfQouYy1hbWJlciA+IHJlY3QgeyBmaWxsOiAjZmFlZWRhOyBzdHJva2U6ICNiYTc1MTc7IHN0cm9rZS13aWR0aDogMC41OyB9Ci5jLWFtYmVyID4gdGV4dCB7IGZpbGw6ICM2MzM4MDY7IH0KcmVjdC5zZWN0aW9uLWJnIHsgZmlsbDogI2Y2ZjVmMDsgc3Ryb2tlOiAjZGRkOyBzdHJva2Utd2lkdGg6IDAuNTsgfQpyZWN0Lmluc2lnaHQtYmcgeyBmaWxsOiAjZjBlZGU0OyBzdHJva2U6ICNjY2M7IHN0cm9rZS13aWR0aDogMC41OyB9Cl1dPjwvc3R5bGU+CiAgPGRlZnM+PG1hcmtlciBpZD0iYTIiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iOCIgcmVmWT0iNSIgbWFya2VyV2lkdGg9IjYiIG1hcmtlckhlaWdodD0iNiIgb3JpZW50PSJhdXRvLXN0YXJ0LXJldmVyc2UiPjxwYXRoIGQ9Ik0yIDFMOCA1TDIgOSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJjb250ZXh0LXN0cm9rZSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvbWFya2VyPjwvZGVmcz4KICA8dGV4dCBjbGFzcz0idGgiIHg9IjM0MCIgeT0iMjgiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkNvbmp1bmN0aW9uIGNvbnZlcmdlIGxvb3Agd2l0aCBMYXp5U2VlazwvdGV4dD4KICA8dGV4dCBjbGFzcz0idHMiIHg9IjEzNiIgeT0iNTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiPmxlYWQgaXRlcmF0b3I8L3RleHQ+CiAgPHRleHQgY2xhc3M9InRzIiB4PSIxMzYiIHk9IjY0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBvcGFjaXR5PSIwLjUiPihjaGVhcGVzdCwgZHJpdmVzKTwvdGV4dD4KICA8dGV4dCBjbGFzcz0idHMiIHg9IjMzNiIgeT0iNTgiIHRleHQtYW5jaG9yPSJtaWRkbGUiPmZvbGxvd2VyIEE8L3RleHQ+CiAgPHRleHQgY2xhc3M9InRzIiB4PSI1MzYiIHk9IjU4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5mb2xsb3dlciBCPC90ZXh0PgogIDxsaW5lIHgxPSIyNDAiIHkxPSI0NCIgeDI9IjI0MCIgeTI9IjM5MCIgc3Ryb2tlPSIjZGRkIiBzdHJva2Utd2lkdGg9IjAuNSIgc3Ryb2tlLWRhc2hhcnJheT0iMyAzIi8+CiAgPGxpbmUgeDE9IjQ0MCIgeTE9IjQ0IiB4Mj0iNDQwIiB5Mj0iMzkwIiBzdHJva2U9IiNkZGQiIHN0cm9rZS13aWR0aD0iMC41IiBzdHJva2UtZGFzaGFycmF5PSIzIDMiLz4KICA8bGluZSB4MT0iNDQiIHkxPSI4MCIgeDI9IjYzNiIgeTI9IjgwIiBzdHJva2U9IiNkZGQiIHN0cm9rZS13aWR0aD0iMC41Ii8+CiAgPHRleHQgY2xhc3M9InRzIiB4PSIyMiIgeT0iMTA4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBvcGFjaXR5PSIwLjQ1Ij4xPC90ZXh0PgogIDxnIGNsYXNzPSJjLXB1cnBsZSI+PHJlY3QgeD0iNTIiIHk9Ijg2IiB3aWR0aD0iMTY4IiBoZWlnaHQ9IjQ0IiByeD0iNiIvPjx0ZXh0IGNsYXNzPSJ0aCIgeD0iMTM2IiB5PSIxMDQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5sZWFkLlNlZWsoMjQpIOKGkiAyNDwvdGV4dD48dGV4dCBjbGFzcz0idHMiIHg9IjEzNiIgeT0iMTIwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+ZnVsbCBTZWVrLCBkcml2ZXMgYWxsPC90ZXh0PjwvZz4KICA8dGV4dCBjbGFzcz0idHMiIHg9IjMzNiIgeT0iOTYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNCQTc1MTciPnRhcmdldCA9IDI0PC90ZXh0PgogIDx0ZXh0IGNsYXNzPSJ0cyIgeD0iNTM2IiB5PSI5NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI0JBNzUxNyI+dGFyZ2V0ID0gMjQ8L3RleHQ+CiAgPGcgY2xhc3M9ImMtdGVhbCI+PHJlY3QgeD0iMjUyIiB5PSI4NiIgd2lkdGg9IjE2OCIgaGVpZ2h0PSI0NCIgcng9IjYiLz48dGV4dCBjbGFzcz0idGgiIHg9IjMzNiIgeT0iMTA0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+QS5MYXp5U2VlaygyNCkg4oaSIDI0PC90ZXh0Pjx0ZXh0IGNsYXNzPSJ0cyIgeD0iMzM2IiB5PSIxMjAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5jb25maXJtZWQg4pyTPC90ZXh0PjwvZz4KICA8ZyBjbGFzcz0iYy1jb3JhbCI+PHJlY3QgeD0iNDUyIiB5PSI4NiIgd2lkdGg9IjE2OCIgaGVpZ2h0PSI0NCIgcng9IjYiLz48dGV4dCBjbGFzcz0idGgiIHg9IjUzNiIgeT0iMTA0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+Qi5MYXp5U2VlaygyNCkg4oaSIDMxPC90ZXh0Pjx0ZXh0IGNsYXNzPSJ0cyIgeD0iNTM2IiB5PSIxMjAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5taXNzIOKAlCByZXR1cm5zIDMxPC90ZXh0PjwvZz4KICA8cGF0aCBkPSJNNDUyIDExOCBMNDM2IDExOCBMNDM2IDE1NCBMMjIwIDE1NCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRDg1QTMwIiBzdHJva2Utd2lkdGg9IjEuMiIgc3Ryb2tlLWRhc2hhcnJheT0iNCAzIiBtYXJrZXItZW5kPSJ1cmwoI2EyKSIvPgogIDx0ZXh0IGNsYXNzPSJ0cyIgeD0iMzI4IiB5PSIxNDgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNEODVBMzAiPnNob3J0LWNpcmN1aXQ6IHJldHVybiAzMSwgU2VlayBsZWFkIHRvIDMxPC90ZXh0PgogIDxsaW5lIHgxPSI0NCIgeTE9IjE3MCIgeDI9IjYzNiIgeTI9IjE3MCIgc3Ryb2tlPSIjZGRkIiBzdHJva2Utd2lkdGg9IjAuNSIvPgogIDx0ZXh0IGNsYXNzPSJ0cyIgeD0iMjIiIHk9IjE5OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgb3BhY2l0eT0iMC40NSI+MjwvdGV4dD4KICA8ZyBjbGFzcz0iYy1wdXJwbGUiPjxyZWN0IHg9IjUyIiB5PSIxNzYiIHdpZHRoPSIxNjgiIGhlaWdodD0iNDQiIHJ4PSI2Ii8+PHRleHQgY2xhc3M9InRoIiB4PSIxMzYiIHk9IjE5NCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPmxlYWQuU2VlaygzMSkg4oaSIDMxPC90ZXh0Pjx0ZXh0IGNsYXNzPSJ0cyIgeD0iMTM2IiB5PSIyMTAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5qdW1wcyBmb3J3YXJkIHRvIDMxPC90ZXh0PjwvZz4KICA8dGV4dCBjbGFzcz0idHMiIHg9IjMzNiIgeT0iMTg2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjQkE3NTE3Ij50YXJnZXQgPSAzMTwvdGV4dD4KICA8dGV4dCBjbGFzcz0idHMiIHg9IjUzNiIgeT0iMTg2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjQkE3NTE3Ij50YXJnZXQgPSAzMTwvdGV4dD4KICA8ZyBjbGFzcz0iYy10ZWFsIj48cmVjdCB4PSIyNTIiIHk9IjE3NiIgd2lkdGg9IjE2OCIgaGVpZ2h0PSI0NCIgcng9IjYiLz48dGV4dCBjbGFzcz0idGgiIHg9IjMzNiIgeT0iMTk0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+QS5MYXp5U2VlaygzMSkg4oaSIDMxPC90ZXh0Pjx0ZXh0IGNsYXNzPSJ0cyIgeD0iMzM2IiB5PSIyMTAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5jb25maXJtZWQg4pyTPC90ZXh0PjwvZz4KICA8ZyBjbGFzcz0iYy10ZWFsIj48cmVjdCB4PSI0NTIiIHk9IjE3NiIgd2lkdGg9IjE2OCIgaGVpZ2h0PSI0NCIgcng9IjYiLz48dGV4dCBjbGFzcz0idGgiIHg9IjUzNiIgeT0iMTk0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+Qi5MYXp5U2VlaygzMSkg4oaSIDMxPC90ZXh0Pjx0ZXh0IGNsYXNzPSJ0cyIgeD0iNTM2IiB5PSIyMTAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5hbHJlYWR5IGF0IDMxIOKckzwvdGV4dD48L2c+CiAgPGxpbmUgeDE9IjQ0IiB5MT0iMjM2IiB4Mj0iNjM2IiB5Mj0iMjM2IiBzdHJva2U9IiNkZGQiIHN0cm9rZS13aWR0aD0iMC41Ii8+CiAgPGcgY2xhc3M9ImMtdGVhbCI+PHJlY3QgeD0iMjMwIiB5PSIyNDQiIHdpZHRoPSIyNjAiIGhlaWdodD0iNDQiIHJ4PSI4Ii8+PHRleHQgY2xhc3M9InRoIiB4PSIzNjAiIHk9IjI2MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPmNvbmp1bmN0aW9uIHJlc3VsdCA9IDMxPC90ZXh0Pjx0ZXh0IGNsYXNzPSJ0cyIgeD0iMzYwIiB5PSIyNzgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5hbGwgdGhyZWUgaXRlcmF0b3JzIGNvbmZpcm1lZCBhdCBkb2MgMzE8L3RleHQ+PC9nPgogIDxyZWN0IHg9IjQ0IiB5PSIzMDYiIHdpZHRoPSI1OTIiIGhlaWdodD0iMTEwIiByeD0iOCIgY2xhc3M9Imluc2lnaHQtYmciLz4KICA8dGV4dCBjbGFzcz0idGgiIHg9IjM0MCIgeT0iMzI2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5XaHkgTGF6eVNlZWsgbWF0dGVycyBoZXJlPC90ZXh0PgogIDx0ZXh0IGNsYXNzPSJ0cyIgeD0iMzQwIiB5PSIzNDgiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkZvbGxvd2VycyBvbmx5IGNvbmZpcm0gcHJlc2VuY2Ug4oCUIHRoZXkgbmV2ZXIgc2NhbiBmb3J3YXJkIHBhc3QgdGhlIHRhcmdldC48L3RleHQ+CiAgPHRleHQgY2xhc3M9InRzIiB4PSIzNDAiIHk9IjM2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+T25lIG1pc3MgaW1tZWRpYXRlbHkgc2hvcnQtY2lyY3VpdHMgd2l0aG91dCBjaGVja2luZyByZW1haW5pbmcgZm9sbG93ZXJzLjwvdGV4dD4KICA8dGV4dCBjbGFzcz0idHMiIHg9IjM0MCIgeT0iMzg0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5Pbmx5IHRoZSBsZWFkIGl0ZXJhdG9yIHBheXMgdGhlIGZ1bGwgY29zdCBvZiBmaW5kaW5nIHRoZSBuZXh0IGNhbmRpZGF0ZSBkb2MuPC90ZXh0PgogIDx0ZXh0IGNsYXNzPSJ0cyIgeD0iMzQwIiB5PSI0MDIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIG9wYWNpdHk9IjAuNSI+Rm9sbG93ZXJzIHNvcnRlZCBieSBlc3RpbWF0ZWQgY29zdCDigJQgY2hlYXBlc3QgaXRlcmF0b3JzIGNvbmZpcm0gZmlyc3QuPC90ZXh0PgogIDwvc3ZnPgo=" width="680" height="430" class="img_ev3q"></p>
<p style="font-size:12px;opacity:0.7"><em>Fig. 3 — In step 1, follower B misses and short-circuits the loop. The lead jumps to 31. In step 2, both followers confirm. Total follower work: two LazySeek calls, zero forward scans.</em></p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="3-exclusion-not--negation">3. Exclusion (NOT / Negation)<a href="https://serenedb.com/blog/iresearch-two-phase-queries#3-exclusion-not--negation" class="hash-link" aria-label="Direct link to 3. Exclusion (NOT / Negation)" title="Direct link to 3. Exclusion (NOT / Negation)">​</a></h4>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<p>In the exclusion iterator, the include side uses a full <code>Seek</code> to advance to the next candidate — it must commit to a definitive next document. Only the exclude side uses <code>LazySeek</code>, because it only needs to answer one question: "are you at this exact document?" If the include <code>Seek</code> advances past the target without landing on it, the exclude side is never touched at all.</p>
<p><img decoding="async" loading="lazy" alt="Fig. 4 — The include side uses Seek to advance definitively. The exclude side uses LazySeek for a cheap presence check, and is never called at all when the include side did not land exactly on a candidate." src="data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNjgwIDQzMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgZGF0YS1maWd1cmUtYmFja2dyb3VuZD0id2hpdGUiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9IiNmZmYiLz4KPHN0eWxlPjwhW0NEQVRBWwoudCB7IGZvbnQ6IDQwMCAxNHB4IC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgJ1NlZ29lIFVJJywgc2Fucy1zZXJpZjsgZmlsbDogIzFhMWExYTsgfQoudHMgeyBmb250OiA0MDAgMTJweCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICdTZWdvZSBVSScsIHNhbnMtc2VyaWY7IGZpbGw6ICM1NTU7IH0KLnRoIHsgZm9udDogNjAwIDE0cHggLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAnU2Vnb2UgVUknLCBzYW5zLXNlcmlmOyBmaWxsOiAjMWExYTFhOyB9Ci5jLWdyYXkgPiByZWN0IHsgZmlsbDogI2YxZWZlODsgc3Ryb2tlOiAjODg4NzgwOyBzdHJva2Utd2lkdGg6IDAuNTsgfQouYy1ncmF5ID4gdGV4dCB7IGZpbGw6ICM0NDQ0NDE7IH0KLmMtYmx1ZSA+IHJlY3QgeyBmaWxsOiAjZTZmMWZiOyBzdHJva2U6ICMzNzhhZGQ7IHN0cm9rZS13aWR0aDogMC41OyB9Ci5jLWJsdWUgPiB0ZXh0IHsgZmlsbDogIzBjNDQ3YzsgfQouYy10ZWFsID4gcmVjdCB7IGZpbGw6ICNlMWY1ZWU7IHN0cm9rZTogIzFkOWU3NTsgc3Ryb2tlLXdpZHRoOiAwLjU7IH0KLmMtdGVhbCA+IHRleHQgeyBmaWxsOiAjMDg1MDQxOyB9Ci5jLWNvcmFsID4gcmVjdCB7IGZpbGw6ICNmYWVjZTc7IHN0cm9rZTogI2Q4NWEzMDsgc3Ryb2tlLXdpZHRoOiAwLjU7IH0KLmMtY29yYWwgPiB0ZXh0IHsgZmlsbDogIzcxMmIxMzsgfQouYy1wdXJwbGUgPiByZWN0IHsgZmlsbDogI2VlZWRmZTsgc3Ryb2tlOiAjN2Y3N2RkOyBzdHJva2Utd2lkdGg6IDAuNTsgfQouYy1wdXJwbGUgPiB0ZXh0IHsgZmlsbDogIzNjMzQ4OTsgfQouYy1hbWJlciA+IHJlY3QgeyBmaWxsOiAjZmFlZWRhOyBzdHJva2U6ICNiYTc1MTc7IHN0cm9rZS13aWR0aDogMC41OyB9Ci5jLWFtYmVyID4gdGV4dCB7IGZpbGw6ICM2MzM4MDY7IH0KcmVjdC5zZWN0aW9uLWJnIHsgZmlsbDogI2Y2ZjVmMDsgc3Ryb2tlOiAjZGRkOyBzdHJva2Utd2lkdGg6IDAuNTsgfQpyZWN0Lmluc2lnaHQtYmcgeyBmaWxsOiAjZjBlZGU0OyBzdHJva2U6ICNjY2M7IHN0cm9rZS13aWR0aDogMC41OyB9Cl1dPjwvc3R5bGU+CiAgPGRlZnM+PG1hcmtlciBpZD0iYTMiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iOCIgcmVmWT0iNSIgbWFya2VyV2lkdGg9IjYiIG1hcmtlckhlaWdodD0iNiIgb3JpZW50PSJhdXRvLXN0YXJ0LXJldmVyc2UiPjxwYXRoIGQ9Ik0yIDFMOCA1TDIgOSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJjb250ZXh0LXN0cm9rZSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvbWFya2VyPjwvZGVmcz4KICA8dGV4dCBjbGFzcz0idGgiIHg9IjM0MCIgeT0iMjgiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkV4Y2x1c2lvbiBpdGVyYXRvcjogU2VlayBvbiBpbmNsdWRlLCBMYXp5U2VlayBvbiBleGNsdWRlPC90ZXh0PgogIDx0ZXh0IGNsYXNzPSJ0cyIgeD0iMTk2IiB5PSI1MiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+aW5jbHVkZSBpdGVyYXRvcjwvdGV4dD4KICA8dGV4dCBjbGFzcz0idHMiIHg9IjUxNiIgeT0iNTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiPmV4Y2x1ZGUgaXRlcmF0b3I8L3RleHQ+CiAgPGxpbmUgeDE9IjM1NiIgeTE9IjQ0IiB4Mj0iMzU2IiB5Mj0iMzkwIiBzdHJva2U9IiNkZGQiIHN0cm9rZS13aWR0aD0iMC41IiBzdHJva2UtZGFzaGFycmF5PSIzIDMiLz4KCiAgPHJlY3QgeD0iNDQiIHk9IjYyIiB3aWR0aD0iNTkyIiBoZWlnaHQ9Ijk2IiByeD0iOCIgY2xhc3M9InNlY3Rpb24tYmciLz4KICA8dGV4dCBjbGFzcz0idHMiIHg9IjYwIiB5PSI4MCIgb3BhY2l0eT0iMC41Ij5jYXNlIDEg4oCUIGluY2x1ZGUgYWR2YW5jZXMgcGFzdCB0YXJnZXQ6IGV4Y2x1ZGUgbmV2ZXIgdG91Y2hlZDwvdGV4dD4KICA8ZyBjbGFzcz0iYy1jb3JhbCI+PHJlY3QgeD0iNjAiIHk9Ijg4IiB3aWR0aD0iMjUyIiBoZWlnaHQ9IjQ0IiByeD0iNiIvPjx0ZXh0IGNsYXNzPSJ0aCIgeD0iMTg2IiB5PSIxMDYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5pbmNsLlNlZWsoMjApIOKGkiAyNDwvdGV4dD48dGV4dCBjbGFzcz0idHMiIHg9IjE4NiIgeT0iMTIyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+bm8gZG9jIDIwIOKAlCBhZHZhbmNlcyB0byAyNDwvdGV4dD48L2c+CiAgPHJlY3QgeD0iMzcyIiB5PSI4OCIgd2lkdGg9IjI1MiIgaGVpZ2h0PSI0NCIgcng9IjYiIGZpbGw9IiNmNmY1ZjAiIHN0cm9rZT0iI2RkZCIgc3Ryb2tlLXdpZHRoPSIwLjUiIG9wYWNpdHk9IjAuMzUiLz4KICA8dGV4dCBjbGFzcz0idHMiIHg9IjQ5OCIgeT0iMTEwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCIgb3BhY2l0eT0iMC4zIj5leGNsdWRlIGl0ZXJhdG9yPC90ZXh0PgogIDxsaW5lIHgxPSIzNzIiIHkxPSI4OCIgeDI9IjYyNCIgeTI9IjEzMiIgc3Ryb2tlPSIjY2NjIiBzdHJva2Utd2lkdGg9IjEuMiIgb3BhY2l0eT0iMC40Ii8+CiAgPGxpbmUgeDE9IjYyNCIgeTE9Ijg4IiB4Mj0iMzcyIiB5Mj0iMTMyIiBzdHJva2U9IiNjY2MiIHN0cm9rZS13aWR0aD0iMS4yIiBvcGFjaXR5PSIwLjQiLz4KICA8dGV4dCBjbGFzcz0idHMiIHg9IjQ5OCIgeT0iMTQ4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBvcGFjaXR5PSIwLjQiPm5ldmVyIGNhbGxlZDwvdGV4dD4KCiAgPHJlY3QgeD0iNDQiIHk9IjE3NCIgd2lkdGg9IjU5MiIgaGVpZ2h0PSI5NiIgcng9IjgiIGNsYXNzPSJzZWN0aW9uLWJnIi8+CiAgPHRleHQgY2xhc3M9InRzIiB4PSI2MCIgeT0iMTkyIiBvcGFjaXR5PSIwLjUiPmNhc2UgMiDigJQgaW5jbHVkZSBmaW5kcyBkb2M6IGNoZWNrIGV4Y2x1ZGUgd2l0aCBMYXp5U2VlayDihpIgcGFzc2VzPC90ZXh0PgogIDxnIGNsYXNzPSJjLXRlYWwiPjxyZWN0IHg9IjYwIiB5PSIyMDAiIHdpZHRoPSIyNTIiIGhlaWdodD0iNDQiIHJ4PSI2Ii8+PHRleHQgY2xhc3M9InRoIiB4PSIxODYiIHk9IjIxOCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPmluY2wuU2VlaygzMSkg4oaSIDMxPC90ZXh0Pjx0ZXh0IGNsYXNzPSJ0cyIgeD0iMTg2IiB5PSIyMzQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5kb2MgMzEgZm91bmQg4pyTPC90ZXh0PjwvZz4KICA8cGF0aCBkPSJNMzEyIDIyMiBMMzcyIDIyMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjODg4IiBzdHJva2Utd2lkdGg9IjEiIG1hcmtlci1lbmQ9InVybCgjYTMpIi8+CiAgPGcgY2xhc3M9ImMtdGVhbCI+PHJlY3QgeD0iMzcyIiB5PSIyMDAiIHdpZHRoPSIyNTIiIGhlaWdodD0iNDQiIHJ4PSI2Ii8+PHRleHQgY2xhc3M9InRoIiB4PSI0OTgiIHk9IjIxOCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPmV4Y2wuTGF6eVNlZWsoMzEpIOKGkiAzODwvdGV4dD48dGV4dCBjbGFzcz0idHMiIHg9IjQ5OCIgeT0iMjM0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+Mzgg4omgIDMxIOKGkiBub3QgZXhjbHVkZWQg4oaSIHBhc3Mg4pyTPC90ZXh0PjwvZz4KCiAgPHJlY3QgeD0iNDQiIHk9IjI4NiIgd2lkdGg9IjU5MiIgaGVpZ2h0PSI5NiIgcng9IjgiIGNsYXNzPSJzZWN0aW9uLWJnIi8+CiAgPHRleHQgY2xhc3M9InRzIiB4PSI2MCIgeT0iMzA0IiBvcGFjaXR5PSIwLjUiPmNhc2UgMyDigJQgaW5jbHVkZSBmaW5kcyBkb2M6IGNoZWNrIGV4Y2x1ZGUgd2l0aCBMYXp5U2VlayDihpIgZXhjbHVkZWQ8L3RleHQ+CiAgPGcgY2xhc3M9ImMtdGVhbCI+PHJlY3QgeD0iNjAiIHk9IjMxMiIgd2lkdGg9IjI1MiIgaGVpZ2h0PSI0NCIgcng9IjYiLz48dGV4dCBjbGFzcz0idGgiIHg9IjE4NiIgeT0iMzMwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+aW5jbC5TZWVrKDM4KSDihpIgMzg8L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSIxODYiIHk9IjM0NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPmRvYyAzOCBmb3VuZCDinJM8L3RleHQ+PC9nPgogIDxwYXRoIGQ9Ik0zMTIgMzM0IEwzNzIgMzM0IiBmaWxsPSJub25lIiBzdHJva2U9IiM4ODgiIHN0cm9rZS13aWR0aD0iMSIgbWFya2VyLWVuZD0idXJsKCNhMykiLz4KICA8ZyBjbGFzcz0iYy1jb3JhbCI+PHJlY3QgeD0iMzcyIiB5PSIzMTIiIHdpZHRoPSIyNTIiIGhlaWdodD0iNDQiIHJ4PSI2Ii8+PHRleHQgY2xhc3M9InRoIiB4PSI0OTgiIHk9IjMzMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPmV4Y2wuTGF6eVNlZWsoMzgpIOKGkiAzODwvdGV4dD48dGV4dCBjbGFzcz0idHMiIHg9IjQ5OCIgeT0iMzQ2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+MzggPSAzOCDihpIgZXhjbHVkZWQg4oaSIGFkdmFuY2UoKTwvdGV4dD48L2c+CgogIDx0ZXh0IGNsYXNzPSJ0cyIgeD0iMzQwIiB5PSI0MDYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIG9wYWNpdHk9IjAuNiI+SW5jbHVkZSB1c2VzIFNlZWsg4oCUIGl0IG11c3QgY29tbWl0IHRvIHRoZSBuZXh0IHJlYWwgZG9jdW1lbnQuPC90ZXh0PgogIDx0ZXh0IGNsYXNzPSJ0cyIgeD0iMzQwIiB5PSI0MjIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIG9wYWNpdHk9IjAuNiI+RXhjbHVkZSB1c2VzIExhenlTZWVrIOKAlCBhIGNoZWFwIHByZXNlbmNlIGNoZWNrLCBjYWxsZWQgb25seSB3aGVuIGluY2x1ZGUgc3VjY2VlZHMuPC90ZXh0PgogIDwvc3ZnPgo=" width="680" height="430" class="img_ev3q"></p>
<p style="font-size:12px;opacity:0.7"><em>Fig. 4 — The include side uses Seek to advance definitively. The exclude side uses LazySeek for a cheap presence check, and is never called at all when the include side did not land exactly on a candidate.</em></p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="4-required--optional-must--should">4. Required + Optional (MUST / SHOULD)<a href="https://serenedb.com/blog/iresearch-two-phase-queries#4-required--optional-must--should" class="hash-link" aria-label="Direct link to 4. Required + Optional (MUST / SHOULD)" title="Direct link to 4. Required + Optional (MUST / SHOULD)">​</a></h4>
<p>A common boolean query pattern is MUST + SHOULD: a required clause that must match, plus optional clauses that boost the score if they also match. <a href="https://github.com/serenedb/serenedb/blob/main/libs/iresearch/include/iresearch/search/boost_iterator.hpp" target="_blank" rel="noopener noreferrer"><code>BoostIterator</code></a> implements this. The required side drives iteration with <code>advance()</code> and <code>Seek</code> — it must find the next real document. The optional sides use <code>LazySeek</code> to check whether they happen to be at the same document: if <code>opt.LazySeek(doc) == doc</code>, the optional clause matched and contributes its score boost; if it returned something larger, the optional clause missed this document and is simply skipped. The optional iterators never need to find the <em>next</em> document, they only need to answer "are you here right now?"</p>
<p><img decoding="async" loading="lazy" alt="Fig. 5 — The required iterator drives via Seek. The optional iterator uses LazySeek purely to decide whether to add a score boost — it never advances the iteration." src="data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNjgwIDM3MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgZGF0YS1maWd1cmUtYmFja2dyb3VuZD0id2hpdGUiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9IiNmZmYiLz4KPHN0eWxlPjwhW0NEQVRBWwoudCB7IGZvbnQ6IDQwMCAxNHB4IC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgJ1NlZ29lIFVJJywgc2Fucy1zZXJpZjsgZmlsbDogIzFhMWExYTsgfQoudHMgeyBmb250OiA0MDAgMTJweCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICdTZWdvZSBVSScsIHNhbnMtc2VyaWY7IGZpbGw6ICM1NTU7IH0KLnRoIHsgZm9udDogNjAwIDE0cHggLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAnU2Vnb2UgVUknLCBzYW5zLXNlcmlmOyBmaWxsOiAjMWExYTFhOyB9Ci5jLWdyYXkgPiByZWN0IHsgZmlsbDogI2YxZWZlODsgc3Ryb2tlOiAjODg4NzgwOyBzdHJva2Utd2lkdGg6IDAuNTsgfQouYy1ncmF5ID4gdGV4dCB7IGZpbGw6ICM0NDQ0NDE7IH0KLmMtYmx1ZSA+IHJlY3QgeyBmaWxsOiAjZTZmMWZiOyBzdHJva2U6ICMzNzhhZGQ7IHN0cm9rZS13aWR0aDogMC41OyB9Ci5jLWJsdWUgPiB0ZXh0IHsgZmlsbDogIzBjNDQ3YzsgfQouYy10ZWFsID4gcmVjdCB7IGZpbGw6ICNlMWY1ZWU7IHN0cm9rZTogIzFkOWU3NTsgc3Ryb2tlLXdpZHRoOiAwLjU7IH0KLmMtdGVhbCA+IHRleHQgeyBmaWxsOiAjMDg1MDQxOyB9Ci5jLWNvcmFsID4gcmVjdCB7IGZpbGw6ICNmYWVjZTc7IHN0cm9rZTogI2Q4NWEzMDsgc3Ryb2tlLXdpZHRoOiAwLjU7IH0KLmMtY29yYWwgPiB0ZXh0IHsgZmlsbDogIzcxMmIxMzsgfQouYy1wdXJwbGUgPiByZWN0IHsgZmlsbDogI2VlZWRmZTsgc3Ryb2tlOiAjN2Y3N2RkOyBzdHJva2Utd2lkdGg6IDAuNTsgfQouYy1wdXJwbGUgPiB0ZXh0IHsgZmlsbDogIzNjMzQ4OTsgfQouYy1hbWJlciA+IHJlY3QgeyBmaWxsOiAjZmFlZWRhOyBzdHJva2U6ICNiYTc1MTc7IHN0cm9rZS13aWR0aDogMC41OyB9Ci5jLWFtYmVyID4gdGV4dCB7IGZpbGw6ICM2MzM4MDY7IH0KcmVjdC5zZWN0aW9uLWJnIHsgZmlsbDogI2Y2ZjVmMDsgc3Ryb2tlOiAjZGRkOyBzdHJva2Utd2lkdGg6IDAuNTsgfQpyZWN0Lmluc2lnaHQtYmcgeyBmaWxsOiAjZjBlZGU0OyBzdHJva2U6ICNjY2M7IHN0cm9rZS13aWR0aDogMC41OyB9Cl1dPjwvc3R5bGU+CiAgPGRlZnM+PG1hcmtlciBpZD0iYTkiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iOCIgcmVmWT0iNSIgbWFya2VyV2lkdGg9IjYiIG1hcmtlckhlaWdodD0iNiIgb3JpZW50PSJhdXRvLXN0YXJ0LXJldmVyc2UiPjxwYXRoIGQ9Ik0yIDFMOCA1TDIgOSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJjb250ZXh0LXN0cm9rZSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvbWFya2VyPjwvZGVmcz4KICA8dGV4dCBjbGFzcz0idGgiIHg9IjM0MCIgeT0iMjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlJlcXVpcmVkICsgT3B0aW9uYWwgKE1VU1QgLyBTSE9VTEQpPC90ZXh0PgogIDx0ZXh0IGNsYXNzPSJ0cyIgeD0iMTMwIiB5PSI0OCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+cmVxdWlyZWQgaXRlcmF0b3I8L3RleHQ+CiAgPHRleHQgY2xhc3M9InRzIiB4PSIxMzAiIHk9IjYwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBvcGFjaXR5PSIwLjUiPihkcml2ZXMgaXRlcmF0aW9uKTwvdGV4dD4KICA8dGV4dCBjbGFzcz0idHMiIHg9IjQwMCIgeT0iNDgiIHRleHQtYW5jaG9yPSJtaWRkbGUiPm9wdGlvbmFsIGl0ZXJhdG9yPC90ZXh0PgogIDx0ZXh0IGNsYXNzPSJ0cyIgeD0iNDAwIiB5PSI2MCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgb3BhY2l0eT0iMC41Ij4oc2NvcmUgYm9vc3Qgb25seSk8L3RleHQ+CiAgPGxpbmUgeDE9IjI2NSIgeTE9IjM4IiB4Mj0iMjY1IiB5Mj0iMjgwIiBzdHJva2U9IiNkZGQiIHN0cm9rZS13aWR0aD0iMC41IiBzdHJva2UtZGFzaGFycmF5PSIzIDMiLz4KICA8bGluZSB4MT0iNDQiIHkxPSI3NCIgeDI9IjYzNiIgeTI9Ijc0IiBzdHJva2U9IiNkZGQiIHN0cm9rZS13aWR0aD0iMC41Ii8+CiAgPHRleHQgY2xhc3M9InRzIiB4PSIyNCIgeT0iMTAyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBvcGFjaXR5PSIwLjQiPjE8L3RleHQ+CiAgPGcgY2xhc3M9ImMtcHVycGxlIj48cmVjdCB4PSI0NCIgeT0iODAiIHdpZHRoPSIyMDAiIGhlaWdodD0iNDQiIHJ4PSI2Ii8+PHRleHQgY2xhc3M9InRoIiB4PSIxNDQiIHk9Ijk4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+cmVxLmFkdmFuY2UoKSDihpIgZG9jIDMxPC90ZXh0Pjx0ZXh0IGNsYXNzPSJ0cyIgeD0iMTQ0IiB5PSIxMTQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5TZWVrIHRvIG5leHQgcmVhbCBkb2M8L3RleHQ+PC9nPgogIDxwYXRoIGQ9Ik0yNDQgMTAyIEwyODAgMTAyIiBmaWxsPSJub25lIiBzdHJva2U9IiM4ODgiIHN0cm9rZS13aWR0aD0iMSIgbWFya2VyLWVuZD0idXJsKCNhOSkiLz4KICA8ZyBjbGFzcz0iYy10ZWFsIj48cmVjdCB4PSIyODAiIHk9IjgwIiB3aWR0aD0iMzUwIiBoZWlnaHQ9IjQ0IiByeD0iNiIvPjx0ZXh0IGNsYXNzPSJ0aCIgeD0iNDU1IiB5PSI5OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPm9wdC5MYXp5U2VlaygzMSkg4oaSIDMxPC90ZXh0Pjx0ZXh0IGNsYXNzPSJ0cyIgeD0iNDU1IiB5PSIxMTQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5wcmVzZW50IOKGkiBhZGQgc2NvcmUgYm9vc3Qg4pyTPC90ZXh0PjwvZz4KICA8bGluZSB4MT0iNDQiIHkxPSIxNDAiIHgyPSI2MzYiIHkyPSIxNDAiIHN0cm9rZT0iI2RkZCIgc3Ryb2tlLXdpZHRoPSIwLjUiLz4KICA8dGV4dCBjbGFzcz0idHMiIHg9IjI0IiB5PSIxNjgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIG9wYWNpdHk9IjAuNCI+MjwvdGV4dD4KICA8ZyBjbGFzcz0iYy1wdXJwbGUiPjxyZWN0IHg9IjQ0IiB5PSIxNDYiIHdpZHRoPSIyMDAiIGhlaWdodD0iNDQiIHJ4PSI2Ii8+PHRleHQgY2xhc3M9InRoIiB4PSIxNDQiIHk9IjE2NCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPnJlcS5hZHZhbmNlKCkg4oaSIGRvYyA0NTwvdGV4dD48dGV4dCBjbGFzcz0idHMiIHg9IjE0NCIgeT0iMTgwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+U2VlayB0byBuZXh0IHJlYWwgZG9jPC90ZXh0PjwvZz4KICA8cGF0aCBkPSJNMjQ0IDE2OCBMMjgwIDE2OCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjODg4IiBzdHJva2Utd2lkdGg9IjEiIG1hcmtlci1lbmQ9InVybCgjYTkpIi8+CiAgPGcgY2xhc3M9ImMtY29yYWwiPjxyZWN0IHg9IjI4MCIgeT0iMTQ2IiB3aWR0aD0iMzUwIiBoZWlnaHQ9IjQ0IiByeD0iNiIvPjx0ZXh0IGNsYXNzPSJ0aCIgeD0iNDU1IiB5PSIxNjQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5vcHQuTGF6eVNlZWsoNDUpIOKGkiA2MTwvdGV4dD48dGV4dCBjbGFzcz0idHMiIHg9IjQ1NSIgeT0iMTgwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+bWlzcyDigJQgc2tpcCwgbm8gYm9vc3Q8L3RleHQ+PC9nPgogIDxsaW5lIHgxPSI0NCIgeTE9IjIwNiIgeDI9IjYzNiIgeTI9IjIwNiIgc3Ryb2tlPSIjZGRkIiBzdHJva2Utd2lkdGg9IjAuNSIvPgogIDx0ZXh0IGNsYXNzPSJ0cyIgeD0iMjQiIHk9IjIzNCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgb3BhY2l0eT0iMC40Ij4zPC90ZXh0PgogIDxnIGNsYXNzPSJjLXB1cnBsZSI+PHJlY3QgeD0iNDQiIHk9IjIxMiIgd2lkdGg9IjIwMCIgaGVpZ2h0PSI0NCIgcng9IjYiLz48dGV4dCBjbGFzcz0idGgiIHg9IjE0NCIgeT0iMjMwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+cmVxLmFkdmFuY2UoKSDihpIgZG9jIDYxPC90ZXh0Pjx0ZXh0IGNsYXNzPSJ0cyIgeD0iMTQ0IiB5PSIyNDYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJjZW50cmFsIj5TZWVrIHRvIG5leHQgcmVhbCBkb2M8L3RleHQ+PC9nPgogIDxwYXRoIGQ9Ik0yNDQgMjM0IEwyODAgMjM0IiBmaWxsPSJub25lIiBzdHJva2U9IiM4ODgiIHN0cm9rZS13aWR0aD0iMSIgbWFya2VyLWVuZD0idXJsKCNhOSkiLz4KICA8ZyBjbGFzcz0iYy10ZWFsIj48cmVjdCB4PSIyODAiIHk9IjIxMiIgd2lkdGg9IjM1MCIgaGVpZ2h0PSI0NCIgcng9IjYiLz48dGV4dCBjbGFzcz0idGgiIHg9IjQ1NSIgeT0iMjMwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0iY2VudHJhbCI+b3B0LkxhenlTZWVrKDYxKSDihpIgNjE8L3RleHQ+PHRleHQgY2xhc3M9InRzIiB4PSI0NTUiIHk9IjI0NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPmFscmVhZHkgYXQgNjEg4oaSIGFkZCBzY29yZSBib29zdCDinJM8L3RleHQ+PC9nPgogIDxyZWN0IHg9IjQ0IiB5PSIyNzQiIHdpZHRoPSI1OTIiIGhlaWdodD0iNzYiIHJ4PSI4IiBjbGFzcz0iaW5zaWdodC1iZyIvPgogIDx0ZXh0IGNsYXNzPSJ0aCIgeD0iMzQwIiB5PSIyOTYiIHRleHQtYW5jaG9yPSJtaWRkbGUiPktleSBhc3ltbWV0cnk8L3RleHQ+CiAgPHRleHQgY2xhc3M9InRzIiB4PSIzNDAiIHk9IjMxNiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+UmVxdWlyZWQgdXNlcyBTZWVrIOKAlCBpdCBtdXN0IGZpbmQgdGhlIG5leHQgcmVhbCBkb2N1bWVudC48L3RleHQ+CiAgPHRleHQgY2xhc3M9InRzIiB4PSIzNDAiIHk9IjMzNCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+T3B0aW9uYWwgdXNlcyBMYXp5U2VlayDigJQgb25seSDigJxhcmUgeW91IGhlcmU/4oCdIE5ldmVyIGRyaXZlcyBmb3J3YXJkLiBOZXZlciBwYXlzIGZvciBhIG1pc3MuPC90ZXh0PgogIDwvc3ZnPgo=" width="680" height="370" class="img_ev3q"></p>
<p style="font-size:12px;opacity:0.7"><em>Fig. 5 — The required iterator drives via Seek. The optional iterator uses LazySeek purely to decide whether to add a score boost — it never advances the iteration.</em></p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="5-a-complex-example">5. A Complex Example<a href="https://serenedb.com/blog/iresearch-two-phase-queries#5-a-complex-example" class="hash-link" aria-label="Direct link to 5. A Complex Example" title="Direct link to 5. A Complex Example">​</a></h4>
<p>Consider the query <code>"quick brown fox" AND NOT "lazy dog"</code>. The full iterator tree, and how laziness flows through every layer:</p>
<p><img decoding="async" loading="lazy" alt="Fig. 6 — The complete query tree. The include branch uses Seek; the exclude branch is only consulted if the include side produces a match, and uses LazySeek throughout." src="https://serenedb.com/blog/assets/images/figure-08-full-query-tree-4c2509cb4a82ae661c16575cd556e92b.svg" width="680" height="480" class="img_ev3q"></p>
<p style="font-size:12px;opacity:0.7"><em>Fig. 6 — The complete query tree. The include branch uses Seek; the exclude branch is only consulted if the include side produces a match, and uses LazySeek throughout.</em></p>
<p>At each step, work is deferred until strictly necessary. Position data is never decoded unless the document-level conjunction confirms all terms are present. The exclude side is never consulted unless the include side finds a candidate.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="results-why-this-matters-for-the-benchmark">Results: Why This Matters for the Benchmark<a href="https://serenedb.com/blog/iresearch-two-phase-queries#results-why-this-matters-for-the-benchmark" class="hash-link" aria-label="Direct link to Results: Why This Matters for the Benchmark" title="Direct link to Results: Why This Matters for the Benchmark">​</a></h3>
<p>The <a href="https://serenedb.com/search-benchmark-game" target="_blank" rel="noopener noreferrer">search-benchmark-game</a> runs over the English Wikipedia corpus with queries from the AOL dataset, measuring throughput on phrase queries, conjunctions, disjunctions and count-only variants. Phrase queries exercise every layer of the lazy evaluation stack simultaneously:</p>
<ul>
<li>The conjunction approximation filters documents cheaply before any position I/O;</li>
<li><code>LazySeek</code> in the conjunction loop avoids full seeks on non-lead posting lists;</li>
<li>Position intersection via frequency-sorted term positions;</li>
<li>For count-only phrase queries, the loop exits after the first position match without full position enumeration.</li>
</ul>
<p>These savings compound. The performance gap over systems that enumerate all positions or always perform full <code>Seek</code>s widens on queries with common terms, long documents and dense posting lists — exactly the conditions the Wikipedia corpus creates.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="whats-next">What's Next<a href="https://serenedb.com/blog/iresearch-two-phase-queries#whats-next" class="hash-link" aria-label="Direct link to What's Next" title="Direct link to What's Next">​</a></h3>
<p>This post covered the lazy evaluation layer — the first line of defense against unnecessary work at query execution time. The next post in the <strong>Search optimization journey</strong> series will examine adaptive posting formats: how posting lists can choose layouts that fit both dense and sparse workloads without forcing every query through the same representation.</p>
<p><strong>Other articles in the Search optimization journey series:</strong></p>
<ul>
<li><a href="https://serenedb.com/blog/search-benchmark-game-overview">Benchmark overview</a></li>
<li><a href="https://serenedb.com/blog/search-optimization-1">Search optimization journey 1: Collecting top-K candidates</a></li>
<li><a href="https://serenedb.com/blog/search-optimization-2">Search optimization journey 2: Block scoring</a></li>
<li><a href="https://serenedb.com/blog/norm-gathering">Search optimization journey 3: Optimize norm gathering</a></li>
<li><a href="https://serenedb.com/blog/search-optimization-5">Search optimization journey 5: Adaptive posting list format</a></li>
</ul>
<hr>
<p style="font-size:14px;opacity:0.7">IResearch is open source (Apache 2.0) and available as part of <a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer">SereneDB</a>. The benchmark results referenced here can be reproduced using the <a href="https://github.com/quickwit-oss/search-benchmark-game" target="_blank" rel="noopener noreferrer">search-benchmark-game</a> framework.</p>
<p style="font-size:14px;opacity:0.7">If you find this work interesting, we would appreciate your support — star us on <a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer">GitHub</a> goes a long way for an early-stage project.</p></div>]]></content>
        <author>
            <name>Andrey Abramov</name>
            <uri>https://www.linkedin.com/in/andrey-abramov-57a16241/ru/?skipRedirect=true</uri>
        </author>
        <category label="Search" term="Search"/>
        <category label="Dev" term="Dev"/>
        <category label="Search optimization journey" term="Search optimization journey"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Let compiler cook!]]></title>
        <id>https://serenedb.com/blog/simd-distances</id>
        <link href="https://serenedb.com/blog/simd-distances"/>
        <updated>2026-04-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Introduction]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="none" viewBox="0 0 28 28" class="logoAvatar_BINX"><path fill="#895AF8" d="M28 14c0-7.732-6.268-14-14-14S0 6.268 0 14s6.268 14 14 14 14-6.268 14-14"></path><path fill="#80BEFF" d="M9.345 18.655c4.556 4.556 11.416 5.084 15.32 1.178 3.906-3.905 3.379-10.764-1.178-15.32C18.931-.043 12.072-.571 8.167 3.334S4.789 14.1 9.345 18.654"></path><path fill="#fff" d="M15.649 12.35c2.278 2.278 5.676 2.574 7.59.66s1.617-5.311-.66-7.59c-2.279-2.277-5.677-2.573-7.59-.66-1.914 1.914-1.618 5.312.66 7.59"></path></svg></div></div><div class="copy_iHNG"><p class="names_ox2I">SereneDB Team</p><p class="meta_OgzX">Apr 10, 2026 · 12 minutes read</p></div></header><h2 class="title_RQ9v">Let compiler cook!</h2><p style="font-size:0.9rem;margin-top:-0.4rem">Optimizing ANN search: How fast-math boosted our vector distances</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg" data-image-lightbox-root="true"><h2 class="anchor anchorWithStickyNavbar_LWe7" id="introduction"><strong>Introduction</strong><a href="https://serenedb.com/blog/simd-distances#introduction" class="hash-link" aria-label="Direct link to introduction" title="Direct link to introduction">​</a></h2>
<p>Sometimes, when we are thinking of writing an efficient implementation of some basic algorithm, we might go with hand-writing (or maybe claude-writing) SIMD intrinsics. Unfortunately, in some cases it isn't the best approach, and today we are going to have a brief talk about one such case.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="why-vector-distances-matter"><strong>Why vector distances matter</strong><a href="https://serenedb.com/blog/simd-distances#why-vector-distances-matter" class="hash-link" aria-label="Direct link to why-vector-distances-matter" title="Direct link to why-vector-distances-matter">​</a></h2>
<p>Approximate Nearest Neighbor (ANN) search — given a query vector, find the <em>k</em> most similar ones in a large dataset. No matter how smart the search algorithm is, it still boils down to computing a huge number of distances between vectors. That makes the distance function a hot spot: it is called so frequently that even a small speedup there translates directly into faster queries end-to-end.</p>
<p>SereneDB already has a fast full-text search engine<a href="https://serenedb.com/blog/simd-distances#textsearch">[5]</a>, and vector search is the next piece needed to enable hybrid search — combining keyword and semantic similarity in a single query.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="vector-distances-benchmarks"><strong>Vector distances Benchmarks</strong><a href="https://serenedb.com/blog/simd-distances#vector-distances-benchmarks" class="hash-link" aria-label="Direct link to vector-distances-benchmarks" title="Direct link to vector-distances-benchmarks">​</a></h2>
<p>There are four typical vector distance functions:</p>
<ul>
<li>L2 squared distance:</li>
</ul>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msubsup><mi>L</mi><mn>2</mn><mn>2</mn></msubsup><mo stretchy="false">(</mo><mi>x</mi><mo separator="true">,</mo><mi>y</mi><mo stretchy="false">)</mo><mo>=</mo><munder><mo>∑</mo><mi>i</mi></munder><mo stretchy="false">(</mo><msub><mi>x</mi><mi>i</mi></msub><mo>−</mo><msub><mi>y</mi><mi>i</mi></msub><msup><mo stretchy="false">)</mo><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">L_2^{2}(x, y) = \sum_i (x_i - y_i)^{2}
</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.1141em;vertical-align:-0.25em"></span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.8641em"><span style="top:-2.453em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">2</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.247em"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.03588em">y</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:2.3277em;vertical-align:-1.2777em"></span><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.05em"><span style="top:-1.8723em;margin-left:0em"><span class="pstrut" style="height:3.05em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span><span style="top:-3.05em"><span class="pstrut" style="height:3.05em"></span><span><span class="mop op-symbol large-op">∑</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.2777em"><span></span></span></span></span></span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal">x</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:1.1141em;vertical-align:-0.25em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em">y</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mclose"><span class="mclose">)</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em"><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span></span>
<ul>
<li>L1 distance:</li>
</ul>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>L</mi><mn>1</mn></msub><mo stretchy="false">(</mo><mi>x</mi><mo separator="true">,</mo><mi>y</mi><mo stretchy="false">)</mo><mo>=</mo><munder><mo>∑</mo><mi>i</mi></munder><mi mathvariant="normal">∣</mi><msub><mi>x</mi><mi>i</mi></msub><mo>−</mo><msub><mi>y</mi><mi>i</mi></msub><mi mathvariant="normal">∣</mi></mrow><annotation encoding="application/x-tex">L_1(x, y) = \sum_i |x_i - y_i|
</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.03588em">y</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:2.3277em;vertical-align:-1.2777em"></span><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.05em"><span style="top:-1.8723em;margin-left:0em"><span class="pstrut" style="height:3.05em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span><span style="top:-3.05em"><span class="pstrut" style="height:3.05em"></span><span><span class="mop op-symbol large-op">∑</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.2777em"><span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">∣</span><span class="mord"><span class="mord mathnormal">x</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em">y</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mord">∣</span></span></span></span></span>
<ul>
<li>Dot (Inner) product:</li>
</ul>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>D</mi><mi>o</mi><mi>t</mi><mo stretchy="false">(</mo><mi>x</mi><mo separator="true">,</mo><mi>y</mi><mo stretchy="false">)</mo><mo>=</mo><munder><mo>∑</mo><mi>i</mi></munder><msub><mi>x</mi><mi>i</mi></msub><mo>⋅</mo><msub><mi>y</mi><mi>i</mi></msub></mrow><annotation encoding="application/x-tex">Dot(x, y) = \sum_i x_i \cdot y_i
</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal">Do</span><span class="mord mathnormal">t</span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.03588em">y</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:2.3277em;vertical-align:-1.2777em"></span><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.05em"><span style="top:-1.8723em;margin-left:0em"><span class="pstrut" style="height:3.05em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span><span style="top:-3.05em"><span class="pstrut" style="height:3.05em"></span><span><span class="mop op-symbol large-op">∑</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.2777em"><span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord"><span class="mord mathnormal">x</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em">y</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span></span></span>
<ul>
<li>Cosine similarity:</li>
</ul>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>C</mi><mi>o</mi><mi>s</mi><mi>i</mi><mi>n</mi><mi>e</mi><mo stretchy="false">(</mo><mi>x</mi><mo separator="true">,</mo><mi>y</mi><mo stretchy="false">)</mo><mo>=</mo><mfrac><mrow><mi>D</mi><mi>o</mi><mi>t</mi><mo stretchy="false">(</mo><mi>x</mi><mo separator="true">,</mo><mi>y</mi><mo stretchy="false">)</mo></mrow><mrow><mi mathvariant="normal">∣</mi><mi mathvariant="normal">∣</mi><mi>x</mi><mi mathvariant="normal">∣</mi><mi mathvariant="normal">∣</mi><mo>⋅</mo><mi mathvariant="normal">∣</mi><mi mathvariant="normal">∣</mi><mi>y</mi><mi mathvariant="normal">∣</mi><mi mathvariant="normal">∣</mi></mrow></mfrac></mrow><annotation encoding="application/x-tex">Cosine(x, y) = \frac{Dot(x, y)}{||x|| \cdot ||y||}
</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal" style="margin-right:0.07153em">C</span><span class="mord mathnormal">os</span><span class="mord mathnormal">in</span><span class="mord mathnormal">e</span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.03588em">y</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:2.363em;vertical-align:-0.936em"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.427em"><span style="top:-2.314em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord">∣∣</span><span class="mord mathnormal">x</span><span class="mord">∣∣</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mord">∣∣</span><span class="mord mathnormal" style="margin-right:0.03588em">y</span><span class="mord">∣∣</span></span></span><span style="top:-3.23em"><span class="pstrut" style="height:3em"></span><span class="frac-line" style="border-bottom-width:0.04em"></span></span><span style="top:-3.677em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord mathnormal">Do</span><span class="mord mathnormal">t</span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal" style="margin-right:0.03588em">y</span><span class="mclose">)</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.936em"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span>
<p>We've added a benchmark to compare our implementations <a href="https://serenedb.com/blog/simd-distances#vectorimpl">[1]</a> with faiss. All benchmarks run on an AMD Ryzen 9 9950X (Zen 5, 16 cores, 48 KiB L1d per core, 1 MiB L2, 32 MiB L3):</p>
<img src="https://serenedb.com/blog/assets/images/distances_small-7c78c643db45c78052f017a8c8c40475.png">
<p>As you can see, dot product and L2 distance take the same time; however, cosine similarity and L1 distance are significantly slower in faiss. Even more, take a look at the SereneDB implementation of L1 distance and the implementation in faiss:</p>
<div class="comparisonRow_tVsp"><div class="comparisonColumn_Bhsi"><p><strong>serenedb:</strong></p><div class="language-cpp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cpp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">template</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">typename</span><span class="token plain"> </span><span class="token class-name">In</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">typename</span><span class="token plain"> </span><span class="token class-name">Abs</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">typename</span><span class="token plain"> </span><span class="token class-name">Out</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">struct</span><span class="token plain"> </span><span class="token class-name">L1Space</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">static_assert</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">std</span><span class="token double-colon punctuation" style="color:#393A34">::</span><span class="token plain">is_signed_v</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">Abs</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> Out </span><span class="token function" style="color:#d73a49">Dist</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> byte_type</span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> l</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> byte_type</span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">uint16_t</span><span class="token plain"> d</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Out s</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">uint16_t</span><span class="token plain"> i </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> i </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> d</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">++</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">auto</span><span class="token plain"> li </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token generic-function function" style="color:#d73a49">static_cast</span><span class="token generic-function generic class-name operator" style="color:#393A34">&lt;</span><span class="token generic-function generic class-name">Abs</span><span class="token generic-function generic class-name operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token generic-function function" style="color:#d73a49">reinterpret_cast</span><span class="token generic-function generic class-name operator" style="color:#393A34">&lt;</span><span class="token generic-function generic class-name keyword" style="color:#00009f">const</span><span class="token generic-function generic class-name"> In</span><span class="token generic-function generic class-name operator" style="color:#393A34">*</span><span class="token generic-function generic class-name operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">l</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">auto</span><span class="token plain"> ri </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token generic-function function" style="color:#d73a49">static_cast</span><span class="token generic-function generic class-name operator" style="color:#393A34">&lt;</span><span class="token generic-function generic class-name">Abs</span><span class="token generic-function generic class-name operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token generic-function function" style="color:#d73a49">reinterpret_cast</span><span class="token generic-function generic class-name operator" style="color:#393A34">&lt;</span><span class="token generic-function generic class-name keyword" style="color:#00009f">const</span><span class="token generic-function generic class-name"> In</span><span class="token generic-function generic class-name operator" style="color:#393A34">*</span><span class="token generic-function generic class-name operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">auto</span><span class="token plain"> lri </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> li </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> ri</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      s </span><span class="token operator" style="color:#393A34">+=</span><span class="token plain"> </span><span class="token generic-function function" style="color:#d73a49">static_cast</span><span class="token generic-function generic class-name operator" style="color:#393A34">&lt;</span><span class="token generic-function generic class-name">Out</span><span class="token generic-function generic class-name operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">std</span><span class="token double-colon punctuation" style="color:#393A34">::</span><span class="token function" style="color:#d73a49">abs</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">lri</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div></div><div class="comparisonColumn_Bhsi"><p><strong>faiss:</strong></p><div class="language-cpp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cpp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// #ifdef USE_AVX</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">float</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fvec_L1</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">float</span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> x</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">float</span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> y</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> size_t d</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    __m256 msum1 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_setzero_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// signmask used for absolute value</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    __m256 signmask </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_castsi256_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">_mm256_set1_epi32</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0x7fffffffUL</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">while</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">d </span><span class="token operator" style="color:#393A34">&gt;=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">8</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        __m256 mx </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_loadu_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">x</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        x </span><span class="token operator" style="color:#393A34">+=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">8</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        __m256 my </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_loadu_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">y</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        y </span><span class="token operator" style="color:#393A34">+=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">8</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// subtract</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> __m256 a_m_b </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_sub_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">mx</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> my</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// find sum of absolute value of distances (manhattan distance)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        msum1 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_add_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msum1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_and_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">signmask</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> a_m_b</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        d </span><span class="token operator" style="color:#393A34">-=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">8</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    __m128 msum2 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_extractf128_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msum1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    msum2 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm_add_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msum2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_extractf128_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msum1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    __m128 signmask2 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm_castsi128_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">_mm_set1_epi32</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0x7fffffffUL</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">d </span><span class="token operator" style="color:#393A34">&gt;=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">4</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        __m128 mx </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm_loadu_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">x</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        x </span><span class="token operator" style="color:#393A34">+=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">4</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        __m128 my </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm_loadu_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">y</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        y </span><span class="token operator" style="color:#393A34">+=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">4</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> __m128 a_m_b </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm_sub_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">mx</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> my</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        msum2 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm_add_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msum2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm_and_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">signmask2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> a_m_b</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        d </span><span class="token operator" style="color:#393A34">-=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">4</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">d </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        __m128 mx </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">masked_read</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">d</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> x</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        __m128 my </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">masked_read</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">d</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> y</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        __m128 a_m_b </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm_sub_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">mx</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> my</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        msum2 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm_add_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msum2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm_and_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">signmask2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> a_m_b</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    msum2 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm_hadd_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msum2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> msum2</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    msum2 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm_hadd_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msum2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> msum2</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm_cvtss_f32</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msum2</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div></div></div>
<p>The implementation in SereneDB is much simpler and more convenient, while in faiss there are multiple SIMD intrinsics, so it takes a while to analyze what is actually going on. And yet faiss is still slower than SereneDB — why?</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="l1-distance-simd-algorithm"><strong>L1 distance SIMD Algorithm</strong><a href="https://serenedb.com/blog/simd-distances#l1-distance-simd-algorithm" class="hash-link" aria-label="Direct link to l1-distance-simd-algorithm" title="Direct link to l1-distance-simd-algorithm">​</a></h2>
<p>Let's discuss the implementation in faiss (don't worry, it is not that hard).</p>
<p>In the first while-loop, 8 floats are loaded from <code>x</code> into <code>mx</code> and from <code>y</code> into <code>my</code>:</p>
<div class="language-cpp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cpp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">__m256 mx </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_loadu_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">x</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">x </span><span class="token operator" style="color:#393A34">+=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">8</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">__m256 my </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_loadu_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">y</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">y </span><span class="token operator" style="color:#393A34">+=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">8</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Then they are subtracted and the most significant bit is set to zero (using <code>signmask</code>) in order to find the absolute value. Afterwards, the value is added to the accumulator <code>msum1</code>:</p>
<div class="language-cpp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cpp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">__m256 signmask </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_castsi256_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">_mm256_set1_epi32</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0x7fffffffUL</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// ...</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> __m256 a_m_b </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_sub_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">mx</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> my</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// subtraction</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">msum1 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_add_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msum1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">/*abs*/</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_and_ps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">signmask</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> a_m_b</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">d </span><span class="token operator" style="color:#393A34">-=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">8</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>After the loop, if there are at least 4 floats remaining, the algorithm performs the same procedure as above but with just 4 floats. Finally, it processes the remaining floats.</p>
<p>This approach is faster than a basic implementation without SIMD instructions, because the algorithm processes 8 floats at a time. However, it could be faster.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="fast-math"><strong>Fast Math</strong><a href="https://serenedb.com/blog/simd-distances#fast-math" class="hash-link" aria-label="Direct link to fast-math" title="Direct link to fast-math">​</a></h2>
<p>Now it is time to reveal the root cause of the efficiency of the SereneDB algorithm: <code>fastmath</code>. Here are the extra flags that are set while compiling the SereneDB implementation: <code>-fassociative-math</code> and <code>-fno-signed-zeros</code></p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="what-do-the-flags-mean"><strong>What do the flags mean?</strong><a href="https://serenedb.com/blog/simd-distances#what-do-the-flags-mean" class="hash-link" aria-label="Direct link to what-do-the-flags-mean" title="Direct link to what-do-the-flags-mean">​</a></h3>
<p>These flags collectively relax the strict rules of the IEEE 754 floating-point standard, giving the compiler more freedom to reorganize and optimize arithmetic. Here is what each one does:</p>
<ul>
<li><code>-fassociative-math</code> allows the compiler to reorder floating-point operations (e.g. <code>(a + b) + c</code> into <code>a + (b + c)</code>). In reality, floating-point addition is not associative due to rounding, so this can slightly change results, although it enables the compiler to use multiple accumulators in parallel, which is exactly what we will see in the assembly below. This flag is crucial in our case, because it enables vectorization by creating additional accumulators and gaining more parallelization.</li>
<li><code>-fno-signed-zeros</code> tells the compiler it may treat -0.0 and +0.0 as identical. This option is actually required to enable <code>-fassociative-math</code>, because addition is not associative with signed zero<a href="https://serenedb.com/blog/simd-distances#signedzero">[3]</a>:</li>
</ul>
<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>a</mi><mo>=</mo><mn>1</mn><mo separator="true">,</mo><mi>b</mi><mo>=</mo><mn>1</mn><mo>⇒</mo><mo>−</mo><mo stretchy="false">(</mo><mi>a</mi><mo>−</mo><mi>b</mi><mo stretchy="false">)</mo><mo>=</mo><mo>−</mo><mo stretchy="false">(</mo><msup><mn>0</mn><mo lspace="0em" rspace="0em">+</mo></msup><mo stretchy="false">)</mo><mo>=</mo><msup><mn>0</mn><mo lspace="0em" rspace="0em">−</mo></msup><mo separator="true">,</mo><mo stretchy="false">(</mo><mo>−</mo><mi>a</mi><mo stretchy="false">)</mo><mo>−</mo><mo stretchy="false">(</mo><mo>−</mo><mi>b</mi><mo stretchy="false">)</mo><mo>=</mo><mo stretchy="false">(</mo><mo>−</mo><mn>1</mn><mo stretchy="false">)</mo><mo>−</mo><mo stretchy="false">(</mo><mo>−</mo><mn>1</mn><mo stretchy="false">)</mo><mo>=</mo><msup><mn>0</mn><mo lspace="0em" rspace="0em">+</mo></msup><mo mathvariant="normal">≠</mo><msup><mn>0</mn><mo lspace="0em" rspace="0em">−</mo></msup></mrow><annotation encoding="application/x-tex">a = 1, b = 1 \Rightarrow -(a - b) = -(0^{+}) = 0^{-}, (-a) - (-b) = (-1) - (-1) = 0^{+} \neq 0^{-}
</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em"></span><span class="mord">1</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal">b</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:0.6444em"></span><span class="mord">1</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">⇒</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord">−</span><span class="mopen">(</span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal">b</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1.0713em;vertical-align:-0.25em"></span><span class="mord">−</span><span class="mopen">(</span><span class="mord"><span class="mord">0</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8213em"><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">+</span></span></span></span></span></span></span></span></span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1.0713em;vertical-align:-0.25em"></span><span class="mord"><span class="mord">0</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8213em"><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">−</span></span></span></span></span></span></span></span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mopen">(</span><span class="mord">−</span><span class="mord mathnormal">a</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mopen">(</span><span class="mord">−</span><span class="mord mathnormal">b</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mopen">(</span><span class="mord">−</span><span class="mord">1</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mopen">(</span><span class="mord">−</span><span class="mord">1</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1.0158em;vertical-align:-0.1944em"></span><span class="mord"><span class="mord">0</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8213em"><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">+</span></span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel"><span class="mrel"><span class="mord vbox"><span class="thinbox"><span class="rlap"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em"></span><span class="inner"><span class="mord"><span class="mrel"></span></span></span><span class="fix"></span></span></span></span></span><span class="mrel">=</span></span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:0.8213em"></span><span class="mord"><span class="mord">0</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8213em"><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">−</span></span></span></span></span></span></span></span></span></span></span></span></span>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="l1-distance-with-fast-math"><strong>L1 distance with Fast Math</strong><a href="https://serenedb.com/blog/simd-distances#l1-distance-with-fast-math" class="hash-link" aria-label="Direct link to l1-distance-with-fast-math" title="Direct link to l1-distance-with-fast-math">​</a></h2>
<p>After compiling with those flags, look at the SereneDB algorithm's assembly code: <a href="https://serenedb.com/blog/simd-distances#godbolt">[2]</a>. As you can see, the algorithm is basically the same, nevertheless, it uses 4 256-bit <code>ymm</code> registers to process floats, so due to ILP and operation independence, the load/subtract operations can be processed in parallel. That way we are able to process 16 floats at a time (4 times more than in faiss).</p>
<p>Although, the compiler did not find an optimal way of accumulating the result:</p>
<div class="language-x86asm codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-x86asm codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">; There are 4 accumulators: ymm0, ymm2, ymm3, ymm4</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vaddps  ymm0, ymm5, ymm0</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vandps  ymm5, ymm6, ymm1</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vaddps  ymm2, ymm5, ymm2</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vandps  ymm5, ymm7, ymm1</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vaddps  ymm3, ymm5, ymm3</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vandps  ymm5, ymm8, ymm1</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vaddps  ymm4, ymm5, ymm4</span><br></span></code></pre></div></div>
<p>It may be faster (and more parallel) to rewrite it as something like:</p>
<div class="language-x86asm codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-x86asm codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">vaddps  ymm0, ymm5, ymm0</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vandps  ymm6, ymm6, ymm1 ; vandps  ymm5, ymm6, ymm1</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vaddps  ymm2, ymm6, ymm2 ; vaddps  ymm2, ymm5, ymm2</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vandps  ymm7, ymm7, ymm1 ; vandps  ymm5, ymm7, ymm1</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vaddps  ymm3, ymm7, ymm3 ; vaddps  ymm3, ymm5, ymm3</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vandps  ymm8, ymm8, ymm1 ; vandps  ymm5, ymm8, ymm1</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vaddps  ymm4, ymm8, ymm4 ; vaddps  ymm4, ymm5, ymm4</span><br></span></code></pre></div></div>
<p>Admittedly, writing the same code with intrinsics does not boost the performance, probably because of register renaming <a href="https://serenedb.com/blog/simd-distances#registers">[4]</a>.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="why-not-4x-port-5-bottleneck"><strong>Why not 4x: Port 5 bottleneck</strong><a href="https://serenedb.com/blog/simd-distances#why-not-4x-port-5-bottleneck" class="hash-link" aria-label="Direct link to why-not-4x-port-5-bottleneck" title="Direct link to why-not-4x-port-5-bottleneck">​</a></h3>
<p>To understand the precise ceiling, let's look at what <code>llvm-mca</code> reports. The block reciprocal throughput is <strong>25.0 cycles</strong>, and the resource pressure table gives the answer immediately:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">SBPort0:  16.89</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">SBPort1:  21.08   (FP arithmetic — vaddps, vsubps)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">SBPort5:  25.03   bottleneck</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">SBPort23: 15.00   (loads)</span><br></span></code></pre></div></div>
<p><code>SBPort5</code> is saturated at exactly <strong>25.03</strong> — matching the throughput ceiling. Port 1 (FP arithmetic) and Port 23 (loads) both have remaining capacity. The bottleneck is neither compute nor memory bandwidth.</p>
<p>Port 5 is shared by two instruction classes that cannot be separated: the <strong><code>vandps</code></strong> instructions used for the computing abs using signmask, and <strong>all branch instructions</strong> (<code>je</code>, <code>jne</code>, <code>jmp</code>, etc.). In the main unrolled loop, every accumulator contributes one <code>vandps</code> to Port 5 — so 4 accumulators means 4x the <code>vandps</code> pressure on Port 5. Combined with the loop-back branch and the numerous conditionals in the tail-handling code, Port 5 becomes fully saturated. The four FP accumulators on Port 1 still have headroom; they just never get the chance to run ahead.</p>
<p>This is the precise reason the speedup is ~45 % rather than 4x: adding more accumulators scales Port 1 work but scales Port 5 pressure equally, and Port 5 is the ceiling.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="cosine-distance-one-loop-vs-three"><strong>Cosine distance: one loop vs. three</strong><a href="https://serenedb.com/blog/simd-distances#cosine-distance-one-loop-vs-three" class="hash-link" aria-label="Direct link to cosine-distance-one-loop-vs-three" title="Direct link to cosine-distance-one-loop-vs-three">​</a></h2>
<p>You may have a question: "Why is the SereneDB implementation of cosine distance faster — isn't it just 3 dot products?" Yes, it is. However, SereneDB computes it in one pass over the data:</p>
<div class="language-cpp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cpp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">Sum ll</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Sum lr</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Sum rr</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">uint16_t</span><span class="token plain"> i </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> i </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> d</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">++</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">auto</span><span class="token plain"> li </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token generic-function function" style="color:#d73a49">static_cast</span><span class="token generic-function generic class-name operator" style="color:#393A34">&lt;</span><span class="token generic-function generic class-name">Sum</span><span class="token generic-function generic class-name operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token generic-function function" style="color:#d73a49">reinterpret_cast</span><span class="token generic-function generic class-name operator" style="color:#393A34">&lt;</span><span class="token generic-function generic class-name keyword" style="color:#00009f">const</span><span class="token generic-function generic class-name"> In</span><span class="token generic-function generic class-name operator" style="color:#393A34">*</span><span class="token generic-function generic class-name operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">l</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">auto</span><span class="token plain"> ri </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token generic-function function" style="color:#d73a49">static_cast</span><span class="token generic-function generic class-name operator" style="color:#393A34">&lt;</span><span class="token generic-function generic class-name">Sum</span><span class="token generic-function generic class-name operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token generic-function function" style="color:#d73a49">reinterpret_cast</span><span class="token generic-function generic class-name operator" style="color:#393A34">&lt;</span><span class="token generic-function generic class-name keyword" style="color:#00009f">const</span><span class="token generic-function generic class-name"> In</span><span class="token generic-function generic class-name operator" style="color:#393A34">*</span><span class="token generic-function generic class-name operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ll </span><span class="token operator" style="color:#393A34">+=</span><span class="token plain"> li </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> li</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  lr </span><span class="token operator" style="color:#393A34">+=</span><span class="token plain"> li </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> ri</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  rr </span><span class="token operator" style="color:#393A34">+=</span><span class="token plain"> ri </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> ri</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> std</span><span class="token double-colon punctuation" style="color:#393A34">::</span><span class="token plain">tuple</span><span class="token punctuation" style="color:#393A34">{</span><span class="token generic-function function" style="color:#d73a49">static_cast</span><span class="token generic-function generic class-name operator" style="color:#393A34">&lt;</span><span class="token generic-function generic class-name">Out</span><span class="token generic-function generic class-name operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ll</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token generic-function function" style="color:#d73a49">static_cast</span><span class="token generic-function generic class-name operator" style="color:#393A34">&lt;</span><span class="token generic-function generic class-name">Out</span><span class="token generic-function generic class-name operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">lr</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                  </span><span class="token generic-function function" style="color:#d73a49">static_cast</span><span class="token generic-function generic class-name operator" style="color:#393A34">&lt;</span><span class="token generic-function generic class-name">Out</span><span class="token generic-function generic class-name operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">rr</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>In contrast, faiss computes it in three separate loops:</p>
<div class="language-cpp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cpp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">float</span><span class="token plain"> norm_x </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> norm_y </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">faiss</span><span class="token double-colon punctuation" style="color:#393A34">::</span><span class="token function" style="color:#d73a49">fvec_norms_L2</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">&amp;</span><span class="token plain">norm_x</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> left</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sz</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">faiss</span><span class="token double-colon punctuation" style="color:#393A34">::</span><span class="token function" style="color:#d73a49">fvec_norms_L2</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">&amp;</span><span class="token plain">norm_y</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> right</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sz</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">float</span><span class="token plain"> product </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> faiss</span><span class="token double-colon punctuation" style="color:#393A34">::</span><span class="token function" style="color:#d73a49">fvec_inner_product</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">left</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> right</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sz</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token generic-function function" style="color:#d73a49">static_cast</span><span class="token generic-function generic class-name operator" style="color:#393A34">&lt;</span><span class="token generic-function generic class-name keyword" style="color:#00009f">float</span><span class="token generic-function generic class-name operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">product </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">norm_x </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> norm_y</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// Actually it uses omp to compute two norms at once, but we don't use omp:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// float norms[2] = {0};</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// faiss::fvec_norms_L2(norms, /*x=*/x.data(), /*d=*/d, /*nx=*/2);</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// float product = faiss::fvec_inner_product(x.data(), /*y=*/x.data() + d, d);</span><br></span></code></pre></div></div>
<p>SereneDB accumulates all three sums — <code>ll</code>, <code>lr</code>, <code>rr</code> — in a single pass. The vectors are loaded into cache once, and all three accumulators are updated together. Faiss reads the same data twice, paying the memory bandwidth cost repeatedly. Combined with the <code>fastmath</code> flags enabling multiple accumulators, this accounts for the ~13% speedup seen in the benchmark.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="bonus-more-benchmarks"><strong>Bonus: More benchmarks!</strong><a href="https://serenedb.com/blog/simd-distances#bonus-more-benchmarks" class="hash-link" aria-label="Direct link to bonus-more-benchmarks" title="Direct link to bonus-more-benchmarks">​</a></h2>
<p>We've also added <code>numkong</code><a href="https://serenedb.com/blog/simd-distances#numkong">[6]</a> and distance utils from <code>ydb</code><a href="https://serenedb.com/blog/simd-distances#ydb">[7]</a> (which are taken from <code>catboost</code><a href="https://serenedb.com/blog/simd-distances#catboost">[8]</a>) to compare with our implementation. For L1 and L2 specifically, we included two <code>ydb</code> implementations: one using SSE intrinsics and one using a plain loop with 4 accumulators.</p>
<p><code>numkong</code> is significantly slower across all distances — it upcasts <code>f32</code> inputs to <code>f64</code> for better numerical precision, which roughly doubles the amount of data processed and prevents the compiler from fitting as many values into a single SIMD register. Therefore, it is not quite fair to compare SereneDB implementations with <code>numkong</code>'s, since the latter has higher precision, but it is still worth mentioning.</p>
<p>For cosine similarity, <code>ydb</code> applies the same single-pass tri-way dot product trick as SereneDB, and the implementation is written using AVX2 intrinsics directly, so the cosine performance is on par with SereneDB.</p>
<p>For L1 and L2 we benchmarked two <code>ydb</code> variants. <code>ydb (sse)</code> is hand-written SSE implementation and is slower simply because it operates on 128-bit registers instead of 256-bit AVX2 ones, halving the effective SIMD width. <code>ydb (4 acc)</code> is a plain loop with 4 accumulators (the same structure as SereneDB) yet it is still noticeably slower. SereneDB computes absolute value with a single <code>vandps</code> against a signmask (<code>0x7fffffff</code>):</p>
<div class="language-x86asm codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-x86asm codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">vsubps  ymm5, ymm5, ymmword ptr [rsi + ...]</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vandps  ymm5, ymm5, ymm1          ; abs in 1 instruction</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vaddps  ymm0, ymm5, ymm0</span><br></span></code></pre></div></div>
<p>The <code>ydb (4 acc)</code> source expresses absolute value as <code>a &gt; b ? a - b : b - a</code>, so the compiler emits a comparison and a conditional blend instead:</p>
<div class="language-x86asm codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-x86asm codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">vcmpltps  ymm7, ymm3, ymm5        ; compare</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vsubps    ymm9, ymm5, ymm3        ; a - b</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vsubps    ymm3, ymm3, ymm5        ; b - a</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vblendvps ymm3, ymm3, ymm9, ymm7  ; abs in 4 instructions</span><br></span></code></pre></div></div>
<p>That is 4 instructions per abs versus 1, repeated across all 4 accumulators every iteration.</p>
<p>Replacing <code>AbsDelta</code> with <code>std::abs</code> on the subtraction result is enough to close the gap:</p>
<div class="language-cpp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cpp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// before</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">template</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">typename</span><span class="token plain"> </span><span class="token class-name">T</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">inline</span><span class="token plain"> T </span><span class="token function" style="color:#d73a49">AbsDelta</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">T a</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> T b</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> a </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> b </span><span class="token operator" style="color:#393A34">?</span><span class="token plain"> a </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> b </span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> b </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> a</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">s1 </span><span class="token operator" style="color:#393A34">+=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">AbsDelta</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">a</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> b</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// after</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">auto</span><span class="token plain"> d1 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> a</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> b</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">s1 </span><span class="token operator" style="color:#393A34">+=</span><span class="token plain"> std</span><span class="token double-colon punctuation" style="color:#393A34">::</span><span class="token function" style="color:#d73a49">abs</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">d1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>With this change the compiler recognises the pattern and emits <code>vandps</code> instead of the compare-and-blend sequence, bringing <code>ydb (4 acc)</code> to the same performance as SereneDB.</p>
<p>The L2 case has an even simpler explanation. Since <code>d*d == abs(d)*abs(d)</code>, the absolute value step is entirely redundant for L2 — squaring always produces a non-negative result regardless of sign. SereneDB skips it and goes straight from subtraction to a fused multiply-add:</p>
<div class="language-x86asm codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-x86asm codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">vsubps      ymm4, ymm4, ymmword ptr [rsi + ...]   ; d = a - b</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vfmadd231ps ymm0, ymm4, ymm4                      ; acc += d*d</span><br></span></code></pre></div></div>
<p>Yet <code>ydb (4 acc)</code> still runs the full compare-and-blend sequence before squaring:</p>
<div class="language-x86asm codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-x86asm codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">vcmpltps    ymm7, ymm3, ymm5        ; \</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vsubps      ymm9, ymm5, ymm3        ;  | unnecessary abs</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vsubps      ymm3, ymm3, ymm5        ;  | for L2</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vblendvps   ymm3, ymm3, ymm9, ymm7  ; /</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vfmadd231ps ymm0, ymm3, ymm3        ; acc += abs(d)*abs(d) == acc += d*d</span><br></span></code></pre></div></div>
<p>The fix is the same — replace <code>AbsDelta</code> with a plain subtraction fed into <code>vfmadd231ps</code>.</p>
<img src="https://serenedb.com/blog/assets/images/distances-1cdc3eb6496dc26e489f1e4782ae0f2a.png">
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="conclusion"><strong>Conclusion</strong><a href="https://serenedb.com/blog/simd-distances#conclusion" class="hash-link" aria-label="Direct link to conclusion" title="Direct link to conclusion">​</a></h2>
<p>Well, well, well, as a result, the simple implementation of L1 and Cosine distances is much faster (about a 45% increase in L1 distance and about 13% in Cosine distance on large vectors). Moreover, it's actually surprising how modern compilers can optimize source code. Using just a few flags, the compiler can vectorize the code and enable parallel execution with multiple accumulators.</p>
<p>In addition, we're planning to improve the performance by introducing a <em>one-to-many</em> approach like in Google's scann library. It refers to the process of taking one query vector and measuring its mathematical distance against many.</p>
<p>SereneDB is actively working on vector search and hybrid search — both features are coming in the next releases, so stay tuned!</p>
<p>If you enjoy reading about low-level optimizations, check out and star our GitHub repo: <a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer">https://github.com/serenedb/serenedb</a>.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="references">References<a href="https://serenedb.com/blog/simd-distances#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References">​</a></h2>
<p><span id="vectorimpl"></span>[1] SereneDB vector distance implementation: <a href="https://github.com/serenedb/serenedb/blob/main/libs/iresearch/include/iresearch/utils/vector.hpp" target="_blank" rel="noopener noreferrer">https://github.com/serenedb/serenedb/blob/main/libs/iresearch/include/iresearch/utils/vector.hpp</a></p>
<p><span id="godbolt"></span>[2] Godbolt disassembly: <a href="https://godbolt.org/z/4jse1eP7x" target="_blank" rel="noopener noreferrer">https://godbolt.org/z/4jse1eP7x</a></p>
<p><span id="signedzero"></span>[3] Signed Zero: <a href="https://en.wikipedia.org/wiki/Signed_zero" target="_blank" rel="noopener noreferrer">https://en.wikipedia.org/wiki/Signed_zero</a></p>
<p><span id="registers"></span>[4] Register Renaming: <a href="https://en.wikipedia.org/wiki/Register_renaming" target="_blank" rel="noopener noreferrer">https://en.wikipedia.org/wiki/Register_renaming</a></p>
<p><span id="textsearch"></span>[5] Search Benchmark, the Game: <a href="https://serenedb.com/search-benchmark-game" target="_blank" rel="noopener noreferrer">https://serenedb.com/search-benchmark-game</a></p>
<p><span id="numkong"></span>[6] NumKong: <a href="https://github.com/ashvardanian/NumKong" target="_blank" rel="noopener noreferrer">https://github.com/ashvardanian/NumKong</a></p>
<p><span id="ydb"></span>[7] YDB distance utils: <a href="https://github.com/ydb-platform/ydb/tree/main/library/cpp" target="_blank" rel="noopener noreferrer">https://github.com/ydb-platform/ydb/tree/main/library/cpp</a></p>
<p><span id="catboost"></span>[8] CatBoost: <a href="https://github.com/catboost/catboost" target="_blank" rel="noopener noreferrer">https://github.com/catboost/catboost</a></p></div>]]></content>
        <author>
            <name>SereneDB Team</name>
        </author>
        <category label="Dev" term="Dev"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Optimize norm gathering]]></title>
        <id>https://serenedb.com/blog/norm-gathering</id>
        <link href="https://serenedb.com/blog/norm-gathering"/>
        <updated>2026-04-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Search optimization journey 3: Optimize norm gathering]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><img class="avatarImage_s4Ar" src="https://serenedb.com/blog/img/mironov.jpeg" alt="Valerii Mironov" loading="lazy"></div></div><div class="copy_iHNG"><p class="names_ox2I">Valerii Mironov</p><p class="meta_OgzX">Apr 1, 2026 · 10 minutes read</p></div></header><h2 class="title_RQ9v">Optimize norm gathering</h2><p style="font-size:0.9rem;margin-top:-0.4rem">Search optimization journey 3: Optimize norm gathering</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg hideFirstHeading_VVMX" data-image-lightbox-root="true"><h2 class="anchor anchorWithStickyNavbar_LWe7" id="search-optimization-journey-3-optimize-norm-gathering"><strong>Search optimization journey 3: Optimize norm gathering</strong><a href="https://serenedb.com/blog/norm-gathering#search-optimization-journey-3-optimize-norm-gathering" class="hash-link" aria-label="Direct link to search-optimization-journey-3-optimize-norm-gathering" title="Direct link to search-optimization-journey-3-optimize-norm-gathering">​</a></h2>
<p>We recently won the <a href="https://serenedb.com/blog/search-benchmark-game-overview">search benchmark game</a> across query and collection types. Part of the work involved hand-tuned AVX2 code. That was fine while we only shipped x86_64 binaries. Then we decided to add linux/aarch64 releases (Docker, deb, tar.gz) so people could run SereneDB on ARM machines and Apple Silicon through Docker.</p>
<p>Most of the AVX2 code either compiled away behind <code>#ifdef</code> or had straightforward portable alternatives. One piece of code made us pause: the norm gathering loop. We weren't sure whether the portable version would regress, so we built a microbenchmark. What we found was surprising.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="what-are-norms-and-where-do-they-show-up">What are norms and where do they show up<a href="https://serenedb.com/blog/norm-gathering#what-are-norms-and-where-do-they-show-up" class="hash-link" aria-label="Direct link to What are norms and where do they show up" title="Direct link to What are norms and where do they show up">​</a></h3>
<p>BM25 normalizes term frequency by document length. The document length is stored per-document as a "norm" value and read during scoring.</p>
<img src="https://serenedb.com/blog/img/blog/norm-gathering/bm25-formula-light.svg" alt="BM25 formula showing where norms are used" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%"><img src="https://serenedb.com/blog/img/blog/norm-gathering/bm25-formula-dark.svg" alt="BM25 formula showing where norms are used" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%">
<p>As described in the <a href="https://serenedb.com/blog/search-optimization-2">block scoring post</a>, IResearch scores documents in blocks of 128. For each block the engine reads 128 norm values from columnar storage.</p>
<p>Unlike Lucene and Tantivy, which quantize norms to a single byte (losing ~10% retrieval quality in older Lucene versions), IResearch stores norms at full precision: 1, 2, or 4 bytes depending on the maximum document length in the segment. We also support different scoring models (BM25 with tunable k1/b parameters, TFIDF with norms) through a pluggable scoring framework.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-problem">The problem<a href="https://serenedb.com/blog/norm-gathering#the-problem" class="hash-link" aria-label="Direct link to The problem" title="Direct link to The problem">​</a></h3>
<p>Here's the core loop. For each document in a block of 128, read its norm value from a byte array at an offset derived from the document ID:</p>
<div class="language-cpp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cpp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">scores_size_t i </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> i </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> kPostingBlock</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">++</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    values</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">ReadValue</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">origin</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> docs</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> doc_base</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p><code>ReadValue</code> loads 1, 2, or 4 bytes depending on the encoding. The issue is that <code>docs[i] - doc_base</code> gives a different offset each iteration. Each load goes to a different memory location. This is a gather pattern.</p>
<img src="https://serenedb.com/blog/img/blog/norm-gathering/gather-pattern-light.svg" alt="Dense vs sparse document ID access patterns" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%"><img src="https://serenedb.com/blog/img/blog/norm-gathering/gather-pattern-dark.svg" alt="Dense vs sparse document ID access patterns" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%">
<p>The compiler will not auto-vectorize a gather. Even with <code>#pragma clang loop vectorize(enable)</code>, clang refuses to emit <code>vpgatherdd</code>. More on this below.</p>
<p>We had an AVX2 version using explicit gather intrinsics:</p>
<div class="language-cpp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cpp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">auto</span><span class="token plain"> indices </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_loadu_si256</span><span class="token punctuation" style="color:#393A34">(</span><span class="token generic-function function" style="color:#d73a49">reinterpret_cast</span><span class="token generic-function generic class-name operator" style="color:#393A34">&lt;</span><span class="token generic-function generic class-name keyword" style="color:#00009f">const</span><span class="token generic-function generic class-name"> __m256i</span><span class="token generic-function generic class-name operator" style="color:#393A34">*</span><span class="token generic-function generic class-name operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">docs </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> i</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">indices </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_sub_epi32</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">indices</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> base</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">auto</span><span class="token plain"> gathered </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_i32gather_epi32</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token generic-function function" style="color:#d73a49">reinterpret_cast</span><span class="token generic-function generic class-name operator" style="color:#393A34">&lt;</span><span class="token generic-function generic class-name keyword" style="color:#00009f">const</span><span class="token generic-function generic class-name"> </span><span class="token generic-function generic class-name keyword" style="color:#00009f">int</span><span class="token generic-function generic class-name operator" style="color:#393A34">*</span><span class="token generic-function generic class-name operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">origin</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> indices</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    std</span><span class="token double-colon punctuation" style="color:#393A34">::</span><span class="token function" style="color:#d73a49">to_underlying</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">Encoding</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">gathered </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">_mm256_and_si256</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">gathered</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> mask</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">_mm256_storeu_si256</span><span class="token punctuation" style="color:#393A34">(</span><span class="token generic-function function" style="color:#d73a49">reinterpret_cast</span><span class="token generic-function generic class-name operator" style="color:#393A34">&lt;</span><span class="token generic-function generic class-name">__m256i</span><span class="token generic-function generic class-name operator" style="color:#393A34">*</span><span class="token generic-function generic class-name operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">values </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> i</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> gathered</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>This works on x86 with AVX2. On aarch64, NEON has no gather instructions at all (only SVE does). We wanted to know: can we do better than both the scalar loop and the AVX2 gather, portably?</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-key-insight-not-all-blocks-are-sparse">The key insight: not all blocks are sparse<a href="https://serenedb.com/blog/norm-gathering#the-key-insight-not-all-blocks-are-sparse" class="hash-link" aria-label="Direct link to The key insight: not all blocks are sparse" title="Direct link to The key insight: not all blocks are sparse">​</a></h3>
<p>In posting lists, document IDs within a block are sorted. In many real workloads a significant fraction of blocks contain contiguous IDs: <code>[1100, 1101, 1102, ..., 1227]</code>. For a contiguous block, <code>origin[first], origin[first+1], ..., origin[first+127]</code> is a sequential memory scan. The compiler will happily auto-vectorize that into wide SIMD loads on any architecture.</p>
<p>The check is cheap:</p>
<div class="language-cpp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cpp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">docs</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">kPostingBlock </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> docs</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> kPostingBlock </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// contiguous: sequential read</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">else</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// sparse: individual loads</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>No need to subtract <code>doc_base</code> for the comparison. If the first and last doc differ by exactly 127, the block is contiguous.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="benchmarking">Benchmarking<a href="https://serenedb.com/blog/norm-gathering#benchmarking" class="hash-link" aria-label="Direct link to Benchmarking" title="Direct link to Benchmarking">​</a></h3>
<p>We wrote a <a href="https://github.com/serenedb/serenedb/blob/main/tests/bench/micro/norm_gather.cpp" target="_blank" rel="noopener noreferrer">microbenchmark</a> testing three variants (Scalar, Hybrid with the contiguous check, Gather with AVX2 intrinsics) across three scenarios:</p>
<ul>
<li><strong>Dense</strong>: contiguous doc IDs (best case for hybrid)</li>
<li><strong>Sparse</strong>: sorted with random gaps of 1-20</li>
<li><strong>Mixed</strong>: 50/50 random choice between dense and sparse blocks</li>
</ul>
<p>All benchmarks run on an AMD Ryzen 9 9950X (Zen 5, 16 cores, 48 KiB L1d per core, 1 MiB L2, 32 MiB L3), compiled with clang 21 targeting at least Haswell (AVX2). Haswell came out in 2013, so this is baseline for any x86_64 machine you'd actually run a search engine on today.</p>
<p>For the mixed case we pre-generate 60,000 random choices. This matters. Daniel Lemire <a href="https://lemire.me/blog/2026/03/18/how-many-branches-can-your-cpu-predict/" target="_blank" rel="noopener noreferrer">recently measured</a> that AMD Zen 5 can learn 30,000 branch patterns, Apple M4 about 10,000, Intel around 5,000. At 60,000 random entries, no CPU will learn the pattern, so we measure real misprediction cost.</p>
<p>One caveat: the benchmark data fits in L1/L2 cache. In production with large segments, cache misses would affect all variants equally. What we're measuring here is pure computational throughput.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="first-results">First results<a href="https://serenedb.com/blog/norm-gathering#first-results" class="hash-link" aria-label="Direct link to First results" title="Direct link to First results">​</a></h3>
<img src="https://serenedb.com/blog/img/blog/norm-gathering/benchmark-first-light.svg" alt="First benchmark results: Scalar vs Hybrid vs Gather" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%"><img src="https://serenedb.com/blog/img/blog/norm-gathering/benchmark-first-dark.svg" alt="First benchmark results: Scalar vs Hybrid vs Gather" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%">
<p>The hybrid approach dominates dense blocks: <strong>2.7 ns vs 25 ns</strong>, about 8x faster. The compiler turns the sequential loop into wide <code>vmovdqu</code> + <code>vpmovzxwd</code> loads. One memory access replaces eight.</p>
<p>AVX2 gather is ~24 ns regardless of pattern. It barely beats scalar. Each <code>vpgatherdd</code> instruction looks like one operation, but internally the gather unit still issues 8 individual micro-loads. The instruction count drops 3.5x compared to scalar, but the gather unit is the bottleneck.</p>
<p>For mixed blocks, hybrid averages ~17 ns. The branch misprediction penalty is only 2-3 ns.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="looking-at-the-assembly">Looking at the assembly<a href="https://serenedb.com/blog/norm-gathering#looking-at-the-assembly" class="hash-link" aria-label="Direct link to Looking at the assembly" title="Direct link to Looking at the assembly">​</a></h3>
<p>We expected the scalar loop to compile into simple scalar code. It didn't. Clang already auto-vectorizes the scalar loop with the same extract/insert pattern whether you ask for it or not:</p>
<img src="https://serenedb.com/blog/img/blog/norm-gathering/asm-comparison-light.svg" alt="Assembly comparison: scalar, gather, and dense path" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%"><img src="https://serenedb.com/blog/img/blog/norm-gathering/asm-comparison-dark.svg" alt="Assembly comparison: scalar, gather, and dense path" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%">
<p>The compiler vectorizes the index subtraction (<code>vpaddd</code>), extracts each index to a scalar register (<code>vpextrd</code>), does 8 individual loads, and reassembles the result into a vector (<code>vpinsrw</code>). The insert chain is serial: each <code>vpinsrw</code> depends on the previous one.</p>
<p>We tried <code>#pragma clang loop vectorize(enable) interleave(enable)</code>. Same codegen, same performance. The compiler already did everything it could. It just won't emit <code>vpgatherdd</code> on its own.</p>
<p>To inspect assembly we used the <a href="https://github.com/harikrishnan94/cxx-compiler-explorer" target="_blank" rel="noopener noreferrer">C++ Compiler Explorer</a> VS Code extension. It picks up <code>compile_commands.json</code> from your build directory, so you see the assembly your actual compiler produces with your actual flags and dependencies. Much more useful than Godbolt for code that depends on project headers.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="full-unrolling-changes-things">Full unrolling changes things<a href="https://serenedb.com/blog/norm-gathering#full-unrolling-changes-things" class="hash-link" aria-label="Direct link to Full unrolling changes things" title="Direct link to Full unrolling changes things">​</a></h3>
<p>The scalar loop runs 16 iterations (128 elements / 8 per vectorized iteration). Each iteration has an 8-instruction insert chain that must execute serially. What if we removed the loop so the CPU could overlap independent chains?</p>
<div class="language-cpp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cpp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token macro property directive-hash" style="color:#36acaa">#</span><span class="token macro property directive keyword" style="color:#00009f">pragma</span><span class="token macro property" style="color:#36acaa"> </span><span class="token macro property expression" style="color:#36acaa">clang loop </span><span class="token macro property expression function" style="color:#d73a49">unroll</span><span class="token macro property expression punctuation" style="color:#393A34">(</span><span class="token macro property expression" style="color:#36acaa">full</span><span class="token macro property expression punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">scores_size_t i </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> i </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> kPostingBlock</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">++</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    values</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token generic-function function" style="color:#d73a49">ScalarRead</span><span class="token generic-function generic class-name operator" style="color:#393A34">&lt;</span><span class="token generic-function generic class-name">Encoding</span><span class="token generic-function generic class-name operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">origin</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> docs</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> doc_base</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>This made a real difference on sparse data:</p>
<table><thead><tr><th>Variant</th><th>Dense (ns)</th><th>Sparse (ns)</th><th>Mixed (ns)</th></tr></thead><tbody><tr><td>Scalar</td><td>25.7</td><td>25.7</td><td>26.6</td></tr><tr><td>Scalar + unroll(full)</td><td>22.8</td><td>22.5</td><td>23.5</td></tr><tr><td>Gather (AVX2)</td><td>23.5</td><td>23.4</td><td>24.1</td></tr></tbody></table>
<p>Full unrolling beats AVX2 gather on Short encoding. With 16 independent insert chains visible at once, the CPU overlaps their execution through the out-of-order engine. Gather can't benefit from this because <code>vpgatherdd</code> is a single instruction that serializes internally.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-trap-dont-unroll-the-dense-path">The trap: don't unroll the dense path<a href="https://serenedb.com/blog/norm-gathering#the-trap-dont-unroll-the-dense-path" class="hash-link" aria-label="Direct link to The trap: don't unroll the dense path" title="Direct link to The trap: don't unroll the dense path">​</a></h3>
<p>If unrolling helps sparse, why not unroll both paths in the hybrid?</p>
<table><thead><tr><th>Variant</th><th>Dense (ns)</th><th>Sparse (ns)</th></tr></thead><tbody><tr><td>Hybrid (no unroll)</td><td><strong>2.7</strong></td><td>26.0</td></tr><tr><td>Hybrid (unroll sparse)</td><td><strong>2.7</strong></td><td><strong>18.9</strong></td></tr><tr><td>Hybrid (unroll both)</td><td>13.5</td><td>22.5</td></tr></tbody></table>
<p>Unrolling the dense path makes it <strong>5x slower</strong>: 13.5 ns vs 2.7 ns.</p>
<p>Without unrolling, the compiler recognizes the sequential access pattern and emits <code>vpmovzxwd</code> instructions that load 8 consecutive shorts and zero-extend them to 32-bit in a single operation:</p>
<div class="language-x86asm codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-x86asm codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">; non-unrolled dense path: load+widen 8 elements at once</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vpmovzxwd (%rdi,%rcx,2), %ymm0    ; 8 shorts -&gt; 8 dwords</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vpmovzxwd 16(%rdi,%rcx,2), %ymm1  ; next 8</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vpmovzxwd 32(%rdi,%rcx,2), %ymm2  ; next 8</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vpmovzxwd 48(%rdi,%rcx,2), %ymm3  ; next 8</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vmovdqa %ymm0, 128(%r9)           ; store 8 dwords</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vmovdqa %ymm1, 160(%r9)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vmovdqa %ymm2, 192(%r9)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vmovdqa %ymm3, 224(%r9)</span><br></span></code></pre></div></div>
<p>With <code>unroll(full)</code>, the compiler sees 128 individual accesses with constant offsets and gives up on vectorization entirely. It generates 128 scalar <code>movzwl</code> (load one short, zero-extend) + <code>movl</code> (store one dword) pairs:</p>
<div class="language-x86asm codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-x86asm codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">; unrolled dense path: one element at a time</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">leal  -1000(%rax), %edx</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">movzwl (%rdi,%rdx,2), %edx        ; load 1 short</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">movl   %edx, 128(%r9)             ; store 1 dword</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">leal  -999(%rax), %ecx</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">movzwl (%rdi,%rcx,2), %ecx        ; load 1 short</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">movl   %ecx, 132(%r9)             ; store 1 dword</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">; ... 126 more times</span><br></span></code></pre></div></div>
<p>Same data, same addresses. The compiler just lost the forest for the trees.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-final-solution">The final solution<a href="https://serenedb.com/blog/norm-gathering#the-final-solution" class="hash-link" aria-label="Direct link to The final solution" title="Direct link to The final solution">​</a></h3>
<p>Unroll the sparse path. Leave the dense path alone. No intrinsics needed.</p>
<img src="https://serenedb.com/blog/img/blog/norm-gathering/benchmark-final-light.svg" alt="Final benchmark with all variants" class="themedComponent_mlkZ themedComponent--light_NVdE" style="width:100%"><img src="https://serenedb.com/blog/img/blog/norm-gathering/benchmark-final-dark.svg" alt="Final benchmark with all variants" class="themedComponent_mlkZ themedComponent--dark_xIcU" style="width:100%">
<table><thead><tr><th>Benchmark</th><th>Scalar</th><th>Hybrid</th><th>HybridUS</th><th>HybridUB</th><th>Gather</th></tr></thead><tbody><tr><td><strong>Dense</strong></td><td>25.7</td><td><strong>2.7</strong></td><td><strong>2.7</strong></td><td>13.5</td><td>23.5</td></tr><tr><td><strong>Sparse</strong></td><td>25.7</td><td>26.0</td><td><strong>18.9</strong></td><td>22.5</td><td>23.4</td></tr><tr><td><strong>Mixed</strong></td><td>26.6</td><td>17.8</td><td><strong>13.8</strong></td><td>20.7</td><td>24.1</td></tr></tbody></table>
<p>HybridUS (unroll sparse only) wins every row. Here is the production code:</p>
<div class="language-cpp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-cpp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">void</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">GetPostingBlock</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    std</span><span class="token double-colon punctuation" style="color:#393A34">::</span><span class="token plain">span</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> doc_id_t</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> kPostingBlock</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> docs</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    std</span><span class="token double-colon punctuation" style="color:#393A34">::</span><span class="token plain">span</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">uint32_t</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> kPostingBlock</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> values</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">noexcept</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">final</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">auto</span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> IRS_RESTRICT </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> origin </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> _origin</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">auto</span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> IRS_RESTRICT </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> values_data </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> values</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">data</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">auto</span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> IRS_RESTRICT </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> docs_data </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> docs</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">data</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">docs_data</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">kPostingBlock </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> docs_data</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> kPostingBlock </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">auto</span><span class="token plain"> first </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> docs_data</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> _doc_base</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">scores_size_t i </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> i </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> kPostingBlock</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">++</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      values_data</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">ReadValue</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">origin</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> first </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> i</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">else</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">auto</span><span class="token plain"> base </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> _doc_base</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token macro property directive-hash" style="color:#36acaa">#</span><span class="token macro property directive keyword" style="color:#00009f">pragma</span><span class="token macro property" style="color:#36acaa"> </span><span class="token macro property expression" style="color:#36acaa">clang loop </span><span class="token macro property expression function" style="color:#d73a49">unroll</span><span class="token macro property expression punctuation" style="color:#393A34">(</span><span class="token macro property expression" style="color:#36acaa">full</span><span class="token macro property expression punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">scores_size_t i </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> i </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> kPostingBlock</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">++</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      values_data</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">ReadValue</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">origin</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> docs_data</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">i</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> base</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p><a href="https://github.com/serenedb/serenedb/blob/main/libs/iresearch/include/iresearch/formats/column/common.cpp" target="_blank" rel="noopener noreferrer">Source on GitHub</a></p>
<p>Zero platform-specific intrinsics. Works on x86, aarch64, wherever clang runs. Faster than hand-written AVX2 gather in every scenario we tested.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="takeaways">Takeaways<a href="https://serenedb.com/blog/norm-gathering#takeaways" class="hash-link" aria-label="Direct link to Takeaways" title="Direct link to Takeaways">​</a></h3>
<p>Sometimes the compiler knows better than you. The dense path auto-vectorizes into perfect SIMD code. Trying to help with unrolling actively hurts.</p>
<p>Sometimes you need to help the compiler. The sparse path has a serial dependency chain that the compiler can't break. Full unrolling exposes enough independent work for the out-of-order engine to overlap.</p>
<p>The combination gives us the best of both worlds. And we got to delete the AVX2 intrinsics.</p>
<p>Microbenchmarking is tricky to get right. Our data fits in L2, the branch predictor sees a fixed pattern length, and we're measuring one function in isolation. If you think we made a mistake or measured something unfairly, open an <a href="https://github.com/serenedb/serenedb/issues" target="_blank" rel="noopener noreferrer">issue</a> — we're happy to discuss it or fix it. The <a href="https://github.com/serenedb/serenedb/blob/main/tests/bench/micro/norm_gather.cpp" target="_blank" rel="noopener noreferrer">benchmark source</a> is in the repo.</p>
<hr>
<p><strong>Other articles in the Search optimization journey series:</strong></p>
<ul>
<li><a href="https://serenedb.com/blog/search-benchmark-game-overview">Benchmark overview</a></li>
<li><a href="https://serenedb.com/blog/search-optimization-1">Search optimization journey 1: Collecting top-K candidates</a></li>
<li><a href="https://serenedb.com/blog/search-optimization-2">Search optimization journey 2: Block scoring</a></li>
<li><a href="https://serenedb.com/blog/iresearch-two-phase-queries">Search optimization journey 4: How To Efficiently Execute Two Phase Queries?</a></li>
<li><a href="https://serenedb.com/blog/search-optimization-5">Search optimization journey 5: Adaptive posting list format</a></li>
</ul>
<p>If you find this work interesting, we would appreciate your support — star us on <a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer">GitHub</a> goes a long way for an early-stage project.</p></div>]]></content>
        <author>
            <name>Valerii Mironov</name>
            <uri>https://www.linkedin.com/in/valery-mironov/</uri>
        </author>
        <category label="Search" term="Search"/>
        <category label="Dev" term="Dev"/>
        <category label="Search optimization journey" term="Search optimization journey"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The C++ search engine that won the game]]></title>
        <id>https://serenedb.com/blog/search-benchmark-game-overview</id>
        <link href="https://serenedb.com/blog/search-benchmark-game-overview"/>
        <updated>2026-03-18T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[SereneDB IResearch: C++ search engine that won the game]]></summary>
        <content type="html"><![CDATA[<div class="hero_RkbL" data-serene-post-hero="true"><header class="header_j7Ab"><div class="avatars_O9G9" aria-hidden="true"><div class="avatar_fQY1" style="z-index:1"><img class="avatarImage_s4Ar" src="https://serenedb.com/blog/img/abramov.jpeg" alt="Andrey Abramov" loading="lazy"></div></div><div class="copy_iHNG"><p class="names_ox2I">Andrey Abramov</p><p class="meta_OgzX">Mar 18, 2026 · 13 minutes read</p></div></header><h2 class="title_RQ9v">The C++ search engine that won the game</h2><p style="font-size:0.9rem;margin-top:-0.4rem">SereneDB IResearch</p><div style="width:100%;height:0.5px;background:var(--ifm-border-color);margin-top:8px"></div></div><div class="postBody_n3cg hideFirstHeading_VVMX" data-image-lightbox-root="true"><h2 class="anchor anchorWithStickyNavbar_LWe7" id="serenedb-iresearch-c-search-engine-that-won-the-game"><strong>SereneDB IResearch: C++ search engine that won the game</strong><a href="https://serenedb.com/blog/search-benchmark-game-overview#serenedb-iresearch-c-search-engine-that-won-the-game" class="hash-link" aria-label="Direct link to serenedb-iresearch-c-search-engine-that-won-the-game" title="Direct link to serenedb-iresearch-c-search-engine-that-won-the-game">​</a></h2>
<p><a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer">SereneDB</a> is a search-OLAP database where search is a first-class citizen of the query engine. All search functionality is powered by <a href="https://github.com/serenedb/serenedb/tree/main/libs/iresearch" target="_blank" rel="noopener noreferrer">IResearch</a>, an open-source C++ information retrieval library that core members of the SereneDB team have been developing since 2016.
An older version of IResearch is the search foundation behind ArangoSearch in ArangoDB, running in production deployments since 2018.</p>
<p>The SereneDB team has made significant architectural changes to IResearch like block-at-a-time vectorized scoring, redesigned top-K collection pipeline, improvements to phrase and conjunction queries and we now want to share the results.
This post is a methodology-transparent comparison of IResearch against the two most relevant open-source alternatives: Lucene and Tantivy.</p>
<p>It is a benchmark built by the Tantivy team, using a corpus and query set of their choosing.
We thought that was a good reason to compete on their terms.
So we <a href="https://github.com/serenedb/search-benchmark-game" target="_blank" rel="noopener noreferrer">forked</a> the repository, added an IResearch engine implementation and ran the numbers. <a href="https://serenedb.com/search-benchmark-game" target="_blank" rel="noopener noreferrer"></a></p><p style="text-transform:uppercase;font-size:12px;font-weight:900;margin-top:12px"><a href="https://serenedb.com/search-benchmark-game" target="_blank" rel="noopener noreferrer">Detailed benchmark results →</a></p><p></p>
<div class="sd-heatmap-root"><style>
.sd-heatmap-root,
.sd-heatmap-root * { box-sizing: border-box; }

.sd-heatmap-root {
  background: transparent;
  width: 100%;
  font-family: var(--ifm-font-family-base);
  padding-top: 20px;
  padding-right: 20px;
  border-radius: 10px
}

.sd-heatmap-layout {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.sd-heatmap-scroll {
  position: relative;
  width: 100%;
  overflow: visible;
}

.sd-heatmap-lines {
  display:none;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.sd-heatmap-grid {
  display: grid;
  min-width: 780px;
  grid-template-columns: 88px repeat(3, minmax(0, 1fr));
  gap: 20px;
}

.sd-heatmap-grid-desktop {
  display: none;
}

.sd-heatmap-grid-mobile {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.sd-heatmap-row-mobile {
  display: grid;
  grid-template-columns: 88px minmax(0, 1fr);
  gap: 12px;
  align-items: start;
}

.sd-heatmap-axis {
  display: grid;
  grid-template-rows: 20px repeat(4, minmax(56px, 1fr));
  gap: 8px;
  padding-bottom: 28px;
}

.sd-heatmap-axis-spacer { width: 1px; }

.sd-heatmap-axis-label {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding-right: 4px;
  font-size: 8px;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(71, 85, 105, 0.75);
}

@media (max-width: 767px) {
  .sd-heatmap-axis {
    grid-template-rows: 14px repeat(4, 40px);
    gap: 4px;
    padding-bottom: 12px;
  }

  .sd-heatmap-axis-label {
    font-size: 7px;
    padding-right: 2px;
  }

  .sd-heatmap-collection-label {
    font-size: 7px;
    line-height: 14px;
  }

  .sd-heatmap-cell {
    min-height: 40px;
    padding: 6px;
  }
}

.sd-heatmap-card {
  position: relative;
  border-radius: 12px;
  backdrop-filter: blur(4px);
}



.sd-heatmap-card-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
}

.sd-heatmap-collection-label {
  padding: 0 4px;
  text-align: center;
  font-size: 8px;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(71, 85, 105, 0.75);
  word-break: break-word;
}

.sd-heatmap-engine-label {
  margin-top: 16px;
  border: 1px solid rgba(148, 163, 184, 0.35);
  border-radius: 8px;
  padding: 8px 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-size: 11px;
  font-weight: 800;
  color: var(--ifm-color-content);
  text-transform: uppercase;
}

.sd-heatmap-engine-badge {
  font-size: 8px;
  line-height: 1;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 999px;
  color: #ffffff;
  background: var(--ifm-serene-blue);
  text-transform: none;
}

.sd-heatmap-cell {
  position: relative;
  display: flex;
  aspect-ratio: 1 / 1;
  min-height: 56px;
  flex-direction: column;
  justify-content: space-between;
  border-radius: 10px;
  border-width: 1px;
  border-style: solid;
  padding: 8px;
  text-align: left;
  cursor: pointer;
  transition: opacity 0.2s ease, filter 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
}

.sd-heatmap-cell:hover { transform: translateY(-1px); }
.sd-heatmap-cell:focus-visible {
  outline: 2px solid var(--ifm-serene-blue);
  outline-offset: 1px;
}

.sd-heatmap-cell--dimmed {
  opacity: 0.3;
  filter: saturate(0.5);
}

.sd-heatmap-cell--active {
  border-style: dashed;
  border-color: var(--ifm-serene-blue) !important;
  box-shadow: none;
}

.sd-heatmap-cell--good { color: #064e3b; }
.sd-heatmap-cell--mid { color: #78350f; }
.sd-heatmap-cell--bad { color: #7f1d1d; }

[data-theme='dark'] .sd-heatmap-axis-label,
[data-theme='dark'] .sd-heatmap-collection-label {
  color: rgba(203, 213, 225, 0.82);
}

[data-theme='dark'] .sd-heatmap-engine-label { color: #e2e8f0; }
[data-theme='dark'] .sd-heatmap-engine-badge {
  color: #ffffff;
  background: var(--ifm-serene-blue);
}
[data-theme='dark'] .sd-heatmap-cell--good { color: #d1fae5; }
[data-theme='dark'] .sd-heatmap-cell--mid { color: #fef3c7; }
[data-theme='dark'] .sd-heatmap-cell--bad { color: #fee2e2; }

.sd-heatmap-cell-value {
  font-size: 15px;
  line-height: 1;
  font-weight: 800;
}

.sd-heatmap-cell-delta {
  font-size: 8px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.sd-heatmap-footnote {
  border: 1px solid rgba(148, 163, 184, 0.25);
  background: rgba(148, 163, 184, 0.08);
  border-radius: 8px;
  padding: 8px;
}

.sd-heatmap-footnote p {
  margin: 0;
  text-align: center;
  font-size: 12px;
  color: rgba(71, 85, 105, 0.8);
}

@media (min-width: 1024px) {
  .sd-heatmap-scroll {
    overflow-x: auto;
  }

  .sd-heatmap-grid-desktop {
    display: grid;
  }

  .sd-heatmap-grid-mobile {
    display: none;
  }
}
.sd-heatmap-footnote{
    margin-left: 107px;
   }


@media (min-width: 1280px) {
  .sd-heatmap-footnote{
    margin-left: 107px;
  }
  .sd-heatmap-layout {
  }
}

@media (max-width: 1024px) {
     .sd-heatmap-layout{
     margin-left: auto;
     margin-right:auto;
     padding-right: 30px;
     max-width: 400px;
   }
}

@media (max-width: 768px) {
   .sd-heatmap-layout {
     padding-right: 70px;
     max-width: 400px;
   }
   .sd-heatmap-footnote {
     margin-left: 100px;
   }
}

@media (max-width: 500px) {
   .sd-heatmap-layout {
     padding-right: 0px;
     margin-left: -70px;
     max-width: 400px;
   }
   .sd-heatmap-footnote {
     margin-left: 100px;
   }
}
</style><div class="sd-heatmap-layout"><div class="sd-heatmap-scroll"><svg aria-hidden="true" class="sd-heatmap-lines"></svg><div class="sd-heatmap-grid sd-heatmap-grid-desktop"><div class="sd-heatmap-axis"><div class="sd-heatmap-axis-spacer"></div><div class="sd-heatmap-axis-label">OR</div><div class="sd-heatmap-axis-label">AND</div><div class="sd-heatmap-axis-label">phrase</div><div class="sd-heatmap-axis-label">other</div></div><div class="sd-heatmap-card"><div class="sd-heatmap-card-grid"><div class="sd-heatmap-collection-label">count</div><div class="sd-heatmap-collection-label">top_100</div><div class="sd-heatmap-collection-label">top_100 count</div><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="union" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">253</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="union" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">651</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="union" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">1374</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="intersection" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">236</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="intersection" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">475</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="intersection" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">474</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="phrase" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">796</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="phrase" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">800</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="phrase" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">804</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="other" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">905</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="other" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">982</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="other" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">1115</span><span class="sd-heatmap-cell-delta">best</span></button></div><div class="sd-heatmap-engine-label"><span>iresearch</span><span class="sd-heatmap-engine-badge">by SereneDB</span></div></div><div class="sd-heatmap-card sd-heatmap-card--with-separator"><div class="sd-heatmap-card-grid"><div class="sd-heatmap-collection-label">count</div><div class="sd-heatmap-collection-label">top_100</div><div class="sd-heatmap-collection-label">top_100 count</div><button type="button" data-heatmap-engine="lucene" data-heatmap-query="union" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.29524684067549517);border-color:rgba(217, 119, 6, 0.396807346167416)"><span class="sd-heatmap-cell-value">379</span><span class="sd-heatmap-cell-delta">+49.8%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="union" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.23171923175992337);border-color:rgba(217, 119, 6, 0.34344415467833567)"><span class="sd-heatmap-cell-value">797</span><span class="sd-heatmap-cell-delta">+22.3%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="union" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.5969958738195035);border-color:rgba(153, 27, 27, 0.7455795510012115)"><span class="sd-heatmap-cell-value">4648</span><span class="sd-heatmap-cell-delta">+238.3%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="intersection" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.23700516344900746);border-color:rgba(217, 119, 6, 0.3478843372971663)"><span class="sd-heatmap-cell-value">294</span><span class="sd-heatmap-cell-delta">+24.6%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="intersection" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.2156759746979655);border-color:rgba(217, 119, 6, 0.329967818746291)"><span class="sd-heatmap-cell-value">548</span><span class="sd-heatmap-cell-delta">+15.4%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="intersection" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.3017667307227304);border-color:rgba(217, 119, 6, 0.40228405380709353)"><span class="sd-heatmap-cell-value">723</span><span class="sd-heatmap-cell-delta">+52.6%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="phrase" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.4284455302636617);border-color:rgba(153, 27, 27, 0.5910750694083566)"><span class="sd-heatmap-cell-value">1542</span><span class="sd-heatmap-cell-delta">+93.8%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="phrase" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.3754428298316831);border-color:rgba(153, 27, 27, 0.5424892606790428)"><span class="sd-heatmap-cell-value">1359</span><span class="sd-heatmap-cell-delta">+69.9%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="phrase" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.4470104997215848);border-color:rgba(153, 27, 27, 0.6080929580781195)"><span class="sd-heatmap-cell-value">1625</span><span class="sd-heatmap-cell-delta">+102.1%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="other" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.2984640738999065);border-color:rgba(217, 119, 6, 0.3995098220759214)"><span class="sd-heatmap-cell-value">1367</span><span class="sd-heatmap-cell-delta">+51.2%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="other" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.33250223876162577);border-color:rgba(217, 119, 6, 0.42810188055976567)"><span class="sd-heatmap-cell-value">1628</span><span class="sd-heatmap-cell-delta">+65.9%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="other" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.43602529419523695);border-color:rgba(217, 119, 6, 0.515061247123999)"><span class="sd-heatmap-cell-value">2347</span><span class="sd-heatmap-cell-delta">+110.6%</span></button></div><div class="sd-heatmap-engine-label"><span>lucene</span></div></div><div class="sd-heatmap-card sd-heatmap-card--with-separator"><div class="sd-heatmap-card-grid"><div class="sd-heatmap-collection-label">count</div><div class="sd-heatmap-collection-label">top_100</div><div class="sd-heatmap-collection-label">top_100 count</div><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="union" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.5926622405992304);border-color:rgba(153, 27, 27, 0.7416070538826279)"><span class="sd-heatmap-cell-value">817</span><span class="sd-heatmap-cell-delta">+222.5%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="union" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.4570090985760763);border-color:rgba(153, 27, 27, 0.6172583403614034)"><span class="sd-heatmap-cell-value">1346</span><span class="sd-heatmap-cell-delta">+106.6%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="union" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.4934149998976522);border-color:rgba(217, 119, 6, 0.5632685999140279)"><span class="sd-heatmap-cell-value">3233</span><span class="sd-heatmap-cell-delta">+135.4%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="intersection" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.4015821993520682);border-color:rgba(153, 27, 27, 0.5664503494060625)"><span class="sd-heatmap-cell-value">428</span><span class="sd-heatmap-cell-delta">+81.7%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="intersection" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.35160597530080806);border-color:rgba(153, 27, 27, 0.5206388106924075)"><span class="sd-heatmap-cell-value">756</span><span class="sd-heatmap-cell-delta">+59.2%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="intersection" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.3615481599204989);border-color:rgba(153, 27, 27, 0.529752479927124)"><span class="sd-heatmap-cell-value">776</span><span class="sd-heatmap-cell-delta">+63.7%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="phrase" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.2460016779038526);border-color:rgba(217, 119, 6, 0.3554414094392362)"><span class="sd-heatmap-cell-value">1023</span><span class="sd-heatmap-cell-delta">+28.5%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="phrase" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.2517326510031023);border-color:rgba(217, 119, 6, 0.3602554268426059)"><span class="sd-heatmap-cell-value">1048</span><span class="sd-heatmap-cell-delta">+31.0%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="phrase" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.25212343760925354);border-color:rgba(217, 119, 6, 0.36058368759177295)"><span class="sd-heatmap-cell-value">1054</span><span class="sd-heatmap-cell-delta">+31.2%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="other" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.7);border-color:rgba(153, 27, 27, 0.8400000000000001)"><span class="sd-heatmap-cell-value">6477</span><span class="sd-heatmap-cell-delta">+616.1%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="other" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.6903374694085057);border-color:rgba(153, 27, 27, 0.8311426802911304)"><span class="sd-heatmap-cell-value">6681</span><span class="sd-heatmap-cell-delta">+580.6%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="other" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.6758882674632718);border-color:rgba(153, 27, 27, 0.8178975785079992)"><span class="sd-heatmap-cell-value">6996</span><span class="sd-heatmap-cell-delta">+527.6%</span></button></div><div class="sd-heatmap-engine-label"><span>tantivy</span></div></div></div><div class="sd-heatmap-grid-mobile"><div class="sd-heatmap-row-mobile"><div class="sd-heatmap-axis"><div class="sd-heatmap-axis-spacer"></div><div class="sd-heatmap-axis-label">OR</div><div class="sd-heatmap-axis-label">AND</div><div class="sd-heatmap-axis-label">phrase</div><div class="sd-heatmap-axis-label">other</div></div><div class="sd-heatmap-card"><div class="sd-heatmap-card-grid"><div class="sd-heatmap-collection-label">count</div><div class="sd-heatmap-collection-label">top_100</div><div class="sd-heatmap-collection-label">top_100 count</div><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="union" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">253</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="union" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">651</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="union" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">1374</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="intersection" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">236</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="intersection" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">475</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="intersection" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">474</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="phrase" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">796</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="phrase" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">800</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="phrase" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">804</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="other" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">905</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="other" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">982</span><span class="sd-heatmap-cell-delta">best</span></button><button type="button" data-heatmap-engine="iresearch" data-heatmap-query="other" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--good" style="background-color:rgba(16, 185, 129, 0.42);border-color:rgba(5, 150, 105, 0.72)"><span class="sd-heatmap-cell-value">1115</span><span class="sd-heatmap-cell-delta">best</span></button></div><div class="sd-heatmap-engine-label"><span>iresearch</span><span class="sd-heatmap-engine-badge">by SereneDB</span></div></div></div><div class="sd-heatmap-row-mobile"><div class="sd-heatmap-axis"><div class="sd-heatmap-axis-spacer"></div><div class="sd-heatmap-axis-label">OR</div><div class="sd-heatmap-axis-label">AND</div><div class="sd-heatmap-axis-label">phrase</div><div class="sd-heatmap-axis-label">other</div></div><div class="sd-heatmap-card sd-heatmap-card--with-separator"><div class="sd-heatmap-card-grid"><div class="sd-heatmap-collection-label">count</div><div class="sd-heatmap-collection-label">top_100</div><div class="sd-heatmap-collection-label">top_100 count</div><button type="button" data-heatmap-engine="lucene" data-heatmap-query="union" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.29524684067549517);border-color:rgba(217, 119, 6, 0.396807346167416)"><span class="sd-heatmap-cell-value">379</span><span class="sd-heatmap-cell-delta">+49.8%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="union" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.23171923175992337);border-color:rgba(217, 119, 6, 0.34344415467833567)"><span class="sd-heatmap-cell-value">797</span><span class="sd-heatmap-cell-delta">+22.3%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="union" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.5969958738195035);border-color:rgba(153, 27, 27, 0.7455795510012115)"><span class="sd-heatmap-cell-value">4648</span><span class="sd-heatmap-cell-delta">+238.3%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="intersection" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.23700516344900746);border-color:rgba(217, 119, 6, 0.3478843372971663)"><span class="sd-heatmap-cell-value">294</span><span class="sd-heatmap-cell-delta">+24.6%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="intersection" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.2156759746979655);border-color:rgba(217, 119, 6, 0.329967818746291)"><span class="sd-heatmap-cell-value">548</span><span class="sd-heatmap-cell-delta">+15.4%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="intersection" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.3017667307227304);border-color:rgba(217, 119, 6, 0.40228405380709353)"><span class="sd-heatmap-cell-value">723</span><span class="sd-heatmap-cell-delta">+52.6%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="phrase" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.4284455302636617);border-color:rgba(153, 27, 27, 0.5910750694083566)"><span class="sd-heatmap-cell-value">1542</span><span class="sd-heatmap-cell-delta">+93.8%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="phrase" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.3754428298316831);border-color:rgba(153, 27, 27, 0.5424892606790428)"><span class="sd-heatmap-cell-value">1359</span><span class="sd-heatmap-cell-delta">+69.9%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="phrase" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.4470104997215848);border-color:rgba(153, 27, 27, 0.6080929580781195)"><span class="sd-heatmap-cell-value">1625</span><span class="sd-heatmap-cell-delta">+102.1%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="other" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.2984640738999065);border-color:rgba(217, 119, 6, 0.3995098220759214)"><span class="sd-heatmap-cell-value">1367</span><span class="sd-heatmap-cell-delta">+51.2%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="other" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.33250223876162577);border-color:rgba(217, 119, 6, 0.42810188055976567)"><span class="sd-heatmap-cell-value">1628</span><span class="sd-heatmap-cell-delta">+65.9%</span></button><button type="button" data-heatmap-engine="lucene" data-heatmap-query="other" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.43602529419523695);border-color:rgba(217, 119, 6, 0.515061247123999)"><span class="sd-heatmap-cell-value">2347</span><span class="sd-heatmap-cell-delta">+110.6%</span></button></div><div class="sd-heatmap-engine-label"><span>lucene</span></div></div></div><div class="sd-heatmap-row-mobile"><div class="sd-heatmap-axis"><div class="sd-heatmap-axis-spacer"></div><div class="sd-heatmap-axis-label">OR</div><div class="sd-heatmap-axis-label">AND</div><div class="sd-heatmap-axis-label">phrase</div><div class="sd-heatmap-axis-label">other</div></div><div class="sd-heatmap-card sd-heatmap-card--with-separator"><div class="sd-heatmap-card-grid"><div class="sd-heatmap-collection-label">count</div><div class="sd-heatmap-collection-label">top_100</div><div class="sd-heatmap-collection-label">top_100 count</div><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="union" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.5926622405992304);border-color:rgba(153, 27, 27, 0.7416070538826279)"><span class="sd-heatmap-cell-value">817</span><span class="sd-heatmap-cell-delta">+222.5%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="union" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.4570090985760763);border-color:rgba(153, 27, 27, 0.6172583403614034)"><span class="sd-heatmap-cell-value">1346</span><span class="sd-heatmap-cell-delta">+106.6%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="union" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.4934149998976522);border-color:rgba(217, 119, 6, 0.5632685999140279)"><span class="sd-heatmap-cell-value">3233</span><span class="sd-heatmap-cell-delta">+135.4%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="intersection" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.4015821993520682);border-color:rgba(153, 27, 27, 0.5664503494060625)"><span class="sd-heatmap-cell-value">428</span><span class="sd-heatmap-cell-delta">+81.7%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="intersection" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.35160597530080806);border-color:rgba(153, 27, 27, 0.5206388106924075)"><span class="sd-heatmap-cell-value">756</span><span class="sd-heatmap-cell-delta">+59.2%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="intersection" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.3615481599204989);border-color:rgba(153, 27, 27, 0.529752479927124)"><span class="sd-heatmap-cell-value">776</span><span class="sd-heatmap-cell-delta">+63.7%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="phrase" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.2460016779038526);border-color:rgba(217, 119, 6, 0.3554414094392362)"><span class="sd-heatmap-cell-value">1023</span><span class="sd-heatmap-cell-delta">+28.5%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="phrase" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.2517326510031023);border-color:rgba(217, 119, 6, 0.3602554268426059)"><span class="sd-heatmap-cell-value">1048</span><span class="sd-heatmap-cell-delta">+31.0%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="phrase" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--mid" style="background-color:rgba(245, 158, 11, 0.25212343760925354);border-color:rgba(217, 119, 6, 0.36058368759177295)"><span class="sd-heatmap-cell-value">1054</span><span class="sd-heatmap-cell-delta">+31.2%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="other" data-heatmap-collection="COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.7);border-color:rgba(153, 27, 27, 0.8400000000000001)"><span class="sd-heatmap-cell-value">6477</span><span class="sd-heatmap-cell-delta">+616.1%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="other" data-heatmap-collection="TOP_100" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.6903374694085057);border-color:rgba(153, 27, 27, 0.8311426802911304)"><span class="sd-heatmap-cell-value">6681</span><span class="sd-heatmap-cell-delta">+580.6%</span></button><button type="button" data-heatmap-engine="tantivy" data-heatmap-query="other" data-heatmap-collection="TOP_100_COUNT" class="sd-heatmap-cell sd-heatmap-cell--bad" style="background-color:rgba(220, 38, 38, 0.6758882674632718);border-color:rgba(153, 27, 27, 0.8178975785079992)"><span class="sd-heatmap-cell-value">6996</span><span class="sd-heatmap-cell-delta">+527.6%</span></button></div><div class="sd-heatmap-engine-label"><span>tantivy</span></div></div></div></div></div><div class="sd-heatmap-footnote"><p>All values have been measured in microseconds</p></div></div></div>
<hr>
<p>If you find this interesting, we'd be grateful if you support SereneDB with a star on <a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer">GitHub</a>.
For an early-stage project, it means more than you might think.</p>
<hr>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="iresearch">IResearch<a href="https://serenedb.com/blog/search-benchmark-game-overview#iresearch" class="hash-link" aria-label="Direct link to IResearch" title="Direct link to IResearch">​</a></h3>
<p>IResearch is an embeddable C++ search engine built to live inside database kernels by SereneDB team.
It provides a comprehensive feature set like Lucene — inverted indexes with BM25/TFIDF scoring, columnar storage, HNSW-based vector search, S2 geospatial indexing, semi-structured filtering and a pluggable NLP pipeline covering many languages — while integrating natively with the host database's write-ahead log and transaction protocol.
There is no separate process, no network hop and no consistency gap: IResearch participates directly in the host's commit protocol, giving search queries read-committed visibility over the same data the primary storage sees.
A complete description of IResearch's capabilities is available at <a href="https://github.com/serenedb/serenedb/tree/main/libs/iresearch" target="_blank" rel="noopener noreferrer">IResearch</a>.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="search-benchmark-the-game">Search Benchmark, the Game<a href="https://serenedb.com/blog/search-benchmark-game-overview#search-benchmark-the-game" class="hash-link" aria-label="Direct link to Search Benchmark, the Game" title="Direct link to Search Benchmark, the Game">​</a></h2>
<p>The database world has no shortage of well-established benchmarks.
TPC-H has been the standard for analytical query performance since 1999;
TPC-C covers transactional workloads;
ClickBench, introduced by ClickHouse, has become a standard for OLAP query benchmarking.
Each of these has a fixed schema, a fixed query set and a reproducible methodology that makes cross-engine comparison credible.</p>
<p>Search benchmarks are, by comparison, surprisingly thin.
Lucene has the great <a href="https://benchmarks.mikemccandless.com/" target="_blank" rel="noopener noreferrer">nightly benchmark suite</a>.
It tracks indexing throughput, query latency and segment merge performance over time.
It is an excellent tool for tracking Lucene's own regression history, but it is not easily extensible to other engines.</p>
<p>The most suitable option for a fair multi-engine comparison is the <a href="https://github.com/quickwit-oss/search-benchmark-game" target="_blank" rel="noopener noreferrer">search-benchmark-game</a>, originally developed by the Tantivy team and maintained by Quickwit.
It provides a standardized corpus of the English Wikipedia — a fixed query workload derived from the AOL query dataset and a simple harness that any engine can implement by satisfying a small interface.
The benchmark covers the query types that matter in practice: single-term lookups, phrase queries, boolean conjunctions and disjunctions and count-only variants of each.
Results are reported as best-of-100 single-threaded throughput, with Lucene's query cache disabled and Java's JIT warmed up before timing begins.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="corpus">Corpus<a href="https://serenedb.com/blog/search-benchmark-game-overview#corpus" class="hash-link" aria-label="Direct link to Corpus" title="Direct link to Corpus">​</a></h3>
<p>The corpus is the English Wikipedia, pre-processed into a single JSON file.
Stemming is disabled.
Queries are derived from the AOL query dataset — specifically, multi-term queries that return at least one result when searched as a phrase.
They contain no personal information.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="execution">Execution<a href="https://serenedb.com/blog/search-benchmark-game-overview#execution" class="hash-link" aria-label="Direct link to Execution" title="Direct link to Execution">​</a></h3>
<p>Each engine is benchmarked independently in a single-threaded mode.
The benchmark driver communicates with each engine over stdin/stdout, issuing one query at a time and measuring wall-clock time with <code>time.monotonic()</code>.</p>
<p>Before timing begins, each engine goes through a <strong>60-second warmup phase</strong> where all queries are issued repeatedly in shuffled order.
This ensures the index data is fully loaded into the OS page cache and, for JVM-based engines like Lucene, that the JIT compiler has had time to compile the hot paths.
All benchmarks are therefore warm-cache measurements.</p>
<p>After warmup, the full query set is run <strong>10 times</strong> in the same shuffled order.
All 10 durations are recorded per query.
The reported per-query latency is the <strong>median</strong> across the 10 runs.
Aggregate statistics across all queries are reported as <strong>average, P50, P90 and P99</strong>.</p>
<p>Queries are shuffled with a fixed random seed, ensuring reproducibility and preventing any systematic ordering effects from favouring engines with sequential access patterns.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="what-is-measured">What is measured<a href="https://serenedb.com/blog/search-benchmark-game-overview#what-is-measured" class="hash-link" aria-label="Direct link to What is measured" title="Direct link to What is measured">​</a></h3>
<p>Each query measures end-to-end latency as seen by the benchmark driver — from writing the query to stdin to reading the result count from stdout.
This includes any serialization overhead, but engines are simple in-process servers so this cost is negligible.
No network stack is involved.</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="query-types">Query Types<a href="https://serenedb.com/blog/search-benchmark-game-overview#query-types" class="hash-link" aria-label="Direct link to Query Types" title="Direct link to Query Types">​</a></h4>
<p><strong>Term</strong> — A single high-frequency stop word (<code>the</code>). The posting list for such a term spans nearly the entire corpus, leaving no room for skipping or early termination. This is a degenerate but important stress test for raw posting list traversal speed.</p>
<p><strong>Intersection</strong> — All terms are required, expressed with a <code>+</code> prefix on every token (e.g. <code>+griffith +observatory</code>). This is the most selective query type, typically matching the fewest documents. It heavily exercises conjunctive traversal and skipping efficiency. Engines that implement score pruning can avoid visiting large portions of the posting lists.</p>
<p><strong>Union</strong> — All terms are optional with no prefix (e.g. <code>griffith observatory</code>). This matches the broadest set of documents and is typically the most expensive query type, as the engine must merge many posting lists and score far more candidates. It stresses score collector management and the cost of BM25 scoring at scale.</p>
<p><strong>Phrase</strong> — Terms must appear consecutively and in order, wrapped in quotes (e.g. <code>"griffith observatory"</code>). This requires the engine to verify positional information after the initial term match, adding overhead on top of standard intersection traversal. Phrase queries tend to match very few documents, but the position-checking step makes them more expensive per candidate than a plain intersection.</p>
<p><strong>IntersectionUnion</strong> — Some terms carry a + prefix (mandatory) while others are bare (optional, scoring only). For example, <code>+climate policy</code> will only return documents containing "climate", but documents that also mention "policy" will rank higher. This is the default query mode in Lucene and reflects typical real-world search behavior, namely the mandatory terms act as a hard filter, while the optional terms act as a ranking signal. It tests the engine's ability to efficiently combine strict filtering with scoring-time boosting. Also known as <strong>Reqiured/Optional</strong> query.</p>
<p><strong>Negated</strong> — One required term combined with one or more excluded terms (e.g. <code>+python -snake -monty</code>). The engine must match documents containing the required term while filtering out any that contain the excluded terms. Queries are designed around ambiguous words like Java, Jaguar, Mercury, Apple where the negated terms carve out a specific semantic meaning. This tests how efficiently an engine handles exclusion without degrading traversal of the primary posting list.</p>
<p><strong>Two-phase critic</strong> — A single query combining a phrase requirement with an additional required term: <code>+"the who" +uk</code>. This forces a two-phase evaluation where the engine first finds phrase matches and then filters by the second term. It is a targeted stress test for engines that handle phrase and term constraints in separate passes.</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="collection-types">Collection Types<a href="https://serenedb.com/blog/search-benchmark-game-overview#collection-types" class="hash-link" aria-label="Direct link to Collection Types" title="Direct link to Collection Types">​</a></h4>
<p><strong>COUNT</strong> — Only the total number of matching documents is returned. No results are fetched and no scoring is performed. This isolates raw posting list traversal speed in its purest form. The engine can skip scoring entirely and potentially use shortcuts unavailable when ranking is required.</p>
<p><strong>TOP 100</strong> — The 100 highest-scoring documents by BM25 are returned. This mode tests the engine's ability to evaluate documents, maintain a priority queue of the best candidates and utilize dynamic pruning strategies (like WAND/Maxscore or Block-Max WAND/Maxscore). The engine attempts to quickly establish a high enough competitive threshold to safely skip evaluating lower-scoring documents, measuring how efficiently the engine can short-circuit the scoring process while still fulfilling the result quota.</p>
<p><strong>TOP 100 COUNT</strong> —  Both the top 100 scored results and the exact total match count are returned. Crucially, requiring the total hit count alongside ranked results typically disables dynamic pruning optimizations entirely. Every single matching document must be fully traversed and scored. This is the most demanding mode, but it provides vital insight into the raw throughput of the scoring function itself, completely free from the interference of early-termination algorithms. This metric is especially valuable for use cases where ranking is implemented externally, allowing engineers to measure the pure speed of the engine's scoring phase.</p>
<div class="inline_uTt6" data-serene-cta="inline"><div class="inlineRow_KSr0"><div class="rule_iSYF" aria-hidden="true"></div><a class="button_Zmt9 buttonGithub_qDw8" href="https://github.com/serenedb/serenedb" rel="noreferrer" target="_blank" aria-label="Star SereneDB on GitHub (opens in a new tab)"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" fill="none" viewBox="0 0 17 16" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8.5 0C3.804 0 0 3.67 0 8.202c0 3.63 2.433 6.696 5.812 7.782.425.072.584-.174.584-.39 0-.194-.01-.84-.01-1.527-2.136.38-2.689-.502-2.859-.964-.095-.236-.51-.963-.87-1.158-.298-.154-.723-.534-.011-.544.669-.01 1.147.595 1.307.841.765 1.24 1.986.892 2.475.677.075-.534.298-.892.542-1.097-1.891-.205-3.867-.913-3.867-4.05 0-.892.329-1.63.87-2.205-.084-.205-.382-1.045.086-2.173 0 0 .712-.216 2.337.84a8.2 8.2 0 0 1 2.125-.276c.723 0 1.445.092 2.125.276 1.626-1.066 2.338-.84 2.338-.84.467 1.128.17 1.968.085 2.173.542.575.871 1.302.871 2.205 0 3.147-1.987 3.845-3.878 4.05.308.256.574.748.574 1.517 0 1.097-.011 1.979-.011 2.256 0 .215.16.471.584.39C14.567 14.897 17 11.821 17 8.201 17 3.671 13.196 0 8.5 0" clip-rule="evenodd"></path></svg><span>Star Us</span></a><a class="button_Zmt9 buttonContact_p8xH" href="https://serenedb.com/form">Contact Us</a><div class="rule_iSYF" aria-hidden="true"></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-good-the-bad-the-ugly">The Good, the Bad, the Ugly<a href="https://serenedb.com/blog/search-benchmark-game-overview#the-good-the-bad-the-ugly" class="hash-link" aria-label="Direct link to The Good, the Bad, the Ugly" title="Direct link to The Good, the Bad, the Ugly">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-good">The Good<a href="https://serenedb.com/blog/search-benchmark-game-overview#the-good" class="hash-link" aria-label="Direct link to The Good" title="Direct link to The Good">​</a></h3>
<p><strong>Isolated search benchmark.</strong> The benchmark is focused on search retrieval performance. Each engine runs as a simple in-process server communicating over stdin/stdout, with no network stack and no unrelated system activity in the measurement loop. What you measure is the search engine.</p>
<p><strong>Reproducibility.</strong> The corpus (English Wikipedia), the query set and the execution parameters are all fixed and publicly available. Queries are shuffled with a fixed random seed. Anyone can clone the repository, run <code>make bench</code> and get comparable results on their own hardware.</p>
<p><strong>Disk I/O is deliberately eliminated.</strong> The benchmark runs a 60-second warmup phase before any timing begins, issuing all queries repeatedly to ensure the index is fully resident in the OS page cache. The corpus is approximately 5 GB. It's small enough to fit entirely in RAM and no disk reads occur during measurement after warmup. This isolates retrieval algorithm performance from storage hardware differences.</p>
<p><strong>Extensive scoring coverage.</strong> Every query is benchmarked across three collection modes — COUNT, TOP 100 and TOP 100 COUNT. This cleanly separates traversal cost from ranking cost and ranking cost from the additional overhead of exact hit counting.</p>
<p><strong>Phrase queries.</strong> Many search benchmarks skip phrase queries entirely. This one includes them as a first-class query type, which matters because position list decoding and adjacency checking are a meaningfully different workload from plain term or boolean queries.</p>
<p><strong>Realistic queries and dataset.</strong> The queries are derived from the AOL query dataset and the corpus is Wikipedia. Both are quite close to real-world search workloads. The queries cover a natural distribution of specificities, from narrow named entities like <code>"robert green ingersoll"</code> to broad concepts like <code>digital scanning</code>.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-bad">The Bad<a href="https://serenedb.com/blog/search-benchmark-game-overview#the-bad" class="hash-link" aria-label="Direct link to The Bad" title="Direct link to The Bad">​</a></h3>
<p><strong>No deletions, no updates.</strong> The benchmark indexes a static corpus once and never modifies it. Real search engines spend significant effort managing segment merges, tombstone filtering and live/deleted document tracking. A single consolidated index segment with no deleted documents is the best-case scenario for any engine and it doesn't reflect production conditions.</p>
<p><strong>Single index segment.</strong> All engines operate on one fully-merged segment. Multi-segment indexes are the norm in production (Lucene-based systems in particular) and segment merging, per-segment overhead and cross-segment result merging all have real costs that this benchmark never exercises.</p>
<p><strong>No field values, no sort-by-field.</strong> The benchmark only measures full-text retrieval ranked by BM25. There is no access to stored fields, doc values, or numeric attributes. Sorting by date, filtering by category, or retrieving stored fields alongside results — all common in real applications — are entirely absent.</p>
<p><strong>Limited query complexity.</strong> The query set covers term, phrase, union, intersection, mandatory/optional, negation and one two-phase query. There are no range queries, no faceted queries, no nested boolean structures, no wildcard or fuzzy queries. Real-world query workloads are considerably more varied.</p>
<p><strong>Indexing performance is not measured.</strong> The benchmark only measures query latency. Index build time, indexing throughput and memory usage during indexing are not captured, even though these are often primary concerns when evaluating a search engine.</p>
<p><strong>The query difficulty distribution is skewed toward easy queries.</strong> Looking at the actual result counts from the benchmark (using Tantivy 0.25 as a reference), 60% of phrase queries return fewer than 100 results and 53% of intersection queries return fewer than 1,000 results. These are queries that complete in microseconds. In production search, the high-cardinality queries — the ones that match hundreds of thousands of documents and take tens of milliseconds — are typically far more common, since users tend to search with short, broad terms. The benchmark has them (union queries have a median result count of ~170k and the single <code>the</code> term query matches 4.5M documents), but they are outnumbered by the many narrow phrase and intersection queries that any engine handles trivially.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="next-steps">Next Steps<a href="https://serenedb.com/blog/search-benchmark-game-overview#next-steps" class="hash-link" aria-label="Direct link to Next Steps" title="Direct link to Next Steps">​</a></h2>
<p>The benchmark results above give us a clear picture of where things stand today. They also point directly at what needs to be done next — both in terms of improving the benchmark itself and pushing SereneDB's performance further.</p>
<p><strong>End-to-end search benchmarks.</strong> Isolated retrieval benchmarks like search-benchmark-game are valuable, but they only tell part of the story. We are planning a ClickBench-style benchmark for search engines and databases — a full end-to-end comparison that puts SereneDB up against established players like Elasticsearch and OpenSearch on realistic workloads, measuring not just query latency but indexing throughput, resource usage and overall system behavior under load.</p>
<p><strong>Extending the query set.</strong> As outlined in the weaknesses above, the current query set in search-benchmark-game has limited structural diversity. We plan to contribute additional query types — range queries, nested boolean expressions, fuzzy and wildcard queries — to make the benchmark more representative of real-world search workloads and more useful to the broader community.</p>
<p><strong>Indexing performance.</strong> Search-benchmark-game currently measures query performance only. We plan to add an indexing benchmark track, covering ingestion throughput, segment merge behavior and resource consumption during index build. This is an important gap, especially when comparing engines with fundamentally different indexing architectures.</p>
<p><strong>Making IResearch even faster.</strong> IResearch already performs strongly in the current benchmark results. We have a concrete set of improvements planned that we expect to push performance further — see for the full <a href="https://github.com/serenedb/serenedb/issues/364" target="_blank" rel="noopener noreferrer">roadmap</a>.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="closing-thoughts">Closing Thoughts<a href="https://serenedb.com/blog/search-benchmark-game-overview#closing-thoughts" class="hash-link" aria-label="Direct link to Closing Thoughts" title="Direct link to Closing Thoughts">​</a></h2>
<p>SereneDB is committed to continuing development of IResearch as a standalone library, so that C++ native projects can benefit from enterprise-grade search capabilities without the burden of shipping a JVM dependency.
For teams building in C++ today, that is often a non-starter and IResearch gives them a way out.</p>
<p>If you like what we're doing, we'd be grateful if you support SereneDB with a star on <a href="https://github.com/serenedb/serenedb" target="_blank" rel="noopener noreferrer">GitHub</a>.</p>
<p>For those interested in the technical internals behind these results, we have written a series of deep-dives covering the key architectural decisions in IResearch:</p>
<ul>
<li><a href="https://serenedb.com/blog/search-optimization-1">Search optimization journey 1: Collecting top-K candidates</a></li>
<li><a href="https://serenedb.com/blog/search-optimization-2">Search optimization journey 2: Block scoring</a></li>
<li><a href="https://serenedb.com/blog/norm-gathering">Search optimization journey 3: Optimize norm gathering</a></li>
<li><a href="https://serenedb.com/blog/iresearch-two-phase-queries">Search optimization journey 4: How To Efficiently Execute Two Phase Queries?</a></li>
<li><a href="https://serenedb.com/blog/search-optimization-5">Search optimization journey 5: Adaptive posting list format</a></li>
</ul>
<p>Stay tuned!</p></div>]]></content>
        <author>
            <name>Andrey Abramov</name>
            <uri>https://www.linkedin.com/in/andrey-abramov-57a16241/ru/?skipRedirect=true</uri>
        </author>
        <category label="Database" term="Database"/>
        <category label="Search" term="Search"/>
        <category label="Search optimization journey" term="Search optimization journey"/>
    </entry>
</feed>