<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://kengoa.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://kengoa.github.io/" rel="alternate" type="text/html" /><updated>2026-05-21T11:22:59+00:00</updated><id>https://kengoa.github.io/feed.xml</id><title type="html">Kengo’s Blog</title><subtitle>Thoughts on software engineering and other things.</subtitle><entry><title type="html">Fixing Cars</title><link href="https://kengoa.github.io/essay/2026/05/21/fixing-cars.html" rel="alternate" type="text/html" title="Fixing Cars" /><published>2026-05-21T11:21:00+00:00</published><updated>2026-05-21T11:21:00+00:00</updated><id>https://kengoa.github.io/essay/2026/05/21/fixing-cars</id><content type="html" xml:base="https://kengoa.github.io/essay/2026/05/21/fixing-cars.html"><![CDATA[<p>During my recent camping trip to Wales, I noticed that my car boot was making an abnormally loud clunk when opened by the power liftgate. I called my Mazda dealer and was quoted 190 pounds for <em>investigating</em> the issue, without a guarantee of a fix.</p>

<p>Having some time off between jobs and feeling somewhat outraged by the price, I decided to see if I could have a go at this myself. For context, this is my very first car which I bought earlier this year, and I didn’t know the difference between a wrench and a spanner.</p>

<p>It turned out that this was a common defect, and Mazda had published a technical service bulletin <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> for technicians, which I found through a Reddit thread <sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> from another CX-30 owner who had faced the same issue. Having gained enough confidence that this also applied to my car based on the diagnosis and the affected models, I took a very small leap of faith and bought a plastic panel remover and a 10-mm combination spanner on Amazon for a combined total of 13 pounds.</p>

<p>The actual root cause was that one of the three bolts on the liftgate was loose due to a manufacturing error. The fix was as simple as tightening it with a spanner, and then magically, the loud noise was no more.</p>

<p>With no prior experience in fixing cars or even knowing about car parts, this entire process felt like software problems I encounter every day. For a given problem I had, other people had already faced it and shared how they addressed it on the Internet, and I could refer to a public document from the first party to compare the details and find the actual issue that led to the fix.</p>

<p>Some of the hurdles you need to jump over to get to a solution like this are often less financial or technical than mental. After all, all it took was a small spanner to tighten a small bolt. Still, many people would rather pay other people to do it and not worry about this themselves at all. While this is more than reasonable, if you are the type of person who would not hesitate to open a small PR to fix a bug or even a spelling mistake in an open-source software dependency, I would recommend trying to treat a physical problem like this like it’s a software problem. It might give you a sense of satistifaction of problem solving, and better yet, it might give you back some of the money you lost hosting your free software.</p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p><a href="https://static.nhtsa.gov/odi/tsbs/2023/MC-10232281-0001.pdf">Mazda Technical Service Bulletin</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p><a href="https://www.reddit.com/r/MazdaCX30/comments/1q1kqer/fixed_my_cx30s_loud_clicking_liftgate_saved_645/">Fixed My CX-30’s Loud Clicking Liftgate – Saved $645</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="essay" /><summary type="html"><![CDATA[During my recent camping trip to Wales, I noticed that my car boot was making an abnormally loud clunk when opened by the power liftgate. I called my Mazda dealer and was quoted 190 pounds for investigating the issue, without a guarantee of a fix.]]></summary></entry><entry><title type="html">Side Project of a Side Project</title><link href="https://kengoa.github.io/software/2026/01/02/side-project.html" rel="alternate" type="text/html" title="Side Project of a Side Project" /><published>2026-01-02T08:00:00+00:00</published><updated>2026-01-02T08:00:00+00:00</updated><id>https://kengoa.github.io/software/2026/01/02/side-project</id><content type="html" xml:base="https://kengoa.github.io/software/2026/01/02/side-project.html"><![CDATA[<p>Before switching to a platform engineering role within the AI Engineering Group earlier this year, I spent 9 months working on RAG with financial research documents.</p>

<p>The main lesson I learnt from this experience is that <strong>you have to solve retrieval first before you solve RAG</strong>. To achieve a desired user experience, you need to own (or at least be able to contribute to) the entire pipeline from indexing to retrieval to reranking to generation, rather than simply adding a generation step on top of an existing search system.</p>

<p>In this post, I will describe my personal journey of trying to build a search system for company documents in the UK, and how I ended up writing a Python client for Apache Solr.</p>

<h2 id="search-pipeline">Search Pipeline</h2>

