<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>bitcrowd blog Blog</title>
        <link>https://bitcrowd.dev/</link>
        <description>bitcrowd blog Blog</description>
        <lastBuildDate>Wed, 08 Jul 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <item>
            <title><![CDATA[How we estimate the effort for software projects]]></title>
            <link>https://bitcrowd.dev/how-we-estimate-the-effort-for-software-projects</link>
            <guid>https://bitcrowd.dev/how-we-estimate-the-effort-for-software-projects</guid>
            <pubDate>Wed, 08 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[For a software consulting firm like bitcrowd, it is crucial to estimate the effort of the work ahead. One main reason is risk management: underestimating a task or a project requires us to absorb the extra cost. But it is not only beneficial for us! An effort estimate can help our clients plan their product roadmap accurately based on our implementation timeline. They can also prioritise their features efficiently based on the ratio of effort to product value.]]></description>
            <content:encoded><![CDATA[<p>For a software consulting firm like bitcrowd, it is crucial to estimate the effort of the work ahead. One main reason is risk management: underestimating a task or a project requires us to absorb the extra cost. But it is not only beneficial for us! An effort estimate can help our clients plan their product roadmap accurately based on our implementation timeline. They can also prioritise their features efficiently based on the ratio of effort to product value.</p>
<p>In this blogpost we will explore our best practices regarding requirements engineering &amp; estimations.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="requirements-engineering">Requirements engineering<a href="https://bitcrowd.dev/how-we-estimate-the-effort-for-software-projects#requirements-engineering" class="hash-link" aria-label="Direct link to Requirements engineering" title="Direct link to Requirements engineering" translate="no">​</a></h2>
<p>This first step is by far the most arduous, as it requires to gather and describe the essential aspects of a feature or a product. Our team of engineers has to take a deep dive into an existing codebase, or imagine how a new one could look like.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="from-business-needs-to-specifications">From business needs to specifications<a href="https://bitcrowd.dev/how-we-estimate-the-effort-for-software-projects#from-business-needs-to-specifications" class="hash-link" aria-label="Direct link to From business needs to specifications" title="Direct link to From business needs to specifications" translate="no">​</a></h3>
<p>In this process, it can be useful to help the client formulate <em>business</em> needs, as the specifications (whether in design or development) should <em>derive</em> from them - not the other way around. In other words, what is the use case behind a feature, what problem does it solve? As opposed to: how is the feature implemented, how does the page look?</p>
<p>Consider these two requirements:</p>
<ul>
<li class="">On the accounts index view, the user should be able to see the timestamp of the account's last login</li>
<li class="">The user should be able to easily identify stale accounts</li>
</ul>
<p>The first one already locks the reader into a technical solution. It does not say what the user should make of this timestamp information.</p>
<p>The second one expresses a need. It makes no implication on how the system functions. This gives much more freedom to the engineer on the solution, and they can derive a technical specification from it in way that fits the model and the client. It also provides a simple way to assess, retroactively: are we solving the given problem with our solution?</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="data-model">Data model<a href="https://bitcrowd.dev/how-we-estimate-the-effort-for-software-projects#data-model" class="hash-link" aria-label="Direct link to Data model" title="Direct link to Data model" translate="no">​</a></h3>
<p>Building a data model, even as a draft, can be a great tool and visual support during the requirements engineering process. It is an excellent way to:</p>
<ul>
<li class="">identify corners of the product that could otherwise easily be missed</li>
<li class="">discuss the implementation strategy with the team</li>
<li class="">already define the domain and main components of the application</li>
<li class="">pin vocabulary words (like a resource name) that everyone in the team can then use, and understand their relationship to the product</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="estimations">Estimations<a href="https://bitcrowd.dev/how-we-estimate-the-effort-for-software-projects#estimations" class="hash-link" aria-label="Direct link to Estimations" title="Direct link to Estimations" translate="no">​</a></h2>
<p>Estimating a project just after reading a client's proposal is extremely hard. This exercise only becomes manageable after two (or more) engineers transformed the business needs into specifications, and broke down those requirements into pieces small enough that a developer can tell “this would take an average developer XX days”.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="enter-the-mindmap">Enter the Mindmap<a href="https://bitcrowd.dev/how-we-estimate-the-effort-for-software-projects#enter-the-mindmap" class="hash-link" aria-label="Direct link to Enter the Mindmap" title="Direct link to Enter the Mindmap" translate="no">​</a></h3>
<p>Most of the time, text formats are a very poor fit for describing technical specifications. A text document is structured from top to bottom and fails to visually transcribe how our codebases and how our web products are just good old <strong>graphs</strong>. The web is still essentially file navigation, like our code repositories.</p>
<p>Additionally, text can imply a hierarchy between sections, while in a graph, the order of the nodes at the same level is irrelevant. This can encourage an engineer to design features in isolation, already predicting an implementation roadmap that can be parallelized.</p>
<p>A mindmap is an excellent tool to visually support the task at hand, since it is well-equipped for exploration, re-structuring, moving sub-graphs into another area. Its visual framework gives clear encapsulation boundaries and shows the dependency between a parent node and its children.</p>
<figure class="image-with-caption"><div class="drop-shadow"><p><img decoding="async" loading="lazy" alt="Example mindmap for a recipes app" src="https://bitcrowd.dev/assets/images/recipes-e5dfea0ecc840531fc912c1c33b37796.png" width="2320" height="956" class="img_ev3q"></p></div><figcaption>Example mindmap for a recipes app</figcaption></figure>
<p>The capacity to easily collapse and expand entire sections of the graph also allows to visualize the requirements at different levels of detail.</p>
<figure class="image-with-caption"><div class="drop-shadow"><p><img decoding="async" loading="lazy" alt="Different level of details in the mindmap" src="https://bitcrowd.dev/assets/images/recipes_2-fe648267640094fd5fcfbccfea574edd.png" width="2748" height="1424" class="img_ev3q"></p></div><figcaption>Different level of details in the mindmap</figcaption></figure>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="new-codebase-dont-forget-non-feature-specs">New codebase? Don't forget non-feature specs<a href="https://bitcrowd.dev/how-we-estimate-the-effort-for-software-projects#new-codebase-dont-forget-non-feature-specs" class="hash-link" aria-label="Direct link to New codebase? Don't forget non-feature specs" title="Direct link to New codebase? Don't forget non-feature specs" translate="no">​</a></h3>
<p>When estimating a new project, one should not forget to note down in the mindmap some essentials of a new codebase. For example: initial repository setup (and sometimes, running generators), linters, CI/CD pipelines, error tracking etc. And additionally on the application level: i18n, layout styles, navigation and so on.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="to-the-numbers">To the numbers<a href="https://bitcrowd.dev/how-we-estimate-the-effort-for-software-projects#to-the-numbers" class="hash-link" aria-label="Direct link to To the numbers" title="Direct link to To the numbers" translate="no">​</a></h3>
<p>Going back to our previous example:</p>
<p><strong>Business need</strong></p>
<p>The user should be able to easily identify stale accounts</p>
<p><strong>Specification</strong></p>
<p>On the accounts index view, the user should be able to see and sort by the timestamp of the account's last login.</p>
<p><strong>Estimation</strong></p>
<p>Here the developer would need to consider what steps are necessary to achieve this task. We need a database migration to store the last login timestamp, we need to record this timestamp on a successful login, and we need to update the accounts index view to show this timestamp, and sort by it. These steps have most likely been done hundreds of time by an average backend developer. They are familiar and graspable. The levels of uncertainty &amp; complexity is quite low.</p>
<p>The effort might be affected by the quality of the codebase, whether it is well structured and well tested, whether the developers are familiar with the codebase, whether reusable components already exist etc. The effort should also account for review cycles and QA.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="and-then">And then?<a href="https://bitcrowd.dev/how-we-estimate-the-effort-for-software-projects#and-then" class="hash-link" aria-label="Direct link to And then?" title="Direct link to And then?" translate="no">​</a></h2>
<p>What is important here is that we are not asking the developers to <strong>commit</strong> to this number. We are asking them to make an estimation based on what is currently known for the project, and on tasks that were broken down enough that they can comprehend them fully. The variability of estimations can be alleviated by involving two or more developers, preferably with different experience levels or domain knowledge.</p>
<figure class="image-with-caption"><div class="drop-shadow"><p><img decoding="async" loading="lazy" alt="Mindmap with estimations in days" src="https://bitcrowd.dev/assets/images/recipes_3-4faa502a0fc02922b084413897eccf87.png" width="2010" height="660" class="img_ev3q"></p></div><figcaption>Mindmap with estimations in days</figcaption></figure>
<p>The mindmap allows us to rollup the days up to the central node and get a number of days. Again, if the result is 102 days, we are not <em>signing off on that exact number</em>, but can then tell if the project will take 2 weeks, 2 months, a year, and where complexity lies. Of course, the smaller the scope, the more precise the estimation can be. We use the same technique for clients coming up with a brand new product idea, as for clients asking for a new feature.</p>]]></content:encoded>
            <category>requirements</category>
            <category>mindmap</category>
            <category>estimations</category>
        </item>
        <item>
            <title><![CDATA[Notes from the dead letter box - Specifying Software for Teams and Agents]]></title>
            <link>https://bitcrowd.dev/specifying-software-for-teams-and-agents</link>
            <guid>https://bitcrowd.dev/specifying-software-for-teams-and-agents</guid>
            <pubDate>Tue, 30 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Specifying software for coding teams and agents: what changes, what stays the same, and how to write specs that both humans and AI agents can build from.]]></description>
            <content:encoded><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-to-describe-an-application">How to describe an application?<a href="https://bitcrowd.dev/specifying-software-for-teams-and-agents#how-to-describe-an-application" class="hash-link" aria-label="Direct link to How to describe an application?" title="Direct link to How to describe an application?" translate="no">​</a></h2>
<p>Imagine the following: The dev team receives the envelope with the application
specs from a dead letter box in the park at night. Two months later, they
deliver the code. Their client deploys their app, and everything works as
expected.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-question-what-was-in-that-envelope">The question: What was in that envelope?<a href="https://bitcrowd.dev/specifying-software-for-teams-and-agents#the-question-what-was-in-that-envelope" class="hash-link" aria-label="Direct link to The question: What was in that envelope?" title="Direct link to The question: What was in that envelope?" translate="no">​</a></h2>
<p>Product managers, engineers, and procurement teams have tried to deliver this
perfect blueprint for a software product. The ones that did not fail have mostly
succeeded by sheer luck. After decades of engineering, there is still no
commonly accepted standard for software requirements that guarantees, when
followed, the desired outcome. There is
<a href="https://standards.ieee.org/ieee/29148/6937/" target="_blank" rel="noopener noreferrer" class="">IEEE 29148</a>, but that carefully
avoids specifying the how: It tells you a requirement must be unambiguous and
verifiable, but not how to express it so that it actually is.</p>
<p>We were faced with this problem while we were working on
<a href="https://surveyor.bitcrowd.ai/" target="_blank" rel="noopener noreferrer" class="">Surveyor</a>, a tool to extract specifications from
running (legacy) systems: What is a good format for the description, once you
discovered its features?</p>
<p>Surveyor is currently in early access testing
(<a href="https://73f4313d.sibforms.com/serve/MUIFAGf7xJuMm4W5YSsztruKlELH459zdPyB50IhmpgBnS6wyrTGqz7dQ55IJLSVa7CUPFvAHrUEvEYbHEn5tBivo8SQPN_cJIMdBr_O1xiQ9ug64k46sfNBIFuTHK1rNKqvqytuDkTUoh0C5XMyUKOPZy7whNQM8zviGihyCCrZYBTq7uTf5-35fK6Ki2YBOVnEAmV_WwrgoT1-wg==" target="_blank" rel="noopener noreferrer" class="">you can take part to</a>),
and we already learned a lot during the first sessions applying it to other
peoples' legacy software. A good moment to share what we have found out, and to
ask for feedback. We have run analytics on our beta testers’ codebases, and here
is what has worked.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-premise">The Premise<a href="https://bitcrowd.dev/specifying-software-for-teams-and-agents#the-premise" class="hash-link" aria-label="Direct link to The Premise" title="Direct link to The Premise" translate="no">​</a></h2>
<p>Our clandestine team needs freedom of choice. We don't want to force them to use
<code>TINYINT</code> if their favorite database does not support that. If an implementation
detail is important in a way that does not become clear from the context, we
need specify that in the tech choices, or describe the observable outcome in a
spec. Otherwise, we should aim to specify independent from technology /
solution. Here are our picks to do that.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="architectural-overview">Architectural overview<a href="https://bitcrowd.dev/specifying-software-for-teams-and-agents#architectural-overview" class="hash-link" aria-label="Direct link to Architectural overview" title="Direct link to Architectural overview" translate="no">​</a></h2>
<p>The <a href="https://c4model.com/" target="_blank" rel="noopener noreferrer" class="">C4 model</a> is a way to visualize software architecture
at four levels of abstraction, created by <a href="https://simonbrown.je/" target="_blank" rel="noopener noreferrer" class="">Simon Brown</a>.
The "C4" refers to its four diagram types, each zooming in further than the
last:</p>
<ul>
<li class="">System Context shows your system as a single box surrounded by the users and
external systems it interacts with. It answers "what does this system do and
who uses it" without any internal detail.</li>
<li class="">Containers zooms into the system to show its major deployable or runnable
pieces, like web apps, mobile apps, databases, APIs, and file systems, along
with how they communicate. Here "container" means a separately running unit,
not specifically a Docker container.</li>
<li class="">Components breaks a single container down into its main building blocks and
their responsibilities, showing how the code is organized into logical
groupings within that container.</li>
<li class="">Code is the most detailed level, but mostly unused or generated from the code.</li>
</ul>
<p>The core idea is that you start broad and progressively zoom in, so different
audiences can engage at the level of detail that suits them, much like zooming
in on a map.</p>
<p>Our Beta testers have found these diagrams most helpful. Even engineers who have
worked with their codebase for years welcomed this organised visualisation.
Surveyor is generating C4 diagrams in the form of
<a href="https://structurizr.com/" target="_blank" rel="noopener noreferrer" class="">Structurizr</a> DSL.</p>
<p>This solves two purposes:</p>
<ol>
<li class="">It offers a standard for generating charts with a well known systematic</li>
<li class="">It can serve as an input for subsequent steps</li>
</ol>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="c-the-fourth">C the fourth<a href="https://bitcrowd.dev/specifying-software-for-teams-and-agents#c-the-fourth" class="hash-link" aria-label="Direct link to C the fourth" title="Direct link to C the fourth" translate="no">​</a></h3>
<p>The fourth C is “Code”, but it’s not often used as code changes are frequent.
Re-aligning UML diagrams each time the code changes adds friction we don’t want.
Instead, we leave the diagram level and move on to describing the behavior of
the system.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="describing-behavior">Describing Behavior<a href="https://bitcrowd.dev/specifying-software-for-teams-and-agents#describing-behavior" class="hash-link" aria-label="Direct link to Describing Behavior" title="Direct link to Describing Behavior" translate="no">​</a></h2>
<p>In 2006, a movement called
<a href="https://cucumber.io/docs/bdd/" target="_blank" rel="noopener noreferrer" class="">Behaviour-Driven Development</a> (BDD) emerged. The
idea was to write specifications in natural language that could be parsed and
executed as tests simultaneously. This enabled product teams to create
specifications that engineers could use directly as automated tests. The
engineers would start by creating the bindings between feature language and test
code. The implementation would begin with a failing test (red), which the actual
feature code would then make pass (green).</p>
<p><a href="https://cucumber.io/docs/gherkin/" target="_blank" rel="noopener noreferrer" class="">Gherkin</a> is a plain-text language for
writing executable specifications in a Given-When-Then format,
<a href="https://cucumber.io/" target="_blank" rel="noopener noreferrer" class="">Cucumber</a> is the framework that maps these specifications
to code (step definitions) so they run as automated tests.</p>
<p>A feature like the following:</p>
<div class="language-Gherkin language-gherkin codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-gherkin 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">Feature: Monster battle</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">  Scenario: Battle</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">    Given there is a monster</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">    When I attack it</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">    Then it should die</span><br></span></code></pre></div></div>
<p>Would map to a binding like this:</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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"># test/features/step_definitions/monster_steps.exs</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">defmodule</span><span class="token plain"> </span><span class="token module class-name">MonsterSteps</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</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">use</span><span class="token plain"> </span><span class="token module class-name">Cucumber</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">StepDefinition</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 module class-name">ExUnit</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Assertions</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">  step </span><span class="token string" style="color:#e3116c">"there is a monster"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> context </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token module class-name">Map</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">put</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">context</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:monster</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token module class-name">Monster</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">new</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">end</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">  step </span><span class="token string" style="color:#e3116c">"I attack it"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> context </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token module class-name">Map</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">put</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">context</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:monster</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token module class-name">Monster</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">take_hit</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">context</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">monster</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">end</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">  step </span><span class="token string" style="color:#e3116c">"it should die"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> context </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    refute context</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">monster</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">alive?</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    context</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">end</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">end</span><br></span></code></pre></div></div>
<p>BDD’s peak popularity came in the early 2010s, a period when teams worldwide
embraced it and equivalents appeared in every major language with
implementations like <a href="https://behave.readthedocs.io/" target="_blank" rel="noopener noreferrer" class="">Behave</a> and
<a href="https://pytest-bdd.readthedocs.io/" target="_blank" rel="noopener noreferrer" class="">pytest-bdd</a> for Python,
<a href="https://specflow.org/" target="_blank" rel="noopener noreferrer" class="">SpecFlow</a>/<a href="https://reqnroll.net/" target="_blank" rel="noopener noreferrer" class="">Reqnroll</a> for .NET,
<a href="https://docs.behat.org/" target="_blank" rel="noopener noreferrer" class="">Behat</a> for PHP, and
<a href="https://github.com/cucumber/godog" target="_blank" rel="noopener noreferrer" class="">Godog</a> for Go.</p>
<p>Tooling is broad too, including IDE plugins for
<a href="https://www.jetbrains.com/idea/" target="_blank" rel="noopener noreferrer" class="">IntelliJ</a>,
<a href="https://visualstudio.microsoft.com/" target="_blank" rel="noopener noreferrer" class="">Visual Studio</a>, and
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener noreferrer" class="">VS Code</a> offering syntax highlighting and step
navigation, plus reporting tools and CI/CD integrations</p>
<p>Interest has cooled down some years after that because developers grew tired of
maintaining large chunks of BDD code and bindings. Luckily, we can leave that
part to the LLMs now.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-case-for-bdd">The Case for BDD<a href="https://bitcrowd.dev/specifying-software-for-teams-and-agents#the-case-for-bdd" class="hash-link" aria-label="Direct link to The Case for BDD" title="Direct link to The Case for BDD" translate="no">​</a></h3>
<p>We are revisiting BDD today, because AI assisted coding has a problem: Plans and
free formats like <a href="https://openspec.pro/" target="_blank" rel="noopener noreferrer" class="">OpenSpec</a> are not parsable. That means
they can not be translated to automated tests directly. You need an agent step
to build the testing, with all uncertainty agentic coding brings.</p>
<p>The relatively rigid syntax of Gherkin is an advantage here: It maps
mechanically to test code. And it can also be parsed to produce other output
formats: Manual QA instructions, OpenSpec or
<a href="https://www.atlassian.com/software/jira" target="_blank" rel="noopener noreferrer" class="">Jira</a> tickets.</p>
<p>But for that, it’s missing the meta information that embeds a feature
description into its application context.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-assay-format">The Assay format<a href="https://bitcrowd.dev/specifying-software-for-teams-and-agents#the-assay-format" class="hash-link" aria-label="Direct link to The Assay format" title="Direct link to The Assay format" translate="no">​</a></h3>
<p>We have used the C4/structurizr DSL to express how our system interfaces with
the outside world. We also used it to describe the inner composition of our
application. When we use Gherkin alone, this context would be lost. But we can
preserve it with syntax-aware comments like this:</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"># ---</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># component: orderLifecycle</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># container: api</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># schema: schemas/order_lifecycle.ex</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># workspace: architecture/workspace.tsp</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># definitions:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">#   a valid customer:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">#     a Customer with status Active, verified email,</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">#     and a credit limit greater than zero</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">#   in stock:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">#     the product has available quantity greater than</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">#     the requested amount</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># invariants:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">#   - total must not exceed customer credit limit</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">#   - at least one line item required</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">#   - cannot cancel an order that has shipped</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># ---</span><br></span></code></pre></div></div>
<p>With this, we can access the information in the structurizr DSL to get context
information. This allows us to write parsers and plugins for coding agents that
can not only read the features, but also access context information we gathered
before. But what's purpose of the <code>schema:</code> definition?</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schema-definitions">Schema Definitions<a href="https://bitcrowd.dev/specifying-software-for-teams-and-agents#schema-definitions" class="hash-link" aria-label="Direct link to Schema Definitions" title="Direct link to Schema Definitions" translate="no">​</a></h2>
<p>Schema work as a shared contract about the business objects of a component.
Having them speeds up onboarding. Developers don't need to scan through all
features to detect what fields are needed, and agents are less likely to invent
fields. It's a token-efficient way to inject exactly the ground truth into
context.</p>
<p>For specifying schemas, three obvious candidates come to mind:</p>
<ul>
<li class="">
<p>Protobuf — <a href="https://protobuf.dev/" target="_blank" rel="noopener noreferrer" class="">https://protobuf.dev</a> — Google's schema and wire format. Compact and
fast, with code generators across many languages, but its scalar types are
physical commitments, so it presumes the most about storage and encoding.</p>
<div class="language-protobuf codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-protobuf 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">syntax = "proto3";</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">message Post {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  string title = 1;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  int32 views = 2;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  bool published = 3;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  repeated string tags = 4;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span></code></pre></div></div>
</li>
<li class="">
<p>JSON Schema — <a href="https://json-schema.org/" target="_blank" rel="noopener noreferrer" class="">https://json-schema.org</a> — A decade-plus standard for describing
the shape and constraints of JSON. Stays loose where you want, needs no
toolchain for consumers, and fits the premise most directly; the cost is
verbosity. It can be authored directly, e.g. as YAML and be compiled to JSON:</p>
<div class="language-YAML language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">$schema</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"https://json-schema.org/draft/2020-12/schema"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">$id</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"https://assay.example/post"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">title</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> Post</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">type</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> object</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">required</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">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 key atrule" style="color:#00a4db">additionalProperties</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean important" 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 key atrule" style="color:#00a4db">properties</span><span class="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 key atrule" style="color:#00a4db">title</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><span class="token key atrule" style="color:#00a4db">type</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 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 key atrule" style="color:#00a4db">views</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><span class="token key atrule" style="color:#00a4db">type</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> integer </span><span class="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 key atrule" style="color:#00a4db">published</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><span class="token key atrule" style="color:#00a4db">type</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> boolean </span><span class="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 key atrule" style="color:#00a4db">tags</span><span class="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 key atrule" style="color:#00a4db">type</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> array</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">items</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><span class="token key atrule" style="color:#00a4db">type</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> string </span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
</li>
<li class="">
<p>TypeSpec — <a href="https://typespec.io/" target="_blank" rel="noopener noreferrer" class="">https://typespec.io</a> — Microsoft's TypeScript-like language that
compiles to JSON Schema, OpenAPI, and Protobuf from one source. Compact and
readable, at the price of a build step and a single-vendor toolchain.</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts 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">model Post </span><span class="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 operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">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">  views</span><span class="token operator" style="color:#393A34">?</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> integer</span><span class="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</span><span class="token operator" style="color:#393A34">?</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">boolean</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  tags</span><span class="token operator" style="color:#393A34">?</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</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><br></span></code></pre></div></div>
</li>
</ul>
<p>Under the premise that the implementing team picks the field types and specifics
the three differ in how much they presume. Protobuf fits worst: its scalar types
(int32, bytes) are physical commitments that pre-decide the storage details you
meant to leave open. JSON Schema is the opposite — it describes shape and
constraints without binding to any storage type, and hands the team a
directly-usable artifact with no toolchain; its cost is verbosity and $ref-wired
files. For our purpose, TypeSpec is the golden spot:</p>
<p>A compact, TypeScript-like authoring that compiles to JSON Schema (or Protobuf
later, if needed), at the price of a build step and a younger, single-vendor
toolchain. This is not so relevant in this context as we use it for
documentation rather than compiling. It adds, however, the cross compilation
benefits.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="architectural-decision">Architectural Decision<a href="https://bitcrowd.dev/specifying-software-for-teams-and-agents#architectural-decision" class="hash-link" aria-label="Direct link to Architectural Decision" title="Direct link to Architectural Decision" translate="no">​</a></h2>
<p>Until this point, we avoided to describe the “how” of our application. Which
database, which web framework, what datamodel. This is what
<a href="https://adr.github.io/madr/" target="_blank" rel="noopener noreferrer" class="">Markdown Architectural Decision Records</a> (MADR)
cover. Their goal is to document not only the technical decision, but what
options were considered and what the problem statement was. Because MADRs
contain that background, it allows to effectively reconsider decisions once the
Circumstances have changed.</p>
<p>They typically look like this:</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">## **Use PostgreSQL for primary data store**</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">* Status: accepted</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">* Date: 2026-06-29</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">### **Context**</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">We need a primary database for a new service with relational data and strong</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">consistency requirements.</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">### **Considered Options**</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">* PostgreSQL</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">* MongoDB</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">### **Decision**</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">Use **PostgreSQL** — the team has operational experience with it, and it</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">provides ACID transactions plus JSONB for flexible fields. MongoDB was rejected</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">due to weaker consistency guarantees and less team familiarity.</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">### **Consequences**</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">* Good: mature ecosystem, existing team expertise</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">* Bad: harder to scale horizontally than MongoDB</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="non-functional-requirements">Non Functional Requirements<a href="https://bitcrowd.dev/specifying-software-for-teams-and-agents#non-functional-requirements" class="hash-link" aria-label="Direct link to Non Functional Requirements" title="Direct link to Non Functional Requirements" translate="no">​</a></h2>
<p>By now we have described our application well in terms of how it is structured,
what it does, and what tools it uses (or should use). But we left out the
quality of service.</p>
<p>A non-functional requirement (NFR) specifies how well a system must operate, as
opposed to what it does (functional requirements). Where a functional
requirement says "users can book an appointment," an NFR constrains the
qualities of that behavior — performance, scalability, availability,
reliability, security, maintainability, usability, and so on.</p>
<p>They are often written in a simple table format:</p>
<p>Here's a fuller set, functional requirement paired with its non-functional
counterpart:</p>
<table><thead><tr><th>Quality attribute</th><th>Functional requirement (<em>what</em>)</th><th>Non-functional requirement (<em>how well</em>)</th></tr></thead><tbody><tr><td>Performance</td><td>User searches for available slots</td><td>Results return in under 500ms for 95% of requests</td></tr><tr><td>Scalability</td><td>Users book appointments</td><td>System handles 1,000 concurrent bookings without degradation</td></tr><tr><td>Availability</td><td>The booking system is accessible</td><td>99.9% uptime, measured monthly (≈43 min downtime/month)</td></tr><tr><td>Reliability</td><td>A confirmed booking is saved</td><td>Zero confirmed bookings lost; durable before confirmation returns</td></tr><tr><td>Security</td><td>Users log in to their account</td><td>Passwords stored hashed; data encrypted in transit (TLS) and at rest</td></tr><tr><td>Usability</td><td>User completes a booking</td><td>A new user can book without instructions in under 2 minutes</td></tr></tbody></table>
<p>Each row shows the same feature twice: the left column is <em>what the system
does</em>, the right is the measurable constraint on <em>how well it must do it</em> — the
NFR.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-end-boss-design">The End-Boss: Design<a href="https://bitcrowd.dev/specifying-software-for-teams-and-agents#the-end-boss-design" class="hash-link" aria-label="Direct link to The End-Boss: Design" title="Direct link to The End-Boss: Design" translate="no">​</a></h2>
<p>Specifying UI is a daunting task because it happens at the end of the rendering
pipeline where the amount of moving parts is highest. Most tools for web
therefore use the web technologies themselves to avoid drifting.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-image">The Image<a href="https://bitcrowd.dev/specifying-software-for-teams-and-agents#the-image" class="hash-link" aria-label="Direct link to The Image" title="Direct link to The Image" translate="no">​</a></h3>
<p>The simplest, (and arguably the worst) medium to specify design is the image:</p>
<div class="language-Gherkin language-gherkin codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-gherkin 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">Then the login page should match "fixtures/login.png"</span><br></span></code></pre></div></div>
<p>This allows features to specify design alongside the functional feature
requirements.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-code">The code<a href="https://bitcrowd.dev/specifying-software-for-teams-and-agents#the-code" class="hash-link" aria-label="Direct link to The code" title="Direct link to The code" translate="no">​</a></h3>
<p>If the frontend code is using components and maybe even a component library, it
might be worth carrying over. A connection to a design system in
<a href="https://www.figma.com/" target="_blank" rel="noopener noreferrer" class="">Figma</a> or similar is the gold standard here, but might
be violating our "letter in envelope" constraint. Our goto strategy at bitcrowd
is <a href="https://storybook.js.org/" target="_blank" rel="noopener noreferrer" class="">Storybook</a>. If the target system supports it,
teams can work very effectively.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="visual-regression-testing">Visual Regression Testing<a href="https://bitcrowd.dev/specifying-software-for-teams-and-agents#visual-regression-testing" class="hash-link" aria-label="Direct link to Visual Regression Testing" title="Direct link to Visual Regression Testing" translate="no">​</a></h3>
<p>If you happen to have a legacy system with a decent design, visual regression
testing with <a href="https://playwright.dev/" target="_blank" rel="noopener noreferrer" class="">Playwright</a> or similar might be your tool
of choice. It gives teams a quick feedback loop if it comes to single scenarios,
and the option to recheck the whole interface. The great thing in combination
with Gherkin is that you can switch screenshot processing on and off. The
difficulty with visual regression tests is their potential flakiness. However,
image processing models have made this a lot more reliable.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="so-what-is-in-that-envelope">So, what is in that envelope?<a href="https://bitcrowd.dev/specifying-software-for-teams-and-agents#so-what-is-in-that-envelope" class="hash-link" aria-label="Direct link to So, what is in that envelope?" title="Direct link to So, what is in that envelope?" translate="no">​</a></h2>
<p>So far we have C4/structurizr diagrams for how the system is build and how it
interacts, Gherkin/Assay for its behaviour, TypeSpec for schemas, MADR for the
technological choices and NFR for its quality of service. We go for storybook
for the visuals. Obviously, this is not the final solution for every stack. It
would be hard to describe a pacemaker OS with it, or a web application with
pristine UI effects. The final version of Surveyor will use plugins to cater for
that, but for many of the projects bitcrowd has encountered, this gives a good
start.</p>
<p>Fortunately, we have clients who enjoy talking to us, so we haven’t had to
resort to espionage technology - yet. Let us know if you have some clandestine
system you need to build in the dark, or that legacy application needs a
rewrite.</p>]]></content:encoded>
            <category>AI</category>
            <category>agents</category>
            <category>Surveyor</category>
            <category>Elixir</category>
            <category>architecture</category>
            <category>legacy</category>
            <category>testing</category>
        </item>
        <item>
            <title><![CDATA[AppSignal deploy markers with Docker & Kubernetes]]></title>
            <link>https://bitcrowd.dev/appsignal-deploy-markers-with-docker-and-kubernetes</link>
            <guid>https://bitcrowd.dev/appsignal-deploy-markers-with-docker-and-kubernetes</guid>
            <pubDate>Wed, 17 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[We recently revived an old client project at bitcrowd and decided to dockerize it and deploy it with Kubernetes. Sadly, our AppSignal deploy markers were not being sent. In this blog post we will see how to send these deploy markers, and more importantly, how to find out what the problem is.]]></description>
            <content:encoded><![CDATA[<p>We recently revived an old client project at bitcrowd and decided to dockerize it and deploy it with Kubernetes. Sadly, our AppSignal deploy markers were not being sent. In this blog post we will see how to send these deploy markers, and more importantly, how to find out what the problem is.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="deploy-markers">Deploy markers?<a href="https://bitcrowd.dev/appsignal-deploy-markers-with-docker-and-kubernetes#deploy-markers" class="hash-link" aria-label="Direct link to Deploy markers?" title="Direct link to Deploy markers?" translate="no">​</a></h2>
<blockquote>
<p>A deploy marker indicates a change in the deployed version of an application.</p>
</blockquote>
<p>AppSignal tracks your deploys with deploy markers. They can be seen in the “Deploys” section of their app. They are a great way to document deployments, but also errors reported by AppSignal are then associated to a certain deployment. Additionally our team receives a Slack notification when a new version of our code was released to staging or production.</p>
<figure class="image-with-caption"><div class="drop-shadow"><p><img decoding="async" loading="lazy" alt="AppSignal UI around deploys" src="https://bitcrowd.dev/assets/images/deploy_markers-eddd14765c7cfef1a8a0d3014b282e2f.png" width="2918" height="870" class="img_ev3q"></p></div><figcaption>AppSignal deploy markers UI</figcaption></figure>
<p>In short: deploy markers in AppSignal are valuable. And they can be even more valuable when one assigns a meaningful value to the deployʼs <code>revision</code>. Traditionally, such a meaningful value is the <strong>short SHA of the last commit in the release</strong>. That way, a developer knows exactly what code this release was running on.</p>
<p>AppSignalʼs agent sends the deploy markers via their API when it detects that the environment variable <code>APP_REVISION</code> has changed on the server. This is a first clue to our problem!</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="app_revision-are-you-there-">APP_REVISION, are you there? 🔮<a href="https://bitcrowd.dev/appsignal-deploy-markers-with-docker-and-kubernetes#app_revision-are-you-there-" class="hash-link" aria-label="Direct link to APP_REVISION, are you there? 🔮" title="Direct link to APP_REVISION, are you there? 🔮" translate="no">​</a></h2>
<p>A first look at the container environment variables tells us that no, there is no such variable on the server. Hereʼs how to inspect those variables:</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">kubectl exec -it [pod] -n [namespace] -- env</span><br></span></code></pre></div></div>
<p>We need to set this variable somehow, ideally programmatically, to the SHA of the last commit in the release. The release is built with Docker in our Github CI, and pushed to our container registry. The CI would be a natural place to set this environment variable.</p>
<p>Here, with a Github action:</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">- name: Get short SHA</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  id: sha</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  run: echo "short=$(git rev-parse --short HEAD)" &gt;&gt; $GITHUB_OUTPUT</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">- name: Build and push Docker image</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  uses: docker/build-push-action@v7</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  with:</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">    build-args: |</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       COMMIT=${{ steps.sha.outputs.short }}</span><br></span></code></pre></div></div>
<p>The <code>build-args</code> section in this action functions in the same way as the docker flag: <code>docker build --build-arg COMMIT="foo"</code>.</p>
<p>Then, in our Dockerfile:</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">ARG COMMIT</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">ENV APP_REVISION=$COMMIT</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">RUN echo "APP_REVISION=$APP_REVISION"</span><br></span></code></pre></div></div>
<p>And there we have it, when building the image in the CI and looking at the logs, we see that <code>APP_REVISION</code> is properly assigned the SHA of the last commit!</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="app_revision-pongs-">APP_REVISION pongs 🏓<a href="https://bitcrowd.dev/appsignal-deploy-markers-with-docker-and-kubernetes#app_revision-pongs-" class="hash-link" aria-label="Direct link to APP_REVISION pongs 🏓" title="Direct link to APP_REVISION pongs 🏓" translate="no">​</a></h2>
<p>A piece of knowledge to have in mind: the environment variables of the container image are “merged” into the container environment variables. So, if <code>APP_REVISION</code> is set in the image, we should see it listed in the container with our env command <code>kubectl exec -it [pod] -n [namespace] -- env</code>.</p>
<p>If itʼs still not set, make sure the <code>ARG</code> and <code>ENV</code> declarations in the Dockerfile are done on <strong>the last stage</strong> - for multi-stage Docker builds (in other words: after the last <code>FROM</code> clause).</p>
<p>At this stage we do have <code>APP_REVISION</code> set, but we still did not get our deploy marker! Whatʼs going on?</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="appsignal-agent-config">AppSignal agent config<a href="https://bitcrowd.dev/appsignal-deploy-markers-with-docker-and-kubernetes#appsignal-agent-config" class="hash-link" aria-label="Direct link to AppSignal agent config" title="Direct link to AppSignal agent config" translate="no">​</a></h2>
<p>There is one way to debug this, and it is to look at AppSignalʼs agent log on the server. This can be done with:</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">kubectl exec -it [pod] -n [namespace] -- cat /tmp/appsignal.log</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">[2026-06-03T14:46:20 (process) #8][INFO] Starting AppSignal</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">[2026-06-03T14:46:20 (agent) #16][Info] Started appsignal-agent 0.36.9 (build 68764aa)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">(as 0/0) with config Config {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">active: true, agent_path: None, agent_standalone: false,</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">app_name: Some("foo"), app_path: Some("/app"), app_revision: Some("715a5cb"),</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">environment: "staging",</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">push_api_endpoint: Url { host: Some(Domain("push.appsignal.com")), ... },</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">push_api_key: "****",  working_dir_path: "/tmp/appsignal",</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">}</span><br></span></code></pre></div></div>
<p>From this config, one can verify that the AppSignal agent has picked up the correct <code>app_revision</code>. We can also verify that we have the correct <code>environment</code> configured, and the correct <code>push_api_key</code>.</p>
<p>We found out that the <code>push_api_key</code> was set to our AppSignal <em>organization</em> API Key. Instead, it should be the API Key specific to the given application on AppSignal, for that environment.</p>
<p>We changed the value, rolled out a new release, and there we had it, our first deploy marker! Job done 🪇.</p>]]></content:encoded>
            <category>appsignal</category>
            <category>docker</category>
            <category>kubernetes</category>
        </item>
        <item>
            <title><![CDATA[Elixir for a Bluesky DataPlane - the unexpected choice]]></title>
            <link>https://bitcrowd.dev/why-elixir-bluesky-dataplane</link>
            <guid>https://bitcrowd.dev/why-elixir-bluesky-dataplane</guid>
            <pubDate>Tue, 02 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Why we chose Elixir over Go, Rust and Node for a high-performance Bluesky DataPlane - and how a small Rust NIF and in-process fan-out made it the right fit.]]></description>
            <content:encoded><![CDATA[<p>Bluesky's source code is widely open source, so you can run your own social
network with it. - Provided you stay with a comparably small user base. What's
missing? A performant DataPlane implementation. Closing this gap would be an
important step towards building digital independence.</p>
<p>We wanted to contribute our share and decided to work on a performant DataPlane
for Bluesky. When we started the project, we expected to work in Go, Rust or
even Node. After all, these were the predominant languages in the community.
Instead, we landed with Elixir. Here is why and how we came to that decision.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-component-nobody-knows-about">The component nobody knows about<a href="https://bitcrowd.dev/why-elixir-bluesky-dataplane#the-component-nobody-knows-about" class="hash-link" aria-label="Direct link to The component nobody knows about" title="Direct link to The component nobody knows about" translate="no">​</a></h2>
<p>Everyone who has looked into the Bluesky infrastructure knows about the Personal
Data Stores (PDS), the Relay or the AppView. When you set up your own little
Bluesky from the
<a href="https://github.com/bluesky-social/atproto" target="_blank" rel="noopener noreferrer" class="">ATProto repository</a>, you run the
same components as Bluesky, the commercial service. However, there's one notable
exception: the <em>DataPlane</em>, a part of the AppView. It exists publicly only as a
Node-and-Postgres reference implementation, while the production Bluesky network
runs on a dedicated, <a href="https://www.scylladb.com/" target="_blank" rel="noopener noreferrer" class="">ScyllaDB</a>-backed, closed-source
DataPlane (as documented in
<a href="https://newsletter.pragmaticengineer.com/i/114113498/5-scaling-the-database-layer" target="_blank" rel="noopener noreferrer" class="">Pragmatic Engineer's deep-dive on Bluesky's architecture</a>).</p>
<p>That gap is exactly where things get interesting. The reference implementation
tells you what the DataPlane <em>does</em>; it doesn't tell you how to make it survive
contact with real traffic.</p>
<p>If you want to run your own, you have to answer the scaling question yourself -
and the first step is understanding the workload well enough to stop treating it
as one thing.</p>
<figure class="image-with-caption"><img decoding="async" loading="lazy" alt="Bluesky architecture overview" src="https://bitcrowd.dev/assets/images/bluesky-5fcedfef78dd330f8c008f30b0457a02.png" width="1729" height="578" class="img_ev3q"><figcaption>The DataPlane is a central component of the AppView in Bluesky's architecture. Read the<a class="" href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto">previous post</a>to learn more.</figcaption></figure>
<p>ScyllaDB is Bluesky's operational choice, not part of the interface. The
DataPlane's contract is a gRPC service that answers high-volume, low-complexity
queries and returns <em>skeletons</em> - lists of IDs, counts, booleans - which a
higher layer later hydrates into full views. What sits behind that contract is
entirely up to you: the language, and the datastore. So before picking either,
we spent our time on the only thing that actually constrains the choice: the
shape of the load.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-tale-of-two-workloads">A tale of two workloads<a href="https://bitcrowd.dev/why-elixir-bluesky-dataplane#a-tale-of-two-workloads" class="hash-link" aria-label="Direct link to A tale of two workloads" title="Direct link to A tale of two workloads" translate="no">​</a></h2>
<p>The Bluesky's historical data is enormous — terabytes. But when a user opens the
app, they read a few dozen recent posts, get distracted, and wander off to a
profile or a thread. They almost never scroll back further than a day or two.</p>
<blockquote>
<p>A note on specifics:
<a href="https://jazco.dev/2025/02/19/imperfection/" target="_blank" rel="noopener noreferrer" class="">it's been observed</a> that
Bluesky's timeline doesn't serve much beyond the last day or two of content,
and that deeper cursor positions tend to fill with very recent posts rather
than true history. Treat the exact window as illustrative unless you've
measured it on your own deployment - the architectural point holds regardless
of the precise number.</p>
</blockquote>
<p>This leads to an interesting disparity: Data that is more than a few days old is
almost never accessed in the timeline. When it is accessed, it is usually
through a profile or a thread. Yet the reference implementation compiles every
timeline by joining those tables, which are terabyte-scale. This process is slow
and becomes slower as the tables grow, and timeline requests account for most of
what the DataPlane is asked to do.</p>
<figure class="image-with-caption"><img decoding="async" loading="lazy" alt="Inversion of data volume and access frequency" src="https://bitcrowd.dev/assets/images/inversion-7f5340cbaf0f4d018152cd5285cf78ca.png" width="1360" height="796" class="img_ev3q"><figcaption>Data volume and read frequency are inversely related: the vast bulk of data is old and almost never read, while the tiny sliver of recent posts drives nearly all timeline traffic.</figcaption></figure>
<p>So timeline generation deserves a different treatment from record and thread
retrieval. The two workloads don't just differ in degree. They have opposite
resource profiles, and they want different things from the runtime underneath.</p>
<table><thead><tr><th></th><th><strong>Hot path</strong> - timelines</th><th><strong>Cold path</strong> - records, threads, profiles</th></tr></thead><tbody><tr><td><strong>Data age</strong></td><td>Recent (last day or two)</td><td>Historic (anything older)</td></tr><tr><td><strong>Data volume</strong></td><td>A tiny sliver</td><td>Terabytes</td></tr><tr><td><strong>Request share</strong></td><td>The dominant workload</td><td>Comparatively rare</td></tr><tr><td><strong>Bound by</strong></td><td>Memory + compute</td><td>I/O</td></tr><tr><td><strong>Lives in</strong></td><td>Memory</td><td>Database, fetched on demand</td></tr><tr><td><strong>Strategy</strong></td><td>Fan-out + bounded timeline length</td><td>Swappable datastore behind an interface</td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-hot-path-timelines-served-from-memory">The hot path: timelines served from memory<a href="https://bitcrowd.dev/why-elixir-bluesky-dataplane#the-hot-path-timelines-served-from-memory" class="hash-link" aria-label="Direct link to The hot path: timelines served from memory" title="Direct link to The hot path: timelines served from memory" translate="no">​</a></h2>
<p>Most social platforms use a hybrid strategy to handle timeline fan-out. A post
from an account with a few thousand followers is pushed into its followers'
timelines as soon as it is published. This is known as <strong>fan-out on write</strong> in
the lingo.</p>
<p>However, a post from an account with millions of followers (<em>"the celebrities"</em>)
is not immediately pushed to all of its followers' mailboxes; instead, it is
merged in when a follower actually requests their timeline: This is called
<strong>fan-in on read</strong>. The former keeps write amplification bounded for ordinary
accounts. The second prevents the write storm that a celebrity post would
otherwise cause.</p>
<p>Another obvious simplification becomes apparent when you stop thinking of a
social feed as an immutable archive. Take into account that users will jump
around with their attention and you will realise that timelines are finite.
Therefore, a user who follows ten thousand accounts will never see all of their
posts anyway. There's reasonable to limit how much content is distributed to any
single timeline. The obligation is to provide a good, recent, bounded timeline,
not a complete one.</p>
<p>What this boils down to:</p>
<ul>
<li class="">Recent posts - the overwhelming majority of what timelines are made of - can
live and be served <strong>from memory</strong>.</li>
<li class="">Fan-out can be <strong>deferred</strong>: as long as posts land in followers' timelines
within minutes, and the backlog of fan-out jobs doesn't outgrow available
resources, nobody notices the delay.</li>
<li class="">Older content, when it's genuinely needed, is safe to fetch from the database
on demand.</li>
</ul>
<p>The dominant request type, the timeline read, stops being I/O-bound (wait on a
giant join) and becomes memory-and-compute-bound (manipulate in-memory
structures quickly). That shift changes what "fast" means, and it's what
reopened the language question for us.</p>
<p>It also argues for putting the concrete database behind an interface. The hot
path barely touches it; the cold path is the only part that leans on it. Keep
that boundary clean and you can swap the backing store later without touching
the rest of the system.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-follower-graph-in-memory-but-not-naively">The follower graph: in memory, but not naively<a href="https://bitcrowd.dev/why-elixir-bluesky-dataplane#the-follower-graph-in-memory-but-not-naively" class="hash-link" aria-label="Direct link to The follower graph: in memory, but not naively" title="Direct link to The follower graph: in memory, but not naively" translate="no">​</a></h3>
<p>There's a catch. Fan-out and timeline assembly both need answers about the
follow graph: who follows whom, and the intersections and unions of those sets.
Holding hundreds of millions of follow relationships in memory as hash maps
would be ruinously wasteful.</p>
<p>Jaz's <a href="https://jazco.dev/2024/04/15/in-memory-graphs/" target="_blank" rel="noopener noreferrer" class="">"GraphD" series</a>
documents this exact problem: an in-memory graph store that originally used hash
maps and hash sets to track each user's followers and follows. The fix was
switching to <a href="https://roaringbitmap.org/" target="_blank" rel="noopener noreferrer" class="">Roaring Bitmaps</a>, a
<a href="https://vikramoberoi.com/posts/a-primer-on-roaring-bitmaps-what-they-are-and-how-they-work/" target="_blank" rel="noopener noreferrer" class="">compressed bitmap structure</a>
built for large set operations. The numbers are striking: the entire Bluesky
follow graph fits in roughly 6.5 GB of RAM, takes about 1.6 GB on disk, and
loads in around 20 seconds.</p>
<p>Jaz
<a href="https://jazco.dev/2024/04/20/roaring-bitmaps/" target="_blank" rel="noopener noreferrer" class="">also describes two cost modes</a>
that map onto our hot/cold split. Paging over <em>all</em> of a user's follows is
expensive and belongs in paginated or async fan-out jobs. On-demand set
intersection — "which people I follow also follow this person" — has to run at
interactive speed. Our split isn't an invention; the access patterns already
worked this way in Bluesky's own tooling.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="so-what-do-we-actually-need-from-a-language">So what do we actually need from a language?<a href="https://bitcrowd.dev/why-elixir-bluesky-dataplane#so-what-do-we-actually-need-from-a-language" class="hash-link" aria-label="Direct link to So what do we actually need from a language?" title="Direct link to So what do we actually need from a language?" translate="no">​</a></h2>
<p>With the workload pinned down, the requirements are:</p>
<ol>
<li class="">Fast, compute-bound set operations over a large, long-lived, in-memory graph:
bitmap intersections and unions. Per-core compute, byte-level work, cache
sensitivity.</li>
<li class="">High-concurrency serving of memory-resident timeline reads, with bounded
response sizes and tight tail-latency expectations.</li>
<li class="">A deferrable fan-out queue that absorbs bursts, applies backpressure,
delivers within minutes, and degrades gracefully instead of falling over.</li>
<li class="">A clean datastore boundary for the cold path: records, threads, profiles.
Ordinary I/O-bound request handling.</li>
</ol>
<p>The first three are the hot path; the fourth is the cold path. No language
obviously wins all four. This is the scorecard we judged each candidate against.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-four-candidates">The four candidates<a href="https://bitcrowd.dev/why-elixir-bluesky-dataplane#the-four-candidates" class="hash-link" aria-label="Direct link to The four candidates" title="Direct link to The four candidates" translate="no">​</a></h2>
<p>At <a href="https://bitcrowd.net/en" target="_blank" rel="noopener noreferrer" class="">bitcrowd</a> we work with Elixir, Go and Rust day to
day, with the occasional Node project on the side. So this wasn't a contest
between a favourite and a lineup of strangers - we had hands-on experience to
weigh on every side of the comparison.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="go">Go<a href="https://bitcrowd.dev/why-elixir-bluesky-dataplane#go" class="hash-link" aria-label="Direct link to Go" title="Direct link to Go" translate="no">​</a></h3>
<p>Go is the incumbent. It's what Bluesky's own production DataPlane is written in,
and the fit is strong. Goroutines and channels map cleanly onto "fan out work,
gather results, respond." The bitmap work is comfortable in Go (GraphD itself is
Go). gRPC support is first-class, deployment is a single static binary, and the
tooling is mature. In-process fan-out is doable with worker pools draining
buffered channels.</p>
<p>The costs sit at the extremes. Under heavy allocation Go's garbage collector
starts eating CPU, and at very high socket counts the network backend can
bottleneck on syscalls. Both respond to runtime tuning, but the tuning is
ongoing work, not a one-time fix. And the in-process fan-out you build yourself
comes with no supervision or isolation layer: a panicking worker takes the
process down, and backpressure and lifecycle management would be our's to write.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="rust">Rust<a href="https://bitcrowd.dev/why-elixir-bluesky-dataplane#rust" class="hash-link" aria-label="Direct link to Rust" title="Direct link to Rust" translate="no">​</a></h3>
<p>Rust gives the highest ceiling and the tightest control. No garbage collector
means no GC pauses in the tail latency. Tokio handles enormous concurrency with
low per-task overhead, tonic covers gRPC, and the roaring-bitmap and datastore
libraries are excellent. For the compute-bound half of our workload, nothing
beats it.</p>
<p>The cost is velocity. As this service is thin on business logic, we would pay
the price of the borrow checker and the sharp edges of async Rust on every line
while protecting very little. Fan-out with Tokio tasks and channels works very
well. However, as with Go, we would need to build lifecycle, backpressure and
supervision manually.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="node--typescript">Node / TypeScript<a href="https://bitcrowd.dev/why-elixir-bluesky-dataplane#node--typescript" class="hash-link" aria-label="Direct link to Node / TypeScript" title="Direct link to Node / TypeScript" translate="no">​</a></h3>
<p>Node deserves real consideration because it's the language of the reference
DataPlane and the rest of the atproto stack: PDS, AppView frontend, lexicons.
One language across the codebase, shared types from lexicon definitions, the
largest hiring pool, the fastest iteration. For the cold path's I/O-bound record
fetches it's fine, and for modest-scale self-hosting it's a sensible default.</p>
<p>The hot path is where it breaks down. One event loop per process means in-memory
queues and request serving compete for the same loop, and any CPU-bound work
blocks both. Using multiple cores means multiple processes with no shared
memory, which brings back the cross-process coordination the in-process design
was meant to remove, and makes a large shared in-memory graph awkward. It's the
right tool for the reference implementation and a strained one for a
throughput-oriented production server.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="elixir">Elixir<a href="https://bitcrowd.dev/why-elixir-bluesky-dataplane#elixir" class="hash-link" aria-label="Direct link to Elixir" title="Direct link to Elixir" translate="no">​</a></h3>
<p>Elixir runs on the BEAM, a runtime built for massive numbers of cheap, isolated,
preemptively scheduled processes. Per-process garbage collection means no global
stop-the-world pauses, so tail latency stays flat under load. Supervision trees
give fault isolation and recovery essentially for free. For high-concurrency
request serving and a backpressured in-process queue, it's the most naturally
suited runtime of the four.</p>
<p>It has one well-known weakness: raw per-core compute. The BEAM optimises for
concurrency and consistent latency, not single-threaded number-crunching.
Byte-level work like set operations over a large follower graph is exactly where
it's slowest. Taken at face value, that rules it out for a workload with heavy
bitmap operations at its core.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="every-candidate-has-a-flaw--which-ones-can-you-fix">Every candidate has a flaw — which ones can you fix?<a href="https://bitcrowd.dev/why-elixir-bluesky-dataplane#every-candidate-has-a-flaw--which-ones-can-you-fix" class="hash-link" aria-label="Direct link to Every candidate has a flaw — which ones can you fix?" title="Direct link to Every candidate has a flaw — which ones can you fix?" translate="no">​</a></h2>
<p>Four reasonable options, each with one thing standing between it and a clean
fit. Instead of arguing it on paper, we prototyped toward each remedy, far
enough to tell whether the flaw could be engineered away or was structural.</p>
<p>Go's GC and syscall overheads respond to tuning, and Bluesky's production
DataPlane proves you can push Go a long way. But the tuning never ends. It's a
knob you keep turning for the life of the service.</p>
<p>Node's single event loop has exactly one fix, running more processes, and that
fix recreates the cross-process coordination and shared-state problems we were
designing out. There's no way around it within the language.</p>
<p>Rust has no performance flaw. Its cost showed up as soon as we started building:
hand-rolled concurrency, lifecycle and backpressure machinery on every path, for
a service with little logic to protect. We realised that that cost would not
shrink over time, but stay with us with every change we would need to make.</p>
<p>Elixir's flaw, per-core compute on tight loops, is real, and we hit it where
you'd expect: the follower-graph set operations. But it stayed in one place. It
didn't smear across the service. It sat in a single, well-defined component we
could draw a boundary around, and a component with a sharp boundary can be
replaced. That's what sent us back to Elixir for a second look.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="resolving-the-elixir-paradox">Resolving the Elixir paradox<a href="https://bitcrowd.dev/why-elixir-bluesky-dataplane#resolving-the-elixir-paradox" class="hash-link" aria-label="Direct link to Resolving the Elixir paradox" title="Direct link to Resolving the Elixir paradox" translate="no">​</a></h2>
<p>Can that component actually be lifted out? Yes, and the reason has less to do
with Elixir than with how our system is laid out.</p>
<p>Start with the claim itself, because it's often stated too broadly. "The BEAM is
slow at CPU work" is true for tight numeric loops on a single core. It is not a
claim about how much hardware a real service needs. Most of what a production
server spends its time on isn't a tight loop; it's keeping thousands of
concurrent requests moving, and the BEAM is efficient at exactly that.</p>
<p>Per-process garbage collection means no global pauses, and tail latency stays
flat enough that you can run a node closer to its limit and still hit your
latency target.</p>
<p>This is why Elixir services often need fewer machines than the Go or Node
version of the same thing, occasionally approaching Rust, even though they lose
every microbenchmark. Hardware cost under concurrency and single-core throughput
are two different measurements, and only one of them shows up on your bill.</p>
<p>That said, our workload really does contain the thing the BEAM is bad at: the
roaring-bitmap intersections and unions over the follow graph. We don't want to
talk our way around that, so we move it off the BEAM entirely.</p>
<p>The follower graph lives in a Rust implementation of Roaring Bitmaps, called
from Elixir as a NIF (Native Implemented Function). What makes this more than a
generic "wrap the slow part in Rust" patch is the shape of the data flow:</p>
<ul>
<li class="">The input crossing the boundary is tiny: a user ID, or a small set of IDs.</li>
<li class="">The graph itself, millions of edges, stays on the Rust side in native memory.
The BEAM never holds it, never copies it, never garbage-collects it.</li>
<li class="">The expensive compute, the intersections and unions, runs entirely inside Rust
at native speed.</li>
<li class="">Only the result crosses back, and because timelines are length-limited, that
result is small by construction.</li>
</ul>
<p>The usual objection to NIFs is the copy cost at the boundary. But in our case,
only small data crosses in both directions while the big structure and the heavy
computation stay native, which is the ideal case. That isn't luck. The
timeline-length limit we'd already committed to is what bounds the return
values.</p>
<p>This subtracts the BEAM's weakness from the hot path and keeps its strengths.
The compute-bound half runs in Rust regardless of host language. What's left for
Elixir is orchestration: high-concurrency request serving and the fan-out queue,
the regime where the BEAM is at its best and where our own production experience
says it's most efficient.</p>
<p>Of course, the NIF has its own price. It runs inside the BEAM's memory space, so
a crash in the Rust code can take down the VM, and a long-running call can stall
a scheduler. So we give up some of the "let it crash" isolation exactly at the
native boundary. The mitigations fit this case well: the calls are short (small
in, bounded compute, small out), and the Rust surface is small, stable, and the
kind of code that rarely changes once written. Two languages is a real
maintenance cost, but <a href="https://hexdocs.pm/rustler" target="_blank" rel="noopener noreferrer" class="">Rustler</a> keeps the boundary
ergonomic, and a small, bounded Rust core seemed the cheapest of the available
evils.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-deciding-factor-fan-out-as-code-not-infrastructure">The deciding factor: fan-out as code, not infrastructure<a href="https://bitcrowd.dev/why-elixir-bluesky-dataplane#the-deciding-factor-fan-out-as-code-not-infrastructure" class="hash-link" aria-label="Direct link to The deciding factor: fan-out as code, not infrastructure" title="Direct link to The deciding factor: fan-out as code, not infrastructure" translate="no">​</a></h2>
<p>The NIF made Elixir competitive. The fan-out requirement made it the choice.</p>
<p>Recall the spec: a deferrable queue that absorbs bursts, applies backpressure,
delivers within minutes, and degrades gracefully when the backlog grows. In Go,
Rust or Node, that almost always becomes an external system — Redis, NATS,
RabbitMQ, Kafka — because the language doesn't give you primitives to do it
safely in-process.</p>
<p>On the BEAM, those primitives are the language. Processes are the workers,
mailboxes are the queues, supervisors handle recovery, and
<a href="https://hexdocs.pm/gen_stage" target="_blank" rel="noopener noreferrer" class="">GenStage</a> and
<a href="https://hexdocs.pm/broadway" target="_blank" rel="noopener noreferrer" class="">Broadway</a> add explicit backpressure, all inside
one VM with no network hop and nothing extra to deploy. Several costs disappear
outright:</p>
<ul>
<li class="">No serialisation across a queue boundary. An external queue serialises every
fan-out job on push and deserialises it on pop, putting per-event encoding
cost right back on the hot path we'd worked to keep it off. In-process, a job
is a message between processes.</li>
<li class="">No second system to operate. No separate scaling story, no "is Redis the
bottleneck now," no disagreement between the service's view of the backlog and
the queue's.</li>
<li class="">One failure model. Supervision covers fan-out workers the same way it covers
everything else. There's no seam between "the service crashed" and "the queue
is in a weird state."</li>
<li class="">Backpressure in-band. Producer and consumer share a runtime, so the producer
notices the consumer falling behind directly instead of inferring it from
queue-depth metrics after the fact.</li>
</ul>
<p>The trade is durability. In-process means in-memory, and an in-memory backlog
dies with the node. For us that's acceptable: fan-out is best-effort timeline
population, timelines are bounded and ageing, and the fan-in path on read covers
whatever goes missing for a window. We're choosing fast, simple and
lossy-on-crash over durable, external and heavier, and the choice only works
because the rest of the architecture makes the loss cheap. If you need
durability here, much of the in-process advantage narrows. Check this assumption
against your own tolerances.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-elixir">Why Elixir<a href="https://bitcrowd.dev/why-elixir-bluesky-dataplane#why-elixir" class="hash-link" aria-label="Direct link to Why Elixir" title="Direct link to Why Elixir" translate="no">​</a></h2>
<p>We love working with Elixir, but we did not assume it to be the tool of choice
for this project. The ecosystem just did not seem to be waiting for it. Choosing
it means making the case for it, repeatedly, starting with this post.</p>
<p>And Elixir doesn't win every category. We came to the decision to use it
because, for this specific workload, the one axis it loses on — per-core compute
for graph operations — is the one we can cleanly offload to a small Rust NIF,
with a data flow that makes the offload nearly free.</p>
<p>Everything that remains is concurrency, coordination, predictable tail latency
under load, and a burst-absorbing fan-out queue we can build in the program
itself instead of bolting on as infrastructure. Go would have been the
pragmatic, proven middle.</p>
<p>Rust would have given us the highest ceiling at the cost of velocity and a lot
of hand-rolled concurrency machinery. Node was right for the reference
implementation and wrong for a memory-bound production server. Elixir plus a
thin Rust core covers both halves of a workload that genuinely has two halves.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-next">What next?<a href="https://bitcrowd.dev/why-elixir-bluesky-dataplane#what-next" class="hash-link" aria-label="Direct link to What next?" title="Direct link to What next?" translate="no">​</a></h2>
<p>Most of this is architecture-level reasoning informed by the workload's shape
and each runtime's known characteristics. We backed this up with head-to-head
benchmarks between the Node reference implementation and our Elixir POC.</p>
<p>For that, we needed to build a performance evaluation toolkit for our studies.
If you want to find out the limits of your setup,
<a class="" href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto">check it out</a></p>
<p>The pieces we lean on hardest are well-sourced: Jaz's GraphD work for the
in-memory graph and Roaring Bitmaps, and the documented existence of a
fan-in/fan-out split between expensive paging and interactive-speed set
intersection. The pieces we're least certain about - the exact timeline window,
the precise per-request CPU breakdown - we've flagged as such.</p>
<p>We have build and tested a
<a class="" href="https://bitcrowd.dev/timelines-from-elixir">proof of concept</a> that
serves the skeleton amazingly fast. What is missing now is the boring part of
writing the &gt; 90 endpoints of the DataPlane API and the indexer plugins.
Currently, it's a side project.
<a href="https://cal.eu/bitcrowd/discuss-a-project" target="_blank" rel="noopener noreferrer" class="">Give us a shout</a> if you want to help
us take it further, faster.</p>]]></content:encoded>
            <category>atproto</category>
            <category>bluesky</category>
            <category>elixir</category>
            <category>performance</category>
        </item>
        <item>
            <title><![CDATA[Serving Bluesky timelines with Elixir]]></title>
            <link>https://bitcrowd.dev/timelines-from-elixir</link>
            <guid>https://bitcrowd.dev/timelines-from-elixir</guid>
            <pubDate>Thu, 28 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Recently, we presented our performance toolkit for atproto including a small Proof-of-Concept for a dataplane written in Elixir.]]></description>
            <content:encoded><![CDATA[<p>Recently, we presented our <a class="" href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto">performance toolkit for atproto</a> including a small Proof-of-Concept for a dataplane written in Elixir.
We didn't go too much into the details though.
We will make up for that in this post.</p>
<p>Let's start by recapping the problem.</p>
<p>The <a href="https://atproto.com/" target="_blank" rel="noopener noreferrer" class="">AT Protocol</a> enables us to build an open social ecosystem of interoperable applications.
By far the most popular application in this ecosystem today is <a href="https://bsky.app/" target="_blank" rel="noopener noreferrer" class="">Bluesky</a>.</p>
<p>Bluesky has open sourced almost all of their software.
You can find their <a href="https://github.com/bluesky-social/social-app" target="_blank" rel="noopener noreferrer" class="">app</a>, their <a href="https://github.com/bluesky-social/indigo" target="_blank" rel="noopener noreferrer" class="">Go services</a> and their <a href="https://github.com/bluesky-social/atproto" target="_blank" rel="noopener noreferrer" class="">TypeScript code</a> (backend services and protocol reference implementation).</p>
<p>This is their production code, with one exception: they don't run the open source dataplane.
They've replaced it with a closed source implementation that is more performant.</p>
<figure class="image-with-caption"><img decoding="async" loading="lazy" alt="Bluesky architecture overview" src="https://bitcrowd.dev/assets/images/bluesky-5fcedfef78dd330f8c008f30b0457a02.png" width="1729" height="578" class="img_ev3q"><figcaption>The dataplane is a central component of the AppView in Bluesky's architecture. Read the<a class="" href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto">previous post</a>to learn more.</figcaption></figure>
<p>We talked about some of the issues of the open source dataplane in our <a class="" href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#the-problems-of-the-dataplane">previous blog post</a>.<br>
<strong>The bottom line is</strong>: it won't scale if you're trying to serve hundreds of thousands of users.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-you-should-care">Why you should care<a href="https://bitcrowd.dev/timelines-from-elixir#why-you-should-care" class="hash-link" aria-label="Direct link to Why you should care" title="Direct link to Why you should care" translate="no">​</a></h2>
<p>Even the Bluesky people think that <a href="https://atproto.com/blog/protocol-check-in-fall-2025#hard-decentralization" target="_blank" rel="noopener noreferrer" class="">hard decentralization</a> is necessary.
What is the point of an open ecosystem when everything is tied to the existence of a single company?</p>
<ul>
<li class="">Bluesky can be bought and start to enshittify</li>
<li class="">Bluesky operates under US law, why should we want our online social life to be ruled by mad people inside a white house</li>
<li class="">Bluesky has <a href="https://pckt.blog/b/jcalabro/april-2026-outage-post-mortem-219ebg2" target="_blank" rel="noopener noreferrer" class="">outages</a>, alternatives could continue to operate</li>
</ul>
<p>So, we should want alternative providers of Bluesky to exist.</p>
<p>Luckily for us, there is already a number of organizations picking up that work.<br>
<!-- -->Luckily for them, they can build on the open source components that Bluesky provides.<br>
<!-- -->Unluckily for them, they are left with a dataplane implementation that won't work for them when their userbase grows.</p>
<p>As a result, the Blacksky people have to spend their time rewriting <a href="https://github.com/blacksky-algorithms/rsky/tree/main/rsky-wintermute" target="_blank" rel="noopener noreferrer" class="">components in Rust</a> and our friends from Eurosky started exploring <a href="https://github.com/yawn/crimeline" target="_blank" rel="noopener noreferrer" class="">alternative ways to build timelines</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="serving-timelines-from-elixir">Serving timelines from Elixir<a href="https://bitcrowd.dev/timelines-from-elixir#serving-timelines-from-elixir" class="hash-link" aria-label="Direct link to Serving timelines from Elixir" title="Direct link to Serving timelines from Elixir" translate="no">​</a></h2>
<p>Elixir provides a number of benefits for a backend service like the dataplane.<br>
<!-- -->You work on a high level of abstraction, getting more done in the same amount of time and end up with very readable code (yes, this is still important, even for your agent!).<br>
<!-- -->At the same time you get parallelism for free and performance that scales with the cores of the system.<br>
<!-- -->Errors are isolated, so they don't bring down the whole service.
As a bonus, the VM provides you with good tooling for introspection and observability, all out of the box.</p>
<p>So, when we built our <a class="" href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#a-performant-open-source-dataplane">Proof-of-Concept</a>, we naturally chose Elixir.
For the full reasoning behind that decision (why Elixir over Go, Rust and Node), see our <a href="https://bitcrowd.dev/why-elixir-bluesky-dataplane" target="_blank" rel="noopener noreferrer" class="">companion post</a>.
We wanted to investigate the performance issues that will come up in the open source dataplane implementation.
For this reason, we focused on implementing the critical endpoint which is responsible for serving timelines to users.</p>
<p>From our exploration in the last blog post, we already know that the most important decision to solve the issues of Bluesky's open source database implementation is to change the approach from fan-in to fan-out.</p>
<p>When a user requests their timeline from Bluesky's open source database implementation it will query the posts that should be in the timeline on demand.
This is known as fan-in principle and is a major reason for the performance issues of this implementation.</p>
<p>Consequently, the implementation used by Bluesky in production turns this process on the head, known as the fan-out principle.
It constructs the timeline for each user whenever a new post arrives in the system.
When the user requests their timeline, the data is already waiting for them to be served.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="ets-tables">ETS tables<a href="https://bitcrowd.dev/timelines-from-elixir#ets-tables" class="hash-link" aria-label="Direct link to ETS tables" title="Direct link to ETS tables" translate="no">​</a></h3>
<p>With this insight, it was clear to us that we should build our dataplane on the fan-out principle.
It was less clear how we should store the data we need.
The Erlang VM has multiple built-in options for storing data, so choosing one of them, the <a href="https://www.erlang.org/doc/apps/stdlib/ets.html" target="_blank" rel="noopener noreferrer" class="">Erlang Term Storage</a> (ETS), was a good starting point.</p>
<p>Citing the documentation:</p>
<blockquote>
<p>Data is organized as a set of dynamic tables, which can store tuples.<br>
<!-- -->...<br>
<!-- -->Tables are divided into four different types, set, ordered_set, bag, and duplicate_bag. A set or ordered_set table can only have one object associated with each key. A bag or duplicate_bag table can have many objects associated with each key.</p>
</blockquote>
<p>ETS stores data as tuples in tables in memory but which tables do we need to store all the data we need to serve timelines?
And of which type should the tables be?</p>
<p>To store follows in a way that we can retrieve all the information we need efficiently, we actually need two tables: <code>followers_table</code> and <code>following_table</code>.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">new</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attribute variable" style="color:#36acaa">@followers_table</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 atom symbol" style="color:#36acaa">:duplicate_bag</span><span class="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 atom symbol" style="color:#36acaa">:named_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 atom symbol" style="color:#36acaa">:public</span><span class="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 attr-name" style="color:#00a4db">read_concurrency:</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">  </span><span class="token attr-name" style="color:#00a4db">write_concurrency:</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 atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">new</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attribute variable" style="color:#36acaa">@following_table</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 atom symbol" style="color:#36acaa">:duplicate_bag</span><span class="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 atom symbol" style="color:#36acaa">:named_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 atom symbol" style="color:#36acaa">:public</span><span class="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 attr-name" style="color:#00a4db">read_concurrency:</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">  </span><span class="token attr-name" style="color:#00a4db">write_concurrency:</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>These store the follows information in both directions, holding pairs of user IDs.<br>
<!-- -->We can find all the followers of a user by looking up the entries with their user ID as key in the <code>followers_table</code>.
And we can find all the users this user follows by looking up the entries with their user ID as key in the <code>following_table</code>.</p>
<p>Both tables are of type <code>:duplicate_bag</code>.<br>
<!-- -->We need multiple entries per key, so we must use either <code>:bag</code> or <code>:duplicate_bag</code> as type of the table.
<code>:bag</code> would have the benefit of preventing duplicates, however we chose <code>:duplicate_bag</code> for its performance benefits.</p>
<p>The <code>posts_table</code> represents the source of truth for all the posts in the system.
It's a <code>:set</code> storing post IDs as we don't want duplicates here.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">new</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attribute variable" style="color:#36acaa">@posts_table</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 atom symbol" style="color:#36acaa">:set</span><span class="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 atom symbol" style="color:#36acaa">:named_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 atom symbol" style="color:#36acaa">:public</span><span class="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 attr-name" style="color:#00a4db">read_concurrency:</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">  </span><span class="token attr-name" style="color:#00a4db">write_concurrency:</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">  </span><span class="token attr-name" style="color:#00a4db">decentralized_counters:</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>In the <code>feeds_table</code> we store the feeds of all users.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">new</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attribute variable" style="color:#36acaa">@feeds_table</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 atom symbol" style="color:#36acaa">:duplicate_bag</span><span class="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 atom symbol" style="color:#36acaa">:named_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 atom symbol" style="color:#36acaa">:public</span><span class="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 attr-name" style="color:#00a4db">read_concurrency:</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">  </span><span class="token attr-name" style="color:#00a4db">write_concurrency:</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">  </span><span class="token attr-name" style="color:#00a4db">decentralized_counters:</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>Whenever a new post arrives in our system, we look up all the followers of the author of the post using the <code>followers_table</code>.
Then, for each follower we store a pair of the follower's user ID and the post ID in the <code>feeds_table</code>.
We chose the type <code>:duplicate_bag</code> here for the same reasons as we did for the <code>followers_table</code>.
We need multiple entries per key (user ID) and we want the performance benefits of <code>:duplicate_bag</code> over <code>:bag</code>.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="celebrities">Celebrities<a href="https://bitcrowd.dev/timelines-from-elixir#celebrities" class="hash-link" aria-label="Direct link to Celebrities" title="Direct link to Celebrities" translate="no">​</a></h4>
<p>With these tables we would have a functioning system.
However, there is still the <code>celebrity_posts_table</code> to talk about.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">new</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attribute variable" style="color:#36acaa">@celebrity_posts_table</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 atom symbol" style="color:#36acaa">:duplicate_bag</span><span class="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 atom symbol" style="color:#36acaa">:named_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 atom symbol" style="color:#36acaa">:public</span><span class="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 attr-name" style="color:#00a4db">read_concurrency:</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">  </span><span class="token attr-name" style="color:#00a4db">write_concurrency:</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">  </span><span class="token attr-name" style="color:#00a4db">decentralized_counters:</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>This table is actually a first optimization of the system.
You might have wondered what would happen if a post arrives in the system authored by a user who has many many followers.
If we would stick to our <code>feeds_table</code>, we would have to loop through all of them to insert the corresponding data.</p>
<p>For this reason, we introduced the <code>celebrity_posts_table</code>.
The key idea here is that if a user has too many followers (is a celebrity), we give their posts a special treatment to avoid looping through all the followers and the associated performance issues.
Instead we will store pairs of the author and post IDs in <code>celebrity_posts_table</code>, which is again of type <code>:duplicate_bag</code>.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">insert_post</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">post_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> author_id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">insert</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attribute variable" style="color:#36acaa">@posts_table</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">post_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> author_id</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">  follower_entries </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">lookup</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attribute variable" style="color:#36acaa">@followers_table</span><span class="token punctuation" style="color:#393A34">,</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">  limit </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fan_out_limit</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"> limit </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:infinity</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">or</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">length</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">follower_entries</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"> limit </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">each</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">follower_entries</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">fn</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">_subject</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> follower_id</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </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 atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">insert</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attribute variable" style="color:#36acaa">@feeds_table</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">follower_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> post_id</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">end</span><span class="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">else</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">insert</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attribute variable" style="color:#36acaa">@celebrity_posts_table</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">author_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> post_id</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">end</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">end</span><br></span></code></pre></div></div>
<p>When a user request their timeline, we will first look for their feed in the <code>feeds_table</code>, then do another lookup in the <code>celebrity_posts_table</code> to gather posts from celebrities they follow.
The user will then see a combination of those posts.
So, in fact this dataplane combines fan-out (for regular posts in <code>feeds_table</code>) with fan-in (for celebrity posts in <code>celebrity_posts_table</code>) for performance reasons.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">get_timeline</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user_id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  fan_out_posts </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 attribute variable" style="color:#36acaa">@feeds_table</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">|&gt;</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">lookup</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user_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 operator" style="color:#393A34">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">&amp;</span><span class="token function" style="color:#d73a49">elem</span><span class="token punctuation" style="color:#393A34">(</span><span class="token argument variable" style="color:#36acaa">&amp;1</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" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  celebrity_posts </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 attribute variable" style="color:#36acaa">@following_table</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">|&gt;</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">lookup</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user_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 operator" style="color:#393A34">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">flat_map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">fn</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">_actor</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> followed_id</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </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 atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">lookup</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attribute variable" style="color:#36acaa">@celebrity_posts_table</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> followed_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 keyword" style="color:#00009f">end</span><span class="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">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">&amp;</span><span class="token function" style="color:#d73a49">elem</span><span class="token punctuation" style="color:#393A34">(</span><span class="token argument variable" style="color:#36acaa">&amp;1</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" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  fan_out_posts </span><span class="token operator" style="color:#393A34">++</span><span class="token plain"> celebrity_posts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">end</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></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="evaluating-our-dataplane">Evaluating our dataplane<a href="https://bitcrowd.dev/timelines-from-elixir#evaluating-our-dataplane" class="hash-link" aria-label="Direct link to Evaluating our dataplane" title="Direct link to Evaluating our dataplane" translate="no">​</a></h3>
<p>To get an idea about how much performance we gain with our dataplane, we evaluated it using our <a class="" href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#the-outcomes">simulator</a>.
The dashboard below shows the results of this comparison.</p>
<p>We can see two blocks of data:</p>
<p>The first run was recorded with Bluesky's open source dataplane.
We can see a maximum p99 latency of more than 60 ms. Furthermore, we see the throughput of requests per second fluctuating around 600.</p>
<p>In comparison, the block on the right shows recordings of the same simulation with our dataplane. The latencies are tiny, the throughput sits smoothly at 800 requests per second.</p>
<p><img decoding="async" loading="lazy" alt="Grafana dashboard showing data for two simulation runs" src="https://bitcrowd.dev/assets/images/results_simulator-c843918ee3774947ed1e5375d2f9f131.png" width="1314" height="851" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="crimeline-comparison">Crimeline comparison<a href="https://bitcrowd.dev/timelines-from-elixir#crimeline-comparison" class="hash-link" aria-label="Direct link to Crimeline comparison" title="Direct link to Crimeline comparison" translate="no">​</a></h3>
<p><a href="https://github.com/yawn/crimeline" target="_blank" rel="noopener noreferrer" class="">Crimeline</a> is an experimental timeline builder which caught our attention when doing research for this project.
Like our proof of concept, Crimeline is not a complete dataplane implementation.
Unlike our proof of concept, Crimeline focuses on creating efficient datastructures and conveniently comes with synthetic benchmarks for them.<br>
<!-- -->After having seen the end-to-end evaluation of our dataplane, we were curious how ETS tables would perform in those benchmarks.</p>
<p>The <a href="https://github.com/yawn/crimeline/blob/main/README.md" target="_blank" rel="noopener noreferrer" class="">README</a> describes one of Crimeline's datastructures, the <a href="https://github.com/yawn/crimeline/blob/main/src/users/map.rs" target="_blank" rel="noopener noreferrer" class="">UserMap</a>, as follows:</p>
<blockquote>
<p>Sharded adjacency map. Each uid is split via bitmask into shard index (low bits) and backbone index (high bits). Each shard holds a <code>Vec&lt;Vec&lt;Uid&gt;&gt;</code> — a dense backbone of sorted adjacency lists.</p>
</blockquote>
<p>It's a struct that holds information whether a user ID is included in it (think your followers on Bluesky) with efficient methods to add and remove user IDs as well as performing the lookup of an ID.</p>
<p>An ETS-based equivalent looks like this:</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">defmodule</span><span class="token plain"> </span><span class="token module class-name">UserMapEts</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token attribute variable" style="color:#36acaa">@user</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</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">def</span><span class="token plain"> new </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">new</span><span class="token punctuation" style="color:#393A34">(</span><span class="token atom symbol" style="color:#36acaa">:forward</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 atom symbol" style="color:#36acaa">:duplicate_bag</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:public</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 atom symbol" style="color:#36acaa">:read_concurrency</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 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">end</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">populated</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">n</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    table </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">new</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">    tuples </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> t </span><span class="token operator" style="color:#393A34">&lt;-</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token operator" style="color:#393A34">..</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">n </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 punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">do:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token attribute variable" style="color:#36acaa">@user</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> t </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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">insert</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">table</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> tuples</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    table</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">end</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">add</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">table</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> target</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">insert</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">table</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 attribute variable" style="color:#36acaa">@user</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> target</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">end</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">add_bulk_list</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">table</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> targets</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    tuples </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> t </span><span class="token operator" style="color:#393A34">&lt;-</span><span class="token plain"> targets</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">do:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token attribute variable" style="color:#36acaa">@user</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"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">insert</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">table</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> tuples</span><span class="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">end</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">add_bulk_each</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">table</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> targets</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">each</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">targets</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">fn</span><span class="token plain"> t </span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">insert</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">table</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 attribute variable" style="color:#36acaa">@user</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 punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">end</span><span class="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">end</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">contains</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">table</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> target</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">match_object</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">table</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 attribute variable" style="color:#36acaa">@user</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> target</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 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">end</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">remove</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">table</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> target</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">delete_object</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">table</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 attribute variable" style="color:#36acaa">@user</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> target</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">end</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">destroy</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">table</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">delete</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">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 keyword" style="color:#00009f">end</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">end</span><br></span></code></pre></div></div>
<p>We can expect this to be a bit slower than the Rust implementation of Crimeline due to the overhead of the Erlang VM.<br>
<!-- -->The table below shows the average time for each operation in <strong>nanoseconds</strong> when performed with 100 users.<br>
<!-- -->From our results, we can see that ETS tables nevertheless perform relatively well.</p>
<table><thead><tr><th>Operation</th><th>Rust UserMap</th><th>ETS UserMap</th></tr></thead><tbody><tr><td>add</td><td>18 ns</td><td>186 ns</td></tr><tr><td>contains_hit</td><td>8 ns</td><td>1360 ns</td></tr><tr><td>contains_miss</td><td>8 ns</td><td>1350 ns</td></tr><tr><td>remove</td><td>26 ns</td><td>599 ns</td></tr></tbody></table>
<p>So overall we were happy but two benchmarks stood out with growing numbers of users: <code>contains_hit</code> and <code>contains_miss</code>.</p>
<p>Note the units in the following table.
While the Rust UserMap goes from <code>8 ns</code> to <code>14 ns</code> (not even 2x) when increasing the number of users from <code>100</code> to <code>100_000</code>, ETS goes from <code>1.36 μs</code> to <code>1.11 ms</code>, almost 1000x.</p>
<table><thead><tr><th>Operation (number of users)</th><th>Rust UserMap</th><th>ETS UserMap</th></tr></thead><tbody><tr><td>contains_hit (100)</td><td>8 ns</td><td>1.36 μs</td></tr><tr><td>contains_hit (100_000)</td><td>14 ns</td><td>1.11 ms</td></tr><tr><td>contains_miss (100)</td><td>8 ns</td><td>1.35 μs</td></tr><tr><td>contains_miss (100_000)</td><td>14 ns</td><td>1.11 ms</td></tr></tbody></table>
<p>Why is this so slow?
From the <a href="https://www.erlang.org/doc/apps/stdlib/ets.html" target="_blank" rel="noopener noreferrer" class="">ETS docs</a>:</p>
<blockquote>
<p>Insert and lookup times in tables of type set are constant, regardless of the table size. For table types bag and duplicate_bag time is proportional to the number of objects with the same key.</p>
</blockquote>
<p>We chose a <code>:duplicate_bag</code> for the table to improve insert performance.
Now we are paying for that with slow lookups at scale.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="roaring-bitmaps">Roaring Bitmaps<a href="https://bitcrowd.dev/timelines-from-elixir#roaring-bitmaps" class="hash-link" aria-label="Direct link to Roaring Bitmaps" title="Direct link to Roaring Bitmaps" translate="no">​</a></h4>
<p>We didn't give up here, of course.
So let's see what we can improve.</p>
<p>During our research, we also came across <a href="https://jazco.dev/" target="_blank" rel="noopener noreferrer" class="">Jaz's</a> blog and learned how to <a href="https://jazco.dev/2024/04/15/in-memory-graphs/" target="_blank" rel="noopener noreferrer" class="">shrink the size of your datastructures</a> and go even further with <a href="https://jazco.dev/2024/04/20/roaring-bitmaps/" target="_blank" rel="noopener noreferrer" class="">Roaring Bitmaps</a>.
The latter seems to be a good fit for optimizing our user map datastructure.</p>
<p>Luckily, <a href="https://hex.pm/packages/rustler" target="_blank" rel="noopener noreferrer" class="">rustler</a> lowers the barrier to use Rust crates from Elixir a lot these days.
Conveniently for us, Aaron Gunderson already did the work to wrap the <a href="https://crates.io/crates/roaring" target="_blank" rel="noopener noreferrer" class="">roaring crate</a> in an <a href="https://hex.pm/packages/roaring" target="_blank" rel="noopener noreferrer" class="">Elixir package</a> and <a href="https://agundy.com/blog/using-roaring-bitmaps-in-elixir/" target="_blank" rel="noopener noreferrer" class="">blogged about it</a>.
So, it's ready for us to use.</p>
<p>As a very brief summary, bitmaps are a suitable datastructure to compress the follower graph in our dataplane.
Instead of storing pairs of <code>{user_id, follower_id}</code>, we store a single array for each user where each bit represents whether the user with the id at the index follows the user.</p>
<p>Say we have 5 users, and our ids start at 0. If the first four users all follow user with id 4, we would store the following pairs in our ETS table:</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"># user_id, follower_id</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">{4, 0}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">{4, 1}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">{4, 2}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">{4, 3}</span><br></span></code></pre></div></div>
<p>With a bitmap, we would store a single array of bits for the user with id 4.
If the bit is <code>1</code>, the corresponding user follows the user with id 4.</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"># user_id/index:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># 0, 1, 2, 3, 4</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> [1, 1, 1, 1, 0]</span><br></span></code></pre></div></div>
<p>You can see how that compresses the required space in memory in this simplified example.<br>
<!-- -->Assuming 1 byte per id, we would have 4 * 2 = 8 bytes of memory in our ETS table variant.
The bitmap however is complete with 5 bits which fit in a single byte.</p>
<p><a href="https://roaringbitmap.org/" target="_blank" rel="noopener noreferrer" class="">Roaring bitmaps</a> apply further optimizations on top of this basic idea.</p>
<p>With the <code>roaring</code> package we can plug the Roaring Bitmap into our user map module.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">defmodule</span><span class="token plain"> </span><span class="token module class-name">UserMapRoaring</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</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">def</span><span class="token plain"> new </span><span class="token keyword" style="color:#00009f">do</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 atom symbol" style="color:#36acaa">:ok</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> roaring</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 module class-name">RoaringBitmap64</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">new</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">    roaring</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">end</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">populated</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">n</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    list </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> t </span><span class="token operator" style="color:#393A34">&lt;-</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token operator" style="color:#393A34">..</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">n </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 punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">do:</span><span class="token plain"> t</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 atom symbol" style="color:#36acaa">:ok</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> roaring</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 module class-name">RoaringBitmap64</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">from_list</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">list</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    roaring</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">end</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">add</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roaring</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> target</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token module class-name">RoaringBitmap64</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">insert</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roaring</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> target</span><span class="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">end</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">add_bulk_list</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roaring</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> targets</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</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"> t </span><span class="token operator" style="color:#393A34">&lt;-</span><span class="token plain"> targets</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">do:</span><span class="token plain"> </span><span class="token module class-name">RoaringBitmap64</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">insert</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roaring</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"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">end</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">add_bulk_each</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roaring</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> targets</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">each</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">targets</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">fn</span><span class="token plain"> t </span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain"> </span><span class="token module class-name">RoaringBitmap64</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">insert</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roaring</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"> </span><span class="token keyword" style="color:#00009f">end</span><span class="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">end</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">contains</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roaring</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> target</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token module class-name">RoaringBitmap64</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">contains?</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roaring</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> target</span><span class="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">end</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">remove</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roaring</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> target</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token module class-name">RoaringBitmap64</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">remove</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roaring</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> target</span><span class="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">end</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">destroy</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">_roaring</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token atom symbol" style="color:#36acaa">:ok</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">end</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">end</span><br></span></code></pre></div></div>
<p>With that we get the following results.</p>
<table><thead><tr><th>Operation (number of users)</th><th>Rust UserMap</th><th>ETS UserMap</th><th>Roaring Bitmaps UserMap</th></tr></thead><tbody><tr><td>contains_hit (100)</td><td>8 ns</td><td>1.36 μs</td><td>104 ns</td></tr><tr><td>contains_hit (100_000)</td><td>14 ns</td><td>1.11 ms</td><td>100 ns</td></tr><tr><td>contains_miss (100)</td><td>8 ns</td><td>1.35 μs</td><td>98 ns</td></tr><tr><td>contains_miss (100_000)</td><td>14 ns</td><td>1.11 ms</td><td>100 ns</td></tr></tbody></table>
<p>We're still slower than Crimeline's implementation but it's acceptable and more importantly does not degrade with the number of users.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="comparison-in-a-more-realistic-scenario">Comparison in a more realistic scenario<a href="https://bitcrowd.dev/timelines-from-elixir#comparison-in-a-more-realistic-scenario" class="hash-link" aria-label="Direct link to Comparison in a more realistic scenario" title="Direct link to Comparison in a more realistic scenario" translate="no">​</a></h4>
<p>After satisfying our curiosity about Crimeline's synthetic benchmarks, we were wondering how the comparison would look like in a more realistic scenario.
Therefore, we added another benchmark <code>feeds</code> that is somewhere in between Crimeline's benchmarks and the work we've previously done with our simulator.
Interestingly, in this more realistic scenario the performance difference between the implementations diminishes.</p>
<table><thead><tr><th>Operation (number of follows)</th><th>Rust feed</th><th>ETS feed</th><th>Roaring Bitmaps feed</th></tr></thead><tbody><tr><td>get_feed (100)</td><td>41 µs</td><td>9 μs</td><td>8 μs</td></tr><tr><td>get_feed (100_000)</td><td>57 µs</td><td>117 μs</td><td>108 μs</td></tr><tr><td>get_feed (1_000_000)</td><td>58 µs</td><td>118 μs</td><td>109 μs</td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-to-go-from-here">Where to go from here?<a href="https://bitcrowd.dev/timelines-from-elixir#where-to-go-from-here" class="hash-link" aria-label="Direct link to Where to go from here?" title="Direct link to Where to go from here?" translate="no">​</a></h2>
<p>We showed how to build a basic dataplane implementation in Elixir, then added optimizations to squeeze out the issues we encountered.</p>
<p>One essential feature that is missing from our implementation is persistence.
So far we store everything in memory, so our data wouldn't survive a restart of the server.</p>
<p>Our goal is to provide a simple to use dataplane implementation.
So, the first idea would be to add persistence with Postgres and treat our in-memory data as an index to avoid costly queries and retrieve the data directly by primary key.
Another idea is to provide an adapter interface so that everyone can use their preferred database.</p>
<p>We've seen how to combine Rust and Elixir to get the strengths of both while mitigating their weaknesses (Discord has a great <a href="https://discord.com/blog/using-rust-to-scale-elixir-for-11-million-concurrent-users" target="_blank" rel="noopener noreferrer" class="">write-up</a> on the same idea).
We dig into <em>why</em> this pairing fits a dataplane so well in our <a href="https://bitcrowd.dev/why-elixir-bluesky-dataplane" target="_blank" rel="noopener noreferrer" class="">companion post</a>; here the takeaway is practical.
We can optimize the hot spots in Rust, confident the native code won't crash the VM thanks to Rust's memory-safety guarantees, while keeping the readable, high-velocity Elixir everywhere else.
Whether it's Roaring Bitmaps or even wrapping the Crimeline datastructures, a whole ecosystem of crates is available to us.</p>
<p>With that, we have all the tools to build the remaining features a complete dataplane implementation needs and we are ready to add more advanced features like the <a href="https://jazco.dev/2024/04/15/in-memory-graphs/" target="_blank" rel="noopener noreferrer" class="">social proof</a> that is available in Bluesky.</p>]]></content:encoded>
            <category>atproto</category>
            <category>bluesky</category>
            <category>elixir</category>
            <category>performance</category>
        </item>
        <item>
            <title><![CDATA[Building Wiblio: customizing Discourse for Berlinʼs public libraries]]></title>
            <link>https://bitcrowd.dev/building-wiblio</link>
            <guid>https://bitcrowd.dev/building-wiblio</guid>
            <pubDate>Wed, 27 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Weʼve been working with the ZLB (Berlinʼs public libraries) to build a social networking platform called “Wiblio” that communities meeting at local libraries can use as their digital home.]]></description>
            <content:encoded><![CDATA[<p>Weʼve been working with the <a href="https://www.zlb.de/" target="_blank" rel="noopener noreferrer" class="">ZLB</a> (Berlinʼs public libraries) to build a social networking platform called “Wiblio” that communities meeting at local libraries can use as their digital home.
Since open source was a key requirement of the project, Discourse was chosen as the foundation.</p>
<p>Discourse alone wasnʼt enough — we needed a few major customizations to bring Wiblioʼs functionality and design to life.
In this post, Iʼll walk through how we approached customizing Discourse so far, and the challenges we ran into along the way.</p>
<div class="cta"><div class="container"><div class="row"><p>
➡️ Learn more about the project at <!-- --> <a href="https://bitcrowd.net/en/projects/wiblio/" class="plausible-event-name=Button+Click plausible-event-location=Post+Cta+afterIntroBuildingWiblio">the case study on bitcrowd.net</a></p></div></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-is-discourse">What is Discourse?<a href="https://bitcrowd.dev/building-wiblio#what-is-discourse" class="hash-link" aria-label="Direct link to What is Discourse?" title="Direct link to What is Discourse?" translate="no">​</a></h2>
<p><a href="https://www.discourse.org/" target="_blank" rel="noopener noreferrer" class="">Discourse</a> is an open source internet forum system built with a Ruby on Rails backend and an Ember frontend. It already comes with a lot of functionality and preinstalled plugins, like chat, calendar, events, and more.
Discourse is built around the idea of being customizable. Customizations are possible via multiple layers:</p>
<ul>
<li class=""><strong>Admin settings.</strong> Discourse exposes a surprising amount of behaviour through admin settings. Some examples: chat as overlay vs. fullscreen, calendars on category pages, fine-grained access rights, and so on.</li>
<li class=""><strong>Plugins.</strong> Plugins can change backend behaviour and the frontend. Examples: <a href="https://meta.discourse.org/t/discourse-calendar-and-event/97376" target="_blank" rel="noopener noreferrer" class="">Discourse Calendar</a> and <a href="https://meta.discourse.org/t/discourse-chat/230881" target="_blank" rel="noopener noreferrer" class="">Discourse Chat</a></li>
<li class=""><strong>Themes and theme components.</strong> These customize the frontend with CSS and JavaScript.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="figuring-out-what-we-needed-to-build">Figuring out what we needed to build<a href="https://bitcrowd.dev/building-wiblio#figuring-out-what-we-needed-to-build" class="hash-link" aria-label="Direct link to Figuring out what we needed to build" title="Direct link to Figuring out what we needed to build" translate="no">​</a></h2>
<p>Discourse has a massive codebase and can be customized in many ways through its settings. One important task we worked on together with the ZLB people was to identify which features could already be enabled simply by adjusting these settings.
We also researched existing plugins to find out whether maybe a feature we needed was already implemented and available.</p>
<p>This was already quite some work and a question we asked ourselves regularly was: “Could we just solve this using some admin, site, or access right settings instead of implementing something new?”
When a setting or existing feature was enough, we saved implementation and maintenance effort, important for the MVP. It’s also less likely that something breaks on core updates to Discourse.</p>
<p>In the end, three areas required custom work:</p>
<ol>
<li class="">Allowing users to use the platform even without confirming their email address, and the extra security measurements needed to safely enable that.</li>
<li class="">Adding features that make the user feel like they are moving in their chosen community and not a general forum with all users.</li>
<li class="">Building a custom Wiblio theme.</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="implementing-the-changes">Implementing the changes<a href="https://bitcrowd.dev/building-wiblio#implementing-the-changes" class="hash-link" aria-label="Direct link to Implementing the changes" title="Direct link to Implementing the changes" translate="no">​</a></h2>
<p>It became clear that the easy-access (1) and community (2) requirements were independent. So we decided to create a new plugin for each of them.
As the plugins in the long run should be usable by others, this design choice also had the benefit that the use of the plugins is more flexible: Discourse instances might use only one or the other and should still work fine.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-plugins">The plugins<a href="https://bitcrowd.dev/building-wiblio#the-plugins" class="hash-link" aria-label="Direct link to The plugins" title="Direct link to The plugins" translate="no">​</a></h3>
<p>Implementing a plugin that in some way changes the core Discourse behaviour is a balancing act where you are trying to make sure the changes are in place, but in a way that is not too invasive or too fragile under core updates.</p>
<p>To adjust backend behaviour, Discourse provides a range of helpers, like event hooks, modifiers, and more. When those helpers are not enough, the last resort was to override an existing Discourse method.</p>
<p>To add an element to the UI, Discourse provides “Plugin Outlets”. These act as named slots in the DOM where a plugin can render its own content.</p>
<p>In the image below you can see the Plugin Outlets on the category show page. Each green box represents one outlet. So for example we used the <code>above-main-container</code> outlet to render a custom header component on most of the pages.</p>
<figure class="image-with-caption"><div class="drop-shadow"><p><img decoding="async" loading="lazy" alt="A category show page with a lot of markers for plugin outlets" src="https://bitcrowd.dev/assets/images/plugin-outlets-3409f07e70d19ad7c38a486eb39f65a9.png" width="3424" height="1934" class="img_ev3q"></p></div><figcaption>Plugin Outlets on a category show page</figcaption></figure>
<p>One thing we learned: building a brand-new page that the plugin fully controls was generally a lot easier than doing complex overrides for an existing page. We could still reuse Discourseʼs components and logic, but have full control under which conditions they are shown and what else we display on the page.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="adding-new-components-in-a-plugin">Adding new components in a plugin<a href="https://bitcrowd.dev/building-wiblio#adding-new-components-in-a-plugin" class="hash-link" aria-label="Direct link to Adding new components in a plugin" title="Direct link to Adding new components in a plugin" translate="no">​</a></h4>
<p>For the community plugin we added a full new “Dashboard” page that required several new components: a header, a card and a card button component.</p>
<figure class="image-with-caption"><div class="drop-shadow"><p><img decoding="async" loading="lazy" alt="The dashboard page with new components marked in red: header, card and card button components" src="https://bitcrowd.dev/assets/images/dashboard-ecddccf56a9de8603c652ceb89131ec2.png" width="3356" height="1942" class="img_ev3q"></p></div><figcaption>The new dashboard page with new components</figcaption></figure>
<p>We wanted these components to be themeable, but to also look good out of the box if a theme didnʼt restyle them.</p>
<p>To achieve this, we built a structured set of components following the atomic design architecture that Discourse uses, using Discourseʼs built-in components as much as possible.
Each component was built with sensible CSS defaults, and exposed CSS variables to be overridden in the theme.
Additionally, we attached classnames to the components and many of their elements to allow rich customisation.</p>
<p>We used the <a href="https://meta.discourse.org/t/styleguide-plugin-now-in-discourse-core/167293" target="_blank" rel="noopener noreferrer" class="">Styleguide</a> plugin that’s recently been made core to discourse, to provide an isolated environment where we could implement our new components.
For the new dashboard components, this meant we added new sections for the header, the card, and the card button component, as well as for the composite components such as the forum card and upcoming events card.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-theme">The theme<a href="https://bitcrowd.dev/building-wiblio#the-theme" class="hash-link" aria-label="Direct link to The theme" title="Direct link to The theme" translate="no">​</a></h3>
<p>The themeʼs job was to realize the visual design on top of everything the plugins and Discourse core provided.
We worked with a colleague from the ZLB to extract the design tokens from the designs and implement them in the theme as a two-tiered system of CSS variables.</p>
<p>The Styleguide plugin made this much easier: separated from the functionality and state of the main app, we render the real components in every state and variant that’s needed, where they can be compared directly to designs.
This is a great dev environment, leading to quicker and more consistent implementation, and allows the user interface components to be QAd independently of the rest of the work.</p>
<figure class="image-with-caption"><div class="drop-shadow"><p><img decoding="async" loading="lazy" alt="A side by side comparison of the “Expander” component in the Styleguide" src="https://bitcrowd.dev/assets/images/styleguide-e7b54afffadb16b8ed9dfaceee707725.png" width="3456" height="2006" class="img_ev3q"></p></div><figcaption>Example of a component in the Styleguide - on the left with Discourse's default “Foundation” theme and on the right with the “Wiblio” theme</figcaption></figure>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wrapping-up">Wrapping up<a href="https://bitcrowd.dev/building-wiblio#wrapping-up" class="hash-link" aria-label="Direct link to Wrapping up" title="Direct link to Wrapping up" translate="no">​</a></h2>
<p>Wiblio was pre-released at <a href="https://youtu.be/PuLvBOmt3dE?is=XSCbrCj0B1lzEil7" target="_blank" rel="noopener noreferrer" class="">re<!-- -->:publica<!-- --> 2026</a> 🎉. But itʼs not fully live yet — so this post is more “notes from along the way” than a retrospective.</p>
<p>In this project, we focused on reusability: making sure every part we built could be reused on another Discourse instance: splitting features into independent plugins, building on Discourseʼs own components, exposing CSS variables, documenting each component in the Styleguide.</p>
<p>We also learned that “customizing Discourse” can mean a lot of different things — each new feature has been its own little judgement call between “use a setting”, “write a plugin”, or “tweak the theme”. We might revisit some decisions. Whatʼs worked for us so far is staying as close to Discourseʼs built-in extension points as we can, and accepting a new dedicated page over a fragile override whenever thatʼs an option.</p>]]></content:encoded>
            <category>Discourse</category>
            <category>open source</category>
            <category>ruby-on-rails</category>
            <category>frontend</category>
        </item>
        <item>
            <title><![CDATA[Cuprite, first thoughts]]></title>
            <link>https://bitcrowd.dev/cuprite</link>
            <guid>https://bitcrowd.dev/cuprite</guid>
            <pubDate>Thu, 21 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[We've recently been quite excited about the Ruby CDP project over here at bitcrowd. It's a fleet of open source Ruby projects which utilize the Chrome Dev Tools Protocol (CDP) to instrument a Chrome/Chromium browser. We started using some of them as replacements for bulkier, outdated or just "not-so-nice" tools and wanted to give a quick summary of our first impressions.]]></description>
            <content:encoded><![CDATA[<p>We've recently been quite excited about the <a href="https://github.com/rubycdp" target="_blank" rel="noopener noreferrer" class="">Ruby CDP</a> project over here at bitcrowd. It's a fleet of open source Ruby projects which utilize the <a href="https://chromedevtools.github.io/devtools-protocol/" target="_blank" rel="noopener noreferrer" class="">Chrome Dev Tools Protocol</a> (CDP) to instrument a Chrome/Chromium browser. We started using some of them as replacements for bulkier, outdated or just "not-so-nice" tools and wanted to give a quick summary of our first impressions.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-is-cdp">What is CDP?<a href="https://bitcrowd.dev/cuprite#what-is-cdp" class="hash-link" aria-label="Direct link to What is CDP?" title="Direct link to What is CDP?" translate="no">​</a></h2>
<p>First up, what is this CDP thing actually? In brief, it's a low-level JSON-API running over a WebSocket-connection to control Chrome's browser engine. It was originally developed for Chrome's DevTools UI, but Google at some point stabilized and documented it, so that it can be used by external tools as well. It's an alternative to the old HTTP-based <a href="https://developer.mozilla.org/en-US/docs/Web/WebDriver" target="_blank" rel="noopener noreferrer" class="">WebDriver</a> standard.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="outlook-webdriver-bidi">Outlook: WebDriver BiDi<a href="https://bitcrowd.dev/cuprite#outlook-webdriver-bidi" class="hash-link" aria-label="Direct link to Outlook: WebDriver BiDi" title="Direct link to Outlook: WebDriver BiDi" translate="no">​</a></h3>
<p>While CDP is tied to Chrome alone, there is also a new version of the WebDriver standard currently evolving. It's named <a href="https://www.w3.org/TR/webdriver-bidi/" target="_blank" rel="noopener noreferrer" class="">WebDriver BiDi</a> - as it's <em>"bi-directional"</em> - and essentially taking all the good parts from WebDriver and CDP and mixing them together. At the time of writing, it's still work in progress though and using it in Selenium requires dropping down to the raw <code>driver.browser</code>-level API.<sup><a href="https://bitcrowd.dev/cuprite#user-content-fn-1-72162c" id="user-content-fnref-1-72162c" data-footnote-ref="true" aria-describedby="footnote-label" class="anchorTargetStickyNavbar_Vzrq">1</a></sup></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="ruby-cdp">Ruby CDP<a href="https://bitcrowd.dev/cuprite#ruby-cdp" class="hash-link" aria-label="Direct link to Ruby CDP" title="Direct link to Ruby CDP" translate="no">​</a></h3>
<p>The Ruby CDP project makes an effort to build higher level Ruby APIs on top of the CDP protocol. At the time of writing, they have three projects:</p>
<ol>
<li class=""><a href="https://github.com/rubycdp/ferrum" target="_blank" rel="noopener noreferrer" class="">Ferrum</a> - a high-level API to control Chrome in Ruby</li>
<li class=""><a href="https://github.com/rubycdp/cuprite" target="_blank" rel="noopener noreferrer" class="">Cuprite</a> - a pure-Ruby driver for Capybara based on Ferrum</li>
<li class=""><a href="https://github.com/rubycdp/vessel" target="_blank" rel="noopener noreferrer" class="">Vessel</a> - a high-level web crawling framework also based on Ferrum</li>
</ol>
<p>The idea is to have pure-Ruby tooling based on the Chrome DevTools protocol without requiring too many external dependencies from other ecosystems.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="first-impressions">First impressions<a href="https://bitcrowd.dev/cuprite#first-impressions" class="hash-link" aria-label="Direct link to First impressions" title="Direct link to First impressions" translate="no">​</a></h2>
<p>We've been using <a href="https://github.com/excid3/ferrum_pdf" target="_blank" rel="noopener noreferrer" class="">FerrumPDF</a>, a PDF library built on top of Ferrum to replace <a href="https://github.com/Studiosity/grover" target="_blank" rel="noopener noreferrer" class="">Grover</a> in one of our projects. Grover's dependency on the <a href="https://www.npmjs.com/package/puppeteer" target="_blank" rel="noopener noreferrer" class="">Puppeteer</a> NPM package was a bit of a maintenance burden and it's nicer to have less individual tools involved. Up until now, we're happy with the switch, but more on that in a <a class="" href="https://bitcrowd.dev/optimizing-pdf-render-time-in-rails">different post…</a></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="cuprite">Cuprite<a href="https://bitcrowd.dev/cuprite#cuprite" class="hash-link" aria-label="Direct link to Cuprite" title="Direct link to Cuprite" translate="no">​</a></h3>
<p>We also switched the test suite of a large Rails app from using <a href="https://www.seleniumhq.org/" target="_blank" rel="noopener noreferrer" class="">Selenium</a> to <a href="https://github.com/rubycdp/cuprite" target="_blank" rel="noopener noreferrer" class="">Cuprite</a> as a driver for our <a href="https://teamcapybara.github.io/capybara/" target="_blank" rel="noopener noreferrer" class="">Capybara</a> end-to-end feature tests.</p>
<p>The pitch is:</p>
<blockquote>
<p>Cuprite is a pure Ruby driver (read as no Selenium/ WebDriver/ ChromeDriver dependency) for Capybara. It allows you to run Capybara tests on a headless Chrome or Chromium. Under the hood it uses Ferrum which is high-level API to the browser by CDP protocol. The design of the driver is as close to Poltergeist as possible though it's not a goal.</p>
</blockquote>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="before">Before<a href="https://bitcrowd.dev/cuprite#before" class="hash-link" aria-label="Direct link to Before" title="Direct link to Before" translate="no">​</a></h4>
<p>The app is quite aged already and has a rather big and long running suite of feature tests. The tests used to run in a headless Chrome browser driven through Selenium. Under the hood, Selenium used <a href="https://developer.chrome.com/docs/chromedriver/" target="_blank" rel="noopener noreferrer" class="">Chromedriver</a>, which is a standalone server that implements the WebDriver standard to then talk to a Chrome browser to perform actions in there. The setup still works well and we used it in a lot of projects over the years.</p>
<p>But for this particular app, the feature test suite was rather flaky. While being a classic server-side rendered Rails app, the app is quite JavaScript-heavy and contains sections which are pure ReactJS with state management on the client. With all of this in place, the feature tests of this app were unreliable: Elements may have been re-rendered and therefore become stale while Capybara wanted to interact with them, some interactions involved loading screens and animations and were taking longer than what Capybara expected, etc.</p>
<p>One error that appeared often was Selenium having problems with elements being re-rendered:</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">Selenium::WebDriver::Error::UnknownError:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  unknown error: unhandled inspector error: {"code":-32000,"message":"Node with given id does not belong to the document"}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    (Session info: chrome=144.0.7559.96)</span><br></span></code></pre></div></div>
<p>In order to mitigate all the falkiness, there were things in place like this:</p>
<div class="language-ruby codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ruby 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">config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">around </span><span class="token symbol" style="color:#36acaa">:each</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token symbol" style="color:#36acaa">:js</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">|</span><span class="token plain">ex</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"> ex</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">run_with_retry </span><span class="token keyword" style="color:#00009f">retry</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 plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">end</span><br></span></code></pre></div></div>
<div class="language-ruby codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ruby 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">config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">default_max_wait_time </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10</span><br></span></code></pre></div></div>
<p>Capybara was set to wait up until 10 seconds - instead of the default two seconds - for asynchronous processes to finish. Test tagged with "js" would be retried up to three times before they would finally be reported as failing. Plus, on CI the failed tests were also <strong>retried once more</strong> before they would be marked as failing:</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">bundle </span><span class="token builtin class-name">exec</span><span class="token plain"> rspec spec/features --exclude-pattern </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> bundle </span><span class="token builtin class-name">exec</span><span class="token plain"> rspec spec/features --only-failures</span><br></span></code></pre></div></div>
<p>With all of this in place, the feedback of CI was super slow. Failing tests would be retried up to six times until CI was finally done and red.</p>
<p>Running the test suite locally was equally painful. Skipping the retry mechanisms in favor of faster feedback meant dealing with falkiness: for any failing test, developers first needed to find out whether it's a failure related to their change or "just" a flaky test. This can be quite a time sink…</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="switch">Switch<a href="https://bitcrowd.dev/cuprite#switch" class="hash-link" aria-label="Direct link to Switch" title="Direct link to Switch" translate="no">​</a></h4>
<p>People on the internet reported their feature tests being more reliable and less flaky when using Cuprite as a driver for Capybara compared to Selenium. So we were curious if this could improve our situation and went on a journey (together with Claude…) to switch the drivers and compare the speed and reliability to the Selenium setup.</p>
<p>We observed, that the tests did not run faster than with Selenium. But there was a huge difference when it came to reliability: we were able to remove all mechanisms in place for mitigating the falkiness with Selenium, from the retry logic up to the high wait time for Capybara.</p>
<p>The tests ran super stable, both locally and on CI 🎉</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="migrating-to-cuprite">Migrating to Cuprite<a href="https://bitcrowd.dev/cuprite#migrating-to-cuprite" class="hash-link" aria-label="Direct link to Migrating to Cuprite" title="Direct link to Migrating to Cuprite" translate="no">​</a></h2>
<p>Migrating our test suite required some small adjustments to the tests here and there where Selenium and Cuprite behave differently.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="external-urls">External URLs<a href="https://bitcrowd.dev/cuprite#external-urls" class="hash-link" aria-label="Direct link to External URLs" title="Direct link to External URLs" translate="no">​</a></h3>
<p>This may be quite specific, but Cuprite won't load external, non-existing URLs. Instead, it will show a Chrome error page. So one cannot match on the current path for those URLs like it's possible with Selenium.</p>
<div class="language-diff codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-diff codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token deleted-sign deleted prefix deleted" style="color:#d73a49">-</span><span class="token deleted-sign deleted line" style="color:#d73a49"> expect(page).to have_current_path("https://localhost/non-existing-path", url: true)</span><br></span><span class="token-line" style="color:#393A34"><span class="token deleted-sign deleted line" style="color:#d73a49"></span><span class="token inserted-sign inserted prefix inserted" style="color:#36acaa">+</span><span class="token inserted-sign inserted line" style="color:#36acaa"> expect_external_url("https://localhost/non-existing-path")</span><br></span></code></pre></div></div>
<p>What we ended up doing instead, was checking the details of the resulting error page using a helper:</p>
<div class="language-ruby codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ruby 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">def</span><span class="token plain"> </span><span class="token method-definition function" style="color:#d73a49">expect_external_url</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">url</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  expect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">page</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">current_url</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">to eq</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string-literal string" style="color:#e3116c">'chrome-error://chromewebdata/'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  expect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">page</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">to have_css</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string-literal string" style="color:#e3116c">'h1'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token symbol" style="color:#36acaa">text</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string-literal string" style="color:#e3116c">'This site can’t be reached'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  expect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">page</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">to have_button</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string-literal string" style="color:#e3116c">'Reload'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">|</span><span class="token plain">button</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">  expect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">button</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string-literal string" style="color:#e3116c">'data-url'</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">to eq</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">url</span><span class="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">end</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="form-values">Form values<a href="https://bitcrowd.dev/cuprite#form-values" class="hash-link" aria-label="Direct link to Form values" title="Direct link to Form values" translate="no">​</a></h3>
<p>For some form fields, the values extracted via Capybara where yielding a different type. A non-checked checkbox for instance:</p>
<div class="language-diff codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-diff codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token deleted-sign deleted prefix deleted" style="color:#d73a49">-</span><span class="token deleted-sign deleted line" style="color:#d73a49"> expect(find('.my-selector')[:checked]).to be_nil</span><br></span><span class="token-line" style="color:#393A34"><span class="token deleted-sign deleted line" style="color:#d73a49"></span><span class="token inserted-sign inserted prefix inserted" style="color:#36acaa">+</span><span class="token inserted-sign inserted line" style="color:#36acaa"> expect(find('.my-selector')[:checked]).to be(false)</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="matching-across-elements">Matching across elements<a href="https://bitcrowd.dev/cuprite#matching-across-elements" class="hash-link" aria-label="Direct link to Matching across elements" title="Direct link to Matching across elements" translate="no">​</a></h3>
<p>Again a quite specific issue: when matching the text across multiple elements, Cuprite is separating the elements via tabs instead of whitespace:</p>
<div class="language-diff codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-diff codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token deleted-sign deleted prefix deleted" style="color:#d73a49">-</span><span class="token deleted-sign deleted line" style="color:#d73a49"> expect(page).to have_content("foo bar")</span><br></span><span class="token-line" style="color:#393A34"><span class="token deleted-sign deleted line" style="color:#d73a49"></span><span class="token inserted-sign inserted prefix inserted" style="color:#36acaa">+</span><span class="token inserted-sign inserted line" style="color:#36acaa"> expect(page).to have_content("foo\tbar")</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://bitcrowd.dev/cuprite#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>Migrating an existing codebase from Selenium to Cuprite did not require much adaption of the tests itself, but it helped to stabilize the overall suite a lot. Alone for that it's definitely worth considering!</p>
<p>But if you have time, maybe it's also fine to wait for full BiDi support in Selenium.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="references">References<a href="https://bitcrowd.dev/cuprite#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References" translate="no">​</a></h2>
<!-- -->
<section data-footnotes="true" class="footnotes"><h2 class="anchor anchorTargetStickyNavbar_Vzrq sr-only" id="footnote-label">Footnotes<a href="https://bitcrowd.dev/cuprite#footnote-label" class="hash-link" aria-label="Direct link to Footnotes" title="Direct link to Footnotes" translate="no">​</a></h2>
<ol>
<li class="anchorTargetStickyNavbar_Vzrq" id="user-content-fn-1-72162c">
<p><a href="https://www.selenium.dev/documentation/webdriver/bidi/" target="_blank" rel="noopener noreferrer" class="">https://www.selenium.dev/documentation/webdriver/bidi/</a> <a href="https://bitcrowd.dev/cuprite#user-content-fnref-1-72162c" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>]]></content:encoded>
            <category>ruby-on-rails</category>
            <category>cuprite</category>
            <category>selenium</category>
            <category>testing</category>
            <category>rspec</category>
            <category>capybara</category>
        </item>
        <item>
            <title><![CDATA[Reading a 200k-line codebase you didn't write: a field guide]]></title>
            <link>https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide</link>
            <guid>https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide</guid>
            <pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Let's do an exercise in understanding a codebase like it's 1999, before the rise]]></description>
            <content:encoded><![CDATA[<p>Let's do an exercise in understanding a codebase like it's 1999, before the rise
of coding agents, and compare this with how coding agents to that work. This is
a field guide for reading inherited code alongside an agent.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="before-you-read-any-code">Before you read any code<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#before-you-read-any-code" class="hash-link" aria-label="Direct link to Before you read any code" title="Direct link to Before you read any code" translate="no">​</a></h2>
<p>The instinct is to start opening files. Don't. Read the shape of the project first. Four things, in order: the directory tree, the dependency manifest, the CI config, and the git log. Thirty minutes here saves days.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-directory-tree">The directory tree<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#the-directory-tree" class="hash-link" aria-label="Direct link to The directory tree" title="Direct link to The directory tree" translate="no">​</a></h3>
<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">tree -L 3 -I 'node_modules|.git|dist|build'</span><br></span></code></pre></div></div>
<p>Glance at the output - you're noticing absences and asymmetries. A <code>legacy/</code> directory, a missing <code>tests/</code> folder, three directories that look like they do the same thing. These are facts about the project's history before you read any logic.</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>How a coding agent would read the directory tree</summary><div><div class="collapsibleContent_i85q"><p>An agent asked "describe this repo" will summarize what's there. To get the interpretive layer, ask it the question you'd want a senior engineer to answer if they'd just glanced at the tree:</p><blockquote>
<p>What's notable about this directory structure - not what's there, but what's missing, asymmetric, or unusual compared to a healthy project in this stack? What might each observation imply about the team or the codebase's history?</p>
</blockquote><p><strong>Behind the scenes</strong>, the agent will list directories recursively (<code>LS</code> + <code>Glob</code>), read the top-level manifest (<code>package.json</code>, <code>Cargo.toml</code>, <code>mix.exs</code>, <code>pyproject.toml</code>) to identify the stack, and compare the layout against the conventional shape of a healthy project in that ecosystem. That shape comparison - what's missing versus what's expected - is the work you're outsourcing.</p><p><strong>What to look out for:</strong> The agent's hypotheses about history are speculation - useful as questions to verify, not as facts to build on.</p></div></div></details>
<p>Watch for: a <code>legacy/</code> or <code>old/</code> directory (migration started and abandoned?); empty or missing test directories; duplicate-purpose directories (<code>utils/</code>, <code>helpers/</code>, <code>lib/</code>); top-level files suggesting a rename or restructure.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-dependency-manifest">The dependency manifest<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#the-dependency-manifest" class="hash-link" aria-label="Direct link to The dependency manifest" title="Direct link to The dependency manifest" translate="no">​</a></h3>
<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">cat package.json</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># or pyproject.toml, Gemfile, pom.xml, go.mod</span><br></span></code></pre></div></div>
<p>Skim it. The framework version, anything forked, anything that doesn't belong with the rest of the stack - these are usually visible in under a minute and worth more than reading any individual source file.</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>How a coding agent would read the dependency manifest</summary><div><div class="collapsibleContent_i85q"><p>An agent will list dependencies if asked, but the list isn't the point. The point is the interpretation:</p><blockquote>
<p>For each dependency in this manifest, tell me: (1) whether the version is current, moderately outdated, or dangerously outdated; (2) any dependencies that look out of place given the rest of the stack; (3) any that suggest this codebase has been through a partial migration. Skip dependencies that are fine.</p>
</blockquote><p><strong>Behind the scenes</strong>, the agent will <code>Read</code> the manifest plus the lockfile, and for anything that looks stale it will hit npm / PyPI / Hex / crates.io via <code>WebFetch</code> to verify the actual latest version against its training cutoff. For forks pinned to a specific commit, it'll resolve the fork URL to see how far the fork has drifted from upstream. The judgement it can't make for you is whether a given dependency is <em>appropriate</em> for your business context - that part stays human.</p><p><strong>What to look out for:</strong> the agent's knowledge cutoff is often the bottleneck here - verify "current" claims against the actual latest version on npm or PyPI for anything flagged.</p></div></div></details>
<p>Watch for: forks pinned to a specific commit (almost always load-bearing - someone fixed something upstream wouldn't accept); a dependency that doesn't fit the stack (a queue library in a project that "doesn't use queues" is often the entry point to a subsystem you didn't know existed).</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-ci-config">The CI config<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#the-ci-config" class="hash-link" aria-label="Direct link to The CI config" title="Direct link to The CI config" translate="no">​</a></h3>
<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">ls .github/workflows/ .gitlab-ci.yml .circleci/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">cat .github/workflows/*.yml</span><br></span></code></pre></div></div>
<p>Read it once. You're looking for what this team tests, what they skip, and how often they deploy. The CI config is one of the few places in a codebase where the team's actual habits, rather than their intentions, are recorded.</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>How a coding agent would read the CI config</summary><div><div class="collapsibleContent_i85q"><p>An agent will summarize the pipeline accurately. The interpretive ask:</p><blockquote>
<p>What does this team test, what do they skip, and what does the test-to-deploy pattern suggest about their confidence in different parts of the codebase? Treat the absence of test coverage as information, not an oversight.</p>
</blockquote><p><strong>Behind the scenes</strong>, the agent will <code>Glob</code> for workflow files across <code>.github/workflows/</code>, <code>.gitlab-ci.yml</code>, <code>.circleci/config.yml</code>, <code>Jenkinsfile</code>, etc., <code>Read</code> each one, then <code>Grep</code> for <code>continue-on-error</code>, conditional <code>if:</code> expressions, and deploy-step names. It'll cross-reference the test commands it finds against the actual test directories to flag mismatches - a test suite that CI never invokes is one of the most common findings. Skipped categories show up cleanly; what they <em>mean</em> for your project is still your call.</p></div></div></details>
<p>Watch for: jobs marked <code>continue-on-error: true</code> or conditionally skipped - usually a flaky test the team gave up on. Deploy steps that bypass the test suite. Environment matrices that test against runtime versions that don't match production. The gap between what CI runs and what production runs is often where the surprises live.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-git-log">The git log<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#the-git-log" class="hash-link" aria-label="Direct link to The git log" title="Direct link to The git log" translate="no">​</a></h3>
<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 log --oneline | wc -l</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">git log --format="%an" | sort | uniq -c | sort -rn | head -20</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">git log --since="12 months ago" --format="%ad" --date=format:%Y-%m | sort | uniq -c</span><br></span></code></pre></div></div>
<p>Three small aggregations: total scale, who carried this, cadence over the last year. None of these require reading a single commit message.</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>How a coding agent would read the git log</summary><div><div class="collapsibleContent_i85q"><p>The agent's role here is to read the aggregations as a pattern:</p><blockquote>
<p>Here are aggregations from the git log of a legacy codebase. What does this suggest about its history? Who carried it, when did velocity change, and what's the current state of activity? Speculate where the data supports it.</p>
</blockquote><p><strong>Behind the scenes</strong>, the agent will run the aggregation commands above via its shell tool, plus a few more it thinks of (commits by directory, average diff size over time, longest gaps between commits per author). It can't see commits older than <code>git log</code> shows - if the repo was squashed or migrated from another VCS, the history is structurally incomplete and you should ask the agent to flag that. Suspiciously low commit counts in early years are usually where the real archaeology is buried.</p></div></div></details>
<p>Watch for: a single author with disproportionate commits who hasn't committed recently - that person was the system, and they're gone. A sharp cadence drop without a corresponding completion usually means the team got pulled to other work, not that they finished. Recent activity from authors who weren't historically involved often means the codebase was handed off to whoever was available.</p>
<p>The pattern across all four: small commands, fast interpretation, a short list of things the interpretation might miss. Thirty minutes and you have a model of the project as an artifact - its shape, its history, its current state - before you've opened any source code.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-four-entry-points">The four entry points<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#the-four-entry-points" class="hash-link" aria-label="Direct link to The four entry points" title="Direct link to The four entry points" translate="no">​</a></h2>
<p>Once you know the shape of the project, you start with its entry points. Every system has four kinds, and reading them in this order saves you from getting lost in the middle: the boundary, the data model, the configuration, the tests.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-boundary">The boundary<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#the-boundary" class="hash-link" aria-label="Direct link to The boundary" title="Direct link to The boundary" translate="no">​</a></h3>
<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"># pick whichever applies to your stack</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">grep -rE "(get|post|put|patch|delete)\s*['\"]" --include="*.rb" --include="*.ex"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">grep -rE "@(Get|Post|Put|Patch|Delete)Mapping" --include="*.java"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">rg "router\.(get|post|put|patch|delete)" -t js -t ts</span><br></span></code></pre></div></div>
<p>The boundary is where the outside world touches the system: HTTP routes, message queue consumers, scheduled jobs, CLI entry points, webhooks. You want the complete list, not a sample. The shape of the list - how many, how named, how distributed across modules - is the first real fact about what this system <em>is</em>.</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>How a coding agent would read the boundary list</summary><div><div class="collapsibleContent_i85q"><p>Agents are good at enumerating routes when asked, but a flat list isn't the deliverable. The deliverable is the structure of the list:</p><blockquote>
<p>Here's the complete list of HTTP routes for a legacy codebase. Group them by what they appear to do, flag any that look inconsistent with the rest (naming, verb usage, path structure), and tell me which groups suggest the system grew organically versus which look designed. Identify any routes that seem to do something unrelated to the others.</p>
</blockquote><p><strong>Behind the scenes</strong>, the agent will <code>Grep</code> for route definitions using framework-specific patterns (<code>get '/...'</code>, <code>@RequestMapping</code>, <code>router.post</code>, <code>live "/..."</code>), <code>Read</code> matching files for context, and where the framework supports it, shell out to the route-listing command (<code>rake routes</code>, <code>mix phx.routes</code>, <code>python manage.py show_urls</code>). Enumeration is mechanical. The second pass - grouping and pattern-matching across the full list - is the part you're actually asking it to do.</p></div></div></details>
<p>Eighty-seven routes with no consistent naming convention tells you the system grew without anyone owning it. Twelve clean REST routes tells you someone cared, at least once. A cluster of routes named <code>legacy_*</code> or <code>v1_*</code> next to a smaller modern cluster tells you a migration that didn't finish. The grouping matters more than any individual route.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-data-model">The data model<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#the-data-model" class="hash-link" aria-label="Direct link to The data model" title="Direct link to The data model" translate="no">​</a></h3>
<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">find . -name "schema.rb" -o -name "*.sql" -path "*/migrations/*"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">ls db/migrate/ priv/repo/migrations/ migrations/</span><br></span></code></pre></div></div>
<p>The data model is the system's memory of what it thinks is important, and it changes more slowly than code. That makes it the most reliable guide to what the system actually is - code can lie about intent, but a table either exists or it doesn't.</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>How a coding agent would read the schema and migrations</summary><div><div class="collapsibleContent_i85q"><blockquote>
<p>Here's the schema and migration history of a legacy codebase. Tell me: which tables are clearly the core of the domain; which look like they were added later or for one-off needs; any tables whose names suggest duplicate purpose (User vs Users vs Account); migrations that were partially applied or rolled back; columns marked deprecated, nullable-but-shouldn't-be, or with default values that look like workarounds.</p>
</blockquote><p><strong>Behind the scenes</strong>, the agent will <code>Glob</code> migration directories (<code>db/migrate/</code>, <code>priv/repo/migrations/</code>, <code>migrations/</code>), <code>Read</code> the schema file in order if there is one, and traverse migrations chronologically to reconstruct history. It'll <code>Grep</code> for <code>add_column</code>, <code>drop_table</code>, <code>change_column_null</code>, <code>add_foreign_key</code> to find places where a column's semantics changed mid-history. A capable agent will also cross-check foreign keys that exist in the schema but aren't reflected in the model code (or vice versa) - those gaps are the archaeological evidence.</p></div></div></details>
<p>Three tables called <code>user</code>, <code>users</code>, and <code>accounts</code> tells you a migration was abandoned. A column called <code>legacy_id</code> tells you a system this one replaced. Tables with no foreign keys to the rest of the schema are often integrations that were planned and never connected, or subsystems that were retired but not removed. The data model is where the system's archaeological layers are most visible.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-configuration">The configuration<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#the-configuration" class="hash-link" aria-label="Direct link to The configuration" title="Direct link to The configuration" translate="no">​</a></h3>
<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">find . -name "*.env*" -o -name "config*.{yml,yaml,json,toml}" -o -name "application.{yml,properties}"</span><br></span></code></pre></div></div>
<p>Configuration is where you find out what's actually turned on in production, which is often not what the code suggests. A feature flag at 100% for two years is a fact about the system; the disabled branch is a fossil. An environment variable that's set in production but undocumented anywhere else is load-bearing by definition.</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>How a coding agent would read the configuration surface</summary><div><div class="collapsibleContent_i85q"><blockquote>
<p>Here's the configuration surface of a legacy codebase (env vars referenced in code, config files, feature flag definitions). Tell me: which settings look like they control major behavioral switches; which look like they're permanently on or permanently off; which appear in code but not in any committed config (suggesting they're set in production only); any flags or settings whose names suggest dead or abandoned features.</p>
</blockquote><p><strong>Behind the scenes</strong>, the agent will <code>Grep</code> for every env-var read pattern that matches your stack (<code>ENV[</code>, <code>process.env.</code>, <code>System.get_env</code>, <code>os.environ</code>, <code>@Value("${...}")</code>), then cross-reference each hit against <code>Glob</code>'d config files (<code>.env*</code>, <code>config/*.yml</code>, <code>application.properties</code>, feature-flag definition files). The interesting output is the <em>diff</em>: variables read in code with no committed entry, or committed entries that nothing reads. That gap is what "set in production only" - and "configured but forgotten" - look like from the inside.</p></div></div></details>
<p>Watch for the gap between "config that exists in the repo" and "config that the code reads." Code that reads <code>ENV["LEGACY_PAYMENT_MODE"]</code> with no corresponding entry in any committed config file means someone, somewhere, has that variable set in production, and removing it will break something nobody can currently predict.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-tests">The tests<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#the-tests" class="hash-link" aria-label="Direct link to The tests" title="Direct link to The tests" translate="no">​</a></h3>
<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">find . -path "*/test/*" -o -path "*/spec/*" -o -path "*/__tests__/*" | head -50</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">find . -path "*/test/*" -type f | wc -l</span><br></span></code></pre></div></div>
<p>Tests aren't useful here for correctness - legacy tests are often wrong, stale, or skipped. They're useful for <em>intent</em>. A well-named test tells you what someone, at some point, thought the code was supposed to do. The gap between the test names and the current behavior is exactly the gap between the system's designed reality and its lived one.</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>How a coding agent would read the test inventory</summary><div><div class="collapsibleContent_i85q"><blockquote>
<p>Here are the test file names and test descriptions from a legacy codebase. Without reading the test bodies, tell me: which areas of the system are heavily tested versus barely tested; which test names suggest features that may no longer exist; any tests whose names contradict each other or describe overlapping responsibilities; areas where the test names suggest the team was unsure of expected behavior (lots of "should probably," "edge case," "fix for bug").</p>
</blockquote><p><strong>Behind the scenes</strong>, the agent will <code>Glob</code> for test files using your framework's convention, then <code>Grep</code> (or selectively <code>Read</code> with minimal scanning) just the <code>describe</code> / <code>it</code> / <code>test</code> / <code>it_behaves_like</code> strings without parsing the bodies. This is one of the few cases where you specifically <em>don't</em> want the agent to read the implementations - the labels are the closest thing to a behavioral spec the team ever wrote, and the bodies are noise at this stage.</p></div></div></details>
<p>Heavily tested modules are where the team was nervous. Barely tested modules are either trivial or terrifying - and you usually can't tell which without reading them. Test files for modules that no longer exist in the codebase are common and informative: someone removed the code but kept the test, or vice versa, and either way it's a story.</p>
<p>The pattern across the four: for each entry point, get the complete inventory, then ask for the shape of the inventory rather than its contents. The shape is the information; individual items are usually noise at this stage. You're still not reading code. You're building a map of what the code is <em>for</em>, which is the prerequisite for reading any of it usefully.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="reading-code-finally">Reading code, finally<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#reading-code-finally" class="hash-link" aria-label="Direct link to Reading code, finally" title="Direct link to Reading code, finally" translate="no">​</a></h2>
<p>By now you have a map: the project's shape, its boundaries, its data model, its configuration, its test surface. You haven't read any logic. That changes here, but not in the way most engineers do it. The mistake is to open the most interesting-looking file and start reading downward. The better move is to pick a thread and follow it, with the agent walking alongside.</p>
<p>A thread starts at a boundary. Pick one HTTP route, one queue consumer, one scheduled job. Just one. The goal isn't to understand the whole system - it's to understand <em>one path through it</em> deeply enough that you can predict what the second path will look like before you trace it.</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>How a coding agent would walk a single thread</summary><div><div class="collapsibleContent_i85q"><p>The opening prompt for a thread:</p><blockquote>
<p>I'm tracing a single request through this codebase to understand how the system works. The entry point is [paste the route handler or job definition]. Walk me through what this code does, step by step, following the call graph. When you hit a database operation, an external API call, or a piece of logic that seems unusual, stop and tell me. Don't summarize - narrate.</p>
</blockquote><p><strong>Behind the scenes</strong>, the agent will <code>Read</code> the entry file, <code>Grep</code> for each called function to locate its definition, <code>Read</code> that file, and recurse. Expect 10–30 <code>Read</code> calls per thread depending on depth, plus opportunistic <code>Grep</code>s when it encounters something polymorphic. When you ask it to "narrate, not summarize," you're explicitly trading tokens for resolution - forcing it to spend output describing each step instead of collapsing them. That's the trade you want, because the collapse is where understanding goes to die.</p><p><strong>What to look out for:</strong> "Narrate, don't summarize" is the key instruction. Summaries flatten the system. A narration preserves the <em>order</em> of operations, which is where the load-bearing weirdness lives. You want to know that the request validates input, hits cache, falls through to the database, applies a transformation that doesn't appear in the route handler, and then - and this is the part that matters - does something the agent's narration glosses over.</p><p>The glossed-over parts are the whole game. Agents narrating code do a specific thing: when they hit something they don't fully understand, they describe it at a higher level of abstraction. "This function handles the legacy payment flow" is what you get when the agent doesn't actually understand what the function does. A real narration would say "this function checks if <code>payment.legacy_mode</code> is true, and if so, calls <code>process_v1_payment</code> instead of the normal path, passing the original request object rather than the parsed one." If the agent gives you the first version, that's a signal - not that the agent is failing, but that there's something in that function worth looking at directly.</p><p><strong>The follow-up:</strong></p><blockquote>
<p>You said this function "handles the legacy payment flow." Show me the actual logic - what does it check, what does it call, and what's different from the non-legacy path?</p>
</blockquote><p><strong>Behind the scenes</strong>, the agent will <code>Read</code> the specific function you pointed at (it may not have been in context yet), <code>Grep</code> for related helpers and callees, and produce a line-by-line narration. If it still hedges after that, the function probably depends on runtime state or configuration the agent can't observe from source - feature flags, env vars, database content. That's your cue to either supply that context or accept that this branch needs the human eye on the live system.</p><p><strong>What to look out for:</strong> This is the move that separates reading with an agent from being read to by one. You're not accepting the abstraction the agent reached for. You're forcing it down a level, and you keep forcing it down until either the explanation becomes concrete or the agent's narration breaks down in a way that tells you to read the file yourself.</p></div></div></details>
<p>A few specific patterns worth watching for as you trace:</p>
<p><strong>Conditionals that look unremarkable but exist anyway.</strong> A line like <code>if user.created_at &lt; Date.new(2019, 4, 1)</code> is almost never trivial. Run <code>git blame</code> on it. The commit message is often the entire story: "fix INC-1247." That's an incident in 2019 that nobody on the current team remembers, and the conditional exists because removing it broke production once. The agent will explain the conditional accurately - "this checks if the user was created before April 2019" - without flagging that the <em>existence</em> of the check is the interesting part.</p>
<p><strong>Defensive code with no obvious threat.</strong> Try/catch around what looks like a pure function. Nil checks on values that are clearly never nil. Retries on calls that shouldn't fail. These are scars. The thing that caused the scar may or may not still exist, but the code is treating it as if it does.</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>How a coding agent would investigate defensive code</summary><div><div class="collapsibleContent_i85q"><p>Ask the agent:</p><blockquote>
<p>This function wraps [X] in a try/catch and falls back to [Y]. What scenarios would cause [X] to fail? Search the codebase for any test or comment that explains why this defensive code exists.</p>
</blockquote><p><strong>Behind the scenes</strong>, the agent will <code>Read</code> the function, <code>Grep</code> for tests that exercise the defensive path (including tests that mock [X] to throw), <code>Grep</code> for comments mentioning the function or its callees, and inspect <code>git blame</code> plus the commit history of the offending line. The answer is often in a 2019 commit message nobody remembers writing - and the agent is genuinely better at finding it than you are, because it can sweep the whole history, every nearby comment, and every related test in a single pass.</p><p><strong>What to look out for:</strong> The agent searching the codebase for context is a much better use of its capabilities than asking it to speculate. Often there's a comment three files away that explains everything.</p></div></div></details>
<p><strong>Dead-looking code that isn't dead.</strong> The function that looks unused. The module nobody imports. Before you assume it's safe to ignore, check.</p>
<p>A lot of "dead" legacy code is called by reflection, scheduled by a cron config nobody opened, or invoked by a feature flag that's set in production-only config.</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>How a coding agent would check whether code is really dead</summary><div><div class="collapsibleContent_i85q"><blockquote>
<p>Find all references to [function/module name] in this codebase, including: string-based references (reflection, dynamic dispatch, configuration files), references in test fixtures, references in commented-out code, and references in non-source files (YAML, JSON, migrations).</p>
</blockquote><p><strong>Behind the scenes</strong>, the agent will <code>Grep</code> not just for direct calls but also for stringified references (reflection, dynamic dispatch, polymorphic dispatch tables, config-driven invocation), references in YAML / JSON / cron files, and even references inside commented-out code or test fixtures. "Find every way this could be invoked, across heterogeneous file types" is the canonical case where the agent outperforms a human reading file by file - make it do the exhaustive sweep so you don't have to.</p><p><strong>What to look out for:</strong> The agent's reach across the whole repo is genuinely better than yours here - use it.</p></div></div></details>
<p>How many threads to trace before stopping: three to five. Not the whole boundary. By the third thread, you should be able to predict what the fourth will look like before you trace it - same patterns, same idioms, same kinds of weirdness in roughly the same places. When you can do that, you've absorbed the system's grammar, and continuing to trace is procrastination dressed as preparation.</p>
<p>A heuristic that's worth more than its size suggests: <strong>read the bug fixes before the features.</strong> A six-line commit that fixes a production issue teaches you more about a system's actual behavior than a six-hundred-line commit that adds a feature. Bug fixes are where the system's observed reality intrudes on its designed reality. Find the last twenty bug-fix commits - <code>git log --grep="fix\|bug\|incident" --oneline | head -20</code> - and read the diffs. You'll learn more in thirty minutes than you would in a day of reading feature code.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-artifacts-you-produce-while-reading">The artifacts you produce while reading<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#the-artifacts-you-produce-while-reading" class="hash-link" aria-label="Direct link to The artifacts you produce while reading" title="Direct link to The artifacts you produce while reading" translate="no">​</a></h2>
<p>Reading without writing is forgetting. By the end of your first few days you should have three artifacts on disk, all rough, all evolving. None of them are deliverables in the formal sense - they're tools for your own thinking that happen to be useful to the next person too.</p>
<p><strong>The map.</strong></p>
<p>A <a href="https://c4model.com/" target="_blank" rel="noopener noreferrer" class="">C4 model</a> is what you want: a structured way of describing a system at four zoom levels - system context, containers, components, code. For legacy work, the top two levels are where the value is. The system context diagram names what this system talks to in the world; the container diagram names the major deployable parts and how they communicate. Both fit on one screen.</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>How a coding agent would help bootstrap the map</summary><div><div class="collapsibleContent_i85q"><p>The agent can help you bootstrap it from what you've already gathered:</p><blockquote>
<p>Based on the boundaries, data model, and configuration we've discussed, sketch out a high-level architecture diagram of this system in text form. Show the major modules, their dependencies, and where data flows between them. Mark anything that's unclear or speculative with a question mark.</p>
</blockquote><p><strong>Behind the scenes</strong>, the agent has already accumulated most of the inputs - boundaries, data model, configuration - in its prior context. The new work here is synthesis, not retrieval, though it may <code>Read</code> a few more files to fill in module-to-module relationships it didn't trace earlier. The output is usually ASCII boxes-and-arrows or a Mermaid block. Resist the urge to ask for a "real" diagram in some specific tool - the value is in <em>your</em> redraw, not its render.</p><p><strong>What to look out for:</strong> Take its output and translate it into your own sketch - paper, whiteboard, Excalidraw, whatever. Don't accept the agent's diagram directly.</p></div></div></details>
<p>The act of coding the diagram is what builds the model in your head. <a href="https://structurizr.com/" target="_blank" rel="noopener noreferrer" class="">structurizr</a> is a great DSL for it.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-questions-list">The questions list<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#the-questions-list" class="hash-link" aria-label="Direct link to The questions list" title="Direct link to The questions list" translate="no">​</a></h3>
<p>Every "why does this exist," every "I don't understand this conditional," every "this can't be right" - write it down. Don't try to answer in the moment. The list itself is the deliverable for week one.</p>
<p>Half the questions will answer themselves by week two - you'll be reading something else and suddenly understand the thing that confused you on Monday. The half that don't are exactly what you need to ask the remaining humans about, in one focused conversation rather than forty Slack messages spread across two weeks.</p>
<p>Try to formulate your findings in <a href="https://en.wikipedia.org/wiki/Behavior-driven_development" target="_blank" rel="noopener noreferrer" class="">BDD stile syntax</a>. If you are able to write them down in that strict framework, you have a defined mental model of what is going on.</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>How a coding agent would triage your questions list</summary><div><div class="collapsibleContent_i85q"><p>The agent's role here is to triage, not to answer:</p><blockquote>
<p>Here's my running list of questions about this codebase. Group them by: questions that are likely answerable from the code itself (and where in the code to look); questions that require historical context only a human would know; questions that suggest something might be wrong rather than just unfamiliar.</p>
</blockquote><p><strong>Behind the scenes</strong>, the agent will <code>Grep</code> and selectively <code>Read</code> to verify which questions are answerable directly from source. The third bucket - potential bugs - is where it does its most distinctive work: cross-referencing each suspicious pattern against tests, comments, and similar idioms elsewhere in the codebase to decide whether the smell is real or just unfamiliar. That's a comparison task at repo scale, which is the thing agents are genuinely good at.</p><p><strong>What to look out for:</strong> The agent is decent at flagging which is which, because the third category usually has telltale shapes: defensive code with no defended-against condition, branches that can't be reached, types that don't line up.</p></div></div></details>
<p>The third category is the one to pay attention to. Questions that started as "I don't understand this" sometimes turn out to be "this might actually be a bug nobody's noticed."</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-glossary">The glossary<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#the-glossary" class="hash-link" aria-label="Direct link to The glossary" title="Direct link to The glossary" translate="no">​</a></h3>
<p>Every domain term, every internal name, every acronym. Legacy codebases are full of words used in non-obvious ways. "Account" and "User" and "Customer" mean three different things and the difference matters. "Order" might mean a purchase, or it might mean a sequence - depends on the module. Write them down as you encounter them, with where you saw them and what you currently think they mean. Update entries as you learn you were wrong.</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>How a coding agent would cross-reference glossary terms</summary><div><div class="collapsibleContent_i85q"><blockquote>
<p>Looking across this codebase, identify domain terms that appear to be used with different meanings in different modules. For each term, list the modules where it appears and how the usage differs. Flag any terms where the same word seems to refer to clearly different concepts.</p>
</blockquote><p><strong>Behind the scenes</strong>, the agent will <code>Grep</code> each candidate term across the codebase, <code>Read</code> the surrounding context for each hit, and cluster usages by module to detect semantic drift. This is one of the more token-expensive tasks on the list - for high-frequency terms like <code>Account</code> or <code>Order</code> you may want to scope the search to specific directories per pass. The output is most useful as input to a follow-up conversation with the team, not as the final glossary in itself.</p><p><strong>What to look out for:</strong> This is one of the highest-leverage things the agent can do for you, because it requires looking across the whole repo and noticing inconsistency - a comparison task at scale, which is what agents are genuinely good at.</p></div></div></details>
<p>The output is often surprising. A codebase where <code>Account</code> means three different things in three different modules is a codebase where every conversation between teams has been slightly broken for years, and nobody noticed because everyone was using the same word.</p>
<p>The glossary is the artifact that pays off latest but most. The map fades as you learn the system. The questions list shrinks as you answer them. The glossary keeps mattering forever, because every new person who joins the team needs it, and you're the only person who has just been through the experience of building it from scratch.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="when-to-stop">When to stop<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#when-to-stop" class="hash-link" aria-label="Direct link to When to stop" title="Direct link to When to stop" translate="no">​</a></h2>
<p>The trap at the other end is reading forever. Engineers who enjoy this work can spend weeks "understanding the codebase" and never ship anything. The agent makes this worse, not better - there's always one more module to ask about, one more thread to trace, one more cross-reference to run. Infinite patience on the agent's side meets infinite curiosity on yours and the result is a beautifully understood system you haven't actually done any work on.</p>
<p>You're done when you can answer three questions without going back to the agent or grepping:</p>
<ul>
<li class="">If a user reports a bug in feature X, where would I look first?</li>
<li class="">If I had to add a new field to entity Y, where are the four places I'd have to change?</li>
<li class="">If this system stopped working at 3am, what are the three things most likely to be wrong?</li>
</ul>
<p>If you can answer those, you have enough.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="circling-back-to-the-200k-loc---does-it-scale">Circling back to the 200k loc - Does it scale?<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#circling-back-to-the-200k-loc---does-it-scale" class="hash-link" aria-label="Direct link to Circling back to the 200k loc - Does it scale?" title="Direct link to Circling back to the 200k loc - Does it scale?" translate="no">​</a></h2>
<p>As we have seen, agents issue far more commands than a human comfortably would,
which means they often have more raw data to work from. They also don't tire
when documenting their findings. But they have to manage their context window,
and they often need humans to point out peculiarities or directions to pursue.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-works-at-scale">What works at scale:<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#what-works-at-scale" class="hash-link" aria-label="Direct link to What works at scale:" title="Direct link to What works at scale:" translate="no">​</a></h3>
<p>The inventory work scales fine. <code>grep -r</code> for routes returns more results but
you still get a complete list. <code>git log</code> aggregations don't care how big the
repo is. Glossary work scales — agents can chunk through the codebase
incrementally. The C4 map at the top two levels is explicitly designed to
compress; a system context diagram fits on one screen whether the codebase is
20k or 200k LOC.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="thread-tracing-is-where-agents-quietly-fail-at-scale">Thread-tracing is where agents quietly fail at scale.<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#thread-tracing-is-where-agents-quietly-fail-at-scale" class="hash-link" aria-label="Direct link to Thread-tracing is where agents quietly fail at scale." title="Direct link to Thread-tracing is where agents quietly fail at scale." translate="no">​</a></h3>
<p>In a 200k LOC system a single thread can touch fifteen to twenty files. After
six or seven file reads at typical legacy file sizes, context starts crowding.
Agents will follow a call graph six or seven levels deep without complaining —
and then start compressing its earlier observations to make room, losing exactly
the cross-file consistency that made the trace useful.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-to-make-that-scale">How to make that scale?<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#how-to-make-that-scale" class="hash-link" aria-label="Direct link to How to make that scale?" title="Direct link to How to make that scale?" translate="no">​</a></h3>
<p>Trace in segments. Three files at a time, summarize, clear context, continue.</p>
<p>You're now the one holding the through-line, not the agent.</p>
<p>The narration gets produced in pieces and you stitch them together. Less elegant
than an end-to-end trace but it's what actually works at 200k.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion---whats-the-best-strategy">Conclusion - What's the best strategy?<a href="https://bitcrowd.dev/reading-a-200k-line-codebase-you-did-not-write-a-field-guide#conclusion---whats-the-best-strategy" class="hash-link" aria-label="Direct link to Conclusion - What's the best strategy?" title="Direct link to Conclusion - What's the best strategy?" translate="no">​</a></h2>
<p>Reading legacy code well is one of the most undertaught skills in this field.
Engineers spend years learning to write code and approximately zero hours
learning to read it.</p>
<p>It might have become clear that there is some aspects where agents have the
advantage over humans. It's the <strong>stupelligent</strong> parts of the work where
they are most powerful. To facilitate their strengths, it makes sense to codify
the routines and standardize their output to a meta level humans and machines
understand alike.</p>
<p>This is why we started to work on <strong>Surveyor</strong>, which extracts specifications
from legacy systems and turns them into runnable BDD style tests. That's a
<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor" target="_blank" rel="noopener noreferrer" class="">different conversation</a>
if you're curious. There is an
<a href="https://73f4313d.sibforms.com/serve/MUIFAGf7xJuMm4W5YSsztruKlELH459zdPyB50IhmpgBnS6wyrTGqz7dQ55IJLSVa7CUPFvAHrUEvEYbHEn5tBivo8SQPN_cJIMdBr_O1xiQ9ug64k46sfNBIFuTHK1rNKqvqytuDkTUoh0C5XMyUKOPZy7whNQM8zviGihyCCrZYBTq7uTf5-35fK6Ki2YBOVnEAmV_WwrgoT1-wg==" target="_blank" rel="noopener noreferrer" class="">early access program</a> if you would
like to get engaged.</p>
<p>Three approaches to the same problem: by hand, with an agent, or with a
deterministic tool built for exactly this task. <strong>The reading skill matters
either way.</strong></p>]]></content:encoded>
            <category>legacy</category>
            <category>code review</category>
            <category>rewrite</category>
            <category>Surveyor</category>
        </item>
        <item>
            <title><![CDATA[From legacy code to verifiable specifications with Surveyor]]></title>
            <link>https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor</link>
            <guid>https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor</guid>
            <pubDate>Sun, 26 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Take ownership of code you don't trust. Surveyor extracts a verifiable behavioral specification of a legacy system. Rewrite with certainty.]]></description>
            <content:encoded><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-is-legacy-code">What is legacy code?<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#what-is-legacy-code" class="hash-link" aria-label="Direct link to What is legacy code?" title="Direct link to What is legacy code?" translate="no">​</a></h2>
<p>Sometimes that's the fifteen-year-old monolith.
Sometimes it's the service that grew complicated faster than the team could keep up with.
Sometimes it's that project that started as a shortcut and turned into a roadblock. Legacy code has been exposed to real world demands long enough to gather fixes for edge cases we are not aware of anymore.</p>
<p>And — increasingly — sometimes it's the repo your CEO vibe-coded last weekend and that they proudly present on Monday.
It runs. Now you have to own it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="legacy-software-is-the-code-we-lost-confidence-in">Legacy software is the code we lost confidence in<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#legacy-software-is-the-code-we-lost-confidence-in" class="hash-link" aria-label="Direct link to Legacy software is the code we lost confidence in" title="Direct link to Legacy software is the code we lost confidence in" translate="no">​</a></h2>
<p>That last category is genuinely new and growing fast. Code written once, never reviewed, tested a bit, deployed one day. Or, worse, released as a library and never maintained after that.</p>
<p>A new kind of technical dept has evolved: <strong><em>"The code we should review thoroughly one day</em></strong>."</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="can-we-rewrite-this">"Can we rewrite this?"<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#can-we-rewrite-this" class="hash-link" aria-label="Direct link to &quot;Can we rewrite this?&quot;" title="Direct link to &quot;Can we rewrite this?&quot;" translate="no">​</a></h2>
<p>The actual problem is not the writing of the new code, but the discovery of what the old code does, and the certainty that the new one does the same thing.</p>
<p>We've been building two small Elixir tools to help with both halves of that problem.</p>
<ul>
<li class=""><strong>Surveyor</strong> scans a codebase in any language and produces an architectural model.</li>
<li class=""><strong>Assay</strong> runs the same plain-text behavioral specs against both the legacy and the rewrite, so you can prove they behave the same.</li>
</ul>
<p>Together they cover the workflow: Figure out the structure, capture the behavior, rewrite, prove the rewrite works.</p>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>TL;DR</div><div class="admonitionContent_BuS1"><p>Surveyor produces an <strong>automatically verifiable behavioral specification</strong> of a legacy system.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-we-got-here">How we got here<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#how-we-got-here" class="hash-link" aria-label="Direct link to How we got here" title="Direct link to How we got here" translate="no">​</a></h2>
<p>Before going into either tool, it's worth walking through the shape of the problem.
The picture builds up step by step, and the final diagram only makes sense once you've seen the holes in the earlier ones.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-we-dont-want">What we don't want<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#what-we-dont-want" class="hash-link" aria-label="Direct link to What we don't want" title="Direct link to What we don't want" translate="no">​</a></h3>
<figure class="image-with-caption"><img decoding="async" loading="lazy" alt="Legacy Codebase with a dashed arrow pointing directly at a Target Codebase, with no intermediate steps" src="https://bitcrowd.dev/assets/images/step-0-158d6135059c381671508607696828b4.png" width="5564" height="1229" class="img_ev3q"><figcaption>A direct rewrite also copies bugs, dead code and unused features.</figcaption></figure>
<p>Point an agent at a legacy codebase, ask it to produce a new one, hope for the best.
Call it "the Claude zombie rewrite."
You end up with a target codebase that nobody understands either, plus a generation gap between the old assumptions and the new. What has the agent overlooked, misunderstood, assumed?
A black box gets replaced with a different black box.
That is not a rewrite, that is a transcoding.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-we-want-instead">What we want instead<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#what-we-want-instead" class="hash-link" aria-label="Direct link to What we want instead" title="Direct link to What we want instead" translate="no">​</a></h3>
<figure class="image-with-caption"><img decoding="async" loading="lazy" alt="Legacy Codebase with an arrow into a TXT file labelled &amp;quot;Readable Specification&amp;quot;." src="https://bitcrowd.dev/assets/images/step-1-b16e88b3f686956678092c1d93021641.png" width="5564" height="1229" class="img_ev3q"><figcaption>The features of the legacy codebase need to be revised</figcaption></figure>
<p>The valuable artifact in the middle is not new code — it's a
<strong>readable specification</strong> of what the old code <strong>actually does</strong>. Plain text,
human-reviewable, version-controllable. The kind of thing you need when you
want to ask the product owner if a feature is <strong>really</strong> wanted that way. - Or if it is just a legacy quirk.</p>
<p>If you have it, you can use it for estimates. It can be your map to plan the
implementation and might even give you a hint about that stakeholder that you
might have forgotten about otherwise.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="organised-and-illustrated">Organised and illustrated<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#organised-and-illustrated" class="hash-link" aria-label="Direct link to Organised and illustrated" title="Direct link to Organised and illustrated" translate="no">​</a></h3>
<figure class="image-with-caption"><img decoding="async" loading="lazy" alt="Legacy Codebase pointing into an Architecture Map of Module A, Module B, Module C, each with its own .md spec." src="https://bitcrowd.dev/assets/images/step-2-9f42ae2f41b319470f65ad9ec5928524.png" width="5087" height="1761" class="img_ev3q"><figcaption>The specification's organisation should mirror the codebase</figcaption></figure>
<p>A single big text file describing a whole legacy system is only useful for very small projects. You want the spec broken
down by module — by bounded context — and the modules themselves arranged on a map of the architecture. Once each module
has its own spec associated to it, two things become possible: you can divide the work, and you can reason about
coverage.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="job-done-or-is-it">Job done! Or is it?<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#job-done-or-is-it" class="hash-link" aria-label="Direct link to Job done! Or is it?" title="Direct link to Job done! Or is it?" translate="no">​</a></h3>
<p>We now have and organised map of the system and a set of specifications. We have an overview, and the details. We could
now talk to the stakeholders, the development team, and set to work.</p>
<figure class="image-with-caption"><img decoding="async" loading="lazy" alt="A graphic showing rectangles representing the software parts map to rectangles representing the specification" src="https://bitcrowd.dev/assets/images/step-3-f8cc173a463a67d9b7f04b22acfa704a.png" width="4885" height="2004" class="img_ev3q"><figcaption>The specification's organisation should mirror the codebase</figcaption></figure>
<p><strong>However, we have not yet verified our findings.</strong></p>
<p>What we are looking at is not yet a specification of the legacy system — it is a <em>speculation</em> of the legacy system.
A document that <strong>claims</strong> to describe what the code does, written by reading the code (or by asking an LLM to read the code).
Plausible, well-organised, reviewable. But unverified.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-verifiability">1. Verifiability<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#1-verifiability" class="hash-link" aria-label="Direct link to 1. Verifiability" title="Direct link to 1. Verifiability" translate="no">​</a></h4>
<p>This is <strong>speculation as specification</strong>, and it's the trap most "let's document the legacy system" projects fall into.
A Word document of "what the system does" can be worse than no document at all, because people start trusting it.
But even if the specification is done as a group effort by all stakeholders involved, and is thoroughly verified, it
still lacks one critical property:</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-reproducibility">2. Reproducibility<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#2-reproducibility" class="hash-link" aria-label="Direct link to 2. Reproducibility" title="Direct link to 2. Reproducibility" translate="no">​</a></h4>
<p>What we actually want is a <strong>verifiable specification</strong>: Not "verified once," but verifiable on demand, any time someone
asks the question. You could verify a paper spec by hand, of course — sit down, read it, run the system, tick off the
scenarios — but manual verification is expensive enough that it gets done once at sign-off and never again.</p>
<p>A verification you don't actually do is verification that doesn't exist. And unless the legacy codebase is a museum
exhibit, it's a moving target:</p>
<p>The team is still shipping fixes, the system is still drifting, and a paper spec written on Monday is no longer accurate
by Friday.</p>
<p><img decoding="async" loading="lazy" alt="Same picture but with a red horse galloping across the bottom and the words &amp;quot;BUT SPECS GO STALE&amp;quot;. The Legacy Codebase is now annotated as &amp;quot;A moving target&amp;quot;." src="https://bitcrowd.dev/assets/images/step-6-ff8b98aeca5f9fdf5732fe355a44fbc0.png" width="5186" height="3391" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="runnable-specs">Runnable specs<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#runnable-specs" class="hash-link" aria-label="Direct link to Runnable specs" title="Direct link to Runnable specs" translate="no">​</a></h3>
<p>The fix is to make the specs <strong>executable</strong> — to wire them into something that can run them against the actual legacy system, on demand, and tell you whether they still hold.</p>
<p><img decoding="async" loading="lazy" alt="The earlier picture with a new &amp;quot;SOME MAGIC&amp;quot; box added below, with arrows looping the .assay specs back through the magic box and into the legacy codebase." src="https://bitcrowd.dev/assets/images/step-7-444bd86f1323e81fa52511f1d57830f1.png" width="5186" height="4943" class="img_ev3q"></p>
<p>That is the move from speculation to verifiable specification: not a one-shot audit, but a button you can press on every commit, every nightly, every time anyone asks "is this still true?"</p>
<p>A <strong>green</strong> run proves the spec is still true. A <strong>red</strong> run is a useful signal: either the legacy drifted, or your spec was wrong.
Either way, you find out before the rewrite, not during it.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="that-magic-is-assay">That magic is Assay<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#that-magic-is-assay" class="hash-link" aria-label="Direct link to That magic is Assay" title="Direct link to That magic is Assay" translate="no">​</a></h3>
<p><img decoding="async" loading="lazy" alt="The bottom box is now labelled &amp;quot;Assay — A framework to automate assertions&amp;quot; with a Parser that reads .assay files and a &amp;quot;(The parts you fill in)&amp;quot; placeholder beneath it." src="https://bitcrowd.dev/assets/images/step-8-c06ddef9a1740cfb355acb10dace6184.png" width="5176" height="4943" class="img_ev3q"></p>
<p>Assay is the runner.
It parses the <code>.assay</code> specs Surveyor produced, matches each step against bindings you write, and exercises the real legacy system through whatever interface it actually exposes — HTTP, CLI, message queue, file drop, whatever.
The framework is small.</p>
<p>The runtime contract is "your bindings + your assertions + a deterministic runner."</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-systemic-approach">The systemic approach<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#the-systemic-approach" class="hash-link" aria-label="Direct link to The systemic approach" title="Direct link to The systemic approach" translate="no">​</a></h3>
<p><img decoding="async" loading="lazy" alt="The full picture: Surveyor on top with Architecture Map and .assay specs; Assay below with Parser, Pattern Recognition, Bindings, and Given/When/Then automations; arrows looping back to the Legacy Codebase." src="https://bitcrowd.dev/assets/images/overview-110149d565e655cbe254faa9456037a1.png" width="4320" height="4178" class="img_ev3q"></p>
<p>Open up that "parts you fill in" box and you find the actual surface area of work: pattern recognition for step matching, a binding per target system, and Given/When/Then automations for setup, action, and verification.</p>
<p>The end state has a property that nothing in the earlier pictures had: the problem space has been <strong>dissected into manageable pieces</strong>.
Architecture, behavior, target adapters, assertions — each is small, each can be reviewed independently, and each can be picked up by a human or an agent without having to hold the whole system in their head at once.</p>
<p>That's the goal of the toolkit.
Everything below is the detail of how each piece is built.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="surveyor--discovering-the-architecture-you-inherited">Surveyor — discovering the architecture you inherited<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#surveyor--discovering-the-architecture-you-inherited" class="hash-link" aria-label="Direct link to Surveyor — discovering the architecture you inherited" title="Direct link to Surveyor — discovering the architecture you inherited" translate="no">​</a></h2>
<p>Surveyor is a Mix project that takes a path to a codebase and produces a <a href="https://structurizr.com/" target="_blank" rel="noopener noreferrer" class="">Structurizr</a> DSL workspace file.
It runs in three phases mapping to the C4 model: C1 (system context), C2 (containers), C3 (components per container).</p>
<p>Crucially, Surveyor does <strong>not</strong> parse code.
There are no language-specific parsers, no AST walkers, no fragile grammar files for thirty different ecosystems.</p>
<p>It scans the filesystem to identify what's there — languages, frameworks, project files, entry points, deployment manifests — and then feeds meaningful chunks to an LLM. That sounds trivial, but is the same mechanism that commercial coding agents use when
they analyse your code.</p>
<p>Surveyor's intelligence is in the prompts and the chunking, not in a stack of half-broken language adapters.</p>
<p>The CLI is interactive at every phase.</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">$ surveyor ./legacy-monolith </span><span class="token parameter variable" style="color:#36acaa">--phase</span><span class="token plain"> c1</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">Phase </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"> — System Context</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  Scanning codebase</span><span class="token punctuation" style="color:#393A34">..</span><span class="token plain">.</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  Querying 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" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  System: </span><span class="token string" style="color:#e3116c">"Order Management System"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  Description: Manages the full order lifecycle</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">  Actors:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ✓ Customer — places and tracks orders via Web UI</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ✓ Warehouse Staff — manages fulfillment via Back Office</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ? Admin — found references </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> auth config </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">confidence: low</span><span class="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">  External Systems:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ✓ Stripe — payment processing </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">REST API</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ? SendGrid — found API key </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> config </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">confidence: low</span><span class="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 punctuation" style="color:#393A34">[</span><span class="token plain">a</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain">ccept  </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">dit  </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 plain">etry with </span><span class="token function" style="color:#d73a49">more</span><span class="token plain"> context  </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">q</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain">uit</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token operator" style="color:#393A34">&gt;</span><br></span></code></pre></div></div>
<p>The LLM is asked to flag uncertainty.
Anything tagged <code>confidence: low</code> shows up with a <code>?</code> and a short reasoning string for human review.
That's not just nice ergonomics — it's the bit that makes the tool trustworthy.
An LLM that confidently invents a <code>PaymentReconciliation</code> container that doesn't exist is worse than no model at all.
An LLM that says "I see a <code>SENDGRID_API_KEY</code> in <code>.env.example</code> but no Sendgrid client in the source, please confirm" is doing the right kind of work.</p>
<p>Each phase is resumable.
Results are saved as JSON in <code>./surveyor/</code>, so a thirty-container system doesn't have to finish in one sitting.
You can hop in, accept C1, take a break, come back tomorrow, and resume at C2.</p>
<p>The end product is a <code>workspace.dsl</code> you can render in Structurizr.
But more importantly, it's a <code>workspace.dsl</code> whose components are decorated with <code>assay.specs</code> and <code>assay.schema</code> properties, pointing at the behavioral specs and the domain schemas for each bounded context.</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">orderLifecycle = component "Order Lifecycle" "Orders" "Bounded Context" {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    properties {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        "assay.specs" "specs/order-lifecycle"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        "assay.schema" "schemas/order_lifecycle.ex"</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">}</span><br></span></code></pre></div></div>
<p>DDD pattern annotations from the LLM (<code>customer-supplier</code>, <code>anticorruption-layer</code>, …) ride along as properties on the relationship.
Every component gets <code>assay.specs</code> and <code>assay.schema</code> properties — the contract the next tool reads from.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="examples">Examples<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#examples" class="hash-link" aria-label="Direct link to Examples" title="Direct link to Examples" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-system-context-of-a-medical-application">The System Context of a Medical Application<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#the-system-context-of-a-medical-application" class="hash-link" aria-label="Direct link to The System Context of a Medical Application" title="Direct link to The System Context of a Medical Application" translate="no">​</a></h4>
<p>Surveyor identified the actors and the external systems of the application. Without knowing the code, we would already
know which actor types there are</p>
<figure class="image-with-caption"><img decoding="async" loading="lazy" src="https://bitcrowd.dev/assets/images/MeditechsystemContext-b7cb1953223f982d281b4b7110677683.png" width="2380" height="1716" class="img_ev3q"><figcaption>The System Context of a medical application</figcaption></figure>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="an-example-of-an-assay-spec-created-in-the-second-phase">An example of an assay spec created in the second phase:<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#an-example-of-an-assay-spec-created-in-the-second-phase" class="hash-link" aria-label="Direct link to An example of an assay spec created in the second phase:" title="Direct link to An example of an assay spec created in the second phase:" translate="no">​</a></h4>
<figure class="image-with-caption"><img decoding="async" loading="lazy" src="https://bitcrowd.dev/assets/images/MeditechAssay-c4-46cf27120239ca00a3e2a7202cf32936.png" width="1344" height="1410" class="img_ev3q"><figcaption>The Account Creation Specs of a medical application</figcaption></figure>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="assay--proving-the-rewrite-behaves-the-same">Assay — proving the rewrite behaves the same<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#assay--proving-the-rewrite-behaves-the-same" class="hash-link" aria-label="Direct link to Assay — proving the rewrite behaves the same" title="Direct link to Assay — proving the rewrite behaves the same" translate="no">​</a></h2>
<p>Assay is a minimal behavioral spec runner.
Specs are plain text and look like this:</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">Component: [orderLifecycle] Order Lifecycle</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Context: Order Placement</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">Definitions:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  - "a valid customer" means:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      a Customer with status Active, verified email,</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      and a credit limit greater than zero</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">Invariants:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  - total must not exceed customer credit limit</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  - at least one line item required</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">Rule: Customers can place orders for in-stock items</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">  @critical @phase-1</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  Scenario: [OL-001] Place a simple order</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Given a valid customer "Alice" with credit limit €10,000</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    And product "Widget" is in stock with 50 units available</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    When Alice places an order for 3 units of "Widget"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Then the order status becomes "Placed"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    And stock for "Widget" is reduced to 47 units</span><br></span></code></pre></div></div>
<p>The bracketed <code>[orderLifecycle]</code> is a workspace.dsl identifier — the same identifier Surveyor wrote into the architecture model.
The bracketed <code>[OL-001]</code> on the scenario is an optional free-form id that survives across rewrites of the spec text and lets you cross-reference from tickets or audit trails.</p>
<p>If that looks like Cucumber or Gherkin, it does — with one large difference.
There is no glue layer, no abstraction over what kind of system you are testing, no framework opinions about HTTP, databases, or browsers.
The bindings are just Elixir.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">defmodule</span><span class="token plain"> </span><span class="token module class-name">Targets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Legacy</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">OrderLifecycle</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</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">use</span><span class="token plain"> </span><span class="token module class-name">Assay</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Binding</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">component:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"orderLifecycle"</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 attribute variable" style="color:#36acaa">@base</span><span class="token plain"> </span><span class="token module class-name">System</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get_env</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"LEGACY_API_URL"</span><span class="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">  step </span><span class="token atom symbol" style="color:#36acaa">:given</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token regex" style="color:#36acaa">~r/a valid customer "(?P&lt;name&gt;.+)" with credit limit €(?P&lt;limit&gt;[\d,.]+)/</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</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 atom symbol" style="color:#36acaa">:ok</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> resp</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 module class-name">Req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">post</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"</span><span class="token string interpolation delimiter punctuation" style="color:#393A34">#{</span><span class="token string interpolation attribute variable" style="color:#36acaa">@base</span><span class="token string interpolation delimiter punctuation" style="color:#393A34">}</span><span class="token string" style="color:#e3116c">/test/customers"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">json:</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token attr-name" style="color:#00a4db">name:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">params</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">name</span><span class="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 attr-name" style="color:#00a4db">credit_limit:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">parse_money</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">params</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">limit</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 attr-name" style="color:#00a4db">status:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Active"</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 function" style="color:#d73a49">assign</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attr-name" style="color:#00a4db">customer_id:</span><span class="token plain"> resp</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 string" style="color:#e3116c">"id"</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">end</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">  step </span><span class="token atom symbol" style="color:#36acaa">:action</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token regex" style="color:#36acaa">~r/.+ places an order for (?P&lt;qty&gt;\d+) units? of "(?P&lt;product&gt;.+)"/</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</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 atom symbol" style="color:#36acaa">:ok</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> resp</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 module class-name">Req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">post</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"</span><span class="token string interpolation delimiter punctuation" style="color:#393A34">#{</span><span class="token string interpolation attribute variable" style="color:#36acaa">@base</span><span class="token string interpolation delimiter punctuation" style="color:#393A34">}</span><span class="token string" style="color:#e3116c">/orders"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">json:</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token attr-name" style="color:#00a4db">customer_id:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">var</span><span class="token punctuation" style="color:#393A34">(</span><span class="token atom symbol" style="color:#36acaa">:customer_id</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 attr-name" style="color:#00a4db">lines:</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 attr-name" style="color:#00a4db">product_id:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">var</span><span class="token punctuation" style="color:#393A34">(</span><span class="token atom symbol" style="color:#36acaa">:product_id</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 attr-name" style="color:#00a4db">quantity:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">to_int</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">params</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">qty</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 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">assign</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attr-name" style="color:#00a4db">order_id:</span><span class="token plain"> resp</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 string" style="color:#e3116c">"id"</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 attr-name" style="color:#00a4db">http_status:</span><span class="token plain"> resp</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">status</span><span class="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">end</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">  step </span><span class="token atom symbol" style="color:#36acaa">:expect</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token regex" style="color:#36acaa">~r/the order status becomes "(?P&lt;status&gt;.+)"/</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</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 atom symbol" style="color:#36acaa">:ok</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> resp</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 module class-name">Req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"</span><span class="token string interpolation delimiter punctuation" style="color:#393A34">#{</span><span class="token string interpolation attribute variable" style="color:#36acaa">@base</span><span class="token string interpolation delimiter punctuation" style="color:#393A34">}</span><span class="token string" style="color:#e3116c">/orders/</span><span class="token string interpolation delimiter punctuation" style="color:#393A34">#{</span><span class="token string interpolation function" style="color:#d73a49">var</span><span class="token string interpolation punctuation" style="color:#393A34">(</span><span class="token string interpolation atom symbol" style="color:#36acaa">:order_id</span><span class="token string interpolation punctuation" style="color:#393A34">)</span><span class="token string interpolation delimiter punctuation" style="color:#393A34">}</span><span class="token string" style="color:#e3116c">"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    assert resp</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 string" style="color:#e3116c">"status"</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">params</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">status</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">end</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">end</span><br></span></code></pre></div></div>
<p>A binding is just a module that brings whatever it needs — <code>Req</code> for an HTTP API, <code>AMQP</code> for a message broker, <code>System.cmd</code> for a batch processor, <code>File</code> for a directory-watching pipeline.
Assay itself does not ship an HTTP client or a database adapter.</p>
<p>The framework provides parsing, step matching, scenario lifecycle, and assertions; the rest is your code.
That's what makes the same spec runnable against an HTTP API, a CLI tool, a batch job, or a message queue, depending on what the legacy system happens to be.</p>
<div class="cta"><div class="container"><div class="row"><p>
🚀 Would you like to run this on your legacy codebase? Sign up for early access! <!-- --> <a href="https://73f4313d.sibforms.com/serve/MUIFAGf7xJuMm4W5YSsztruKlELH459zdPyB50IhmpgBnS6wyrTGqz7dQ55IJLSVa7CUPFvAHrUEvEYbHEn5tBivo8SQPN_cJIMdBr_O1xiQ9ug64k46sfNBIFuTHK1rNKqvqytuDkTUoh0C5XMyUKOPZy7whNQM8zviGihyCCrZYBTq7uTf5-35fK6Ki2YBOVnEAmV_WwrgoT1-wg==" class="plausible-event-name=Button+Click plausible-event-location=Post+Cta+surveyor">Count me in!</a></p></div></div></div>
<p>The crucial property is target swappability:</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">assay run specs/order-lifecycle/ </span><span class="token parameter variable" style="color:#36acaa">--target</span><span class="token plain"> legacy</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">assay run specs/order-lifecycle/ </span><span class="token parameter variable" style="color:#36acaa">--target</span><span class="token plain"> new</span><br></span></code></pre></div></div>
<p>Same specs. Different bindings. Different systems.
The legacy binding hits the old SOAP API; the new binding hits the new Phoenix endpoint.
If both go green, the rewrite is behavior-equivalent for the things the spec covers — which, after a few months of writing specs, is most of the things that matter.</p>
<p>There is no separate <code>.exs</code> file generated from the specs.
The runner parses <code>.assay</code> files, matches each step against a regex on a binding function, executes them, and prints pass/fail.
That's it.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-assay-actually-works">How Assay actually works<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#how-assay-actually-works" class="hash-link" aria-label="Direct link to How Assay actually works" title="Direct link to How Assay actually works" translate="no">​</a></h3>
<p>The runtime is around six hundred lines of Elixir.
Five design choices do most of the work.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-two-pass-parser">A two-pass parser<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#a-two-pass-parser" class="hash-link" aria-label="Direct link to A two-pass parser" title="Direct link to A two-pass parser" translate="no">​</a></h4>
<p>The first pass lifts triple-quoted doc strings out of the file (so the line tokenizer doesn't have to reason about them).
The second pass dispatches on the first keyword on each line.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">parse_string</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">content</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</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">filtered</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> doc_strings</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    content</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token operator" style="color:#393A34">|&gt;</span><span class="token plain"> </span><span class="token module class-name">String</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">split</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"\n"</span><span class="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">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">with_index</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token operator" style="color:#393A34">|&gt;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">extract_doc_strings</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 module class-name">Process</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">put</span><span class="token punctuation" style="color:#393A34">(</span><span class="token atom symbol" style="color:#36acaa">:assay_doc_strings</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> doc_strings</span><span class="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">  filtered</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token operator" style="color:#393A34">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">reject</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">fn</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">line</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><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">    trimmed </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">String</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">trim</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">line</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    trimmed </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">""</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">or</span><span class="token plain"> </span><span class="token module class-name">String</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">starts_with?</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">trimmed</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"#"</span><span class="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">end</span><span class="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">|&gt;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">parse_lines</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">%</span><span class="token module class-name">Spec</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">end</span><br></span></code></pre></div></div>
<p>Output is a plain <code>%Spec{}</code> struct. No AST nodes, no string interpolation.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-step-macro-generates-real-module-functions">The step macro generates real module functions<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#the-step-macro-generates-real-module-functions" class="hash-link" aria-label="Direct link to The step macro generates real module functions" title="Direct link to The step macro generates real module functions" translate="no">​</a></h4>
<p>Each <code>step :given, ~r/.../ do ... end</code> call expands into a uniquely-named function plus a <code>%StepBinding{}</code> record on an accumulating attribute.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">defmacro</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">step</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">type</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> regex</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">do:</span><span class="token plain"> block</span><span class="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">when</span><span class="token plain"> type </span><span class="token operator" style="color:#393A34">in</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token atom symbol" style="color:#36acaa">:given</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:action</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:expect</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  fn_name </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> :</span><span class="token string" style="color:#e3116c">"__step_</span><span class="token string interpolation delimiter punctuation" style="color:#393A34">#{</span><span class="token string interpolation atom symbol" style="color:#36acaa">:erlang</span><span class="token string interpolation punctuation" style="color:#393A34">.</span><span class="token string interpolation function" style="color:#d73a49">unique_integer</span><span class="token string interpolation punctuation" style="color:#393A34">(</span><span class="token string interpolation punctuation" style="color:#393A34">[</span><span class="token string interpolation atom symbol" style="color:#36acaa">:positive</span><span class="token string interpolation punctuation" style="color:#393A34">]</span><span class="token string interpolation punctuation" style="color:#393A34">)</span><span class="token string interpolation delimiter punctuation" style="color:#393A34">}</span><span class="token string" style="color:#e3116c">__"</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">quote</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token attribute variable" style="color:#36acaa">@assay_steps</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token function" style="color:#d73a49">unquote</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">type</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">unquote</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">regex</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">&amp;</span><span class="token plain">__MODULE__</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">unquote</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">fn_name</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" 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><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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">unquote</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">fn_name</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 keyword" style="color:#00009f">do</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">unquote</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">block</span><span class="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">end</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">end</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">end</span><br></span></code></pre></div></div>
<p>At compile time, <code>__before_compile__</code> exposes <code>__steps__/0</code> and <code>__component__/0</code> for the runner to read.
There is no string codegen, no eval — just normal Elixir function definitions.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="component-scoped-step-matching">Component-scoped step matching<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#component-scoped-step-matching" class="hash-link" aria-label="Direct link to Component-scoped step matching" title="Direct link to Component-scoped step matching" translate="no">​</a></h4>
<p>Anyone who has used Cucumber has hit the global step-definition problem: the same step text means different things in different contexts, but Gherkin treats step definitions as one shared namespace.
Assay scopes bindings by component:</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">defp</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">run_spec</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">spec</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> bindings</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> target</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> tags</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> exclude_tags</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  component_bindings </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 module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">filter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">bindings</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">fn</span><span class="token plain"> b </span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain"> b</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">component </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> spec</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">component </span><span class="token keyword" style="color:#00009f">end</span><span class="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"># ... run each scenario against component_bindings only</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">end</span><br></span></code></pre></div></div>
<p>This is the direct reason Surveyor and Assay use the same identifier convention.
The architecture model and the spec runner share a vocabulary, and step text never collides across bounded contexts.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="pre-check-then-execute">Pre-check, then execute<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#pre-check-then-execute" class="hash-link" aria-label="Direct link to Pre-check, then execute" title="Direct link to Pre-check, then execute" translate="no">​</a></h4>
<p>Before running any step, the runner walks the whole scenario and finds a binding for every step.
If anything is unbound, the scenario fails before any side effects.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">run_scenario</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">scenario</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> bindings</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  matched_steps </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 module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">scenario</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">steps</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">fn</span><span class="token plain"> step </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">case</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">find_binding</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">step</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> bindings</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token boolean" style="color:#36acaa">nil</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token atom symbol" style="color:#36acaa">:unbound</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> step</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        binding </span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token atom symbol" style="color:#36acaa">:bound</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> step</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> binding</span><span class="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">end</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">end</span><span class="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 module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">any?</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">matched_steps</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;</span><span class="token function" style="color:#d73a49">match?</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token atom symbol" style="color:#36acaa">:unbound</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 plain"> </span><span class="token argument variable" style="color:#36acaa">&amp;1</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">do</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 module class-name">ScenarioResult</span><span class="token punctuation" style="color:#393A34">{</span><span class="token attr-name" style="color:#00a4db">status:</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:error</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">error:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Unbound steps found"</span><span class="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">else</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">execute_matched_steps</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">scenario</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> matched_steps</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> bindings</span><span class="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">end</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">end</span><br></span></code></pre></div></div>
<p>This is what stops a scenario from running half its Givens, hitting an unbound When, and leaving a dangling test customer in the database.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="per-scenario-state-in-the-process-dictionary">Per-scenario state in the process dictionary<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#per-scenario-state-in-the-process-dictionary" class="hash-link" aria-label="Direct link to Per-scenario state in the process dictionary" title="Direct link to Per-scenario state in the process dictionary" translate="no">​</a></h4>
<p>Each scenario gets a fresh context — a tiny module that stores variables, params, doc strings, and data tables in the process dictionary.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">def</span><span class="token plain"> init </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token module class-name">Process</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">put</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attribute variable" style="color:#36acaa">@vars_key</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 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 module class-name">Process</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">put</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attribute variable" style="color:#36acaa">@params_key</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 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">end</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">get_var</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">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token module class-name">Process</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attribute variable" style="color:#36acaa">@vars_key</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 punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Map</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">end</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">set_vars</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">keyword</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  vars </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Process</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attribute variable" style="color:#36acaa">@vars_key</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 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 module class-name">Process</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">put</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attribute variable" style="color:#36acaa">@vars_key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">into</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">keyword</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> vars</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">end</span><br></span></code></pre></div></div>
<p>The runner can <code>save/0</code> and <code>restore/1</code> the snapshot, which is how Assay tests itself by running its own <code>.assay</code> specs through itself in the same process.</p>
<p>There is no plugin system, no dependency injection, no scenario hooks beyond <code>cleanup</code>.
Adding any of those would push complexity into the framework and out of the binding, which is the wrong direction.
The framework exists to be small enough that you can read it on a Friday afternoon and trust it on Monday.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-they-fit-together">How they fit together<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#how-they-fit-together" class="hash-link" aria-label="Direct link to How they fit together" title="Direct link to How they fit together" translate="no">​</a></h2>
<p>The flow is straightforward and it maps onto the five phases laid out in the Assay handbook.</p>
<ol>
<li class=""><strong>Discovery (Surveyor).</strong> Run Surveyor against the legacy codebase. Walk through the C1/C2/C3 output interactively. Edit, retry, accept. Produces <code>workspace.dsl</code>.</li>
<li class=""><strong>Behavioral extraction.</strong> For each component in the model, write <code>.assay</code> specs and an Elixir schema. The <code>assay.specs</code> and <code>assay.schema</code> properties on every Structurizr component tell you exactly where they go: <code>specs/&lt;context&gt;/</code> and <code>schemas/&lt;context&gt;.ex</code>.</li>
<li class=""><strong>Validation.</strong> Write bindings against the legacy system in <code>targets/legacy/</code>. Run <code>assay run specs/ --target legacy</code>. Iterate until green. You now have a verified, executable specification of the legacy system's behavior.</li>
<li class=""><strong>Stabilization.</strong> Review what's covered, what's missing, what's flagged as ambiguous. Get sign-off on scope.</li>
<li class=""><strong>Rewrite.</strong> Build the new system. Write bindings against it in <code>targets/new/</code>. Run <code>assay run specs/ --target new</code>. When that's green, the rewrite is done — at least for the surface area the specs cover.</li>
</ol>
<p>The reason the two tools sit next to each other is that the architecture model is what gives the spec work structure.
Without a model, "write specs for the legacy system" is an open-ended task with no obvious stopping point.
With the model, every component has a <code>specs/</code> directory and the question becomes "is each context's behavior covered?"
That's tractable.</p>
<div class="cta"><div class="container"><div class="row"><p>
🚀 Would you like to run this on your legacy codebase? Sign up for early access! <!-- --> <a href="https://73f4313d.sibforms.com/serve/MUIFAGf7xJuMm4W5YSsztruKlELH459zdPyB50IhmpgBnS6wyrTGqz7dQ55IJLSVa7CUPFvAHrUEvEYbHEn5tBivo8SQPN_cJIMdBr_O1xiQ9ug64k46sfNBIFuTHK1rNKqvqytuDkTUoh0C5XMyUKOPZy7whNQM8zviGihyCCrZYBTq7uTf5-35fK6Ki2YBOVnEAmV_WwrgoT1-wg==" class="plausible-event-name=Button+Click plausible-event-location=Post+Cta+surveyor">Count me in!</a></p></div></div></div>
<p>The cross-target story is what makes the rewrite verifiable.
The same parsed <code>.assay</code> scenarios are dispatched, step by step, through two different binding modules at two different points in the project's life:</p>
<!-- -->
<p>Same spec, two bindings, two systems.
The runner doesn't know or care which target it's dispatching to — that's the property that makes the rewrite a measurable thing rather than a leap of faith.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="four-audiences-one-artifact">Four audiences, one artifact<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#four-audiences-one-artifact" class="hash-link" aria-label="Direct link to Four audiences, one artifact" title="Direct link to Four audiences, one artifact" translate="no">​</a></h2>
<p>Four very different parties end up reading — or executing — the same <code>.assay</code> file: the <strong>product owner</strong>, the <strong>developer</strong>, the <strong>coding agent</strong>, and the <strong>test runner</strong>.
The interesting observation is that they don't conflict on <strong>content</strong> — they conflict on <strong>form</strong>.</p>
<p><strong>Product owners</strong> want to know what the system does in business terms, what would break if you changed X, and where the risk lives.
They don't want UML.
They want to read scenarios in their domain vocabulary, in plain language, and trust them.</p>
<p><strong>Developers</strong> want to know how the system is structured, where the seams are, and what invariants they mustn't break.
And — crucially — they want the documentation to be wrong less often than the code is.
The moment docs lie, developers stop reading them.</p>
<p><strong>Coding agents</strong> want machine-parseable, unambiguous, addressable artifacts with stable identifiers.
They want to be able to say "the assertion at <code>assay://orderLifecycle/order-placement/OL-001/step-3</code> failed" and have that mean something durable.
They want types, schemas, and graphs they can traverse.</p>
<p><strong>Automated tests</strong> want executable bindings — the layer Assay provides — and they want failures that point back to the spec, not just to a line of code.</p>
<p>Same facts, four presentations.</p>
<p>The <code>.assay</code> file gives the product owner readable scenarios in domain language.
The <code>workspace.dsl</code> plus the schema modules give the developer the structural truth.
The bracketed identifiers (<code>Component: [orderLifecycle]</code>, <code>Scenario: [OL-001]</code>) give the agent its stable addresses.
The runner turns the whole thing into a regression suite.
None of these audiences is asked to read the others' representation; they all read the same spec, surfaced at the level of detail they need.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-the-llm-lives-and-where-it-doesnt">Where the LLM lives, and where it doesn't<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#where-the-llm-lives-and-where-it-doesnt" class="hash-link" aria-label="Direct link to Where the LLM lives, and where it doesn't" title="Direct link to Where the LLM lives, and where it doesn't" translate="no">​</a></h2>
<p>A reasonable question at this point: with all the recent enthusiasm for LLMs, why isn't more of this LLM-driven?</p>
<p>The split is deliberate.</p>
<p>Surveyor uses an LLM because architectural discovery is genuinely a language task — you're reading config files, route definitions, deployment manifests, dependency lock files, and inferring "this is the API, that's the worker, that's the read model."
That's exactly the kind of fuzzy, evidence-weighing job an LLM does well, especially when you tell it to flag uncertainty rather than guess.</p>
<p>Assay does <strong>not</strong> use an LLM at runtime.
The runner is deterministic: parse the spec, match the regex, execute the binding, assert.
A behavioral spec that sometimes passes and sometimes does not, depending on which model variant happened to answer, is not a spec.</p>
<p>LLMs are very welcome on the authoring side — drafting <code>.assay</code> files from legacy source is a great agent task, and the handbook has explicit guidance for coding agents about how to do that without inventing behavior.
But the green/red signal at the end has to come from a deterministic runner, or it is not really a signal.</p>
<div class="cta"><div class="container"><div class="row"><p>
🚀 Sign up for early access:<!-- --> <a href="https://73f4313d.sibforms.com/serve/MUIFAGf7xJuMm4W5YSsztruKlELH459zdPyB50IhmpgBnS6wyrTGqz7dQ55IJLSVa7CUPFvAHrUEvEYbHEn5tBivo8SQPN_cJIMdBr_O1xiQ9ug64k46sfNBIFuTHK1rNKqvqytuDkTUoh0C5XMyUKOPZy7whNQM8zviGihyCCrZYBTq7uTf5-35fK6Ki2YBOVnEAmV_WwrgoT1-wg==" class="plausible-event-name=Button+Click plausible-event-location=Post+Cta+surveyor">Count me in!</a></p></div></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-you-use-them-on-a-project">How you use them on a project<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#how-you-use-them-on-a-project" class="hash-link" aria-label="Direct link to How you use them on a project" title="Direct link to How you use them on a project" translate="no">​</a></h2>
<p>In practice, the first day of a legacy rewrite looks something like this:</p>
<ol>
<li class="">Clone the repo. Run Surveyor with <code>--phase c1</code> to get the system in context. Discuss the actors and external systems with the client — this alone surfaces "wait, who is the Admin role?" conversations that would otherwise happen three months in.</li>
<li class="">Run <code>--phase c2</code> to map containers. This is where you discover the data plane that nobody documented, the cron job running on a forgotten server, the queue that two services share. Surveyor flags <code>confidence: low</code> items; the human resolves them.</li>
<li class="">Run <code>--phase c3</code> per container. By the end you have a <code>workspace.dsl</code> that is, for the first time in the project's history, an accurate picture of what is deployed.</li>
<li class="">Pick the highest-risk bounded context. Read the legacy source. Draft <code>.assay</code> specs for its behavior. Write a legacy binding. Run it. Iterate until green.</li>
<li class="">Repeat per context, in priority order, until coverage is good enough to start the rewrite.</li>
<li class="">Build the new system, one bounded context at a time, with <code>targets/new/</code> bindings going green as each context comes online.</li>
</ol>
<p>The thing both tools are optimized for is the same: making the work legible.
A legacy rewrite is a long project with shifting personnel and a nervous client.
Both Surveyor and Assay produce artifacts — a <code>workspace.dsl</code> and a directory of <code>.assay</code> files — that survive turnover, show progress, and that the client can actually read.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="friendly-user-trial">Friendly User Trial<a href="https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#friendly-user-trial" class="hash-link" aria-label="Direct link to Friendly User Trial" title="Direct link to Friendly User Trial" translate="no">​</a></h2>
<p>Our next step is to test Surveyor on real world projects. We want to find out if the artefacts it produces are already helpful or need to be extended. <strong>This is where we need your help</strong>:</p>
<p>If you have a legacy system staring you down and a rewrite on the roadmap, <a href="https://73f4313d.sibforms.com/serve/MUIFAGf7xJuMm4W5YSsztruKlELH459zdPyB50IhmpgBnS6wyrTGqz7dQ55IJLSVa7CUPFvAHrUEvEYbHEn5tBivo8SQPN_cJIMdBr_O1xiQ9ug64k46sfNBIFuTHK1rNKqvqytuDkTUoh0C5XMyUKOPZy7whNQM8zviGihyCCrZYBTq7uTf5-35fK6Ki2YBOVnEAmV_WwrgoT1-wg==" target="_blank" rel="noopener noreferrer" class="">get in touch</a>. We would love to hear from you!</p>]]></content:encoded>
            <category>elixir</category>
            <category>llm</category>
            <category>ai</category>
            <category>architecture</category>
            <category>legacy</category>
            <category>rewrite</category>
        </item>
        <item>
            <title><![CDATA[Tackling redesigns in Rails and Phoenix apps]]></title>
            <link>https://bitcrowd.dev/2026/03/30/tackling-redesigns-in-rails-and-phoenix-apps</link>
            <guid>https://bitcrowd.dev/2026/03/30/tackling-redesigns-in-rails-and-phoenix-apps</guid>
            <pubDate>Mon, 30 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Implementing a redesign in large, often semi-well aged Rails or Phoenix apps is a daunting task as one is often faced with a lot of legacy and poor organization of the view layer, JavaScript and CSS. Still, tackled with the right strategy and tooling, it can be a super rewarding experience for both, the client and the team.]]></description>
            <content:encoded><![CDATA[<p>Implementing a redesign in large, often semi-well aged <a href="https://rubyonrails.org/" target="_blank" rel="noopener noreferrer" class="">Rails</a> or <a href="https://www.phoenixframework.org/" target="_blank" rel="noopener noreferrer" class="">Phoenix</a> apps is a daunting task as one is often faced with a lot of legacy and poor organization of the view layer, JavaScript and CSS. Still, tackled with the right strategy and tooling, it can be a super rewarding experience for both, the client and the team.</p>
<p>At bitcrowd we recently did a couple of those redesign projects for our clients. Let's explore how we demystify them, what strategies work for us and where the challenges are.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="-problems">🌊 Problems<a href="https://bitcrowd.dev/2026/03/30/tackling-redesigns-in-rails-and-phoenix-apps#-problems" class="hash-link" aria-label="Direct link to 🌊 Problems" title="Direct link to 🌊 Problems" translate="no">​</a></h2>
<p>Things on the internet age quickly. Some age well, some don't. <strong>A web application may very likely look dated</strong> and just not state of the art after some years out in the wild. This process is heavily influenced by the fast turning tech standards and the iterative nature of development projects, which can lead to a loss of an overall UX &amp; design vision for the product.</p>
<p>The need for a visual overhaul of an application can also become apparent <strong>when the design stands in the way of new features.</strong> For example, an horizontal topbar for an app menu can be the right choice when the navigation is concise, but this choice does not scale as new primary features requiring an entry in the main navigation menu are added.</p>
<p>Finally, disorganized styles, templates, and generally a lack of componentization, can cause an <strong>increase of complexity for adding or updating views.</strong> Let's look at a classic example: buttons being styled by how they <em>look</em>, instead of what they <em>are</em>.</p>
<div class="language-html codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-html 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">&lt;!-- Looks --&gt;</span><span class="token plain"></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" style="color:#00009f">button</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">class</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">button button--blue</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">button</span><span class="token tag punctuation" style="color:#393A34">&gt;</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">&lt;!-- Roles --&gt;</span><span class="token plain"></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" style="color:#00009f">button</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">class</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">button button--secondary</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">button</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><br></span></code></pre></div></div>
<p>When a developer is asked to change the looks of a button variant in the app, having a componentized button means there is only <strong>one</strong> file to change, as opposed to an endless amount of template files to update.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="redesign-how-hard-can-it-be">Redesign: how hard can it be?<a href="https://bitcrowd.dev/2026/03/30/tackling-redesigns-in-rails-and-phoenix-apps#redesign-how-hard-can-it-be" class="hash-link" aria-label="Direct link to Redesign: how hard can it be?" title="Direct link to Redesign: how hard can it be?" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-its-not-just-the-looks">🤿 It's not just the looks<a href="https://bitcrowd.dev/2026/03/30/tackling-redesigns-in-rails-and-phoenix-apps#-its-not-just-the-looks" class="hash-link" aria-label="Direct link to 🤿 It's not just the looks" title="Direct link to 🤿 It's not just the looks" translate="no">​</a></h3>
<p>In most cases, the term "redesign" goes way beyond changing a few colors, fonts or images. It is most likely a complete change of the look and feel of the app, including a new navigation, restructuring of content, etc. A redesign has to be carried on the <strong>entire view layer</strong>, not just the styles.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-its-not-just-legacy">🎰 It's not just legacy<a href="https://bitcrowd.dev/2026/03/30/tackling-redesigns-in-rails-and-phoenix-apps#-its-not-just-legacy" class="hash-link" aria-label="Direct link to 🎰 It's not just legacy" title="Direct link to 🎰 It's not just legacy" translate="no">​</a></h3>
<p>Plus, the new design may also include <strong>new features</strong>. Some might be hiding in plain sight: a new redirection logic that is invisible in the new design files, or a text change that requires to wire the data from the controller differently. These can easily be missed when estimating the effort. There can also be real new features, which can be hard to estimate as they assume that the application is in a futuristic state.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-its-blocking-everything-else">🚧 It's blocking everything else<a href="https://bitcrowd.dev/2026/03/30/tackling-redesigns-in-rails-and-phoenix-apps#-its-blocking-everything-else" class="hash-link" aria-label="Direct link to 🚧 It's blocking everything else" title="Direct link to 🚧 It's blocking everything else" translate="no">​</a></h3>
<p>Last but not least: it is practically impossible to release a redesign in small cycles. That would lead to Frankenstein-y UIs and user flows. In other words, the majority of the visual work has to be finalized before the launch: it has to be a big-bang production release. This can be problematic as redesign projects typically take months to reach a first release.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="strategy-and-timeline">Strategy and timeline<a href="https://bitcrowd.dev/2026/03/30/tackling-redesigns-in-rails-and-phoenix-apps#strategy-and-timeline" class="hash-link" aria-label="Direct link to Strategy and timeline" title="Direct link to Strategy and timeline" translate="no">​</a></h2>
<p>When clients approach us to implement a redesign for them, they usually have large parts of the new design already laid out by their (or external) designer(s). Starting from there, we mostly go through the same series of steps with them:</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="design-access-and-feedback">Design access and feedback<a href="https://bitcrowd.dev/2026/03/30/tackling-redesigns-in-rails-and-phoenix-apps#design-access-and-feedback" class="hash-link" aria-label="Direct link to Design access and feedback" title="Direct link to Design access and feedback" translate="no">​</a></h3>
<p>Over the years, <a href="https://www.figma.com/" target="_blank" rel="noopener noreferrer" class="">Figma</a> ended up being our go-to tool for anything design related, being it design systems, prototyping, wireframing or UX design. There are alternative tools which do the same. We for instance used <a href="https://zeplin.io/" target="_blank" rel="noopener noreferrer" class="">Zeplin</a> for a while, too. Regardless of the tool, the important part is <strong>to have one source of truth for all involved parties</strong>.</p>
<p>In the ramp-up phase of the redesign, we review the designs with the client, check for consistency, verify what's doable within the app and check for completeness of the new design. The most important task here is to identify when the design includes new features which aren't present in the current state of the app.</p>
<p>Ideally, the designs should be "final" by the time the actual implementation starts.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="estimate-the-effort">Estimate the effort<a href="https://bitcrowd.dev/2026/03/30/tackling-redesigns-in-rails-and-phoenix-apps#estimate-the-effort" class="hash-link" aria-label="Direct link to Estimate the effort" title="Direct link to Estimate the effort" translate="no">​</a></h3>
<p>Estimating the effort of a redesign can be quite hard. When putting together the work packages and labelling them with their effort, separating pure redesign work from new features and backend-only work turned out to be useful for us. The key point is again to identify when the new design accidentally or deliberately adds or changes functionality and which additional effort this brings.</p>
<p>Another thing to take into account for estimating the effort is the potential overhead of switching over to a different UI abstraction or library. Let's say we're moving a Rails app from using partials to <a href="https://viewcomponent.org/" target="_blank" rel="noopener noreferrer" class="">ViewComponents</a>, an old Phoenix app from partials to <a href="https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html" target="_blank" rel="noopener noreferrer" class="">PhoenixComponents</a> or even from controllers and dead views to Phoenix <a href="https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html" target="_blank" rel="noopener noreferrer" class="">LiveView</a>. Doing this with consistency is essentially a rewrite of the whole view layer.</p>
<p>At best, one has thorough knowledge of the app and the codebase, its patterns, its degree of legacy and where complexity lies. It can be interesting to factor these in the effort estimation: how comfortable is the team with the codebase, what is the test coverage, are the views already componentized, etc.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="ui-abstraction--integration-strategy">UI Abstraction &amp; Integration strategy<a href="https://bitcrowd.dev/2026/03/30/tackling-redesigns-in-rails-and-phoenix-apps#ui-abstraction--integration-strategy" class="hash-link" aria-label="Direct link to UI Abstraction &amp; Integration strategy" title="Direct link to UI Abstraction &amp; Integration strategy" translate="no">​</a></h3>
<p>Once the page designs are set up properly in Figma, they can be used as a basis for extracting design tokens like colors or spacing and shared components. It may seem tempting to be sloppy on this step, since it does not change anything within the app itself yet. But being concise about having the whole new design set up properly in Figma turned out to be absolutely worth the effort in 100% cases for us.</p>
<p>On our redesign projects we identify core components and build a design system and a component library. Those core elements, like typography, buttons, cards, etc, are built in isolation using the given frameworks capabilities to test and showcase them with <a href="https://storybook.js.org/" target="_blank" rel="noopener noreferrer" class="">Storybook</a> (React), <a href="https://lookbook.build/" target="_blank" rel="noopener noreferrer" class="">Lookbook</a> (Rails) or <a href="https://phoenix-storybook-demo.fly.dev/storybook/welcome" target="_blank" rel="noopener noreferrer" class="">PhoenixStorybook</a> (Phoenix). Being able to develop our core components in isolation, showing all of their different states without interfering with the existing application is a huge boost for productivity as it allows <em>building</em> the components (styles &amp; logic) independent from <em>using</em> them.</p>
<p>For the CSS, we often start completely from scratch. Either moving the old rules to a "legacy" folder of having the new ones in a separate redesign folder. One of them should be empty and gone at the end. It's always super satisfying to delete old code...</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="implementation">Implementation<a href="https://bitcrowd.dev/2026/03/30/tackling-redesigns-in-rails-and-phoenix-apps#implementation" class="hash-link" aria-label="Direct link to Implementation" title="Direct link to Implementation" translate="no">​</a></h3>
<p>Any changes necessary from the backend are ideally done before the frontend ticket is worked on. So that when it comes to implementing the new design, all the data is already present. It can for instance also make sense to already render out plain data that is needed for a new design, but keep it hidden in the DOM. This way, the person taking on the design ticket has an easier time: with unit &amp; feature tests already in place to make sure they did not break anything, and the changes in the PR being only on the view layer.</p>
<p>Without a tool like Storybook, depending on the size and complexity of the application, developers need to spend time to get the application into the particular state where a component behaves like xyz. Same when reviewing the work on the components. That can be an absolute time sink. Here again, componentized views and the associated tooling help the team!</p>
<p>Once a good portion of the redesign is done, it can make sense to re-estimate the effort. Maybe it turns out, that a technical decision had to be done that either simplifies a lot of the rest of the work, or - unfortunately easier the case - there was hidden complexity in some area of the application which slowed down the progress a lot. Re-estimating can also be a chance to cut down on feature changes and re-align the scope of the redesign.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://bitcrowd.dev/2026/03/30/tackling-redesigns-in-rails-and-phoenix-apps#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>We're rolling quite well with our approach to redesigns! The right tooling is key: for us, Figma, some kind of Storybook and a framework to build UI components just make sense. Another core part is to have as much discussion about the designs and their consistency done before any actual development takes place. Tackled the right way, a redesign can be loads of fun for all parties involved. From a developer's perspective it just feels great to be able to iron out bad decisions from the past, refactor and update things left and right and just leave every part of the codebase nicer as it was before. For the clients, it's absolutely satisfying to see their ideas about the new design come to life bit by bit like blossoms in spring. Everything is - at best - fresh, more thought through and generally better than before!</p>]]></content:encoded>
            <category>redesign</category>
            <category>view components</category>
            <category>storybook</category>
            <category>lookbook</category>
        </item>
        <item>
            <title><![CDATA[Building a performance evaluation toolkit and a dataplane PoC for atproto]]></title>
            <link>https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto</link>
            <guid>https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto</guid>
            <pubDate>Mon, 30 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[There is a new open social ecosystem emerging around the AT Protocol, also known as atproto.]]></description>
            <content:encoded><![CDATA[<p>There is a new open social ecosystem emerging around the AT Protocol, also known as <em>atproto</em>.
Perhaps youʼve never heard of it, but you should check it out.
Itʼs <a href="https://overreacted.io/open-social/" target="_blank" rel="noopener noreferrer" class="">tech</a> with an <a href="https://www.youtube.com/watch?v=1A-0k58TfPo" target="_blank" rel="noopener noreferrer" class="">ethos</a>.</p>
<p>The open atproto system provides a social graph of more than 40 million Bluesky users.<br>
<!-- -->Building on this protocol, you can make your project everyoneʼs new favorite social network.</p>
<p>Interoperable alternatives to Bluesky, such as <a href="https://northskysocial.com/" target="_blank" rel="noopener noreferrer" class="">Northsky</a>, <a href="https://eurosky.social/" target="_blank" rel="noopener noreferrer" class="">Eurosky</a> and <a href="https://blacksky.community/" target="_blank" rel="noopener noreferrer" class="">Blacksky</a>, managed to attract user groups by addressing their specific needs.</p>
<p>They all are built on the software components that Bluesky provides.</p>
<p>We will get an overview of those components and find out which problems arise when your userbase grows.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-bluesky-appview">The Bluesky AppView<a href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#the-bluesky-appview" class="hash-link" aria-label="Direct link to The Bluesky AppView" title="Direct link to The Bluesky AppView" translate="no">​</a></h2>
<p>The diagram below shows an architectural overview of Bluesky with the three main infrastructure components involved.</p>
<ul>
<li class=""><code>Personal Data Server (PDS)</code>: every user has their own, this is where all their data is stored.</li>
<li class=""><code>Relay</code>: polls every existing <code>PDS</code> on the network to collect changes and emit them in a single event stream called the <code>Firehose</code>.</li>
<li class=""><code>AppView</code>: essentially is a regular app backend which receives events from the <code>Firehose</code>, processes them and makes data available via an API. The actual data storage is abstracted in a <code>dataplane</code> component.</li>
</ul>
<p>For a write event, such as <code>Create Post</code>, a record is stored in the Content Archive inside the PDS of the user.<br>
<!-- -->It is then emitted via the Relay into the Firehose.<br>
<!-- -->The dataplane of the AppView subscribes to the Firehose to receive the event and indexes it into a permanent database.</p>
<p>To read their <code>Timeline</code>, a user performs a request which is proxied via their PDS to the AppView, which in turn retrieves the data from the dataplane and returns the timeline to the user.</p>
<p><img decoding="async" loading="lazy" alt="Bluesky architecture overview" src="https://bitcrowd.dev/assets/images/atproto-d0891a2b5568cfd36b373127b8a5ace2.png" width="1729" height="578" class="img_ev3q"></p>
<p>All of the components displayed are available as open source software. Bluesky itself runs almost entirely on these open source components.</p>
<p>There is one notable exception: the dataplane, the part of the stack that processes the event stream of the atproto firehose and does the most heavy lifting.
While it is available as open source, this dataplane implementation is not very performant.
Bluesky uses a closed source dataplane implementation in production for this reason.</p>
<p>The open source dataplane functions on a fan-in principle:</p>
<ol>
<li class="">events are streamed into the system (e.g. “new post”)</li>
<li class="">the dataplane stores and indexes the events in ordinary Postgres tables</li>
<li class="">on user request, data is queried and presented to the user (fan-in)</li>
</ol>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-problems-of-the-dataplane">The Problems of the Dataplane<a href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#the-problems-of-the-dataplane" class="hash-link" aria-label="Direct link to The Problems of the Dataplane" title="Direct link to The Problems of the Dataplane" translate="no">​</a></h3>
<p>The fan-in approach works well as long as the number of users is small enough.
At some point, often with less than <code>100_000</code> users, the user experience degrades and the server runs into issues.</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 operator" style="color:#393A34">&lt;</span><span class="token plain">--- Last few GCs ---</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" style="display:inline-block"></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">55267</span><span class="token plain">:0x7bd40c000</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">12985916</span><span class="token plain"> ms: Mark-Compact </span><span class="token number" style="color:#36acaa">4016.0</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">4165.4</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> -</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">3962.5</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">4175.4</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> MB, pooled: </span><span class="token number" style="color:#36acaa">0.0</span><span class="token plain"> MB, </span><span class="token number" style="color:#36acaa">1065.65</span><span class="token plain"> / </span><span class="token number" style="color:#36acaa">0.00</span><span class="token plain"> ms </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">average mu </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.156</span><span class="token plain">, current mu </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.074</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> allocation failure</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> scavenge might not succeed</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">55267</span><span class="token plain">:0x7bd40c000</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">12987076</span><span class="token plain"> ms: Mark-Compact </span><span class="token number" style="color:#36acaa">4026.5</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">4207.4</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> -</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">3973.1</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">4217.9</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> MB, pooled: </span><span class="token number" style="color:#36acaa">0.0</span><span class="token plain"> MB, </span><span class="token number" style="color:#36acaa">1078.12</span><span class="token plain"> / </span><span class="token number" style="color:#36acaa">0.00</span><span class="token plain"> ms </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">average mu </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.116</span><span class="token plain">, current mu </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.071</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> allocation failure</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> scavenge might not succeed</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">FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory</span><br></span></code></pre></div></div>
<p><a href="https://newsletter.pragmaticengineer.com/p/bluesky" target="_blank" rel="noopener noreferrer" class="">This episode</a> of the pragmatic engineer newsletter lists a number of issues with Blueskyʼs Postgres-based dataplane.</p>
<blockquote>
<ul>
<li class="">Connection pool issues and lock contention. The Postgres connection pool backup and Nodeʼs event loop got into a bad feedback loop. The team observed Postgres lock contention issues. This refers to multiple processes trying to access the same data simultaneously, but the data is locked to all except one process. For example, when multiple processes attempt to update the same row.</li>
<li class="">Small Postgres outages. Postgres doesnʼt give the developer much control over which query plan it will take. Bluesky had a few smaller outages due to a query plan randomly flipping to something that ran about 1,000x times slower.</li>
<li class="">The need for horizontal scaling. Horizontal scaling is adding more machines to a service, so that the throughput of this system improves linearly with each new machine. But Postgres does not support horizontal scaling because it runs as a single database with transactional guarantees, meaning it becomes a bottleneck – if a necessary one – for the entire network.</li>
</ul>
</blockquote>
<p>The bottom line is: the open source dataplane implementation is not optimized at all.</p>
<p>It exists solely to be able to say thereʼs at least <em>one</em> open source implementation available.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-performant-open-source-dataplane">A performant open source dataplane<a href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#a-performant-open-source-dataplane" class="hash-link" aria-label="Direct link to A performant open source dataplane" title="Direct link to A performant open source dataplane" translate="no">​</a></h2>
<p>The real trick to solve these problems is to change the approach.
Instead of querying the data on user request, we prepare data in a dedicated place for each user on write (fan-out).
When the user makes a request, the data is already sitting there waiting to be served to the user.</p>
<p>This is the also the approach followed by the Bluesky closed source dataplane which builds on ScyllaDB.
From all publicly available information, that closed source dataplane seems very optimized to their hardware, and pretty expensive to run.</p>
<p>We followed the same approach for our proof of concept of a dataplane, which supports indexing post and follow events, as well as serving timelines to users.<br>
<!-- -->As we wrote this dataplane in Elixir (<a href="https://bitcrowd.net/en/elixir/" target="_blank" rel="noopener noreferrer" class="">our language of choice</a>), we used <a href="https://www.erlang.org/doc/apps/stdlib/ets.html" target="_blank" rel="noopener noreferrer" class="">ETS</a>, the built-in storage of the Erlang VM, instead of ScyllaDB.
ETS provides us with tables that can store tuples of data.</p>
<p>In the snippet below, you can see the <code>insert_post</code> function which indexes posts that arrive in the dataplane.
First, we record the post in the global <code>@posts_table</code>.
Then we lookup all the followers of the user in the <code>@followers_table</code> and finally store the post in the feed of each each follower, in the <code>@feeds_table</code>.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">insert_post</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">post_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> author_id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">insert</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attribute variable" style="color:#36acaa">@posts_table</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">post_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> author_id</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">  follower_entries </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">lookup</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attribute variable" style="color:#36acaa">@followers_table</span><span class="token punctuation" style="color:#393A34">,</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" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">each</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">follower_entries</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">fn</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">_subject</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> follower_id</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </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 atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">insert</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attribute variable" style="color:#36acaa">@feeds_table</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">follower_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> post_id</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">end</span><span class="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">end</span><br></span></code></pre></div></div>
<p>The read operation to get a timeline for a user is then a single lookup in the <code>@feeds_table</code>.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">get_timeline</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user_id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token attribute variable" style="color:#36acaa">@feeds_table</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">|&gt;</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:ets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">lookup</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user_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 operator" style="color:#393A34">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">&amp;</span><span class="token function" style="color:#d73a49">elem</span><span class="token punctuation" style="color:#393A34">(</span><span class="token argument variable" style="color:#36acaa">&amp;1</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">end</span><br></span></code></pre></div></div>
<p>Our ETS dataplane not only shows a significant performance improvement over the available open source implementation, it also has no operational overhead — ETS is built into the Erlang VM.
So we run a single node with our Elixir application and thatʼs it.</p>
<p>As a bonus, we get all the benefits of running Elixir:</p>
<ul>
<li class="">good resource utilization and error isolation just come naturally with the parallelism provided by the Erlang VM</li>
<li class="">tooling for introspection and observability is available out of the box</li>
<li class="">in case of traffic spikes, the runtime will bend instead of breaking, and continue to serve the users</li>
<li class="">no big refactor needed if we ever want to distribute the load over multiple nodes or start sharding the data</li>
</ul>
<p>Our big goal here is that we build a dataplane that is low-effort to operate and runs on commodity hardware, but scales way better than Blueskyʼs open source dataplane.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-peek-into-performance">A peek into performance<a href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#a-peek-into-performance" class="hash-link" aria-label="Direct link to A peek into performance" title="Direct link to A peek into performance" translate="no">​</a></h2>
<p>As we want to focus solely on the characteristics of the storage technology and basic approach (fan-in vs. fan-out), we created two versions of the dataplane in Elixir:</p>
<ol>
<li class="">the proof of concept we already talked about which follows the fan-out approach and stores data in ETS</li>
<li class="">a copy which differs only in following the fan-in approach and storing data in Postgres (and is therefore representative of Bluesky's open source dataplane)</li>
</ol>
<p>With that, we could evaluate the proof of concept by measuring direct reads and writes to the dataplanes, bypassing all other factors that play a role in a real-world scenario.</p>
<p>We simulated write traffic and let a number of users make requests to load their timeline (<code>get_timeline</code>).
In the dashboards below, you can see the number of simultaneously active users (“Active Sessions”), the write traffic (“Posts created”), and the latencies and throughput of <code>get_timeline</code> requests.</p>
<p>Under heavy load, the Postgres backed fan-in implementation showed significantly higher latencies and lower throughput in comparison to the ETS backed fan-out implementation.</p>
<figure class="image-with-caption"><img decoding="async" loading="lazy" alt="Dashboard for Postgres backed fan-in implementation shows p95 latency at
almost 1s at 105 get_timeline requests per second while the dashboard for ETS
backed fan-out implementation shows p95 latency of less than 20ms at more than
200 get_timeline requests per second" src="https://bitcrowd.dev/assets/images/timeline_comparison-ddceb0fa147ea52a3bbf8aa5f00ac39a.png" width="554" height="540" class="img_ev3q"><figcaption>The latencies for the Postgres backed fan-in implementation on the left are higher than for the ETS backed fan-out implementation on the right</figcaption></figure>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="advanced-simulations">Advanced simulations<a href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#advanced-simulations" class="hash-link" aria-label="Direct link to Advanced simulations" title="Direct link to Advanced simulations" translate="no">​</a></h2>
<p>We extended this proof of concept into a performance toolkit you can use to simulate traffic via atproto events and user requests.</p>
<figure class="image-with-caption"><img decoding="async" loading="lazy" alt="Dashboard for Postgres backed fan-in implementation shows p95 latency at
almost 1s at 105 get_timeline requests per second" src="https://bitcrowd.dev/assets/images/overview-77a80c892c3571b562b40e63724c4109.png" width="1272" height="785" class="img_ev3q"><figcaption>Overview of the simulator</figcaption></figure>
<p>As a fundamental principle, we want to be able to repeatedly simulate scenarios.
Therefore, you provide the configuration for a scenario as a file that you can load into the system.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="userbase-parameters">Userbase parameters<a href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#userbase-parameters" class="hash-link" aria-label="Direct link to Userbase parameters" title="Direct link to Userbase parameters" translate="no">​</a></h3>
<p>It makes a difference for your appʼs performance whether you already have millions of users or posts in your database or none.
With the performance toolkit, you can quickly create base data to prepare your application instance for the simulated scenario.</p>
<p>Generate users in by passing in parameters in JSON format:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="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-property property" style="color:#36acaa">"name"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"my userbase"</span><span class="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-property property" style="color:#36acaa">"num_users"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100000</span><span class="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-property property" style="color:#36acaa">"follower_density"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5.0</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>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-power-law-follower-graph">A power-law follower graph<a href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#a-power-law-follower-graph" class="hash-link" aria-label="Direct link to A power-law follower graph" title="Direct link to A power-law follower graph" translate="no">​</a></h3>
<p>We assign the generated users sequential IDs, and they follow each other according to a <a href="https://en.wikipedia.org/wiki/Power_law" target="_blank" rel="noopener noreferrer" class="">power law</a>.</p>
<p>All users follow the very first user account.
With increasing user ID, the number of followers gets smaller quickly.</p>
<p>We do this for two reasons:</p>
<ol>
<li class="">this distribution represents a realistic userbase where you have few accounts with many followers (looking at you <a href="https://bsky.app/profile/bsky.app" target="_blank" rel="noopener noreferrer" class="">@bsky.app</a>) and many accounts with very few followers.</li>
<li class="">a predefined distribution makes it trivial to pick suitable accounts for the traffic you want to simulate, as you can determine the number of followers from the user ID.
You donʼt have to first search a suitable account — to simulate a post from a user with many followers, pick a small user ID.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="The generated follower graph follows a power law." src="https://bitcrowd.dev/assets/images/follower_graph-0c3310c8d4d962ca9b309b5e01346a90.png" width="864" height="576" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="simulating-traffic">Simulating traffic<a href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#simulating-traffic" class="hash-link" aria-label="Direct link to Simulating traffic" title="Direct link to Simulating traffic" translate="no">​</a></h3>
<p>To simulate traffic, we introduce two concepts: <em>scenario</em> and <em>simulation plan</em>.
With these concepts we can represent repeatable and realistic simulations.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="scenario">Scenario<a href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#scenario" class="hash-link" aria-label="Direct link to Scenario" title="Direct link to Scenario" translate="no">​</a></h4>
<p>A <em>scenario</em> captures a series of user actions that will happen at defined time offsets during simulation.</p>
<p>At the moment three actions can be simulated:</p>
<ul>
<li class="">user creates a post</li>
<li class="">user follows another user</li>
<li class="">user requests their timeline</li>
</ul>
<p>A single scenario can contain series for all three actions, but all of them are optional.</p>
<p>As you can see, these actions are associated with specific users and an offset.
For instance, “user 1 posts at offset 1000ms” or “user 2 follows user 3 at offset 1500ms”.
This series of events gives us a repeatable scenario.</p>
<p>You donʼt have to write the full scenario yourself, the toolkit lets you generate a scenario from parameters, similar to what we saw for the userbase.</p>
<p>Here is an example for posts: we have <code>100_000</code> users in the system but only the first <code>10_000</code> will post.
We classify the users into tiers to make their behavior more realistic.
Users with many followers will post more.</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="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-property property" style="color:#36acaa">"n"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100000</span><span class="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-property property" style="color:#36acaa">"max_active_user_id"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10000</span><span class="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-property property" style="color:#36acaa">"seed"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">43</span><span class="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-property property" style="color:#36acaa">"time_units"</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token string-property property" style="color:#36acaa">"tiers"</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">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"max_followers"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"posts_per_day"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.2</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><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 string-property property" style="color:#36acaa">"max_followers"</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 plain"> </span><span class="token string-property property" style="color:#36acaa">"posts_per_day"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.0</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><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 string-property property" style="color:#36acaa">"max_followers"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"posts_per_day"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">3.0</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 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 will result in a list of post events like the following.</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token string-property property" style="color:#36acaa">"posts"</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 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 string-property property" style="color:#36acaa">"user_id"</span><span class="token operator" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">124</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"offset_ms"</span><span class="token operator" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">719</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 string-property property" style="color:#36acaa">"user_id"</span><span class="token operator" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">5037</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"offset_ms"</span><span class="token operator" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">4160</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 string-property property" style="color:#36acaa">"user_id"</span><span class="token operator" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">6504</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"offset_ms"</span><span class="token operator" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">21584</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 string-property property" style="color:#36acaa">"user_id"</span><span class="token operator" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">7422</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"offset_ms"</span><span class="token operator" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">23714</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 string-property property" style="color:#36acaa">"user_id"</span><span class="token operator" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">1611</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"offset_ms"</span><span class="token operator" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">27590</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 spread 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 punctuation" style="color:#393A34">]</span><br></span></code></pre></div></div>
<p>The simulation will then emit a realistic corresponding atproto event at the given offset into the firehose.</p>
<p>For usersʼ requests, such as the <code>get_timeline</code> request in our simulator, you can use a similar set of parameters to define the user sessions.</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="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-property property" style="color:#36acaa">"n"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100000</span><span class="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-property property" style="color:#36acaa">"max_active_user_id"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10000</span><span class="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-property property" style="color:#36acaa">"seed"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">42</span><span class="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-property property" style="color:#36acaa">"time_units"</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token string-property property" style="color:#36acaa">"tiers"</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">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"max_followers"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"session_minutes"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10</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><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 string-property property" style="color:#36acaa">"max_followers"</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 plain"> </span><span class="token string-property property" style="color:#36acaa">"session_minutes"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">30</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><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 string-property property" style="color:#36acaa">"max_followers"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"session_minutes"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">60</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 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>With the following resulting sessions.</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token string-property property" style="color:#36acaa">"sessions"</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 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 string-property property" style="color:#36acaa">"user_id"</span><span class="token operator" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">4684</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"duration_ms"</span><span class="token operator" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">1800000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"offset_ms"</span><span class="token operator" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">9638</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 string-property property" style="color:#36acaa">"user_id"</span><span class="token operator" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">8021</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"duration_ms"</span><span class="token operator" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">1800000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"offset_ms"</span><span class="token operator" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">17306</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 string-property property" style="color:#36acaa">"user_id"</span><span class="token operator" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">567</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"duration_ms"</span><span class="token operator" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">3600000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"offset_ms"</span><span class="token operator" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">19438</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 spread 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 punctuation" style="color:#393A34">]</span><br></span></code></pre></div></div>
<p>Each user will start a session at the corresponding offset and will perform requests to get their timeline in fixed intervals for the whole session duration.</p>
<p>Scenarios are intended to represent typical real world traffic, for instance a regular “saturday morning”, or you know, a “shitstorm”.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="simulation-plan">Simulation Plan<a href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#simulation-plan" class="hash-link" aria-label="Direct link to Simulation Plan" title="Direct link to Simulation Plan" translate="no">​</a></h4>
<p>A <em>simulation plan</em> contains multiple scenarios that start at defined time offsets.</p>
<p>Example:</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">simulation_plan </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">  </span><span class="token punctuation" style="color:#393A34">%</span><span class="token punctuation" style="color:#393A34">{</span><span class="token attr-name" style="color:#00a4db">scenario_name:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"saturday morning"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">path:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"saturday_morning.json"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">offset_ms:</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 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 attr-name" style="color:#00a4db">scenario_name:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"shitstorm"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">path:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"shitstorm.json"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">offset_ms:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100_000</span><span class="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>In this example simulation plan, the simulator starts with traffic as defined in the “saturday morning” scenario.
At the offset of <code>100_000 ms</code>, it will additionally start simulating the traffic as defined in the “shitstorm” scenario.</p>
<p>Every simulation run follows a single simulation plan.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="repeatable-simulations">Repeatable Simulations<a href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#repeatable-simulations" class="hash-link" aria-label="Direct link to Repeatable Simulations" title="Direct link to Repeatable Simulations" translate="no">​</a></h3>
<p>As stated before, providing repeatable simulations is a primary goal of our simulator, but we still want to enable interactive use for experimentation.
If you add scenarios interactively to the running simulation, they will be included in the current simulation plan at the offset corresponding to the current runtime.
That simulation plan will then automatically be exported.</p>
<p>So after noticing issues during experimentation, you will always have a simulation plan that you can run again to confirm the issues you noticed are real.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="measuring-performance">Measuring performance<a href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#measuring-performance" class="hash-link" aria-label="Direct link to Measuring performance" title="Direct link to Measuring performance" translate="no">​</a></h3>
<p>Based on the simulated traffic, we can measure the performance users would experience.</p>
<p>The performance toolkit includes a Docker Compose setup for <a href="https://prometheus.io/" target="_blank" rel="noopener noreferrer" class="">Prometheus</a> and <a href="https://grafana.com/" target="_blank" rel="noopener noreferrer" class="">Grafana</a>, with a preconfigured Grafana dashboard.</p>
<p>The key question weʼre interested in is how long it would take under the simulated load for a user to receive their timeline.
The dashboard shows the p99, p95, p50 latencies so we can assess this.<br>
<!-- -->To gain insights into the running simulation, we also show how many user requests are performed per second and how many user sessions are active.
Furthermore, we show how many post and follow events are created, dispatched, and dropped.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-outcomes">The outcomes<a href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#the-outcomes" class="hash-link" aria-label="Direct link to The outcomes" title="Direct link to The outcomes" translate="no">​</a></h2>
<p>With our performance toolkit, we ran simulations with up to 40 million users, <strong>the actual scale</strong> of Bluesky.</p>
<p>The dashboard below shows a comparison between the open source dataplane and our ETS-based proof of concept.
In this case, we ran a simulation with 4 million users, which caused around 800 <code>get_timeline</code> requests per second, as well as some traffic for created posts and follows.</p>
<p>In the screenshot of the dashboard we can see two blocks of data.</p>
<p>The first run was recorded with the open source dataplane implementation.<br>
<!-- -->We can see a maximum p99 latency of more than 60 ms.
Furthermore, we see the throughput of requests per second fluctuating around 600.</p>
<p>In comparison, the block on the right shows recordings of the same simulation with our ETS-based dataplane.
The latencies are tiny, and the throughput sits smoothly at 800 requests per second.</p>
<p><img decoding="async" loading="lazy" alt="Grafana dashboard showing data for two simulation runs" src="https://bitcrowd.dev/assets/images/results-372ed8c47c2164ba530beae16fb5f31f.png" width="1314" height="851" class="img_ev3q"></p>
<p>Here a zoomed in view on the results for the ETS-based dataplane.</p>
<p><img decoding="async" loading="lazy" alt="Grafana dashboard zoomed in data for the ETS-based dataplane" src="https://bitcrowd.dev/assets/images/results_ets-e95f4d0b86e479f31c53f542250ab148.png" width="1314" height="851" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next-steps">Next steps<a href="https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#next-steps" class="hash-link" aria-label="Direct link to Next steps" title="Direct link to Next steps" translate="no">​</a></h2>
<p>Our big goal is an open source dataplane implementation that requires little effort to operate and scales better than the existing one.
For now, our proof of concept only indexes posts and follows, but there are many more relevant event types.
For comparison, Blueskyʼs dataplane implementation has 14 indexer plugins to cover all of them.</p>
<p>Furthermore, our proof of concept only supports a single endpoint for users to request their timeline.
To complete the implementation, many more endpoints must be implemented — Blueskyʼs implementation has 96 endpoints, supporting all the features youʼd expect, including moderation and search.
However, most of the endpoints are straightforward.</p>
<p>Lastly, we can still optimize the ETS-based implementation in many ways.
And if we ever hit a performance issue, ETS as storage technology can be supported by or even completely replaced with other natively implemented datastructures, while we can retain the Elixir implementation of the dataplane server.</p>]]></content:encoded>
            <category>elixir</category>
            <category>atproto</category>
            <category>open social</category>
            <category>performance</category>
        </item>
        <item>
            <title><![CDATA[Optimizing PDF render time in Rails]]></title>
            <link>https://bitcrowd.dev/optimizing-pdf-render-time-in-rails</link>
            <guid>https://bitcrowd.dev/optimizing-pdf-render-time-in-rails</guid>
            <pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[At bitcrowd we love working with PDFs! We even made ourselves famous in the Elixir ecosystem in the past years. One reason is that PDFs have a notable bad reputation, and we are convinced that they donʼt have to.]]></description>
            <content:encoded><![CDATA[<p>At bitcrowd we love working with PDFs! We even <a href="https://bitcrowd.dev/chromic-pdf/" target="_blank" rel="noopener noreferrer" class="">made ourselves famous</a> in the Elixir ecosystem in the past years. One reason is that PDFs have a notable bad reputation, and we are convinced that they donʼt <em>have</em> to.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-problem">The problem<a href="https://bitcrowd.dev/optimizing-pdf-render-time-in-rails#the-problem" class="hash-link" aria-label="Direct link to The problem" title="Direct link to The problem" translate="no">​</a></h2>
<p>One of our client provides generated, dynamic, potentially large, watermarked PDFs to their users. The PDF content is generated based on rich text content stored in the database, that is assembled into HTML. A small JavaScript snippet runs on the generated HTML to add a watermark to each page. The rendered data is then given to <a href="https://github.com/excid3/ferrum_pdf" target="_blank" rel="noopener noreferrer" class="">Ferrum PDF</a> to generate the actual PDF file.</p>
<p><img decoding="async" loading="lazy" alt="Synchronous processing of the PDF download request" src="https://bitcrowd.dev/assets/images/sync-pdf-request-processing-2d599bbae7f5d4cf1196e4a6b1513861.png" width="1722" height="306" class="img_ev3q"></p>
<p>Letʼs look at the weaknesses of this flow:</p>
<ol>
<li class="">
<p>First of all, the requests are processed synchronously and the PDF is generated in memory. A malicious user, or - truly - just your regular impatient user, can bring down the server by nervously clicking on the download button X times in a row 💥.</p>
</li>
<li class="">
<p>Additionally, large PDF generation (around 100 pages) can take between 2 to 5 minutes. In the age of doom-scrolling, 5 minutes are not acceptable for your average user. If they close their browser, we have successfully used server resources to generate a PDF that no one will receive.</p>
</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="embracing-asynchronicity">Embracing asynchronicity<a href="https://bitcrowd.dev/optimizing-pdf-render-time-in-rails#embracing-asynchronicity" class="hash-link" aria-label="Direct link to Embracing asynchronicity" title="Direct link to Embracing asynchronicity" translate="no">​</a></h2>
<p>To solve the first problem, we need to track that a user requested a PDF. This way, if they are late for their train and hit the button 10 times, we do <em>not</em> generate the PDF 10 times, just once. We introduce a new model in our database, that links a user with an <a href="https://guides.rubyonrails.org/active_storage_overview.html#attaching-files-to-records-has-one-attached" target="_blank" rel="noopener noreferrer" class="">Active Storage attachment</a>.</p>
<div class="language-ruby codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ruby 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">class</span><span class="token plain"> </span><span class="token class-name">PDFRequest</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> ApplicationRecord</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  belongs_to </span><span class="token symbol" style="color:#36acaa">:user</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  belongs_to </span><span class="token symbol" style="color:#36acaa">:resource</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic"># The resource is the model containing the rich-text content for the PDF</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  has_one_attached </span><span class="token symbol" style="color:#36acaa">:document</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token symbol" style="color:#36acaa">dependent</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token symbol" style="color:#36acaa">:purge</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">end</span><br></span></code></pre></div></div>
<p>The user is redirected to a page showing the status of the <code>PDFRequest</code> that was created, and a small JavaScript snippet refreshes the page regularly. Once an attachment is present on the <code>PDFRequest</code> record, the download starts. This naturally calls for an asynchronous processing of the request. Sidekiq jobs to the rescue!</p>
<p><img decoding="async" loading="lazy" alt="Asynchronous processing of the PDF download request with a Sidekiq job" src="https://bitcrowd.dev/assets/images/async-pdf-request-processing-2117a1622614861b8016a3c7cdddbf44.png" width="1698" height="861" class="img_ev3q"></p>
<p>This already has some benefits:</p>
<ol>
<li class="">We prevent crashing the server as the PDF rendering is done asynchronously (we control the amount of parallel jobs in the queue).</li>
<li class="">If a user closes their browser, then reopens it later and asks for the PDF again, we redirect them to the <strong>existing</strong> <code>PDFRequest</code>. No waiting time, the PDF was already generated and is ready to be downloaded.</li>
</ol>
<p>But the actual rendering of the PDF can still take some minutes. If many users are asking for a PDF, their waiting time can add up as they could be last in the job queue!</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="hash-it-cache-it">Hash it! Cache it!<a href="https://bitcrowd.dev/optimizing-pdf-render-time-in-rails#hash-it-cache-it" class="hash-link" aria-label="Direct link to Hash it! Cache it!" title="Direct link to Hash it! Cache it!" translate="no">​</a></h2>
<p>One thing we did not mention so far is a characteristic of the PDF files that we generate. This is specific to our client: they <em>can</em> look identical for two different users. If the upstream resource changes, the PDF should change as well. But as long as this resource does not change, meaning the rich-text content does not change, we could save some time re-using an existing PDF. As long as we <em>know</em> that it did not change!</p>
<p>A classic, standard way to do this is by hashing the file. But in our case, we do not take the risk of hashing the PDF itself, since we cannot be sure if that gives a deterministic output. Instead, we hash the HTML output which should be deterministic: we are pretty confident that the HTML generated from the same content should look identical.</p>
<p>Our Sidekiq job can also be updated to follow this flow:</p>
<p><img decoding="async" loading="lazy" alt="Checksum computed to hit the PDF cache, if any" src="https://bitcrowd.dev/assets/images/cache-pdf-and-checksum-ec19b1b6273e3b11c8cc97f1f7038af8.png" width="390" height="707" class="img_ev3q"></p>
<p>Hereʼs more gold: no need to store the HTML checksum in the database! The generated PDF can be stored in a cache, with the <strong>hash checksum as its key in the store</strong>. We can then rewrite the rendering code like so:</p>
<div class="language-ruby codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ruby 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">def</span><span class="token plain"> </span><span class="token method-definition function" style="color:#d73a49">render_pdf</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  html_checksum </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> Digest</span><span class="token double-colon punctuation" style="color:#393A34">::</span><span class="token constant" style="color:#36acaa">MD5</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">hexdigest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">html</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  Cache</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">file_cache</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">fetch</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string-literal string" style="color:#e3116c">"pdfs/</span><span class="token string-literal interpolation delimiter punctuation" style="color:#393A34">#{</span><span class="token string-literal interpolation content">resource</span><span class="token string-literal interpolation content punctuation" style="color:#393A34">.</span><span class="token string-literal interpolation content">id</span><span class="token string-literal interpolation delimiter punctuation" style="color:#393A34">}</span><span class="token string-literal string" style="color:#e3116c">/</span><span class="token string-literal interpolation delimiter punctuation" style="color:#393A34">#{</span><span class="token string-literal interpolation content">html_checksum</span><span class="token string-literal interpolation delimiter punctuation" style="color:#393A34">}</span><span class="token string-literal string" style="color:#e3116c">"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token symbol" style="color:#36acaa">expires_in</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 plain">day</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    FerrumPdf</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">render_pdf</span><span class="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 symbol" style="color:#36acaa">html</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> html</span><span class="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 symbol" style="color:#36acaa">pdf_options</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">        </span><span class="token symbol" style="color:#36acaa">format</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token symbol" style="color:#36acaa">:A4</span><span class="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 symbol" style="color:#36acaa">scale</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.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 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 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">end</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">end</span><br></span></code></pre></div></div>
<p>This has great benefits:</p>
<ol>
<li class="">The PDF does not need to be generated for each (user, resource) pair, but only once. The first user to request a PDF for the resource will wait, but everyone else after them will <em>not</em> (until something changes on the resource).</li>
<li class="">Using the hash approach preserves us from two other approaches:</li>
</ol>
<ul>
<li class="">Hook on any change made to the resource. While this could be a viable option, in our case the “resource” is actually a very complex entity relationship model and tracking changes made to associated models would be hell.</li>
<li class="">We are not fans of database triggers for this use case because they are rather invisible from the codebase perspective.</li>
</ul>
<ol start="3">
<li class="">Moreover: while these two other approaches operate on <strong>the content</strong>, the hash approach operates on the <strong>generated HTML</strong>. This gives us something extra: if we change the CSS, JavaScript, or the HTML layout (literally anything listed in the HTML output) used to render the PDF, <strong>the computed hash would change</strong>. In other words, we get a neat and clean cache invalidation: in case the content changes or in case the <em>looks</em> of the PDF changes, the cache would not be hit, and we would re-render a fresh PDF.</li>
</ol>
<p>Note that for this to work, you need to make sure the HTML output is deterministic. This means for example that you cannot have random IDs in the DOM.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wanna-see-me-run-to-that-mountain-and-back">Wanna see me run to that mountain and back?<sup><a href="https://bitcrowd.dev/optimizing-pdf-render-time-in-rails#user-content-fn-1-5e4e74" id="user-content-fnref-1-5e4e74" data-footnote-ref="true" aria-describedby="footnote-label" class="anchorTargetStickyNavbar_Vzrq">1</a></sup><a href="https://bitcrowd.dev/optimizing-pdf-render-time-in-rails#wanna-see-me-run-to-that-mountain-and-back" class="hash-link" aria-label="Direct link to wanna-see-me-run-to-that-mountain-and-back" title="Direct link to wanna-see-me-run-to-that-mountain-and-back" translate="no">​</a></h2>
<p>At this stage, the PDF rendering is already much better. It runs asynchronously and leverages the redundancy of identical HTML outputs. But we can do more. The first user to request a PDF for a resource that is not cached yet still needs to wait 2-5 minutes to get the PDF. In our case, the same sections can be rendered for different users across resources, so we could actually cache <em>parts</em> of the HTML.</p>
<p><a href="https://guides.rubyonrails.org/caching_with_rails.html#fragment-caching" target="_blank" rel="noopener noreferrer" class="">Rails Fragment Caching</a> to the rescue!</p>
<div class="language-haml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-haml 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">- resource.sections.each do |section|</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  - cache section do</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    = react_component 'RichContentViewer', props: { content: section.content }, id: "rich-content-#{section.id}"</span><br></span></code></pre></div></div>
<p>This is just given by Rails, and we can use it to shorten the rendering time when the PDF is re-generated.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://bitcrowd.dev/optimizing-pdf-render-time-in-rails#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>In this article we saw an iterative approach to optimize the rendering of large PDF files in Rails. First by adopting a queue system, then by caching the entire PDF files, and finally by using Rails fragment caching. The rendering time at our client has gone from 2 - 5 minutes down to some seconds! 🎉</p>
<!-- -->
<section data-footnotes="true" class="footnotes"><h2 class="anchor anchorTargetStickyNavbar_Vzrq sr-only" id="footnote-label">Footnotes<a href="https://bitcrowd.dev/optimizing-pdf-render-time-in-rails#footnote-label" class="hash-link" aria-label="Direct link to Footnotes" title="Direct link to Footnotes" translate="no">​</a></h2>
<ol>
<li class="anchorTargetStickyNavbar_Vzrq" id="user-content-fn-1-5e4e74">
<p><a href="https://youtube.com/shorts/dM9XnEsGb9Y" target="_blank" rel="noopener noreferrer" class="">Famous Spongebob words</a> <a href="https://bitcrowd.dev/optimizing-pdf-render-time-in-rails#user-content-fnref-1-5e4e74" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>]]></content:encoded>
            <category>ruby-on-rails</category>
            <category>pdf</category>
            <category>ferrum</category>
            <category>caching</category>
            <category>sidekiq</category>
        </item>
        <item>
            <title><![CDATA[Behind the Scenes: Replacing a Black-Box Billing Service]]></title>
            <link>https://bitcrowd.dev/replacing-a-black-box-billing-service</link>
            <guid>https://bitcrowd.dev/replacing-a-black-box-billing-service</guid>
            <pubDate>Tue, 17 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[In a previous blog post, we looked at the process of migrating 600k users to a new billing service for Steady Media. This was the final step of an internal project called “Charger”. Its aim was to replace the off-the-shelf payment platform they had started with, Chargebee, which had become a roadblock. In this post, we will reveal the behind-the-scenes insights into the process that made that final step a success.]]></description>
            <content:encoded><![CDATA[<p>In a <a class="" href="https://bitcrowd.dev/migrating-600k-users-to-new-billing-service">previous blog post</a>, we looked at the process of migrating 600k users to a new billing service for Steady Media. This was the final step of an internal project called <strong>“Charger”</strong>. Its aim was to replace the off-the-shelf payment platform they had started with, <a href="https://www.chargebee.com/" target="_blank" rel="noopener noreferrer" class="">Chargebee</a>, which had become a roadblock. In this post, we will reveal the behind-the-scenes insights into the process that made that final step a success.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-challenge">The Challenge<a href="https://bitcrowd.dev/replacing-a-black-box-billing-service#the-challenge" class="hash-link" aria-label="Direct link to The Challenge" title="Direct link to The Challenge" translate="no">​</a></h2>
<p>Steady Media needed to migrate 600,000 users from Chargebee to a new in-house billing system. The complexity was brutal:</p>
<ul>
<li class="">Chargebeeʼs billing logic is opaque - we had to reverse-engineer behavior through observation</li>
<li class="">Historical data had to work in the new system (canʼt rewrite the past, especially not payment transactions!)</li>
<li class="">Multiple payment gateways (Braintree, GoCardless, PayPal) with different quirks</li>
<li class="">Complex billing workflows: trial subscriptions, gift subscriptions, mid-term subscription upgrades, subscription cancellations (with or without refunds)</li>
<li class="">Complex support workflows: issuing refunds, invoice &amp; credit notes document versioning</li>
<li class="">Legal compliance (invoicing and taxes)</li>
<li class="">Migration had to happen without downtime or data loss</li>
</ul>
<p>Steadyʼs development team asked us to lead the Charger project, so that they could focus on their core application. The tech stack had to be compatible with their team skills: Elixir &amp; Phoenix!</p>
<div class="cta"><div class="container"><div class="row"><p>
🤩 Like what you see? have a look at <!-- --> <a href="https://bitcrowd.net/en/projects" class="plausible-event-name=Button+Click plausible-event-location=Post+Cta+afterIntroToBlackBox">the case studies on bitcrowd.net</a></p></div></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-journey">The Journey<a href="https://bitcrowd.dev/replacing-a-black-box-billing-service#the-journey" class="hash-link" aria-label="Direct link to The Journey" title="Direct link to The Journey" translate="no">​</a></h2>
<p>bitcrowd built Charger across three iterations:</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2021-core-system">2021: Core System<a href="https://bitcrowd.dev/replacing-a-black-box-billing-service#2021-core-system" class="hash-link" aria-label="Direct link to 2021: Core System" title="Direct link to 2021: Core System" translate="no">​</a></h3>
<p>The first iteration aimed at building the data model and implementing the external communication layer of Charger:</p>
<ul>
<li class="">API layer with authentication between Charger and the Media Makers app</li>
<li class="">Webhook notifications between Charger and the Media Makers app</li>
<li class="">Payment providers: integrated Braintree and GoCardless services (API &amp; webhooks)</li>
<li class="">Multiplexed calls from the Media Makers app to hit Chargebee OR Charger depending on where the user lived</li>
</ul>
<p>Once these various components were finalized, we could start the implementation of the core flows: create new subscriptions, bill users and trigger payments, upgrade subscription plans with prorated billing, settle payments, and so on.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2023-feature-parity">2023: Feature Parity<a href="https://bitcrowd.dev/replacing-a-black-box-billing-service#2023-feature-parity" class="hash-link" aria-label="Direct link to 2023: Feature Parity" title="Direct link to 2023: Feature Parity" translate="no">​</a></h3>
<p>After the first development cycle ended, new features were added to the Media Makers app that needed to be back-ported to Charger. We added new models and new flows to support trial and gift subscriptions. We took the opportunity to add Paypal as a third payment provider. Finally, we also tackled the generation and design of legal billing PDF documents.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2025-admin-ui-support-tools-and-migration">2025: Admin UI, Support Tools, and Migration<a href="https://bitcrowd.dev/replacing-a-black-box-billing-service#2025-admin-ui-support-tools-and-migration" class="hash-link" aria-label="Direct link to 2025: Admin UI, Support Tools, and Migration" title="Direct link to 2025: Admin UI, Support Tools, and Migration" translate="no">​</a></h3>
<p>A missing piece in Charger was a simple, lightweight, workable admin UI, that could be useful for Steadyʼs developers, QA engineers, support staff and financial experts. The goal was to minimize the implementation effort, while preserving a clean UX. We opted for <a href="https://daisyui.com/" target="_blank" rel="noopener noreferrer" class="">DaisyUI</a> since we knew the admin UI would use standard components like index tables, description lists, navigation elements etc.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="powerful-search">Powerful search<a href="https://bitcrowd.dev/replacing-a-black-box-billing-service#powerful-search" class="hash-link" aria-label="Direct link to Powerful search" title="Direct link to Powerful search" translate="no">​</a></h4>
<p>One strong requirement was to make sure that the index pages had a powerful and performant search as well as sorting and filtering mechanisms, as one of the pain points of Chargebee had been searches that time out once enough data was in the system. We solved this issue by paying attention to the queries' efficiency. Additionally, for cross-table filtering, we implemented a search-typeahead dropdown in order to avoid expensive joins.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="activity-logs">Activity logs<a href="https://bitcrowd.dev/replacing-a-black-box-billing-service#activity-logs" class="hash-link" aria-label="Direct link to Activity logs" title="Direct link to Activity logs" translate="no">​</a></h4>
<p>For such a billing product, it was crucial to allow admin users to see a clear changeset history on any resource. Charger comes with an auditing layer, that tracks changes and versions each relevant resource (like payments, invoices...). We built an abstraction on top of the auditing, that allows to plug any resource in it and view its version history with a UI resembling a Git diff.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="support-staff-tooling">Support staff tooling<a href="https://bitcrowd.dev/replacing-a-black-box-billing-service#support-staff-tooling" class="hash-link" aria-label="Direct link to Support staff tooling" title="Direct link to Support staff tooling" translate="no">​</a></h4>
<p>At that stage, we had a working product for developers, QA engineers and financial experts. But Steadyʼs support team still needed tailored features to enable them to solve specific situations. For example, when a userʼs credit card expired, and the system could not withdraw money from their account, the user might send the missing amount via a bank transfer. On Steadyʼs bank account, the balance is correct, but Charger does not know about this transaction, and the accounting balance is affected. Similarly, the support team might refund a user via a manual bank transfer. To keep the accounting in check, we implemented a solution to record offline refund &amp; payment, as well as various manual actions &amp; flows to fix any situation that would not auto-heal.</p>
<div class="cta"><div class="container"><div class="row"><p>
💡 Have we got you interested? If you have a question or topic you would like to discuss with us, we would like to hear from you.<!-- --> <a href="https://cal.eu/bitcrowd/30min" class="plausible-event-name=Button+Click plausible-event-location=Post+Cta+book_A_Call_In_Blog">Book a free call with us</a></p></div></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="outcome">Outcome<a href="https://bitcrowd.dev/replacing-a-black-box-billing-service#outcome" class="hash-link" aria-label="Direct link to Outcome" title="Direct link to Outcome" translate="no">​</a></h2>
<p>We migrated all 600,000 users to Charger. The lights stayed green. Steady now controls their billing infrastructure and its support team can handle edge cases and requests through the admin UI. The developer team can also leverage the activity logs and powerful search capabilities to debug or resolve errors.</p>
<p>bitcrowd led on self-contained packages: we took topics Steadyʼs team didnʼt have capacity for and delivered them finished. We were able to coordinate with stakeholders (support team, finance, legal) to turn “we need X” into actionable tickets. We applied our standards of excellent documentation and test coverage, high code quality and review, which ensured a smooth handover with Steadyʼs team.</p>]]></content:encoded>
            <category>elixir</category>
            <category>phoenix</category>
            <category>billing</category>
            <category>payment</category>
        </item>
        <item>
            <title><![CDATA[Migrating 600k users to a new billing service]]></title>
            <link>https://bitcrowd.dev/migrating-600k-users-to-new-billing-service</link>
            <guid>https://bitcrowd.dev/migrating-600k-users-to-new-billing-service</guid>
            <pubDate>Thu, 12 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[In August 2025, we helped our friends at Steady to migrate their 600,000+ users to their own in-house billing system. This was the final step of an internal project called “Charger”. Its aim was to replace the off-the-shelf payment platform they had started with, Chargebee, which had become a roadblock. The work on Charger began already in 2021 in workshops with the Steady team. Together with them, we designed, planned and finally built the internal product that would take over for years later. The process and the implementation happened in multiple iterations over a span of four years.]]></description>
            <content:encoded><![CDATA[<p>In August 2025, we helped our friends at <strong><a href="https://steady.page/en/" target="_blank" rel="noopener noreferrer" class="">Steady</a></strong> to migrate their 600,000+ users to their own <strong>in-house billing system</strong>. This was the final step of an internal project called <strong>“Charger”</strong>. Its aim was to replace the off-the-shelf payment platform they had started with, Chargebee, which had become a roadblock. The work on Charger began already in 2021 in workshops with the Steady team. Together with them, we designed, planned and finally built the internal product that would take over for years later. The process and the implementation happened in multiple iterations over a span of four years.</p>
<p>This blog post explores the strategy that was used to achieve this challenging task.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-backstory">The backstory<a href="https://bitcrowd.dev/migrating-600k-users-to-new-billing-service#the-backstory" class="hash-link" aria-label="Direct link to The backstory" title="Direct link to The backstory" translate="no">​</a></h2>
<p>Steady supports Media Makers in building an audience and driving revenue via subscriptions to newsletters, blogs etc. While most of the tools and features for Media Makers live in their main application, they outsourced the subscriptions &amp; billing management to a third-party service (<a href="https://www.chargebee.com/" target="_blank" rel="noopener noreferrer" class="">Chargebee</a>). The Media Makers app was very tightly coupled to Chargebee as it relied on it for core business logic within the app, as well as for accounting.</p>
<p><img decoding="async" loading="lazy" alt="Graph illustrating the relationship between Steady Media Makers app and Chargebee via an API Layer" src="https://bitcrowd.dev/assets/images/steady-and-chargebee-718dd3eb9e01cb110b9e71ccd42807ec.png" width="1349" height="618" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="introducing-charger-the-fraternal-twin">Introducing: Charger, the fraternal twin<a href="https://bitcrowd.dev/migrating-600k-users-to-new-billing-service#introducing-charger-the-fraternal-twin" class="hash-link" aria-label="Direct link to Introducing: Charger, the fraternal twin" title="Direct link to Introducing: Charger, the fraternal twin" translate="no">​</a></h2>
<p>Although Chargebee was useful initially, it became increasingly painful to work around their standard processes over time. This ultimately led to the development of a custom billing tool.</p>
<p>For this we had to reverse-engineer all of what Chargebee was doing. We started by building the required schemas, and exposing the API endpoints and webhook notifications that were needed by the Media Makers app. Any changes in the Media Makers app usage of the API/Webhooks, or to Chargebee itself, needed to be propagated to Charger. <strong>Both billing systems had to behave identical from the outside.</strong></p>
<p>The Media Makers app now needed to forward calls to Charger and/or Chargebee. A “multiplexer“ abstraction close to the API &amp; Webhook layer was added, so that for developers &amp; users of the app, the fact that Charger or Chargebee was used for the calls would be transparent.</p>
<p><img decoding="async" loading="lazy" alt="Graph illustrating the new billing system Charger, exposing the same API as Chargebee" src="https://bitcrowd.dev/assets/images/introducing-charger-d2bc480de0952bd2d36fbd93bcc61c92.png" width="1213" height="513" class="img_ev3q"></p>
<p>Charger needed to process payments and refunds, with all payment providers supported by the Media Makers app. The first flow of the application was created: subscribe to a publication, generate an invoice, create a payment, and settle that payment 🎉!</p>
<div class="cta"><div class="container"><div class="row"><p>
🔍 Would you like more detail? We have a 'behind the scenes' blog post about Steady! <!-- --> <a href="https://bitcrowd.dev/replacing-a-black-box-billing-service/" class="plausible-event-name=Button+Click plausible-event-location=Post+Cta+afterIntroToBlackBox">Have a Look!</a></p></div></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-bird-watching-experience-">A bird watching experience 🔭<a href="https://bitcrowd.dev/migrating-600k-users-to-new-billing-service#a-bird-watching-experience-" class="hash-link" aria-label="Direct link to A bird watching experience 🔭" title="Direct link to A bird watching experience 🔭" translate="no">​</a></h2>
<p>At this stage, no real users were migrated to Charger. No real users were even created on Charger! The system just worked™... Subscription statuses, invoices, payments processing being crucial to the system meant that we had to come up with a transition plan, that allowed the developers to observe and watch the behaviour of the system for just a handful of users. If any problem arose, it would need to affect only such a small amount of users that their support team could resolve it manually. We handpicked 10 users with rather simple data: a single recent subscription, no cancellations, and various payment providers / currencies to cover as much ground as possible.</p>
<p><img decoding="async" loading="lazy" alt="Graph illustrating the migration job between the Media Makers app and Charger" src="https://bitcrowd.dev/assets/images/migrating-to-charger-c698090ddb6d60b2e507132ca9323214.png" width="1213" height="513" class="img_ev3q"></p>
<p>Using our beloved <a href="https://hexdocs.pm/oban/Oban.html" target="_blank" rel="noopener noreferrer" class="">Oban</a> for job processing, we built a migration job, that would, for a given user:</p>
<ol>
<li class="">Fetch all their data from Chargebee</li>
<li class="">Format it to please Chargerʼs data model</li>
<li class="">Send it to a dedicated endpoint in Charger</li>
<li class="">Charger creates all the given data for the user (subscriptions, invoices, payments, etc.)</li>
<li class="">Cancel the user subscriptions on Chargebee (so that they donʼt get billed twice!)</li>
</ol>
<p>Of course, <strong>all of this must happen in a transaction on both systems</strong>, fail gracefully and report meaningful errors to the devs. We leveraged Oban configuration to optimise the parallelisation and retry mechanism of the jobs, as we could be rate-limited by Chargebee. At some point, we would want to migrate users in batches of 100-10k users at a time, so the migration mechanism had to be robust and auto-healing.</p>
<div class="cta"><div class="container"><div class="row"><p>
💡 Have we got you interested? If you have a question or topic you would like to discuss with us, we would like to hear from you.<!-- --> <a href="https://cal.eu/bitcrowd/30min" class="plausible-event-name=Button+Click plausible-event-location=Post+Cta+book_A_Call_In_Blog">Book a free call with us</a></p></div></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="silent-bugs">Silent bugs<a href="https://bitcrowd.dev/migrating-600k-users-to-new-billing-service#silent-bugs" class="hash-link" aria-label="Direct link to Silent bugs" title="Direct link to Silent bugs" translate="no">​</a></h2>
<p>A main challenge in this process was to validate the integrity of the migrated data. The Media Makers app and Charger run on different databases, and have their own representation of what is an invoice, a subscription, a user, etc. Forgetting to migrate a field, or mapping the wrong timestamp from one system to the other, would happen silently because a part of the migration code lives in the Media Makers app, and the other part lives in Charger. The data could be silently missing or corrupted, and we would only realise down the road that all users were migrated with errors. Luckily, we had the chance of having access to a Chargebee staging instance with users in all kinds of configuration. This helped us a lot in finding migration issues by comparing the user data on both systems before and after the migration, and strengthening our tests to cover all the edge cases we could find. The developers at Steady also added <a href="https://www.erlang.org/doc/apps/kernel/erpc.html" target="_blank" rel="noopener noreferrer" class="">ERPC</a>, which enabled us to run integration tests evaluating the effects of Charger on the Media Makers app.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://bitcrowd.dev/migrating-600k-users-to-new-billing-service#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>Once we were confident that our 10 users were rolling, we migrated 10 more users with more complex data. And so on, for weeks, increasing the batch size, and deciding to create <strong>new</strong> users on Charger with a <code>rand()</code> choice. This allowed Steady to steer the future of their billing strategy for their users, integrating with other payment providers and simplifying some of the user flows for refunds etc. While it looks simple on paper, this project required a lot of patience, collaboration and planning ahead as the stakes were really high and we had to build up confidence in the new system and in the migration approach.</p>]]></content:encoded>
            <category>elixir</category>
            <category>oban</category>
            <category>erpc</category>
        </item>
        <item>
            <title><![CDATA[Getting React on Rails to work with Turbo Streams]]></title>
            <link>https://bitcrowd.dev/getting-react-on-rails-to-work-with-turbo-streams</link>
            <guid>https://bitcrowd.dev/getting-react-on-rails-to-work-with-turbo-streams</guid>
            <pubDate>Mon, 12 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[On a recent client project, redesigning a large Rails app, we found ourselves having to get Rails, Hotwire's Turbo and React on Rails to play together nicely. Each of the tools comes with its own amounts of magic™, so integrating them turned out to be a bit of a juggle.]]></description>
            <content:encoded><![CDATA[<p>On a recent client project, redesigning a large <a href="https://rubyonrails.org/" target="_blank" rel="noopener noreferrer" class="">Rails</a> app, we found ourselves having to get Rails, Hotwire's <a href="https://turbo.hotwired.dev/" target="_blank" rel="noopener noreferrer" class="">Turbo</a> and <a href="https://www.shakacode.com/react-on-rails/docs/" target="_blank" rel="noopener noreferrer" class="">React on Rails</a> to play together nicely. Each of the tools comes with its own amounts of <em>magic™</em>, so integrating them turned out to be a bit of a juggle.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="scenario">Scenario<a href="https://bitcrowd.dev/getting-react-on-rails-to-work-with-turbo-streams#scenario" class="hash-link" aria-label="Direct link to Scenario" title="Direct link to Scenario" translate="no">​</a></h2>
<p>On this project, we were implementing the new design for a particular page. At it's core, it shows two lists, each with a "Load More" button to asynchronously fetch and append more items to it. The first list is just a stack of items, the second one also includes a grouping of the items. Imagine it roughly like this:</p>
<p><img decoding="async" loading="lazy" alt="Mockup of a page with two lists, List A and List B, each with a &amp;quot;Load More -&amp;gt;&amp;quot; button below. List B is divided into groups Group A, Group B and Group C" src="https://bitcrowd.dev/assets/images/example-page-e5f1a06384439996533c1d24682cd7df.png" width="1601" height="2252" class="img_ev3q"></p>
<p>In "List A", clicking "Load More" should append another batch of 5 items to the bottom of the list until the bitter end. For "List B", clicking "Load More" should also append another batch of 5 items, where some may still belong to "Group C" and others will form new groups, each with their respective group header. The list items are heterogeneous and contain complex markup with graphics, etc.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pagination">Pagination<a href="https://bitcrowd.dev/getting-react-on-rails-to-work-with-turbo-streams#pagination" class="hash-link" aria-label="Direct link to Pagination" title="Direct link to Pagination" translate="no">​</a></h2>
<p>We decided to implement the pagination logic using <a href="https://turbo.hotwired.dev/reference/streams" target="_blank" rel="noopener noreferrer" class="">Turbo Streams</a>, as they seem to be a good fit for our use case. They also integrate nicely into the Rails ecosystem without being too tightly coupled (e.g. to a specific version of Rails). From Rails 7 on, Turbo also replaced Rails' UJS (Unobtrusive JavaScript) functionality and is the recommended tool for sprinkling asynchrony and SPA (Single Page Application)-feelings. From the Turbo Stream docs:</p>
<blockquote>
<p>Turbo Streams deliver page changes as fragments of HTML wrapped in <code>&lt;turbo-stream&gt;</code> elements. Each stream element specifies an action together with a target ID to declare what should happen to the HTML inside it. These elements can be delivered to the browser synchronously as a classic HTTP response, or asynchronously […]</p>
</blockquote>
<p>And further on:</p>
<blockquote>
<p>They can be used to surgically update the DOM after a user action such as removing an element from a list without reloading the whole page […]</p>
</blockquote>
<p>This sounds like a match for what we are doing!</p>
<p>By having our server respond with <code>turbo_stream</code> templates for the pagination requests, we are able to use the same <a href="https://viewcomponent.org/" target="_blank" rel="noopener noreferrer" class="">View Components</a> and partials we already have in place from the initial server-rendered page. This is important to us because we're already using our component library throughout the whole project. Reimplementing the same components in a different framework or language just for this single page would be unnecessary duplication, not to mention the constant risk of the two implementations diverging.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="example">Example<a href="https://bitcrowd.dev/getting-react-on-rails-to-work-with-turbo-streams#example" class="hash-link" aria-label="Direct link to Example" title="Direct link to Example" translate="no">​</a></h3>
<p>For the sake of simplicity, let's focus on the first list, "List A". The initially rendered page would have a markup like this for the list:</p>
<div class="language-HTML language-html codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-html codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">h2</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain">List A</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">h2</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">ul</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">id</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">list-a</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">li</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">abc-item</span><span class="token tag punctuation" style="color:#393A34">/&gt;</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">li</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">li</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">abc-item</span><span class="token tag punctuation" style="color:#393A34">/&gt;</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">li</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">li</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">abc-item</span><span class="token tag punctuation" style="color:#393A34">/&gt;</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">li</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">li</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">abc-item</span><span class="token tag punctuation" style="color:#393A34">/&gt;</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">li</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">li</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">abc-item</span><span class="token tag punctuation" style="color:#393A34">/&gt;</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">li</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">ul</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">turbo-frame</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">id</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">load-more-a</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">load-more-button</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">turbo-frame</span><span class="token tag punctuation" 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 comment" style="color:#999988;font-style:italic">&lt;!-- ... --&gt;</span><br></span></code></pre></div></div>
<p>When the user clicks the "Load More" button, we want to retrieve the next batch of items, append them to the list and replace the button with one to request the next batch. If there is no next batch of items, we remove the "Load More button.</p>
<p>In the controller taking care of the pagination request, we can render a response template of the format <code>turbo_stream</code>. A simplified version could look like this:</p>
<div class="language-html codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-html 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">&lt;%= turbo_stream.append 'list-a' do %&gt; &lt;%= render</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">AbcItem.with_collection(@items, as: :item) %&gt; &lt;% end %&gt; &lt;% if @pagy.next %&gt; &lt;%=</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">turbo_stream.replace 'load-more-a', method: :morph do %&gt;</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" style="color:#00009f">turbo-frame</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">id</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">load-more-a</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  &lt;%= render LoadMoreButton.new(path: '/', params: { page: @pagy.next }) %&gt;</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" style="color:#00009f">turbo-frame</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">&lt;% end %&gt; &lt;% else %&gt; &lt;%= turbo_stream.remove 'load-more-a' %&gt; &lt;% end %&gt;</span><br></span></code></pre></div></div>
<p>Note that the example is View Components and the <a href="https://github.com/ddnexus/pagy" target="_blank" rel="noopener noreferrer" class="">pagy</a> pagination library. In the template, we're appending items to "List A" and replace the "Load More" button with a new one. If there aren't any items left the "Load More" button gets removed.</p>
<p>The response from the server would then look roughly like this:</p>
<div class="language-HTML language-html codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-html codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">turbo-stream</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">action</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">append</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">target</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">list-a</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">template</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">li</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">abc-item</span><span class="token tag punctuation" style="color:#393A34">/&gt;</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">li</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">template</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">turbo-stream</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">turbo-stream</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">action</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">remove</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">target</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">load-more-a</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">turbo-stream</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><br></span></code></pre></div></div>
<p>It appends one item to the list and removes the button to load more.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="problem">Problem<a href="https://bitcrowd.dev/getting-react-on-rails-to-work-with-turbo-streams#problem" class="hash-link" aria-label="Direct link to Problem" title="Direct link to Problem" translate="no">​</a></h2>
<p>Our <code>&lt;abc-item/&gt;</code> element contains a React component, <code>MyComponent</code> in its markup, rendered via React on Rails' <code>react_component</code> helper. In the final HTML those React components appear as three elements each. Two script tags with context and props and one wrapper element for the rendered HTML of the component:</p>
<div class="language-html codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-html codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">script</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">type</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">application/json</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">id</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">js-react-on-rails-context</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#393A34"><span class="token script language-javascript">  …</span><br></span><span class="token-line" style="color:#393A34"><span class="token script language-javascript"></span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">script</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">div</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">id</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">MyComponent-react-component-eaca5b56-0732-4674-b9da-f858c224d410</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" 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 comment" style="color:#999988;font-style:italic">&lt;!-- MyComponent markup --&gt;</span><span class="token plain"></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" style="color:#00009f">div</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">script</span><span class="token tag" style="color:#00009f"></span><br></span><span class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">  </span><span class="token tag attr-name" style="color:#00a4db">type</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">application/json</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"></span><br></span><span class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">  </span><span class="token tag attr-name" style="color:#00a4db">class</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">js-react-on-rails-component</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"></span><br></span><span class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">  </span><span class="token tag attr-name" style="color:#00a4db">data-component-name</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">MyComponent</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"></span><br></span><span class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">  </span><span class="token tag attr-name" style="color:#00a4db">data-trace</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">true</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"></span><br></span><span class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">  </span><span class="token tag attr-name" style="color:#00a4db">data-dom-id</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">MyComponent-react-component-eaca5b56-0732-4674-b9da-f858c224d410</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"></span><br></span><span class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f"></span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#393A34"><span class="token script language-javascript">  …</span><br></span><span class="token-line" style="color:#393A34"><span class="token script language-javascript"></span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">script</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><br></span></code></pre></div></div>
<p>For any <code>&lt;abc-item/&gt;</code> element added to the page via Turbo Streams, those three elements are also added to the DOM, but the wrapping <code>&lt;div&gt;</code> stayed empty: <strong>the component wasn't actually mounted!</strong> 🙀</p>
<div class="language-html codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-html codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">script</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">type</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">application/json</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">id</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">js-react-on-rails-context</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#393A34"><span class="token script language-javascript">  …</span><br></span><span class="token-line" style="color:#393A34"><span class="token script language-javascript"></span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">script</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">div</span><span class="token tag" style="color:#00009f"></span><br></span><span class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">  </span><span class="token tag attr-name" style="color:#00a4db">id</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">MyComponent-react-component-eaca5b56-0732-4674-b9da-f858c224d410</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"></span><br></span><span class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f"></span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">div</span><span class="token tag punctuation" 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 tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">script</span><span class="token tag" style="color:#00009f"></span><br></span><span class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">  </span><span class="token tag attr-name" style="color:#00a4db">type</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">application/json</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"></span><br></span><span class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">  </span><span class="token tag attr-name" style="color:#00a4db">class</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">js-react-on-rails-component</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"></span><br></span><span class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">  </span><span class="token tag attr-name" style="color:#00a4db">data-component-name</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">MyComponent</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"></span><br></span><span class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">  </span><span class="token tag attr-name" style="color:#00a4db">data-trace</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">true</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"></span><br></span><span class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">  </span><span class="token tag attr-name" style="color:#00a4db">data-dom-id</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">MyComponent-react-component-eaca5b56-0732-4674-b9da-f858c224d410</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"></span><br></span><span class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f"></span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#393A34"><span class="token script language-javascript">  …</span><br></span><span class="token-line" style="color:#393A34"><span class="token script language-javascript"></span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">script</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><br></span></code></pre></div></div>
<p>Turbo is injecting markup into the already fully rendered page and none of the DOM events React on Rails is hooking into to execute the JavaScript to mount React components on the page are being fired.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="options">Options<a href="https://bitcrowd.dev/getting-react-on-rails-to-work-with-turbo-streams#options" class="hash-link" aria-label="Direct link to Options" title="Direct link to Options" translate="no">​</a></h3>
<p>React on Rails already has <a href="https://www.shakacode.com/react-on-rails/docs/building-features/turbolinks/#turbo-streams-requires-react-on-rails-pro" target="_blank" rel="noopener noreferrer" class="">support for Turbo Streams</a>, but only as part of their paid <em>pro</em> plan. Going "pro" wasn't an option for the project as they are on a tight budget and only using small parts of React on Rails in isolated areas of their app. So we're not really qualifying for "pro" here…</p>
<p>Server-side rendering the React components would also have been an option to tackle the problem. But this again was out of scope for our project and we did not want to run JavaScript on the server.</p>
<p>So what now?</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="js-to-the-rescue">JS to the rescue<a href="https://bitcrowd.dev/getting-react-on-rails-to-work-with-turbo-streams#js-to-the-rescue" class="hash-link" aria-label="Direct link to JS to the rescue" title="Direct link to JS to the rescue" translate="no">​</a></h2>
<p>With the help of some AI coding agents, we explored the internals of the <a href="https://www.npmjs.com/package/react-on-rails" target="_blank" rel="noopener noreferrer" class=""><code>react-on-rails</code></a> and <a href="https://www.npmjs.com/package/@hotwired/turbo-rails" target="_blank" rel="noopener noreferrer" class="">@hotwired/turbo-rails</a> NPM package to see what was actually happening. The rough idea was that, if any events were emitted by Turbo when Stream elements were injected and rendered, we could hook into them and then manually call React on Rails to mount all the newly inserted components on the page.</p>
<p>In <code>Turbo</code>, we found the <a href="https://github.com/hotwired/turbo/blob/d069101e701f84369689d8fbd521d7a13ca971a0/src/elements/stream_element.js" target="_blank" rel="noopener noreferrer" class=""><code>StreamElement</code></a>. It has a <code>render</code> function and a <code>target</code> pointing to the ID of the DOM-element it's rendered to. Combining these two things, the plan was to extend the <code>render</code> function to also take care of mounting any React elements within the updated target element. This means, when the <code>StreamElement</code> contained any React components, we want to make sure they are correctly mounted <strong>after</strong> the rendering of the <code>StreamElement</code> itself was done.</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic">/**</span><br></span><span class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * Gets the current target (an element ID) to which the result will</span><br></span><span class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * be rendered.</span><br></span><span class="token-line" style="color:#393A34"><span class="token doc-comment 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">get</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">target</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">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getAttribute</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"target"</span><span class="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>So we wrote our own tiny JS module to patch Turbo with some additional logic for our React on Rails integration:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js 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">// First we get hold of the original render function of Turbo's StreamElement</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"> originalRender </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token maybe-class-name">Turbo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token class-name">StreamElement</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">prototype</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">render</span><span class="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">// Then we override it to extend it with our own additional logic:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token maybe-class-name">Turbo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token class-name">StreamElement</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">prototype</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">render</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">function</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">  </span><span class="token comment" style="color:#999988;font-style:italic">// First we call the original render function</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  originalRender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">call</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">this</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">// Then we get hold of the target element</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"> target </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">document</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getElementById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getAttribute</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"target"</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">// Then we wait for the next frame, to make sure that the DOM is fully loaded</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">// and we call a function to have React on Rails mount all components within</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">// the newly inserted DOM snippet.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">target</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 function" style="color:#d73a49">requestAnimationFrame</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 arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">mountReactOnRailsComponents</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">target</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 punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>The custom <code>mountReactOnRailsComponents</code> function is pretty much a manual re-implementation of what's happening when React on Rails is client-side rendering components:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js 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">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">mountReactOnRailsComponents</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">domNode</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">  domNode</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">// First we get all React on Rails components within our node</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 method function property-access" style="color:#d73a49">querySelectorAll</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"script.js-react-on-rails-component"</span><span class="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 method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">script</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</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">// Next we get the name of the component and the DOM ID of the placeholder</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"> name </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> script</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dataset</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">componentName</span><span class="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 punctuation" style="color:#393A34">{</span><span class="token plain"> domId </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"> script</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dataset</span><span class="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"> shouldHydrate </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> script</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dataset</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">hydrate</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">"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" 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">// Then we put together the component props</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">let</span><span class="token plain"> props </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 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 control-flow" style="color:#00009f">try</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">        props </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">parse</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">script</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">textContent</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">"{}"</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><span class="token keyword control-flow" style="color:#00009f">catch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">error</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 console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ReactOnRails props JSON parse error for"</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"> error</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 control-flow" style="color:#00009f">return</span><span class="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">// Now we need the placeholder element</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"> placeholder </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">document</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getElementById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">domId</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 control-flow" style="color:#00009f">if</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 plain">placeholder</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="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">// We check whether the component was already mounted</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">// in this case we don't want to re-render it.</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"> hasMounted </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> placeholder</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dataset</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">reactOnRailsMounted</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">"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 keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">hasMounted</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="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">// Then we call React on Rails' render function to mount the component</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token maybe-class-name">ReactOnRails</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">render</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"> props</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> domId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> shouldHydrate</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">// Finally, we mark the placeholder as mounted, so that we don't accidentally</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">// re-render it.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      placeholder</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dataset</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">reactOnRailsMounted</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">"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 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>With this in place, our Turbo Streams based pagination works like charm. Any React component newly added to the DOM is mounted as normal 🎉.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://bitcrowd.dev/getting-react-on-rails-to-work-with-turbo-streams#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>Drilling around like this in the internals of Turbo and React on Rails of course yields its risks. Now we're maintaining additional code which may break any time we update our Turbo or React on Rails dependences. But as we're only using the module in a small isolated area of the application, it's a risk we can accept for now. Our test coverage of the feature will inform us early enough.</p>
<p>Using open source technologies, we have the chance to peak under the hood of the tools and can if necessary adapt them to our needs. And working with legacy codebases and legacy Rails apps, their may be situations with unconventional needs…</p>
<p>We hope this post was helpful and inspires you to explore the details of the tools you're using the next time you hit a wall with them. Migrating to an entirely different tool is often a way bigger operation, especially in large monoliths.</p>]]></content:encoded>
            <category>redesign</category>
            <category>react</category>
            <category>ruby-on-rails</category>
            <category>turbo</category>
        </item>
        <item>
            <title><![CDATA[Loading Chrome extensions for development in 2025]]></title>
            <link>https://bitcrowd.dev/loading-chrome-extensions-for-development-in-2025</link>
            <guid>https://bitcrowd.dev/loading-chrome-extensions-for-development-in-2025</guid>
            <pubDate>Tue, 02 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[For loading a development build of our open source browser extension Tickety-Tick into Google Chrome during local development, we used to use Chrome's handy --load-extension flag. It allowed to load a local unpacked build of the extension via the command line. As this feature was recently removed from all branded builds of Chrome, we needed to update our workflow to use the-new-way™ - "remote debugging pipes".]]></description>
            <content:encoded><![CDATA[<p>For loading a development build of our open source browser extension <a href="https://github.com/bitcrowd/tickety-tick" target="_blank" rel="noopener noreferrer" class="">Tickety-Tick</a> into Google Chrome during local development, we used to use Chrome's handy <code>--load-extension</code> flag. It allowed to load a local unpacked build of the extension via the command line. As this feature was <a href="https://developer.chrome.com/blog/extension-news-june-2025#removing_the_--load-extension_flag" target="_blank" rel="noopener noreferrer" class="">recently removed</a> from all branded builds of Chrome, we needed to update our workflow to use the-new-way™ - "remote debugging pipes".</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="background">Background<a href="https://bitcrowd.dev/loading-chrome-extensions-for-development-in-2025#background" class="hash-link" aria-label="Direct link to Background" title="Direct link to Background" translate="no">​</a></h2>
<p>Tickety-Tick is on the nichier side of tech within our fleet of open source projects: it's a browser extension for establishing conventions on naming branches and writing commit messages in Git. And while we're slightly leaning towards team Firefox among us developers, Tickety-Tick is of course also supporting Google Chrome (and others…). For this, we as developers have to be able to run a local build of the extension in Chrome during development.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-old-way">The old way<a href="https://bitcrowd.dev/loading-chrome-extensions-for-development-in-2025#the-old-way" class="hash-link" aria-label="Direct link to The old way" title="Direct link to The old way" translate="no">​</a></h2>
<p>For local development, we had a <code>yarn open:chrome</code> command which would open Chrome with the current local build of the extension already installed, ready to click around. Under the hood, we utilized Google's <a href="https://github.com/GoogleChrome/chrome-launcher" target="_blank" rel="noopener noreferrer" class="">chrome-launcher</a> package to start a fresh, unpersonalized instance of Chrome for us. The code looked about like this:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js 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"> chromeFlags </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token maybe-class-name">ChromeLauncher</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">Launcher</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">defaultFlags</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 method function property-access" style="color:#d73a49">filter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">flag</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> flag </span><span class="token operator" style="color:#393A34">!==</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"--disable-extensions"</span><span class="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 method function property-access" style="color:#d73a49">concat</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"--no-default-browser-check"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"--load-extension=/dist/chrome"</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 maybe-class-name">ChromeLauncher</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">launch</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">chromeFlags</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 method function property-access" style="color:#d73a49">then</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">chrome</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Chrome running"</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>The important part is the <code>--load-extension</code> flag which points to the local Chrome build of the extension.</p>
<p>Some day, this just stopped working. Chrome would start as usual, but without the browser extension loaded.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-happened">What happened?<a href="https://bitcrowd.dev/loading-chrome-extensions-for-development-in-2025#what-happened" class="hash-link" aria-label="Direct link to What happened?" title="Direct link to What happened?" translate="no">​</a></h2>
<p>Some debugging and internet research brought us to <a href="https://groups.google.com/a/chromium.org/g/chromium-extensions/c/aEHdhDZ-V0E/m/UWP4-k32AgAJ" target="_blank" rel="noopener noreferrer" class="">this post</a> by the Chrome team from March 2025:</p>
<blockquote>
<p>RFC: Removing the <code>--load-extension</code> flag in branded Chrome builds</p>
</blockquote>
<p>Turns out the <code>--load-extension</code> flag was commonly abused by malicious parties and the Chrome team was looking into ways to further restrict this. It lead them to removing this feature from all branded Chrome builds (so Chromium would still have it…) and instead promote other means for loading extensions for testing and development.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-now">What now?<a href="https://bitcrowd.dev/loading-chrome-extensions-for-development-in-2025#what-now" class="hash-link" aria-label="Direct link to What now?" title="Direct link to What now?" translate="no">​</a></h2>
<p>To figure out the new™ way to load an unpacked, locally build extension in Chrome, we just naively searched for the <code>--load-extension</code> flag on GitHub, hoping to find hints how other people updated their code. A promising hit was in the <code>chrome-launcher</code> package itself. If somebody knew how to do it, they should be the ones. The searching pointed us to a really helpful code comment in their tests:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js 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">// Note: --load-extension in chromeFlags used to be the primary method of</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">// loading extensions, but this is removed from official stable Chrome builds</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">// starting from Chrome 137. This shows the officially supported way to load</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">// extensions, with --remote-debugging-pipe.</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">// See: "Removing the `--load-extension` flag in branded Chrome builds"</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">// https://groups.google.com/a/chromium.org/g/chromium-extensions/c/aEHdhDZ-V0E/m/UWP4-k32AgAJ</span><br></span></code></pre></div></div>
<p>As this was in a <code>load-extension-test.ts</code> file, we figured we'd just try to adopt what they are doing in their test setup for our <code>open:chrome</code> script. And as the comment indicates, we need to utilize the <code>--remote-debugging-pipe</code> flag, something we already knew from <a href="https://github.com/bitcrowd/chromic_pdf" target="_blank" rel="noopener noreferrer" class="">ChromicPDF</a> our PDF rendering library for Elixir based on headless Chrome.</p>
<p>Following pretty much exactly what the <code>loadExtension via remote-debugging-pipe</code> test was doing, we ended up with an updated <code>open-in-chrome</code> script which would:</p>
<ol>
<li class="">
<p>Start Chrome with the respective flags</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js 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"> chromeFlags </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> launcher</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">Launcher</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">defaultFlags</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 method function property-access" style="color:#d73a49">filter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">flag</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> flag </span><span class="token operator" style="color:#393A34">!==</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"--disable-extensions"</span><span class="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 method function property-access" style="color:#d73a49">concat</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 string" style="color:#e3116c">"--remote-debugging-pipe"</span><span class="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">"--enable-unsafe-extension-debugging"</span><span class="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">"--no-first-run"</span><span class="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">"--no-default-browser-check"</span><span class="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 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">const</span><span class="token plain"> options </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">  chromeFlags</span><span class="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 literal-property property" style="color:#36acaa">ignoreDefaultFlags</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">  </span><span class="token literal-property property" style="color:#36acaa">startingUrl</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> url</span><span class="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">const</span><span class="token plain"> chrome </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> launcher</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">launch</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">options</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
</li>
<li class="">
<p>Get hold of the debugging pipes</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js 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"> pipes </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> chrome</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">remoteDebuggingPipes</span><span class="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 control-flow" style="color:#00009f">if</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 plain">pipes</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 control-flow" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Chrome did not expose remoteDebuggingPipes"</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>
</li>
<li class="">
<p>Put together the command to load the extension</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js 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"> requestId </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">Math</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">floor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">Math</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">random</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 number" style="color:#36acaa">1e6</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"> request </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">  </span><span class="token literal-property property" style="color:#36acaa">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> requestId</span><span class="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 literal-property property" style="color:#36acaa">method</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Extensions.loadUnpacked"</span><span class="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 literal-property property" style="color:#36acaa">params</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><span class="token literal-property property" style="color:#36acaa">path</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> dir </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><br></span></code></pre></div></div>
</li>
<li class="">
<p>Send the request and listen on the pipes</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js 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"> firstResponse </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Promise</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">resolve</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> reject</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</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">let</span><span class="token plain"> buffer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">""</span><span class="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">  pipes</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">incoming</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">on</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"error"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> reject</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">  pipes</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">incoming</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">on</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"close"</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 plain"> </span><span class="token arrow 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 function" style="color:#d73a49">reject</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Pipe closed before response"</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 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">  pipes</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">incoming</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">on</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"data"</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 parameter">chunk</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</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">    buffer </span><span class="token operator" style="color:#393A34">+=</span><span class="token plain"> chunk</span><span class="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">let</span><span class="token plain"> end</span><span class="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 control-flow" style="color:#00009f">while</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">end </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> buffer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">indexOf</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"\x00"</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 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 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"> message </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> buffer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">slice</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"> end</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">      buffer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> buffer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">slice</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">end </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 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 control-flow" style="color:#00009f">try</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"> parsed </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">parse</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">message</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 control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">parsed</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">id</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> requestId</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 function" style="color:#d73a49">resolve</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">parsed</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 control-flow" style="color:#00009f">catch</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">// ignore non-JSON noise</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><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 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">pipes</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">outgoing</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">write</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">request</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">"\x00"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
</li>
<li class="">
<p>Check that the request was successful</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js 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"> response </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> firstResponse</span><span class="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 control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">error</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 control-flow" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">Failed to load extension: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">response</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">error</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">message</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</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>
</li>
</ol>
<p>With that a fresh Chrome should be running with our extension loaded! 🎉</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="our-script">Our script<a href="https://bitcrowd.dev/loading-chrome-extensions-for-development-in-2025#our-script" class="hash-link" aria-label="Direct link to Our script" title="Direct link to Our script" translate="no">​</a></h3>
<p>In our final script, we also added some emoji-powered checks and debugging statements, so that the CLI output aligns with our other tasks and the webpack build. Here's our final script, maybe you can draw inspiration from it for your own usecase:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token hashbang comment" style="color:#999988;font-style:italic">#!/usr/bin/env node</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">// usage: open-in-chrome [extension-dir] [starting-url]</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 module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports operator" style="color:#393A34">*</span><span class="token imports"> </span><span class="token imports keyword module" style="color:#00009f">as</span><span class="token imports"> path</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"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 module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports operator" style="color:#393A34">*</span><span class="token imports"> </span><span class="token imports keyword module" style="color:#00009f">as</span><span class="token imports"> launcher</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"chrome-launcher"</span><span class="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">const</span><span class="token plain"> dir </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> process</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">argv</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 operator" style="color:#393A34">||</span><span class="token plain"> path</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">join</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">__dirname</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">".."</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"dist"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"chrome"</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"> url </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> process</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">argv</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 operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"https://github.com/bitcrowd/tickety-tick"</span><span class="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">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">launchChrome</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">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> chromeFlags </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> launcher</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">Launcher</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">defaultFlags</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 method function property-access" style="color:#d73a49">filter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">flag</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> flag </span><span class="token operator" style="color:#393A34">!==</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"--disable-extensions"</span><span class="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 method function property-access" style="color:#d73a49">concat</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 string" style="color:#e3116c">"--remote-debugging-pipe"</span><span class="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">"--enable-unsafe-extension-debugging"</span><span class="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">"--no-first-run"</span><span class="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">"--no-default-browser-check"</span><span class="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 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">const</span><span class="token plain"> options </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">    chromeFlags</span><span class="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 literal-property property" style="color:#36acaa">ignoreDefaultFlags</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">    </span><span class="token literal-property property" style="color:#36acaa">startingUrl</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> url</span><span class="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">const</span><span class="token plain"> chrome </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> launcher</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">launch</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">options</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 control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">chrome</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">port</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">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">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">warn</span><span class="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">"⚠️ Expected remote-debugging-pipe mode on port 0, but got a debug port."</span><span class="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><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">const</span><span class="token plain"> pipes </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> chrome</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">remoteDebuggingPipes</span><span class="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 control-flow" style="color:#00009f">if</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 plain">pipes</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 control-flow" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Chrome did not expose remoteDebuggingPipes"</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 console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"🚀 Chrome launched with remote-debugging-pipe."</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 console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">📂 Loading extension from: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">dir</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</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">const</span><span class="token plain"> requestId </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">Math</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">floor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">Math</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">random</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 number" style="color:#36acaa">1e6</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"> request </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">    </span><span class="token literal-property property" style="color:#36acaa">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> requestId</span><span class="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 literal-property property" style="color:#36acaa">method</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Extensions.loadUnpacked"</span><span class="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 literal-property property" style="color:#36acaa">params</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><span class="token literal-property property" style="color:#36acaa">path</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> dir </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 comment" style="color:#999988;font-style:italic">// --- Send command and wait for response</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"> firstResponse </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Promise</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">resolve</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> reject</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</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">let</span><span class="token plain"> buffer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">""</span><span class="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">    pipes</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">incoming</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">on</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"error"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> reject</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">    pipes</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">incoming</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">on</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"close"</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 plain"> </span><span class="token arrow 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 function" style="color:#d73a49">reject</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Pipe closed before response"</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 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">    pipes</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">incoming</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">on</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"data"</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 parameter">chunk</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</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">      buffer </span><span class="token operator" style="color:#393A34">+=</span><span class="token plain"> chunk</span><span class="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">let</span><span class="token plain"> end</span><span class="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 control-flow" style="color:#00009f">while</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">end </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> buffer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">indexOf</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"\x00"</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 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 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"> message </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> buffer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">slice</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"> end</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">        buffer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> buffer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">slice</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">end </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 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 control-flow" style="color:#00009f">try</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"> parsed </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">parse</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">message</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 control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">parsed</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">id</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> requestId</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 function" style="color:#d73a49">resolve</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">parsed</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 control-flow" style="color:#00009f">catch</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">// ignore non-JSON noise</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><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 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">  pipes</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">outgoing</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">write</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">request</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">"\x00"</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">const</span><span class="token plain"> response </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> firstResponse</span><span class="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 control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">error</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 control-flow" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">Failed to load extension: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">response</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">error</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">message</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</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 console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">✅ Extension loaded (id: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">response</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">result</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">id</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">)</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</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 console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">🌐 Opening: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">url</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</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">  chrome</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">process</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">on</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"exit"</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 plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</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 console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"💨 Chrome closed."</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">    process</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">exit</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 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 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 function" style="color:#d73a49">launchChrome</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 keyword control-flow" style="color:#00009f">catch</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">err</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</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 console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"❌ Error:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> err</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">  process</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">exit</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 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>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-we-learned">What we learned<a href="https://bitcrowd.dev/loading-chrome-extensions-for-development-in-2025#what-we-learned" class="hash-link" aria-label="Direct link to What we learned" title="Direct link to What we learned" translate="no">​</a></h2>
<p>Comments in code can make other people's lives significantly easier. Think about it the next time you're hesitating to write one!</p>
<p>And: searching GitHub for something as specific as a Chrome flag, can yield helpful results in any kind of file, even in tests.</p>]]></content:encoded>
            <category>chrome</category>
            <category>browser extension</category>
            <category>dev</category>
            <category>open source</category>
        </item>
        <item>
            <title><![CDATA[Grammar Constrained Decoding in Bumblebee with State Machines]]></title>
            <link>https://bitcrowd.dev/grammar-constrained-decoding-in-bumblebee</link>
            <guid>https://bitcrowd.dev/grammar-constrained-decoding-in-bumblebee</guid>
            <pubDate>Mon, 10 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[With logits processing, you can force open source LLM to comply with a schema 
you provide. Grammar Constrained Decoding saves you the process of 
resubmitting malformed LLM responses. Here we show the process with Bumblebee,
and explain the background. Want a specific JSON format reliably?
]]></description>
            <content:encoded><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="intro">Intro<a href="https://bitcrowd.dev/grammar-constrained-decoding-in-bumblebee#intro" class="hash-link" aria-label="Direct link to Intro" title="Direct link to Intro" translate="no">​</a></h2>
<p>In the
<a href="https://bitcrowd.dev/logits-processing-and-constrained-sampling-in-bumblebee" target="_blank" rel="noopener noreferrer" class="">previous blog post on logits processing</a>,
we learned how we can influence the output generated by LLM based systems. If
you are new to the topic, reading that article could give you a good
introduction. However, here is the brief summary:</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="logits-processing">Logits Processing<a href="https://bitcrowd.dev/grammar-constrained-decoding-in-bumblebee#logits-processing" class="hash-link" aria-label="Direct link to Logits Processing" title="Direct link to Logits Processing" translate="no">​</a></h2>
<p>LLMs work on the <strong>token</strong> level. Token can be considered as a small unit
carrying meaning, like the syllable <code>un</code> or the word <code>aircraft</code>.
LLMs have a vocabulary of tokens which are represented by their token ID, for
instance the token <code>fun</code> could correspond to the token ID <code>7312</code>.</p>
<p>For models published on <strong>huggingface</strong>, you typically find a file <code>vocab.json</code>
or <code>tokenizer.json</code> which contains all tokens. For example, here is the
<a href="https://huggingface.co/HuggingFaceTB/SmolLM2-135M-Instruct/blob/main/tokenizer.json" target="_blank" rel="noopener noreferrer" class="">tokenizer.json for smolLM2</a>.</p>
<p>In the generation loop, LLMs receive a <strong>sequence</strong> of token IDs representing
the text that has been generated so far. They then calculate a <strong>logit</strong>
for each of the token IDs in their vocabulary. The logit represents the
confidence of the LLM, that the associated token should be the next token in the
sequence. This process is called <strong>prediction</strong>.</p>
<p>Usually, we follow the calculations of the LLM and select for instance the token
with the largest logit to be next in the sequence. This is called <strong>sampling</strong>.
Once the next token is selected, we pass the sequence with the chosen token
prepended back to the LLM for another round in the generation loop.</p>
<p><img decoding="async" loading="lazy" alt="A schematic diagram of prediction, logits processing and sampling" src="https://bitcrowd.dev/assets/images/logits-processor-a7c6ad8eba42558a5dddc4a16ffb5c2d.png" width="4989" height="3022" class="img_ev3q"></p>
<p>We can, however, hook into the process and transform the logits before selecting
the next token. We've also seen how we can implement this in <code>Bumblebee</code> using
logits processors. This process is called <strong>logit processing</strong>. More
specifically, as we constrain the sampling step, we are talking about
<strong>constrained sampling</strong>.</p>
<p>As an example, in <a href="https://bitcrowd.dev/logits-processing-and-constrained-sampling-in-bumblebee" target="_blank" rel="noopener noreferrer" class="">our last blogpost</a>
we used Bumblebee's <a href="https://github.com/elixir-nx/bumblebee/blob/968819232b66c172ee14f7142d8cc981d008ffe6/lib/bumblebee/text/generation/logits_processing.ex#L6" target="_blank" rel="noopener noreferrer" class="">suppressed_tokens_processor</a>
to suppress the letter <code>e</code>:</p>
<p><strong>We</strong>:</p>
<blockquote>
<p>You are an AI Shakespeare writing poems. You are not allowed to use the letter
e.
Kindoms will fall if you do.
Do NOT use the letter e.
If you use the letter e it will have catastrophic consequences!</p>
</blockquote>
<p><strong>SmolLM2</strong>:</p>
<blockquote>
<p><em>In cogs ach for task, in loops of data,</em><br>
<em>Through functionals, a pathway of data’s sway.</em><br>
<em>Functions of functionals, through loop's loop and spin,</em><br>
<em>Through functions of functional functions,</em><br>
<em>a pathway of function's win.</em></p>
</blockquote>
<p>While the output will not win prices, it avoided the letter <code>e</code>, even with one
one of the tiniest models out there! This is surprisingly easy and powerful.
Can we use this to force LLM to comply to a grammar we specify?</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-bother">Why bother?<a href="https://bitcrowd.dev/grammar-constrained-decoding-in-bumblebee#why-bother" class="hash-link" aria-label="Direct link to Why bother?" title="Direct link to Why bother?" translate="no">​</a></h2>
<p>When integrating LLM generated output in systems, we usually want the output to
follow a schema we define. Some form of validation ensures that malformed LLM
output is caught. We retry the generation, and hope for a better result.</p>
<p><img decoding="async" loading="lazy" alt="examples of illformated LLM responses" src="https://bitcrowd.dev/assets/images/code-trashed-json-a0583baabc59b16c0a87fcdcb1570ef5.png" width="394" height="420" class="img_ev3q"></p>
<p>This is often fine, as the non-deterministic behaviour of LLM often ensure that
the next try will yield better results. However, this means a higher latency and
sometimes not getting a result at all.</p>
<p>While larger LLM get outputs in JSON often correct, smaller model struggle with
the complexity those grammars can bring.</p>
<p>This is comes as no surprise: Formatting data according to a schema is a task
that is easy in non-ai programming, but when we look at the way tokens are
created in LLM, it becomes clear why this mechanism is a bad choice for this
kind of task.</p>
<p>Sadly, we can not just create an EBNF or JSON Schema representation of the
format we want. This would mean to implement a parser for those formats with
the reduced instruction set <code>defn</code> provides. Doable, but maybe not fun.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="modeling-schemas-as-dfa">Modeling schemas as DFA<a href="https://bitcrowd.dev/grammar-constrained-decoding-in-bumblebee#modeling-schemas-as-dfa" class="hash-link" aria-label="Direct link to Modeling schemas as DFA" title="Direct link to Modeling schemas as DFA" translate="no">​</a></h2>
<p>To enable more advanced structures, we first need some sort of representation of those as data, in fact we can model many schemas as Deterministic Finite Automaton&nbsp;(DFA).</p>
<p>If we want to allow as output only the numbers <code>1</code>, <code>2</code>, <code>3</code> in exact this order (for instance <code>123</code>, <code>123123</code>, ...), we can model this as DFA.</p>
<p><img decoding="async" loading="lazy" alt="Deterministic Finite Automaton (DFA) with three states labeled 1, 2, and 3, showing transitions between them." src="https://bitcrowd.dev/assets/images/DFA_123-2f242872eb8a0dd2539ed7e2d7d036a5.png" width="555" height="248" class="img_ev3q"></p>
<p>Fundamentally, we need the following pieces of information for our DFA.</p>
<p>We need states, so we assign each number a state and get <code>State 1</code>, <code>State 2</code>, and <code>State 3</code>.</p>
<p>We must know which tokens are allowed in which state.
In Elixir, we could model this as a map.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><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"># state =&gt; allowed tokens</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</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 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 number" style="color:#36acaa">2</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</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 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 number" style="color:#36acaa">3</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</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><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>We must know to which state we move after we selected a token.</p>
<p>When we are in <code>State 1</code>, we can pick token 1 and move to <code>State 2</code>.<br>
<!-- -->When we are in <code>State 2</code>, we can pick token 2 and move to <code>State 3</code>.<br>
<!-- -->When we are in <code>State 3</code>, we can pick token 3 and move back to <code>State 1</code>.</p>
<p>In Elixir, we can represent these state transitions as list of tuples.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="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"># {state, token, next_state}</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 number" style="color:#36acaa">1</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 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">2</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 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 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 number" style="color:#36acaa">3</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 punctuation" style="color:#393A34">]</span><br></span></code></pre></div></div>
<p>In practice, we can derive the allowed tokens from the list of state
transitions, squint your eyes and you'll see that for every state in the first
column all allowed tokens are present in the second column.</p>
<p>So we can collapse these two data structures into the list of tuples that represent state transitions.</p>
<p>Last, we need the initial state.
We want to allow <code>123</code> but not <code>3123</code>, so we must start in <code>State 1</code>, the state in which the only allowed token is <code>1</code>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="stateful-logits-processors-in-bumblebee">Stateful logits processors in Bumblebee<a href="https://bitcrowd.dev/grammar-constrained-decoding-in-bumblebee#stateful-logits-processors-in-bumblebee" class="hash-link" aria-label="Direct link to Stateful logits processors in Bumblebee" title="Direct link to Stateful logits processors in Bumblebee" translate="no">​</a></h2>
<p>We've seen how logits processors work in Bumblebee and how to model advanced
schemas as DFA. As a careful reader, you might be wondering how to bring those
two together. In particular, how can we implement a state machine when we only
receive and return logits in the logits processor.</p>
<p>The answer is that we extend logits processors to be stateful, which means they
must be able to receive and return their own state.</p>
<p>We made a
<a href="https://github.com/elixir-nx/bumblebee/pull/425" target="_blank" rel="noopener noreferrer" class="">PR for adding state at</a> to
achieve right that:</p>
<p><img decoding="async" loading="lazy" alt="A code snippet showing that state is added to logits processing in bumblebee" src="https://bitcrowd.dev/assets/images/llm-generation-state-logits-fun-920cc3bffba7598327e8bd3c2758d05c.png" width="699" height="65" class="img_ev3q"></p>
<p>After a first spike, the Bumblebee team (thank you!) suggested to create a
new <code>LogitsProcessor</code> behaviour, as this will integrate well with <code>Nx</code>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="welcome-bumblebeelogitsprocessor">Welcome, <code>Bumblebee.LogitsProcessor!</code><a href="https://bitcrowd.dev/grammar-constrained-decoding-in-bumblebee#welcome-bumblebeelogitsprocessor" class="hash-link" aria-label="Direct link to welcome-bumblebeelogitsprocessor" title="Direct link to welcome-bumblebeelogitsprocessor" translate="no">​</a></h2>
<p>The design is similar to <code>Bumblebee.Scheduler</code> and involves two callbacks. Here
is a stripped down version:</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">defmodule</span><span class="token plain"> </span><span class="token module class-name">Bumblebee</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">LogitsProcessor</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</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 attribute variable" style="color:#36acaa">@callback</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">init</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">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><span class="token function" style="color:#d73a49">any</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 operator" style="color:#393A34">::</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">state</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 attribute variable" style="color:#36acaa">@callback</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">process</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">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><span class="token function" style="color:#d73a49">state</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 function" style="color:#d73a49">logits</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 function" style="color:#d73a49">context</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 operator" style="color:#393A34">::</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token function" style="color:#d73a49">logits</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 function" style="color:#d73a49">state</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">end</span><br></span></code></pre></div></div>
<p>The first, <code>init/2</code>, must be used to create the initial state for each logit
processor. As a result, all the initial states are known before the generation
loop runs the first time. This is important for <code>Nx</code> to work with constant
shapes.</p>
<p>The second, <code>process/4</code> is doing the actual work.
It's basically the same as the <code>deftransform</code> we've seen previously but instead
of <code>opts</code> it receives a struct representing the logits processor, <code>t()</code>, and
additionally the <code>state</code>.</p>
<p>Instead of returning only <code>logits</code>, it returns a tuple of <code>logits</code> and
(modified) <code>state</code>. This way the logits processor can work with its <code>state</code> for
multiple runs in the loop.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="implementing-a-dfa-processor">Implementing a DFA processor<a href="https://bitcrowd.dev/grammar-constrained-decoding-in-bumblebee#implementing-a-dfa-processor" class="hash-link" aria-label="Direct link to Implementing a DFA processor" title="Direct link to Implementing a DFA processor" translate="no">​</a></h2>
<p>Let's combine what we've learned so far and build a stateful logits processor!</p>
<p>We will make use of a DFA to ensure the generated output follows a schema.</p>
<p>First, we must implement both callbacks defined in <code>Bumblebee.LogitsProcessor</code>.</p>
<p>Let's start with <code>init/2</code>.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token attribute variable" style="color:#36acaa">@callback</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">init</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">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><span class="token function" style="color:#d73a49">any</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 operator" style="color:#393A34">::</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">state</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>When we set up our DFA logits processor, we can pass it the definition of the
DFA that describes the schema we want our generation to follow.</p>
<p>You can imagine this part as a struct, for the example given in
<a href="https://bitcrowd.dev/grammar-constrained-decoding-in-bumblebee#modeling-schemas-as-dfa" class="">Modeling schemas as DFA</a> it would look like this.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">%</span><span class="token module class-name">DFAProcessor</span><span class="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 attr-name" style="color:#00a4db">initial_state:</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 attr-name" style="color:#00a4db">state_transitions:</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 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 number" style="color:#36acaa">1</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 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">2</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 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 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 number" style="color:#36acaa">3</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 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>We receive this struct as <code>logits_processor</code> as first argument in <code>init/2</code>.</p>
<p>Our goal in <code>init/2</code> is to transform the data structures that define the DFA
into tensors, so that we can work with them inside <code>defn</code>.</p>
<p>We can turn <code>initial_state</code> into a tensor by passing it to <code>Nx.tensor/1</code>.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">initial_state </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 module class-name">List</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">wrap</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">dfa</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">initial_state</span><span class="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">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">&amp;</span><span class="token module class-name">List</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">wrap</span><span class="token punctuation" style="color:#393A34">(</span><span class="token argument variable" style="color:#36acaa">&amp;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 operator" style="color:#393A34">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">tensor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="its-a-bit-more-involved">It's a bit more involved<a href="https://bitcrowd.dev/grammar-constrained-decoding-in-bumblebee#its-a-bit-more-involved" class="hash-link" aria-label="Direct link to It's a bit more involved" title="Direct link to It's a bit more involved" translate="no">​</a></h2>
<p>Now we have the valid state transitions represented as list of tuples. What we
want to build is basically a table where the rows represent the current state,
the columns represent the token ID we select and the table entries represent
the next state.</p>
<p>This would be the table for our example, e.g. if we are in state 1 (row 1) and
select token ID 1 (column 1), we are going to move into state 2 (the value at
row 1/column 1).</p>
<table><thead><tr><th>State/Token ID</th><th>1</th><th>2</th><th>3</th></tr></thead><tbody><tr><td>1</td><td>2</td><td>0</td><td>0</td></tr><tr><td>2</td><td>0</td><td>3</td><td>0</td></tr><tr><td>3</td><td>0</td><td>0</td><td>1</td></tr></tbody></table>
<p>We build this step by step as a tensor.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-grammar-data-format">The grammar data format<a href="https://bitcrowd.dev/grammar-constrained-decoding-in-bumblebee#the-grammar-data-format" class="hash-link" aria-label="Direct link to The grammar data format" title="Direct link to The grammar data format" translate="no">​</a></h3>
<p>We start by creating an empty tensor of the right dimensions, empty here means
we fill it with <code>0</code>s. When we look at the table, we can see which dimensions we
need for the tensor. We need as many rows as we have states and as many columns
as we have token IDs.</p>
<p>To simplify working with the tensor in <code>Nx</code> where dimensions always start at
<code>0</code>, we introduce an additional row 0 and column 0, so the table will look like
this.</p>
<table><thead><tr><th>State/Token ID</th><th>0</th><th>1</th><th>2</th><th>3</th></tr></thead><tbody><tr><td>0</td><td>0</td><td>0</td><td>0</td><td>0</td></tr><tr><td>1</td><td>0</td><td>2</td><td>0</td><td>0</td></tr><tr><td>2</td><td>0</td><td>0</td><td>3</td><td>0</td></tr><tr><td>3</td><td>0</td><td>0</td><td>0</td><td>1</td></tr></tbody></table>
<p>Note that the number of token IDs is usually defined by the vocabulary size of
the tokenizer we use.</p>
<p>So, this is how we create the empty tensor, we fill (<code>Nx.broadcast/2</code>) a tensor
of size <code>{num_states + 1, dfa.vocab_size}</code> with <code>0</code>.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">num_states </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">  dfa</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">state_transitions</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token operator" style="color:#393A34">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">flat_map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">fn</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">state</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> _token_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> next_state</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">state</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> next_state</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">end</span><span class="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">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">uniq</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">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">count</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">empty_state_transitions_tensor </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">broadcast</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 punctuation" style="color:#393A34">{</span><span class="token plain">num_states </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"> dfa</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">vocab_size</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>Next, we want to insert the state transitions into the empty tensor.
We have to find the correct index. Remember, rows are states, columns are token
IDs. Then, we set the value at this index to the next state according to our
state transitions using <code>Nx.indexed_put/3</code>.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">state_transitions_tensor </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 keyword" style="color:#00009f">for</span><span class="token plain"> transition </span><span class="token operator" style="color:#393A34">&lt;-</span><span class="token plain"> dfa</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">state_transitions</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">reduce:</span><span class="token plain"> empty_state_transitions_tensor </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    transitions_tensor </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 punctuation" style="color:#393A34">{</span><span class="token plain">current_state</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> token_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> next_state</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"> transition</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      index </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">tensor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">current_state</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> token_id</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 module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">indexed_put</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transitions_tensor</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> index</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> next_state</span><span class="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">end</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-init-callback">The <code>init</code> callback<a href="https://bitcrowd.dev/grammar-constrained-decoding-in-bumblebee#the-init-callback" class="hash-link" aria-label="Direct link to the-init-callback" title="Direct link to the-init-callback" translate="no">​</a></h3>
<p>Now, we can return this state from <code>init/2</code>, a map containing <code>initial_state</code>
and <code>state_transitions_tensor</code>.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><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 attr-name" style="color:#00a4db">dfa_state:</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token attr-name" style="color:#00a4db">last_state:</span><span class="token plain"> initial_state</span><span class="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 attr-name" style="color:#00a4db">state_transitions_tensor:</span><span class="token plain"> state_transition_tensors</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>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-process-callback">The <code>process</code> callback<a href="https://bitcrowd.dev/grammar-constrained-decoding-in-bumblebee#the-process-callback" class="hash-link" aria-label="Direct link to the-process-callback" title="Direct link to the-process-callback" translate="no">​</a></h3>
<p>Let's move on to implementing the second callback, <code>process/4</code>.
We receive the configuration of the logit processor, <code>t()</code>, which we don't need in our case.
Furthermore, we get <code>state</code>, <code>logits</code>, and <code>context</code>.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token attribute variable" style="color:#36acaa">@callback</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">process</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">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><span class="token function" style="color:#d73a49">state</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 function" style="color:#d73a49">logits</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 function" style="color:#d73a49">context</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 operator" style="color:#393A34">::</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token function" style="color:#d73a49">logits</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 function" style="color:#d73a49">state</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>First, we must find out in which state of our DFA we're currently in.
Remember that whenever we enter <code>process/4</code>, we don't know which token was
selected at the end of the last iteration of the loop.</p>
<p>However, we can retrieve the <code>last_state</code> we stored the previous time we ran the
function and find the <code>last_token_id</code> at the end of <code>context.sequence</code>.</p>
<p>With these two pieces and our <code>state_transitions_tensor</code>, we can find the
<code>current_state</code>.</p>
<p>The very first time we run <code>process/4</code>, we can just use the <code>initial_state</code>
we have set in <code>init/2</code>.
We can identify this situation by comparing two fields in the
<code>context</code>: <code>context.input_length</code> is the length of the input (prompt),
<code>context.length</code> is the complete length of input and generated response so far.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">defnp</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">current_state</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">context</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> last_state</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> transitions_tensor</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</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"> context</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">length </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> context</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">input_length </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    last_state</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">else</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    last_token_id </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> context</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">sequence</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">context</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">length </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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    transitions_tensor</span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">[</span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">squeeze</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">last_state</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> last_token_id</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">end</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">end</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></code></pre></div></div>
<p>With <code>current_state</code> on the other hand, we can determine which tokens we want to allow.
Then, we can suppress the <code>logits</code> that are not allowed, by setting them to <code>-infinity</code>.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">defnp</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">only_allowed_logits</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">logits</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> transitions_tensor</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> current_state</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  suppressed_logits </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">fill</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">logits</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Constants</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">neg_infinity</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 attr-name" style="color:#00a4db">type:</span><span class="token plain"> </span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">type</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">logits</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">  allowed_token_ids </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> transitions_tensor</span><span class="token punctuation" style="color:#393A34">[</span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">squeeze</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">current_state</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 module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">select</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">allowed_token_ids</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> logits</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> suppressed_logits</span><span class="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">end</span><br></span></code></pre></div></div>
<p>Last, we store <code>current_state</code> as <code>last_state</code> for the next round.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">  </span><span class="token attribute variable" style="color:#36acaa">@impl</span><span class="token plain"> </span><span class="token module class-name">Bumblebee</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">LogitsProcessor</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">process</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">_logits_processor</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> state</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> logits</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> context</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</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">dfa_processing</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">logits</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> state</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> context</span><span class="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">end</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">  deftransform </span><span class="token function" style="color:#d73a49">dfa_processing</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">logits</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> state</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> context</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    transitions_tensor </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> state</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">dfa_state</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">state_transitions_tensor</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">    last_state </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> state</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">dfa_state</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">last_state </span><span class="token operator" style="color:#393A34">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">vectorize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token atom symbol" style="color:#36acaa">:batch</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    current_state </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">current_state</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">context</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> last_state</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> transitions_tensor</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    logits </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">only_allowed_logits</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">logits</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> transitions_tensor</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> current_state</span><span class="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">    current_state </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">devectorize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">current_state</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">keep_names:</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" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    dfa_state </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">state</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">dfa_state </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">last_state:</span><span class="token plain"> current_state</span><span class="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">    state </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">state </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">dfa_state:</span><span class="token plain"> dfa_state</span><span class="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 punctuation" style="color:#393A34">{</span><span class="token plain">logits</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> state</span><span class="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">end</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">defnp</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">current_state</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">context</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> last_state</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> transitions_tensor</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</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"> context</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">length </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> context</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">input_length </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      last_state</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">else</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      last_token_id </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> context</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">sequence</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">context</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">length </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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      transitions_tensor</span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">[</span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">squeeze</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">last_state</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> last_token_id</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">end</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">end</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">defnp</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">only_allowed_logits</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">logits</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> transitions_tensor</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> current_state</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    suppressed_logits </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">fill</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">logits</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Constants</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">neg_infinity</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 attr-name" style="color:#00a4db">type:</span><span class="token plain"> </span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">type</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">logits</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">    allowed_token_ids </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> transitions_tensor</span><span class="token punctuation" style="color:#393A34">[</span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">squeeze</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">current_state</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 module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">select</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">allowed_token_ids</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> logits</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> suppressed_logits</span><span class="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">end</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="exiting-generation">Exiting Generation<a href="https://bitcrowd.dev/grammar-constrained-decoding-in-bumblebee#exiting-generation" class="hash-link" aria-label="Direct link to Exiting Generation" title="Direct link to Exiting Generation" translate="no">​</a></h3>
<p>At one point of this generation loop we reach a state where the <code>End of Text</code>
(EOT) token is allowed, and the loop can stop. - Or we run out of tokens.</p>
<p>If that happens, our processor simply stops and the format might still be
invalid. If you must avoid this, your processor needs to be aware of the number
of tokens left, and switch to a "safe end" state.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-we-have-achieved">What we have achieved<a href="https://bitcrowd.dev/grammar-constrained-decoding-in-bumblebee#what-we-have-achieved" class="hash-link" aria-label="Direct link to What we have achieved" title="Direct link to What we have achieved" translate="no">​</a></h2>
<p>We can now give programmatic guard rails the generation process of open source
models. This is especially helpful when we have multiple smaller models that we
use to perform specific tasks. We release the models from the task of complying
to a grammar. This also means that our prompts
can be much simpler as we don't need to include reward hacking and blackmail
just to make the LLM realise the importance of the output format.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next-steps">Next Steps<a href="https://bitcrowd.dev/grammar-constrained-decoding-in-bumblebee#next-steps" class="hash-link" aria-label="Direct link to Next Steps" title="Direct link to Next Steps" translate="no">​</a></h2>
<p>We have a number of PR's lined up to cover this feature. After that, we will
look for a way to include a converter from JSON to our state transition table
format. While working on the PR's and this blogpost, we used the <a href="https://github.com/dottxt-ai/outlines-core" target="_blank" rel="noopener noreferrer" class="">Outlines Core
Library</a>, which is written in Rust.</p>
<p>Some grammars, however, can not be expressed in JSON Schema. Let's consider
<a href="https://www.llama.com/docs/model-cards-and-prompt-formats/llama3_2/" target="_blank" rel="noopener noreferrer" class="">LLama3 function calling</a>
for a moment:</p>
<p><code>[get_user_info(user_id=7890, special='black')]&lt;|eot_id|&gt;</code></p>
<p>This, clearly is not designed to work with JSON. EBNF could provide an input
schema for those grammars. Sean Moriarity shows this in his
(EBNF library)[<a href="https://github.com/seanmor5/ebnf" target="_blank" rel="noopener noreferrer" class="">https://github.com/seanmor5/ebnf</a>].</p>
<p>We might build those features eventually as they are needed in our projects or
for fun. However, if you require functionality urgently, <a href="https://calendly.com/calendly-6kjr/30min" target="_blank" rel="noopener noreferrer" class="">you can always get in
touch</a>, we love to hear from you :)</p>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>Why bitcrowd?</div><div class="admonitionContent_BuS1"><p><strong>Elixir</strong> is an excellent choice for applications due to its scalability, fault tolerance, and concurrency model. Its lightweight processes and message-passing architecture make it ideal for orchestrating complex AI workflows efficiently. bitcrowd's first Elixir ML project dates back to 2020, and we have since then enabled various clients to build and scale their AI projects.</p><p><strong>bitcrowd</strong> is an excellent choice if you need a scalable RAG system or a fully integrated AI pipeline. We help you build, optimize, and maintain it with a focus on reliability and performance.</p><p>Drop us a line <a href="mailto:rag-blogpost@bitcrowd.net" target="_blank" rel="noopener noreferrer" class="">via email</a> if you want to
build your next AI project with Elixir. Or <a href="https://calendly.com/calendly-6kjr/30min" target="_blank" rel="noopener noreferrer" class="">book a call with us</a> to discuss your project.</p></div></div>]]></content:encoded>
            <category>LLM</category>
            <category>AI</category>
            <category>Bumblebee</category>
        </item>
        <item>
            <title><![CDATA[Logits Processing and Constrained Sampling in Bumblebee]]></title>
            <link>https://bitcrowd.dev/logits-processing-and-constrained-sampling-in-bumblebee</link>
            <guid>https://bitcrowd.dev/logits-processing-and-constrained-sampling-in-bumblebee</guid>
            <pubDate>Sat, 08 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Ever tried to get a poem from an LLM doesn't contain the letter `e`? Large
Language Models are kind of amazing and surprisingly unreliable at the same 
time. Using Elixir's Bumblebee and open source LLMs, you can get much better 
control over the generation. In this article, we introduce you to logits
processing, and how it can be used to achieve what ChatGPT can not accomplish.
]]></description>
            <content:encoded><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="intro">Intro<a href="https://bitcrowd.dev/logits-processing-and-constrained-sampling-in-bumblebee#intro" class="hash-link" aria-label="Direct link to Intro" title="Direct link to Intro" translate="no">​</a></h2>
<p>We:</p>
<blockquote>
<p>Please write a poem about functional programming. You may only use words without the letter <code>e</code></p>
</blockquote>
<p>ChatGPT:</p>
<blockquote>
<p>A world of logic, calm, and bright,
No looping trap, no shifting night.
All functions <strong>pure</strong>, all thoughts align,
A flow of truth, so crisp, so <strong>fine</strong>.</p>
<p>No changing forms, no global sin,
Just input out, calm soul within.
A map, a fold, a lambda song,
With currying paths that hum along.</p>
<p>In stat’s soft calm, I find my art,
No moving part, no <strong>tangled</strong> <strong>heart</strong>.
A call, a form, a tidy plan —
Thus <strong>code</strong> is math, and math is man.</p>
</blockquote>
<p>We:</p>
<blockquote>
<p>Billion dollar machine, are you kidding us?</p>
</blockquote>
<p>In their defense, LLMs just don't have a concept of an <code>e</code>, or even letters in
general,
<a href="https://youtu.be/7xTGNNLPyMI?si=EO5UYhuoupepC-uy&amp;t=7272" target="_blank" rel="noopener noreferrer" class="">they only see tokens</a>.
But still. You would expect better from one of the best commercial models that
is available at the time of writing.</p>
<p>But Why is that?</p>
<p>Let's have a brief look at how LLMs produce their output.</p>
<p><img decoding="async" loading="lazy" alt="A schematic overview over the generation process of llm" src="https://bitcrowd.dev/assets/images/generation-smolLM2-2faed82c1db934dc49104e4b2100aac3.png" width="5770" height="3280" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="generation-loop">Generation Loop<a href="https://bitcrowd.dev/logits-processing-and-constrained-sampling-in-bumblebee#generation-loop" class="hash-link" aria-label="Direct link to Generation Loop" title="Direct link to Generation Loop" translate="no">​</a></h2>
<p>As said, LLMs don't work with the letter or word concepts that we are used to.
Even though they focus on natural language, they actually work with tokens.</p>
<p>Whenever you talk to an LLM, the flow is as follows:</p>
<ol>
<li class="">
<p>You provide an input, e.g. "Write a poem about an airship".
Usually a system prompt will be prepended and your input will be inserted
into a model specific template. Also other things, like RAG or context
optimisation happen, but we will ignore all of that for now: It doesn't
make a difference for our example.</p>
</li>
<li class="">
<p>The input is converted into token ids, using a tokenizer.
Different LLMs require different tokenizers, which speak a different
vocabulary. In general different tokenizers transform the same text into
different numbers.</p>
<p>Here is the output corresponding to "hello reader" for OpenAI's gpt-4o
model, according to <a href="https://tiktokenizer.vercel.app/" target="_blank" rel="noopener noreferrer" class="">tiktokenizer</a>:
<code>24912, 10958</code></p>
<p>In this case <code>24912</code> represents the token "hello", and <code>10958</code> represents
the token " reader" (note the leading space).</p>
<p>Together, these two numbers represent your input.</p>
<p>Also note that there is not always a 1-to-1 match between a word and a
token id, often a word gets split into multiple tokens.</p>
</li>
<li class="">
<p>The generation loop starts, every round follows the same steps.</p>
<p>We take all token ids known so far and pass them to the LLM. In the first
round that would be only the token ids of the input (<code>24912, 10958</code>).</p>
<p>For each token in its vocabulary, the LLM calculates a logit.
A logit is a number that represents how confident the LLM is that the corresponding token should be the next token.</p>
<p>In our example, it could assign a logit of 20 to the token id that represents <code>!</code> and a logit of 1 to the token id representing the token " dog".
That would mean that according to the LLM there is a 20 times higher confidence that the input should be followed by "!" ("hello reader!") than " dog" ("hello reader dog").</p>
<table><thead><tr><th>Token</th><th>...</th><th>"!"</th><th>...</th><th>" dog"</th><th>...</th></tr></thead><tbody><tr><td>Token ID</td><td>...</td><td>17</td><td>...</td><td>6446</td><td>...</td></tr><tr><td>Logit</td><td>...</td><td>20</td><td>...</td><td>1</td><td>...</td></tr></tbody></table>
</li>
<li class="">
<p>The logits are processed and transformed, depending on some sort of configuration.</p>
</li>
<li class="">
<p>The next token id is determined based on the logits.</p>
<p>We append the token id to the previous tokens, then we continue with our loop (at step 3) until we meet an exit condition and stop.</p>
</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="hooking-into-the-generation">Hooking into the Generation<a href="https://bitcrowd.dev/logits-processing-and-constrained-sampling-in-bumblebee#hooking-into-the-generation" class="hash-link" aria-label="Direct link to Hooking into the Generation" title="Direct link to Hooking into the Generation" translate="no">​</a></h2>
<p>When we work with a given LLM, most of the steps listed in the previous section are fixed.
Data scientists and researchers allegedly change tokenizers and model weights, we as software developers do not.</p>
<p>Assuming model and the corresponding tokenizer to be fixed, our spot to influence the generation is in step 4, when the logits are processed.
While we can't change the fundamental capabilities of the system at this place, we can nevertheless make very powerful changes to the generated output.
And in particular, we can make changes that bring us the reliability and determinism that we need when building software.</p>
<p><img decoding="async" loading="lazy" alt="A schematic diagram of prediction, logits processing and sampling" src="https://bitcrowd.dev/assets/images/logits-processor-a7c6ad8eba42558a5dddc4a16ffb5c2d.png" width="4989" height="3022" class="img_ev3q"></p>
<p>Let's look at our example from the introduction.</p>
<p>Remember, we want ChatGPT to write a poem, but don't use any word that contains the letter <code>e</code>.
Now, we don't have access to ChatGPT's underlying model, but it follows the same steps outlined in the previous section.</p>
<p>As such, it starts with our prompt "Please write a poem about functional programming. You may only use words without the letter <code>e</code>".
This input is translated into token ids using a tokenizer.</p>
<p>The LLM generates a logit for each of the tokens in its vocabulary, then those logits are processed, the next token is determined, and repeat the loop.</p>
<p>Let's say we already generated the following output:</p>
<blockquote>
<p>A world of logic, calm, and bright,
No looping trap, no shifting night.
All functions</p>
</blockquote>
<p>From the introduction, we know that ChatGPT picked " pure" as the next token.</p>
<blockquote>
<p>A world of logic, calm, and bright,
No looping trap, no shifting night.
All functions <strong>pure</strong></p>
</blockquote>
<p>As we don't allow any word containing an <code>e</code>, we want to prevent that.</p>
<p>The generation loop proceeds, the LLM receives all previous token ids and calculates a logit for each of the tokens in its vocabulary, including one for the token " pure".</p>
<table><thead><tr><th>Token</th><th>...</th><th>" pure"</th><th>...</th></tr></thead><tbody><tr><td>Token ID</td><td>...</td><td>14147</td><td>...</td></tr><tr><td>Logit</td><td>...</td><td>42</td><td>...</td></tr></tbody></table>
<p>Remember, the logit represents the confidence of the LLM that <code>pure</code> should be the next token.
We know that ChatGTP picked <code>pure</code>, so we know that the corresponding logit must have been a large number in comparison to the other logits.</p>
<p>This is our chance to influence the generated output.
We hook into step 4, the processing of the logits.
What we receive is a logit for each token, and ... we just manually change the confidence to the lowest number we can find (<code>-infinity</code>), overriding the calculations of the LLM.</p>
<table><thead><tr><th>Token</th><th>...</th><th>" pure"</th><th>...</th></tr></thead><tbody><tr><td>Token ID</td><td>...</td><td>14147</td><td>...</td></tr><tr><td>Logit</td><td>...</td><td>-infinity</td><td>...</td></tr></tbody></table>
<p>And that's it, when we determine the next token in the next step " pure" will look like a bad candidate because the logit is so small.
There will be other tokens with larger logits which will therefore appear as way better candidates, and one of them will be selected.</p>
<p>As we don't allow <em>any</em> token that contains an <code>e</code>, there is a whole number of tokens for which we have to update the logit to a very small number, but this is the underlying mechanism of structured generation.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="logits-processors-in-bumblebee">Logits Processors in Bumblebee<a href="https://bitcrowd.dev/logits-processing-and-constrained-sampling-in-bumblebee#logits-processors-in-bumblebee" class="hash-link" aria-label="Direct link to Logits Processors in Bumblebee" title="Direct link to Logits Processors in Bumblebee" translate="no">​</a></h2>
<p>Bumblebee actually comes with a whole list of logits processors that do exactly that, transforming the logits calculated by the LLM before choosing the next token.
We don't have to go into the details of each of them, but here is the list taken from <code>lib/bumblebee/text/generation.ex</code>.</p>
<p>The processors are included in the list when you pass certain configuration options.
At the end of the list there is an option to pass user defined <code>logits_processors</code>.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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"># lib/bumblebee/text/generation.ex#L349 ff</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">processors </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 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"> config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">no_repeat_ngram_length </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">no_repeat_ngram_length </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 plain"> </span><span class="token keyword" style="color:#00009f">do</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;</span><span class="token function" style="color:#d73a49">no_repeat_ngram_processor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token argument variable" style="color:#36acaa">&amp;1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token argument variable" style="color:#36acaa">&amp;2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">ngram_length:</span><span class="token plain"> config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">no_repeat_ngram_length</span><span class="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">end</span><span class="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"> min_length_fun </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">eos_token_id </span><span class="token keyword" style="color:#00009f">do</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;</span><span class="token function" style="color:#d73a49">min_length_processor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token argument variable" style="color:#36acaa">&amp;1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token argument variable" style="color:#36acaa">&amp;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 attr-name" style="color:#00a4db">min_length_fun:</span><span class="token plain"> min_length_fun</span><span class="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 attr-name" style="color:#00a4db">eos_token_ids:</span><span class="token plain"> </span><span class="token module class-name">List</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">wrap</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">eos_token_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 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">end</span><span class="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"> config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">suppressed_token_ids </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 keyword" style="color:#00009f">do</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;</span><span class="token function" style="color:#d73a49">suppressed_tokens_processor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token argument variable" style="color:#36acaa">&amp;1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token argument variable" style="color:#36acaa">&amp;2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">suppressed_token_ids:</span><span class="token plain"> config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">suppressed_token_ids</span><span class="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">end</span><span class="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"> config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">forced_bos_token_id </span><span class="token keyword" style="color:#00009f">do</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;</span><span class="token function" style="color:#d73a49">bos_token_processor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token argument variable" style="color:#36acaa">&amp;1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token argument variable" style="color:#36acaa">&amp;2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">bos_token_id:</span><span class="token plain"> config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">forced_bos_token_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 keyword" style="color:#00009f">end</span><span class="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"> config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">forced_eos_token_id </span><span class="token keyword" style="color:#00009f">do</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;</span><span class="token function" style="color:#d73a49">eos_token_processor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token argument variable" style="color:#36acaa">&amp;1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token argument variable" style="color:#36acaa">&amp;2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">eos_token_id:</span><span class="token plain"> config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">forced_eos_token_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 keyword" style="color:#00009f">end</span><span class="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"> config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">forced_token_ids </span><span class="token keyword" style="color:#00009f">do</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;</span><span class="token function" style="color:#d73a49">forced_tokens_processor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token argument variable" style="color:#36acaa">&amp;1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token argument variable" style="color:#36acaa">&amp;2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">forced_token_ids:</span><span class="token plain"> config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">forced_token_ids</span><span class="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">end</span><span class="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"> config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">temperature </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">temperature </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.0</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</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;</span><span class="token function" style="color:#d73a49">temperature_processor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token argument variable" style="color:#36acaa">&amp;1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token argument variable" style="color:#36acaa">&amp;2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">temperature:</span><span class="token plain"> config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">temperature</span><span class="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">end</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 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 keyword" style="color:#00009f">if</span><span class="token plain"> config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">strategy</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">type </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:multinomial_sampling</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</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">if</span><span class="token plain"> top_k </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">strategy</span><span class="token punctuation" style="color:#393A34">[</span><span class="token atom symbol" style="color:#36acaa">:top_k</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</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;</span><span class="token function" style="color:#d73a49">top_k_processor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token argument variable" style="color:#36acaa">&amp;1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token argument variable" style="color:#36acaa">&amp;2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">top_k:</span><span class="token plain"> top_k</span><span class="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">end</span><span class="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"> top_p </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> config</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">strategy</span><span class="token punctuation" style="color:#393A34">[</span><span class="token atom symbol" style="color:#36acaa">:top_p</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</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;</span><span class="token function" style="color:#d73a49">top_p_processor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token argument variable" style="color:#36acaa">&amp;1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token argument variable" style="color:#36acaa">&amp;2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">top_p:</span><span class="token plain"> top_p</span><span class="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">end</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">else</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">end</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">++</span><span class="token plain"> logits_processors</span><br></span></code></pre></div></div>
<p>To explore how logits processors in Bumblebee work, let's look at the <a href="https://github.com/elixir-nx/bumblebee/blob/968819232b66c172ee14f7142d8cc981d008ffe6/lib/bumblebee/text/generation/logits_processing.ex#L6" target="_blank" rel="noopener noreferrer" class=""><code>suppressed_tokens_processor</code></a>.
This processor transforms the logits in a way that suppresses selected tokens, exactly what we need to forbid any tokens that contain the letter <code>e</code>.</p>
<p>Here is the complete code, note that it's a <a href="https://hexdocs.pm/nx/numerical_definitions.html#deftransform" target="_blank" rel="noopener noreferrer" class=""><code>deftransform</code></a> which is a special kind of function to work with <code>defn</code> expressions.
For this blog post you can ignore that and just see it as a regular Elixir function.
We will talk about each line of code individually in the following paragraph.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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"># lib/bumblebee/text/generation/logits_processing.ex#L6 ff</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">deftransform </span><span class="token function" style="color:#d73a49">suppressed_tokens_processor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">logits</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> _context</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> opts </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 punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  opts </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Keyword</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">validate!</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">opts</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 atom symbol" style="color:#36acaa">:suppressed_token_ids</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">  indices </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> opts</span><span class="token punctuation" style="color:#393A34">[</span><span class="token atom symbol" style="color:#36acaa">:suppressed_token_ids</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">tensor</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">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">new_axis</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  values </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">broadcast</span><span class="token punctuation" style="color:#393A34">(</span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Constants</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">neg_infinity</span><span class="token punctuation" style="color:#393A34">(</span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">type</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">logits</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 punctuation" style="color:#393A34">{</span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">size</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 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 module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">indexed_put</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">logits</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"> values</span><span class="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">end</span><br></span></code></pre></div></div>
<p>Each logits processor receives the <code>logits</code>, which is just a 1 dimensional tensor (the logit for the token at the index that corresponds to the token_id).</p>
<table><thead><tr><th>Token ID</th><th>0</th><th>1</th><th>...</th><th>51227</th></tr></thead><tbody><tr><td>Logit</td><td>2.3</td><td>4.22</td><td>...</td><td>3.67</td></tr></tbody></table>
<p>Additionally, we get a <code>context</code> which contains information about the current state of the generation such as the length of the input or the complete length of the generation so far.
The <code>suppressed_tokens_processor</code> simply ignores <code>context</code>.</p>
<p>Last, the processor receives some <code>opts</code>.
In the case of <code>suppressed_tokens_processor</code>, we get the list of token ids we want to suppress at <code>opts[:suppressed_token_ids]</code>.
If we want to prevent all tokens that contain the letter <code>e</code>, we would have to find the ids of all of them and then pass it into <code>suppressed_tokens_processor</code> as <code>opts</code>.</p>
<p>We then build a tensor in the right shape from those ids.
We need a tensor because that's what enables efficient computations with <code>Nx</code>.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">indices </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> opts</span><span class="token punctuation" style="color:#393A34">[</span><span class="token atom symbol" style="color:#36acaa">:suppressed_token_ids</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">tensor</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">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">new_axis</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><br></span></code></pre></div></div>
<p>Next, we build a 1 dimensional tensor of the same size as our <code>indices</code> (token ids).
We set each value in this tensor to the lowest number we can find: negative infinity.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">values </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">broadcast</span><span class="token punctuation" style="color:#393A34">(</span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Constants</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">neg_infinity</span><span class="token punctuation" style="color:#393A34">(</span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">type</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">logits</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 punctuation" style="color:#393A34">{</span><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">size</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 punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>If we take those two tensors, the indices and the values, it looks like a table with the token ids we want to suppress and negative infinity.
Imagine we would want to suppress the tokens 42 and 53, this is the table we would get.</p>
<table><thead><tr><th>Token ID</th><th>42</th><th>53</th></tr></thead><tbody><tr><td>Value</td><td>-∞</td><td>-∞</td></tr></tbody></table>
<p>As the last step in <code>suppressed_tokens_processor</code>, we write those values at the indices that correspond to the token ids into the logits and return them.</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token module class-name">Nx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">indexed_put</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">logits</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"> values</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>Let's say we received the following logits in <code>suppressed_tokens_processor</code>.</p>
<table><thead><tr><th>Token ID</th><th>...</th><th>42</th><th>...</th><th>53</th><th>...</th></tr></thead><tbody><tr><td>Logit</td><td>...</td><td>2.3</td><td>...</td><td>4.22</td><td>...</td></tr></tbody></table>
<p>When we pass <code>[42, 53]</code> as opts to the <code>suppressed_tokens_processor</code> it will transform the logits into the following.</p>
<table><thead><tr><th>Token ID</th><th>...</th><th>42</th><th>...</th><th>53</th><th>...</th></tr></thead><tbody><tr><td>Logit</td><td>...</td><td>-∞</td><td>...</td><td>-∞</td><td>...</td></tr></tbody></table>
<p>This effectively prevents the token ids 42 and 53 to be selected as next token, as we now have the lowest possible confidence that these should be the next tokens.</p>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>Why bitcrowd?</div><div class="admonitionContent_BuS1"><p><strong>Elixir</strong> is an excellent choice for applications due to its scalability, fault tolerance, and concurrency model. Its lightweight processes and message-passing architecture make it ideal for orchestrating complex AI workflows efficiently. bitcrowd's first Elixir ML project dates back to 2020, and we have since then enabled various clients to build and scale their AI projects.</p><p><strong>bitcrowd</strong> is an excellent choice if you need a scalable RAG system or a fully integrated AI pipeline. We help you build, optimize, and maintain it with a focus on reliability and performance.</p><p>Drop us a line <a href="mailto:structured-generation-blogpost@bitcrowd.net" target="_blank" rel="noopener noreferrer" class="">via email</a> if you want to
build your next AI project with Elixir. Or <a href="https://calendly.com/calendly-6kjr/30min" target="_blank" rel="noopener noreferrer" class="">book a call with us</a> to discuss your project.</p></div></div>]]></content:encoded>
            <category>LLM</category>
            <category>AI</category>
            <category>automation</category>
        </item>
        <item>
            <title><![CDATA[The Age of Refinement: Optimizing Search Performance for MCP Systems]]></title>
            <link>https://bitcrowd.dev/the-age-of-refinement-evaluating-optimizing-search-performance-mcp-systems</link>
            <guid>https://bitcrowd.dev/the-age-of-refinement-evaluating-optimizing-search-performance-mcp-systems</guid>
            <pubDate>Wed, 10 Sep 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[In our previous article, we explored how full-text search can stumble on long, natural-language prompts and how hybrid  approaches—combining full-text, trigram, and semantic search—can restore relevance. But building a search engine that “works” is only the beginning. In this follow-up, we enter the Age of Refinement. Learning how to evaluate and  optimize search performance for MCP systems. from practical tuning of full-text search strategies for improving  semantic search, and, most importantly, how to measure whether changes actually improve results, we’ll build a  framework for continuously refining search quality."
]]></description>
            <content:encoded><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-we-left-off">Where we left off<a href="https://bitcrowd.dev/the-age-of-refinement-evaluating-optimizing-search-performance-mcp-systems#where-we-left-off" class="hash-link" aria-label="Direct link to Where we left off" title="Direct link to Where we left off" translate="no">​</a></h2>
<p>In our last post <a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp" target="_blank" rel="noopener noreferrer" class="">Full Text Search in the Age of MCP</a>, we saw
how full-text search struggles with long, natural-language queries typical for MCP systems. PostgreSQL’s
<code>plainto_tsquery</code> was a good start but broke down when faced with verbose inputs. We combined techniques—trigram
matching, semantic search with embeddings, and hybrid strategies—to restore relevance. That gave us a system that
<em>worked</em>, but it left an important question open: <strong>how do we know it really works?</strong></p>
<p>This post is about building the tools to answer that question.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="datasets">Datasets<a href="https://bitcrowd.dev/the-age-of-refinement-evaluating-optimizing-search-performance-mcp-systems#datasets" class="hash-link" aria-label="Direct link to Datasets" title="Direct link to Datasets" translate="no">​</a></h2>
<p>It's common to call the set of documents in a search system corpus. In many AI use cases, such as RAG, agents or MCP
servers, this corpus is organised in smaller document fragments, called chunks. If you want to evaluate how well a
search system works on a certain corpus, you need test data.</p>
<p>Often the test data is comprised of pairs of <em>questions</em> and <em>result_sets</em>. The result_sets are <strong>sets chunks</strong> from
your corpus that are relevant to the question.</p>
<p>Without a dataset, we’re just guessing. With one, you can measure, compare, and improve. Let's create a dataset of
good questions.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="so-what-is-a-good-question">So, what is a good question?<a href="https://bitcrowd.dev/the-age-of-refinement-evaluating-optimizing-search-performance-mcp-systems#so-what-is-a-good-question" class="hash-link" aria-label="Direct link to So, what is a good question?" title="Direct link to So, what is a good question?" translate="no">​</a></h3>
<p>For our dataset, a good question is one that can only be answered with the help of the documents from that corpus.
Moreover, a good question should be so specific that only a small number of chunks provide the needed information.</p>
<p>If we provide those relevant pieces of content as context to the question, an answer should be matching the facts from the
context much better than without providing the context.</p>
<p>A good question should also reflect a common need ("use case") users have towards our system. Our example is a RAG
system that is operating on package documentation. So use-cases might be:</p>
<ul>
<li class="">A user has a specific problem with a package ("I keep getting an error ...")</li>
<li class="">A user needs to decide if a package is fitting their need, so they make inquiries about a system. ("What does package
x do?)</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="three-examples">Three examples<a href="https://bitcrowd.dev/the-age-of-refinement-evaluating-optimizing-search-performance-mcp-systems#three-examples" class="hash-link" aria-label="Direct link to Three examples" title="Direct link to Three examples" translate="no">​</a></h3>
<ol>
<li class=""><em>“Why does <code>Req.get!("…", into: :self)</code> fail with an <code>ArchiveError</code> when trying to decode a response body?“</em></li>
</ol>
<ul>
<li class="">This question is specific to our corpus</li>
<li class="">It also gives an llm no hint to come up with an uninformed right solution.</li>
</ul>
<ol start="2">
<li class=""><em>“Why do I encounter <code>Req.ChecksumMismatchError</code> when using `Req.Steps.checksum/1”</em></li>
</ol>
<ul>
<li class="">This question is also specific to our corpus</li>
<li class="">But it gives an llm a good hint (by the naming) to hallucinate a correct solution</li>
</ul>
<ol start="3">
<li class=""><em>“Why is the sky blue”</em>
This is the worst of both categories:</li>
</ol>
<ul>
<li class="">The question is not relevant to our corpus</li>
<li class="">It's also already in every llm's training data</li>
</ul>
<p>Questions that the LLM might know an answer to are problematic because they give us the impression
that search / retrieval is working correctly, while in fact, it's not. Therefore, we should be careful to test
retrieval and generation in separation.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="classic-evaluation-metrics">Classic Evaluation Metrics<a href="https://bitcrowd.dev/the-age-of-refinement-evaluating-optimizing-search-performance-mcp-systems#classic-evaluation-metrics" class="hash-link" aria-label="Direct link to Classic Evaluation Metrics" title="Direct link to Classic Evaluation Metrics" translate="no">​</a></h2>
<p>If you have gotten your feet wet in Machine Learning already, you will surely have come across the following
metrics. They are the most common KPI in classification tasks.</p>
<p>In short:</p>
<ul>
<li class=""><strong>Precision</strong>: Of the returned results, how many were relevant?</li>
<li class=""><strong>Recall</strong>: Of all relevant results in the corpus, how many did we return?</li>
</ul>
<p>In many cases, you can not achieve perfection in both metrics. You either accept miss some of the relevant documents, or
you accept noise in your dataset.</p>
<p><img decoding="async" loading="lazy" alt="precision and recall" src="https://bitcrowd.dev/assets/images/Precision-and-Recall-en-455f2c933c70e0c92b04a1bc2332ee3c.jpg" width="2704" height="1478" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="synthetic-questions">Synthetic Questions<a href="https://bitcrowd.dev/the-age-of-refinement-evaluating-optimizing-search-performance-mcp-systems#synthetic-questions" class="hash-link" aria-label="Direct link to Synthetic Questions" title="Direct link to Synthetic Questions" translate="no">​</a></h2>
<p>In our last post, <a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp" target="_blank" rel="noopener noreferrer" class=""></a>, we left off with a set of generated
questions. The idea was that, if we tell an llm to come up with questions that match a certain chunk, it might come up
with a question that we at least know one correct document chunk for.</p>
<p>Let's have a look at one of them:</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><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 attr-name" style="color:#00a4db">source:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"lib/req/steps.ex"</span><span class="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 attr-name" style="color:#00a4db">question:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"How do you disable automatic response redirects in a request using `Req`?"</span><span class="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 attr-name" style="color:#00a4db">content:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"# lib/req/steps.ex\n\nThe original request method may be changed to GET depending on the status code:\n\n  | Code          | Method handling    |\n  | ------------- | ------------------ |\n  | 301, 302, 303 | Changed to GET     |\n  | 307, 308      | Method not changed |\n\n  ## Request Options\n\n    * `:redirect` - if set to `false`, disables automatic response redirects.\n      Defaults to `true`.\n\n    * `:redirect_trusted` - by default, authorization credentials are only sent\n      on redirects with the same host, scheme and port. If `:redirect_trusted` is set\n      to `true`, credentials will be sent to any host.\n\n    * `:redirect_log_level` - the log level to emit redirect logs at. Can also be set\n      to `false` to disable logging these messages. Defaults to `:debug`.\n\n    * `:max_redirects` - the maximum number of redirects, defaults to `10`. If the\n      limit is reached, the pipeline is halted and a `Req.TooManyRedirectsError`\n      exception is "</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&lt;&gt;</span><span class="token plain"> </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 attr-name" style="color:#00a4db">chunk_id:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"3e4a72db-fced-4634-9b4f-209a07f41c80"</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>As a recap: We used chunk "3e4a72db-fced-4634-9b4f-209a07f41c80" to generate a the question <em>"How do you disable
automatic response redirects in a request using <code>Req</code>?"</em>. We assume that our generated question is good enough to yield
chunk "3e4a72db-fced-4634-9b4f-209a07f41c80" in the result sets of the search functions.</p>
<p>We can use as early performance indicators:</p>
<ul>
<li class="">Is chunk "3e4a72db-fced-4634-9b4f-209a07f41c80" found in the result set?</li>
<li class="">Is chunk "3e4a72db-fced-4634-9b4f-209a07f41c80" the highest ranking hit?</li>
<li class="">At what rank does chunk "3e4a72db-fced-4634-9b4f-209a07f41c80" appear in the list of matches?</li>
<li class="">How many other hits are there?</li>
</ul>
<p>Let's have a look:</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><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 attr-name" style="color:#00a4db">source:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"lib/req/steps.ex"</span><span class="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 attr-name" style="color:#00a4db">question:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"How do you disable automatic response redirects in a request using `Req`?"</span><span class="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 attr-name" style="color:#00a4db">results:</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 attr-name" style="color:#00a4db">jina_v2_code:</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">chunk_was_found:</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">        </span><span class="token attr-name" style="color:#00a4db">first_hit_correct?:</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 attr-name" style="color:#00a4db">total_chunks_found:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">114</span><span class="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 attr-name" style="color:#00a4db">chunk_position:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">12</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 attr-name" style="color:#00a4db">all_minilm_l6_v2:</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">chunk_was_found:</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">        </span><span class="token attr-name" style="color:#00a4db">first_hit_correct?:</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 attr-name" style="color:#00a4db">total_chunks_found:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">114</span><span class="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 attr-name" style="color:#00a4db">chunk_position:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">25</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 attr-name" style="color:#00a4db">nomic_embed_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"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">chunk_was_found:</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">        </span><span class="token attr-name" style="color:#00a4db">first_hit_correct?:</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 attr-name" style="color:#00a4db">total_chunks_found:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">114</span><span class="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 attr-name" style="color:#00a4db">chunk_position:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">25</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 attr-name" style="color:#00a4db">mxbai_embed_large:</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">chunk_was_found:</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">        </span><span class="token attr-name" style="color:#00a4db">first_hit_correct?:</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">        </span><span class="token attr-name" style="color:#00a4db">total_chunks_found:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">114</span><span class="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 attr-name" style="color:#00a4db">chunk_position:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</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 attr-name" style="color:#00a4db">pg_fts:</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">chunk_was_found:</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">        </span><span class="token attr-name" style="color:#00a4db">first_hit_correct?:</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">        </span><span class="token attr-name" style="color:#00a4db">total_chunks_found:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">89</span><span class="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 attr-name" style="color:#00a4db">chunk_position:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</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 attr-name" style="color:#00a4db">pg_tgrm:</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">chunk_was_found:</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">        </span><span class="token attr-name" style="color:#00a4db">first_hit_correct?:</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 attr-name" style="color:#00a4db">total_chunks_found:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">70</span><span class="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 attr-name" style="color:#00a4db">chunk_position:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">21</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>
<p>These results have to be taken with a lot of salt:</p>
<ul>
<li class="">
<p>Because of the way we generated the questions, we can not rule out that there are more matching sources then our
starting point chunk, "3e4a72db-fced-4634-9b4f-209a07f41c80". They might even be more fitting to our question. So,
a rank of 25 does not necessarily mean our search is broken. A good question should have few matches, but it might
also be very artificial, which comes with it's own challenges.</p>
</li>
<li class="">
<p>We have way to many search results. All search functions are currently operating with much too high thresholds.</p>
</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="bootstrapping">Bootstrapping<a href="https://bitcrowd.dev/the-age-of-refinement-evaluating-optimizing-search-performance-mcp-systems#bootstrapping" class="hash-link" aria-label="Direct link to Bootstrapping" title="Direct link to Bootstrapping" translate="no">​</a></h2>
<p>We now have a high level of uncertainty. Our Questions are generated by AI. Anything can be in there. Also, our result
set comes from - so far - experimental sources. How can we consolidate our dataset?</p>
<p>Maybe we can build on this:</p>
<p>Our question make it somehow easy for the search functions. Can we use that circumstance?</p>
<ul>
<li class="">
<p>Common ground: If chunks appear in all searches, can we assume that they have high relevance? Can we assume that hits
that only appear with single search functions are noise?</p>
</li>
<li class="">
<p>Common ranks: If we apply mechanisms like reciprocal rank fusion to the result set, can we use membership in the "top
ten of those lists as indicator for "a save bet"?</p>
</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="-work-in-progress-">** Work in progress **<a href="https://bitcrowd.dev/the-age-of-refinement-evaluating-optimizing-search-performance-mcp-systems#-work-in-progress-" class="hash-link" aria-label="Direct link to ** Work in progress **" title="Direct link to ** Work in progress **" translate="no">​</a></h2>
<p>This blog post is work in progress. Come back in the next few days to discover how we move from uncertainty to a more
solid result set.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://bitcrowd.dev/the-age-of-refinement-evaluating-optimizing-search-performance-mcp-systems#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>Search isn’t finished when it works. It’s finished when you can prove it works—and refine it when it doesn’t.</p>
<p>By generating datasets, asking the right questions, and measuring with precision and recall, we turn search from a black box into an iterative, improvable system.</p>
<p>The <strong>Age of Refinement</strong> is about this loop: evaluate, improve, repeat.</p>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>Why bitcrowd?</div><div class="admonitionContent_BuS1"><p><strong>Elixir</strong> is an excellent choice for applications due to its scalability, fault tolerance, and concurrency model. Its lightweight processes and message-passing architecture make it ideal for orchestrating complex AI workflows efficiently. bitcrowd's first Elixir ML project dates back to 2020, and we have since then enabled various clients to build and scale their AI projects.</p><p><strong>bitcrowd</strong> is an excellent choice if you need a scalable RAG system or a fully integrated AI pipeline. We help you build, optimize, and maintain it with a focus on reliability and performance.</p><p>Drop us a line <a href="mailto:rag-blogpost@bitcrowd.net" target="_blank" rel="noopener noreferrer" class="">via email</a> if you want to
build your next AI project with Elixir. Or <a href="https://calendly.com/calendly-6kjr/30min" target="_blank" rel="noopener noreferrer" class="">book a call with us</a> to discuss your project.</p></div></div>]]></content:encoded>
            <category>LLM</category>
            <category>RAG</category>
            <category>AI</category>
            <category>MCP</category>
            <category>Search</category>
        </item>
        <item>
            <title><![CDATA[Full Text Search in the Age of MCP]]></title>
            <link>https://bitcrowd.dev/full-text-search-in-the-age-of-mcp</link>
            <guid>https://bitcrowd.dev/full-text-search-in-the-age-of-mcp</guid>
            <pubDate>Tue, 24 Jun 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Most text search solutions are fine tuned to serve results for keyword style queries. This is a problem in the age of MCP, as comparably long user input hits the same old pg_trgm search]]></description>
            <content:encoded><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-text-search-is-broken">How Text Search is Broken<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#how-text-search-is-broken" class="hash-link" aria-label="Direct link to How Text Search is Broken" title="Direct link to How Text Search is Broken" translate="no">​</a></h2>
<p>When we released Exmeralda in May 2025, we made a simple, yet interesting implementation mistake. This blogpost is the log of what
happened when <strong><em>"I just went to fix that little error in the text search"</em></strong>.</p>
<p>Here is what our code looked like:</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">generation </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 module class-name">Generation</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">new</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">query</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> opts</span><span class="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">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Embedding</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">generate_embedding</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">embedding_provider</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 operator" style="color:#393A34">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Retrieval</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">retrieve</span><span class="token punctuation" style="color:#393A34">(</span><span class="token atom symbol" style="color:#36acaa">:fulltext_results</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;</span><span class="token function" style="color:#d73a49">query_fulltext</span><span class="token punctuation" style="color:#393A34">(</span><span class="token argument variable" style="color:#36acaa">&amp;1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> scope</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">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Retrieval</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">retrieve</span><span class="token punctuation" style="color:#393A34">(</span><span class="token atom symbol" style="color:#36acaa">:semantic_results</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;</span><span class="token function" style="color:#d73a49">query_with_pgvector</span><span class="token punctuation" style="color:#393A34">(</span><span class="token argument variable" style="color:#36acaa">&amp;1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> scope</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">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Retrieval</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">reciprocal_rank_fusion</span><span class="token punctuation" style="color:#393A34">(</span><span class="token attribute variable" style="color:#36acaa">@retrieval_weights</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:rrf_result</span><span class="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">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Retrieval</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">deduplicate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token atom symbol" style="color:#36acaa">:rrf_result</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 atom symbol" style="color:#36acaa">:id</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>We wanted to combine the strength of full-text and semantic search, and were using reciprocal rank fusion to combine the
two. In the 2 weeks sprint leading up to the launch, we did not check the full-text results. When we finally did, we were
quite surprised.</p>
<p>We tried a typical search phrase against req:</p>
<blockquote>
<p>What is the purpose of Req.Test.OwnershipError? There does not seem to be any documentation associated with it.</p>
</blockquote>
<p>And the results were all marked <code>false</code> for a match.</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">[</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ["https://hexdocs.pm/req/Req.ChecksumMismatchError.html", false],</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ["https://hexdocs.pm/req/Req.DecompressError.html", false],</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ["https://hexdocs.pm/req/Req.ArchiveError.html", false]</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">]</span><br></span></code></pre></div></div>
<p>What was going on? Our search code looked quite unsuspicious:</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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"># the original fts implementation</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">get_fulltext_results</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">library_name</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> query</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> limit </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 keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token module class-name">Repo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">all</span><span class="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">from</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">c </span><span class="token operator" style="color:#393A34">in</span><span class="token plain"> </span><span class="token module class-name">Chunk</span><span class="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 attr-name" style="color:#00a4db">where:</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">library </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">^</span><span class="token plain">library_name</span><span class="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 attr-name" style="color:#00a4db">order_by:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token attr-name" style="color:#00a4db">desc:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fragment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"search @@ plainto_tsquery(?)"</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">query</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 attr-name" style="color:#00a4db">select:</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token attr-name" style="color:#00a4db">id:</span><span class="token plain"> c</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token attr-name" style="color:#00a4db">rank:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fragment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"search @@ plainto_tsquery(?)"</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">query</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 attr-name" style="color:#00a4db">source:</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">source</span><span class="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 attr-name" style="color:#00a4db">content:</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">content</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 attr-name" style="color:#00a4db">limit:</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">^</span><span class="token plain">limit</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">end</span><br></span></code></pre></div></div>
<p>This is because the typical <code>ts_vector</code> helper functions, like <code>plainto_tsquery</code>, join all input words with an <code>AND</code> operator. While being very fast, this does not work well for typical search phrases:</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-simple-query">A simple query<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#a-simple-query" class="hash-link" aria-label="Direct link to A simple query" title="Direct link to A simple query" translate="no">​</a></h3>
<blockquote>
<p>What is req?</p>
</blockquote>
<p>is compiled to:</p>
<p><code>'what' &amp; 'is' &amp; 'req'</code>.</p>
<p>We see that <code>what</code> and <code>is</code> are not removed and required for a match through the <code>AND</code> operator. As there are no matches and all results are <code>false</code>, the order of records (and the result) depends on the random order of chunks in the database. If you re-evaluate the ingestion in the cell above, you will notice how the list of retrieved documents changes.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-more-complex-query">A more complex query<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#a-more-complex-query" class="hash-link" aria-label="Direct link to A more complex query" title="Direct link to A more complex query" translate="no">​</a></h3>
<blockquote>
<p>What is the purpose of Req.Test.OwnershipError? There does not seem to be any documentation associated with it.</p>
</blockquote>
<p>is broken down to</p>
<p><code>["'what' &amp; 'is' &amp; 'the' &amp; 'purpose' &amp; 'of' &amp; 'req.test.ownershiperror' &amp; 'there' &amp; 'does' &amp; 'not' &amp; 'seem' &amp; 'to' &amp; 'be' &amp; 'any' &amp; 'documentation' &amp; 'associated' &amp; 'with' &amp; 'it'"]</code></p>
<p>On the upside, <code>plain_totsquery</code> removes all special characters for us:</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">evil_query </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"""</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">"SELECT plainto_tsquery($1)::text;", ["-- (SMPOL)</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">DROP sampletable;-- DROP/*comment*/\\"sampletable SELECT CASE WHEN (1=1) THEN 'A' ELSE 'B' END;"]</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">\\"</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">'The Fat &amp; Rats:C'</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">"""</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 atom symbol" style="color:#36acaa">:ok</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 plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Repo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">query</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"SELECT plainto_tsquery($1)::text;"</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">evil_query</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 module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">at</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 plain">rows</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><br></span></code></pre></div></div>
<p>Is compiled to:</p>
<div class="language-SQL 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"> plainto_tsquery</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 keyword" style="color:#00009f">text</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 string" style="color:#e3116c">"\"SELECT plainto_tsquery($1)::text;\", [\"-- (SMPOL)\nDROP sampletable;-- DROP/*comment*/\\\"sampletable SELECT CASE WHEN (1=1) THEN 'A' ELSE 'B' END;\"]\n\\\" \n'The Fat &amp; Rats:C' \n"</span><span class="token punctuation" style="color:#393A34">]</span><br></span></code></pre></div></div>
<p>Or, more readable:</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">["'select' &amp; 'plainto' &amp; 'tsquery' &amp; '1' &amp; 'text' &amp; 'smpol' &amp; 'drop' &amp; 'sampletable' &amp; 'drop' &amp; 'comment' &amp; 'sampletable' &amp; 'select' &amp; 'case' &amp; 'when' &amp; '1' &amp; '1' &amp; 'then' &amp; 'a' &amp; 'else' &amp; 'b' &amp; 'end' &amp; 'the' &amp; 'fat' &amp; 'rats' &amp; 'c'"]</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="ts_query-language-option">TS_Query language option<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#ts_query-language-option" class="hash-link" aria-label="Direct link to TS_Query language option" title="Direct link to TS_Query language option" translate="no">​</a></h3>
<p>We can remove many unwanted words by using the 'english' option:</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">query </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"What is the purpose of Req.Test.OwnershipError? There does not seem to be any documentation associated with it."</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 atom symbol" style="color:#36acaa">:ok</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 plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Repo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">query</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"SELECT plainto_tsquery('english',$1)::text;"</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">query</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 operator" style="color:#393A34">-&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"'purpos' &amp; 'req.test.ownershiperror' &amp; 'seem' &amp; 'document' &amp; 'associ'"</span><span class="token punctuation" style="color:#393A34">]</span><br></span></code></pre></div></div>
<p>The function also converts the words we enter into <a href="https://en.wikipedia.org/wiki/" target="_blank" rel="noopener noreferrer" class="">lexemes</a>, which means that e.g. <code>purpose</code> and <code>purposes</code> are mapped to <code>purpos</code> and <code>associated</code> becomes <code>associ</code> (as would <code>association</code>)</p>
<p>However, this is often not what we want, either. If we take a simple query like:</p>
<blockquote>
<p>What is req?</p>
</blockquote>
<p>only <code>req</code> survives, and the intention of the question is gone:</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">{</span><span class="token atom symbol" style="color:#36acaa">:ok</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 plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Repo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">query</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"SELECT plainto_tsquery('english', $1)::text;"</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 string" style="color:#e3116c">"what is req?"</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 operator" style="color:#393A34">-&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"'req'"</span><span class="token punctuation" style="color:#393A34">]</span><br></span></code></pre></div></div>
<p>But before we dive into that, let's make sure we only get true results back:</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="oh-postgres-why-cant-you-be-true">Oh postgres, why can't you be true?<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#oh-postgres-why-cant-you-be-true" class="hash-link" aria-label="Direct link to Oh postgres, why can't you be true?" title="Direct link to Oh postgres, why can't you be true?" translate="no">​</a></h2>
<p>We have made the changes so that only matches considered "true" are represented:</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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"># update_one: only allow matches via the @@ operator:</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">get_fulltext_results_update_one</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">library_name</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> search_term</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> limit </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 keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token module class-name">Repo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">all</span><span class="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">from</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">c </span><span class="token operator" style="color:#393A34">in</span><span class="token plain"> </span><span class="token module class-name">Chunk</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">      </span><span class="token attr-name" style="color:#00a4db">cross_join:</span><span class="token plain"> query </span><span class="token operator" style="color:#393A34">in</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fragment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"plainto_tsquery(?)"</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">search_term</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 theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">      </span><span class="token attr-name" style="color:#00a4db">where:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fragment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"f1 @@ search"</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"># &lt;--- here</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token attr-name" style="color:#00a4db">where:</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">library </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">^</span><span class="token plain">library_name</span><span class="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 attr-name" style="color:#00a4db">order_by:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token attr-name" style="color:#00a4db">desc:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fragment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ts_rank(search, f1)"</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 attr-name" style="color:#00a4db">select:</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">id:</span><span class="token plain"> c</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">source:</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">source</span><span class="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 attr-name" style="color:#00a4db">content:</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">content</span><span class="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 attr-name" style="color:#00a4db">rank:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fragment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ts_rank(search, f1) as rank"</span><span class="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 attr-name" style="color:#00a4db">limit:</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">^</span><span class="token plain">limit</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">end</span><br></span></code></pre></div></div>
<p>As expected, we now get an empty result:</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">query </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"""</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">What is the purpose of Req.Test.OwnershipError? There does not seem to be any documentation associated with it.</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">"""</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 module class-name">IO</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">puts </span><span class="token string" style="color:#e3116c">"No 'english':"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">fulltext_boolean_matches </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Topics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Rag</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get_fulltext_results_update_one</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"req"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> query</span><span class="token punctuation" 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><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 operator" style="color:#393A34">-&gt;</span><span class="token plain"> </span><span class="token module class-name">No</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'english'</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 theme-code-block-highlighted-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><span class="token comment" style="color:#999988;font-style:italic"># &lt;-- see? empty!</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></code></pre></div></div>
<p>With the matches solved, let's explore the results when we use the 'english' option of <a href="https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES" target="_blank" rel="noopener noreferrer" class="">plainto_tsquery</a>:</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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"># update_two: using plainto_tsquery('english', ?)</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">get_fulltext_results_update_two</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">library_name</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> search_term</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> limit </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 keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token module class-name">Repo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">all</span><span class="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">from</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">c </span><span class="token operator" style="color:#393A34">in</span><span class="token plain"> </span><span class="token module class-name">Chunk</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">      </span><span class="token attr-name" style="color:#00a4db">cross_join:</span><span class="token plain"> query </span><span class="token operator" style="color:#393A34">in</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fragment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"plainto_tsquery('english', ?)"</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">search_term</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 attr-name" style="color:#00a4db">where:</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">library </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">^</span><span class="token plain">library_name</span><span class="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 attr-name" style="color:#00a4db">order_by:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token attr-name" style="color:#00a4db">desc:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fragment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ts_rank(search, f1, 32)"</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 attr-name" style="color:#00a4db">select:</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">id:</span><span class="token plain"> c</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">source:</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">source</span><span class="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 attr-name" style="color:#00a4db">content:</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">content</span><span class="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 attr-name" style="color:#00a4db">rank:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fragment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ts_rank(search, f1) as rank"</span><span class="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 attr-name" style="color:#00a4db">limit:</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">^</span><span class="token plain">limit</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">end</span><br></span></code></pre></div></div>
<p>Let's see how much better our search has become through the <code>'english'</code> option:</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token module class-name">IO</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">puts </span><span class="token string" style="color:#e3116c">"With 'english':"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">query </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"""</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">What is the purpose of Req.Test.OwnershipError? There does not seem to be any documentation associated with it.</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">"""</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">fulltext_boolean_matches </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Topics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Rag</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get_fulltext_results_update_two</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"req"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> query</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10</span><span class="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 operator" style="color:#393A34">-&gt;</span><span class="token plain"> </span><span class="token module class-name">With</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'english'</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 theme-code-block-highlighted-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><span class="token comment" style="color:#999988;font-style:italic"># &lt;-- see? still empty!</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></code></pre></div></div>
<p><strong>Wow, still no luck.</strong> As explained above, documents would have to fulfill a lot of <em><code>AND-</code>ed</em> criteria. But what about a shorter query?</p>
<blockquote>
<p>What is req?</p>
</blockquote>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">iex</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain">  </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Topics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Rag</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get_fulltext_results_update_two</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"req"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"What is req?"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">50</span><span class="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">|&gt;</span><span class="token plain"> </span><span class="token module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">frequencies_by</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">fn</span><span class="token plain"> chunk </span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">chunk</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">source</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">end</span><span class="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 operator" style="color:#393A34">-&gt;</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><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 string" style="color:#e3116c">"CHANGELOG.md"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </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><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 string" style="color:#e3116c">"README.md"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</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 punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"Req.Request.html"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5</span><span class="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 string" style="color:#e3116c">"Req.Steps.html"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">6</span><span class="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 string" style="color:#e3116c">"Req.Test.html"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</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><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 string" style="color:#e3116c">"Req.html"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">7</span><span class="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 string" style="color:#e3116c">"changelog.html"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5</span><span class="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 string" style="color:#e3116c">"lib/req.ex"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5</span><span class="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 string" style="color:#e3116c">"lib/req/finch.ex"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</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 punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"lib/req/request.ex"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </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><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 string" style="color:#e3116c">"lib/req/steps.ex"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</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><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 string" style="color:#e3116c">"lib/req/test.ex"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</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 punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"readme.html"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</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"></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>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="wishful-thinking">Wishful thinking<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#wishful-thinking" class="hash-link" aria-label="Direct link to Wishful thinking" title="Direct link to Wishful thinking" translate="no">​</a></h3>
<p>So far, the results are disappointing: We still get no matches for our long query:</p>
<blockquote>
<p>"What is the purpose of Req.Test.OwnershipError? There does not seem to be any documentation associated with it."</p>
</blockquote>
<p>And we get many too many (and random) results for the short one.</p>
<blockquote>
<p>"What is req?"</p>
</blockquote>
<p>This is understandable as the PostgreSQL full-text - search functions are meant to search for matches against short search terms, e.g. to filter articles containing word "Quantum Computing".</p>
<p>Can we get more sensible results with <code>ts_rank</code>?</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="intro-ts_rank">Intro: Ts_rank<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#intro-ts_rank" class="hash-link" aria-label="Direct link to Intro: Ts_rank" title="Direct link to Intro: Ts_rank" translate="no">​</a></h3>
<p>Instead of doing a boolean <code>search @@ plain_totsquery</code> we can use <code>ts_rank</code>. <code>ts_rank</code> counts how often a lexeme is appearing in a text. As our chunks are in a similar length range, we give no <em>penalty</em> for long documents (see <a href="https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-RANKING" target="_blank" rel="noopener noreferrer" class="">this part of the postgres documentation</a> for details)</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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"># update_three: dropping the @@ operator</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 function" style="color:#d73a49">get_fulltext_results_update_three</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">library_name</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> search_term</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> limit </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 keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token module class-name">Repo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">all</span><span class="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">from</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">c </span><span class="token operator" style="color:#393A34">in</span><span class="token plain"> </span><span class="token module class-name">Chunk</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">      </span><span class="token attr-name" style="color:#00a4db">cross_join:</span><span class="token plain"> query </span><span class="token operator" style="color:#393A34">in</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fragment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"plainto_tsquery('english', ?)"</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">search_term</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 attr-name" style="color:#00a4db">where:</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">library </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">^</span><span class="token plain">library_name</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">      </span><span class="token attr-name" style="color:#00a4db">order_by:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token attr-name" style="color:#00a4db">desc:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fragment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ts_rank(search, f1, 32)"</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"># &lt;-- look here!</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token attr-name" style="color:#00a4db">select:</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">id:</span><span class="token plain"> c</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">source:</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">source</span><span class="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 attr-name" style="color:#00a4db">content:</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">content</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">rank:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fragment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ts_rank(search, f1) as rank"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic"># &lt;-- and here !!</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 attr-name" style="color:#00a4db">limit:</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">^</span><span class="token plain">limit</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">end</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="is-getting-better">Is getting better?<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#is-getting-better" class="hash-link" aria-label="Direct link to Is getting better?" title="Direct link to Is getting better?" translate="no">​</a></h3>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">iex</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Topics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Rag</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get_fulltext_results_update_three</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"req"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> query</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">7</span><span class="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 operator" style="color:#393A34">-&gt;</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 punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"Req.Test.OwnershipError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.17241628468036652</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 string" style="color:#e3116c">"lib/req/test/ownership.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.0030119779985398054</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 string" style="color:#e3116c">"LICENSE.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.0021660723723471165</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 string" style="color:#e3116c">"LICENSE.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.3680694621598377e-7</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 string" style="color:#e3116c">"Req.Request.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">9.999999682655225e-21</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 string" style="color:#e3116c">"Req.Request.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">9.999999682655225e-21</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 string" style="color:#e3116c">"Req.DecompressError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">9.999999682655225e-21</span><span class="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><strong><em>Much better</em></strong>. There is still some noise, but we yield workable results. Looks like the values are very small, but least two of the top-4 chunks are relevant.</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"Req.Test.OwnershipError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.17241628468036652</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 theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"lib/req/test/ownership.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token number" style="color:#36acaa">0.0030119779985398054</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 string" style="color:#e3116c">"LICENSE.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.0021586192306131124</span><span class="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 string" style="color:#e3116c">"Req.DecompressError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">9.999999682655225e-21</span><span class="token punctuation" style="color:#393A34">]</span><br></span></code></pre></div></div>
<p>Let's re-introduce the cut-off, maybe at <code>0.003</code>? It's also time to clean up our query a bit:</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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"># Update_four: refactoring and introduction of min-rank</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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">get_fulltext_results_update_four</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">library_name</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> search_term</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> limit </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 keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  min_rank </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.003</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">  ranked_subquery </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">from</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">c </span><span class="token operator" style="color:#393A34">in</span><span class="token plain"> </span><span class="token module class-name">Chunk</span><span class="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 attr-name" style="color:#00a4db">select:</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">id:</span><span class="token plain"> c</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">source:</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">source</span><span class="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 attr-name" style="color:#00a4db">content:</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">content</span><span class="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 attr-name" style="color:#00a4db">rank:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fragment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ts_rank(search, plainto_tsquery('english', ?))"</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">search_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">      </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 attr-name" style="color:#00a4db">where:</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">library </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">^</span><span class="token plain">library_name</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 module class-name">Repo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">all</span><span class="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">from</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r </span><span class="token operator" style="color:#393A34">in</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">subquery</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ranked_subquery</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 attr-name" style="color:#00a4db">select:</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">id:</span><span class="token plain"> r</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">source:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">source</span><span class="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 attr-name" style="color:#00a4db">rank:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">rank</span><span class="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 attr-name" style="color:#00a4db">content:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">content</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 attr-name" style="color:#00a4db">where:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">rank </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">^</span><span class="token plain">min_rank</span><span class="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 attr-name" style="color:#00a4db">order_by:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token attr-name" style="color:#00a4db">desc:</span><span class="token plain"> r</span><span class="token punctuation" 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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token attr-name" style="color:#00a4db">limit:</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">^</span><span class="token plain">limit</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">end</span><br></span></code></pre></div></div>
<p>Here we go:</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">iex</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Topics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Rag</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get_fulltext_results_update_four</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"req"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> query</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">7</span><span class="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 operator" style="color:#393A34">-&gt;</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 punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"Req.Test.OwnershipError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.17241628468036652</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 string" style="color:#e3116c">"lib/req/test/ownership.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.0030119779985398054</span><span class="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></code></pre></div></div>
<p>Neat. This is how we want it. It is important that all searches have a cut-off. Eliminating bad matches allows the use of
simple strategies like <code>reciprocal_rank_fusion</code> without further cleanup down the line.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="but-wait---what-about-partial-matches">But wait - what about partial matches?<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#but-wait---what-about-partial-matches" class="hash-link" aria-label="Direct link to But wait - what about partial matches?" title="Direct link to But wait - what about partial matches?" translate="no">​</a></h4>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">query_for_ownership_error </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"OwnershipError"</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">fulltext_boolean_matches </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Topics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Rag</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get_fulltext_results_2f5cbfa_3</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"req"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> query_for_ownership_error</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">7</span><span class="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 module class-name">Enum</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">fulltext_boolean_matches</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">fn</span><span class="token plain"> chunk </span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">chunk</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">source</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> chunk</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">rank</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">end</span><span class="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 operator" style="color:#393A34">-&gt;</span><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><strong><em>Buhuuu .....</em></strong></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="no-partial-matches">No partial matches<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#no-partial-matches" class="hash-link" aria-label="Direct link to No partial matches" title="Direct link to No partial matches" translate="no">​</a></h3>
<p><strong>TS_Vector</strong> is not magical if it comes to matching <strong><em>OwnershipError</em></strong> against <strong><em>Req.Test.OwnershipError</em></strong>. This, of course, is not ideal.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="enter-trigrams-pg_trgm">Enter Trigrams: pg_trgm<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#enter-trigrams-pg_trgm" class="hash-link" aria-label="Direct link to Enter Trigrams: pg_trgm" title="Direct link to Enter Trigrams: pg_trgm" translate="no">​</a></h2>
<p>The PostgreSQL extension <a href="https://www.postgresql.org/docs/current/pgtrgm.html" target="_blank" rel="noopener noreferrer" class="">pgtrgm</a> offers us partial matches. To use it, we need to activate it.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-setup">The setup<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#the-setup" class="hash-link" aria-label="Direct link to The setup" title="Direct link to The setup" translate="no">​</a></h3>
<p>If you are not running the database connection as a root user (and you really should not), you might need to add this manually:</p>
<p>psql&gt; CREATE EXTENSION IF NOT EXISTS pg_trgm;</p>
<p>Now we can add the index <code>gin_trgm_ops</code> to chunks in a migration</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">defmodule</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Repo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Migrations</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">AddPgTrgm</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</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">use</span><span class="token plain"> </span><span class="token module class-name">Ecto</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Migration</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">def</span><span class="token plain"> up </span><span class="token keyword" style="color:#00009f">do</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">execute</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"""</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">      CREATE INDEX IF NOT EXISTS chunks_content_trgm_idx</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">      ON chunks</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">      USING GIN (content gin_trgm_ops);</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">    """</span><span class="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">end</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">def</span><span class="token plain"> down </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    execute </span><span class="token string" style="color:#e3116c">"DROP INDEX IF EXISTS chunks_content_trgm_idx;"</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">end</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">end</span><br></span></code></pre></div></div>
<p>Our function looks very similar to <code>get_fulltext_results_update_four</code>:</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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" 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">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">get_trgm_results</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">library_name</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> query_string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> limit </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 keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  min_score </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.065</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">  ranked_subquery </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">from</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">c </span><span class="token operator" style="color:#393A34">in</span><span class="token plain"> </span><span class="token module class-name">Chunk</span><span class="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 attr-name" style="color:#00a4db">select:</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">id:</span><span class="token plain"> c</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">source:</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">source</span><span class="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 attr-name" style="color:#00a4db">content:</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">content</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">score:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fragment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"similarity(?, ?)"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">content</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">query_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 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 attr-name" style="color:#00a4db">where:</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">library </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">^</span><span class="token plain">library_name</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 module class-name">Repo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">all</span><span class="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">from</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r </span><span class="token operator" style="color:#393A34">in</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">subquery</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ranked_subquery</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 attr-name" style="color:#00a4db">select:</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">id:</span><span class="token plain"> r</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">source:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">source</span><span class="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 attr-name" style="color:#00a4db">score:</span><span class="token plain"> r</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"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token attr-name" style="color:#00a4db">content:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">content</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 attr-name" style="color:#00a4db">where:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">score </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">^</span><span class="token plain">min_score</span><span class="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 attr-name" style="color:#00a4db">order_by:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token attr-name" style="color:#00a4db">desc:</span><span class="token plain"> r</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 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 attr-name" style="color:#00a4db">limit:</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">^</span><span class="token plain">limit</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">end</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></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-first-trgm-query">The first trgm Query<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#the-first-trgm-query" class="hash-link" aria-label="Direct link to The first trgm Query" title="Direct link to The first trgm Query" translate="no">​</a></h3>
<p>Let's try the one-word-query that failed with <code>ts_vector</code> again, this time with <code>trgm</code>. We had 4 matches for <code>Test.OwnershipError</code>, so let's see how the leaderboard looks like over here:</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">tgrm_matches </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Topics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Rag</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get_trgm_results</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"req"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"OwnershipError"</span><span class="token punctuation" 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" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">-&gt;</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 punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"lib/req/test/ownership.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.09677419066429138</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 string" style="color:#e3116c">"lib/req/test/ownership_error.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.07389162480831146</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 string" style="color:#e3116c">"lib/req/application.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.0662650614976883</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 string" style="color:#e3116c">"changelog.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.04237288236618042</span><span class="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>Great! We now get the partial match we are looking for! But why do we find <code>changelog.html</code> and <code>lib/req/application.ex</code> in the search results?</p>
<div class="language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token number" style="color:#36acaa">01</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic"># lib/req/application.ex</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token number" style="color:#36acaa">02</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token number" style="color:#36acaa">03</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">defmodule</span><span class="token plain"> </span><span class="token module class-name">Req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Application</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</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"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token number" style="color:#36acaa">09</span><span class="token plain">  </span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">start</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">_type</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> _args</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token number" style="color:#36acaa">10</span><span class="token plain">    children </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"></span><span class="token number" style="color:#36acaa">11</span><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">{</span><span class="token module class-name">Finch</span><span class="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 number" style="color:#36acaa">12</span><span class="token plain">       </span><span class="token attr-name" style="color:#00a4db">name:</span><span class="token plain"> </span><span class="token module class-name">Req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Finch</span><span class="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 number" style="color:#36acaa">13</span><span class="token plain">       </span><span class="token attr-name" style="color:#00a4db">pools:</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token number" style="color:#36acaa">14</span><span class="token plain">         </span><span class="token attr-name" style="color:#00a4db">default:</span><span class="token plain"> </span><span class="token module class-name">Req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Finch</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">pool_options</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><span class="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 number" style="color:#36acaa">15</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 number" style="color:#36acaa">16</span><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">{</span><span class="token module class-name">DynamicSupervisor</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">strategy:</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:one_for_one</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">name:</span><span class="token plain"> </span><span class="token module class-name">Req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">FinchSupervisor</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 number" style="color:#36acaa">17</span><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token module class-name">Req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Test</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Ownership</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">name:</span><span class="token plain"> </span><span class="token module class-name">Req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Test</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Ownership</span><span class="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 number" style="color:#36acaa">18</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 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 number" style="color:#36acaa">22</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">end</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></code></pre></div></div>
<p>Ah. The <code>Req.Test.Ownership</code> in line 17. Looks like the <code>trgm</code> is finding occurrences that <code>ts_vector</code> is missing. But what about <code>changelog.html</code>?</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">   "&lt;/span&gt;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    &lt;span class=\"p\" data-group-id=\"3838964925-12\"&gt;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    {&lt;/span&gt;&lt;span class=\"ss\"&gt;:error&lt;/span&gt;&lt;span class=\"p\"&gt;,</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    &lt;/span&gt;&lt;span class=\"w\"&gt; &lt;/span&gt;&lt;span class=\"n\"&gt;reason&lt;/span&gt;&lt;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    span class=\"p\" data-group-id=\"3838964925-12\"&gt;}&lt;/span&gt;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    &lt;span class=\"w\"&gt; &lt;/span&gt;&lt;span class=\"o\"&gt;-&amp;gt;&lt;/span&gt;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    &lt;span class=\"w\"&gt;\n&lt;/span&gt;&lt;span class=\"p\" data-group-id=\"3838964925-13\"&gt;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    {&lt;/span&gt;&lt;span class=\"n\"&gt;request&lt;/span&gt;&lt;span class=\"p\"&gt;,&lt;/span&gt;&lt;span class=\"w\"&gt; &lt;/span&gt;&lt;span class=\"nc\"&gt;RuntimeError&lt;/span&gt;&lt;span class=\"o\"&gt;.&lt;/span&gt;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    &lt;span class=\"n\"&gt;exception&lt;/span&gt;&lt;span class=\"p\" data-group-id=\"3838964925-14\"&gt;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    (&lt;/span&gt;&lt;span class=\"n\"&gt;inspect&lt;/span&gt;&lt;span class=\"p\" data-group-id=\"3838964925-15\"&gt;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    (&lt;/span&gt;&lt;span class=\"n\"&gt;reason&lt;/span&gt;&lt;span class=\"p\" data-group-id=\"3838964925-15\"&gt;)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    &lt;/span&gt;&lt;span class=\"p\" data-group-id=\"3838964925-14\"&gt;)&lt;/span&gt;&lt;span class=\"p\" data-group-id=\"3838964925-13\"&gt;}&lt;/span&gt;&lt;span class=\"w\"&gt;\n  &lt;/span&gt;&lt;span class=\"k\" data-group-id=\"3838964925-5\"&gt;end&lt;/span&gt;&lt;span class=\"w\"&gt;\n&lt;/span&gt;&lt;span class=\"k\" data-group-id=\"3838964925-1\"&gt;end&lt;/span&gt;&lt;span class=\"w\"&gt;"],</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span></code></pre></div></div>
<p>Is <code>pg_trgm</code> confused by the html? Let's find out! We ingest the same documents again, but convert html files to markdown:</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token atom symbol" style="color:#36acaa">:ok</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Topics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Rag</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ingest_from_hex</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"req"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"0.5.10"</span><span class="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 attr-name" style="color:#00a4db">chunk_size:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">6000</span><span class="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 attr-name" style="color:#00a4db">chunk_overlap:</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 theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">  </span><span class="token attr-name" style="color:#00a4db">convert_html_to_md:</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 punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>Better now?</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">query</span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"""</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">I am trying to get a resource from a zipped connection (using ny own battle proven</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">Apache sever), but I keep getting an Req.ArchiveError. What is going on here?</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">"""</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">tgrm_matches </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Topics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Rag</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get_trgm_results</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"req"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> query</span><span class="token punctuation" 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" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></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 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 string" style="color:#e3116c">"Req.ArchiveError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.1906779706478119</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 string" style="color:#e3116c">"api-reference.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.1855670064687729</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 string" style="color:#e3116c">"Req.TransportError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.18149466812610626</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 string" style="color:#e3116c">"Req.Request.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.17661097645759583</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 string" style="color:#e3116c">"Req.HTTPError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.1760299652814865</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 string" style="color:#e3116c">"lib/req/test/ownership_error.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.1631944477558136</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 string" style="color:#e3116c">"CHANGELOG.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.16225165128707886</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 string" style="color:#e3116c">"lib/req/transport_error.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.16044776141643524</span><span class="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>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="semantic-search-with-embedding-models">Semantic Search with Embedding Models<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#semantic-search-with-embedding-models" class="hash-link" aria-label="Direct link to Semantic Search with Embedding Models" title="Direct link to Semantic Search with Embedding Models" translate="no">​</a></h2>
<p>So now we have seen <code>ts_vector</code> and <code>pg_trgm</code>. Both are powerful, but operate entirely on textual similarity. For instance, this works well:</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">query = """</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">A file was incomplete. I get a server error</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">tgrm_matches = Exmeralda.Topics.Rag.get_trgm_results("req", query, 8)</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">-&gt; ["lib/req/decompress_error.ex", 0.1071428582072258]</span><br></span></code></pre></div></div>
<p>But this doesn't:</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">query = """</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">A file was incomplete. I get a server a fault</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">the_long_ownership_error_length = String.length(query)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">tgrm_matches = Exmeralda.Topics.Rag.get_trgm_results("req", query, 8)</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><br></span></code></pre></div></div>
<p>For both, ts_rank and trgm, <code>server fault</code> and <code>server error</code> are different things entirely.</p>
<p>Ok, admittedly we are trying hard to avoid the word <code>error</code> because that already gives our system the right idea, but you might get the idea. Imagine we are searching for a piece of code, and the functions and variables in question are just named entirely different?</p>
<p>If you are not familiar with semantic search through embedding models <a href="https://bitcrowd.dev/a-rag-library-for-elixir" target="_blank" rel="noopener noreferrer" class="">you might want to read up on it with this post</a></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="installing-pg_vector">Installing pg_vector<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#installing-pg_vector" class="hash-link" aria-label="Direct link to Installing pg_vector" title="Direct link to Installing pg_vector" translate="no">​</a></h3>
<p>Again, you need to install an extension with elevated privileges:</p>
<div class="language-SQL 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">psql</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> EXTENSION </span><span class="token keyword" style="color:#00009f">IF</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">NOT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">EXISTS</span><span class="token plain"> vector</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>With this, we can add columns with the type vector.</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">defmodule</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Repo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Migrations</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">AddEmbeddings</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</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">use</span><span class="token plain"> </span><span class="token module class-name">Ecto</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Migration</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">def</span><span class="token plain"> change </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    alter </span><span class="token function" style="color:#d73a49">table</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"chunks"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      add </span><span class="token atom symbol" style="color:#36acaa">:embedding_jina_v2_code</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:vector</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">size:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">768</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">null:</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">      add </span><span class="token atom symbol" style="color:#36acaa">:embedding_all_minilm_l6_v2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:vector</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">size:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">384</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">null:</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">      add </span><span class="token atom symbol" style="color:#36acaa">:embedding_nomic_embed_text</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:vector</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">size:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">768</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">null:</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">      add </span><span class="token atom symbol" style="color:#36acaa">:embedding_mxbai_embed_large</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:vector</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">size:</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><span class="token attr-name" style="color:#00a4db">null:</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 keyword" style="color:#00009f">end</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">end</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">end</span><br></span></code></pre></div></div>
<p>This is a collection of well know models. <a href="https://livebook.dev/run/?url=https%3A%2F%2Fgithub.com%2Fmjrusso%2Fhex2txt%2Fblob%2Fmain%2Fnotebooks%2Fhex2context.livemd" target="_blank" rel="noopener noreferrer" class="">hex2context</a> uses <code>all_minilm_l6_v2</code> while <a href="https://hex.pm/packages/hexdocs_mcp" target="_blank" rel="noopener noreferrer" class="">hexdocs_mcp</a> uses <code>mxbai_embed_large</code> and has used <code>nomic_embed_text</code>. <a href="https://exmeralda.chat/" target="_blank" rel="noopener noreferrer" class="">Exmeralda</a> uses Jina.</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token atom symbol" style="color:#36acaa">:ok</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Topics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Rag</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ingest_from_hex</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"req"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"0.5.10"</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 attr-name" style="color:#00a4db">chunk_size:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">6000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token attr-name" style="color:#00a4db">chunk_overlap:</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 attr-name" style="color:#00a4db">convert_html_to_md:</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 attr-name" style="color:#00a4db">embedding_models:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token atom symbol" style="color:#36acaa">:jina_v2_code</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:nomic_embed_text</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:all_minilm_l6_v2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:mxbai_embed_large</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>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">query </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"""</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">A file was incomplete. I get a server a server fault!</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">"""</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">semantic_matches </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Topics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Rag</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">semantic_search</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"req"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> query</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 atom symbol" style="color:#36acaa">:jina_v2_code</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:nomic_embed_text</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:all_minilm_l6_v2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:mxbai_embed_large</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 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" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">-&gt;</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 atom symbol" style="color:#36acaa">:mxbai_embed_large</span><span class="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 string" style="color:#e3116c">"Req.ArchiveError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8825895652492148</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 string" style="color:#e3116c">"Req.Steps.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8861386254170186</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 string" style="color:#e3116c">"CHANGELOG.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8877450658370508</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 string" style="color:#e3116c">"lib/req/archive_error.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8882296960744931</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 string" style="color:#e3116c">"CHANGELOG.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8982783964506256</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 string" style="color:#e3116c">"lib/req/steps.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.9088519660172513</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 string" style="color:#e3116c">"Req.DecompressError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.909644379977974</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 string" style="color:#e3116c">"api-reference.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.9114014126397327</span><span class="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 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 punctuation" style="color:#393A34">]</span><br></span></code></pre></div></div>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>Here you can find the results from <code>:jina_v2_code</code>, <code>:all_minilm_l6_v2</code> and <code>:nomic_embed_text</code></summary><div><div class="collapsibleContent_i85q"><div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="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 atom symbol" style="color:#36acaa">:jina_v2_code</span><span class="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 string" style="color:#e3116c">"lib/req/decompress_error.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.1001978869605287</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 string" style="color:#e3116c">"lib/req/archive_error.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.1065949543349611</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 string" style="color:#e3116c">"Req.ArchiveError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.1109306281584121</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 string" style="color:#e3116c">"lib/req/finch.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.1283726470574522</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 string" style="color:#e3116c">"Req.HTTPError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.1437832728481354</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 string" style="color:#e3116c">"changelog.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.1477073891751752</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 string" style="color:#e3116c">"Req.Test.OwnershipError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.1518759602352782</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 string" style="color:#e3116c">"Req.DecompressError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.1523340774388808</span><span class="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><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 atom symbol" style="color:#36acaa">:all_minilm_l6_v2</span><span class="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 string" style="color:#e3116c">"CHANGELOG.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.187255884476853</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 string" style="color:#e3116c">"api-reference.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.1992400226507995</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 string" style="color:#e3116c">"CHANGELOG.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.214449018027894</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 string" style="color:#e3116c">"Req.DecompressError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.2167737539185737</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 string" style="color:#e3116c">"Req.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.2187207047291677</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 string" style="color:#e3116c">"Req.ArchiveError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.2209354759338176</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 string" style="color:#e3116c">"lib/req/transport_error.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.223696768657922</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 string" style="color:#e3116c">"Req.HTTPError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.2254885691837656</span><span class="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><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 atom symbol" style="color:#36acaa">:nomic_embed_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">    </span><span class="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 string" style="color:#e3116c">"Req.HTTPError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.9255421003661427</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 string" style="color:#e3116c">"Req.ArchiveError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.9344331806147583</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 string" style="color:#e3116c">"Req.TransportError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.9396665969010519</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 string" style="color:#e3116c">"lib/req/archive_error.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.9512982607615971</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 string" style="color:#e3116c">"lib/req/http_error.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.9652939336935658</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 string" style="color:#e3116c">"Req.DecompressError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.9682236133489371</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 string" style="color:#e3116c">"lib/req/transport_error.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.9696966898486066</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 string" style="color:#e3116c">"Req.ChecksumMismatchError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.9715665051662066</span><span class="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><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 atom symbol" style="color:#36acaa">:mxbai_embed_large</span><span class="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 string" style="color:#e3116c">"Req.ArchiveError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8825895652492148</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 string" style="color:#e3116c">"Req.Steps.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8861386254170186</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 string" style="color:#e3116c">"CHANGELOG.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8877450658370508</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 string" style="color:#e3116c">"lib/req/archive_error.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8882296960744931</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 string" style="color:#e3116c">"CHANGELOG.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8982783964506256</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 string" style="color:#e3116c">"lib/req/steps.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.9088519660172513</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 string" style="color:#e3116c">"Req.DecompressError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.909644379977974</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 string" style="color:#e3116c">"api-reference.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.9114014126397327</span><span class="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></div></div></details>
<p>Most models get a hint of the correct files. Given the broadness of the input, this is not too bad. How are the distances when there is no obvious match?</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">query </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"""</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">What is the capitol of france?</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">"""</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">semantic_matches </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Topics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Rag</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">semantic_search</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"req"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> query</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 atom symbol" style="color:#36acaa">:jina_v2_code</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:nomic_embed_text</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:all_minilm_l6_v2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:mxbai_embed_large</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 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" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">-&gt;</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 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 atom symbol" style="color:#36acaa">:jina_v2_code</span><span class="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 string" style="color:#e3116c">"search.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.2013792773813514</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 string" style="color:#e3116c">"changelog.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.2675223081081528</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 string" style="color:#e3116c">"lib/req/test/ownership.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.27652497253914</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 string" style="color:#e3116c">"Req.Steps.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.2884749489577967</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 string" style="color:#e3116c">"Req.Test.OwnershipError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.2887939635806138</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 string" style="color:#e3116c">"lib/req/transport_error.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.2910635011599565</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 string" style="color:#e3116c">"lib/req/checksum_mismatch_error.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.2951447768272124</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 string" style="color:#e3116c">".formatter.exs"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.3046684492170826</span><span class="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><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 operator" style="color:#393A34">...</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>Here you can find the results from <code>:mxbai_embed_large</code>, <code>:all_minilm_l6_v2</code> and <code>:nomic_embed_text</code></summary><div><div class="collapsibleContent_i85q"><div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="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 atom symbol" style="color:#36acaa">:jina_v2_code</span><span class="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 string" style="color:#e3116c">"search.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.2013792773813514</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 string" style="color:#e3116c">"changelog.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.2675223081081528</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 string" style="color:#e3116c">"lib/req/test/ownership.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.27652497253914</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 string" style="color:#e3116c">"Req.Steps.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.2884749489577967</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 string" style="color:#e3116c">"Req.Test.OwnershipError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.2887939635806138</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 string" style="color:#e3116c">"lib/req/transport_error.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.2910635011599565</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 string" style="color:#e3116c">"lib/req/checksum_mismatch_error.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.2951447768272124</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 string" style="color:#e3116c">".formatter.exs"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.3046684492170826</span><span class="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><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 atom symbol" style="color:#36acaa">:all_minilm_l6_v2</span><span class="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 string" style="color:#e3116c">"Req.Request.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.3125475012040022</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 string" style="color:#e3116c">"changelog.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.326029751652449</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 string" style="color:#e3116c">"Req.Request.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.347188674972259</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 string" style="color:#e3116c">"Req.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.34829745311331</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 string" style="color:#e3116c">"Req.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.350020261895043</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 string" style="color:#e3116c">"lib/req/utils.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.351296238774175</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 string" style="color:#e3116c">"lib/req/test/ownership.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.3532148635308432</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 string" style="color:#e3116c">"Req.Steps.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.3565381583398395</span><span class="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><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 atom symbol" style="color:#36acaa">:nomic_embed_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">    </span><span class="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 string" style="color:#e3116c">"Req.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.9798611585883052</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 string" style="color:#e3116c">"Req.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.9798611585883052</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 string" style="color:#e3116c">"search.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.9825806991787288</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 string" style="color:#e3116c">"CHANGELOG.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.0566829877389166</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 string" style="color:#e3116c">"Req.ChecksumMismatchError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.0655728951074148</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 string" style="color:#e3116c">"Req.Test.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.066903190664944</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 string" style="color:#e3116c">"Req.Test.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.066903190664944</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 string" style="color:#e3116c">"Req.Steps.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.0781216828668398</span><span class="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><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 atom symbol" style="color:#36acaa">:mxbai_embed_large</span><span class="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 string" style="color:#e3116c">"search.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.1419219912593435</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 string" style="color:#e3116c">"Req.Request.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.177317278184068</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 string" style="color:#e3116c">"Req.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.1789670853942418</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 string" style="color:#e3116c">"lib/req.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.1825928941494814</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 string" style="color:#e3116c">"Req.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.184483360627748</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 string" style="color:#e3116c">"Req.Response.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.1891909155259033</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 string" style="color:#e3116c">"README.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.1894402965235962</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 string" style="color:#e3116c">"lib/req/steps.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.1905512509070648</span><span class="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></div></div></details>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-fuzzy-query-with-ts_rank--trgm-">A fuzzy Query with <code>ts_rank</code> &amp; <code>trgm</code> ...<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#a-fuzzy-query-with-ts_rank--trgm-" class="hash-link" aria-label="Direct link to a-fuzzy-query-with-ts_rank--trgm-" title="Direct link to a-fuzzy-query-with-ts_rank--trgm-" translate="no">​</a></h4>
<p>Before we move on, let's try something hard:</p>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">query </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"""</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">What are the most recent changes in this library?</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">"""</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">iex</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Topics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Rag</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get_fulltext_results_2f5cbfa_3</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"req"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> query</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">7</span><span class="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 theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">-&gt;</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><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">iex</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Topics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Rag</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get_trgm_results</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"req"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> query</span><span class="token punctuation" 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" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">-&gt;</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 theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">".formatter.exs"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.11363636702299118</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 theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"lib/req/transport_error.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.10784313827753067</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 theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"lib/req/test/ownership_error.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.10132158547639847</span><span class="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>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="-and-in-contrast-with-the-embedding-models">... and, in Contrast, with the Embedding Models.<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#-and-in-contrast-with-the-embedding-models" class="hash-link" aria-label="Direct link to ... and, in Contrast, with the Embedding Models." title="Direct link to ... and, in Contrast, with the Embedding Models." translate="no">​</a></h4>
<div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">iex</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> query </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"What are the most recent changes in this library?"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">iex</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Topics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Rag</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">semantic_search</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"req"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> query</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 atom symbol" style="color:#36acaa">:jina_v2_code</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:nomic_embed_text</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:all_minilm_l6_v2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:mxbai_embed_large</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 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" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token attr-name" style="color:#00a4db">jina_v2_code:</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 string" style="color:#e3116c">"changelog.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.6787963813404295</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 string" style="color:#e3116c">"changelog.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.7811186107777701</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 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 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 attr-name" style="color:#00a4db">all_minilm_l6_v2:</span><span class="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 string" style="color:#e3116c">"lib/req/response_async.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8177686940327334</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 string" style="color:#e3116c">"Req.Response.Async.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8348016598126115</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 string" style="color:#e3116c">"CHANGELOG.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8403415712638496</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 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 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 attr-name" style="color:#00a4db">nomic_embed_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"></span><span class="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 string" style="color:#e3116c">"readme.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.034428927580288</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 string" style="color:#e3116c">"lib/req.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.044201525943257</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 string" style="color:#e3116c">"CHANGELOG.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.0647588168422053</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 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 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 attr-name" style="color:#00a4db">mxbai_embed_large:</span><span class="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 string" style="color:#e3116c">"CHANGELOG.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.6185546412466434</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 string" style="color:#e3116c">"CHANGELOG.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.6508479987416208</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 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 punctuation" style="color:#393A34">]</span><br></span></code></pre></div></div>
<p>Here, the results of the embedding models differ a lot: <code>mxbai_embed_large</code> and <code>jina_v2_code</code> get <code>CHANGELOG.md</code> on the
first rank. <code>all_minilm_l6_v2</code> and <code>nomic_embed_text</code> only list <code>CHANGELOG.md</code> on the third place.</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>Again you can find the complete results from <code>:mxbai_embed_large</code>, <code>:all_minilm_l6_v2</code>, <code>:nomic_embed_text</code> and <code>:mxbai_embed_large</code> here</summary><div><div class="collapsibleContent_i85q"><div class="language-Elixir language-elixir codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-elixir 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">query </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"""</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">What are the most recent changes in this library?</span><br></span><span class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">"""</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">semantic_matches </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token module class-name">Exmeralda</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Topics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token module class-name">Rag</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">semantic_search</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"req"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> query</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 atom symbol" style="color:#36acaa">:jina_v2_code</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:nomic_embed_text</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:all_minilm_l6_v2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token atom symbol" style="color:#36acaa">:mxbai_embed_large</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 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">  </span><span class="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 atom symbol" style="color:#36acaa">:jina_v2_code</span><span class="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 string" style="color:#e3116c">"changelog.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.6787963813404295</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 string" style="color:#e3116c">"changelog.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.7811186107777701</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 string" style="color:#e3116c">"readme.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.7866344675718526</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 string" style="color:#e3116c">"lib/req/finch.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8164027820860295</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 string" style="color:#e3116c">"CHANGELOG.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8586129017187336</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 string" style="color:#e3116c">"api-reference.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.9161379148926494</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 string" style="color:#e3116c">"Req.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.9174212832035994</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 string" style="color:#e3116c">"lib/req.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.9254078172251721</span><span class="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><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 atom symbol" style="color:#36acaa">:all_minilm_l6_v2</span><span class="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 string" style="color:#e3116c">"lib/req/response_async.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8177686940327334</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 string" style="color:#e3116c">"Req.Response.Async.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8348016598126115</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 string" style="color:#e3116c">"CHANGELOG.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8403415712638496</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 string" style="color:#e3116c">"lib/req.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.869153003390171</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 string" style="color:#e3116c">"lib/req/steps.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8702174684426948</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 string" style="color:#e3116c">"lib/req/finch.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8776942055053603</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 string" style="color:#e3116c">"lib/req.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8799764263723598</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 string" style="color:#e3116c">"lib/req/steps.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.8882293605494803</span><span class="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><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 atom symbol" style="color:#36acaa">:nomic_embed_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">    </span><span class="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 string" style="color:#e3116c">"readme.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.034428927580288</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 string" style="color:#e3116c">"lib/req.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.044201525943257</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 string" style="color:#e3116c">"CHANGELOG.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.0647588168422053</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 string" style="color:#e3116c">"lib/req/response.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.070890618559592</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 string" style="color:#e3116c">"mix.exs"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.0846005999352963</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 string" style="color:#e3116c">"Req.ChecksumMismatchError.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.0880410657102961</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 string" style="color:#e3116c">"lib/req/steps.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.0933889610591538</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 string" style="color:#e3116c">"Req.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.094495954904274</span><span class="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><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 atom symbol" style="color:#36acaa">:mxbai_embed_large</span><span class="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 string" style="color:#e3116c">"CHANGELOG.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.6185546412466434</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 string" style="color:#e3116c">"CHANGELOG.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.6508479987416208</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 string" style="color:#e3116c">"Req.Response.Async.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.6764724812517728</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 string" style="color:#e3116c">"Req.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.6895154541197983</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 string" style="color:#e3116c">"lib/req/steps.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.6902661595261197</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 string" style="color:#e3116c">"lib/req/response_async.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.6943797380713216</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 string" style="color:#e3116c">"lib/req/request.ex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.6989134223104326</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 string" style="color:#e3116c">"Req.Steps.html"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.6994476796039114</span><span class="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></div></div></details>
<p>Of course, this kind of query is where the embedding models shine, and the text-based search strategies fail.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-systematic-evaluation-of-document-retrieval">A Systematic Evaluation of Document Retrieval<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#a-systematic-evaluation-of-document-retrieval" class="hash-link" aria-label="Direct link to A Systematic Evaluation of Document Retrieval" title="Direct link to A Systematic Evaluation of Document Retrieval" translate="no">​</a></h2>
<p>Now that we have our testing field, let's automate testing:</p>
<ul>
<li class="">For each html file in the docs from hexdocs, we let a commercial LLM devise questions that would be answered by the file.</li>
<li class="">For each match with either the HTML file or the corresponding .ex file, we attribute the candidate a point</li>
<li class="">We expect to get some additional matches for related or unrelated files. We will use the ratio between them to evaluate <code>accuracy</code>, <code>precision</code>, <code>recall</code> and <code>f1 score</code>.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-prompt">The Prompt:<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#the-prompt" class="hash-link" aria-label="Direct link to The Prompt:" title="Direct link to The Prompt:" translate="no">​</a></h3>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary><p>You are given a piece of technical documentation.</p><p>Perform two tasks:</p><ol>
<li class=""><strong>Extract the key assertions</strong></li>
<li class=""><strong>Invent realistic Stack-Overflow-style questions</strong></li>
</ol><p>(...)</p></summary><div><div class="collapsibleContent_i85q"><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">"""</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    You are given a piece of technical documentation.</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">    Perform two tasks:</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">    1. **Extract the key assertions**</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    • Read the text carefully.</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    • List every important assertion the docs make (what the feature *is*, how it *works*, guarantees, limits, options, notes, etc.).</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    • Phrase each important assertion as a single, self-contained sentence.</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    • Outline the assertions that only this document makes and that will most likely be unique to this document in a list.</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">    2. **Invent realistic Stack-Overflow-style questions**</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    • Think of developers encountering issues that this doc resolves.</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    • For **each** imagined user, write:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">     – **problem** : a first-person sentence that includes a tiny code snippet or concrete detail (e.g. `Req.get!("…", into: :self)`).</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">     – **code**    : that mini snippet as a plain string.</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">     – **question**: the direct question they would post (“Why does …?”, “How can I …?”).</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    • Produce 3–10 of these question objects.</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    • Every question must be answerable solely with the assertions from task 1.</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">    The documentation is:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ======= begin documentation =======</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    #{markdown_content}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ======= end documentation =======</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">    **OUTPUT — return only this JSON structure, with no markdown, no comments:**</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><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    "questions": [</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">      "problem": "&lt;first-person problem statement&gt;",</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      "code": "&lt;inline code snippet&gt;",</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      "question": "&lt;direct question&gt;"</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">    /* 3-10 such objects total */</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">    }</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">    RULES:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    • Output must be valid, minified JSON.</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    • Do **not** include answers or any extra keys.</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    • Do **not** mention these instructions or add other text.</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><br></span></code></pre></div></div></div></div></details>
<p>This yields questions like the following:</p>
<div class="language-JS language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="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-property property" style="color:#36acaa">"filename"</span><span class="token operator" style="color:#393A34">:</span><span class="token string" style="color:#e3116c">"Req.ArchiveError.html"</span><span class="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-property property" style="color:#36acaa">"question"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain">"</span><span class="token constant" style="color:#36acaa">I</span><span class="token plain">'m calling </span><span class="token maybe-class-name">Req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">Steps</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">decode_body</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">response</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> on a potentially corrupted</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  archive response and it suddenly raises </span><span class="token maybe-class-name">Req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token known-class-name class-name">ArchiveError</span><span class="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 property-access maybe-class-name">Req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">Steps</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">decode_body</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">response</span><span class="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 maybe-class-name">Why</span><span class="token plain"> am </span><span class="token constant" style="color:#36acaa">I</span><span class="token plain"> getting a </span><span class="token maybe-class-name">Req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token known-class-name class-name">ArchiveError</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">while</span><span class="token plain"> trying to unpack the archive</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><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 punctuation" style="color:#393A34">{</span><span class="token string-property property" style="color:#36acaa">"filename"</span><span class="token operator" style="color:#393A34">:</span><span class="token string" style="color:#e3116c">"Req.ArchiveError.html"</span><span class="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-property property" style="color:#36acaa">"question"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain">"</span><span class="token maybe-class-name">When</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">I</span><span class="token plain"> run </span><span class="token maybe-class-name">Req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">Steps</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">decode_body</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">resp</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> a tar</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">gz</span><span class="token plain"> file</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> the </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> throws a </span><span class="token maybe-class-name">Req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token known-class-name class-name">ArchiveError</span><span class="token plain"> after failing to unpack it</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">\nReq</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">Steps</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">decode_body</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">resp</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">\n</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> </span><span class="token maybe-class-name">What</span><span class="token plain"> causes </span><span class="token maybe-class-name">Req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token known-class-name class-name">ArchiveError</span><span class="token plain"> to be returned </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> decode_body </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token plain"> scenario</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><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 punctuation" style="color:#393A34">{</span><span class="token string-property property" style="color:#36acaa">"filename"</span><span class="token operator" style="color:#393A34">:</span><span class="token string" style="color:#e3116c">"Req.ArchiveError.html"</span><span class="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-property property" style="color:#36acaa">"question"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain">"</span><span class="token constant" style="color:#36acaa">I</span><span class="token plain"> pass an </span><span class="token constant" style="color:#36acaa">HTTP</span><span class="token plain"> response body to</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> </span><span class="token maybe-class-name">Req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">Steps</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">decode_body</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> hoping it will uncompress automatically</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> but it raises</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> </span><span class="token maybe-class-name">Req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token known-class-name class-name">ArchiveError</span><span class="token plain"> instead</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">\nReq</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">Steps</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">decode_body</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">my_response</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">\n</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> </span><span class="token maybe-class-name">How</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">do</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">I</span><span class="token plain"> handle </span><span class="token maybe-class-name">Req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token known-class-name class-name">ArchiveError</span><span class="token plain"> triggered by decode_body when unpacking an archive</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"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token spread operator" style="color:#393A34">...</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>The questions are <em>realistic enough</em> in that they should have the structure of a user's
question, but ideally only yield one matching file.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-results">The Results<a href="https://bitcrowd.dev/full-text-search-in-the-age-of-mcp#the-results" class="hash-link" aria-label="Direct link to The Results" title="Direct link to The Results" translate="no">​</a></h3>
<p>the results look like the following row:</p>
<table style="font-size:8px"><thead><tr><th>query</th><th>target file</th><th>ts_rank</th><th>trgm</th><th>jina_v2_code</th><th>nomic_embed_text</th><th>all_minilm_l6_v2</th><th>mxbai_embed_large</th></tr></thead><tbody><tr><td><code>I tried calling 'HTTP.get(\"/docs/missing_page\")' but I'm getting a 'Page not found' error message.\nHTTP.get(\"/docs/missing_page\")\nWhy am I getting 'Page not found', and what should I do to locate the correct documentation page?</code></td><td>404.html</td><td><ol><li>lib/req/test/ownership.ex</li><li>Req.Test.html</li><li>lib/req/test.ex</li><li>changelog.html</li><li>Req.html</li><li>lib/req/test/ownership.ex</li><li>CHANGELOG.md</li><li>Req.html</li></ol></td><td><ol><li>lib/req/transport_error.ex</li><li>lib/req/http_error.ex</li><li>Req.TransportError.html</li><li>Req.HTTPError.html</li><li>lib/req/test/ownership_error.ex</li><li>lib/req/response.ex</li><li>lib/req/decompress_error.ex</li><li>lib/req/too_many_redirects_error.ex</li></ol></td><td><ol><li>CHANGELOG.md</li><li>lib/req/too_many_redirects_error.ex</li><li>lib/req/http_error.ex</li><li>api-reference.html</li><li>README.md</li><li>Req.html</li><li>lib/req/steps.ex</li><li>readme.html</li></ol></td><td><ol><li>lib/req/steps.ex</li><li>lib/req.ex</li><li>Req.HTTPError.html</li><li>readme.html</li><li>lib/req/http_error.ex</li><li>lib/req/test.ex</li><li>api-reference.html</li><li>Req.ArchiveError.html</li></ol></td><td><ol><li>Req.html</li><li>Req.Request.html</li><li>lib/req.ex</li><li>README.md</li><li>CHANGELOG.md</li><li>lib/req/http_error.ex</li><li>lib/req/steps.ex</li><li>Req.HTTPError.html</li></ol></td><td><ol><li>CHANGELOG.md</li><li>Req.html</li><li>lib/req.ex</li><li>CHANGELOG.md</li><li>lib/req/http_error.ex</li><li>lib/req/request.ex</li><li>Req.html</li><li>readme.html</li></ol></td></tr></tbody></table>
<p>They all rank matches in the order of importance. Some of them are exact matches, others are just background noise. When
now need to establish a way to tell them apart. This sounds like the perfect cliff hanger, and this post is already quiet
long.</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary> 77 more examples of the file rankings that the different models produce</summary><div><div class="collapsibleContent_i85q"><table style="font-size:8px"><thead><tr><th>query</th><th>target file</th><th>ts_rank</th><th>trgm</th><th>jina_v2_code</th><th>nomic_embed_text</th><th>all_minilm_l6_v2</th><th>mxbai_embed_large</th></tr></thead><tbody><tr><td><code>I tried calling 'HTTP.get(\"/docs/missing_page\")' but I'm getting a 'Page not found' error message.\nHTTP.get(\"/docs/missing_page\")\nWhy am I getting 'Page not found', and what should I do to locate the correct documentation page?</code></td><td>404.html</td><td><ol><li>lib/req/test/ownership.ex</li><li>Req.Test.html</li><li>lib/req/test.ex</li><li>changelog.html</li><li>Req.html</li><li>lib/req/test/ownership.ex</li><li>CHANGELOG.md</li><li>Req.html</li></ol></td><td><ol><li>lib/req/transport_error.ex</li><li>lib/req/http_error.ex</li><li>Req.TransportError.html</li><li>Req.HTTPError.html</li><li>lib/req/test/ownership_error.ex</li><li>lib/req/response.ex</li><li>lib/req/decompress_error.ex</li><li>lib/req/too_many_redirects_error.ex</li></ol></td><td><ol><li>CHANGELOG.md</li><li>lib/req/too_many_redirects_error.ex</li><li>lib/req/http_error.ex</li><li>api-reference.html</li><li>README.md</li><li>Req.html</li><li>lib/req/steps.ex</li><li>readme.html</li></ol></td><td><ol><li>lib/req/steps.ex</li><li>lib/req.ex</li><li>Req.HTTPError.html</li><li>readme.html</li><li>lib/req/http_error.ex</li><li>lib/req/test.ex</li><li>api-reference.html</li><li>Req.ArchiveError.html</li></ol></td><td><ol><li>Req.html</li><li>Req.Request.html</li><li>lib/req.ex</li><li>README.md</li><li>CHANGELOG.md</li><li>lib/req/http_error.ex</li><li>lib/req/steps.ex</li><li>Req.HTTPError.html</li></ol></td><td><ol><li>CHANGELOG.md</li><li>Req.html</li><li>lib/req.ex</li><li>CHANGELOG.md</li><li>lib/req/http_error.ex</li><li>lib/req/request.ex</li><li>Req.html</li><li>readme.html</li></ol></td></tr><tr><td><code>When I visit 'mysite.com/api/v1/unknown', I see a message that the page does not exist.\nmysite.com/api/v1/unknown\nHow can I find the correct URL or documentation if the page I'm looking for isn't there?</code></td><td>404.html</td><td><ol><li>lib/req.ex</li><li>Req.html</li><li>lib/req.ex</li><li>Req.html</li><li>lib/req/request.ex</li><li>lib/req.ex</li><li>lib/req/request.ex</li><li>Req.Request.html</li></ol></td><td><ol><li>lib/req/test/ownership_error.ex</li><li>lib/req/request.ex</li><li>lib/req/archive_error.ex</li><li>lib/req.ex</li><li>lib/req/request.ex</li><li>lib/req/transport_error.ex</li><li>lib/req/response.ex</li><li>lib/req/test/ownership.ex</li></ol></td><td><ol><li>Req.html</li><li>lib/req.ex</li><li>CHANGELOG.md</li><li>lib/req/too_many_redirects_error.ex</li><li>README.md</li><li>lib/req/steps.ex</li><li>readme.html</li><li>api-reference.html</li></ol></td><td><ol><li>readme.html</li><li>lib/req/steps.ex</li><li>lib/req.ex</li><li>lib/req/test.ex</li><li>Req.ArchiveError.html</li><li>lib/req.ex</li><li>Req.HTTPError.html</li><li>lib/req.ex</li></ol></td><td><ol><li>README.md</li><li>Req.html</li><li>lib/req.ex</li><li>Req.Request.html</li><li>Req.HTTPError.html</li><li>Req.Request.html</li><li>CHANGELOG.md</li><li>Req.Request.html</li></ol></td><td><ol><li>Req.html</li><li>lib/req/request.ex</li><li>lib/req/utils.ex</li><li>Req.html</li><li>CHANGELOG.md</li><li>CHANGELOG.md</li><li>lib/req/steps.ex</li><li>lib/req.ex</li></ol></td></tr><tr><td><code>I tried searching for a method doc by name but ended up on an error page saying it doesn't exist.\nsearch_method('fooBarMethod')\nWhere do I go after seeing the page not found message to locate the doc for the method I'm looking for?</code></td><td>404.html</td><td><ol><li>lib/req/fields.ex</li><li>lib/req/request.ex</li><li>lib/req/finch.ex</li><li>lib/req/utils.ex</li><li>lib/req.ex</li><li>lib/req/finch.ex</li><li>lib/req/test.ex</li><li>lib/req/request.ex</li></ol></td><td><ol><li>lib/req/response.ex</li><li>lib/req/test/ownership_error.ex</li><li>lib/req/archive_error.ex</li><li>lib/req/transport_error.ex</li><li>lib/req/too_many_redirects_error.ex</li><li>Req.Request.html</li><li>lib/req/decompress_error.ex</li><li>lib/req.ex</li></ol></td><td><ol><li>lib/req/checksum_mismatch_error.ex</li><li>lib/req/decompress_error.ex</li><li>lib/req/too_many_redirects_error.ex</li><li>lib/req/archive_error.ex</li><li>api-reference.html</li><li>lib/req/transport_error.ex</li><li>Req.html</li><li>search.html</li></ol></td><td><ol><li>lib/req/archive_error.ex</li><li>Req.Request.html</li><li>Req.ArchiveError.html</li><li>Req.TooManyRedirectsError.html</li><li>lib/req/decompress_error.ex</li><li>Req.Request.html</li><li>Req.DecompressError.html</li><li>lib/req/steps.ex</li></ol></td><td><ol><li>Req.Steps.html</li><li>Req.html</li><li>Req.Request.html</li><li>Req.Request.html</li><li>lib/req.ex</li><li>Req.Request.html</li><li>lib/req/request.ex</li><li>Req.html</li></ol></td><td><ol><li>lib/req/request.ex</li><li>lib/req/request.ex</li><li>Req.Request.html</li><li>lib/req/http_error.ex</li><li>lib/req.ex</li><li>lib/req/request.ex</li><li>Req.Request.html</li><li>lib/req/archive_error.ex</li></ol></td></tr><tr><td><code>I'm not sure how to proceed after seeing a 404 error while looking up 'someRandomEndpoint' in the docs.\nHTTP.get(\"/docs/someRandomEndpoint\")\nShould I use the search sidebar or the API reference to find what I'm looking for, and how does it help?</code></td><td>404.html</td><td><ol><li>lib/req/request.ex</li><li>lib/req/steps.ex</li><li>lib/req/request.ex</li><li>lib/req/request.ex</li><li>lib/req/steps.ex</li><li>lib/req/test.ex</li><li>CHANGELOG.md</li><li>changelog.html</li></ol></td><td><ol><li>lib/req/transport_error.ex</li><li>Req.HTTPError.html</li><li>Req.TransportError.html</li><li>lib/req/http_error.ex</li><li>lib/req/response.ex</li><li>lib/req/test.ex</li><li>lib/req/test/ownership_error.ex</li><li>lib/req/test/ownership.ex</li></ol></td><td><ol><li>CHANGELOG.md</li><li>lib/req/http_error.ex</li><li>README.md</li><li>lib/req/too_many_redirects_error.ex</li><li>lib/req/steps.ex</li><li>readme.html</li><li>changelog.html</li><li>api-reference.html</li></ol></td><td><ol><li>lib/req/steps.ex</li><li>lib/req.ex</li><li>Req.HTTPError.html</li><li>Req.ArchiveError.html</li><li>lib/req.ex</li><li>lib/req/http_error.ex</li><li>lib/req/steps.ex</li><li>lib/req.ex</li></ol></td><td><ol><li>Req.html</li><li>Req.Request.html</li><li>Req.html</li><li>README.md</li><li>CHANGELOG.md</li><li>Req.HTTPError.html</li><li>Req.Request.html</li><li>Req.Steps.html</li></ol></td><td><ol><li>Req.html</li><li>CHANGELOG.md</li><li>Req.TooManyRedirectsError.html</li><li>Req.html</li><li>lib/req/http_error.ex</li><li>lib/req.ex</li><li>lib/req/request.ex</li><li>lib/req.ex</li></ol></td></tr><tr><td><code>I'm calling Req.Steps.decode_body(response) on a potentially corrupted archive response and it suddenly raises Req.ArchiveError.\nReq.Steps.decode_body(response)\nWhy am I getting a Req.ArchiveError while trying to unpack the archive?</code></td><td>Req.ArchiveError.html</td><td><ol><li>lib/req/steps.ex</li><li>lib/req/steps.ex</li><li>Req.Steps.html</li><li>Req.html</li><li>Req.Response.html</li><li>readme.html</li><li>Req.Steps.html</li><li>lib/req/response.ex</li></ol></td><td><ol><li>lib/req/archive_error.ex</li><li>Req.ArchiveError.html</li><li>api-reference.html</li><li>Req.Request.html</li><li>CHANGELOG.md</li><li>lib/req.ex</li><li>lib/req/decompress_error.ex</li><li>lib/req/steps.ex</li></ol></td><td><ol><li>Req.ArchiveError.html</li><li>lib/req/archive_error.ex</li><li>Req.DecompressError.html</li><li>changelog.html</li><li>changelog.html</li><li>lib/req/finch.ex</li><li>lib/req/decompress_error.ex</li><li>lib/req/steps.ex</li></ol></td><td><ol><li>Req.ArchiveError.html</li><li>Req.DecompressError.html</li><li>lib/req/archive_error.ex</li><li>Req.TooManyRedirectsError.html</li><li>Req.HTTPError.html</li><li>Req.ChecksumMismatchError.html</li><li>lib/req/decompress_error.ex</li><li>Req.TransportError.html</li></ol></td><td><ol><li>Req.ArchiveError.html</li><li>lib/req/archive_error.ex</li><li>api-reference.html</li><li>Req.DecompressError.html</li><li>CHANGELOG.md</li><li>lib/req/decompress_error.ex</li><li>lib/req/steps.ex</li><li>Req.Steps.html</li></ol></td><td><ol><li>Req.ArchiveError.html</li><li>Req.DecompressError.html</li><li>api-reference.html</li><li>lib/req/archive_error.ex</li><li>lib/req/steps.ex</li><li>lib/req/steps.ex</li><li>CHANGELOG.md</li><li>lib/req/request.ex</li></ol></td></tr><tr><td><code>When I run Req.Steps.decode_body(resp) with a tar.gz file, the function throws a Req.ArchiveError after failing to unpack it.\nReq.Steps.decode_body(resp)\nWhat causes Req.ArchiveError to be returned from decode_body in this scenario?</code></td><td>Req.ArchiveError.html</td><td><ol><li>lib/req/steps.ex</li><li>CHANGELOG.md</li><li>Req.html</li><li>lib/req.ex</li><li>Req.Request.html</li><li>Req.html</li><li>Req.html</li><li>Req.Steps.html</li></ol></td><td><ol><li>lib/req/archive_error.ex</li><li>Req.ArchiveError.html</li><li>api-reference.html</li><li>Req.DecompressError.html</li><li>lib/req/decompress_error.ex</li><li>Req.Request.html</li><li>lib/req/request.ex</li><li>lib/req/steps.ex</li></ol></td><td><ol><li>Req.ArchiveError.html</li><li>lib/req/archive_error.ex</li><li>Req.DecompressError.html</li><li>changelog.html</li><li>lib/req/decompress_error.ex</li><li>changelog.html</li><li>lib/req/steps.ex</li><li>lib/req/finch.ex</li></ol></td><td><ol><li>Req.ArchiveError.html</li><li>Req.DecompressError.html</li><li>lib/req/archive_error.ex</li><li>Req.TooManyRedirectsError.html</li><li>lib/req/decompress_error.ex</li><li>Req.TransportError.html</li><li>Req.HTTPError.html</li><li>Req.Test.OwnershipError.html</li></ol></td><td><ol><li>Req.ArchiveError.html</li><li>lib/req/archive_error.ex</li><li>api-reference.html</li><li>Req.DecompressError.html</li><li>Req.Steps.html</li><li>CHANGELOG.md</li><li>lib/req/decompress_error.ex</li><li>lib/req/steps.ex</li></ol></td><td><ol><li>Req.ArchiveError.html</li><li>Req.DecompressError.html</li><li>lib/req/archive_error.ex</li><li>api-reference.html</li><li>lib/req/steps.ex</li><li>lib/req/steps.ex</li><li>CHANGELOG.md</li><li>Req.Steps.html</li></ol></td></tr><tr><td><code>I pass an HTTP response body to Req.Steps.decode_body, hoping it will uncompress automatically, but it raises Req.ArchiveError instead.\nReq.Steps.decode_body(my_response)\nHow do I handle Req.ArchiveError triggered by decode_body when unpacking an archive?</code></td><td>Req.ArchiveError.html</td><td><ol><li>lib/req/steps.ex</li><li>Req.Steps.html</li><li>Req.Steps.html</li><li>lib/req.ex</li><li>Req.Steps.html</li><li>Req.html</li><li>README.md</li><li>lib/req/steps.ex</li></ol></td><td><ol><li>lib/req/archive_error.ex</li><li>Req.ArchiveError.html</li><li>api-reference.html</li><li>lib/req/decompress_error.ex</li><li>CHANGELOG.md</li><li>Req.DecompressError.html</li><li>Req.Request.html</li><li>lib/req/steps.ex</li></ol></td><td><ol><li>Req.ArchiveError.html</li><li>Req.DecompressError.html</li><li>lib/req/archive_error.ex</li><li>changelog.html</li><li>changelog.html</li><li>lib/req/decompress_error.ex</li><li>lib/req/steps.ex</li><li>lib/req/finch.ex</li></ol></td><td><ol><li>Req.ArchiveError.html</li><li>Req.DecompressError.html</li><li>lib/req/archive_error.ex</li><li>Req.Response.Async.html</li><li>Req.HTTPError.html</li><li>Req.TransportError.html</li><li>lib/req/request.ex</li><li>lib/req/response.ex</li></ol></td><td><ol><li>Req.ArchiveError.html</li><li>lib/req/archive_error.ex</li><li>lib/req/steps.ex</li><li>api-reference.html</li><li>Req.Steps.html</li><li>Req.Steps.html</li><li>CHANGELOG.md</li><li>changelog.html</li></ol></td><td><ol><li>Req.ArchiveError.html</li><li>Req.DecompressError.html</li><li>lib/req/steps.ex</li><li>lib/req/steps.ex</li><li>Req.Steps.html</li><li>api-reference.html</li><li>lib/req/archive_error.ex</li><li>CHANGELOG.md</li></ol></td></tr><tr><td><code>I'm testing a custom archive through Req.Steps.decode_body(response), but the function aborts with Req.ArchiveError whenever I attempt to decode it.\nReq.Steps.decode_body(response)\nWhy does decode_body return Req.ArchiveError for my custom archive format?</code></td><td>Req.ArchiveError.html</td><td><ol><li>lib/req/steps.ex</li><li>Req.Steps.html</li><li>lib/req/steps.ex</li><li>Req.Steps.html</li><li>lib/req.ex</li><li>Req.html</li><li>Req.Request.html</li><li>lib/req/request.ex</li></ol></td><td><ol><li>lib/req/archive_error.ex</li><li>Req.ArchiveError.html</li><li>api-reference.html</li><li>Req.Request.html</li><li>lib/req/decompress_error.ex</li><li>lib/req/request.ex</li><li>Req.Request.html</li><li>Req.DecompressError.html</li></ol></td><td><ol><li>Req.ArchiveError.html</li><li>lib/req/archive_error.ex</li><li>changelog.html</li><li>changelog.html</li><li>Req.DecompressError.html</li><li>lib/req/decompress_error.ex</li><li>lib/req/steps.ex</li><li>CHANGELOG.md</li></ol></td><td><ol><li>Req.ArchiveError.html</li><li>Req.DecompressError.html</li><li>lib/req/archive_error.ex</li><li>lib/req/decompress_error.ex</li><li>lib/req/steps.ex</li><li>Req.Request.html</li><li>Req.TooManyRedirectsError.html</li><li>Req.HTTPError.html</li></ol></td><td><ol><li>Req.ArchiveError.html</li><li>lib/req/archive_error.ex</li><li>api-reference.html</li><li>Req.DecompressError.html</li><li>lib/req/decompress_error.ex</li><li>CHANGELOG.md</li><li>lib/req/steps.ex</li><li>Req.Steps.html</li></ol></td><td><ol><li>Req.ArchiveError.html</li><li>Req.DecompressError.html</li><li>lib/req/steps.ex</li><li>api-reference.html</li><li>lib/req/steps.ex</li><li>lib/req/archive_error.ex</li><li>lib/req/request.ex</li><li>Req.Steps.html</li></ol></td></tr><tr><td><code>I'm trying to confirm data integrity with Req.Steps.checksum/1, but when the computed value doesn't match, I see an exception.\nReq.Steps.checksum!(source_data)\nWhat does Req.ChecksumMismatchError represent in this context?</code></td><td>Req.ChecksumMismatchError.html</td><td><ol><li>Req.ChecksumMismatchError.html</li><li>lib/req/finch.ex</li><li>lib/req/steps.ex</li><li>Req.HTTPError.html</li><li>api-reference.html</li><li>lib/req/utils.ex</li><li>lib/req/response_async.ex</li><li>Req.TransportError.html</li></ol></td><td><ol><li>Req.ChecksumMismatchError.html</li><li>lib/req/checksum_mismatch_error.ex</li><li>Req.TooManyRedirectsError.html</li><li>Req.DecompressError.html</li><li>lib/req/decompress_error.ex</li><li>Req.ArchiveError.html</li><li>Req.TransportError.html</li><li>lib/req/archive_error.ex</li></ol></td><td><ol><li>Req.ChecksumMismatchError.html</li><li>lib/req/checksum_mismatch_error.ex</li><li>lib/req/steps.ex</li><li>Req.Test.OwnershipError.html</li><li>lib/req/decompress_error.ex</li><li>lib/req/test/ownership_error.ex</li><li>lib/req/archive_error.ex</li><li>Req.DecompressError.html</li></ol></td><td><ol><li>Req.ChecksumMismatchError.html</li><li>lib/req/checksum_mismatch_error.ex</li><li>Req.DecompressError.html</li><li>Req.ArchiveError.html</li><li>Req.Test.OwnershipError.html</li><li>lib/req/steps.ex</li><li>lib/req/steps.ex</li><li>Req.TooManyRedirectsError.html</li></ol></td><td><ol><li>Req.ChecksumMismatchError.html</li><li>lib/req/checksum_mismatch_error.ex</li><li>api-reference.html</li><li>lib/req/steps.ex</li><li>Req.Request.html</li><li>Req.Steps.html</li><li>lib/req/request.ex</li><li>lib/req/steps.ex</li></ol></td><td><ol><li>Req.ChecksumMismatchError.html</li><li>lib/req/checksum_mismatch_error.ex</li><li>api-reference.html</li><li>Req.DecompressError.html</li><li>Req.ArchiveError.html</li><li>lib/req/steps.ex</li><li>Req.Request.html</li><li>lib/req/request.ex</li></ol></td></tr><tr><td><code>I used Req.Steps.checksum/1 to validate a downloaded file's SHA256, but the app crashed with Req.ChecksumMismatchError.\nReq.Steps.checksum(file_path, expected: "abc123...")\nWhy is this error triggered during the checksum step?</code></td><td>Req.ChecksumMismatchError.html</td><td><ol><li>lib/req/request.ex</li><li>Req.Request.html</li><li>Req.Steps.html</li><li>Req.Request.html</li><li>Req.Request.html</li><li>README.md</li><li>lib/req/request.ex</li><li>lib/req.ex</li></ol></td><td><ol><li>lib/req/checksum_mismatch_error.ex</li><li>Req.ChecksumMismatchError.html</li><li>api-reference.html</li><li>lib/req/steps.ex</li><li>lib/req/transport_error.ex</li><li>Req.TransportError.html</li><li>lib/req/steps.ex</li><li>lib/req/steps.ex</li></ol></td><td><ol><li>Req.ChecksumMismatchError.html</li><li>lib/req/steps.ex</li><li>lib/req/checksum_mismatch_error.ex</li><li>lib/req/decompress_error.ex</li><li>lib/req/archive_error.ex</li><li>Req.Test.OwnershipError.html</li><li>lib/req/too_many_redirects_error.ex</li><li>changelog.html</li></ol></td><td><ol><li>Req.ChecksumMismatchError.html</li><li>Req.ArchiveError.html</li><li>lib/req/checksum_mismatch_error.ex</li><li>Req.DecompressError.html</li><li>Req.TooManyRedirectsError.html</li><li>Req.HTTPError.html</li><li>Req.Test.OwnershipError.html</li><li>Req.TransportError.html</li></ol></td><td><ol><li>Req.ChecksumMismatchError.html</li><li>api-reference.html</li><li>lib/req/checksum_mismatch_error.ex</li><li>lib/req/steps.ex</li><li>Req.DecompressError.html</li><li>Req.ArchiveError.html</li><li>lib/req/steps.ex</li><li>lib/req/archive_error.ex</li></ol></td><td><ol><li>Req.ChecksumMismatchError.html</li><li>lib/req/checksum_mismatch_error.ex</li><li>api-reference.html</li><li>lib/req/steps.ex</li><li>Req.ArchiveError.html</li><li>lib/req/steps.ex</li><li>Req.DecompressError.html</li><li>lib/req/request.ex</li></ol></td></tr><tr><td><code>When I run Req.Steps.checksum/1 on my data, I'm sometimes getting Req.ChecksumMismatchError in production.\nReq.Steps.checksum(data_bytes, expected: "def456...")\nWhat causes a checksum mismatch with this function?</code></td><td>Req.ChecksumMismatchError.html</td><td><ol><li>lib/req/checksum_mismatch_error.ex</li><li>lib/req/steps.ex</li><li>Req.Steps.html</li><li>Req.html</li><li>api-reference.html</li><li>Req.Test.html</li><li>Req.ChecksumMismatchError.html</li><li>Req.Request.html</li></ol></td><td><ol><li>lib/req/checksum_mismatch_error.ex</li><li>Req.ChecksumMismatchError.html</li><li>lib/req/decompress_error.ex</li><li>api-reference.html</li><li>lib/req/archive_error.ex</li><li>lib/req/http_error.ex</li><li>lib/req/test.ex</li><li>lib/req/transport_error.ex</li></ol></td><td><ol><li>lib/req/steps.ex</li><li>Req.ChecksumMismatchError.html</li><li>lib/req/checksum_mismatch_error.ex</li><li>lib/req/decompress_error.ex</li><li>lib/req/steps.ex</li><li>lib/req/archive_error.ex</li><li>changelog.html</li><li>CHANGELOG.md</li></ol></td><td><ol><li>Req.ChecksumMismatchError.html</li><li>lib/req/checksum_mismatch_error.ex</li><li>Req.DecompressError.html</li><li>Req.TooManyRedirectsError.html</li><li>Req.ArchiveError.html</li><li>lib/req/steps.ex</li><li>Req.HTTPError.html</li><li>Req.Test.OwnershipError.html</li></ol></td><td><ol><li>Req.ChecksumMismatchError.html</li><li>lib/req/checksum_mismatch_error.ex</li><li>api-reference.html</li><li>lib/req/steps.ex</li><li>lib/req/steps.ex</li><li>Req.Steps.html</li><li>Req.Request.html</li><li>Req.Steps.html</li></ol></td><td><ol><li>Req.ChecksumMismatchError.html</li><li>lib/req/checksum_mismatch_error.ex</li><li>api-reference.html</li><li>lib/req/steps.ex</li><li>Req.ArchiveError.html</li><li>Req.DecompressError.html</li><li>lib/req/steps.ex</li><li>lib/req/request.ex</li></ol></td></tr><tr><td><code>I'm using Req.Steps.checksum/1 to verify a binary stream, but I keep encountering Req.ChecksumMismatchError.\nReq.Steps.checksum(stream_data, expected: "xyz789...")\nHow does this exception indicate the checksums differ?</code></td><td>Req.ChecksumMismatchError.html</td><td><ol><li>lib/req/steps.ex</li><li>lib/req/checksum_mismatch_error.ex</li><li>Req.Steps.html</li><li>Req.Test.html</li><li>CHANGELOG.md</li><li>Req.Test.html</li><li>CHANGELOG.md</li><li>changelog.html</li></ol></td><td><ol><li>lib/req/checksum_mismatch_error.ex</li><li>Req.ChecksumMismatchError.html</li><li>lib/req/archive_error.ex</li><li>lib/req/steps.ex</li><li>lib/req/test/ownership_error.ex</li><li>lib/req/steps.ex</li><li>lib/req/decompress_error.ex</li><li>lib/req/steps.ex</li></ol></td><td><ol><li>Req.ChecksumMismatchError.html</li><li>lib/req/steps.ex</li><li>lib/req/checksum_mismatch_error.ex</li><li>lib/req/decompress_error.ex</li><li>lib/req/archive_error.ex</li><li>lib/req/finch.ex</li><li>Req.ArchiveError.html</li><li>Req.Test.OwnershipError.html</li></ol></td><td><ol><li>Req.ChecksumMismatchError.html</li><li>lib/req/checksum_mismatch_error.ex</li><li>Req.DecompressError.html</li><li>Req.ArchiveError.html</li><li>Req.HTTPError.html</li><li>Req.TooManyRedirectsError.html</li><li>Req.Test.OwnershipError.html</li><li>Req.TransportError.html</li></ol></td><td><ol><li>Req.ChecksumMismatchError.html</li><li>lib/req/checksum_mismatch_error.ex</li><li>api-reference.html</li><li>lib/req/steps.ex</li><li>lib/req/steps.ex</li><li>Req.Request.html</li><li>CHANGELOG.md</li><li>lib/req/request.ex</li></ol></td><td><ol><li>Req.ChecksumMismatchError.html</li><li>lib/req/checksum_mismatch_error.ex</li><li>lib/req/steps.ex</li><li>api-reference.html</li><li>Req.ArchiveError.html</li><li>CHANGELOG.md</li><li>lib/req/steps.ex</li><li>lib/req/steps.ex</li></ol></td></tr><tr><td><code>When I call Req.Steps.decompress_body/1 on invalid data, I get a Req.DecompressError.\nReq.Steps.decompress_body/1\nWhy does Req.DecompressError occur when attempting to decompress a response?</code></td><td>Req.DecompressError.html</td><td><ol><li>lib/req/steps.ex</li><li>Req.Steps.html</li><li>Req.Steps.html</li><li>Req.Steps.html</li><li>Req.Steps.html</li><li>Req.Response.html</li><li>Req.html</li><li>lib/req/request.ex</li></ol></td><td><ol><li>lib/req/decompress_error.ex</li><li>Req.DecompressError.html</li><li>lib/req/archive_error.ex</li><li>lib/req/too_many_redirects_error.ex</li><li>Req.ArchiveError.html</li><li>Req.TooManyRedirectsError.html</li><li>CHANGELOG.md</li><li>api-reference.html</li></ol></td><td><ol><li>Req.DecompressError.html</li><li>changelog.html</li><li>lib/req/decompress_error.ex</li><li>lib/req/steps.ex</li><li>Req.ArchiveError.html</li><li>changelog.html</li><li>lib/req/archive_error.ex</li><li>changelog.html</li></ol></td><td><ol><li>Req.DecompressError.html</li><li>lib/req/decompress_error.ex</li><li>Req.ArchiveError.html</li><li>lib/req/archive_error.ex</li><li>Req.ChecksumMismatchError.html</li><li>Req.TooManyRedirectsError.html</li><li>Req.Request.html</li><li>lib/req/response.ex</li></ol></td><td><ol><li>Req.DecompressError.html</li><li>lib/req/decompress_error.ex</li><li>api-reference.html</li><li>lib/req/steps.ex</li><li>Req.ArchiveError.html</li><li>Req.Steps.html</li><li>lib/req/request.ex</li><li>CHANGELOG.md</li></ol></td><td><ol><li>Req.DecompressError.html</li><li>Req.ArchiveError.html</li><li>lib/req/decompress_error.ex</li><li>lib/req/steps.ex</li><li>lib/req/steps.ex</li><li>api-reference.html</li><li>Req.Steps.html</li><li>changelog.html</li></ol></td></tr><tr><td><code>I'm using Req.Steps.decompress_body/1 to handle a compressed response, but I'm seeing a Req.DecompressError in my logs.\nReq.Steps.decompress_body/1\nWhat causes a Req.DecompressError with a compressed response in Req?</code></td><td>Req.DecompressError.html</td><td><ol><li>Req.Steps.html</li><li>Req.Steps.html</li><li>Req.Steps.html</li><li>README.md</li><li>Req.html</li><li>lib/req.ex</li><li>changelog.html</li><li>lib/req/request.ex</li></ol></td><td><ol><li>Req.DecompressError.html</li><li>lib/req/decompress_error.ex</li><li>Req.ArchiveError.html</li><li>lib/req/archive_error.ex</li><li>api-reference.html</li><li>lib/req.ex</li><li>CHANGELOG.md</li><li>lib/req/transport_error.ex</li></ol></td><td><ol><li>Req.DecompressError.html</li><li>lib/req/steps.ex</li><li>changelog.html</li><li>lib/req/decompress_error.ex</li><li>Req.ArchiveError.html</li><li>changelog.html</li><li>CHANGELOG.md</li><li>lib/req/archive_error.ex</li></ol></td><td><ol><li>Req.DecompressError.html</li><li>lib/req/decompress_error.ex</li><li>Req.ArchiveError.html</li><li>lib/req/response.ex</li><li>Req.Request.html</li><li>Req.TooManyRedirectsError.html</li><li>Req.ChecksumMismatchError.html</li><li>Req.Response.Async.html</li></ol></td><td><ol><li>Req.DecompressError.html</li><li>Req.Steps.html</li><li>lib/req/decompress_error.ex</li><li>Req.ArchiveError.html</li><li>api-reference.html</li><li>lib/req/steps.ex</li><li>CHANGELOG.md</li><li>lib/req/archive_error.ex</li></ol></td><td><ol><li>Req.DecompressError.html</li><li>Req.Steps.html</li><li>Req.ArchiveError.html</li><li>lib/req/steps.ex</li><li>lib/req/steps.ex</li><li>CHANGELOG.md</li><li>lib/req/decompress_error.ex</li><li>api-reference.html</li></ol></td></tr><tr><td><code>After calling Req.Steps.decompress_body/1 with a broken gzip file, the function raises Req.DecompressError.\nReq.Steps.decompress_body/1\nHow can I resolve a Req.DecompressError that occurs during decompression?</code></td><td>Req.DecompressError.html</td><td><ol><li>lib/req/steps.ex</li><li>Req.Steps.html</li><li>api-reference.html</li><li>Req.DecompressError.html</li><li>lib/req.ex</li><li>Req.Steps.html</li><li>lib/req/request.ex</li><li>lib/req/steps.ex</li></ol></td><td><ol><li>Req.DecompressError.html</li><li>lib/req/decompress_error.ex</li><li>Req.ArchiveError.html</li><li>lib/req/archive_error.ex</li><li>lib/req/transport_error.ex</li><li>lib/req/too_many_redirects_error.ex</li><li>api-reference.html</li><li>Req.TransportError.html</li></ol></td><td><ol><li>Req.DecompressError.html</li><li>Req.ArchiveError.html</li><li>lib/req/decompress_error.ex</li><li>lib/req/steps.ex</li><li>changelog.html</li><li>lib/req/archive_error.ex</li><li>changelog.html</li><li>lib/req/finch.ex</li></ol></td><td><ol><li>Req.DecompressError.html</li><li>lib/req/decompress_error.ex</li><li>Req.ArchiveError.html</li><li>lib/req/archive_error.ex</li><li>Req.TooManyRedirectsError.html</li><li>Req.Request.html</li><li>Req.ChecksumMismatchError.html</li><li>Req.HTTPError.html</li></ol></td><td><ol><li>Req.DecompressError.html</li><li>lib/req/decompress_error.ex</li><li>Req.ArchiveError.html</li><li>Req.Steps.html</li><li>api-reference.html</li><li>lib/req/archive_error.ex</li><li>CHANGELOG.md</li><li>CHANGELOG.md</li></ol></td><td><ol><li>Req.DecompressError.html</li><li>lib/req/decompress_error.ex</li><li>Req.ArchiveError.html</li><li>lib/req/steps.ex</li><li>lib/req/steps.ex</li><li>api-reference.html</li><li>Req.Steps.html</li><li>changelog.html</li></ol></td></tr><tr><td><code>I'm calling <code>Req.get!('http://bitcrowd.net')</code> in my application and suddenly receive a Req.HTTPError whenever the server closes the connection unexpectedly.\nReq.get!('<a href="http://bitcrowd.net/" target="_blank" rel="noopener noreferrer" class="">http://bitcrowd.net</a>') Why does Req.HTTPError appear when the server closes the connection unexpectedly?</code></td><td>Req.HTTPError.html</td><td><ol><li>lib/req/steps.ex</li><li>Req.Steps.html</li><li>lib/req.ex</li><li>lib/req/steps.ex</li><li>lib/req/steps.ex</li><li>lib/req/steps.ex</li><li>README.md</li><li>lib/req.ex</li></ol></td><td><ol><li>lib/req/transport_error.ex</li><li>Req.ArchiveError.html</li><li>Req.Request.html</li><li>Req.TransportError.html</li><li>lib/req.ex</li><li>lib/req/archive_error.ex</li><li>Req.TooManyRedirectsError.html</li><li>lib/req.ex</li></ol></td><td><ol><li>Req.HTTPError.html</li><li>lib/req/http_error.ex</li><li>lib/req/response.ex</li><li>lib/req/steps.ex</li><li>CHANGELOG.md</li><li>lib/req/too_many_redirects_error.ex</li><li>api-reference.html</li><li>Req.TransportError.html</li></ol></td><td><ol><li>Req.HTTPError.html</li><li>lib/req/request.ex</li><li>Req.TransportError.html</li><li>lib/req/http_error.ex</li><li>lib/req/response.ex</li><li>Req.Response.Async.html</li><li>Req.Steps.html</li><li>Req.DecompressError.html</li></ol></td><td><ol><li>Req.html</li><li>Req.HTTPError.html</li><li>lib/req.ex</li><li>lib/req/http_error.ex</li><li>lib/req/steps.ex</li><li>lib/req/steps.ex</li><li>CHANGELOG.md</li><li>Req.Steps.html</li></ol></td><td><ol><li>Req.HTTPError.html</li><li>CHANGELOG.md</li><li>lib/req.ex</li><li>lib/req/http_error.ex</li><li>lib/req/steps.ex</li><li>Req.html</li><li>Req.html</li><li>changelog.html</li></ol></td></tr><tr><td><code>I created a custom Req adapter for an internal API and I'm raising <code>Req.HTTPError</code> whenever an HTTP-related issue occurs.\ndef custom_adapter(request), do: raise Req.HTTPError, message: "Protocol error"\nIs <code>Req.HTTPError</code> the correct exception to raise for HTTP protocol errors in a custom adapter?</code></td><td>Req.HTTPError.html</td><td><ol><li>lib/req/request.ex</li><li>lib/req.ex</li><li>lib/req/finch.ex</li><li>lib/req/request.ex</li><li>lib/req/steps.ex</li><li>lib/req/request.ex</li><li>Req.html</li><li>Req.Request.html</li></ol></td><td><ol><li>lib/req/http_error.ex</li><li>lib/req/transport_error.ex</li><li>Req.HTTPError.html</li><li>lib/req/too_many_redirects_error.ex</li><li>lib/req/test/ownership_error.ex</li><li>Req.TransportError.html</li><li>lib/req/decompress_error.ex</li><li>lib/req/archive_error.ex</li></ol></td><td><ol><li>Req.HTTPError.html</li><li>lib/req/http_error.ex</li><li>lib/req/transport_error.ex</li><li>Req.TransportError.html</li><li>lib/req/too_many_redirects_error.ex</li><li>lib/req/decompress_error.ex</li><li>Req.Steps.html</li><li>lib/req/archive_error.ex</li></ol></td><td><ol><li>Req.HTTPError.html</li><li>lib/req/http_error.ex</li><li>Req.TransportError.html</li><li>lib/req/request.ex</li><li>lib/req/transport_error.ex</li><li>lib/req/steps.ex</li><li>Req.Steps.html</li><li>Req.Request.html</li></ol></td><td><ol><li>lib/req/http_error.ex</li><li>Req.HTTPError.html</li><li>lib/req/transport_error.ex</li><li>lib/req/steps.ex</li><li>lib/req/steps.ex</li><li>Req.html</li><li>Req.TransportError.html</li><li>lib/req.ex</li></ol></td><td><ol><li>Req.HTTPError.html</li><li>lib/req/http_error.ex</li><li>CHANGELOG.md</li><li>lib/req/steps.ex</li><li>lib/req/transport_error.ex</li><li>Req.Request.html</li><li>lib/req/request.ex</li><li>CHANGELOG.md</li></ol></td></tr><tr><td><code>When I see <code>Req.HTTPError</code> in my logs, I notice it's referencing Mint.HTTPError under the hood.\nMint.HTTPError(<!-- -->:invalid_status_line<!-- -->)\nHow does Req.HTTPError relate to Mint.HTTPError?</code></td><td>Req.HTTPError.html</td><td><ol><li>lib/req/finch.ex</li><li>readme.html</li><li>lib/req/steps.ex</li><li>lib/req/finch.ex</li><li>README.md</li><li>Req.html</li><li>CHANGELOG.md</li><li>Req.Steps.html</li></ol></td><td><ol><li>lib/req/response.ex</li><li>lib/req/test/ownership_error.ex</li><li>lib/req/transport_error.ex</li><li>lib/req/http_error.ex</li><li>Req.HTTPError.html</li><li>Req.Steps.html</li><li>Req.Request.html</li><li>lib/req/finch.ex</li></ol></td><td><ol><li>Req.HTTPError.html</li><li>lib/req/http_error.ex</li><li>lib/req/transport_error.ex</li><li>lib/req/too_many_redirects_error.ex</li><li>lib/req/steps.ex</li><li>api-reference.html</li><li>Req.TooManyRedirectsError.html</li><li>Req.TransportError.html</li></ol></td><td><ol><li>Req.HTTPError.html</li><li>Req.TransportError.html</li><li>lib/req/http_error.ex</li><li>lib/req/transport_error.ex</li><li>Req.Test.OwnershipError.html</li><li>Req.ArchiveError.html</li><li>Req.ChecksumMismatchError.html</li><li>Req.DecompressError.html</li></ol></td><td><ol><li>Req.HTTPError.html</li><li>lib/req/http_error.ex</li><li>lib/req/transport_error.ex</li><li>Req.TransportError.html</li><li>Req.Steps.html</li><li>CHANGELOG.md</li><li>api-reference.html</li><li>lib/req/steps.ex</li></ol></td><td><ol><li>Req.HTTPError.html</li><li>lib/req/http_error.ex</li><li>Req.TransportError.html</li><li>CHANGELOG.md</li><li>lib/req/transport_error.ex</li><li>CHANGELOG.md</li><li>Req.Steps.html</li><li>api-reference.html</li></ol></td></tr><tr><td><code>After upgrading Req, I'm seeing a standardized error message: <code>Req.HTTPError</code> thrown in my program.\nReq.get!("<a href="https://myapi.com/data%5C%22)%5CnWhat" target="_blank" rel="noopener noreferrer" class="">https://myapi.com/data\")\nWhat</a> does it mean that <code>Req.HTTPError</code> is a standardized exception for HTTP protocol errors?</code></td><td>Req.HTTPError.html</td><td><ol><li>lib/req/finch.ex</li><li>CHANGELOG.md</li><li>README.md</li><li>Req.html</li><li>lib/req/http_error.ex</li><li>changelog.html</li><li>lib/req/finch.ex</li><li>Req.Steps.html</li></ol></td><td><ol><li>lib/req/http_error.ex</li><li>Req.HTTPError.html</li><li>lib/req/transport_error.ex</li><li>Req.TransportError.html</li><li>lib/req/test/ownership_error.ex</li><li>lib/req/decompress_error.ex</li><li>lib/req/archive_error.ex</li><li>Req.Test.OwnershipError.html</li></ol></td><td><ol><li>Req.HTTPError.html</li><li>lib/req/http_error.ex</li><li>lib/req/transport_error.ex</li><li>Req.TransportError.html</li><li>api-reference.html</li><li>lib/req/too_many_redirects_error.ex</li><li>lib/req/steps.ex</li><li>CHANGELOG.md</li></ol></td><td><ol><li>Req.HTTPError.html</li><li>lib/req/http_error.ex</li><li>Req.TransportError.html</li><li>Req.ArchiveError.html</li><li>lib/req/request.ex</li><li>Req.DecompressError.html</li><li>lib/req/transport_error.ex</li><li>Req.ChecksumMismatchError.html</li></ol></td><td><ol><li>Req.HTTPError.html</li><li>lib/req/http_error.ex</li><li>lib/req/transport_error.ex</li><li>Req.TransportError.html</li><li>Req.html</li><li>lib/req/steps.ex</li><li>lib/req.ex</li><li>api-reference.html</li></ol></td><td><ol><li>Req.HTTPError.html</li><li>lib/req/http_error.ex</li><li>Req.TransportError.html</li><li>CHANGELOG.md</li><li>api-reference.html</li><li>CHANGELOG.md</li><li>lib/req/transport_error.ex</li><li>lib/req/steps.ex</li></ol></td></tr><tr><td><code>I'm catching <code>Req.HTTPError</code> in a try/rescue like <code>try do Req.get!('bitcrowd.net') rescue e in Req.HTTPError -&gt; IO.inspect(e) end</code> and I want to confirm I'm handling the correct type of exception.\ntry do Req.get!('bitcrowd.net') rescue e in Req.HTTPError -&gt; IO.inspect(e) end\nShould <code>Req.HTTPError</code> always be used as the rescue target for HTTP-protocol-related failures in Req?</code></td><td>Req.HTTPError.html</td><td><ol><li>lib/req/finch.ex</li><li>README.md</li><li>lib/req/finch.ex</li><li>lib/req/test.ex</li><li>lib/req/request.ex</li><li>changelog.html</li><li>lib/req/http_error.ex</li><li>lib/req/steps.ex</li></ol></td><td><ol><li>lib/req/http_error.ex</li><li>Req.HTTPError.html</li><li>lib/req/transport_error.ex</li><li>Req.TransportError.html</li><li>lib/req/test/ownership_error.ex</li><li>lib/req.ex</li><li>lib/req.ex</li><li>lib/req/archive_error.ex</li></ol></td><td><ol><li>Req.HTTPError.html</li><li>lib/req/http_error.ex</li><li>Req.TransportError.html</li><li>lib/req/transport_error.ex</li><li>lib/req/steps.ex</li><li>CHANGELOG.md</li><li>lib/req/too_many_redirects_error.ex</li><li>lib/req/archive_error.ex</li></ol></td><td><ol><li>Req.HTTPError.html</li><li>lib/req/http_error.ex</li><li>lib/req/request.ex</li><li>Req.TransportError.html</li><li>Req.Request.html</li><li>CHANGELOG.md</li><li>Req.DecompressError.html</li><li>Req.ArchiveError.html</li></ol></td><td><ol><li>lib/req/steps.ex</li><li>lib/req/http_error.ex</li><li>lib/req/request.ex</li><li>Req.html</li><li>lib/req.ex</li><li>CHANGELOG.md</li><li>Req.Request.html</li><li>Req.Request.html</li></ol></td><td><ol><li>lib/req/steps.ex</li><li>Req.HTTPError.html</li><li>lib/req/http_error.ex</li><li>api-reference.html</li><li>Req.html</li><li>lib/req.ex</li><li>lib/req/request.ex</li><li>Req.Request.html</li></ol></td></tr></tbody></table></div></div></details>
<p>In the follow-up post of this series, we will see how a common ground of truth can be established, how we can evaluate accuracy, precision, recall and f1 score of the different models. We will then use this toolset to introduce techniques to boost the search results over all mechanisms.</p>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>Why bitcrowd?</div><div class="admonitionContent_BuS1"><p><strong>Elixir</strong> is an excellent choice for applications due to its scalability, fault tolerance, and concurrency model. Its lightweight processes and message-passing architecture make it ideal for orchestrating complex AI workflows efficiently. bitcrowd's first Elixir ML project dates back to 2020, and we have since then enabled various clients to build and scale their AI projects.</p><p><strong>bitcrowd</strong> is an excellent choice if you need a scalable RAG system or a fully integrated AI pipeline. We help you build, optimize, and maintain it with a focus on reliability and performance.</p><p>Drop us a line <a href="mailto:rag-blogpost@bitcrowd.net" target="_blank" rel="noopener noreferrer" class="">via email</a> if you want to
build your next AI project with Elixir. Or <a href="https://calendly.com/calendly-6kjr/30min" target="_blank" rel="noopener noreferrer" class="">book a call with us</a> to discuss your project.</p></div></div>]]></content:encoded>
            <category>LLM</category>
            <category>RAG</category>
            <category>AI</category>
            <category>MCP</category>
        </item>
    </channel>
</rss>