<?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://gnarl.dev/blog</id>
    <title>Gnarl blog</title>
    <updated>2026-08-15T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://gnarl.dev/blog"/>
    <subtitle>Gnarl Blog</subtitle>
    <icon>https://gnarl.dev/img/favicon.png</icon>
    <rights>© 2026 Lucenia</rights>
    <entry>
        <title type="html"><![CDATA[Why a St. Bernard]]></title>
        <id>https://gnarl.dev/blog/why-a-st-bernard</id>
        <link href="https://gnarl.dev/blog/why-a-st-bernard"/>
        <updated>2026-08-15T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The mascot is not a joke. It is the shortest description of what the system does.]]></summary>
        <content type="html"><![CDATA[<p>People keep asking about the dog. His name is Gnarly.</p>
<p>A St. Bernard finds what is buried. Not in a controlled environment, not with
precise coordinates — in bad weather, across terrain nobody mapped, working from
a faint signal and its own judgment. Then it brings back something useful.</p>
<p>That is the whole product description.</p>
<p>Centralized search is the opposite metaphor. It says: before we can find
anything, move the mountain here. Sort it into our bins. Then we will search it,
quickly, in ideal conditions.</p>
<p>That works right up until the mountain is too heavy to move — or legally cannot
be moved at all. Which, increasingly, is the normal case.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-barrel">The barrel<a href="https://gnarl.dev/blog/why-a-st-bernard#the-barrel" class="hash-link" aria-label="Direct link to The barrel" title="Direct link to The barrel" translate="no">​</a></h2>
<p>The barrel carries the Gnarl knot: an unbroken loop with no beginning and no
center. That is the topology. There is no head node in a knot.</p>
<p>It is also, historically, a myth — St. Bernards never actually carried brandy
barrels; a painter added one in 1820 and it stuck. We kept it anyway, because the
useful part of the image is that the dog arrives carrying something, not what is
in the barrel.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-cover">The cover<a href="https://gnarl.dev/blog/why-a-st-bernard#the-cover" class="hash-link" aria-label="Direct link to The cover" title="Direct link to The cover" translate="no">​</a></h2>
<p>There is a second reason. The St. Bernard on the cover of <em>Scaling Search and
Retrieval for Contextual AI</em> is the same dog. O'Reilly picks the animal; the
author does not. Getting a search rescue dog for a book about finding things in
enormous piles was luck, and it seemed wasteful not to keep it.</p>
<p>So the mascot came before the product, and the product turned out to be about
exactly what the mascot already did.</p>
<p>— <a href="https://scalingsearch.ai/" target="_blank" rel="noopener noreferrer" class="">Read the book</a> · <a class="" href="https://gnarl.dev/docs/getting-started/install">Run a node</a></p>]]></content>
        <author>
            <name>Nicholas Knize</name>
            <uri>https://scalingsearch.ai</uri>
        </author>
        <category label="brand" term="brand"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Manifests, not indexes — routing a query with no map]]></title>
        <id>https://gnarl.dev/blog/manifests-not-indexes</id>
        <link href="https://gnarl.dev/blog/manifests-not-indexes"/>
        <updated>2026-08-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How Gnarl decides which nodes to ask when nobody holds a global index of who has what.]]></summary>
        <content type="html"><![CDATA[<p>Ask a distributed search engine where a document lives and it consults something
that knows: a routing table, a shard map, a coordinator. Take that away and the
obvious question is how anything gets found at all.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="elimination-beats-lookup">Elimination beats lookup<a href="https://gnarl.dev/blog/manifests-not-indexes#elimination-beats-lookup" class="hash-link" aria-label="Direct link to Elimination beats lookup" title="Direct link to Elimination beats lookup" translate="no">​</a></h2>
<p>Gnarl never asks "who has this document?" It asks "who can I rule out?"</p>
<p>Every node publishes a <strong>manifest</strong>: a compact, signed statement of what it can
answer. A few kilobytes, whether the node holds fifty thousand documents or fifty
million.</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#9CDCFE;--prism-background-color:#1E1E1E"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#9CDCFE;background-color:#1E1E1E"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#9CDCFE"><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">  </span><span class="token property">"index"</span><span class="token operator" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"imagery"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">  </span><span class="token property">"doc_count"</span><span class="token operator" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(181, 206, 168)">48219</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">  </span><span class="token property">"fields"</span><span class="token operator" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">[</span><span class="token string" style="color:rgb(206, 145, 120)">"title"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"captured_at"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"sensor"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">]</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">  </span><span class="token property">"summary"</span><span class="token operator" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">    </span><span class="token property">"time_range"</span><span class="token operator" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">[</span><span class="token string" style="color:rgb(206, 145, 120)">"2019-03-01T00:00:00Z"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"2026-07-28T00:00:00Z"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">]</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">    </span><span class="token property">"bbox"</span><span class="token operator" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">[</span><span class="token number" style="color:rgb(181, 206, 168)">-124.7</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token number" style="color:rgb(181, 206, 168)">24.5</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token number" style="color:rgb(181, 206, 168)">-66.9</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token number" style="color:rgb(181, 206, 168)">49.4</span><span class="token punctuation" style="color:rgb(212, 212, 212)">]</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">    </span><span class="token property">"terms"</span><span class="token operator" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"bloom:8kb:base64…"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">  </span><span class="token property">"signature"</span><span class="token operator" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"ed25519:9c41…2f7e"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><br></div></code></pre></div></div>
<p>Given a query, the planner walks its cached manifests and discards every node it
can <em>prove</em> cannot contribute:</p>
<table><thead><tr><th>Query says</th><th>Manifest says</th><th>Result</th></tr></thead><tbody><tr><td><code>captured_at &gt; 2024</code></td><td>newest doc is 2021</td><td>eliminated</td></tr><tr><td><code>within(puget sound)</code></td><td>bbox is over Europe</td><td>eliminated</td></tr><tr><td><code>match("glacier")</code></td><td>Bloom filter says no</td><td>eliminated</td></tr><tr><td><code>sensor = "landsat-8"</code></td><td>never seen that sensor</td><td>eliminated</td></tr></tbody></table>
<p>Whatever survives gets asked.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-is-safe">Why this is safe<a href="https://gnarl.dev/blog/manifests-not-indexes#why-this-is-safe" class="hash-link" aria-label="Direct link to Why this is safe" title="Direct link to Why this is safe" translate="no">​</a></h2>
<p>Every summary is <strong>conservative by construction</strong>. A Bloom filter can say "maybe"
when the answer is no, but it can never say "no" when the answer is yes. A bbox
is always the enclosing box, never a tighter one. A time range always spans the
extremes.</p>
<p>So the failure mode is asking a node that turns out to have nothing — one wasted
round trip. The failure mode that would actually hurt, silently skipping a node
that had the answer, is structurally impossible.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-cost-of-being-wrong-is-bounded">The cost of being wrong is bounded<a href="https://gnarl.dev/blog/manifests-not-indexes#the-cost-of-being-wrong-is-bounded" class="hash-link" aria-label="Direct link to The cost of being wrong is bounded" title="Direct link to The cost of being wrong is bounded" translate="no">​</a></h2>
<p>False positives are not free, so queries carry a budget:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#9CDCFE;--prism-background-color:#1E1E1E"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#9CDCFE;background-color:#1E1E1E"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#9CDCFE"><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token property">"scope"</span><span class="token operator" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token property">"max_nodes"</span><span class="token operator" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(181, 206, 168)">64</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token property">"deadline_ms"</span><span class="token operator" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(181, 206, 168)">750</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token property">"min_coverage"</span><span class="token operator" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(181, 206, 168)">0.9</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><br></div></code></pre></div></div>
<p>When more nodes survive elimination than the budget allows, the planner ranks
survivors by expected contribution — how strongly the manifest matched, how fast
that node has been answering lately, how often it has answered at all — and asks
the best ones first.</p>
<p>The response reports exactly what happened, so "we asked 64 of 300 eligible
nodes" is a fact the caller gets to see rather than a detail buried in the
runtime.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="propagation">Propagation<a href="https://gnarl.dev/blog/manifests-not-indexes#propagation" class="hash-link" aria-label="Direct link to Propagation" title="Direct link to Propagation" translate="no">​</a></h2>
<p>Manifests gossip. A node pushes a new version to a random subset of peers, which
forward it on until the network converges — seconds, on networks of hundreds of
nodes.</p>
<p>Versions are monotonic and signed. A stale manifest cannot overwrite a fresh one,
and a peer cannot publish a manifest on somebody else's behalf.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-this-buys">What this buys<a href="https://gnarl.dev/blog/manifests-not-indexes#what-this-buys" class="hash-link" aria-label="Direct link to What this buys" title="Direct link to What this buys" translate="no">​</a></h2>
<p>Adding a node is starting a process. It indexes what it can already read, gossips
a manifest, and starts receiving the queries it can answer. Nothing rebalances.
No map is updated. Nobody is told to make room.</p>
<p>More detail in <a class="" href="https://gnarl.dev/docs/concepts/routing">Routing and manifests</a>.</p>]]></content>
        <author>
            <name>The Gnarl team</name>
            <uri>https://lucenia.io</uri>
        </author>
        <category label="architecture" term="architecture"/>
        <category label="routing" term="routing"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Introducing Gnarl — search without the cluster]]></title>
        <id>https://gnarl.dev/blog/introducing-gnarl</id>
        <link href="https://gnarl.dev/blog/introducing-gnarl"/>
        <updated>2026-08-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Why we are building decentralized search infrastructure, and what a network of independent nodes gets you that a cluster never will.]]></summary>
        <content type="html"><![CDATA[<p>Every distributed search system built in the last twenty years starts from the
same assumption: before you can search data, you must move it into a cluster you
control.</p>
<p>That assumption used to be free. It is not free anymore.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-bill-for-centralization">The bill for centralization<a href="https://gnarl.dev/blog/introducing-gnarl#the-bill-for-centralization" class="hash-link" aria-label="Direct link to The bill for centralization" title="Direct link to The bill for centralization" translate="no">​</a></h2>
<p>Three things changed at once.</p>
<p><strong>Data got heavier than the compute.</strong> A petabyte imagery archive costs more to
move than to search. Ingestion is no longer a setup step you do once — it is a
permanent tax you pay forever, in egress, in duplicate storage, and in the lag
between when data lands and when it becomes searchable.</p>
<p><strong>Boundaries became legal, not architectural.</strong> Residency rules, partner
agreements, and classification levels are not problems you solve with a faster
pipeline. Some data will never be allowed into your cluster, no matter how good
the cluster is.</p>
<p><strong>The blast radius stopped being acceptable.</strong> One coordinator, one shard
rebalance at the wrong moment, one config push — and the search surface for the
whole organization goes dark together.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-we-are-building-instead">What we are building instead<a href="https://gnarl.dev/blog/introducing-gnarl#what-we-are-building-instead" class="hash-link" aria-label="Direct link to What we are building instead" title="Direct link to What we are building instead" translate="no">​</a></h2>
<p>Gnarl replaces the cluster with a network. Every place your data already lives
becomes a node: it indexes locally, publishes a signed summary of what it can
answer, and answers queries against its own data.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#9CDCFE;--prism-background-color:#1E1E1E"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#9CDCFE;background-color:#1E1E1E"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#9CDCFE"><span class="token plain">gnarl </span><span class="token function" style="color:rgb(220, 220, 170)">node</span><span class="token plain"> up </span><span class="token parameter variable" style="color:rgb(156, 220, 254)">--data</span><span class="token plain"> ./corpus</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">gnarl </span><span class="token function" style="color:rgb(220, 220, 170)">join</span><span class="token plain"> </span><span class="token parameter variable" style="color:rgb(156, 220, 254)">--network</span><span class="token plain"> acme-internal</span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">gnarl search </span><span class="token string" style="color:rgb(206, 145, 120)">"glacier retreat"</span><br></div></code></pre></div></div>
<p>There is no coordinator in that network. The node a query happens to arrive at
plans the fan-out for that query, and stops being special the moment it returns
results.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-manifest-is-the-whole-trick">The manifest is the whole trick<a href="https://gnarl.dev/blog/introducing-gnarl#the-manifest-is-the-whole-trick" class="hash-link" aria-label="Direct link to The manifest is the whole trick" title="Direct link to The manifest is the whole trick" translate="no">​</a></h2>
<p>The hard part of a network without a coordinator is not executing a query. It is
deciding <strong>who to ask</strong> without a central index of who has what.</p>
<p>Every node publishes a manifest — a few kilobytes describing what it can answer.
Time ranges. Bounding boxes. Field names. A Bloom filter over its vocabulary.
Never the documents.</p>
<p>Planning is then a process of elimination rather than lookup. The planner does
not ask "who has this document?" It asks "who can I prove cannot help?" and asks
everyone who is left. Summaries are conservative by construction, so a false
positive costs one wasted request and a false negative cannot happen.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="partial-answers-honestly-reported">Partial answers, honestly reported<a href="https://gnarl.dev/blog/introducing-gnarl#partial-answers-honestly-reported" class="hash-link" aria-label="Direct link to Partial answers, honestly reported" title="Direct link to Partial answers, honestly reported" translate="no">​</a></h2>
<p>In a network, some nodes will be slow, some will be offline, and some will refuse
you. That is not an outage — it is one fewer answer. Every response says so:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#9CDCFE;--prism-background-color:#1E1E1E"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#9CDCFE;background-color:#1E1E1E"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#9CDCFE"><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain">  </span><span class="token property">"coverage"</span><span class="token operator" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token property">"eligible"</span><span class="token operator" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(181, 206, 168)">41</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token property">"answered"</span><span class="token operator" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(181, 206, 168)">39</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token property">"timed_out"</span><span class="token operator" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(181, 206, 168)">2</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token property">"complete"</span><span class="token operator" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token boolean">false</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#9CDCFE"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><br></div></code></pre></div></div>
<p>We think this is strictly more honest than a cluster that quietly returns results
from the shards that happened to be up. You can require completeness, retry the
stragglers, or degrade on purpose. What you cannot do is mistake a partial answer
for a whole one.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-this-is-going">Where this is going<a href="https://gnarl.dev/blog/introducing-gnarl#where-this-is-going" class="hash-link" aria-label="Direct link to Where this is going" title="Direct link to Where this is going" translate="no">​</a></h2>
<p>Gnarl is early. The wire protocol and manifest format are documented and stable,
and the client SDKs you build against are open source at
<a href="https://github.com/lucenia/gnarl-client" target="_blank" rel="noopener noreferrer" class="">github.com/lucenia/gnarl-client</a>. The
node itself is a commercial product from Lucenia, with a free tier for running a
single peer.</p>
<p>If your data is somewhere it cannot leave, we would like to hear about it.</p>
<p>— <a class="" href="https://gnarl.dev/docs/getting-started/install">Install a node</a> · <a class="" href="https://gnarl.dev/docs/concepts/architecture">Read the architecture</a></p>]]></content>
        <author>
            <name>Nicholas Knize</name>
            <uri>https://scalingsearch.ai</uri>
        </author>
        <category label="announcement" term="announcement"/>
        <category label="architecture" term="architecture"/>
    </entry>
</feed>