<p>With the above lesson in mind, I thought about building a sandbox environment where I could tune different search pipeline configurations against different sets of requirements. As a starting point, for example, I’d ask myself:</p>

<ul>
  <li>How often do I need to ingest new documents?</li>
  <li>How fast should search be?</li>
  <li>What is the distribution of keywords like in the document domain?</li>
</ul>

<p>Answers to these questions will influence the system architecture, often with tradeoffs across different components.</p>

<p>For instance, the frequency and lengths of new documents inform whether we can apply compute-heavy document understanding or enrichment steps. We cannot spend multiple seconds ingesting a single document if new documents keep arriving while processing it.</p>

<p>If this requirement is not strict, then we can potentially afford to extract metadata from documents such as named entities or rephrasing some of the keywords and setting up specific fields to search over for BM25, which would improve both latency and relevance for sparse retrieval.</p>

<p>Keyword distribution also has consequences on how we handle ambiguity and vocabulary mismatch between the user query space and the document domain, with implications for the choice of the embedding algorithm, vector search databases, and vector search algorithms.</p>

<h2 id="the-side-project">The Side Project</h2>

<p>The actual project I wanted to work on was a better search system for company information on UK startups using the <a href="https://developer-specs.company-information.service.gov.uk/document-api/reference/document-location/fetch-a-document">Document API</a> from Companies House <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>. This was motivated by my personal experience that startups are generally much less transparent when things are not going well, and that the current user experience of Companies House is not ideal in terms of document discoverability and readability.</p>

<p>Providing a better search system for such information could potentially help alleviate the pain point of not having enough realistic information on company financials and governance structures prior to joining a startup. This assumes the existence of documents disclosing such information in the Companies House database already. This is often the case after startups raise Series A or Series B rounds when they typically accelerate their hiring efforts.</p>

<p>In Companies House, the documents are almost always indexed as PDFs and the frequency of document updates is generally low, say, no more than one document per day per company. This necessitates <strong>(1)</strong> a document understanding step from PDF to some form of structure that can be indexed into the search system and <strong>(2)</strong> some flexibility on latency to run this step, opening up the possibility of using something more advanced than simple PDF parsing, such as vision-language model-based <a href="https://arxiv.org/abs/2503.11576">SmolDocling</a>.</p>

<p>With those requirements, I came up with the following diagram with necessary components for setting up the search pipeline.</p>

<p><img src="/images/system.png" alt="system" class="article-img" /></p>

<h2 id="searching-for-search-servers">Searching for Search Servers</h2>

<p>I started to look for the libraries and technologies to fill in those components in the diagram above, and considered multiple options for the search server. With the exception of Meilisearch which is built in Rust, the mainstream enterprise search options are Elasticsearch, OpenSearch, and Apache Solr, which are all built on Apache Lucene. I had experience using Apache Solr in a previous RAG project and decided to stick with it due to its robust sparse-retrieval performance and ecosystem maturity.</p>

<p>This HackerNews comment from <a href="https://news.ycombinator.com/item?id=24178656">tekkk on Aug 16, 2020</a> echoes my sentiment on Solr:</p>

<blockquote>
  <p>Solr is one of those technologies which works but isn’t really glorious to use and is a bit stuffy with its XML configurations and Java interfaces. It’s a bit of a shame, because search engines are so popular nowadays and everybody seems to be fixated on using ElasticSearch, which from what I’ve read and heard is resource-hungry and not really suited for simple text-search.</p>
</blockquote>

<p>and note that this comment predates the rise of LLMs which subsequently led to the popularity in RAG and has since invited a number of ML practitioners who are typically not familiar with the Java ecosystem, including myself.</p>

<p>I believe this is the primary point of friction where the interface becomes the main bottleneck for developing a search system.
For example, the <em>de facto</em> official Python client for Apache Solr is <em>pysolr</em>, which has been around for well over a decade and is implemented in <a href="https://github.com/django-haystack/pysolr/blob/master/pysolr.py">one 1500-line Python file</a>.</p>

<p>This unfortunately falls short of expectations for a modern Python experience. For instance, pysolr does not support async operations at all, making concurrent queries inefficient compared to libraries that leverage Python’s async features. Indexing documents is also less ergonomic, and there is no built-in type safety, which can lead to runtime errors and make integration with typed codebases more challenging.</p>

<p>Another source of confusion for new users is that pysolr exposes Solr’s query syntax directly. Parameters like <b>q</b>, <b>fl</b>, and <b>qf</b> are somewhat terse abbreviations for a new user, and the relationship between query parameters and query parsers is not clearly documented or enforced by the client. This can make it difficult to understand the hierarchy and structure of queries, especially for those unfamiliar with Solr’s internals.</p>

<p>Let’s take a look at an actual example. The following snippet sets up a Solr client and uses <a href="https://solr.apache.org/guide/solr/latest/query-guide/edismax-query-parser.html">Extended DisMax Parser</a> to find the keyword “preference shares” (prioritizing the <em>summary</em> field matches over the <em>content</em> field) and returns the results grouped by <em>company_name</em> field with a limit of 3 documents per group.</p>

<p>As a prerequisite, Solr’s search request is constructed as HTTP query parameters and is sent as a GET request to a given collection of documents. In pysolr, those are set directly as Python dictionaries as follows.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pysolr</span> <span class="kn">import</span> <span class="n">Solr</span>

<span class="n">solr</span> <span class="o">=</span> <span class="n">Solr</span><span class="p">(</span><span class="s">'http://localhost:8983/solr/company_documents'</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>

<span class="n">params</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">'defType'</span><span class="p">:</span> <span class="s">'edismax'</span><span class="p">,</span>
    <span class="s">'q'</span><span class="p">:</span> <span class="s">'preference shares'</span><span class="p">,</span>
    <span class="s">'qf'</span><span class="p">:</span> <span class="s">'summary^2.0 content^1.0'</span><span class="p">,</span>
    <span class="s">'group'</span><span class="p">:</span> <span class="s">'true'</span><span class="p">,</span>
    <span class="s">'group.field'</span><span class="p">:</span> <span class="s">'company_name'</span><span class="p">,</span>
    <span class="s">'group.limit'</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
    <span class="s">'group.ngroups'</span><span class="p">:</span> <span class="s">'true'</span>
<span class="p">}</span>

<span class="n">results</span> <span class="o">=</span> <span class="n">solr</span><span class="p">.</span><span class="n">search</span><span class="p">(</span><span class="o">**</span><span class="n">params</span><span class="p">)</span>
</code></pre></div></div>

<p>The response here is an untyped dictionary that the user has to parse and validate manually, and it’s also hard to understand all the fields needed in the params and how they interact with each other.</p>

<h2 id="writing-a-new-client">Writing a New Client</h2>

<p>This is the point where I decided to rewrite the Python client from scratch with a modern setup with full type-safety and async support. My aim of building this client was to provide a better developer experience with a clearer interface with full IDE support which also doubles as a wrapper around the official <a href="https://solr.apache.org/guide/solr/latest/query-guide/query-syntax-and-parsers.html">Solr Query Guide</a> <sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">2</a></sup>, and having a standardised way to provide the document class in the search responses using Pydantic models.</p>

<p>I named this client Taiyo after the Japanese translation of the word <em>sun</em> which Solr-related projects often get the inspiration from, such as <a href="https://github.com/tow/sunburnt">sunburned</a> which is another Python client. The full documentation is available in <a href="https://taiyoproj.github.io">Taiyo Docs</a> and the source code is available on <a href="https://github.com/taiyoproj/taiyo">GitHub</a>.</p>

<p>Going back to the Extended DisMax query example. Taiyo now allows you to set up a query parser like the following:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">taiyo.params</span> <span class="kn">import</span> <span class="n">GroupParamsConfig</span>
<span class="kn">from</span> <span class="nn">taiyo.parsers</span> <span class="kn">import</span> <span class="n">ExtendedDisMaxQueryParser</span>

<span class="n">parser</span> <span class="o">=</span> <span class="n">ExtendedDisMaxQueryParser</span><span class="p">(</span>
        <span class="n">query</span><span class="o">=</span><span class="s">"preference shares"</span><span class="p">,</span>
        <span class="n">query_fields</span><span class="o">=</span><span class="p">{</span><span class="s">"summary"</span><span class="p">:</span> <span class="mf">2.0</span><span class="p">,</span> <span class="s">"content"</span><span class="p">:</span> <span class="mf">1.0</span><span class="p">},</span>
        <span class="n">configs</span><span class="o">=</span><span class="p">[</span>
            <span class="n">GroupParamsConfig</span><span class="p">(</span><span class="n">by</span><span class="o">=</span><span class="s">"company_name"</span><span class="p">,</span> <span class="n">limit</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">ngroups</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
        <span class="p">],</span>
<span class="p">)</span>
</code></pre></div></div>
<p>with <code class="language-plaintext highlighter-rouge">GroupParamsConfig</code> object passed in as a config object and each field autocompleted in the IDE with documentation referenced from the official query guide.</p>

<p>Alternatively, it’s also possible to set up the same exact query with a pandas-like chaining on the parser object:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">taiyo.parsers</span> <span class="kn">import</span> <span class="n">ExtendedDisMaxQueryParser</span>

<span class="n">parser</span> <span class="o">=</span> <span class="n">ExtendedDisMaxQueryParser</span><span class="p">(</span>
    <span class="n">query</span><span class="o">=</span><span class="s">"preference shares"</span><span class="p">,</span>
    <span class="n">query_fields</span><span class="o">=</span><span class="p">{</span><span class="s">"summary"</span><span class="p">:</span> <span class="mf">2.0</span><span class="p">,</span> <span class="s">"content"</span><span class="p">:</span> <span class="mf">1.0</span><span class="p">}</span>
<span class="p">).</span><span class="n">group</span><span class="p">(</span>
    <span class="n">by</span><span class="o">=</span><span class="s">"company_name"</span><span class="p">,</span>
    <span class="n">limit</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span>
    <span class="n">ngroups</span><span class="o">=</span><span class="bp">True</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Whether you use config objects or method chaining is a matter of personal preference or complexity. If you need a highly custom/multiple layers of configuration, you might want to opt for config objects that can be set up in different places (i.e in a iteration loop or loaded from a separate file).</p>

<p>If a potential user is hesitant on switching to a new client, they can also make use of Taiyo’s <a href="https://taiyoproj.github.io/parsers/overview/">query parsers</a> which serialises into Python dictionaries that other clients like pysolr accepts.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">taiyo.parsers</span> <span class="kn">import</span> <span class="n">StandardParser</span>

<span class="n">parser</span> <span class="o">=</span> <span class="n">StandardParser</span><span class="p">(</span>
    <span class="n">query</span><span class="o">=</span><span class="s">"preference shares"</span><span class="p">,</span>
    <span class="n">query_operator</span><span class="o">=</span><span class="s">"OR"</span><span class="p">,</span>
    <span class="n">filter_queries</span><span class="o">=</span><span class="p">[</span><span class="s">"created_at:[2025-01-01 TO *]"</span><span class="p">],</span>
    <span class="n">rows</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span>
<span class="p">)</span>

<span class="c1"># Build query parameters as dictionary
</span><span class="n">params</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="n">build</span><span class="p">()</span>

<span class="c1"># {
#     'q': 'preference shares',
#     'q.op': 'OR',
#     'fq': ['created_at:[2025-01-01 TO *]'],
#     'rows': 10,
#     'defType': 'lucene'
# }
</span>
<span class="c1"># Use with httpx or any HTTP library
</span><span class="kn">import</span> <span class="nn">httpx</span>
<span class="n">response</span> <span class="o">=</span> <span class="n">httpx</span><span class="p">.</span><span class="n">get</span><span class="p">(</span>
    <span class="s">"http://localhost:8983/solr/company_documents/select"</span><span class="p">,</span>
    <span class="n">params</span><span class="o">=</span><span class="n">params</span>
<span class="p">)</span>
</code></pre></div></div>

<p>You can also make use of the document model when searching for documents. For instance, if you set up the following <code class="language-plaintext highlighter-rouge">CompanyDocument</code> class derived from <code class="language-plaintext highlighter-rouge">taiyo.SolrDocument</code>, then you can specify this in the parser such that the responses are fully typed.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">taiyo</span> <span class="kn">import</span> <span class="n">StandardParser</span><span class="p">,</span> <span class="n">SolrDocument</span>

<span class="k">class</span> <span class="nc">CompanyDocument</span><span class="p">(</span><span class="n">SolrDocument</span><span class="p">):</span>
    <span class="n">document_id</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">company_name</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">company_number</span><span class="p">:</span> <span class="nb">int</span>
    <span class="n">category</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">created_at</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">summary</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">content</span><span class="p">:</span> <span class="nb">str</span>

<span class="c1"># Search returns typed results
</span><span class="n">parser</span> <span class="o">=</span> <span class="n">StandardParser</span><span class="p">(</span><span class="n">query</span><span class="o">=</span><span class="s">"preference shares"</span><span class="p">)</span>
<span class="n">results</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">search</span><span class="p">(</span><span class="n">parser</span><span class="p">,</span> <span class="n">document_model</span><span class="o">=</span><span class="n">CompanyDocument</span><span class="p">)</span>

<span class="c1"># Access typed company documents
</span><span class="k">for</span> <span class="n">doc</span> <span class="ow">in</span> <span class="n">results</span><span class="p">.</span><span class="n">docs</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">doc</span><span class="p">.</span><span class="n">category</span><span class="si">}</span><span class="s"> document for </span><span class="si">{</span><span class="n">doc</span><span class="p">.</span><span class="n">company_name</span><span class="si">}</span><span class="s"> (</span><span class="si">{</span><span class="n">doc</span><span class="p">.</span><span class="n">company_number</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Summary: </span><span class="si">{</span><span class="n">doc</span><span class="p">.</span><span class="n">summary</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p>With the async support with <code class="language-plaintext highlighter-rouge">httpx</code>, you can also index typed documents asynchronously as follows:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">asyncio</span>
<span class="kn">from</span> <span class="nn">taiyo</span> <span class="kn">import</span> <span class="n">AsyncSolrClient</span><span class="p">,</span> <span class="n">SolrDocument</span>

<span class="k">async</span> <span class="k">def</span> <span class="nf">ingest</span><span class="p">(</span><span class="n">docs</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">CompanyDocument</span><span class="p">]):</span>
    <span class="k">async</span> <span class="k">with</span> <span class="n">AsyncSolrClient</span><span class="p">(</span><span class="s">"http://localhost:8983/solr"</span><span class="p">)</span> <span class="k">as</span> <span class="n">client</span><span class="p">:</span>
        <span class="n">client</span><span class="p">.</span><span class="n">set_collection</span><span class="p">(</span><span class="s">"company_documents"</span><span class="p">)</span>

        <span class="c1"># Split into batches and process concurrently
</span>        <span class="n">batch_size</span> <span class="o">=</span> <span class="mi">100</span>
        <span class="n">batches</span> <span class="o">=</span> <span class="p">[</span><span class="n">docs</span><span class="p">[</span><span class="n">i</span><span class="p">:</span><span class="n">i</span> <span class="o">+</span> <span class="n">batch_size</span><span class="p">]</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">docs</span><span class="p">),</span> <span class="n">batch_size</span><span class="p">)]</span>

        <span class="c1"># Index all batches concurrently
</span>        <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">gather</span><span class="p">(</span><span class="o">*</span><span class="p">[</span><span class="n">client</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">batch</span><span class="p">,</span> <span class="n">commit</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span> <span class="k">for</span> <span class="n">batch</span> <span class="ow">in</span> <span class="n">batches</span><span class="p">])</span>
        <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="n">commit</span><span class="p">()</span>

<span class="n">docs</span> <span class="o">=</span> <span class="p">...</span>
<span class="n">asyncio</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">ingest</span><span class="p">(</span><span class="n">docs</span><span class="p">))</span>
</code></pre></div></div>

<h2 id="making-something-i-want">Making something I want</h2>

<p>There are several lines of work planned, including supporting more query parsers such as XML parsers, along with clearer documentation. Also note that this client doesn’t abstract away the Lucene query syntax in the query parameter, which could be provided as a utility function in this library, should I find an intuitive interface.</p>

<p>The inevitable next step is to build out the original side project I set out to work on, which is to build a better full-text search system for company documents. The best thing about this client is that it would not be the end of the world if nobody finds it useful, as long as it helps me on my original journey and I can find value in it myself.</p>

<p>YC’s famous motto <em>Make something people want</em> <sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">3</a></sup> is great advice for building venture-scale companies, but making something <em>I</em> want is more than enough as a heuristic for building side projects while keeping a full-time job, which was definitely the case for <a href="https://kengoa.github.io/software/2024/11/03/small-software.html">Visprex</a> in 2024 and Taiyo in 2025. I’m looking forward to writing about another side project this year, hopefully more often than in the past two years.</p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Here I focus on the UK as I’m based in London and more familiar with the UK startup ecosystem <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>I have used Copilot’s Agent mode extensively to write docstrings and generate unit tests based on the official Apache Solr documenation, provided via the <a href="http://context7.com/">context7</a> MCP server. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p>More details on Paul Graham’s essay <a href="https://paulgraham.com/good.html">Be Good</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="software" /><summary type="html"><![CDATA[Before switching to a platform engineering role within the AI Engineering Group earlier this year, I spent 9 months working on RAG with financial research documents.]]></summary></entry><entry><title type="html">On Being Mediocre at Hard Things</title><link href="https://kengoa.github.io/essay/2024/12/31/being-mediocre-at-hard-things.html" rel="alternate" type="text/html" title="On Being Mediocre at Hard Things" /><published>2024-12-31T09:00:00+00:00</published><updated>2024-12-31T09:00:00+00:00</updated><id>https://kengoa.github.io/essay/2024/12/31/being-mediocre-at-hard-things</id><content type="html" xml:base="https://kengoa.github.io/essay/2024/12/31/being-mediocre-at-hard-things.html"><![CDATA[<p>After a year of turmoil and struggles in the startup world, I started a new role at a big company in early 2024, which meant I once again had the mental space to pursue personal interests this year.</p>

<p>On the technical side, I worked on my data visualisation tool and <a href="https://kengoa.github.io/software/2024/11/03/small-software.html">wrote about it</a> on this blog. On the non-technical side, I took on two new hobbies: <b>long-distance running</b> and <b>chess</b>.</p>

<h3>Numbers</h3>
<p>While somewhat unrelated at first glance, the common denominator between running and chess is that your progress is measurable and easily trackable, and that you have full accountability over the results. Luck plays a very small role in running and chess compared to team sports or other types of games of skill that I had dabbled in. I signed up on Chess.com and reinstalled Strava on my phone after I accepted the job offer in February, and eventually it felt natural to me that I worked towards a certain goal for each. After all, I liked optimising metrics.</p>

<p>Around 8 months ago, I set myself to run a full marathon in 4 hours <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> and to reach a 1000 rating on Chess.com <sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> by the end of the year. As of writing, I have achieved the goal for chess and fell short for running. <a href="https://www.chess.com/stats/live/rapid/kngar/0">My Chess.com profile</a> shows I’ve played 865 games of Rapid Chess <sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup> in the past year with a peak rating of 1082 on the 17th of December, which puts me at around the 85th percentile on the platform. During this span, I won 446 games, drew 33, and lost 386. For running, I’ve recorded 60 runs for a total of 577 kilometers (358 miles) and I completed the Taipei Marathon two weeks ago with a personal net time of 4 hours, 11 minutes and 6 seconds, which is around the 54th percentile for the finishing time for males <sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>.</p>

<h3>Approach</h3>
<p>Those results are in line with my expectations based on the rates of progression from practices throughout the year, although it was much easier to form these expectations for chess as it had a much faster iteration cycle.</p>

<p>For marathon training, I did one long run every weekend with the distance progressively increasing from 10 km to 31 km, with occasional weekday runs between 5 and 10 kilometers. I initially suffered back pains after long runs, which then slowly went away after I started core strength training with planks. I also discovered <a href="https://www.reddit.com/r/Marathon_Training/">r/Marathon_Training</a> a few months before the race, which was helpful for learning fueling strategies and what to expect of <em>the unknown distance</em>, which for me was the last 11 kilometers. I can now confirm the conventional wisdom from this subreddit: a full marathon can be split into two halves - the first 32 km and the last 10 (or the first 20 miles and the last 6.2).</p>

<p>Chess, on the other hand, was much easier to progress and the overall mental burden was lower. After picking up the basics, I have exclusively played variations of <a href="https://www.chess.com/openings/London-System">The London System</a> as White and <a href="https://www.chess.com/openings/Kings-Indian-Defense">King’s Indian Defence</a> as Black, and used Chess.com’s Game Review functionality to look back on my games and watched <a href="https://www.youtube.com/channel/UCQHX6ViZmPsWiYSFAyS0a3Q">GothamChess</a> to learn principles and openings. As a side note, chess’s popularity exploded during COVID thanks to a combination of people staying at home and the release of Queen’s Gambit on Netflix, and while the online resources are truly wonderful, there’s lots of drama and gossip entertainment as competitive chess is a unique sport where the world’s top players are chronically online and active in the streaming space.</p>

<h3>Am I good enough?</h3>
<p>Long-distance running and chess are generally hard hobbies to get into, in that the initial learning curve is steep where lots of people drop off. Like language learning, you feel like you’re venturing into a new world and learning a lot at first, and then there’s a phase of plateau where you have to keep practicing consistently to get into the intermediate level. Once you get past it, however, there are lots of people who are very good, and it’s objectively clear that they’re very good.</p>

<p>The visibility of people who are very good often makes you question why you should pursue it at all, especially if you have a competitive personality like myself. It is obviously unreasonable to think that you can be decent at something you started less than a year ago, but I had to develop a sense of patience to limit the comparison within versions of myself and not with others. Accepting mediocrity, both internally and externally, was a sign of maturity that helped me focus on the trajectory and not the current state.</p>

<p>Eventually, I started to appreciate the non-competitive aspects of these activities.
Playing speed chess during odd hours of the day made me better at quickly switching my brain into a problem-solving mode under time pressure, which was a nice continuation from practicing for coding interviews earlier this year.</p>

<p>Running requires very little gear compared to other sports, meaning that there’s basically no constraints on locations. This added another aspect to my travels. Exploring new places on foot makes you feel like you belong in that new environment and allows you to see places a tourist might not. Apart from my usual runs in Victoria Park in London, I also went running in Innsbruck, Munich, Zurich, Naples, Kyoto, and Tokyo, and came back with a better sense of the geography each time.</p>

<h3>The year ahead</h3>

<p>Next year, I’m planning to get back into basketball where the goal is to play for a local amateur team and get some playing time. Another goal is to do a 5 km run in 20 minutes, which is practically a new sport. Unlike marathons which I can realistically attempt once or twice a year, I can aim for my personal best on a random weekday morning, and I’m excited to see how fast I can go.</p>

<hr />
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>This translates to running at the pace of 5:41 mins/km for 41.295 km, or 9:09 mins/mile for 26.2 miles. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>Chess.com uses the Glicko system for ratings, as explained in <a href="https://www.chess.com/terms/chess-ratings">Chess Ratings</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>A type of speed chess where each player is given a total of 10 minutes to make moves. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p>According to <a href="https://runrepeat.com/how-do-you-masure-up-the-runners-percentile-calculator">How fast a runner are you really? - RunRepeat</a> for a male with a 04:11:06 marathon record. <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="essay" /><summary type="html"><![CDATA[After a year of turmoil and struggles in the startup world, I started a new role at a big company in early 2024, which meant I once again had the mental space to pursue personal interests this year.]]></summary></entry><entry><title type="html">Small Software for Small Data</title><link href="https://kengoa.github.io/software/2024/11/03/small-software.html" rel="alternate" type="text/html" title="Small Software for Small Data" /><published>2024-11-03T20:10:00+00:00</published><updated>2024-11-03T20:10:00+00:00</updated><id>https://kengoa.github.io/software/2024/11/03/small-software</id><content type="html" xml:base="https://kengoa.github.io/software/2024/11/03/small-software.html"><![CDATA[<p>In this post, I’d like to reflect on the reasons why I think there’s a place for small software tailored for small data and my motivation for building a data visualisation tool called <em>Visprex</em>.</p>

<h2>Why Small Data?</h2>

<p>In February 2023, a blog article titled <a href="https://motherduck.com/blog/big-data-is-dead/">Big Data is Dead</a> was published by MotherDuck <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>, a cloud data warehouse company built on top of <a href="https://duckdb.org/">DuckDB</a>. Among several reasons why the prophesied Big Data movement is often misguided, one explanation that resonated with me the most is the separation between the analytics workload and the overall data sizes.</p>

<p>Reflecting on his time working on Google’s BigQuery, the author Jordan Tigani writes:</p>

<blockquote>
  <p>Customers with giant data sizes almost never queried huge amounts of data</p>
</blockquote>

<p>which is also reflective of my experience working at a large marketplace company with hundreds of millions of monthly transactions, where key analytics decisions were made from representative samples by data analysts and domain experts.</p>

<p>Another distinction I would like to illustrate here is that the type and size of data required in analytics and modelling are somewhat different from one another.</p>

<p>In the modern machine learning context, modelling is often associated with building a predictive system (i.e. with neural networks) which benefits from having more training data. More concretely, it helps to train the model with edge case scenarios which minimises the loss function on top of capturing general data trends, as the model is able to run inference on specific data points using high-dimensional features.</p>

<p>On the other hand, the output of analytics work is mainly actionable insights for business functions, where the stakeholders’ primary interest is coming up with a policy or a campaign that is effective for the average user in some segment or cohort. For those purposes, having representative user data is often more than enough.</p>

<p>There are always nuances in how analytics workload should be handled, but I do agree with the main points of the article that it is time to acknowledge the headaches associated with infrastructures built for scale, and that there’s room for small software in some contexts such as analytics and linear modelling where a simple, single-machine software can solve most of your problems at hand.</p>

<h2>Building Small Software</h2>

<p>In the spirit of building small software, I have been working on a side project called <a href="https://visprex.com">Visprex</a> for over a year now, which enables you to use your browser as a data visualisation tool for your CSV files.</p>

<p>The main features of Visprex include the ability to plot histograms, scatterplots, and a correlation matrix, with additional utility tools for inspecting individual data points from plots, feature transformations, and filtering. See <a href="https://docs.visprex.com">Visprex Docs</a> for more details and use cases.</p>

<h3>Motivation and Architectural Choices</h3>

<p>The focus on these visualisation steps is motivated by my background in economics which I read for my undergraduate degree. I spent a lot of time thinking about data distributions and linear relationships between features during this time, and I believe a small tool like Visprex can help you quickly build an intuition about your dataset without referring to data visualisation libraries on your software, which in my opinion is the least creative part of data science workflows.</p>

<p>While this software is not suitable for large datasets (&gt;100 MB), it is sufficient to get an idea of data distributions from representative samples in many analytics cases.</p>

<p>Another choice I made for this small data use case is to <b>process everything on the frontend</b> with TypeScript. While this naturally limits the capacity to handle large data, it provides two important benefits for the potential user and the maintainer.</p>

<p>The benefit for the user is data privacy. You can load sensitive data and don’t have to worry about it leaving your laptop. Convincing the user of this, however, is another story. A technical person can check the Network tab to see that there are no network calls made. Another simpler way to ensure this is to turn off your Wi-Fi after the site loads. While some mobile applications are successful at this, communicating that your website works offline remains a difficult problem.</p>

<p>The benefit for the maintainer is low upkeep. It costs very little for me to run this project as everything is bundled into less than 50 kilobytes of static assets and a few hundred kilobytes of minified JavaScript code, and I do not have to worry about maintaining a database which tends to be costly even for a small project.</p>

<h3>Upcoming Features</h3>

<p>I’m planning on adding a few more features to Visprex, and one is visualisation for time series data. I’m using <a href="https://www.papaparse.com/docs">Papa Parse</a> for parsing data from CSV files, and I need to come up with a better way to handle timestamps of different formats to conditionally display line charts for different features on the Y axis and the timestamp on the X axis.</p>

<p>Another feature I’m keen to work on is <a href="https://duckdb.org/docs/api/wasm/overview.html">DuckDB Wasm</a> integration, which would add a SQL editor tab for transforming and querying data and then updates the in-memory data automatically as inputs for your visualisation. While this increases the bundle size and is useful for advanced users who can write SQL queries in practice, it addresses the main limitation of <em>everything-on-the-frontend</em> approach, as WebAssembly can handle up to 4 GB of in-memory data <sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>.</p>

<h3>Looking Back</h3>

<p>It’s been an enjoyable and fulfilling experience so far to be able to build and showcase a tool that I personally think is useful to my friends and colleagues without worrying about data privacy or running costs. As my day jobs have always been in backend systems, I learnt a lot about building frontend applications with TypeScript and React along with managing a domain and writing documentation.</p>

<p>I’d also like to give credit to Dr Sean Brocklebank from the University of Edinburgh and Dr Anna Brocklebank for providing feedback on the early iterations of Visprex.</p>

<p>If you have any feedback or feature requests, please feel free to reach out at <a href="mailto:kengo@hey.com">kengo@hey.com</a>. If you’re interested in how I built Visprex, the code is open-source on <a href="https://github.com/visprex/visprex">GitHub</a>.</p>

<h3>Edit: HackerNews</h3>

<p>One week after I published this blog post, I made a submission to HackerNews titled <a href="https://news.ycombinator.com/item?id=42096837">Show HN: Visprex – Open-source, in-browser data visualisation tool for CSV files</a>. This unexpectedly went on to the front page over the weekend and I received a number of useful feedback comments, suggestions, upvotes, and GitHub stars. I was particularly happy to see <a href="https://news.ycombinator.com/item?id=42098358">this comment</a> from <em>parsimo2010</em>:</p>

<blockquote>
  <p>I like this a lot- I am going to show it to my students!
They seem to hate learning R, and while this doesn’t prevent them from having to build a model, this will speed up the exploration steps.</p>
</blockquote>

<p>which describes the exact audience and the use case I intended Visprex for.
Thank you to the HN community if you also came to this blog post from there. I’m planning to work on the features mentioned in the threads (timestamp parsing and data preprocessing), and will keep writing on this blog about major feature releases and updates.</p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p><a href="https://news.ycombinator.com/item?id=34694926">Hacker News discussion</a> on Big Data is Dead <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p><a href="https://stackoverflow.com/questions/40417774/memory-limits-in-webassembly">StackOverflow</a> post on memory limits in WebAssembly <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="software" /><summary type="html"><![CDATA[In this post, I’d like to reflect on the reasons why I think there’s a place for small software tailored for small data and my motivation for building a data visualisation tool called Visprex.]]></summary></entry></feed>