<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<title>Writing</title>
	<subtitle>Research notes, technical explorations, and things I find interesting.</subtitle>
	<link href="https://blog.stark.pub/posts/feed.xml" rel="self" type="application/atom+xml"/>
    <link href="https://blog.stark.pub/posts/"/>
	<updated>2026-08-31T00:00:00+00:00</updated>
	<id>https://blog.stark.pub/posts/feed.xml</id>
	<entry xml:lang="en">
		<title>Hermetic Enough: Nix Across the Software Lifecycle</title>
		<published>2026-08-31T00:00:00+00:00</published>
		<updated>2026-08-31T00:00:00+00:00</updated>
		<link href="https://blog.stark.pub/posts/hermetic-enough-nix/" type="text/html"/>
		<id>https://blog.stark.pub/posts/hermetic-enough-nix/</id>
		<content type="html">&lt;details open&gt;
  &lt;summary&gt;&lt;span&gt;TLDR&lt;&#x2F;span&gt;&lt;&#x2F;summary&gt;
  &lt;ul&gt;
&lt;li&gt;I started using Nix for declarative developer environments. It has gradually spread across most of the software lifecycle in my projects.&lt;&#x2F;li&gt;
&lt;li&gt;One flake now describes development shells, CI checks, release archives, Docker images, firmware builds, NixOS modules, and VM tests.&lt;&#x2F;li&gt;
&lt;li&gt;The nicest example is a NixOS test that boots two Norna nodes, connects them through a virtual network, and checks that they lose and rediscover each other.&lt;&#x2F;li&gt;
&lt;li&gt;Some parts are not hermetic. Prebuilt binaries, unsandboxed tests, and real hardware do not become pure because I put them in a Nix file.&lt;&#x2F;li&gt;
&lt;li&gt;Nix still has a steep learning curve, but I find the cost easier to justify when it solves more than local development.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;&#x2F;details&gt;
&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;a class=&quot;zola-anchor&quot; href=&quot;#intro&quot; aria-label=&quot;Anchor link for: intro&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Nix is a great tool for several things. I got started with declarative developer environments. It then became the operating system for my workstation, server, router, and VPS. Over the past year, I’ve moved more and more of the software lifecycle into Nix. This is an attempt to summarize what I’ve learned, what works for me, and perhaps convey why I get some amount of enjoyment from it :)&lt;&#x2F;p&gt;
&lt;p&gt;Three rather different projects have gradually converged on a similar Nix structure: &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;perstarkse&#x2F;minne&quot;&gt;Minne&lt;&#x2F;a&gt;, Norna, which I’m in the process of open-sourcing, and a private CMS project. The details differ, but the structure has stuck around long enough that I think it is worth sharing.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;example-norna&quot;&gt;Example, Norna&lt;a class=&quot;zola-anchor&quot; href=&quot;#example-norna&quot; aria-label=&quot;Anchor link for: example-norna&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Norna is a small distributed-state and policy project. It has a host daemon and firmware for an ESP32-C3. Two nodes should discover each other over UDP, exchange state, and notice when the other node disappears.&lt;&#x2F;p&gt;
&lt;p&gt;I could test the protocol in Rust and test the NixOS service separately. That still would not tell me whether two deployed services can actually find each other.&lt;&#x2F;p&gt;
&lt;p&gt;The Nix test boots two complete NixOS machines and gives them one virtual network:&lt;&#x2F;p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
  flowchart LR
    accTitle: Norna two-node test
    accDescr: Two Norna nodes exchange UDP traffic over a virtual network while the test controls both machines and checks their diagnostics.
    A[node A&amp;lt;br&amp;#x2F;&amp;gt;192.168.100.1] &amp;lt;--&amp;gt;|UDP| B[node B&amp;lt;br&amp;#x2F;&amp;gt;192.168.100.2]
    T[test] --&amp;gt; A
    T --&amp;gt; B
&lt;&#x2F;pre&gt;
&lt;p&gt;The test waits until both services are running and each node can see the other through its diagnostics socket. Then it stops node B. Node A should eventually mark it as no longer live. The test starts B again and waits until both nodes rediscover each other.&lt;&#x2F;p&gt;
&lt;p&gt;This is what made the larger pattern click for me. The same flake provides the daemon package, NixOS module, machine configuration, virtual network, and the command that decides whether the whole thing works. I can run it again after changing any of those pieces.&lt;&#x2F;p&gt;
&lt;p&gt;I have neighboring tests for packet loss, reordering, fanout, and a ten-node shape. QEMU is not a real network, but this catches considerably more than “both crates compiled”.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hermetic-enough&quot;&gt;Hermetic enough&lt;a class=&quot;zola-anchor&quot; href=&quot;#hermetic-enough&quot; aria-label=&quot;Anchor link for: hermetic-enough&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;A perfectly hermetic build has a strict meaning. My projects do not always reach it.&lt;&#x2F;p&gt;
&lt;p&gt;What I care about is knowing the important inputs and the places where outside state enters the build. If I download a prebuilt binary, the URL and hash should be visible. If a test needs an unsandboxed builder, that should be visible too. Unsupported targets should fail deliberately rather than work by accident on my machine.&lt;&#x2F;p&gt;
&lt;p&gt;Minne uses prebuilt dependencies for parts of its stack. On &lt;code&gt;x86_64-linux&lt;&#x2F;code&gt;, for example, it fetches a SurrealDB archive by hash and patches the binary against libraries from the locked nixpkgs input. Other supported systems use SurrealDB from nixpkgs. The downloaded archive is pinned, but compatibility with the selected libraries is still something I need to test.&lt;&#x2F;p&gt;
&lt;p&gt;The important versions and hashes are gathered in one file:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nix&quot; data-name=&quot;nix&#x2F;versions.nix&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot; data-name=&quot;nix&#x2F;versions.nix&quot;&gt;{
  minneVersion = &amp;quot;1.0.5&amp;quot;;
  ortVersion = &amp;quot;1.23.2&amp;quot;;
  rustVersion = &amp;quot;1.91.1&amp;quot;;
  rustManifestSha256 = &amp;quot;sha256-SDu4snEWjuZU475PERvu+iO50Mi39KVjqCeJeNvpguU=&amp;quot;;
  # ...
  surrealdbVersion = &amp;quot;2.6.5&amp;quot;;
  # Platform-specific hashes follow.
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Native dependencies are still annoying, but I know where to start looking when one changes.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;one-set-of-inputs-several-build-graphs&quot;&gt;One set of inputs, several build graphs&lt;a class=&quot;zola-anchor&quot; href=&quot;#one-set-of-inputs-several-build-graphs&quot; aria-label=&quot;Anchor link for: one-set-of-inputs-several-build-graphs&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;I used to call this “one locked graph”. That is close enough for conversation, but not quite accurate.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;flake.lock&lt;&#x2F;code&gt; pins the flake inputs. &lt;code&gt;Cargo.lock&lt;&#x2F;code&gt;, fixed-output hashes, and other project files pin more of the application. Nix then evaluates different derivation graphs depending on the host, target, features, and requested output. A Windows archive and ESP32-C3 firmware obviously do not share one identical closure.&lt;&#x2F;p&gt;
&lt;p&gt;The relationship looks more like this:&lt;&#x2F;p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
  flowchart LR
    accTitle: Project outputs derived from locked inputs
    accDescr: Locked project inputs feed development shells, CI checks, packages, release archives, a Docker image, NixOS modules, VM tests, and firmware checks.
    lock[locked inputs&amp;lt;br&amp;#x2F;&amp;gt;flake.lock + Cargo.lock + hashes] --&amp;gt; dev[dev shell]
    lock --&amp;gt; ci[CI checks]
    lock --&amp;gt; pkg[packages]
    pkg --&amp;gt; release[release archives]
    pkg --&amp;gt; image[Docker image]
    pkg --&amp;gt; module[NixOS module]
    module --&amp;gt; vm[VM tests]
    lock --&amp;gt; firmware[firmware]
&lt;&#x2F;pre&gt;
&lt;p&gt;Norna’s &lt;code&gt;nix&#x2F;&lt;&#x2F;code&gt; directory shows how I currently arrange this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;nix&amp;#x2F;
├── build
│   ├── build-std-vendor-crates.nix
│   ├── firmware-esp32c3.nix
│   ├── firmware-qemu-esp32c3.nix
│   ├── no-std-kernel.nix
│   ├── release.nix
│   ├── toolchain.nix
│   └── write-cargo-checksum.py
├── modules
│   ├── agent.nix
│   ├── checks.nix
│   ├── context.nix
│   ├── dev-shell.nix
│   ├── formatter.nix
│   └── packages.nix
└── tests
    ├── embedded-smoke.nix
    ├── network-chaos.nix
    ├── network-fanout.nix
    ├── network-scale.nix
    ├── network-sync.nix
    ├── smoke.nix
    └── tui-smoke.nix
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;build&#x2F;&lt;&#x2F;code&gt; contains package and cross-target recipes. &lt;code&gt;modules&#x2F;&lt;&#x2F;code&gt; exposes them as packages, checks, apps, and development shells. &lt;code&gt;tests&#x2F;&lt;&#x2F;code&gt; contains the NixOS tests that boot whole systems.&lt;&#x2F;p&gt;
&lt;p&gt;I initially put much more directly in &lt;code&gt;flake.nix&lt;&#x2F;code&gt;. Eventually it became a long mix of toolchains, packages, and scripts. This split is easier for me to navigate, and there is very little magic to it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;from-development-shell-to-release-artifact&quot;&gt;From development shell to release artifact&lt;a class=&quot;zola-anchor&quot; href=&quot;#from-development-shell-to-release-artifact&quot; aria-label=&quot;Anchor link for: from-development-shell-to-release-artifact&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;For Minne, the development shell selects the Rust toolchain and native build dependencies. &lt;code&gt;nix run .#dev&lt;&#x2F;code&gt; starts a process-compose stack with SurrealDB, MinIO, Tailwind, and the setup around them. &lt;code&gt;nix run .#dev-down&lt;&#x2F;code&gt; stops it again.&lt;&#x2F;p&gt;
&lt;p&gt;Native libraries were what pushed this beyond a developer-shell project. When I added reranking to Minne, ONNX Runtime worked locally and then failed in Nix, Docker, and release builds in slightly different ways. I wrote a &lt;a href=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;bundling-onnxruntime-rust-nix&#x2F;&quot;&gt;separate post about that adventure&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;What I ended up doing was fairly simple: disable the crate’s automatic download, make ONNX Runtime an explicit input, and supply &lt;code&gt;ORT_DYLIB_PATH&lt;&#x2F;code&gt; from the package. The application accepts a path. Nix, Docker, or the release archive decides where that path points.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;embedded-targets&quot;&gt;Embedded targets&lt;a class=&quot;zola-anchor&quot; href=&quot;#embedded-targets&quot; aria-label=&quot;Anchor link for: embedded-targets&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Norna’s daemon uses a stable host toolchain. The firmware has its own ESP32-C3 toolchain and target. The portable core also needs to compile with &lt;code&gt;no_std&lt;&#x2F;code&gt; and &lt;code&gt;alloc&lt;&#x2F;code&gt; for &lt;code&gt;riscv32imc-unknown-none-elf&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The check runs:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;cargo build --offline \
  --target riscv32imc-unknown-none-elf \
  -Z build-std=core,alloc \
  -p norna-core --no-default-features
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Getting this offline was decidedly not elegant. Cargo’s normal vendoring did not include everything required by &lt;code&gt;-Z build-std&lt;&#x2F;code&gt;. The Nix code therefore contains a long list of standard-library crates and hashes, while a small Python script creates the Cargo checksum files. The firmware derivation also writes reduced Cargo manifests for its isolated build.&lt;&#x2F;p&gt;
&lt;p&gt;This creates a shadow dependency description which can drift from the workspace. It currently proves that the portable core and firmware build in the intended environment, but Rust upgrades will require care. I would only copy this machinery for an embedded target that actually needs it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ci-and-agents&quot;&gt;CI and agents&lt;a class=&quot;zola-anchor&quot; href=&quot;#ci-and-agents&quot; aria-label=&quot;Anchor link for: ci-and-agents&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;CI can easily become another build system. In these projects it mostly installs Nix, restores a store cache where one is configured, and builds a named output.&lt;&#x2F;p&gt;
&lt;p&gt;Minne and the CMS run &lt;code&gt;nix flake check&lt;&#x2F;code&gt; in CI. Norna’s normal GitHub workflow only builds &lt;code&gt;checks.x86_64-linux.agent-fast&lt;&#x2F;code&gt;. That gate contains formatting, Clippy, workspace tests, and coverage. The firmware and NixOS VM checks exist as outputs, but they are not currently run by the ordinary pull-request workflow.&lt;&#x2F;p&gt;
&lt;p&gt;Minne’s release workflow needs the ONNX Runtime version, so it reads it from the flake:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;nix eval .#lib.ortVersion --raw
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The flake also exposes a named check for agreement between that pin and the nixpkgs package:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nix&quot; data-name=&quot;nix&#x2F;modules&#x2F;checks.nix&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot; data-name=&quot;nix&#x2F;modules&#x2F;checks.nix&quot;&gt;ortVersion = pkgs.runCommand &amp;quot;ort-version-check&amp;quot; { } &amp;#x27;&amp;#x27;
  if [ &amp;quot;${pkgs.onnxruntime.version}&amp;quot; != &amp;quot;${ortVersion}&amp;quot; ]; then
    echo &amp;quot;pkgs.onnxruntime.version is ${pkgs.onnxruntime.version}, but flake pins ${ortVersion}&amp;quot; &amp;gt;&amp;amp;2
    exit 1
  fi
  touch $out
&amp;#x27;&amp;#x27;;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Agents use the same outputs. &lt;code&gt;nix develop --command ...&lt;&#x2F;code&gt; gives them the pinned tools and native dependencies instead of inviting them to guess which compiler to install. It is not a security boundary. The development shell does not hide credentials, disable the network, or prevent writes to my home directory.&lt;&#x2F;p&gt;
&lt;p&gt;Norna makes the fast path explicit:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nix&quot; data-name=&quot;nix&#x2F;modules&#x2F;agent.nix&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot; data-name=&quot;nix&#x2F;modules&#x2F;agent.nix&quot;&gt;agentFastInputs = [
  self&amp;#x27;.checks.treefmt
  workspaceClippy
  workspaceTests
  workspaceCoverage
];
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A full check set with cross toolchains and several QEMU machines is too slow for every edit, especially on a cold store. The quick path keeps the feedback loop useful. The heavier outputs still need to run somewhere regularly; merely naming a check does not verify it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;testing-deployment-details&quot;&gt;Testing deployment details&lt;a class=&quot;zola-anchor&quot; href=&quot;#testing-deployment-details&quot; aria-label=&quot;Anchor link for: testing-deployment-details&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The Norna example at the start tests the network between two deployed services. The simpler VM tests are useful too.&lt;&#x2F;p&gt;
&lt;p&gt;Minne imports its actual NixOS module, starts SurrealDB and Minne, and checks the live and ready endpoints:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nix&quot; data-name=&quot;nix&#x2F;tests&#x2F;vm-smoke.nix&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot; data-name=&quot;nix&#x2F;tests&#x2F;vm-smoke.nix&quot;&gt;testScript = &amp;#x27;&amp;#x27;
  machine.wait_for_unit(&amp;quot;surrealdb.service&amp;quot;)
  machine.wait_for_unit(&amp;quot;minne.service&amp;quot;)
  machine.wait_for_open_port(3000)
  machine.succeed(&amp;quot;curl -sf http:&amp;#x2F;&amp;#x2F;127.0.0.1:3000&amp;#x2F;api&amp;#x2F;v1&amp;#x2F;live&amp;quot;)
  machine.succeed(&amp;quot;curl -sf http:&amp;#x2F;&amp;#x2F;127.0.0.1:3000&amp;#x2F;api&amp;#x2F;v1&amp;#x2F;ready&amp;quot;)
&amp;#x27;&amp;#x27;;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The CMS tests more of its surrounding system. One VM checks systemd hardening, reaches the application through nginx over TLS, runs the backup service, and validates the archive with &lt;code&gt;gzip -t&lt;&#x2F;code&gt;. Another boots Garage beside the CMS, signs in through the admin interface, uploads an image, and compares the downloaded bytes with the original.&lt;&#x2F;p&gt;
&lt;p&gt;These tests use fake credentials and validate the NixOS deployment. They take time and work best with KVM acceleration, which is why I want the cheap evaluation and package checks as well.&lt;&#x2F;p&gt;
&lt;p&gt;What they give me is confidence in details below the Rust process: systemd settings, environment mapping, ports, proxies, files, and communication between services. Those are exactly the details I otherwise tend to discover after deployment.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-trade&quot;&gt;The trade&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-trade&quot; aria-label=&quot;Anchor link for: the-trade&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;This setup has real costs. Nix evaluation errors can be cryptic. Cross-compilation still requires understanding linkers and targets. The offline &lt;code&gt;build-std&lt;&#x2F;code&gt; workaround is custom maintenance. VM closures are large, and cold caches hurt. A team adopting this structure would also have to learn enough Nix to debug it rather than depending on one person.&lt;&#x2F;p&gt;
&lt;p&gt;It is hermetic enough for me: explicit enough that I can find the compromises and test the important paths.&lt;&#x2F;p&gt;
</content>
	</entry>
	<entry xml:lang="en">
		<title>Refactoring a retrieval pipeline through evaluation</title>
		<published>2025-11-10T00:00:00+00:00</published>
		<updated>2025-11-10T00:00:00+00:00</updated>
		<link href="https://blog.stark.pub/posts/eval-retrieval-refactor/" type="text/html"/>
		<id>https://blog.stark.pub/posts/eval-retrieval-refactor/</id>
		<content type="html">&lt;details open&gt;
  &lt;summary&gt;&lt;span&gt;TLDR&lt;&#x2F;span&gt;&lt;&#x2F;summary&gt;
  &lt;ul&gt;
&lt;li&gt;I am the developer behind &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;perstarkse&#x2F;minne&quot;&gt;Minne&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Minne uses a combined retrieval strategy for the “slow search” consisting of vector comparison, full text search, exploration of a graph database&lt;&#x2F;li&gt;
&lt;li&gt;Current design was created in 2024, inspired by psychology and a Zettelkasten style of knowledge mapping&lt;&#x2F;li&gt;
&lt;li&gt;The post details the process of implementing a evaluation pattern against several established datasets to evaluate and steer continued development&lt;&#x2F;li&gt;
&lt;li&gt;The end results showed improved performance with latency reduced from 1600ms to 36ms&lt;&#x2F;li&gt;
&lt;li&gt;Reranking proved to add significant latency to no improvement&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;&#x2F;details&gt;
&lt;h2 id=&quot;how-did-i-actually-get-in-this-mess-and-background&quot;&gt;How did I actually get in this mess, and background&lt;a class=&quot;zola-anchor&quot; href=&quot;#how-did-i-actually-get-in-this-mess-and-background&quot; aria-label=&quot;Anchor link for: how-did-i-actually-get-in-this-mess-and-background&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;For the past year my passion project has been developing &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;perstarkse&#x2F;minne&quot;&gt;Minne&lt;&#x2F;a&gt;, which is a save-for-later and personal knowledge management system (PKMS). Since learning about the &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;zettelkasten.de&#x2F;introduction&#x2F;&quot;&gt;Zettelkasten&lt;&#x2F;a&gt; method, I became quite enamored with the idea of having a personal knowledge database. I’ve tried a couple of different tools, but none really to my liking. While I truly believe a lot of the benefits of a PKMS comes from the manual process of creating links between knowledge and documents, by means of creating neural pathways and working through knowledge, I’ve learned that I’m not &lt;em&gt;that&lt;&#x2F;em&gt; enamored with PKMS. As a psychologist, I’m fascinated by how we offload memory to digital tools. So development began.&lt;&#x2F;p&gt;
&lt;p&gt;When designing Minne in 2024 I envisioned having content as a base layer, and then extracted knowledge entities as an abstraction of it. And that this higher level plane of knowledge could work well for searching and working on. That then these knowledge entities are based in and connected to the underlying documents&#x2F;content that it was derived from. I think it came out quite nifty, and to some respect it mimics a human learning pattern.&lt;&#x2F;p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
  flowchart TB
subgraph Ingestion
A[Content] --&amp;gt; B[Chunks]
B --&amp;gt; C[Knowledge Entities]
end
subgraph Retrieval
C --&amp;gt; D[Graph Exploration]
C --&amp;gt; E[Vector + Text Search]
end
&lt;&#x2F;pre&gt;
&lt;p&gt;We humans learn these higher level abstractions connected to different entities which we then can reference and explore if we need more detail. That these second layer knowledge entities exists as a compressed version and that some fidelity loss occurs but that’s fine. Can’t overload the context anyway, or we lose quality.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;current-retrieval-pipeline&quot;&gt;Current retrieval pipeline&lt;a class=&quot;zola-anchor&quot; href=&quot;#current-retrieval-pipeline&quot; aria-label=&quot;Anchor link for: current-retrieval-pipeline&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;In the application we look through this knowledge entity database in two separate occurrences. For simplicity I call this &lt;em&gt;slow search&lt;&#x2F;em&gt;. As of writing, this retrieval pipeline consists of vector comparison, full text search, and exploration of the graph database.
&lt;pre class=&quot;mermaid&quot;&gt;
  flowchart LR
Q[Query&amp;#x2F;Embedding] --&amp;gt; V[Vector Search]
Q --&amp;gt; L[Lexical Search]
V --&amp;gt; M[Fuse Scores]
L --&amp;gt; M
M --&amp;gt; D[Expand Graph]
D --&amp;gt; E[Attach Chunks]
E --&amp;gt; F[Rerank]
F --&amp;gt; G[Assemble]
&lt;&#x2F;pre&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;When we ingest new content, we run slow search so the LLM that creates new knowledge entities can link them to existing ones and work with a context-aware view of the graph.&lt;&#x2F;li&gt;
&lt;li&gt;When we chat with an LLM in the application, it gets the result of slow search as context, which lets it answer in a knowledge-aware way and attach references.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;While I &lt;em&gt;like&lt;&#x2F;em&gt; the current design, it’s not the fastest. We run several different database operations on quite a lot of data, using the knowledge entities as primary candidates. The project has started to see some real users, and I’d like to explore optimizing the retrieval pipeline. It should serve its purpose first, not just give me the pleasure of maintaining a complex retrieval pipeline.&lt;&#x2F;p&gt;
&lt;figure&gt;
    &lt;img src=&quot;.&amp;#x2F;office-space-meme.webp&quot; alt=&quot;meme trying to captivate my current stance on the complex retrieval pipeline&quot;&gt;
    
    &lt;figcaption&gt;What does this *nice* and complex retrieval pipeline actually do for me, and is it doing it well enough to justify the complexity?&lt;&#x2F;figcaption&gt;
    
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;getting-started&quot;&gt;Getting started&lt;a class=&quot;zola-anchor&quot; href=&quot;#getting-started&quot; aria-label=&quot;Anchor link for: getting-started&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;So to proceed I opted to set up a evaluation framework, measuring both speed and retrieval accuracy. At this point in time I’m not evaluating the actual LLM performance, but rather the accuracy of the slow search itself. Which means answering the question, is the answer to a question inside the data retrieved from our knowledge system?&lt;&#x2F;p&gt;
&lt;h3 id=&quot;datasets&quot;&gt;Datasets&lt;a class=&quot;zola-anchor&quot; href=&quot;#datasets&quot; aria-label=&quot;Anchor link for: datasets&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;For datasets, I faced an interesting decision. What type of data should this “slow search” be optimized for. What are the users actually ingesting in the system? What am I ingesting? I’ve found I use it for an assortment of things, written notes, audio notes, links for reading later, some pdf documents. But creating a meaningful representative dataset would be hard. So even though it would have been preferred I opted to abandon creating my own dataset, and use established ones. I chose two:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;SQUAD v2, it acts as a simpler and lighter dataset. Shorter paragraphs.&lt;&#x2F;li&gt;
&lt;li&gt;Natural Questions (dev). It’s a dataset on wikipedia articles. Some articles are really long and could prove to be challenging.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Initially, I stuck to these two. After tuning for these sets I tried adding hybrid search, which consequently lowered results. Which ran some bells about me not understanding something enough… So I eventually brought in some more tests in from the BEIR test suite. More on that those later.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;evaluation-harness&quot;&gt;Evaluation harness&lt;a class=&quot;zola-anchor&quot; href=&quot;#evaluation-harness&quot; aria-label=&quot;Anchor link for: evaluation-harness&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;For my current design, each evaluation means running each article through the ingestion pipeline to create the knowledge entities and relationships. This is a somewhat costly operation. To test retrieval precision effectively, I needed distractors. So for each question, I opted to ingest nine incorrect contexts along with the correct one. This meant that to answer the first 200 questions, I had to ingest a total of 2000 articles.&lt;&#x2F;p&gt;
&lt;p&gt;Considering the computational effort involved in converting the raw dataset to my knowledge design, I needed persistence. This allows me to decouple the ingestion cost from the retrieval experimentation. I can iterate on the retrieval pipeline against a static, pre-ingested set. If I need to change the ingestion logic, I can simply recreate the set. This setup also gives me the flexibility to modify parameters at runtime, like the number of snippets to pass, the total amount to retrieve from the initial search, or whether to enable re-ranking.&lt;&#x2F;p&gt;
&lt;details &gt;
  &lt;summary&gt;&lt;span&gt;Infrastructure: The in-memory detour&lt;&#x2F;span&gt;&lt;&#x2F;summary&gt;
  &lt;p&gt;I initially opted to use an embedded in-memory database instance for the evaluations. While great for unit tests, not the best approach for this.&lt;&#x2F;p&gt;
&lt;p&gt;Since the DB was ephemeral, I had to re-ingest and re-index the entire dataset (2000+ articles) for every single evaluation run. Creating HNSW and BM25 indexes is computationally expensive, and the embedded SurrealDB engine handled index creation in a single thread. This meant waiting hours just to run a quick retrieval test.&lt;&#x2F;p&gt;
&lt;p&gt;The solution was obvious: move to a persistent SurrealDB instance. This decoupled the heavy indexing step from the retrieval tests, allowing me to iterate on the pipeline in seconds rather than hours.&lt;&#x2F;p&gt;

&lt;&#x2F;details&gt;
&lt;h3 id=&quot;initial-findings&quot;&gt;Initial findings&lt;a class=&quot;zola-anchor&quot; href=&quot;#initial-findings&quot; aria-label=&quot;Anchor link for: initial-findings&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Initial runs on datasets showed the following results, which in my opinion are fairly respectable, except for latency. The majority of latency comes from the embedding and reranking, or so I thought! More detailed logging showed that for moderately sized datasets the majority of latency comes from the search and chunk loading. It worked fine for my smallish production dataset, but scaled really badly. Which makes it clear I need to do some heavy refactoring. So preliminary tests shows good hit@k1-5, but abhorrent latency.&lt;&#x2F;p&gt;
&lt;details &gt;
  &lt;summary&gt;&lt;span&gt;SurrealDB: Pains&lt;&#x2F;span&gt;&lt;&#x2F;summary&gt;
  &lt;p&gt;When running the evals for the Natural Questions dataset (about 1000 wiki articles), another issue became clear. When creating HNSW indexes, memory usage in SurrealDB spiked greatly, to well over 100GB, and it was creating intermediate disk entities in excess of 20GB. It was clear something was wrong.&lt;&#x2F;p&gt;
&lt;p&gt;In efforts to resolve this I tried:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Separating embeddings to a separate table (negligible impact).&lt;&#x2F;li&gt;
&lt;li&gt;Modifying HNSW parameters (helped, but not dramatically).&lt;&#x2F;li&gt;
&lt;li&gt;Removing HNSW indexes from the initial migration and only applying them &lt;em&gt;after&lt;&#x2F;em&gt; ingestion.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The last point turned out to be the key. The initial process causing the spike was: applying migrations containing a 1536 HNSW index -&amp;gt; removing the index -&amp;gt; ingesting data -&amp;gt; creating index.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;&#x2F;strong&gt; I have since moved to a memory-backed SurrealDB instance running in Docker. Interestingly, RAM usage has become even better during ingestion with this setup compared to the rocksdb backed host. I’m not 100% sure why, but the previous behavior remains a bit of an annoyance&#x2F;quirk.
&lt;strong&gt;Update 2&lt;&#x2F;strong&gt; Considering how well memory backed surrealdb works compared to rocksdb backed, at least when running via docker. It’s clear that running rocksdb backed via docker was a very bad idea. Have not explored yet if this is to be expected or a bug.&lt;&#x2F;p&gt;

&lt;&#x2F;details&gt;
&lt;h2 id=&quot;vector-search&quot;&gt;Vector search&lt;a class=&quot;zola-anchor&quot; href=&quot;#vector-search&quot; aria-label=&quot;Anchor link for: vector-search&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;So, now we’ve got a decent enough evaluation pipeline. Let’s start by establishing a baseline.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;experimental-setup&quot;&gt;Experimental setup&lt;a class=&quot;zola-anchor&quot; href=&quot;#experimental-setup&quot; aria-label=&quot;Anchor link for: experimental-setup&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;For all experiments, unless otherwise noted, we are using:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Embeddings&lt;&#x2F;strong&gt;: &lt;code&gt;nomic-ai&#x2F;nomic-embed-text-v1.5&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Tokenizer&lt;&#x2F;strong&gt;: &lt;code&gt;bert-base-cased&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Reranker&lt;&#x2F;strong&gt;: &lt;code&gt;jinaai&#x2F;jina-reranker-v1-turbo-en&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;reranking-surprise&quot;&gt;Reranking surprise&lt;a class=&quot;zola-anchor&quot; href=&quot;#reranking-surprise&quot; aria-label=&quot;Anchor link for: reranking-surprise&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;I initially ran all my tests with a reranker enabled, assuming it would boost performance. However, when I finally ran a test &lt;em&gt;without&lt;&#x2F;em&gt; it, I was shocked.&lt;&#x2F;p&gt;
&lt;figure&gt;
    &lt;img src=&quot;.&amp;#x2F;reranking-suprise.webp&quot; alt=&quot;meme, I&amp;#x27;m quite surprised that reranking is lowering my scores&quot;&gt;
    
    &lt;figcaption&gt;Wait, whaat?&lt;&#x2F;figcaption&gt;
    
&lt;&#x2F;figure&gt;
&lt;p&gt;The reranker was adding significant latency (~550ms per query) but actually &lt;em&gt;lowered&lt;&#x2F;em&gt; performance on the Natural Questions dataset.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lesson Learned:&lt;&#x2F;strong&gt; Always verify your assumptions.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;For the rest of this post, results are presented &lt;strong&gt;without reranking&lt;&#x2F;strong&gt;, unless otherwise specified.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ablations&quot;&gt;Ablations&lt;a class=&quot;zola-anchor&quot; href=&quot;#ablations&quot; aria-label=&quot;Anchor link for: ablations&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Here are the results for different vector chunk sizes and strategies.&lt;&#x2F;p&gt;







&lt;div class=&quot;space-y-6&quot;&gt;
  
  
  
  
&lt;div class=&quot;space-y-2&quot; style=&quot;margin: 1.25rem 0; position: relative;&quot;&gt;
  &lt;table
    style=&quot;width:100%; border-collapse:collapse; border-spacing:0; font-size:0.95em; line-height:1.45; border:0 !important; outline:0 !important; box-shadow:none !important;&quot;&gt;
    &lt;caption
      style=&quot;text-align:left; font-weight:600; letter-spacing:-0.01em; padding:4px 0 6px 4px; border-bottom:0; border-top:0; border-left:0; border-right:0; outline:0; box-shadow:none;&quot;&gt;
      Baseline (Hybrid + Reranker)
    &lt;&#x2F;caption&gt;
    &lt;thead style=&quot;border-bottom:1px solid #e5e7eb;&quot;&gt;
      &lt;tr&gt;
        &lt;th style=&quot;text-align:left; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;dataset&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;mrr&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;ndcg&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;hit@5&lt;&#x2F;th&gt;
        
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;avg ms&lt;&#x2F;th&gt;
        
      &lt;&#x2F;tr&gt;
    &lt;&#x2F;thead&gt;
    &lt;tbody&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important; border-bottom:1px solid #f3f4f6 !important; text-align:left;&quot;&gt;
          SQuAD v2.0&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.757
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.769
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.855
          
        &lt;&#x2F;td&gt;

        
        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          1488
          
        &lt;&#x2F;td&gt;
        
      &lt;&#x2F;tr&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important;  text-align:left;&quot;&gt;
          Natural Questions (dev)&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.863
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.863
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.87
          
        &lt;&#x2F;td&gt;

        
        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          3850
          
        &lt;&#x2F;td&gt;
        
      &lt;&#x2F;tr&gt;
      
    &lt;&#x2F;tbody&gt;
  &lt;&#x2F;table&gt;
  
  &lt;aside style=&quot;
        display:block;
        font-size:0.9em;
        color:var(--text-pale-color, #6b7280);
        margin:0.5rem 0 0;
        padding:0.5rem 0;
        position:absolute;
        top:50%;
        transform:translateY(-50%);
        right:100%;
        margin-right:1rem;
        width:26ch;
        max-width:30ch;
        min-width:20ch;
        padding-right:0;
        border-right:0;
        white-space:normal;
        word-break:normal;
        hyphens:auto;
        float:none;
      &quot; class=&quot;evals-note-desktop&quot;&gt;
    &lt;div style=&quot;margin-top:0.15rem;&quot;&gt;&lt;p&gt;&lt;strong&gt;Config:&lt;&#x2F;strong&gt;&lt;br&gt;- Strategy: Complex Hybrid (Vector + Lexical + Graph)&lt;br&gt;- Reranker: On&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
  &lt;&#x2F;aside&gt;
  &lt;aside style=&quot;
        display:none;
        font-size:0.9em;
        color:var(--text-pale-color, #6b7280);
        margin:0 0 0 0;
        padding:0 0 0;
        width:100%;
        max-width:100%;
        min-width:0;
        position:static;
        left:auto;
        transform:none;
        white-space:normal;
        word-break:normal;
        hyphens:auto;
      &quot; class=&quot;evals-note-mobile&quot;&gt;
    &lt;div style=&quot;margin-top:0.15rem;&quot;&gt;&lt;p&gt;&lt;strong&gt;Config:&lt;&#x2F;strong&gt;&lt;br&gt;- Strategy: Complex Hybrid (Vector + Lexical + Graph)&lt;br&gt;- Reranker: On&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
  &lt;&#x2F;aside&gt;
  
&lt;&#x2F;div&gt;

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
&lt;&#x2F;div&gt;

&lt;style&gt;
  @media (max-width: 1024px) {
    .evals-note-desktop {
      display: none !important;
    }

    .evals-note-mobile {
      display: block !important;
    }
  }

  @media (min-width: 1025px) {
    .evals-note-desktop {
      display: block !important;
    }

    .evals-note-mobile {
      display: none !important;
    }
  }
&lt;&#x2F;style&gt;







&lt;div class=&quot;space-y-6&quot;&gt;
  
  
  
  
  
  
&lt;div class=&quot;space-y-2&quot; style=&quot;margin: 1.25rem 0; position: relative;&quot;&gt;
  &lt;table
    style=&quot;width:100%; border-collapse:collapse; border-spacing:0; font-size:0.95em; line-height:1.45; border:0 !important; outline:0 !important; box-shadow:none !important;&quot;&gt;
    &lt;caption
      style=&quot;text-align:left; font-weight:600; letter-spacing:-0.01em; padding:4px 0 6px 4px; border-bottom:0; border-top:0; border-left:0; border-right:0; outline:0; box-shadow:none;&quot;&gt;
      Vector Only (256-512)
    &lt;&#x2F;caption&gt;
    &lt;thead style=&quot;border-bottom:1px solid #e5e7eb;&quot;&gt;
      &lt;tr&gt;
        &lt;th style=&quot;text-align:left; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;dataset&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;mrr&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;ndcg&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;hit@5&lt;&#x2F;th&gt;
        
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;avg ms&lt;&#x2F;th&gt;
        
      &lt;&#x2F;tr&gt;
    &lt;&#x2F;thead&gt;
    &lt;tbody&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important; border-bottom:1px solid #f3f4f6 !important; text-align:left;&quot;&gt;
          SQuAD v2.0&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.859
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (+0.102)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.875
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (+0.106)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.925
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (+0.07)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          24
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (-1464)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;
        
      &lt;&#x2F;tr&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important;  text-align:left;&quot;&gt;
          Natural Questions&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.628
          
          &lt;span
            style=&quot;font-size:0.85em; color:#dc2626;&quot;&gt;
            (-0.234)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.662
          
          &lt;span
            style=&quot;font-size:0.85em; color:#dc2626;&quot;&gt;
            (-0.201)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.808
          
          &lt;span
            style=&quot;font-size:0.85em; color:#dc2626;&quot;&gt;
            (-0.062)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          69
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (-3781)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;
        
      &lt;&#x2F;tr&gt;
      
    &lt;&#x2F;tbody&gt;
  &lt;&#x2F;table&gt;
  
  &lt;aside style=&quot;
        display:block;
        font-size:0.9em;
        color:var(--text-pale-color, #6b7280);
        margin:0.5rem 0 0;
        padding:0.5rem 0;
        position:absolute;
        top:50%;
        transform:translateY(-50%);
        right:100%;
        margin-right:1rem;
        width:26ch;
        max-width:30ch;
        min-width:20ch;
        padding-right:0;
        border-right:0;
        white-space:normal;
        word-break:normal;
        hyphens:auto;
        float:none;
      &quot; class=&quot;evals-note-desktop&quot;&gt;
    &lt;div style=&quot;margin-top:0.15rem;&quot;&gt;&lt;p&gt;&lt;strong&gt;Config:&lt;&#x2F;strong&gt;&lt;br&gt;- Strategy: Vector Only&lt;br&gt;- Chunk Size: 256-512&lt;br&gt;- Reranker: Off&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
  &lt;&#x2F;aside&gt;
  &lt;aside style=&quot;
        display:none;
        font-size:0.9em;
        color:var(--text-pale-color, #6b7280);
        margin:0 0 0 0;
        padding:0 0 0;
        width:100%;
        max-width:100%;
        min-width:0;
        position:static;
        left:auto;
        transform:none;
        white-space:normal;
        word-break:normal;
        hyphens:auto;
      &quot; class=&quot;evals-note-mobile&quot;&gt;
    &lt;div style=&quot;margin-top:0.15rem;&quot;&gt;&lt;p&gt;&lt;strong&gt;Config:&lt;&#x2F;strong&gt;&lt;br&gt;- Strategy: Vector Only&lt;br&gt;- Chunk Size: 256-512&lt;br&gt;- Reranker: Off&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
  &lt;&#x2F;aside&gt;
  
&lt;&#x2F;div&gt;

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
&lt;&#x2F;div&gt;

&lt;style&gt;
  @media (max-width: 1024px) {
    .evals-note-desktop {
      display: none !important;
    }

    .evals-note-mobile {
      display: block !important;
    }
  }

  @media (min-width: 1025px) {
    .evals-note-desktop {
      display: block !important;
    }

    .evals-note-mobile {
      display: none !important;
    }
  }
&lt;&#x2F;style&gt;







&lt;div class=&quot;space-y-6&quot;&gt;
  
  
  
  
  
  
  
  
&lt;div class=&quot;space-y-2&quot; style=&quot;margin: 1.25rem 0; position: relative;&quot;&gt;
  &lt;table
    style=&quot;width:100%; border-collapse:collapse; border-spacing:0; font-size:0.95em; line-height:1.45; border:0 !important; outline:0 !important; box-shadow:none !important;&quot;&gt;
    &lt;caption
      style=&quot;text-align:left; font-weight:600; letter-spacing:-0.01em; padding:4px 0 6px 4px; border-bottom:0; border-top:0; border-left:0; border-right:0; outline:0; box-shadow:none;&quot;&gt;
      Vector Only (512-1024)
    &lt;&#x2F;caption&gt;
    &lt;thead style=&quot;border-bottom:1px solid #e5e7eb;&quot;&gt;
      &lt;tr&gt;
        &lt;th style=&quot;text-align:left; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;dataset&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;mrr&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;ndcg&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;hit@5&lt;&#x2F;th&gt;
        
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;avg ms&lt;&#x2F;th&gt;
        
      &lt;&#x2F;tr&gt;
    &lt;&#x2F;thead&gt;
    &lt;tbody&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important; border-bottom:1px solid #f3f4f6 !important; text-align:left;&quot;&gt;
          SQuAD v2.0&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.862
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (+0.003)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.877
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (+0.002)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.92
          
          &lt;span
            style=&quot;font-size:0.85em; color:#dc2626;&quot;&gt;
            (-0.005)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          28
          
          &lt;span
            style=&quot;font-size:0.85em; color:#dc2626;&quot;&gt;
            (+4)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;
        
      &lt;&#x2F;tr&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important;  text-align:left;&quot;&gt;
          Natural Questions&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.692
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (+0.063)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.723
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (+0.061)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.86
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (+0.052)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          73
          
          &lt;span
            style=&quot;font-size:0.85em; color:#dc2626;&quot;&gt;
            (+4)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;
        
      &lt;&#x2F;tr&gt;
      
    &lt;&#x2F;tbody&gt;
  &lt;&#x2F;table&gt;
  
  &lt;aside style=&quot;
        display:block;
        font-size:0.9em;
        color:var(--text-pale-color, #6b7280);
        margin:0.5rem 0 0;
        padding:0.5rem 0;
        position:absolute;
        top:50%;
        transform:translateY(-50%);
        right:100%;
        margin-right:1rem;
        width:26ch;
        max-width:30ch;
        min-width:20ch;
        padding-right:0;
        border-right:0;
        white-space:normal;
        word-break:normal;
        hyphens:auto;
        float:none;
      &quot; class=&quot;evals-note-desktop&quot;&gt;
    &lt;div style=&quot;margin-top:0.15rem;&quot;&gt;&lt;p&gt;&lt;strong&gt;Config:&lt;&#x2F;strong&gt;&lt;br&gt;- Strategy: Vector Only&lt;br&gt;- Chunk Size: 512-1024&lt;br&gt;- Reranker: Off&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
  &lt;&#x2F;aside&gt;
  &lt;aside style=&quot;
        display:none;
        font-size:0.9em;
        color:var(--text-pale-color, #6b7280);
        margin:0 0 0 0;
        padding:0 0 0;
        width:100%;
        max-width:100%;
        min-width:0;
        position:static;
        left:auto;
        transform:none;
        white-space:normal;
        word-break:normal;
        hyphens:auto;
      &quot; class=&quot;evals-note-mobile&quot;&gt;
    &lt;div style=&quot;margin-top:0.15rem;&quot;&gt;&lt;p&gt;&lt;strong&gt;Config:&lt;&#x2F;strong&gt;&lt;br&gt;- Strategy: Vector Only&lt;br&gt;- Chunk Size: 512-1024&lt;br&gt;- Reranker: Off&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
  &lt;&#x2F;aside&gt;
  
&lt;&#x2F;div&gt;

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
&lt;&#x2F;div&gt;

&lt;style&gt;
  @media (max-width: 1024px) {
    .evals-note-desktop {
      display: none !important;
    }

    .evals-note-mobile {
      display: block !important;
    }
  }

  @media (min-width: 1025px) {
    .evals-note-desktop {
      display: block !important;
    }

    .evals-note-mobile {
      display: none !important;
    }
  }
&lt;&#x2F;style&gt;







&lt;div class=&quot;space-y-6&quot;&gt;
  
  
  
  
  
  
  
  
  
  
&lt;div class=&quot;space-y-2&quot; style=&quot;margin: 1.25rem 0; position: relative;&quot;&gt;
  &lt;table
    style=&quot;width:100%; border-collapse:collapse; border-spacing:0; font-size:0.95em; line-height:1.45; border:0 !important; outline:0 !important; box-shadow:none !important;&quot;&gt;
    &lt;caption
      style=&quot;text-align:left; font-weight:600; letter-spacing:-0.01em; padding:4px 0 6px 4px; border-bottom:0; border-top:0; border-left:0; border-right:0; outline:0; box-shadow:none;&quot;&gt;
      Vector Only (1024-2048)
    &lt;&#x2F;caption&gt;
    &lt;thead style=&quot;border-bottom:1px solid #e5e7eb;&quot;&gt;
      &lt;tr&gt;
        &lt;th style=&quot;text-align:left; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;dataset&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;mrr&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;ndcg&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;hit@5&lt;&#x2F;th&gt;
        
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;avg ms&lt;&#x2F;th&gt;
        
      &lt;&#x2F;tr&gt;
    &lt;&#x2F;thead&gt;
    &lt;tbody&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important; border-bottom:1px solid #f3f4f6 !important; text-align:left;&quot;&gt;
          SQuAD v2.0&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.862
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.877
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.92
          
        &lt;&#x2F;td&gt;

        
        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          26
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (-2)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;
        
      &lt;&#x2F;tr&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important;  text-align:left;&quot;&gt;
          Natural Questions&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.794
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (+0.102)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.809
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (+0.086)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.905
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (+0.045)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          45
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (-28)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;
        
      &lt;&#x2F;tr&gt;
      
    &lt;&#x2F;tbody&gt;
  &lt;&#x2F;table&gt;
  
  &lt;aside style=&quot;
        display:block;
        font-size:0.9em;
        color:var(--text-pale-color, #6b7280);
        margin:0.5rem 0 0;
        padding:0.5rem 0;
        position:absolute;
        top:50%;
        transform:translateY(-50%);
        right:100%;
        margin-right:1rem;
        width:26ch;
        max-width:30ch;
        min-width:20ch;
        padding-right:0;
        border-right:0;
        white-space:normal;
        word-break:normal;
        hyphens:auto;
        float:none;
      &quot; class=&quot;evals-note-desktop&quot;&gt;
    &lt;div style=&quot;margin-top:0.15rem;&quot;&gt;&lt;p&gt;&lt;strong&gt;Config:&lt;&#x2F;strong&gt;&lt;br&gt;- Strategy: Vector Only&lt;br&gt;- Chunk Size: 1024-2048&lt;br&gt;- Reranker: Off&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
  &lt;&#x2F;aside&gt;
  &lt;aside style=&quot;
        display:none;
        font-size:0.9em;
        color:var(--text-pale-color, #6b7280);
        margin:0 0 0 0;
        padding:0 0 0;
        width:100%;
        max-width:100%;
        min-width:0;
        position:static;
        left:auto;
        transform:none;
        white-space:normal;
        word-break:normal;
        hyphens:auto;
      &quot; class=&quot;evals-note-mobile&quot;&gt;
    &lt;div style=&quot;margin-top:0.15rem;&quot;&gt;&lt;p&gt;&lt;strong&gt;Config:&lt;&#x2F;strong&gt;&lt;br&gt;- Strategy: Vector Only&lt;br&gt;- Chunk Size: 1024-2048&lt;br&gt;- Reranker: Off&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
  &lt;&#x2F;aside&gt;
  
&lt;&#x2F;div&gt;

  
  
  
  
  
  
  
  
  
  
  
  
  
&lt;&#x2F;div&gt;

&lt;style&gt;
  @media (max-width: 1024px) {
    .evals-note-desktop {
      display: none !important;
    }

    .evals-note-mobile {
      display: block !important;
    }
  }

  @media (min-width: 1025px) {
    .evals-note-desktop {
      display: block !important;
    }

    .evals-note-mobile {
      display: none !important;
    }
  }
&lt;&#x2F;style&gt;







&lt;div class=&quot;space-y-6&quot;&gt;
  
  
  
  
  
  
  
  
  
  
  
  
&lt;div class=&quot;space-y-2&quot; style=&quot;margin: 1.25rem 0; position: relative;&quot;&gt;
  &lt;table
    style=&quot;width:100%; border-collapse:collapse; border-spacing:0; font-size:0.95em; line-height:1.45; border:0 !important; outline:0 !important; box-shadow:none !important;&quot;&gt;
    &lt;caption
      style=&quot;text-align:left; font-weight:600; letter-spacing:-0.01em; padding:4px 0 6px 4px; border-bottom:0; border-top:0; border-left:0; border-right:0; outline:0; box-shadow:none;&quot;&gt;
      Vector Only (1024-2048) + Reranker
    &lt;&#x2F;caption&gt;
    &lt;thead style=&quot;border-bottom:1px solid #e5e7eb;&quot;&gt;
      &lt;tr&gt;
        &lt;th style=&quot;text-align:left; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;dataset&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;mrr&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;ndcg&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;hit@5&lt;&#x2F;th&gt;
        
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;avg ms&lt;&#x2F;th&gt;
        
      &lt;&#x2F;tr&gt;
    &lt;&#x2F;thead&gt;
    &lt;tbody&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important; border-bottom:1px solid #f3f4f6 !important; text-align:left;&quot;&gt;
          SQuAD v2.0&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.888
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (+0.026)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.9
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (+0.023)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.935
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (+0.015)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          394
          
          &lt;span
            style=&quot;font-size:0.85em; color:#dc2626;&quot;&gt;
            (+368)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;
        
      &lt;&#x2F;tr&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important;  text-align:left;&quot;&gt;
          Natural Questions&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.76
          
          &lt;span
            style=&quot;font-size:0.85em; color:#dc2626;&quot;&gt;
            (-0.034)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.782
          
          &lt;span
            style=&quot;font-size:0.85em; color:#dc2626;&quot;&gt;
            (-0.027)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.89
          
          &lt;span
            style=&quot;font-size:0.85em; color:#dc2626;&quot;&gt;
            (-0.015)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          601
          
          &lt;span
            style=&quot;font-size:0.85em; color:#dc2626;&quot;&gt;
            (+556)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;
        
      &lt;&#x2F;tr&gt;
      
    &lt;&#x2F;tbody&gt;
  &lt;&#x2F;table&gt;
  
  &lt;aside style=&quot;
        display:block;
        font-size:0.9em;
        color:var(--text-pale-color, #6b7280);
        margin:0.5rem 0 0;
        padding:0.5rem 0;
        position:absolute;
        top:50%;
        transform:translateY(-50%);
        right:100%;
        margin-right:1rem;
        width:26ch;
        max-width:30ch;
        min-width:20ch;
        padding-right:0;
        border-right:0;
        white-space:normal;
        word-break:normal;
        hyphens:auto;
        float:none;
      &quot; class=&quot;evals-note-desktop&quot;&gt;
    &lt;div style=&quot;margin-top:0.15rem;&quot;&gt;&lt;p&gt;&lt;strong&gt;Config:&lt;&#x2F;strong&gt;&lt;br&gt;- Strategy: Vector Only&lt;br&gt;- Reranker: On&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
  &lt;&#x2F;aside&gt;
  &lt;aside style=&quot;
        display:none;
        font-size:0.9em;
        color:var(--text-pale-color, #6b7280);
        margin:0 0 0 0;
        padding:0 0 0;
        width:100%;
        max-width:100%;
        min-width:0;
        position:static;
        left:auto;
        transform:none;
        white-space:normal;
        word-break:normal;
        hyphens:auto;
      &quot; class=&quot;evals-note-mobile&quot;&gt;
    &lt;div style=&quot;margin-top:0.15rem;&quot;&gt;&lt;p&gt;&lt;strong&gt;Config:&lt;&#x2F;strong&gt;&lt;br&gt;- Strategy: Vector Only&lt;br&gt;- Reranker: On&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
  &lt;&#x2F;aside&gt;
  
&lt;&#x2F;div&gt;

  
  
  
  
  
  
  
  
  
  
  
&lt;&#x2F;div&gt;

&lt;style&gt;
  @media (max-width: 1024px) {
    .evals-note-desktop {
      display: none !important;
    }

    .evals-note-mobile {
      display: block !important;
    }
  }

  @media (min-width: 1025px) {
    .evals-note-desktop {
      display: block !important;
    }

    .evals-note-mobile {
      display: none !important;
    }
  }
&lt;&#x2F;style&gt;
&lt;h2 id=&quot;hybrid-search&quot;&gt;Hybrid search&lt;a class=&quot;zola-anchor&quot; href=&quot;#hybrid-search&quot; aria-label=&quot;Anchor link for: hybrid-search&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;With the above results natural-questions-dev I was pleased with the details for tokenization, chunking &amp;amp; overlap. But I still wanted to explore hybrid retrieval. When I ran the first tests there was no clear improvement what so ever. Even some slightly degraded results. Which led me to reevaluate the choice of NQ-dev and Squad-v2 for my choice. It’s clear both these datasets respond favorably to dense queries. Whilst the user might ingest whatever content they wish to their deployment of Minne. I realized I needed to make my dataset more challenging. Some research and I came onto BEIR, and its datasets. I selected a sample of a series of datasets and wired them into my eval harness.&lt;&#x2F;p&gt;
&lt;p&gt;Results clearly demonstrated my pipeline were having significant issues with the more challenging datasets. Since I want a solution somewhat generalizable I did not want to tune for these sparse-favored datasets too much. I spent ample time running ablations tuning linear fusion parameters. Then I tried RRF and got improved results, with a increased simplicity. Easier to reason about, and fewer knobs.&lt;&#x2F;p&gt;







&lt;div class=&quot;space-y-6&quot;&gt;
  
  
  
  
  
  
  
  
  
  
  
  
  
  
&lt;div class=&quot;space-y-2&quot; style=&quot;margin: 1.25rem 0; position: relative;&quot;&gt;
  &lt;table
    style=&quot;width:100%; border-collapse:collapse; border-spacing:0; font-size:0.95em; line-height:1.45; border:0 !important; outline:0 !important; box-shadow:none !important;&quot;&gt;
    &lt;caption
      style=&quot;text-align:left; font-weight:600; letter-spacing:-0.01em; padding:4px 0 6px 4px; border-bottom:0; border-top:0; border-left:0; border-right:0; outline:0; box-shadow:none;&quot;&gt;
      BEIR Subsets (RRF, no rerank, 1k–2k chunks)
    &lt;&#x2F;caption&gt;
    &lt;thead style=&quot;border-bottom:1px solid #e5e7eb;&quot;&gt;
      &lt;tr&gt;
        &lt;th style=&quot;text-align:left; padding:6px 4px 4px; width:25%; font-weight:400; border:0 !important;&quot;&gt;dataset&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:25%; font-weight:400; border:0 !important;&quot;&gt;mrr&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:25%; font-weight:400; border:0 !important;&quot;&gt;ndcg&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:25%; font-weight:400; border:0 !important;&quot;&gt;hit@5&lt;&#x2F;th&gt;
        
      &lt;&#x2F;tr&gt;
    &lt;&#x2F;thead&gt;
    &lt;tbody&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important; border-bottom:1px solid #f3f4f6 !important; text-align:left;&quot;&gt;
          NQ (BEIR)&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.981
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.983
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.99
          
        &lt;&#x2F;td&gt;

        
      &lt;&#x2F;tr&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important; border-bottom:1px solid #f3f4f6 !important; text-align:left;&quot;&gt;
          FEVER&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.979
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.982
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.99
          
        &lt;&#x2F;td&gt;

        
      &lt;&#x2F;tr&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important; border-bottom:1px solid #f3f4f6 !important; text-align:left;&quot;&gt;
          HotpotQA&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.943
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.948
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.965
          
        &lt;&#x2F;td&gt;

        
      &lt;&#x2F;tr&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important; border-bottom:1px solid #f3f4f6 !important; text-align:left;&quot;&gt;
          BEIR mix&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.839
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.854
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.898
          
        &lt;&#x2F;td&gt;

        
      &lt;&#x2F;tr&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important; border-bottom:1px solid #f3f4f6 !important; text-align:left;&quot;&gt;
          SciFact&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.768
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.793
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.865
          
        &lt;&#x2F;td&gt;

        
      &lt;&#x2F;tr&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important; border-bottom:1px solid #f3f4f6 !important; text-align:left;&quot;&gt;
          FiQA&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.561
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.599
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.71
          
        &lt;&#x2F;td&gt;

        
      &lt;&#x2F;tr&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important; border-bottom:1px solid #f3f4f6 !important; text-align:left;&quot;&gt;
          TREC-COVID&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.414
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.472
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; border-bottom:1px solid #f3f4f6 !important;&quot;&gt;
          0.64
          
        &lt;&#x2F;td&gt;

        
      &lt;&#x2F;tr&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important;  text-align:left;&quot;&gt;
          NFCorpus&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.181
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.2
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.255
          
        &lt;&#x2F;td&gt;

        
      &lt;&#x2F;tr&gt;
      
    &lt;&#x2F;tbody&gt;
  &lt;&#x2F;table&gt;
  
  &lt;aside style=&quot;
        display:block;
        font-size:0.9em;
        color:var(--text-pale-color, #6b7280);
        margin:0.5rem 0 0;
        padding:0.5rem 0;
        position:absolute;
        top:50%;
        transform:translateY(-50%);
        right:100%;
        margin-right:1rem;
        width:26ch;
        max-width:30ch;
        min-width:20ch;
        padding-right:0;
        border-right:0;
        white-space:normal;
        word-break:normal;
        hyphens:auto;
        float:none;
      &quot; class=&quot;evals-note-desktop&quot;&gt;
    &lt;div style=&quot;margin-top:0.15rem;&quot;&gt;&lt;p&gt;Performance across the subsets of my BEIR mix varies a lot. &lt;br&gt; &lt;br&gt;Near perfect recall for datasets favorable to dense queries, but really poor performance on those with technical and precise questions&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
  &lt;&#x2F;aside&gt;
  &lt;aside style=&quot;
        display:none;
        font-size:0.9em;
        color:var(--text-pale-color, #6b7280);
        margin:0 0 0 0;
        padding:0 0 0;
        width:100%;
        max-width:100%;
        min-width:0;
        position:static;
        left:auto;
        transform:none;
        white-space:normal;
        word-break:normal;
        hyphens:auto;
      &quot; class=&quot;evals-note-mobile&quot;&gt;
    &lt;div style=&quot;margin-top:0.15rem;&quot;&gt;&lt;p&gt;Performance across the subsets of my BEIR mix varies a lot. &lt;br&gt; &lt;br&gt;Near perfect recall for datasets favorable to dense queries, but really poor performance on those with technical and precise questions&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
  &lt;&#x2F;aside&gt;
  
&lt;&#x2F;div&gt;

  
  
  
  
  
  
  
  
  
&lt;&#x2F;div&gt;

&lt;style&gt;
  @media (max-width: 1024px) {
    .evals-note-desktop {
      display: none !important;
    }

    .evals-note-mobile {
      display: block !important;
    }
  }

  @media (min-width: 1025px) {
    .evals-note-desktop {
      display: block !important;
    }

    .evals-note-mobile {
      display: none !important;
    }
  }
&lt;&#x2F;style&gt;
&lt;p&gt;I’m not at all particularly impressed by my current Hybrid search, it’s underperforming my expectations. And since I don’t want to tune for what I think might be more of an edge case, it will have to do for now.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;final-comparisons&quot;&gt;Final comparisons&lt;a class=&quot;zola-anchor&quot; href=&quot;#final-comparisons&quot; aria-label=&quot;Anchor link for: final-comparisons&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;







&lt;div class=&quot;space-y-6&quot;&gt;
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
&lt;div class=&quot;space-y-2&quot; style=&quot;margin: 1.25rem 0; position: relative;&quot;&gt;
  &lt;table
    style=&quot;width:100%; border-collapse:collapse; border-spacing:0; font-size:0.95em; line-height:1.45; border:0 !important; outline:0 !important; box-shadow:none !important;&quot;&gt;
    &lt;caption
      style=&quot;text-align:left; font-weight:600; letter-spacing:-0.01em; padding:4px 0 6px 4px; border-bottom:0; border-top:0; border-left:0; border-right:0; outline:0; box-shadow:none;&quot;&gt;
      BEIR mix (initial, complex + rerank)
    &lt;&#x2F;caption&gt;
    &lt;thead style=&quot;border-bottom:1px solid #e5e7eb;&quot;&gt;
      &lt;tr&gt;
        &lt;th style=&quot;text-align:left; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;dataset&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;mrr&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;ndcg&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;hit@5&lt;&#x2F;th&gt;
        
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;avg ms&lt;&#x2F;th&gt;
        
      &lt;&#x2F;tr&gt;
    &lt;&#x2F;thead&gt;
    &lt;tbody&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important;  text-align:left;&quot;&gt;
          BEIR mix&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.779
          
          &lt;span
            style=&quot;font-size:0.85em; color:#dc2626;&quot;&gt;
            (-0.06)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.791
          
          &lt;span
            style=&quot;font-size:0.85em; color:#dc2626;&quot;&gt;
            (-0.063)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.835
          
          &lt;span
            style=&quot;font-size:0.85em; color:#dc2626;&quot;&gt;
            (-0.063)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          1696.6
          
          &lt;span
            style=&quot;font-size:0.85em; color:#dc2626;&quot;&gt;
            (+1406)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;
        
      &lt;&#x2F;tr&gt;
      
    &lt;&#x2F;tbody&gt;
  &lt;&#x2F;table&gt;
  
  &lt;aside style=&quot;
        display:block;
        font-size:0.9em;
        color:var(--text-pale-color, #6b7280);
        margin:0.5rem 0 0;
        padding:0.5rem 0;
        position:absolute;
        top:50%;
        transform:translateY(-50%);
        right:100%;
        margin-right:1rem;
        width:26ch;
        max-width:30ch;
        min-width:20ch;
        padding-right:0;
        border-right:0;
        white-space:normal;
        word-break:normal;
        hyphens:auto;
        float:none;
      &quot; class=&quot;evals-note-desktop&quot;&gt;
    &lt;div style=&quot;margin-top:0.15rem;&quot;&gt;&lt;p&gt;Decent enough results, but speed is not great&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
  &lt;&#x2F;aside&gt;
  &lt;aside style=&quot;
        display:none;
        font-size:0.9em;
        color:var(--text-pale-color, #6b7280);
        margin:0 0 0 0;
        padding:0 0 0;
        width:100%;
        max-width:100%;
        min-width:0;
        position:static;
        left:auto;
        transform:none;
        white-space:normal;
        word-break:normal;
        hyphens:auto;
      &quot; class=&quot;evals-note-mobile&quot;&gt;
    &lt;div style=&quot;margin-top:0.15rem;&quot;&gt;&lt;p&gt;Decent enough results, but speed is not great&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
  &lt;&#x2F;aside&gt;
  
&lt;&#x2F;div&gt;

  
  
  
  
  
  
  
&lt;&#x2F;div&gt;

&lt;style&gt;
  @media (max-width: 1024px) {
    .evals-note-desktop {
      display: none !important;
    }

    .evals-note-mobile {
      display: block !important;
    }
  }

  @media (min-width: 1025px) {
    .evals-note-desktop {
      display: block !important;
    }

    .evals-note-mobile {
      display: none !important;
    }
  }
&lt;&#x2F;style&gt;







&lt;div class=&quot;space-y-6&quot;&gt;
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
&lt;div class=&quot;space-y-2&quot; style=&quot;margin: 1.25rem 0; position: relative;&quot;&gt;
  &lt;table
    style=&quot;width:100%; border-collapse:collapse; border-spacing:0; font-size:0.95em; line-height:1.45; border:0 !important; outline:0 !important; box-shadow:none !important;&quot;&gt;
    &lt;caption
      style=&quot;text-align:left; font-weight:600; letter-spacing:-0.01em; padding:4px 0 6px 4px; border-bottom:0; border-top:0; border-left:0; border-right:0; outline:0; box-shadow:none;&quot;&gt;
      BEIR mix (revised, hybrid no rerank)
    &lt;&#x2F;caption&gt;
    &lt;thead style=&quot;border-bottom:1px solid #e5e7eb;&quot;&gt;
      &lt;tr&gt;
        &lt;th style=&quot;text-align:left; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;dataset&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;mrr&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;ndcg&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;hit@5&lt;&#x2F;th&gt;
        
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;avg ms&lt;&#x2F;th&gt;
        
      &lt;&#x2F;tr&gt;
    &lt;&#x2F;thead&gt;
    &lt;tbody&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important;  text-align:left;&quot;&gt;
          BEIR mix&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.798
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (+0.019)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.813
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (+0.022)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.86
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (+0.025)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          36
          
          &lt;span
            style=&quot;font-size:0.85em; color:#059669;&quot;&gt;
            (-1661)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;
        
      &lt;&#x2F;tr&gt;
      
    &lt;&#x2F;tbody&gt;
  &lt;&#x2F;table&gt;
  
  &lt;aside style=&quot;
        display:block;
        font-size:0.9em;
        color:var(--text-pale-color, #6b7280);
        margin:0.5rem 0 0;
        padding:0.5rem 0;
        position:absolute;
        top:50%;
        transform:translateY(-50%);
        right:100%;
        margin-right:1rem;
        width:26ch;
        max-width:30ch;
        min-width:20ch;
        padding-right:0;
        border-right:0;
        white-space:normal;
        word-break:normal;
        hyphens:auto;
        float:none;
      &quot; class=&quot;evals-note-desktop&quot;&gt;
    &lt;div style=&quot;margin-top:0.15rem;&quot;&gt;&lt;p&gt;Slightly better precision, but way lower latency&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
  &lt;&#x2F;aside&gt;
  &lt;aside style=&quot;
        display:none;
        font-size:0.9em;
        color:var(--text-pale-color, #6b7280);
        margin:0 0 0 0;
        padding:0 0 0;
        width:100%;
        max-width:100%;
        min-width:0;
        position:static;
        left:auto;
        transform:none;
        white-space:normal;
        word-break:normal;
        hyphens:auto;
      &quot; class=&quot;evals-note-mobile&quot;&gt;
    &lt;div style=&quot;margin-top:0.15rem;&quot;&gt;&lt;p&gt;Slightly better precision, but way lower latency&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
  &lt;&#x2F;aside&gt;
  
&lt;&#x2F;div&gt;

  
  
  
  
  
&lt;&#x2F;div&gt;

&lt;style&gt;
  @media (max-width: 1024px) {
    .evals-note-desktop {
      display: none !important;
    }

    .evals-note-mobile {
      display: block !important;
    }
  }

  @media (min-width: 1025px) {
    .evals-note-desktop {
      display: block !important;
    }

    .evals-note-mobile {
      display: none !important;
    }
  }
&lt;&#x2F;style&gt;







&lt;div class=&quot;space-y-6&quot;&gt;
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
&lt;div class=&quot;space-y-2&quot; style=&quot;margin: 1.25rem 0; position: relative;&quot;&gt;
  &lt;table
    style=&quot;width:100%; border-collapse:collapse; border-spacing:0; font-size:0.95em; line-height:1.45; border:0 !important; outline:0 !important; box-shadow:none !important;&quot;&gt;
    &lt;caption
      style=&quot;text-align:left; font-weight:600; letter-spacing:-0.01em; padding:4px 0 6px 4px; border-bottom:0; border-top:0; border-left:0; border-right:0; outline:0; box-shadow:none;&quot;&gt;
      BEIR mix (revised, hybrid AND rerank)
    &lt;&#x2F;caption&gt;
    &lt;thead style=&quot;border-bottom:1px solid #e5e7eb;&quot;&gt;
      &lt;tr&gt;
        &lt;th style=&quot;text-align:left; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;dataset&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;mrr&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;ndcg&lt;&#x2F;th&gt;
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;hit@5&lt;&#x2F;th&gt;
        
        &lt;th style=&quot;text-align:right; padding:6px 4px 4px; width:20%; font-weight:400; border:0 !important;&quot;&gt;avg ms&lt;&#x2F;th&gt;
        
      &lt;&#x2F;tr&gt;
    &lt;&#x2F;thead&gt;
    &lt;tbody&gt;
      
      
      &lt;tr&gt;
        &lt;td
          style=&quot;padding:6px 4px 4px; font-weight:400; border:0 !important;  text-align:left;&quot;&gt;
          BEIR mix&lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.783
          
          &lt;span
            style=&quot;font-size:0.85em; color:#dc2626;&quot;&gt;
            (-0.015)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.802
          
          &lt;span
            style=&quot;font-size:0.85em; color:#dc2626;&quot;&gt;
            (-0.011)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;

        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          0.86
          
        &lt;&#x2F;td&gt;

        
        
        &lt;td
          style=&quot;text-align:right; padding:6px 4px 4px; border:0 !important; &quot;&gt;
          583
          
          &lt;span
            style=&quot;font-size:0.85em; color:#dc2626;&quot;&gt;
            (+547)
          &lt;&#x2F;span&gt;
          
        &lt;&#x2F;td&gt;
        
      &lt;&#x2F;tr&gt;
      
    &lt;&#x2F;tbody&gt;
  &lt;&#x2F;table&gt;
  
  &lt;aside style=&quot;
        display:block;
        font-size:0.9em;
        color:var(--text-pale-color, #6b7280);
        margin:0.5rem 0 0;
        padding:0.5rem 0;
        position:absolute;
        top:50%;
        transform:translateY(-50%);
        right:100%;
        margin-right:1rem;
        width:26ch;
        max-width:30ch;
        min-width:20ch;
        padding-right:0;
        border-right:0;
        white-space:normal;
        word-break:normal;
        hyphens:auto;
        float:none;
      &quot; class=&quot;evals-note-desktop&quot;&gt;
    &lt;div style=&quot;margin-top:0.15rem;&quot;&gt;&lt;p&gt;With reranking toggled on, we see a clear increase in latency and lower results&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
  &lt;&#x2F;aside&gt;
  &lt;aside style=&quot;
        display:none;
        font-size:0.9em;
        color:var(--text-pale-color, #6b7280);
        margin:0 0 0 0;
        padding:0 0 0;
        width:100%;
        max-width:100%;
        min-width:0;
        position:static;
        left:auto;
        transform:none;
        white-space:normal;
        word-break:normal;
        hyphens:auto;
      &quot; class=&quot;evals-note-mobile&quot;&gt;
    &lt;div style=&quot;margin-top:0.15rem;&quot;&gt;&lt;p&gt;With reranking toggled on, we see a clear increase in latency and lower results&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
  &lt;&#x2F;aside&gt;
  
&lt;&#x2F;div&gt;

  
  
  
&lt;&#x2F;div&gt;

&lt;style&gt;
  @media (max-width: 1024px) {
    .evals-note-desktop {
      display: none !important;
    }

    .evals-note-mobile {
      display: block !important;
    }
  }

  @media (min-width: 1025px) {
    .evals-note-desktop {
      display: block !important;
    }

    .evals-note-mobile {
      display: none !important;
    }
  }
&lt;&#x2F;style&gt;
&lt;p&gt;Clear improvements in latency compared to the initial implementation. Even with reranking off for the initial strategy, there would be over 1s latency for each query.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fin&quot;&gt;Fin&lt;a class=&quot;zola-anchor&quot; href=&quot;#fin&quot; aria-label=&quot;Anchor link for: fin&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;It’s been a long adventure, spanning several weeks. But it’s been mostly fun. I’ve been checked on my assumptions more than once, which I think is very healthy. The suggestion that I read more than once when researching this project was “Create your own dataset”. I get that now. Having my own dataset early on would have spared me significant time.&lt;&#x2F;p&gt;
&lt;p&gt;So to summarize. I went from this retrieval pipeline operating on chunks as well as a synthetic data layer.&lt;&#x2F;p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
  flowchart LR
Q[Query] --&amp;gt; V[Vector Search]
Q --&amp;gt; L[Lexical Search]
V --&amp;gt; M[Fuse w&amp;#x2F; Linear Weights]
L --&amp;gt; M
M --&amp;gt; D[Graph Expansion]
D --&amp;gt; E[Attach Chunks]
E --&amp;gt; R[Reranker]
R --&amp;gt; G[Assemble]
&lt;&#x2F;pre&gt;
&lt;p&gt;To this:&lt;&#x2F;p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
  flowchart LR
Q[Query] --&amp;gt; V[Vector Search]
Q --&amp;gt; L[Lexical Search]
V --&amp;gt; R[RRF Fusion]
L --&amp;gt; R
R --&amp;gt; G[Result]
&lt;&#x2F;pre&gt;
&lt;p&gt;The new pipeline is easier to easier to reason about, easier to debug, having equal or better performance and significantly faster. I’m absolutely up for introducing more complexity again in attempts to boost results on sparse-favored datasets! Query rewriting, merging several queries, etc. But for now, and for Minne 1.0 I’m pleased.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;related-posts&quot;&gt;Related posts&lt;a class=&quot;zola-anchor&quot; href=&quot;#related-posts&quot; aria-label=&quot;Anchor link for: related-posts&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;bundling-onnxruntime-rust-nix&#x2F;&quot;&gt;Bundling ONNX Runtime in Rust with Nix, Docker and GitHub Actions&lt;&#x2F;a&gt; - The dependency management challenges I faced when adding the reranker that this evaluation eventually removed.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;psychological-research-using-llms&#x2F;&quot;&gt;Using LLMs for psychological research&lt;&#x2F;a&gt; - Another take on applying machine learning to real-world problems, this time analyzing student well-being.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
	</entry>
	<entry xml:lang="en">
		<title>Bundling ONNX Runtime in Rust with Nix, Docker and GitHub Actions</title>
		<published>2025-11-02T00:00:00+00:00</published>
		<updated>2025-11-02T00:00:00+00:00</updated>
		<link href="https://blog.stark.pub/posts/bundling-onnxruntime-rust-nix/" type="text/html"/>
		<id>https://blog.stark.pub/posts/bundling-onnxruntime-rust-nix/</id>
		<content type="html">&lt;details open&gt;
  &lt;summary&gt;&lt;span&gt;TLDR&lt;&#x2F;span&gt;&lt;&#x2F;summary&gt;
  &lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Context:&lt;&#x2F;strong&gt; Adding reranking to &lt;a href=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;eval-retrieval-refactor&#x2F;&quot;&gt;Minne&lt;&#x2F;a&gt; pulled in &lt;code&gt;fastembed&lt;&#x2F;code&gt; + &lt;code&gt;ort&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Problem:&lt;&#x2F;strong&gt; Auto-downloading shared libraries works locally but breaks in &lt;strong&gt;sandboxed&lt;&#x2F;strong&gt; environments (Nix) and constrained CI (Docker&#x2F;GitHub Actions).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Solution:&lt;&#x2F;strong&gt; Explicitly bundle &lt;code&gt;libonnxruntime&lt;&#x2F;code&gt; as a build input and set &lt;code&gt;ORT_DYLIB_PATH&lt;&#x2F;code&gt; to bypass &lt;code&gt;fastembed-rs&lt;&#x2F;code&gt; auto-download mechanisms.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Outcome:&lt;&#x2F;strong&gt; A unified deployment strategy across Nix, Docker, and binaries.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;&#x2F;details&gt;
&lt;h2 id=&quot;context-the-stack&quot;&gt;Context: The Stack&lt;a class=&quot;zola-anchor&quot; href=&quot;#context-the-stack&quot; aria-label=&quot;Anchor link for: context-the-stack&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Before diving in, here are the components involved in this conflict:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Anush008&#x2F;fastembed-rs&quot;&gt;fastembed-rs&lt;&#x2F;a&gt;:&lt;&#x2F;strong&gt; A Rust crate for embedding generation that wraps the ONNX Runtime. By default, it attempts to download the shared library at runtime.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;ort.pyke.io&#x2F;&quot;&gt;ONNX Runtime (ORT)&lt;&#x2F;a&gt;:&lt;&#x2F;strong&gt; The cross-platform inference engine from Microsoft. It’s a heavy native dependency (&lt;code&gt;.so&lt;&#x2F;code&gt;, &lt;code&gt;.dll&lt;&#x2F;code&gt;, &lt;code&gt;.dylib&lt;&#x2F;code&gt;) that needs to match the host architecture.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;nixos.org&#x2F;manual&#x2F;nix&#x2F;stable&#x2F;command-ref&#x2F;conf-file.html#conf-sandbox&quot;&gt;Nix Sandboxing&lt;&#x2F;a&gt;:&lt;&#x2F;strong&gt; Nix builds run in an isolated environment with no network access, preventing the “magic download” strategy of many modern crates.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;why-i-m-even-writing-this&quot;&gt;Why I’m even writing this&lt;a class=&quot;zola-anchor&quot; href=&quot;#why-i-m-even-writing-this&quot; aria-label=&quot;Anchor link for: why-i-m-even-writing-this&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;I like shipping stuff that can be run in several ways: Nix flake, build from source, Docker, and release binaries. That’s what I’ve been doing for Minne. Up until v0.2.6 this was fine.&lt;&#x2F;p&gt;
&lt;p&gt;Then I enabled reranking by pulling in &lt;code&gt;fastembed-rs&lt;&#x2F;code&gt;, which in turn uses &lt;code&gt;ort&lt;&#x2F;code&gt;. Locally it worked great: the crate downloads what it needs. But my deployment methods failed: Nix (sandbox, no network), Docker and through GitHub Actions (Windows in particular) things broke.&lt;&#x2F;p&gt;
&lt;p&gt;So this post is me writing down the approach I ended up with. It’s centered on my case, but the idea in general is to stop relying on crate download magic, and supply dependencies myself.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-solution-architecture-explicit-dependency-injection&quot;&gt;The Solution Architecture: Explicit Dependency Injection&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-solution-architecture-explicit-dependency-injection&quot; aria-label=&quot;Anchor link for: the-solution-architecture-explicit-dependency-injection&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Before diving into Nix, Docker and CI, here’s the model I ended up with:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Turn off auto-download in the Rust crate via features.&lt;&#x2F;strong&gt; no build-time network calls.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Put ONNX Runtime somewhere predictable&lt;&#x2F;strong&gt; for each environment.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Teach the application to look for it in a predictable way&lt;&#x2F;strong&gt; (&lt;code&gt;ORT_DYLIB_PATH&lt;&#x2F;code&gt; + a small fallback).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Repeat the same&lt;&#x2F;strong&gt; in Nix, Docker, GitHub Actions, and release archives.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;That’s it. Everything below is just doing this four times.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;rust-configuration-disabling-fastembed-rs-auto-download&quot;&gt;Rust Configuration: Disabling &lt;code&gt;fastembed-rs&lt;&#x2F;code&gt; Auto-Download&lt;a class=&quot;zola-anchor&quot; href=&quot;#rust-configuration-disabling-fastembed-rs-auto-download&quot; aria-label=&quot;Anchor link for: rust-configuration-disabling-fastembed-rs-auto-download&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;This was the dependency line I used:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;toml&quot; class=&quot;language-toml &quot;&gt;&lt;code class=&quot;language-toml&quot; data-lang=&quot;toml&quot;&gt;fastembed = { version = &amp;quot;5.2.0&amp;quot;, default-features = false, features = [&amp;quot;hf-hub-native-tls&amp;quot;, &amp;quot;ort-load-dynamic&amp;quot;] }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The important parts:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;default-features = false&lt;&#x2F;code&gt; so it doesn’t pull in download stuff I don’t control.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ort-load-dynamic&lt;&#x2F;code&gt; so it uses &lt;code&gt;dlopen()&lt;&#x2F;code&gt; and respects &lt;code&gt;ORT_DYLIB_PATH&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Then I made the app look for the lib like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::{env, path::PathBuf};

fn ort_lib_dir() -&amp;gt; PathBuf {
    if let Ok(p) = env::var(&amp;quot;ORT_DYLIB_PATH&amp;quot;) {
        return PathBuf::from(p);
    }

    &amp;#x2F;&amp;#x2F; fallback: .&amp;#x2F;lib next to binary
    let exe = env::current_exe().unwrap_or_else(|_| PathBuf::from(&amp;quot;.&amp;quot;));
    exe.parent()
        .map(|p| p.join(&amp;quot;lib&amp;quot;))
        .unwrap_or_else(|| PathBuf::from(&amp;quot;.&amp;#x2F;lib&amp;quot;))
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;So every deployment method only has to do one of these:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;set &lt;code&gt;ORT_DYLIB_PATH&lt;&#x2F;code&gt; to the actual file&lt;&#x2F;li&gt;
&lt;li&gt;or package &lt;code&gt;libonnxruntime.*&lt;&#x2F;code&gt; in &lt;code&gt;.&#x2F;lib&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I think this is the interesting reusable bit.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;nixos-sandbox-linking-native-libraries&quot;&gt;NixOS Sandbox: Linking Native Libraries&lt;a class=&quot;zola-anchor&quot; href=&quot;#nixos-sandbox-linking-native-libraries&quot; aria-label=&quot;Anchor link for: nixos-sandbox-linking-native-libraries&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Nix builds in a sandbox and only sees declared inputs. Quoting the Nix manual: when &lt;code&gt;sandbox = true&lt;&#x2F;code&gt;, builds only see the Nix store, the build dir and what you bind in. Network is isolated for normal derivations.&lt;&#x2F;p&gt;
&lt;p&gt;So a crate that wants to download ONNX Runtime during build or run is not going to work inside that sandbox. That forced me to make ONNX Runtime a real input.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;my-nix-flake-for-minne&quot;&gt;My Nix flake for Minne&lt;a class=&quot;zola-anchor&quot; href=&quot;#my-nix-flake-for-minne&quot; aria-label=&quot;Anchor link for: my-nix-flake-for-minne&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;This is the shape I used (shortened to the relevant bit):&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nix&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot;&gt;{
  description = &amp;quot;Minne application flake&amp;quot;;

  inputs = {
    nixpkgs.url = &amp;quot;github:NixOS&amp;#x2F;nixpkgs&amp;#x2F;nixos-unstable&amp;quot;;
    flake-utils.url = &amp;quot;github:numtide&amp;#x2F;flake-utils&amp;quot;;
    crane.url = &amp;quot;github:ipetkov&amp;#x2F;crane&amp;quot;;
  };

  outputs = { self, nixpkgs, flake-utils, crane }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
        lib = pkgs.lib;
        craneLib = crane.mkLib pkgs;
        libExt = if pkgs.stdenv.isDarwin then &amp;quot;dylib&amp;quot; else &amp;quot;so&amp;quot;;
        minne-pkg = craneLib.buildPackage {
          src = lib.cleanSourceWith {
            src = .&amp;#x2F;.;
            filter = let
              extraPaths = [
                (toString .&amp;#x2F;common&amp;#x2F;migrations)
                (toString .&amp;#x2F;common&amp;#x2F;schemas)
                (toString .&amp;#x2F;html-router&amp;#x2F;templates)
                (toString .&amp;#x2F;html-router&amp;#x2F;assets)
              ];
            in
              path: type:
                craneLib.filterCargoSources path type
                || lib.any (x: lib.hasPrefix x (toString path)) extraPaths;
          };

          pname = &amp;quot;minne&amp;quot;;
          version = &amp;quot;0.2.6&amp;quot;;
          doCheck = false;

          nativeBuildInputs = [ pkgs.pkg-config pkgs.rustfmt pkgs.makeWrapper ];
          buildInputs = [ pkgs.openssl pkgs.chromium pkgs.onnxruntime ];

          postInstall = &amp;#x27;&amp;#x27;
            wrapProgram $out&amp;#x2F;bin&amp;#x2F;main \
              --set CHROME ${pkgs.chromium}&amp;#x2F;bin&amp;#x2F;chromium \
              --set ORT_DYLIB_PATH ${pkgs.onnxruntime}&amp;#x2F;lib&amp;#x2F;libonnxruntime.${libExt}
            for b in worker server; do
              if [ -x &amp;quot;$out&amp;#x2F;bin&amp;#x2F;$b&amp;quot; ]; then
                wrapProgram $out&amp;#x2F;bin&amp;#x2F;$b \
                  --set CHROME ${pkgs.chromium}&amp;#x2F;bin&amp;#x2F;chromium \
                  --set ORT_DYLIB_PATH ${pkgs.onnxruntime}&amp;#x2F;lib&amp;#x2F;libonnxruntime.${libExt}
              fi
            done
          &amp;#x27;&amp;#x27;;
        };
      in {
        packages = {
          minne-pkg = minne-pkg;
          default = minne-pkg;
        };
      });
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Key points:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;I pulled in &lt;code&gt;pkgs.onnxruntime&lt;&#x2F;code&gt; as a normal build input.&lt;&#x2F;li&gt;
&lt;li&gt;I wrapped the binaries to &lt;strong&gt;set&lt;&#x2F;strong&gt; &lt;code&gt;ORT_DYLIB_PATH&lt;&#x2F;code&gt; to that exact file.&lt;&#x2F;li&gt;
&lt;li&gt;Chromium is also wrapped.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So the Nix solution is: “declare ONNX Runtime as an input, and hard-set the env var when installing.”&lt;&#x2F;p&gt;
&lt;h2 id=&quot;docker-build-baking-in-the-onnx-runtime&quot;&gt;Docker Build: Baking in the ONNX Runtime&lt;a class=&quot;zola-anchor&quot; href=&quot;#docker-build-baking-in-the-onnx-runtime&quot; aria-label=&quot;Anchor link for: docker-build-baking-in-the-onnx-runtime&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Docker is looser than Nix, so it could have worked with download on startup, but I don’t want another feature set and I prefer to align all deployment methods to the same pattern. So I downloaded the exact ONNX Runtime release during the build and set the env var in the image.&lt;&#x2F;p&gt;
&lt;p&gt;This is the Dockerfile I used:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;dockerfile&quot; class=&quot;language-dockerfile &quot;&gt;&lt;code class=&quot;language-dockerfile&quot; data-lang=&quot;dockerfile&quot;&gt;# === Builder ===
FROM rust:1.86-bookworm AS builder
WORKDIR &amp;#x2F;usr&amp;#x2F;src&amp;#x2F;minne
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends \
    pkg-config clang cmake git &amp;amp;&amp;amp; rm -rf &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;apt&amp;#x2F;lists&amp;#x2F;*

# Cache deps
COPY Cargo.toml Cargo.lock .&amp;#x2F;
RUN mkdir -p api-router common composite-retrieval html-router ingestion-pipeline json-stream-parser main worker
COPY api-router&amp;#x2F;Cargo.toml .&amp;#x2F;api-router&amp;#x2F;
COPY common&amp;#x2F;Cargo.toml .&amp;#x2F;common&amp;#x2F;
COPY composite-retrieval&amp;#x2F;Cargo.toml .&amp;#x2F;composite-retrieval&amp;#x2F;
COPY html-router&amp;#x2F;Cargo.toml .&amp;#x2F;html-router&amp;#x2F;
COPY ingestion-pipeline&amp;#x2F;Cargo.toml .&amp;#x2F;ingestion-pipeline&amp;#x2F;
COPY json-stream-parser&amp;#x2F;Cargo.toml .&amp;#x2F;json-stream-parser&amp;#x2F;
COPY main&amp;#x2F;Cargo.toml .&amp;#x2F;main&amp;#x2F;
RUN cargo build --release --bin main --features ingestion-pipeline&amp;#x2F;docker || true

# Build
COPY . .
RUN cargo build --release --bin main --features ingestion-pipeline&amp;#x2F;docker

# === Runtime ===
FROM debian:bookworm-slim

RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends \
    chromium libnss3 libasound2 libgbm1 libxshmfence1 \
    ca-certificates fonts-dejavu fonts-noto-color-emoji \
    libgomp1 libstdc++6 curl \
  &amp;amp;&amp;amp; rm -rf &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;apt&amp;#x2F;lists&amp;#x2F;*

ARG ORT_VERSION=1.22.0
RUN mkdir -p &amp;#x2F;opt&amp;#x2F;onnxruntime &amp;amp;&amp;amp; \
    curl -fsSL -o &amp;#x2F;tmp&amp;#x2F;ort.tgz \
      &amp;quot;https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;microsoft&amp;#x2F;onnxruntime&amp;#x2F;releases&amp;#x2F;download&amp;#x2F;v${ORT_VERSION}&amp;#x2F;onnxruntime-linux-x64-${ORT_VERSION}.tgz&amp;quot; &amp;amp;&amp;amp; \
    tar -xzf &amp;#x2F;tmp&amp;#x2F;ort.tgz -C &amp;#x2F;opt&amp;#x2F;onnxruntime --strip-components=1 &amp;amp;&amp;amp; rm &amp;#x2F;tmp&amp;#x2F;ort.tgz

ENV CHROME_BIN=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;chromium \
    SSL_CERT_FILE=&amp;#x2F;etc&amp;#x2F;ssl&amp;#x2F;certs&amp;#x2F;ca-certificates.crt \
    ORT_DYLIB_PATH=&amp;#x2F;opt&amp;#x2F;onnxruntime&amp;#x2F;lib&amp;#x2F;libonnxruntime.so

RUN useradd -m appuser
USER appuser
WORKDIR &amp;#x2F;home&amp;#x2F;appuser

COPY --from=builder &amp;#x2F;usr&amp;#x2F;src&amp;#x2F;minne&amp;#x2F;target&amp;#x2F;release&amp;#x2F;main &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin&amp;#x2F;main
EXPOSE 3000
CMD [&amp;quot;main&amp;quot;]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;So here the pattern is: &lt;strong&gt;download once at build time, put it under &lt;code&gt;&#x2F;opt&#x2F;onnxruntime&lt;&#x2F;code&gt;, set &lt;code&gt;ORT_DYLIB_PATH&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;. No runtime download.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;github-actions-cross-platform-bundling&quot;&gt;GitHub Actions: Cross-Platform Bundling&lt;a class=&quot;zola-anchor&quot; href=&quot;#github-actions-cross-platform-bundling&quot; aria-label=&quot;Anchor link for: github-actions-cross-platform-bundling&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;I added a block in the GH workflow that creates a &lt;code&gt;lib&#x2F;&lt;&#x2F;code&gt; dir and downloads the matching ONNX Runtime artifact per OS, then lets &lt;code&gt;cargo-dist&lt;&#x2F;code&gt; package it, and also set the dist toml file to include the lib folder when building.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;    - name: Prepare lib dir (Unix)
      if: runner.os != &amp;#x27;Windows&amp;#x27;
      run: |
        mkdir -p lib
        rm -f lib&amp;#x2F;*

    - name: Prepare lib dir (Windows)
      if: runner.os == &amp;#x27;Windows&amp;#x27;
      shell: pwsh
      run: |
        New-Item -ItemType Directory -Force -Path lib | Out-Null
        Get-ChildItem -Path lib -Force | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue

    - name: Fetch ONNX Runtime (Linux)
      if: runner.os == &amp;#x27;Linux&amp;#x27;
      env:
        ORT_VER: 1.22.0
      run: |
        set -euo pipefail
        ARCH=&amp;quot;$(uname -m)&amp;quot;
        case &amp;quot;$ARCH&amp;quot; in
          x86_64)  URL=&amp;quot;https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;microsoft&amp;#x2F;onnxruntime&amp;#x2F;releases&amp;#x2F;download&amp;#x2F;v${ORT_VER}&amp;#x2F;onnxruntime-linux-x64-${ORT_VER}.tgz&amp;quot; ;;
          aarch64) URL=&amp;quot;https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;microsoft&amp;#x2F;onnxruntime&amp;#x2F;releases&amp;#x2F;download&amp;#x2F;v${ORT_VER}&amp;#x2F;onnxruntime-linux-aarch64-${ORT_VER}.tgz&amp;quot; ;;
          *) echo &amp;quot;Unsupported arch $ARCH&amp;quot;; exit 1 ;;
        esac
        curl -fsSL -o ort.tgz &amp;quot;$URL&amp;quot;
        tar -xzf ort.tgz
        cp -v onnxruntime-*&amp;#x2F;lib&amp;#x2F;libonnxruntime.so* lib&amp;#x2F;
        [ -f lib&amp;#x2F;libonnxruntime.so ] || cp -v lib&amp;#x2F;libonnxruntime.so.* lib&amp;#x2F;libonnxruntime.so
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and similarly for macOS and Windows (&lt;code&gt;onnxruntime.dll&lt;&#x2F;code&gt;).&lt;&#x2F;p&gt;
&lt;p&gt;That way the release artifact can contain:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;the binary&lt;&#x2F;li&gt;
&lt;li&gt;the &lt;code&gt;lib&#x2F;&lt;&#x2F;code&gt; dir with the right ONNX Runtime file&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;and since the Rust side already looks at &lt;code&gt;ORT_DYLIB_PATH&lt;&#x2F;code&gt; and &lt;code&gt;.&#x2F;lib&lt;&#x2F;code&gt;, it just works.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;closing&quot;&gt;Closing&lt;a class=&quot;zola-anchor&quot; href=&quot;#closing&quot; aria-label=&quot;Anchor link for: closing&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;So the final shape is:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;the Rust app is &lt;em&gt;dumb&lt;&#x2F;em&gt; and only cares about &lt;code&gt;ORT_DYLIB_PATH&lt;&#x2F;code&gt; or &lt;code&gt;.&#x2F;lib&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Nix, Docker, CI, and release all supply ONNX Runtime in their own way&lt;&#x2F;li&gt;
&lt;li&gt;I don’t rely on auto-download in anymore, and operate on a specific pinned version of onnxruntime&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;related-posts&quot;&gt;Related posts&lt;a class=&quot;zola-anchor&quot; href=&quot;#related-posts&quot; aria-label=&quot;Anchor link for: related-posts&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;eval-retrieval-refactor&#x2F;&quot;&gt;Refactoring a retrieval pipeline through evaluation&lt;&#x2F;a&gt; - The reranking feature that prompted this ONNX Runtime work, and how I evaluated its (lack of) impact on retrieval quality.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;nixos-abstractions&#x2F;&quot;&gt;NixOS, an operating system for me?&lt;&#x2F;a&gt; - More context on why I use NixOS and what the Nix sandbox means for development.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
	</entry>
	<entry xml:lang="en">
		<title>Summer project 2025: Migrating to Clan and a Dendritic Architecture</title>
		<published>2025-08-30T00:00:00+00:00</published>
		<updated>2025-08-30T00:00:00+00:00</updated>
		<link href="https://blog.stark.pub/posts/clan-migration/" type="text/html"/>
		<id>https://blog.stark.pub/posts/clan-migration/</id>
		<content type="html">&lt;details open&gt;
  &lt;summary&gt;&lt;span&gt;TLDR&lt;&#x2F;span&gt;&lt;&#x2F;summary&gt;
  &lt;ul&gt;
&lt;li&gt;Five machine NixOS homelab consisting of workstations, server and router migration to &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;clan.lol&quot;&gt;Clan&lt;&#x2F;a&gt; and dendritic flake-parts&lt;&#x2F;li&gt;
&lt;li&gt;Public and private bits split&lt;&#x2F;li&gt;
&lt;li&gt;Abstractions to create maintainable modules (Router module)&lt;&#x2F;li&gt;
&lt;li&gt;Clan vars wrapper developed to add features like discovery and ACL permissions&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;perstarkse&#x2F;infra&quot;&gt;Repo&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;&#x2F;details&gt;
&lt;h2 id=&quot;summer-of-2025&quot;&gt;Summer of 2025&lt;a class=&quot;zola-anchor&quot; href=&quot;#summer-of-2025&quot; aria-label=&quot;Anchor link for: summer-of-2025&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Summers are when I dive into one or two new projects. I look forward to it.&lt;&#x2F;p&gt;
&lt;details &gt;
  &lt;summary&gt;&lt;span&gt;In past years, summer projects included:&lt;&#x2F;span&gt;&lt;&#x2F;summary&gt;
  &lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;PCI_passthrough_via_OVMF&quot;&gt;VFIO&lt;&#x2F;a&gt; &amp;amp; &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;looking-glass.io&#x2F;&quot;&gt;Looking-Glass&lt;&#x2F;a&gt; for near native performance in Windows VMs.&lt;&#x2F;li&gt;
&lt;li&gt;Rust, reading &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;www.zero2prod.com&quot;&gt;Zero to Production in Rust&lt;&#x2F;a&gt;, the &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;stable&#x2F;book&#x2F;&quot;&gt;Rust Book&lt;&#x2F;a&gt;, doing &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rustlings&quot;&gt;Rustlings&lt;&#x2F;a&gt;, and developing &lt;a href=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;eval-retrieval-refactor&#x2F;&quot;&gt;Minne&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;NixOS, deploying to my first workstations.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;&#x2F;details&gt;
&lt;p&gt;This year I picked two projects to balance my time with family: build a 40 m² deck and tidy my homelab configuration. The former drew far more praise—unsurprisingly.&lt;&#x2F;p&gt;
&lt;figure&gt;
    &lt;img src=&quot;.&amp;#x2F;finished_deck.webp&quot; alt=&quot;Newly constructed 40 square meter wooden deck reflecting a successful summer construction project&quot;&gt;
    
    &lt;figcaption&gt;Finished deck&lt;&#x2F;figcaption&gt;
    
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;pain&quot;&gt;Pain&lt;a class=&quot;zola-anchor&quot; href=&quot;#pain&quot; aria-label=&quot;Anchor link for: pain&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Over the years my homelab has grown in scope and machines. This spring I replaced my old router with a new N150 running NixOS, which I detailed in the &lt;a href=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;n150-nixos-router&#x2F;&quot;&gt;blog&lt;&#x2F;a&gt;. My server now runs my save-for-later PKM, &lt;a href=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;eval-retrieval-refactor&#x2F;&quot;&gt;Minne&lt;&#x2F;a&gt;, and its demo deployment.&lt;&#x2F;p&gt;
&lt;p&gt;I initially kept my configurations public, but as I added services (email, etc.) they leaked too many personal details. Another goal with the refactor was to split the private bits to a private repo while keeping the main configuration shareable.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;orchestration-deployment-options&quot;&gt;Orchestration&#x2F;Deployment options&lt;a class=&quot;zola-anchor&quot; href=&quot;#orchestration-deployment-options&quot; aria-label=&quot;Anchor link for: orchestration-deployment-options&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;I’ve used remote builds on my workstation for low-resource devices; it’s been solid. While revising the setup I evaluated Colmena, deploy-rs, and Clan (a longer list exists &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;nix-community.github.io&#x2F;awesome-nix&#x2F;#deployment-tools&quot;&gt;here&lt;&#x2F;a&gt;). &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;clan.lol&#x2F;&quot;&gt;Clan&lt;&#x2F;a&gt; stood out: active development, clear ambition, great people—and its secrets module (“vars”), an overlay on sops-nix, made creating and sharing secrets between machines straightforward.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dendritic-configurations&quot;&gt;Dendritic configurations&lt;a class=&quot;zola-anchor&quot; href=&quot;#dendritic-configurations&quot; aria-label=&quot;Anchor link for: dendritic-configurations&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;A quick detour into architecture. Over time my configuration became more chaotic, and it became harder to localize changes. I lacked a consistent pattern.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Mightyiam’s&lt;&#x2F;em&gt; &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mightyiam&#x2F;dendritic&quot;&gt;repo&lt;&#x2F;a&gt; is where I first came across the Dendritic Pattern.&lt;&#x2F;p&gt;
&lt;blockquote class=&quot;quote&quot;&gt;
  
  &lt;div class=&quot;icon&quot; style=&quot;display: none;&quot;&gt;&lt;svg fill=&quot;currentColor&quot; xmlns=&quot;http:&#x2F;&#x2F;www.w3.org&#x2F;2000&#x2F;svg&quot;  width=&quot;10&quot; height=&quot;10&quot; viewBox=&quot;796 698 200 200&quot;&gt;
&lt;g&gt;
	&lt;path d=&quot;M885.208,749.739v-40.948C836.019,708.791,796,748.81,796,798v89.209h89.208V798h-48.26
		C836.948,771.39,858.598,749.739,885.208,749.739z&quot;&#x2F;&gt;
	&lt;path d=&quot;M996,749.739v-40.948c-49.19,0-89.209,40.019-89.209,89.209v89.209H996V798h-48.26
		C947.74,771.39,969.39,749.739,996,749.739z&quot;&#x2F;&gt;
&lt;&#x2F;g&gt;
&lt;&#x2F;svg&gt;
&lt;&#x2F;div&gt;
  &lt;div class=&quot;content&quot;&gt;&lt;p&gt;The Dendritic Pattern:&lt;&#x2F;p&gt;
&lt;p&gt;A Nix flake-parts usage pattern in which every Nix file is a flake-parts module&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
  
  &lt;div class=&quot;from&quot;&gt;
    &lt;p&gt;— mightyiam&lt;&#x2F;p&gt;

  &lt;&#x2F;div&gt;
  
&lt;&#x2F;blockquote&gt;
&lt;p&gt;You can still make a mess, but it helps—and it’s fun.&lt;&#x2F;p&gt;
&lt;p&gt;My current setup is what I think of as a hybrid dendritic approach. All features live under &lt;code&gt;modules&#x2F;&lt;&#x2F;code&gt; and are written as flake‑parts modules, each exposing &lt;code&gt;nixosModules.&amp;lt;name&amp;gt;&lt;&#x2F;code&gt; or &lt;code&gt;homeModules.&amp;lt;name&amp;gt;&lt;&#x2F;code&gt;. Machines then compose these features in their &lt;code&gt;configuration.nix&lt;&#x2F;code&gt; by importing the relevant modules. I might move to a fully dendritic design, but for now I’m pleased.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;goals&quot;&gt;Goals&lt;a class=&quot;zola-anchor&quot; href=&quot;#goals&quot; aria-label=&quot;Anchor link for: goals&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;At this point the plan is beginning to shape up. We’ve got a framework to implement in a certain manner, and target machines:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Io&lt;&#x2F;em&gt;: Router, reverse proxy, etc.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;Makemake&lt;&#x2F;em&gt;: Server, hosting internal and external services, media, backups.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;Charon&lt;&#x2F;em&gt;: Workstation.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The goals for this project are:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Refactor existing configuration for increased maintainability.&lt;&#x2F;li&gt;
&lt;li&gt;Use Clan where appropriate.&lt;&#x2F;li&gt;
&lt;li&gt;Enable sharing of main repo, separating sensitive bits out of the main config.&lt;&#x2F;li&gt;
&lt;li&gt;Increase security across my machines.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;migration-strategy&quot;&gt;Migration strategy&lt;a class=&quot;zola-anchor&quot; href=&quot;#migration-strategy&quot; aria-label=&quot;Anchor link for: migration-strategy&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The plan was to create a fresh configuration around Clan and the new architecture rather than refactoring in place, for three reasons:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;It’s recommended in the &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;docs.clan.lol&#x2F;main&#x2F;guides&#x2F;migrations&#x2F;convert-existing-NixOS-configuration&#x2F;&quot;&gt;Clan migration guide&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;I wanted a public repository going forward, without rewriting history.&lt;&#x2F;li&gt;
&lt;li&gt;It offered a cleaner path.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;I started by setting up a new VM following the getting started guide. The flake template helped. Once I had a working deployment—with host SSH keys and user&#x2F;root passwords as secrets—I migrated the old configuration into the new pattern, added options to form meaningful abstractions, and placed them under &lt;code&gt;config.my.*&lt;&#x2F;code&gt;. It was fun taking some time and creating abstractions over different areas. One module, or rather set of modules, I found particularly meaningful was the ones related to router functionality. Network stuff isn’t my specialty, and maintaining it was a bit fragile. Creating an abstraction layer on top makes maintenance easier, making changes like adding static ip addresses, forwarding ports and reverse proxying is now simplified.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nix&quot; data-name=&quot;router&#x2F;configuration.nix&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot; data-name=&quot;router&#x2F;configuration.nix&quot;&gt;{
  my.router = {
    enable = true;
    hostname = &amp;quot;io&amp;quot;;
    lan = { subnet = &amp;quot;10.0.0&amp;quot;; interfaces = [ &amp;quot;enp2s0&amp;quot; &amp;quot;enp3s0&amp;quot; &amp;quot;enp4s0&amp;quot; ]; };

    machines = [
      { name = &amp;quot;makemake&amp;quot;; ip = &amp;quot;10&amp;quot;; mac = &amp;quot;00:d0:b4:02:bb:3c&amp;quot;;
        portForwards = [ { port = 25; protocol = &amp;quot;tcp&amp;quot;; } ]; }
    ];

    wireguard.peers = [ { name = &amp;quot;phone&amp;quot;; ip = 2; publicKey = &amp;quot;&amp;lt;redacted&amp;gt;&amp;quot;; persistentKeepalive = 25; } ];

    nginx = {
      enable = true;
      acmeEmail = &amp;quot;per@stark.pub&amp;quot;;
      wildcardCerts = [
        { name = &amp;quot;lanstark&amp;quot;; baseDomain = &amp;quot;lan.stark.pub&amp;quot;; dnsProvider = &amp;quot;cloudflare&amp;quot;;
          environmentFile = config.my.secrets.getPath &amp;quot;api-key-cloudflare-dns&amp;quot; &amp;quot;api-token&amp;quot;; }
      ];
      virtualHosts = [
        { domain = &amp;quot;minne.stark.pub&amp;quot;; target = &amp;quot;makemake&amp;quot;; port = 3000; cloudflareOnly = true; }
      ];
    };
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;clan&quot;&gt;Clan&lt;a class=&quot;zola-anchor&quot; href=&quot;#clan&quot; aria-label=&quot;Anchor link for: clan&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;inventory&quot;&gt;Inventory&lt;a class=&quot;zola-anchor&quot; href=&quot;#inventory&quot; aria-label=&quot;Anchor link for: inventory&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;One file lists machines and roles (router, server, workstation). Each entry sets hostname, users, networking roles, and which features to import. Below is an excerpt from my configuration, declaring three machines, sshd setup for all, admin module, the user p for all machines. Note that deploy.buildHost is set to @localhost, this is since I use my workstation to deploy from, letting it evaluate and build, saving time.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nix&quot; data-name=&quot;inventory.nix (excerpt)&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot; data-name=&quot;inventory.nix (excerpt)&quot;&gt;{
  inventory = {
    machines.io = {
      deploy.targetHost = &amp;quot;root@io.lan&amp;quot;;
      deploy.buildHost = &amp;quot;root@localhost&amp;quot;;
      tags = [&amp;quot;server&amp;quot;];
    };
    machines.charon = {
      deploy.targetHost = &amp;quot;root@localhost&amp;quot;;
      tags = [&amp;quot;client&amp;quot;];
    };
    instances = {
      sshd-basic = {
        module = {
          name = &amp;quot;sshd&amp;quot;;
          input = &amp;quot;clan-core&amp;quot;;
        };
        roles.server.tags.all = {};
        roles.client.tags.all = {};
      };
      user-p = {
        module = {
          name = &amp;quot;users&amp;quot;;
          input = &amp;quot;clan-core&amp;quot;;
        };
        roles.default.tags.all = {};
        roles.default.settings = {
          user = &amp;quot;p&amp;quot;;
          prompt = true;
        };
      };
      admin = {
        roles.default.tags.all = {};
        roles.default.settings = {
          allowedKeys = {
            &amp;quot;p&amp;quot; = &amp;quot;ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII6uq8nXD+QBMhXqRNywwCa&amp;#x2F;dl2VVvG&amp;#x2F;2nvkw9HEPFzn&amp;quot;;
          };
        };
      };
  };
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;modules&quot;&gt;Modules&lt;a class=&quot;zola-anchor&quot; href=&quot;#modules&quot; aria-label=&quot;Anchor link for: modules&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Services are features under &lt;code&gt;modules&#x2F;system&lt;&#x2F;code&gt; and &lt;code&gt;modules&#x2F;home&lt;&#x2F;code&gt; exposed as &lt;code&gt;nixosModules.*&lt;&#x2F;code&gt; or &lt;code&gt;homeModules.*&lt;&#x2F;code&gt; and enabled per machine. Examples: reverse proxy, minne, media stack, backups. They all share the same pattern, which is somewhat more verbose, but offers some flake-parts niceties.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nix&quot; data-name=&quot;interception-tools.nix&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot; data-name=&quot;interception-tools.nix&quot;&gt;{
  config.flake.nixosModules.interception-tools = {pkgs, ...}: {
    services.interception-tools = {
      enable = true;
    };
  };
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This enables us to do several things. One is that we can use &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;vic&#x2F;import-tree&quot;&gt;import-tree&lt;&#x2F;a&gt;, to import all nix files from a directory into our flake and import them to our configurations by importing using their declared names. This also enables us to share config.* between all our modules. Reusing used options between modules with ease.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nix&quot; data-name=&quot;io&#x2F;configuration.nix&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot; data-name=&quot;io&#x2F;configuration.nix&quot;&gt;{
  imports = with modules.nixosModules;
    [
      interception-tools
      system-stylix
      shared
      options
      router
      home-assistant
      k3s
    ]
    ++ (with vars-helper.nixosModules; [default]);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;secrets&quot;&gt;Secrets&lt;a class=&quot;zola-anchor&quot; href=&quot;#secrets&quot; aria-label=&quot;Anchor link for: secrets&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Secrets use Clan “vars” on top of sops-nix. You might’ve noticed the vars-helper.nixosModules.default in the above code snippet. It’s a wrapper around clan vars I made to improve ergonomics and add some additional features, you can find it &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;perstarkse&#x2F;clan-vars-helper&quot;&gt;here&lt;&#x2F;a&gt;. For example it adds discovery of files by the use of tags, some helper functions to set up secrets in userspace, allow read access via ACL to secrets, wrapping commands in systemd environments for improved security.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;private-configuration&quot;&gt;Private configuration&lt;a class=&quot;zola-anchor&quot; href=&quot;#private-configuration&quot; aria-label=&quot;Anchor link for: private-configuration&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Having this structure meant that having a private repo containing sensitive bits, and importing this into my public configuration was quite straightforward. I set up a repository in a similar manner, with flake-parts, import-tree and sops (since I want to keep the secrets pertaining to services localized to where they are created).&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nix&quot; data-name=&quot;hello-world-service.nix&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot; data-name=&quot;hello-world-service.nix&quot;&gt;{
  config.flake.nixosModules.hello-service = {
    config,
    pkgs,
    ...
  }: {
    # Set up sops and have it point to the secrets.yaml in the private repo, and the key managed by clan vars
    sops = {
      defaultSopsFile = .&amp;#x2F;..&amp;#x2F;..&amp;#x2F;secrets.yaml;
      age.sshKeyPaths = [&amp;quot;&amp;#x2F;run&amp;#x2F;secrets&amp;#x2F;vars&amp;#x2F;openssh&amp;#x2F;ssh.id_ed25519&amp;quot;];
      secrets.hello = {};
    };
    systemd.services.hello-world-tmp = {
      description = &amp;quot;Create hello world file with secret&amp;quot;;
      serviceConfig.Type = &amp;quot;oneshot&amp;quot;;
      script = &amp;#x27;&amp;#x27;
        ${pkgs.coreutils}&amp;#x2F;bin&amp;#x2F;echo &amp;quot;Hello from private repo!&amp;quot; &amp;gt; &amp;#x2F;tmp&amp;#x2F;hello-world
        ${pkgs.coreutils}&amp;#x2F;bin&amp;#x2F;echo &amp;quot;Secret: $(${pkgs.coreutils}&amp;#x2F;bin&amp;#x2F;cat ${config.sops.secrets.hello.path})&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;tmp&amp;#x2F;hello-world
        ${pkgs.coreutils}&amp;#x2F;bin&amp;#x2F;chmod 644 &amp;#x2F;tmp&amp;#x2F;hello-world
      &amp;#x27;&amp;#x27;;
      wantedBy = [&amp;quot;multi-user.target&amp;quot;];
    };
  };
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is then consumed by my public config&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nix&quot; data-name=&quot;flake.nix&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot; data-name=&quot;flake.nix&quot;&gt;{
  private-infra = {
    url = &amp;quot;git+ssh:&amp;#x2F;&amp;#x2F;git@github.com&amp;#x2F;perstarkse&amp;#x2F;private-infra.git&amp;quot;;
    inputs.nixpkgs.follows = &amp;quot;nixpkgs&amp;quot;;
  };  
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And then the modules are imported in my:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nix&quot; data-name=&quot;charon&#x2F;configuration.nix&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot; data-name=&quot;charon&#x2F;configuration.nix&quot;&gt;{
  imports  = [&amp;quot;module&amp;quot;] ++ (with private-infra.nixosModules; [hello-service]);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;end&quot;&gt;End&lt;a class=&quot;zola-anchor&quot; href=&quot;#end&quot; aria-label=&quot;Anchor link for: end&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;At the point of writing I’ve reached the point where the summer is at it’s last legs here in Sweden and all machines and services are migrated.&lt;&#x2F;p&gt;
&lt;p&gt;It’s time for reflection. It’s been a fun summer project, some aspects of it more than others. Assuming a dendritic nix pattern (ish) and utilizing options to create meaningful abstractions, reducing code duplication, improving maintainability sure was fun.&lt;&#x2F;p&gt;
&lt;p&gt;Implementing Clan, well, I knew it was going to be a bit of a challenge due to how early it was. Just in the past month documentation has really improved and I think getting started is significantly easier now. I really do enjoy clan vars, the sshd service. And I know there is significantly more services to use, distributed s3 storage, databases, etc.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;related-posts&quot;&gt;Related posts&lt;a class=&quot;zola-anchor&quot; href=&quot;#related-posts&quot; aria-label=&quot;Anchor link for: related-posts&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;nixos-abstractions&#x2F;&quot;&gt;NixOS, an operating system for me?&lt;&#x2F;a&gt; - Where my NixOS journey started and the philosophy behind declarative configuration.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;n150-nixos-router&#x2F;&quot;&gt;N150 NixOS router, a guide&lt;&#x2F;a&gt; - The router that became part of this migration, detailed in an earlier post.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
	</entry>
	<entry xml:lang="en">
		<title>N150 NixOS router, a guide</title>
		<published>2025-05-23T00:00:00+00:00</published>
		<updated>2025-05-23T00:00:00+00:00</updated>
		<link href="https://blog.stark.pub/posts/n150-nixos-router/" type="text/html"/>
		<id>https://blog.stark.pub/posts/n150-nixos-router/</id>
		<content type="html">&lt;h2 id=&quot;the-why&quot;&gt;The why&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-why&quot; aria-label=&quot;Anchor link for: the-why&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;For years, I’ve relied on two Google Nest Wi-Fi units to handle my home networking needs. To be fair, they’ve served me well—stable, low-maintenance, and good enough for day-to-day use. That’s probably why I kept postponing any kind of upgrade. But eventually, I reached a point where I felt it was time for a change.&lt;&#x2F;p&gt;
&lt;p&gt;There were a few things pushing me to make a change:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;I wanted more control over my network, and features such as VLAN tagging.&lt;&#x2F;li&gt;
&lt;li&gt;And I was tired of managing everything through the Google Home app instead of using SSH.&lt;&#x2F;li&gt;
&lt;li&gt;I wanted to explore running a NixOS router, I use NixOS for the rest of my machines and enjoy it.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;hardware&quot;&gt;Hardware&lt;a class=&quot;zola-anchor&quot; href=&quot;#hardware&quot; aria-label=&quot;Anchor link for: hardware&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;A while back, I bought an N100-based motherboard and CPU combo from AliExpress. It was compact, power-efficient, and came with multiple NICs, M.2 slots, and SATA ports—plenty for most DIY network projects. It worked flawlessly and honestly exceeded expectations.&lt;&#x2F;p&gt;
&lt;p&gt;For the router I picked up an &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;www.aliexpress.com&#x2F;item&#x2F;1005004360072281.html&quot;&gt;N150-based board&lt;&#x2F;a&gt;, added an M.2 SSD and 16GB of DDR5 RAM (which is probably overkill, but useful for future flexibility), and got to work. Besides routing, I also plan to run a few lightweight networking services on it. I already have a separate server for heavier workloads, but it’s nice to spread things out and keep the network-focused features close to the router itself.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;unboxing-and-installation&quot;&gt;Unboxing and installation&lt;a class=&quot;zola-anchor&quot; href=&quot;#unboxing-and-installation&quot; aria-label=&quot;Anchor link for: unboxing-and-installation&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The N150 device was packaged quite neatly and contained all the specified parts. The device feels quite sturdy. I forgot to order a wall mount, which topton sells via Aliexpress, so I decided to print &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;www.printables.com&#x2F;model&#x2F;903657-slim-wall-mount-for-topton-mini-pc&#x2F;files&quot;&gt;one&lt;&#x2F;a&gt; instead.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;figure&gt;
    &lt;img src=&quot;.&amp;#x2F;n150_mini_pc.webp&quot; alt=&quot;Beelink N150 Mini PC internal hardware layout with M.2 and RAM slots&quot;&gt;
    
    &lt;figcaption&gt;View from bottom with backplate dismounted&lt;&#x2F;figcaption&gt;
    
&lt;&#x2F;figure&gt;

Installing the M.2 SSD and RAM was quite simple. The device opened easily by removing four screws in the bottom.&lt;&#x2F;p&gt;
&lt;p&gt;It booted directly, recognizing the RAM and disk without issue. Inspecting BIOS settings showed appropriate settings to my knowledge. VT-d enabled. i226 LAN PXE was disabled, but I don’t plan to use it.&lt;&#x2F;p&gt;
&lt;p&gt;Installing NixOS went without flaw. Its always a bit of a chore setting up secrets management on a new device but it’s fairly straight forward. I use sops as well as git-agecrypt. The installer correctly set the hardware.cpu.intel.updateMicrocode to the same value as nixos-hardware.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;system-configuration&quot;&gt;System Configuration&lt;a class=&quot;zola-anchor&quot; href=&quot;#system-configuration&quot; aria-label=&quot;Anchor link for: system-configuration&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The N150 hardware has some known issues with power management that are common among budget mini PCs. BIOS options for power limiting and advanced C-state control are often limited or non-functional.&lt;&#x2F;p&gt;
&lt;p&gt;I applied some power management configuration. Please get in touch if you’ve got better ideas on how to do it.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nix&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot;&gt;{
environment.systemPackages = [
  pkgs.lm_sensors
  pkgs.powertop
];

services.tlp.enable = true;
powerManagement = {
  enable = true;
  powertop.enable = true;
  cpuFreqGovernor = &amp;quot;powersave&amp;quot;;
};

boot.kernelParams = [
  &amp;quot;intel_pstate=passive&amp;quot;
  &amp;quot;processor.max_cstate=6&amp;quot;
];

boot.blacklistedKernelModules = [ &amp;quot;i915&amp;quot; ];
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The results seems decent for now. At idle, the system maintains:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU temperatures&lt;&#x2F;strong&gt;: 42-46°C across all cores&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;NVMe SSD&lt;&#x2F;strong&gt;: 59-65°C&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;C-state behavior&lt;&#x2F;strong&gt;: Individual cores spend 99% of time in deep sleep states (C6&#x2F;C7), while the package maintains lighter sleep (C2).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The device is clearly warm&#x2F;hot to the touch. But the provided temperatures shows it’s acceptable for now. I plan to wall mount it, which according to someone at STH forums improved cooling somewhat.&lt;&#x2F;p&gt;
&lt;p&gt;When configuring the network settings and testing it became clear there is an issue when inserting in to NIC ports which are down, it will not go up again. It turned out that PCIe port power management and EEE is responsible. Setting this resulted in everything working as it should.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;boot.kernelParams = [
  &amp;quot;pcie_port_pm=off&amp;quot; # Disable PCIe port power management
  &amp;quot;igc.eee_enable=0&amp;quot; # Disable Energy Efficient Ethernet
];
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;network-configuration&quot;&gt;Network configuration&lt;a class=&quot;zola-anchor&quot; href=&quot;#network-configuration&quot; aria-label=&quot;Anchor link for: network-configuration&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;A benefit of doing this on NixOS is having the configuration evaluated before runtime. This makes it easy to catch errors, like the one below I encountered when mistyping a firewall rule.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ruleset.conf:13:60-86: Error: Statement after terminal statement has no effect
iifname &amp;quot;enp1s0&amp;quot; tcp dport 22 limit rate 5&amp;#x2F;minute accept commen &amp;quot;Allow SSH from WAN&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Another aspect I enjoyed was evaluating and building the router system remotely from my workstation, deploying it over SSH. This both saves time and makes for a great workflow experience.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;nixos-rebuild switch --flake .#io --target-host root@io.lan --use-remote-sudo
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For the networking stack I settled for:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;systemd-networkd&lt;&#x2F;li&gt;
&lt;li&gt;nftables&lt;&#x2F;li&gt;
&lt;li&gt;Unbound&lt;&#x2F;li&gt;
&lt;li&gt;Kea DHCP&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;update-after-running-it-for-a-couple-of-months&quot;&gt;Update after running it for a couple of months&lt;a class=&quot;zola-anchor&quot; href=&quot;#update-after-running-it-for-a-couple-of-months&quot; aria-label=&quot;Anchor link for: update-after-running-it-for-a-couple-of-months&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;At this point in time I’ve used the router with mostly that same settings for a couple of months. The device has worked flawlessly. The N150 is a quite capable device. At one point I had my usual build server down in a migration, and the N150 acted as a fully capable build server in the meantime. I was a bit mindful of the temperatures, but it has not been an issue, idling at 46 degrees C.&lt;&#x2F;p&gt;
&lt;p&gt;I had one outage, and it was when cloudflare dns was down, showcasing the vulnerability of having it as the sole dns resolver. I’ve since done some revisions, but that’s mainly been adding additional features and abstracting some settings into a more user friendly (me friendly in the future) interface. If you’re interested the code is reachable &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;perstarkse&#x2F;infra&#x2F;tree&#x2F;main&#x2F;modules&#x2F;system&#x2F;router&quot;&gt;here&lt;&#x2F;a&gt; and enables a router configured by the following&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nix&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot;&gt; my.router = {
    enable = true;
    hostname = &amp;quot;io&amp;quot;;
    lan = {
      subnet = &amp;quot;10.0.0&amp;quot;;
      dhcpRange = { start = 100; end = 200; };
      interfaces = [&amp;quot;enp2s0&amp;quot; &amp;quot;enp3s0&amp;quot; &amp;quot;enp4s0&amp;quot;];
    };
    wan.interface = &amp;quot;enp1s0&amp;quot;;
    ipv6.ulaPrefix = &amp;quot;fd00:711a:edcd:7e75&amp;quot;;

    wireguard = {
      enable = true;
      peers = [
        {
          name = &amp;quot;phone&amp;quot;;
          ip = 2;
          publicKey = &amp;quot;NWwGu+pFOMhV6r6mlE7efbe1qSEU8cPHKljl+j1Wxxo=&amp;quot;;
          persistentKeepalive = 25;
        }
      ];
    };

    machines = [
      {
        name = &amp;quot;charon&amp;quot;;
        ip = &amp;quot;15&amp;quot;;
        mac = &amp;quot;f0:2f:74:de:91:0a&amp;quot;;
        portForwards = [];
      }
      {
        name = &amp;quot;makemake&amp;quot;;
        ip = &amp;quot;10&amp;quot;;
        mac = &amp;quot;00:d0:b4:02:bb:3c&amp;quot;;
        portForwards = [
          { port = 25; protocol = &amp;quot;tcp&amp;quot;; }
          { port = 465; protocol = &amp;quot;tcp&amp;quot;; }
          { port = 993; protocol = &amp;quot;tcp&amp;quot;; }
          { port = 32400; protocol = &amp;quot;tcp&amp;quot;; }
        ];
      }
    ];

    services = [
      { name = &amp;quot;mail.stark.pub&amp;quot;;    target = &amp;quot;10.0.0.10&amp;quot;; }
      { name = &amp;quot;chat.stark.pub&amp;quot;;    target = &amp;quot;10.0.0.1&amp;quot;; }
      { name = &amp;quot;minne.stark.pub&amp;quot;;   target = &amp;quot;10.0.0.1&amp;quot;; }
      { name = &amp;quot;request.stark.pub&amp;quot;; target = &amp;quot;10.0.0.1&amp;quot;; }
      { name = &amp;quot;vault.stark.pub&amp;quot;;   target = &amp;quot;10.0.0.1&amp;quot;; }
    ];

    dhcp = {
      enable = true;
      validLifetime = 86400;
      renewTimer = 43200;
      rebindTimer = 75600;
      domainName = &amp;quot;lan&amp;quot;;
    };

    dns = {
      enable = true;
      upstreamServers = [
        &amp;quot;1.1.1.1@853#cloudflare-dns.com&amp;quot;
        &amp;quot;1.0.0.1@853#cloudflare-dns.com&amp;quot;
        &amp;quot;2606:4700:4700::1111@853#cloudflare-dns.com&amp;quot;
        &amp;quot;2606:4700:4700::1001@853#cloudflare-dns.com&amp;quot;
      ];
      localZone = &amp;quot;lan.&amp;quot;;
    };

    nginx = {
      enable = true;
      acmeEmail = &amp;quot;per@stark.pub&amp;quot;;
      ddclient.enable = true;
      virtualHosts = [
        { domain = &amp;quot;vault.stark.pub&amp;quot;;   target = &amp;quot;makemake&amp;quot;; port = 8322; websockets = true; lanOnly = true; acmeDns01 = { dnsProvider = &amp;quot;cloudflare&amp;quot;; environmentFile = config.my.secrets.getPath &amp;quot;api-key-cloudflare-dns&amp;quot; &amp;quot;api-token&amp;quot;; }; }
        { domain = &amp;quot;chat.stark.pub&amp;quot;;    target = &amp;quot;makemake&amp;quot;; port = 7909; websockets = true; cloudflareOnly = true; }
        { domain = &amp;quot;request.stark.pub&amp;quot;; target = &amp;quot;makemake&amp;quot;; port = 5055; websockets = true; cloudflareOnly = true; }
        {
          domain = &amp;quot;minne.stark.pub&amp;quot;;
          target = &amp;quot;makemake&amp;quot;;
          port = 3000;
          websockets = false;
          cloudflareOnly = true;
          extraConfig = &amp;#x27;&amp;#x27;
            proxy_set_header Connection &amp;quot;close&amp;quot;;
            proxy_http_version 1.1;
            chunked_transfer_encoding off;
            proxy_buffering off;
            proxy_cache off;
          &amp;#x27;&amp;#x27;;
        }
        {
          domain = &amp;quot;minne-demo.stark.pub&amp;quot;;
          target = &amp;quot;makemake&amp;quot;;
          port = 3001;
          websockets = false;
          cloudflareOnly = true;
          extraConfig = &amp;#x27;&amp;#x27;
            proxy_set_header Connection &amp;quot;close&amp;quot;;
            proxy_http_version 1.1;
            chunked_transfer_encoding off;
            proxy_buffering off;
            proxy_cache off;
          &amp;#x27;&amp;#x27;;
        }
      ];
    };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;flake-initial&quot;&gt;Flake (initial)&lt;a class=&quot;zola-anchor&quot; href=&quot;#flake-initial&quot; aria-label=&quot;Anchor link for: flake-initial&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Im fairly certain there are some improvements to be done, and possibly some redundancies. I suspect it will be a work in progress for quite some while. But having this declarative nature (and rollbacks) is as always a huge benefit when iterating. Should you the reader have any questions or suggestions feel free to reach out by &lt;a href=&quot;mailto:per@stark.pub&quot;&gt;mail&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nix&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot;&gt;{pkgs, ...}: let
  lanSubnet = &amp;quot;10.0.0&amp;quot;;
  lanCidr = &amp;quot;${lanSubnet}.0&amp;#x2F;24&amp;quot;;
  routerIp = &amp;quot;${lanSubnet}.1&amp;quot;;
  dhcpStart = &amp;quot;${lanSubnet}.100&amp;quot;;
  dhcpEnd = &amp;quot;${lanSubnet}.200&amp;quot;;

  # Machines
  charon = &amp;quot;${lanSubnet}.15&amp;quot;;
  makemake = &amp;quot;${lanSubnet}.10&amp;quot;;
  encke = &amp;quot;${lanSubnet}.156&amp;quot;;

  ulaPrefix = &amp;quot;fd99:d22a:eddd:7e75&amp;quot;;
in {
  boot.kernel.sysctl = {
    &amp;quot;net.ipv4.conf.all.forwarding&amp;quot; = true;
    &amp;quot;net.ipv4.conf.default.rp_filter&amp;quot; = 1;
    &amp;quot;net.ipv4.conf.enp1s0.rp_filter&amp;quot; = 1;
    &amp;quot;net.ipv4.conf.br-lan.rp_filter&amp;quot; = 1;

    &amp;quot;net.ipv6.conf.all.forwarding&amp;quot; = true;
    &amp;quot;net.ipv6.conf.all.accept_ra&amp;quot; = 0;
    &amp;quot;net.ipv6.conf.all.autoconf&amp;quot; = 0;
    &amp;quot;net.ipv6.conf.all.use_tempaddr&amp;quot; = 0;
  };

  boot.kernelParams = [
    &amp;quot;pcie_port_pm=off&amp;quot; # Disable PCIe port power management
    &amp;quot;igc.eee_enable=0&amp;quot; # Disable Energy Efficient Ethernet
  ];

  networking = {
    hostName = &amp;quot;io&amp;quot;;
    useNetworkd = true;
    useDHCP = false;
    networkmanager.enable = false;
    firewall.enable = false;

    nftables = {
      enable = true;
      tables = {
        filterV4 = {
          family = &amp;quot;ip&amp;quot;;
          content = &amp;#x27;&amp;#x27;
            chain input {
              type filter hook input priority 0; policy drop;
              iifname &amp;quot;lo&amp;quot; accept
              iifname &amp;quot;br-lan&amp;quot; accept
              iifname &amp;quot;enp1s0&amp;quot; ct state established,related accept
              iifname &amp;quot;enp1s0&amp;quot; ip protocol icmp accept
            }
            chain forward {
              type filter hook forward priority 0; policy drop;
              iifname &amp;quot;br-lan&amp;quot; oifname &amp;quot;enp1s0&amp;quot; accept
              iifname &amp;quot;enp1s0&amp;quot; oifname &amp;quot;br-lan&amp;quot; ct state established,related accept
              iifname &amp;quot;enp1s0&amp;quot; oifname &amp;quot;br-lan&amp;quot; ip daddr ${makemake} tcp dport { 80, 443, 25, 465, 993 } accept
            }
          &amp;#x27;&amp;#x27;;
        };
        natV4 = {
          family = &amp;quot;ip&amp;quot;;
          content = &amp;#x27;&amp;#x27;
            chain prerouting {
              type nat hook prerouting priority -100;
              iifname &amp;quot;enp1s0&amp;quot; tcp dport { 80, 443, 25, 465, 993 } dnat to ${makemake}
            }
            chain postrouting {
              type nat hook postrouting priority 100;
              oifname &amp;quot;enp1s0&amp;quot; masquerade
            }
          &amp;#x27;&amp;#x27;;
        };
        filterV6 = {
          family = &amp;quot;ip6&amp;quot;;
          content = &amp;#x27;&amp;#x27;
            chain input {
              type filter hook input priority 0; policy drop;
              iifname &amp;quot;lo&amp;quot; accept
              iifname &amp;quot;br-lan&amp;quot; accept
              iifname &amp;quot;enp1s0&amp;quot; ct state established,related accept
              iifname &amp;quot;enp1s0&amp;quot; icmpv6 type {
                destination-unreachable, packet-too-big, time-exceeded,
                parameter-problem, nd-router-advert, nd-neighbor-solicit,
                nd-neighbor-advert
              } accept
              iifname &amp;quot;enp1s0&amp;quot; udp dport dhcpv6-client udp sport dhcpv6-server accept
            }
            chain forward {
              type filter hook forward priority 0; policy drop;
              iifname &amp;quot;br-lan&amp;quot; oifname &amp;quot;enp1s0&amp;quot; accept
              iifname &amp;quot;enp1s0&amp;quot; oifname &amp;quot;br-lan&amp;quot; ct state established,related accept
            }
          &amp;#x27;&amp;#x27;;
        };
      };
    };
  };

  systemd.network = {
    enable = true; 

    # Define the bridge device itself
    netdevs.&amp;quot;20-br-lan&amp;quot; = {
      netdevConfig = {
        Kind = &amp;quot;bridge&amp;quot;;
        Name = &amp;quot;br-lan&amp;quot;;
      };
    };

    networks = {
      # WAN interface configuration (enp1s0)
      &amp;quot;20-wan&amp;quot; = {
        matchConfig.Name = &amp;quot;enp1s0&amp;quot;;
        networkConfig = {
          DHCP = &amp;quot;yes&amp;quot;; 
          IPv4Forwarding = true;
          IPv6Forwarding = true;
          IPv6AcceptRA = true; 
        };
        dhcpV6Config.WithoutRA = &amp;quot;solicit&amp;quot;; 
        linkConfig.RequiredForOnline = &amp;quot;routable&amp;quot;;
      };

      # LAN bridge (br-lan) configuration
      &amp;quot;10-br-lan&amp;quot; = {
        matchConfig.Name = &amp;quot;br-lan&amp;quot;;
        # Addresses for the router on the LAN bridge
        address = [
          &amp;quot;${routerIp}&amp;#x2F;24&amp;quot;
          &amp;quot;${ulaPrefix}::1&amp;#x2F;64&amp;quot; 
        ];
        networkConfig = {
          ConfigureWithoutCarrier = true; 
          DHCPPrefixDelegation = true; 
          IPv6SendRA = true; 
          IPv6AcceptRA = false; 
        };
        bridgeConfig = {}; 
        ipv6Prefixes = [
          {
            AddressAutoconfiguration = true;
            OnLink = true;
            Prefix = &amp;quot;${ulaPrefix}::&amp;#x2F;64&amp;quot;;
          }
        ];
        linkConfig.RequiredForOnline = &amp;quot;no&amp;quot;;
      };

      # Configure enp2s0 to be part of br-lan
      &amp;quot;30-enp2s0-lan&amp;quot; = {
        matchConfig.Name = &amp;quot;enp2s0&amp;quot;;
        networkConfig = {
          Bridge = &amp;quot;br-lan&amp;quot;;
          ConfigureWithoutCarrier = true;
        };
      };
      # Configure enp3s0 to be part of br-lan
      &amp;quot;30-enp3s0-lan&amp;quot; = {
        matchConfig.Name = &amp;quot;enp3s0&amp;quot;;
        networkConfig = {
          Bridge = &amp;quot;br-lan&amp;quot;;
          ConfigureWithoutCarrier = true;
        };
      };
      # Configure enp4s0 to be part of br-lan
      &amp;quot;30-enp4s0-lan&amp;quot; = {
        matchConfig.Name = &amp;quot;enp4s0&amp;quot;;
        networkConfig = {
          Bridge = &amp;quot;br-lan&amp;quot;;
          ConfigureWithoutCarrier = true;
        };
      };
    };
  };

  services.unbound = {
    enable = true;
    settings = {
      server = {
        interface = [
          &amp;quot;127.0.0.1&amp;quot;
          &amp;quot;::1&amp;quot;
          routerIp
          &amp;quot;${ulaPrefix}::1&amp;quot;
        ];
        access-control = [
          &amp;quot;127.0.0.0&amp;#x2F;8 allow&amp;quot;
          &amp;quot;::1 allow&amp;quot;
          &amp;quot;${lanCidr} allow&amp;quot;
          &amp;quot;${ulaPrefix}::&amp;#x2F;64 allow&amp;quot;
          &amp;quot;0.0.0.0&amp;#x2F;0 refuse&amp;quot;
          &amp;quot;::0&amp;#x2F;0 refuse&amp;quot;
        ];
        cache-min-ttl = 0;
        cache-max-ttl = 86400;
        do-tcp = true;
        do-udp = true;
        prefetch = true;
        num-threads = 1;
        so-reuseport = true;
        local-zone = &amp;#x27;&amp;#x27;
          &amp;quot;lan.&amp;quot; static
        &amp;#x27;&amp;#x27;;
        local-data = [
          # Router
          &amp;#x27;&amp;#x27;&amp;quot;io.lan. IN A ${routerIp}&amp;quot;&amp;#x27;&amp;#x27;
          &amp;#x27;&amp;#x27;&amp;quot;io.lan. IN AAAA ${ulaPrefix}::1&amp;quot;&amp;#x27;&amp;#x27;

          # Machines
          &amp;#x27;&amp;#x27;&amp;quot;charon.lan. IN A ${charon}&amp;quot;&amp;#x27;&amp;#x27;
          &amp;#x27;&amp;#x27;&amp;quot;makemake.lan. IN A ${makemake}&amp;quot;&amp;#x27;&amp;#x27;
          &amp;#x27;&amp;#x27;&amp;quot;encke.lan. IN A ${encke}&amp;quot;&amp;#x27;&amp;#x27;

          # Services
          &amp;#x27;&amp;#x27;&amp;quot;mail.stark.pub. IN A ${makemake}&amp;quot;&amp;#x27;&amp;#x27;
        ];
      };
      forward-zone = [
        {
          name = &amp;quot;.&amp;quot;;
          forward-addr = [
            &amp;quot;1.1.1.1@853#cloudflare-dns.com&amp;quot;
            &amp;quot;1.0.0.1@853#cloudflare-dns.com&amp;quot;
            &amp;quot;2606:4700:4700::1111@853#cloudflare-dns.com&amp;quot;
            &amp;quot;2606:4700:4700::1001@853#cloudflare-dns.com&amp;quot;
          ];
          forward-tls-upstream = true;
        }
      ];
    };
  };

  services.kea.dhcp4 = {
    enable = true;
    settings = {
      interfaces-config.interfaces = [&amp;quot;br-lan&amp;quot;];
      lease-database = {
        name = &amp;quot;&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;kea&amp;#x2F;dhcp4-leases.csv&amp;quot;;
        type = &amp;quot;memfile&amp;quot;;
        persist = true;
        lfc-interval = 3600;
      };
      valid-lifetime = 86400;
      renew-timer = 43200;
      rebind-timer = 75600;
      subnet4 = [
        {
          id = 1;
          subnet = lanCidr;
          pools = [
            {
              pool = &amp;quot;${dhcpStart} - ${dhcpEnd}&amp;quot;;
            }
          ];
          reservations = [
            {
              hw-address = &amp;quot;f0:2f:74:de:91:0a&amp;quot;;
              ip-address = &amp;quot;${charon}&amp;quot;;
              hostname = &amp;quot;charon&amp;quot;;
            }
            {
              hw-address = &amp;quot;00:d0:b4:02:bb:3c&amp;quot;;
              ip-address = &amp;quot;${makemake}&amp;quot;;
              hostname = &amp;quot;makemake&amp;quot;;
            }
            {
              hw-address = &amp;quot;52:54:00:a7:db:fe&amp;quot;;
              ip-address = &amp;quot;${encke}&amp;quot;;
              hostname = &amp;quot;encke&amp;quot;;
            }
          ];
          option-data = [
            {
              name = &amp;quot;routers&amp;quot;;
              data = routerIp;
            }
            {
              name = &amp;quot;domain-name-servers&amp;quot;;
              data = routerIp;
            }
            {
              name = &amp;quot;domain-name&amp;quot;;
              data = &amp;quot;lan&amp;quot;;
            }
          ];
        }
      ];
    };
  };
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;frequently-asked-questions&quot;&gt;Frequently Asked Questions&lt;a class=&quot;zola-anchor&quot; href=&quot;#frequently-asked-questions&quot; aria-label=&quot;Anchor link for: frequently-asked-questions&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;




&lt;div class=&quot;faq-section&quot; itemscope itemtype=&quot;https:&#x2F;&#x2F;schema.org&#x2F;FAQPage&quot;&gt;
  
    
    
      
      
      &lt;div class=&quot;faq-item&quot; itemscope itemprop=&quot;mainEntity&quot; itemtype=&quot;https:&#x2F;&#x2F;schema.org&#x2F;Question&quot;&gt;
        &lt;h3 itemprop=&quot;name&quot;&gt;Can the N150 mini PC run NixOS as a router?&lt;&#x2F;h3&gt;
        &lt;div itemscope itemprop=&quot;acceptedAnswer&quot; itemtype=&quot;https:&#x2F;&#x2F;schema.org&#x2F;Answer&quot;&gt;
          &lt;div itemprop=&quot;text&quot;&gt;&lt;p&gt;Yes, the N150 mini PC works well as a NixOS router. It has multiple Intel i226-V NICs, low power consumption, and enough performance for home network routing with VLANs, firewall rules, and DNS services. The Intel N150 CPU with 16GB RAM handles routing, Unbound DNS, and Kea DHCP without issues.&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
        &lt;&#x2F;div&gt;
      &lt;&#x2F;div&gt;
    
  
    
    
      
      
      &lt;div class=&quot;faq-item&quot; itemscope itemprop=&quot;mainEntity&quot; itemtype=&quot;https:&#x2F;&#x2F;schema.org&#x2F;Question&quot;&gt;
        &lt;h3 itemprop=&quot;name&quot;&gt;What networking stack works best for a NixOS router?&lt;&#x2F;h3&gt;
        &lt;div itemscope itemprop=&quot;acceptedAnswer&quot; itemtype=&quot;https:&#x2F;&#x2F;schema.org&#x2F;Answer&quot;&gt;
          &lt;div itemprop=&quot;text&quot;&gt;&lt;p&gt;For a NixOS router, I recommend systemd-networkd for network management, nftables for firewall rules, Unbound for DNS resolution, and Kea for DHCP. This combination is well-supported in NixOS and allows fully declarative configuration with pre-runtime validation.&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
        &lt;&#x2F;div&gt;
      &lt;&#x2F;div&gt;
    
  
    
    
      
      
      &lt;div class=&quot;faq-item&quot; itemscope itemprop=&quot;mainEntity&quot; itemtype=&quot;https:&#x2F;&#x2F;schema.org&#x2F;Question&quot;&gt;
        &lt;h3 itemprop=&quot;name&quot;&gt;How do I fix NIC ports not coming up on the N150?&lt;&#x2F;h3&gt;
        &lt;div itemscope itemprop=&quot;acceptedAnswer&quot; itemtype=&quot;https:&#x2F;&#x2F;schema.org&#x2F;Answer&quot;&gt;
          &lt;div itemprop=&quot;text&quot;&gt;&lt;p&gt;If NIC ports don’t come up after cable insertion on the N150, disable PCIe port power management and Energy Efficient Ethernet by adding these kernel parameters: &lt;code&gt;pcie_port_pm=off&lt;&#x2F;code&gt; and &lt;code&gt;igc.eee_enable=0&lt;&#x2F;code&gt;. This resolves the issue with Intel i226-V NICs going offline.&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
        &lt;&#x2F;div&gt;
      &lt;&#x2F;div&gt;
    
  
    
    
      
      
      &lt;div class=&quot;faq-item&quot; itemscope itemprop=&quot;mainEntity&quot; itemtype=&quot;https:&#x2F;&#x2F;schema.org&#x2F;Question&quot;&gt;
        &lt;h3 itemprop=&quot;name&quot;&gt;What are the idle temperatures for an N150 NixOS router?&lt;&#x2F;h3&gt;
        &lt;div itemscope itemprop=&quot;acceptedAnswer&quot; itemtype=&quot;https:&#x2F;&#x2F;schema.org&#x2F;Answer&quot;&gt;
          &lt;div itemprop=&quot;text&quot;&gt;&lt;p&gt;The N150 running NixOS as a router idles at 42-46C for CPU cores and 59-65C for NVMe SSD. Individual cores spend 99% of time in deep sleep states (C6&#x2F;C7). Wall mounting the device can improve cooling slightly.&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
        &lt;&#x2F;div&gt;
      &lt;&#x2F;div&gt;
    
  
    
    
      
      
      &lt;div class=&quot;faq-item&quot; itemscope itemprop=&quot;mainEntity&quot; itemtype=&quot;https:&#x2F;&#x2F;schema.org&#x2F;Question&quot;&gt;
        &lt;h3 itemprop=&quot;name&quot;&gt;How do I deploy NixOS configuration to a router remotely?&lt;&#x2F;h3&gt;
        &lt;div itemscope itemprop=&quot;acceptedAnswer&quot; itemtype=&quot;https:&#x2F;&#x2F;schema.org&#x2F;Answer&quot;&gt;
          &lt;div itemprop=&quot;text&quot;&gt;&lt;p&gt;Use &lt;code&gt;nixos-rebuild switch --flake .#hostname --target-host root@router.lan --use-remote-sudo&lt;&#x2F;code&gt; to build and deploy the configuration from your workstation over SSH. This saves time and provides a better workflow than building directly on the router.&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
        &lt;&#x2F;div&gt;
      &lt;&#x2F;div&gt;
    
  
&lt;&#x2F;div&gt;

&lt;script type=&quot;application&#x2F;ld+json&quot;&gt;
{
  &quot;@context&quot;: &quot;https:&#x2F;&#x2F;schema.org&quot;,
  &quot;@type&quot;: &quot;FAQPage&quot;,
  &quot;mainEntity&quot;: [
    
      
      
        
        
        {
          &quot;@type&quot;: &quot;Question&quot;,
          &quot;name&quot;: &quot;Can the N150 mini PC run NixOS as a router?&quot;,
          &quot;acceptedAnswer&quot;: {
            &quot;@type&quot;: &quot;Answer&quot;,
            &quot;text&quot;: &quot;Yes, the N150 mini PC works well as a NixOS router. It has multiple Intel i226-V NICs, low power consumption, and enough performance for home network routing with VLANs, firewall rules, and DNS services. The Intel N150 CPU with 16GB RAM handles routing, Unbound DNS, and Kea DHCP without issues.&quot;
          }
        },
      
    
      
      
        
        
        {
          &quot;@type&quot;: &quot;Question&quot;,
          &quot;name&quot;: &quot;What networking stack works best for a NixOS router?&quot;,
          &quot;acceptedAnswer&quot;: {
            &quot;@type&quot;: &quot;Answer&quot;,
            &quot;text&quot;: &quot;For a NixOS router, I recommend systemd-networkd for network management, nftables for firewall rules, Unbound for DNS resolution, and Kea for DHCP. This combination is well-supported in NixOS and allows fully declarative configuration with pre-runtime validation.&quot;
          }
        },
      
    
      
      
        
        
        {
          &quot;@type&quot;: &quot;Question&quot;,
          &quot;name&quot;: &quot;How do I fix NIC ports not coming up on the N150?&quot;,
          &quot;acceptedAnswer&quot;: {
            &quot;@type&quot;: &quot;Answer&quot;,
            &quot;text&quot;: &quot;If NIC ports don&amp;amp;#x27;t come up after cable insertion on the N150, disable PCIe port power management and Energy Efficient Ethernet by adding these kernel parameters: `pcie_port_pm=off` and `igc.eee_enable=0`. This resolves the issue with Intel i226-V NICs going offline.&quot;
          }
        },
      
    
      
      
        
        
        {
          &quot;@type&quot;: &quot;Question&quot;,
          &quot;name&quot;: &quot;What are the idle temperatures for an N150 NixOS router?&quot;,
          &quot;acceptedAnswer&quot;: {
            &quot;@type&quot;: &quot;Answer&quot;,
            &quot;text&quot;: &quot;The N150 running NixOS as a router idles at 42-46C for CPU cores and 59-65C for NVMe SSD. Individual cores spend 99% of time in deep sleep states (C6&amp;amp;#x2F;C7). Wall mounting the device can improve cooling slightly.&quot;
          }
        },
      
    
      
      
        
        
        {
          &quot;@type&quot;: &quot;Question&quot;,
          &quot;name&quot;: &quot;How do I deploy NixOS configuration to a router remotely?&quot;,
          &quot;acceptedAnswer&quot;: {
            &quot;@type&quot;: &quot;Answer&quot;,
            &quot;text&quot;: &quot;Use `nixos-rebuild switch --flake .#hostname --target-host root@router.lan --use-remote-sudo` to build and deploy the configuration from your workstation over SSH. This saves time and provides a better workflow than building directly on the router.&quot;
          }
        }
      
    
  ]
}
&lt;&#x2F;script&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;related-posts&quot;&gt;Related posts&lt;a class=&quot;zola-anchor&quot; href=&quot;#related-posts&quot; aria-label=&quot;Anchor link for: related-posts&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;nixos-abstractions&#x2F;&quot;&gt;NixOS, an operating system for me?&lt;&#x2F;a&gt; - The philosophy and benefits of NixOS that made this declarative router possible.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;clan-migration&#x2F;&quot;&gt;Summer project 2025: Migrating to Clan and a Dendritic Architecture&lt;&#x2F;a&gt; - How I later refactored this router configuration into a more maintainable architecture.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
	</entry>
	<entry xml:lang="en">
		<title>Migrating from i3&#x2F;X11 to Hyprland on NixOS</title>
		<published>2025-05-21T00:00:00+00:00</published>
		<updated>2025-05-21T00:00:00+00:00</updated>
		<link href="https://blog.stark.pub/posts/i3-to-hyprland/" type="text/html"/>
		<id>https://blog.stark.pub/posts/i3-to-hyprland/</id>
		<content type="html">&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;a class=&quot;zola-anchor&quot; href=&quot;#introduction&quot; aria-label=&quot;Anchor link for: introduction&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;After years of using a stable X11 + i3 setup (with an &lt;strong&gt;NVIDIA GTX 1650&lt;&#x2F;strong&gt; and an ultra-wide monitor), I decided it was time to make the move to Wayland. While my X11 setup “just worked” (apart from some minor screen tearing fixed with &lt;code&gt;ForceFullCompositionPipeline&lt;&#x2F;code&gt;), it’s clear Wayland is the future, and migration is inevitable for anyone running Linux on modern hardware. When looking at alternatives I tried a couple, Sway, Niri, before settling on Hyprland. I looked around and tried to find more experiences of doing the switch that I was planning, but found limited. Therefore I thought I’d share mine, hopefully helpful for someone else.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;setup&quot;&gt;Setup&lt;a class=&quot;zola-anchor&quot; href=&quot;#setup&quot; aria-label=&quot;Anchor link for: setup&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OS&lt;&#x2F;strong&gt;: NixOS (unstable channel, flake-based config)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;GPU&lt;&#x2F;strong&gt;: NVIDIA GTX 1650 (proprietary drivers)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Monitor&lt;&#x2F;strong&gt;: Ultra-wide, variable refresh rate supported&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;VFIO&#x2F;GPU passthrough&lt;&#x2F;strong&gt;: yes, somewhat involved boot&#x2F;kernel config&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;initial-steps-installation&quot;&gt;Initial Steps &amp;amp; Installation&lt;a class=&quot;zola-anchor&quot; href=&quot;#initial-steps-installation&quot; aria-label=&quot;Anchor link for: initial-steps-installation&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The &lt;strong&gt;Hyprland&lt;&#x2F;strong&gt; wiki was well written and easy to follow. For NixOS, you have the choice between the package or pulling the repo as a Flake. To stay current, I opted to use the flake solution.&lt;&#x2F;p&gt;
&lt;p&gt;A few key points:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Environment variables and NVIDIA tweaks are essential (see Hyprland FAQ and wiki).&lt;&#x2F;li&gt;
&lt;li&gt;Use &lt;strong&gt;proprietary NVIDIA driver&lt;&#x2F;strong&gt; for GTX 1650 and similar, the open version is recommended.&lt;&#x2F;li&gt;
&lt;li&gt;For VFIO passthrough, ensure kernel modules for NVIDIA are loaded at the right stage (initrd).&lt;&#x2F;li&gt;
&lt;li&gt;Oddly, you may still need to set the X11 driver to &lt;code&gt;&quot;nvidia&quot;&lt;&#x2F;code&gt; in your NixOS config—even if you’re not using X11 anymore.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;nix&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot;&gt;{
services.xserver.videoDrivers = [ &amp;quot;nvidia&amp;quot; ];
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Debugging involved &lt;code&gt;journalctl&lt;&#x2F;code&gt;, &lt;code&gt;dmesg&lt;&#x2F;code&gt;, and careful reading of &lt;code&gt;nvidia-smi&lt;&#x2F;code&gt; outputs.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;memory-leaks&quot;&gt;Memory leaks!&lt;a class=&quot;zola-anchor&quot; href=&quot;#memory-leaks&quot; aria-label=&quot;Anchor link for: memory-leaks&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;So one issue that became evident a while in was issues with VRAM memory. Hyprland was using up all there was, causing graphical glitches, issues with setting windows to fullscreen. Some debugging and I found this &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;NVIDIA&#x2F;egl-wayland&#x2F;issues&#x2F;126&quot;&gt;issue&lt;&#x2F;a&gt; discussing excessive VRAM usage with Xwayland. Following those instructions it now seems stable.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;a class=&quot;zola-anchor&quot; href=&quot;#final-thoughts&quot; aria-label=&quot;Anchor link for: final-thoughts&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;I’ll see where this goes. These are my notes and impressions just a couple of days in. I hope to return to the topic in a while.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;august-update&quot;&gt;August update&lt;a class=&quot;zola-anchor&quot; href=&quot;#august-update&quot; aria-label=&quot;Anchor link for: august-update&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;I used Hyprland for a couple of months, and the ecosystem. Some plugins were nice. The flash on focus change was in particular nice. I tried with some different tiling methods. Didn’t really find any to my full liking. Qutebrowser kept crashing occasionally. It used quite a lot of VRAM. For many that might not be an issue, but I only have 4GB, and on more than once occasion I had to close windows to free VRAM.&lt;&#x2F;p&gt;
&lt;p&gt;The choice was now between other wayland window managers and going back to i3. I figured I should give Sway another change, and I’m glad I did. I’ve not had any issues what so ever. The fix above regarding memory leaks is still needed. I created a small program that hooks into the sway IPC to create a opacity effect on focus change. Inspired by the one created for Hyprland. For my usage, where I have thin borders and no gaps, it’s sometimes helpful for seeing which is the focused window when switching focus. Feel free to look it up, I’ve precompiled binaries and it’s built using rust so performance implications are minimal. &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;perstarkse&#x2F;sway-focus-flash&quot;&gt;Source&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;figure&gt;
    &lt;img src=&quot;.&amp;#x2F;screenshot.webp&quot; alt=&quot;Sway window manager layout on NixOS with Rust code editor and opacity focus effect&quot;&gt;
    
    &lt;figcaption&gt;Current layout when working on this post, showcasing the opacity effect when switching focus to the main window&lt;&#x2F;figcaption&gt;
    
&lt;&#x2F;figure&gt;
&lt;p&gt;All in all, Hyprland is quite interesting. It has an vast community behind it, of people really interested in improving their workflow and making things look pretty. It still has some quirks and rough edges, and it will be fun to see where it’s at in a while.&lt;&#x2F;p&gt;
</content>
	</entry>
	<entry xml:lang="en">
		<title>Axum Ergonomics - HTML response middleware</title>
		<published>2025-03-15T00:00:00+00:00</published>
		<updated>2025-03-15T00:00:00+00:00</updated>
		<link href="https://blog.stark.pub/posts/axum-ergonomics/" type="text/html"/>
		<id>https://blog.stark.pub/posts/axum-ergonomics/</id>
		<content type="html">&lt;pre&gt;&lt;code&gt;.layer(map_response_with_state(app_state.clone(), with_template_response))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;why&quot;&gt;Why?&lt;a class=&quot;zola-anchor&quot; href=&quot;#why&quot; aria-label=&quot;Anchor link for: why&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Developing a web application using Rust, Axum and Minijinja is &lt;strong&gt;fun&lt;&#x2F;strong&gt; and once the design and groundwork is laid it’s quite easy, maintainable and performant. I’m not in any way an expert, continously finding new ways handle recurring issues and challenges.&lt;&#x2F;p&gt;
&lt;p&gt;Recently I took a gander on refactoring my growing application. With the amount of routes growing I took a look at simplifying the conversion of state into HTML. During this work I came across a quite enjoyable approach which I thought I’d share given my enjoyment of it, and that it it isn’t too widely used, which prompted me writing this post.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;axum-ergonomics&#x2F;map_response_with_state.png&quot; alt=&quot;github search&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;rendering&quot;&gt;Rendering&lt;a class=&quot;zola-anchor&quot; href=&quot;#rendering&quot; aria-label=&quot;Anchor link for: rendering&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;I’m using &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;minijinja&#x2F;latest&#x2F;minijinja&#x2F;index.html&quot;&gt;MiniJinja&lt;&#x2F;a&gt; a templating engine which I highly recommend. I explored a couple (Askama, Tera) before settling on MiniJinja. Simplifying a bit, it uses Serde to serialize values and passes it to the templates which it then renders into HTML, which then the handler returns as a response to the request.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;middleware&quot;&gt;Middleware&lt;a class=&quot;zola-anchor&quot; href=&quot;#middleware&quot; aria-label=&quot;Anchor link for: middleware&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Looking through the Axum documentation there is &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;axum&#x2F;latest&#x2F;axum&#x2F;middleware&#x2F;fn.map_response_with_state.html&quot;&gt;map_response_with_state()&lt;&#x2F;a&gt;, which is a function to create a middleware that transforms the response sent from the handler. This means we can localize all HTML rendering to this middleware.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pub async fn with_template_response(
    State(state): State&amp;lt;HtmlState&amp;gt;,
    response: Response,
) -&amp;gt; Response {
    &amp;#x2F;&amp;#x2F; Clone the TemplateResponse from extensions
    let template_response = response.extensions().get::&amp;lt;TemplateResponse&amp;gt;().cloned();

    if let Some(template_response) = template_response {
        TemplateStateWrapper {
            state,
            template_response,
        }
        .into_response()
    } else {
        response
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Rusts type system makes it easy to check if the type matches our TemplateResponse type, and if it doesn’t we just forward the response and do nothing with it. So in some instances I need to do stuff with the rendered response, like change headers, but for now its quite limited, the current approach allows me to just do .into_response() inside the handler and it will pass through the middleware with limited interference, mostly to see if the response matches the struct.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;error-handling&quot;&gt;Error handling&lt;a class=&quot;zola-anchor&quot; href=&quot;#error-handling&quot; aria-label=&quot;Anchor link for: error-handling&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Implementing error conversion also means we can use ? to propagate errors, and handle them in a fitting way.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;impl From&amp;lt;surrealdb::Error&amp;gt; for HtmlError {
    fn from(err: surrealdb::Error) -&amp;gt; Self {
        HtmlError::AppError(AppError::from(err))
    }
}

impl IntoResponse for HtmlError {
    fn into_response(self) -&amp;gt; Response {
        match self {
            HtmlError::AppError(err) =&amp;gt; {
                let template_response = match err {
                    AppError::NotFound(_) =&amp;gt; TemplateResponse::not_found(),
                    AppError::Auth(_) =&amp;gt; TemplateResponse::unauthorized(),
                    AppError::Validation(msg) =&amp;gt; TemplateResponse::bad_request(&amp;amp;msg),
                    _ =&amp;gt; {
                        tracing::error!(&amp;quot;Internal error: {:?}&amp;quot;, err);
                        TemplateResponse::server_error()
                    }
                };
                template_response.into_response()
            }
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;wrapping-it-all-together&quot;&gt;Wrapping it all together&lt;a class=&quot;zola-anchor&quot; href=&quot;#wrapping-it-all-together&quot; aria-label=&quot;Anchor link for: wrapping-it-all-together&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;This is an example of how a handler could look. Do note that there are other approaches that might be more concise for constructing the context than mine, but I enjoy being a bit strict and having a struct for passing the response, which helps when refactoring, catching errors that might occur when changing the data types returned from helper functions for example.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pub async fn show_text_content_edit_form(
    State(state): State&amp;lt;HtmlState&amp;gt;,
    RequireUser(user): RequireUser,
    Path(id): Path&amp;lt;String&amp;gt;,
) -&amp;gt; Result&amp;lt;impl IntoResponse, HtmlError&amp;gt; {
    let text_content = User::get_and_validate_text_content(&amp;amp;id, &amp;amp;user.id, &amp;amp;state.db).await?;

    #[derive(Serialize)]
    pub struct TextContentEditModalData {
        pub user: User,
        pub text_content: TextContent,
    }

    Ok(TemplateResponse::new_template(
        &amp;quot;content&amp;#x2F;edit_text_content_modal.html&amp;quot;,
        TextContentEditModalData { user, text_content },
    ))
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This post is not designed to be comprehensive or a detailed venture in to the considerations to be made about using it. But for me, it offered a &lt;strong&gt;fun&lt;&#x2F;strong&gt; approach, and I wanted to share it. If you got any questions or corrections to be made, please reach out by email.&lt;&#x2F;p&gt;
</content>
	</entry>
	<entry xml:lang="en">
		<title>Quadratic Voting, and a simple tool for managing surveys</title>
		<published>2025-01-03T00:00:00+00:00</published>
		<updated>2025-01-03T00:00:00+00:00</updated>
		<link href="https://blog.stark.pub/posts/quadratic-voting/" type="text/html"/>
		<id>https://blog.stark.pub/posts/quadratic-voting/</id>
		<content type="html">&lt;h2 id=&quot;what-is-it&quot;&gt;What is it&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-is-it&quot; aria-label=&quot;Anchor link for: what-is-it&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Quadratic voting (QV) is a voting system that allows participants to express how strongly they feel about different options. Unlike traditional voting where each person gets one equal vote, QV lets voters allocate “credits” across different choices, with the cost of additional votes increasing quadratically. For example, casting 3 votes costs 9 credits, while 4 votes costs 16 credits.&lt;&#x2F;p&gt;
&lt;p&gt;This mechanism helps prevent “tyranny of the majority” by giving people a way to show how much they care about specific issues. Voters can choose to spread their influence across many topics or concentrate it on what matters most to them.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;my-usecase&quot;&gt;My usecase&lt;a class=&quot;zola-anchor&quot; href=&quot;#my-usecase&quot; aria-label=&quot;Anchor link for: my-usecase&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;As someone engaged in local politics, I’ve observed how traditional yes&#x2F;no polling can miss important nuances. By implementing QV in my organization’s decision-making process, I believe we can better understand not just what people support, but how strongly they feel about different initiatives. This leads to more informed decisions that better reflect our community’s true preferences.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;history&quot;&gt;History&lt;a class=&quot;zola-anchor&quot; href=&quot;#history&quot; aria-label=&quot;Anchor link for: history&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;QV emerged from early work by Vickrey, Clarke, and Groves on market mechanisms. However, it was E. Glen Weyl at Microsoft Research who developed and named the modern concept of Quadratic Voting. Weyl’s primary motivation was addressing the “tyranny of the majority” problem in democratic systems, where concentrated majorities can systematically benefit at the expense of minorities.&lt;&#x2F;p&gt;
&lt;p&gt;The system has since been tested in various settings, from corporate governance to public policy. Notable implementations include the Colorado House of Representatives’ use of QV to prioritize bills in 2019, and Volt Germany’s application in determining party manifesto priorities.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;nxform&quot;&gt;nxForm&lt;a class=&quot;zola-anchor&quot; href=&quot;#nxform&quot; aria-label=&quot;Anchor link for: nxform&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Since I wanted to try this out for a organization that I am active in which I feel could benefit from the QV approach, I developed nxForm, a web application tailored to meet my specific needs. (Note: nxForm is now internal tooling only. &lt;a href=&quot;mailto:per@stark.pub&quot;&gt;Contact me&lt;&#x2F;a&gt; if you’re interested.)&lt;&#x2F;p&gt;
&lt;p&gt;The primary motivation came from language barriers within our organization - many members are more comfortable with Swedish than English. While several quadratic voting platforms exist, none offered Swedish localization. Additionally, I needed features like QR code sharing for in-person voting sessions and wanted full control over our data. Some existing solutions also had concerning issues, such as invalid SSL certificates.&lt;&#x2F;p&gt;
&lt;p&gt;Beyond solving immediate organizational needs, nxForm was conceived with research potential in mind. I also saw an opportunity to explore how quadratic voting might enhance psychological research, particularly in survey methodology. The ability to measure preference intensity could offer additional insights into human decision-making and group dynamics.&lt;&#x2F;p&gt;
&lt;p&gt;From a technical perspective, nxForm provided an exciting opportunity to work with cutting-edge technologies:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Frontend&lt;&#x2F;strong&gt;: Built with SvelteKit, offering a smooth, responsive user experience&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Backend&lt;&#x2F;strong&gt;: Powered by Rust and the Axum framework, delivering exceptional performance and reliability&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Infrastructure&lt;&#x2F;strong&gt;: Deployed on AWS Lambda for scalability and cost-effectiveness&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The choice of Rust and Axum for the backend proved particularly rewarding and &lt;strong&gt;fun&lt;&#x2F;strong&gt;. The combination offers outstanding ergonomics and developer experience, while maintaining the performance and safety guarantees Rust is known for. This tech stack not only ensures a snappy user experience but also provides the robustness needed for serious research applications.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;quadratic-voting&#x2F;nxform.png&quot; alt=&quot;nxForm&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
	</entry>
	<entry xml:lang="en">
		<title>NixOS, an operating system for me?</title>
		<published>2024-08-24T00:00:00+00:00</published>
		<updated>2024-08-24T00:00:00+00:00</updated>
		<link href="https://blog.stark.pub/posts/nixos-abstractions/" type="text/html"/>
		<id>https://blog.stark.pub/posts/nixos-abstractions/</id>
		<content type="html">&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;a class=&quot;zola-anchor&quot; href=&quot;#intro&quot; aria-label=&quot;Anchor link for: intro&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;For the past year, I’ve been using NixOS for my systems, starting with a VM and eventually including my workstation, work laptop, NAS, and more. All these systems are managed from the same configuration file, accessible on GitHub. During this time, I’ve experienced some frustrations, learned a lot, submitted packages to nixpkgs, and taken over maintenance of some.&lt;&#x2F;p&gt;
&lt;p&gt;What prompted this exploration was the fragility of several systems I’d used before. Even with backups of dotfiles, relevant etc files, and installed packages, restoring a broken system was a tedious process.&lt;&#x2F;p&gt;
&lt;p&gt;When I discovered NixOS with its declarative configurations, it clicked. This post details my current conceptual view on what NixOS is for me, what I enjoy about it, and some frustrations I’ve encountered. If you’re new to Nix or NixOS, I recommend reading the short summary at the bottom of the page.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-is-nixos-for-me&quot;&gt;What is NixOS for Me?&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-is-nixos-for-me&quot; aria-label=&quot;Anchor link for: what-is-nixos-for-me&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;In its completeness, the best way I can describe NixOS (or a subset of its components) is as a wrapper around a Linux system. It abstracts away several aspects of managing a system and services. Setting up a user, OpenSSH, and adding a public SSH key as an authorized key is as simple as adding:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nix&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot;&gt;{
  users.users = {
    p = {
      isNormalUser = true;
      openssh.authorizedKeys.keys = [
        &amp;quot;ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII6uq8nXD+QBMhXqRNywwCa&amp;#x2F;dl2VVvG&amp;#x2F;2nvkw9HEPFzn p@charon&amp;quot;
      ];
    };
  };
  
  services.openssh = {
    enable = true;
    settings.PermitRootLogin = &amp;quot;no&amp;quot;;
    settings.PasswordAuthentication = false;
  };
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;These configurations aren’t hard to set up, and a seasoned Linux user might do them in their sleep. However, having them declared in a configuration file and instantly applied makes things a breeze.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;other-notable-aspects&quot;&gt;Other Notable Aspects&lt;a class=&quot;zola-anchor&quot; href=&quot;#other-notable-aspects&quot; aria-label=&quot;Anchor link for: other-notable-aspects&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;It ensures reproducibility of my systems. Backups are essential for data, but having the system itself be reproducible makes restoring much easier.&lt;&#x2F;li&gt;
&lt;li&gt;It documents changes I’ve made to a system, and with version control, they’re timestamped and reversible.&lt;&#x2F;li&gt;
&lt;li&gt;It offers great developer tooling, for example for setting up project enviroments. In my opinion it offers a substantially better version control than for example nvm for nodejs versions. It’s easy to have environment variables set up and automatically applied with direnv.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;on-having-the-right-amount-of-abstraction&quot;&gt;On Having the Right Amount of Abstraction&lt;a class=&quot;zola-anchor&quot; href=&quot;#on-having-the-right-amount-of-abstraction&quot; aria-label=&quot;Anchor link for: on-having-the-right-amount-of-abstraction&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Abstractions can be great, but they also carry significant drawbacks. Looking at the example above, it’s easy to draw two conclusions: &lt;strong&gt;A&lt;&#x2F;strong&gt;, more can be done with less code and mental effort. I don’t have to remember all the commands, file system locations, and action sequences. &lt;strong&gt;B&lt;&#x2F;strong&gt;, one loses a certain connection to the underlying system. One can, and often should, read the modules&#x2F;nixpkgs to understand what they do. By doing so, one can learn the underlying system without a doubt.&lt;&#x2F;p&gt;
&lt;p&gt;I’ve heard concerns in the r&#x2F;nixos community that using NixOS impedes the learning journey. I agree to a large degree. I think learning this more abstracted way is a shallower learning experience. However, it also enables a wider scope. Most of us have limited time, and tools that enable us to do more are generally good. One can wonder, to what purpose does the learning serve? Being a great sysadmin? If so, perhaps it’s useful to run something else.&lt;&#x2F;p&gt;
&lt;p&gt;As always, key aspects to abstraction are finding the correct level. Too abstracted, and it becomes a hindrance; too little, and it offers small benefits. As of writing, I’m quite pleased with the amount.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;current-pain-points&quot;&gt;Current pain points&lt;a class=&quot;zola-anchor&quot; href=&quot;#current-pain-points&quot; aria-label=&quot;Anchor link for: current-pain-points&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Python projects. Have for me meant a significant issue. There are most likely great solutions, but for me the solutions I’ve opted for have always felt a bit hacky and suboptimal.&lt;&#x2F;li&gt;
&lt;li&gt;Documentation is a bit lacking. My current best workaround is looking through the options and searching github for Nix language files containing what I am looking for. Most time there is someone who has already solved the issue I’m having.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;nix&quot;&gt;Nix&lt;a class=&quot;zola-anchor&quot; href=&quot;#nix&quot; aria-label=&quot;Anchor link for: nix&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Nix is several things:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;A package manager serving the nixpkgs repository containing over 100,000 packages.
&lt;ul&gt;
&lt;li&gt;Nix can be used on various distros, as well as MacOS (nix-darwin).&lt;&#x2F;li&gt;
&lt;li&gt;Nix package manager isolates packages from each other using the “Nix store” concept.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;The language in which NixOS and nixpkgs configuration files are written.
&lt;ul&gt;
&lt;li&gt;The Nix language is a functional programming language allowing for concise and reproducible configuration.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;A build tool enabling reproducible builds of packages and configurations.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Here’s an example of a part of a &lt;code&gt;.nix&lt;&#x2F;code&gt; config:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nix&quot; class=&quot;language-nix &quot;&gt;&lt;code class=&quot;language-nix&quot; data-lang=&quot;nix&quot;&gt;{pkgs, ...}: {
  home.packages = with pkgs; [
    pinentry-gtk2
  ];

  home = {
    username = &amp;quot;p&amp;quot;;
    homeDirectory = &amp;quot;&amp;#x2F;home&amp;#x2F;p&amp;quot;;
  };
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;nixos&quot;&gt;NixOS&lt;a class=&quot;zola-anchor&quot; href=&quot;#nixos&quot; aria-label=&quot;Anchor link for: nixos&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;NixOS is a Linux distribution built on top of Nix, providing:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;A declarative configuration model, where the user describes the desired system configuration.
&lt;ul&gt;
&lt;li&gt;NixOS configuration files define the desired state of the system, which is then built and managed by Nix.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;A set of tools for managing the system configuration, including &lt;code&gt;nixos-rebuild&lt;&#x2F;code&gt; and &lt;code&gt;nixos-config&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;A community-driven repository of configuration modules, allowing users to easily customize their system.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;related-posts&quot;&gt;Related posts&lt;a class=&quot;zola-anchor&quot; href=&quot;#related-posts&quot; aria-label=&quot;Anchor link for: related-posts&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;bundling-onnxruntime-rust-nix&#x2F;&quot;&gt;Bundling ONNX Runtime in Rust with Nix, Docker and GitHub Actions&lt;&#x2F;a&gt; - A practical example of working with the Nix sandbox when a Rust crate wants to download dependencies.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;clan-migration&#x2F;&quot;&gt;Summer project 2025: Migrating to Clan and a Dendritic Architecture&lt;&#x2F;a&gt; - Where my NixOS journey went next: migrating to Clan and flake-parts.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;n150-nixos-router&#x2F;&quot;&gt;N150 NixOS router, a guide&lt;&#x2F;a&gt; - Building a NixOS router from scratch.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
	</entry>
	<entry xml:lang="en">
		<title>Using LLMs for psychological research</title>
		<published>2024-06-10T00:00:00+00:00</published>
		<updated>2025-10-01T00:00:00+00:00</updated>
		<link href="https://blog.stark.pub/posts/psychological-research-using-llms/" type="text/html"/>
		<id>https://blog.stark.pub/posts/psychological-research-using-llms/</id>
		<content type="html">&lt;details open&gt;
  &lt;summary&gt;&lt;span&gt;TLDR&lt;&#x2F;span&gt;&lt;&#x2F;summary&gt;
  &lt;ul&gt;
&lt;li&gt;The author is clinical psychologist who at the time was studying software development&lt;&#x2F;li&gt;
&lt;li&gt;There was a discord group chat with a lot of activity&lt;&#x2F;li&gt;
&lt;li&gt;During early LLM days the author had the idea to do quantitative analysis on text classified chat messages&lt;&#x2F;li&gt;
&lt;li&gt;Results were in line with psychological theory&lt;&#x2F;li&gt;
&lt;li&gt;Shows you could probably use LLMs to track students well being and optimize learning&lt;&#x2F;li&gt;
&lt;li&gt;Probably a lot of ethics considerations tho&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;.&#x2F;2024-per-stark-student-stress-analysis-llm.pdf&quot;&gt;Paper&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;&#x2F;details&gt;
&lt;h2 id=&quot;the-idea-was-formed&quot;&gt;The idea was formed&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-idea-was-formed&quot; aria-label=&quot;Anchor link for: the-idea-was-formed&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;At the time GPT-3.5 became available I was studying fullstack development. I was already somewhat proficient due to self-studies, but I was on parental leave, had the time, thought it could be fun, and some formal merits are good, right? The courses were all remote, and we students came to primarily communicate via discord. Messages per week in the channel ranged from 560-2200 during the first months, so it was quite active.&lt;&#x2F;p&gt;
&lt;p&gt;Early days in the LLM hype era, the capabilities of the models was not evident, and definitely not proven. I was quite fascinated about the possibilities they could enable.&lt;&#x2F;p&gt;
&lt;p&gt;In psychology research, qualitative research is sometimes used, interviews, transcriptions, observations etc. Sometimes you might do labeling and analysis of the content, to be used in a more quantitative way. The idea occurred to me: use LLMs to label&#x2F;analyze chat messages, with regards to more nuanced psychological measures than sentiment for example. We might with the use of analyzed chat messages get real actionable information about student well-being and optimize learning environment.&lt;&#x2F;p&gt;
&lt;details &gt;
  &lt;summary&gt;&lt;span&gt;Sidenote: why do I keep ending up as peer rep?&lt;&#x2F;span&gt;&lt;&#x2F;summary&gt;
  &lt;p&gt;I’m not sure. A bit reluctantly, I keep raising my hand. Looking back, I’ve been a peer rep in elementary school, high school, university, and at work. That pattern—and a steady interest in student well-being—is a big part of why this research question stuck.&lt;&#x2F;p&gt;

&lt;&#x2F;details&gt;
&lt;h2 id=&quot;stress-learning-and-support&quot;&gt;Stress, Learning and Support&lt;a class=&quot;zola-anchor&quot; href=&quot;#stress-learning-and-support&quot; aria-label=&quot;Anchor link for: stress-learning-and-support&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;This section will be a compressed summary on how stress, learning and what psychological factors that relates. For those already knowledgeable about these relationships, skip ahead.&lt;&#x2F;p&gt;
&lt;p&gt;In short, we’ll use two established theories to help us understand. First, being Vygotsky’s theory of zone of proximal development. It states that learning is proceeding at an optimal pace when the demands are just above the comfort level. The second theory is Karasek’s demand-control-support model. Stress and well-being depend on the balance of demand, amount of percieved control of ones situation, and experienced support. High demand with ample control is fine, but high demands with low control leads to stress. Social support can act as a buffer.&lt;&#x2F;p&gt;
&lt;p&gt;So, in this specific context, if the ambition of the educational program is to have students learn and integrate, the demands should be set at an appropriate level, promoting the sense of control and providing support when needed. Sounds easy right?&lt;&#x2F;p&gt;
&lt;h2 id=&quot;method&quot;&gt;Method&lt;a class=&quot;zola-anchor&quot; href=&quot;#method&quot; aria-label=&quot;Anchor link for: method&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;So the idea was: can we use zero shot LLMs (ie no fine-tuning, no specifically trained models, but rather commercially available ones) to analyze discord messages to determine experienced demand, control and support, as well as measures we’d expect to be dependent of those variables, like sentiment?&lt;&#x2F;p&gt;
&lt;p&gt;It would be possible, and one could argue interesting, to look at individual persons in the chat, and follow them longitudinally. But ethics is worth considering, so I opted to remove user identity from each message, and simply send the message content. I also chose to have the analysis done per week, so all messages for each week gets aggregated under one analysis.&lt;&#x2F;p&gt;
&lt;p&gt;After extracting the messages from Discord, the data was cleaned, PII removed, clustered by week, and sent to a LLM for analysis (initially GPT-3.5-turbo, later GPT-4o). The prompt was fairly simple:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Analyze the text and assign scores (0-100) for &amp;#x27;Demand&amp;#x27;, &amp;#x27;Control&amp;#x27;, &amp;#x27;Support&amp;#x27;, &amp;#x27;Sentiment&amp;#x27;, and &amp;#x27;Engagement&amp;#x27; based on the Demand Control Support Model and sentiment analysis. Do your best attempt at interpreting, no excuses! Output the scores in JSON format. No other output is allowed.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;result&quot;&gt;Result&lt;a class=&quot;zola-anchor&quot; href=&quot;#result&quot; aria-label=&quot;Anchor link for: result&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The rest was a matter of statistical analysis.  Please read the paper for more details, but in short:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;There was significant correlation in the expected direction.&lt;&#x2F;li&gt;
&lt;li&gt;Multiple regression analysis showed that the model (Karasek’s) was doing a good job at explaining differences in sentiment (R-squared=0.788).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;figure&gt;
    &lt;img src=&quot;.&amp;#x2F;regression-model.png&quot; alt=&quot;Multiple regression analysis results showing the relationship between demand, control, and support with student sentiment, processed via GPT-3.5&quot;&gt;
    
    &lt;figcaption&gt;Multiple regression analysis&lt;&#x2F;figcaption&gt;
    
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;so-what-to-take-from-it&quot;&gt;So, what to take from it?&lt;a class=&quot;zola-anchor&quot; href=&quot;#so-what-to-take-from-it&quot; aria-label=&quot;Anchor link for: so-what-to-take-from-it&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Several things:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Convergent validity of the findings with the demand-control-support model leads us to believe that the LLM is doing a decent enough job of analyzing large amounts of chat data, most of it in swedish.&lt;&#x2F;li&gt;
&lt;li&gt;Monitoring chat like this can give an aggregate measure of information pertaining to psychological variables of interest for educational settings.&lt;&#x2F;li&gt;
&lt;li&gt;Use of LLMs in research have a way to go in regards of being validated, but in comparison to manual labeling and rating, which has its own issues with validity, its seemingly a valid tool which opens up new research possibilities.&lt;&#x2F;li&gt;
&lt;li&gt;Monitoring via chat holds the potential for some frightening situations, the possibility existed beforehand of course, but with the enormous cost decreases in comparison to doing it manually, its significantly more likely to happen in the future.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;looking-back&quot;&gt;Looking back&lt;a class=&quot;zola-anchor&quot; href=&quot;#looking-back&quot; aria-label=&quot;Anchor link for: looking-back&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;It was a fun project, at a certain point in time where using LLMs like this was really quite new. For me personally it offered a glimpse into what could possibly be done at a reasonable cost. It would have been totally possible to do the same study without LLMs, but enormously costly considering the amount of messages. I think I haven’t, and possibly, many others too, fully comprehended the amount of insight that could be gotten on a persons inner functioning by observation and monitoring, at least not in a fully automated way.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;related-posts&quot;&gt;Related posts&lt;a class=&quot;zola-anchor&quot; href=&quot;#related-posts&quot; aria-label=&quot;Anchor link for: related-posts&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;eval-retrieval-refactor&#x2F;&quot;&gt;Refactoring a retrieval pipeline through evaluation&lt;&#x2F;a&gt; - Another dive into using machine learning for practical applications, this time optimizing retrieval in my knowledge management system.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
	</entry>
	<entry xml:lang="en">
		<title>Zen and the art of ethereum development, the BuidlGuidl way</title>
		<published>2024-05-09T00:00:00+00:00</published>
		<updated>2024-05-09T00:00:00+00:00</updated>
		<link href="https://blog.stark.pub/posts/speedrun-ethereum/" type="text/html"/>
		<id>https://blog.stark.pub/posts/speedrun-ethereum/</id>
		<content type="html">&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;a class=&quot;zola-anchor&quot; href=&quot;#intro&quot; aria-label=&quot;Anchor link for: intro&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;BuidlGuidl is a curated group of Ethereum builders creating products, prototypes, and tutorials to enrich the web3 ecosystem. Providing video tutorials, newsletters and coordination tools.&lt;&#x2F;p&gt;
&lt;p&gt;They also maintain a full stack solution for developing great dapps with the &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;scaffold-eth&#x2F;scaffold-eth-2&quot;&gt;Scaffold-ETH 2 toolkit&lt;&#x2F;a&gt;. A solution that I highly recommend for getting going and prototyping.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;speedrunethereum.com&#x2F;&quot;&gt;Speedrun Ethereum&lt;&#x2F;a&gt; is an intensive, hands-on program designed to help developers quickly learn and master the skills needed to build decentralized applications (dApps) on the Ethereum blockchain. Created by the team at BuidlGuidl, this program is known for its practical, project-based approach to Ethereum development.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;a class=&quot;zola-anchor&quot; href=&quot;#prerequisites&quot; aria-label=&quot;Anchor link for: prerequisites&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;To get the most out of the Speedrun Ethereum program, it’s recommended that you have a basic understanding of the following:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;JavaScript or TypeScript&lt;&#x2F;li&gt;
&lt;li&gt;Solidity (the programming language for Ethereum smart contracts)&lt;&#x2F;li&gt;
&lt;li&gt;Some familiarity with tools like Hardhat and ethers.js, which are commonly used in Ethereum development&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;speed-run-ethereum&quot;&gt;Speed Run Ethereum&lt;a class=&quot;zola-anchor&quot; href=&quot;#speed-run-ethereum&quot; aria-label=&quot;Anchor link for: speed-run-ethereum&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The programme contains 8 challenges in total, providing opportunities to explore state channels, multisig wallets, SVG NFTs and more. The tooling provided makes the overhead minimal, improving the chance of actually learning the essential skills.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;simple-nft-example-challenge-0&quot;&gt;Simple NFT Example, Challenge #0&lt;a class=&quot;zola-anchor&quot; href=&quot;#simple-nft-example-challenge-0&quot; aria-label=&quot;Anchor link for: simple-nft-example-challenge-0&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;🎫 Create a simple NFT to learn basics of 🏗 scaffold-eth. You’ll use 👷‍♀️ HardHat to compile and deploy smart contracts. Then, you’ll use a template React app full of important Ethereum components and hooks. Finally, you’ll deploy an NFT to a public network to share with friends! 🚀&lt;&#x2F;p&gt;
&lt;h3 id=&quot;decentralized-staking-app-challenge-1&quot;&gt;Decentralized Staking App, Challenge #1&lt;a class=&quot;zola-anchor&quot; href=&quot;#decentralized-staking-app-challenge-1&quot; aria-label=&quot;Anchor link for: decentralized-staking-app-challenge-1&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;🦸 A superpower of Ethereum is allowing you, the builder, to create a simple set of rules that an adversarial group of players can use to work together. In this challenge, you create a decentralized application where users can coordinate a group funding effort. The users only have to trust the code.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;token-vendor-challenge-2&quot;&gt;Token Vendor, Challenge #2&lt;a class=&quot;zola-anchor&quot; href=&quot;#token-vendor-challenge-2&quot; aria-label=&quot;Anchor link for: token-vendor-challenge-2&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;🤖 Smart contracts are kind of like “always on” vending machines that anyone can access. Let’s make a decentralized, digital currency (an ERC20 token). Then, let’s build an unstoppable vending machine that will buy and sell the currency. We’ll learn about the “approve” pattern for ERC20s and how contract to contract interactions work.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;join-buidlguidl&quot;&gt;Join BuidlGuidl&lt;a class=&quot;zola-anchor&quot; href=&quot;#join-buidlguidl&quot; aria-label=&quot;Anchor link for: join-buidlguidl&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;When you’ve completed the first three challenges you have the option to join the BuidlGuidl.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;build-a-dex-challenge-4&quot;&gt;Build a DEX, challenge #4&lt;a class=&quot;zola-anchor&quot; href=&quot;#build-a-dex-challenge-4&quot; aria-label=&quot;Anchor link for: build-a-dex-challenge-4&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;The challenge is to build a simple DEX solution, where the user can seamlessly trade a ERC20 token with ETH. You will implement the pricing logic &lt;code&gt;x * y = k&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;state-channel-application-challenge-5&quot;&gt;State Channel Application, challenge #5&lt;a class=&quot;zola-anchor&quot; href=&quot;#state-channel-application-challenge-5&quot; aria-label=&quot;Anchor link for: state-channel-application-challenge-5&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;This challenge was really quite interesting. In this challenge, you’ll build a state channel application that enables users to exchange value-for-service at high frequency. You’ll create a contract that collects ETH from clients and keeps track of balances, implement off-chain service exchange between the contract owner and clients, and add a challenge mechanism with a timeout to protect clients.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cohort-4&quot;&gt;Cohort 4&lt;a class=&quot;zola-anchor&quot; href=&quot;#cohort-4&quot; aria-label=&quot;Anchor link for: cohort-4&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;I recently recieved an invitation to join the fourth batch of the BG batch program. The github repo for the shared project is &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;BuidlGuidl&#x2F;batch4.buidlguidl.com&#x2F;tree&#x2F;main&quot;&gt;here&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;It involved checking in to the deployed contract using a own deployed proxy contract, setting up a socials page and contributing to what you want. I chose to be helpful with reviews, comments and implementing a graph plotting when the builders checked in to the smart contract.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;speedrun-ethereum&#x2F;buidlguidl-batch-4.png&quot; alt=&quot;Chart&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The experience was very pleasant, people were helpful and encouraging. 10&#x2F;10 🚀&lt;&#x2F;p&gt;
&lt;h2 id=&quot;final-project&quot;&gt;Final project&lt;a class=&quot;zola-anchor&quot; href=&quot;#final-project&quot; aria-label=&quot;Anchor link for: final-project&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The wonderful people at buidlguidl have set up a grants solution, where you can apply with an idea and get funding which I generously was given for my idea. What I chose to develop was a community building experience. The users submit inspirations to a contract and the application generates a story from these inspirations which is then presented in the application together with appropriate artwork. Relying on a smart contract for record keeping means there is a great immutable version control and a timeline on how the story has evolved can be shown. Feel free to fork or inspect if you want too, &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;perstarkse&#x2F;lets-create-a-story&quot;&gt;github&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;speedrun-ethereum&#x2F;buidlguidl-project-timeline.gif&quot; alt=&quot;timeline&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
	</entry>
	<entry xml:lang="en">
		<title>pass - A Self-Hosted, Secure, and CLI-Friendly Password Manager</title>
		<published>2024-04-27T00:00:00+00:00</published>
		<updated>2024-04-27T00:00:00+00:00</updated>
		<link href="https://blog.stark.pub/posts/gpg-pass/" type="text/html"/>
		<id>https://blog.stark.pub/posts/gpg-pass/</id>
		<content type="html">&lt;p&gt;As a long-time user of 1Password, I’ve grown accustomed to the convenience and security it provides. However, I’ve always been disappointed with the CLI experience. The command-line interface is clunky and limited, making it difficult to manage my passwords efficiently. That’s why I decided to explore alternative solutions, and I’m excited to share my experience with &lt;code&gt;pass&lt;&#x2F;code&gt;, a self-hosted, secure, and CLI-friendly password manager.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;a class=&quot;zola-anchor&quot; href=&quot;#prerequisites&quot; aria-label=&quot;Anchor link for: prerequisites&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Before we dive into setting up &lt;code&gt;pass&lt;&#x2F;code&gt;, you’ll need a few prerequisites:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;GPG key&lt;&#x2F;strong&gt;: You’ll need a GPG key to encrypt your password store. I highly recommend using &lt;code&gt;gpg-tui&lt;&#x2F;code&gt; for managing your GPG keys. It’s a user-friendly, terminal-based interface that makes it easy to generate and manage your keys.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;gnupg&lt;&#x2F;strong&gt;: You’ll also need &lt;code&gt;gnupg&lt;&#x2F;code&gt; installed on your system. This is a requirement for &lt;code&gt;pass&lt;&#x2F;code&gt; to function properly.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;installing-pass&quot;&gt;Installing pass&lt;a class=&quot;zola-anchor&quot; href=&quot;#installing-pass&quot; aria-label=&quot;Anchor link for: installing-pass&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Installing &lt;code&gt;pass&lt;&#x2F;code&gt; is a breeze. You can use your favorite package manager to install it. If you’re using NixOS, you can use &lt;code&gt;nix-shell -p pass&lt;&#x2F;code&gt; to create a temporary “install” environment.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;initializing-the-password-store&quot;&gt;Initializing the Password Store&lt;a class=&quot;zola-anchor&quot; href=&quot;#initializing-the-password-store&quot; aria-label=&quot;Anchor link for: initializing-the-password-store&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Once &lt;code&gt;pass&lt;&#x2F;code&gt; is installed, you can initialize the password store using the following command:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pass init
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This will create a new password store in the default location &lt;code&gt;~&#x2F;.password-store&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;setting-up-the-git-repo&quot;&gt;Setting up the Git Repo&lt;a class=&quot;zola-anchor&quot; href=&quot;#setting-up-the-git-repo&quot; aria-label=&quot;Anchor link for: setting-up-the-git-repo&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;To store your passwords securely, you’ll need to set up a Git repository. You can do this using the following command:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pass git init
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This will create a new Git repository in the &lt;code&gt;~&#x2F;.password-store&lt;&#x2F;code&gt; directory.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;setting-up-the-remote-tracking-repo&quot;&gt;Setting up the Remote Tracking Repo&lt;a class=&quot;zola-anchor&quot; href=&quot;#setting-up-the-remote-tracking-repo&quot; aria-label=&quot;Anchor link for: setting-up-the-remote-tracking-repo&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;To ensure that your password store is backed up and accessible across multiple devices, you’ll need to set up a remote tracking repository. I recommend using a private GitHub repository for this purpose.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;generating-passwords&quot;&gt;Generating Passwords&lt;a class=&quot;zola-anchor&quot; href=&quot;#generating-passwords&quot; aria-label=&quot;Anchor link for: generating-passwords&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;pass&lt;&#x2F;code&gt; comes with a built-in password generator that can generate strong, unique passwords for you. You can use the following command to generate a new password:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pass generate
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;inserting-passwords-with-multiline-support&quot;&gt;Inserting Passwords with Multiline Support&lt;a class=&quot;zola-anchor&quot; href=&quot;#inserting-passwords-with-multiline-support&quot; aria-label=&quot;Anchor link for: inserting-passwords-with-multiline-support&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;One of the most powerful features of &lt;code&gt;pass&lt;&#x2F;code&gt; is its support for multiline inserts. This allows you to add not only passwords but also additional information such as usernames, notes, and more. To insert a new password with multiline support, you can use the following command:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pass insert multiline
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This will open a text editor where you can enter your password and additional information in a formatted way. For example:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Username: myusername
Password: mypassword
URL: https:&amp;#x2F;&amp;#x2F;example.com
Notes: This is a note about the password
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This makes it easy to store and manage complex password entries with additional information.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;using-rofi-pass&quot;&gt;Using rofi-pass&lt;a class=&quot;zola-anchor&quot; href=&quot;#using-rofi-pass&quot; aria-label=&quot;Anchor link for: using-rofi-pass&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;rofi-pass&lt;&#x2F;code&gt; is a powerful plugin that allows you to search and manage your passwords using the &lt;code&gt;rofi&lt;&#x2F;code&gt; fuzzy finder. To use &lt;code&gt;rofi-pass&lt;&#x2F;code&gt;, simply execute the following command:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;rofi-pass
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This will open a &lt;code&gt;rofi&lt;&#x2F;code&gt; interface where you can search and manage your passwords.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;using-pass-otp&quot;&gt;Using pass-otp&lt;a class=&quot;zola-anchor&quot; href=&quot;#using-pass-otp&quot; aria-label=&quot;Anchor link for: using-pass-otp&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;pass-otp&lt;&#x2F;code&gt; is a plugin that allows you to manage one-time passwords (OTPs) using &lt;code&gt;pass&lt;&#x2F;code&gt;. One of the most useful features of &lt;code&gt;pass-otp&lt;&#x2F;code&gt; is the ability to scan QR codes and add OTPs to your password store. You can use the following command to scan a QR code and add an OTP to your password store:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;xclip -selection clipboard -t image&amp;#x2F;png -o - | zbarimg -q --raw - | pass otp append &amp;lt;your_entry&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This command uses &lt;code&gt;xclip&lt;&#x2F;code&gt; to capture the QR code from the clipboard, &lt;code&gt;zbarimg&lt;&#x2F;code&gt; to decode the QR code, and &lt;code&gt;pass otp&lt;&#x2F;code&gt; to add the OTP to your password store.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pass-for-ios&quot;&gt;pass for iOS&lt;a class=&quot;zola-anchor&quot; href=&quot;#pass-for-ios&quot; aria-label=&quot;Anchor link for: pass-for-ios&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;If you’re an iOS user, you’ll be happy to know that there’s a &lt;code&gt;pass&lt;&#x2F;code&gt; app available for &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mssun&#x2F;passforios&quot;&gt;iOS devices&lt;&#x2F;a&gt;. The app allows you to access your password store on the go, with a simple and intuitive interface. You can search, view, and copy passwords, as well as generate new passwords using the app’s built-in password generator.&lt;&#x2F;p&gt;
&lt;p&gt;To get started with &lt;code&gt;pass for iOS&lt;&#x2F;code&gt;, simply download the app from the App Store and follow the setup instructions. You’ll need to configure the app to connect to your password store, which can be done using the app’s built-in Git integration. So far from my own use I’m very pleased, it handles OTP codes great.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;a class=&quot;zola-anchor&quot; href=&quot;#conclusion&quot; aria-label=&quot;Anchor link for: conclusion&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;In this post, we’ve covered the basics of setting up &lt;code&gt;pass&lt;&#x2F;code&gt;, a self-hosted, secure, and CLI-friendly password manager. With &lt;code&gt;pass&lt;&#x2F;code&gt;, you can manage your passwords efficiently and securely, using a command-line interface that’s both powerful and intuitive. The multiline insert feature, &lt;code&gt;rofi-pass&lt;&#x2F;code&gt; plugin, and &lt;code&gt;pass-otp&lt;&#x2F;code&gt; plugin make it easy to manage complex password entries, search and manage your passwords, and add OTPs to your password store. I hope this guide has been helpful in getting you started with &lt;code&gt;pass&lt;&#x2F;code&gt;. Happy password managing!&lt;&#x2F;p&gt;
</content>
	</entry>
	<entry xml:lang="en">
		<title>Writing PDFs with Markdown</title>
		<published>2024-03-26T00:00:00+00:00</published>
		<updated>2025-10-07T00:00:00+00:00</updated>
		<link href="https://blog.stark.pub/posts/markdown-to-pdf/" type="text/html"/>
		<id>https://blog.stark.pub/posts/markdown-to-pdf/</id>
		<content type="html">
&lt;blockquote class=&quot;callout tip has-title&quot;&gt;
  
  
  &lt;p class=&quot;title&quot;&gt;
    &lt;span class=&quot;icon&quot;&gt;
      &lt;svg xmlns=&quot;http:&#x2F;&#x2F;www.w3.org&#x2F;2000&#x2F;svg&quot; viewBox=&quot;0 0 24 24&quot; width=&quot;18&quot; height=&quot;18&quot;&gt;&lt;path d=&quot;M9.97308 18H11V13H13V18H14.0269C14.1589 16.7984 14.7721 15.8065 15.7676 14.7226C15.8797 14.6006 16.5988 13.8564 16.6841 13.7501C17.5318 12.6931 18 11.385 18 10C18 6.68629 15.3137 4 12 4C8.68629 4 6 6.68629 6 10C6 11.3843 6.46774 12.6917 7.31462 13.7484C7.40004 13.855 8.12081 14.6012 8.23154 14.7218C9.22766 15.8064 9.84103 16.7984 9.97308 18ZM10 20V21H14V20H10ZM5.75395 14.9992C4.65645 13.6297 4 11.8915 4 10C4 5.58172 7.58172 2 12 2C16.4183 2 20 5.58172 20 10C20 11.8925 19.3428 13.6315 18.2443 15.0014C17.624 15.7748 16 17 16 18.5V21C16 22.1046 15.1046 23 14 23H10C8.89543 23 8 22.1046 8 21V18.5C8 17 6.37458 15.7736 5.75395 14.9992Z&quot; fill=&quot;currentColor&quot;&gt;&lt;&#x2F;path&gt;&lt;&#x2F;svg&gt;

    &lt;&#x2F;span&gt;
    &lt;strong&gt;Typst is probably a great alternative&lt;&#x2F;strong&gt;
  &lt;&#x2F;p&gt;
  &lt;div class=&quot;content&quot;&gt;
    &lt;p&gt;Since a while back I’ve started using typst instead of pandoc. I have not fully explored if it can do all the following things in this post, but I highly suspect it. No real dependencies, great syntax and lots of community behind it.&lt;&#x2F;p&gt;

  &lt;&#x2F;div&gt;
  
&lt;&#x2F;blockquote&gt;

&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;a class=&quot;zola-anchor&quot; href=&quot;#intro&quot; aria-label=&quot;Anchor link for: intro&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Writing documents using Microsoft Office, Google Docs, Open Office etc, has in my opinion not provided the best UX experience. Especially not if you are used to mainly using the keyboard to navigate and produce. For much of my writing I just need sensible defaults and not worrying about formatting.
Using &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Markdown&quot;&gt;Markdown&lt;&#x2F;a&gt; and &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;pandoc.org&#x2F;&quot;&gt;Pandoc&lt;&#x2F;a&gt; you can write documents in Markdown in your favorite text editor, mine is &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;helix-editor.com&#x2F;&quot;&gt;Helix&lt;&#x2F;a&gt; and end up with great looking PDFs, HTML, EPUB amongst others.&lt;&#x2F;p&gt;
&lt;p&gt;I have used it to great extent to write academic papers, notes, cover letters amongst others. The image below is a snippet of this post when it was in writing.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;markdown-to-pdf&#x2F;markdown-helix-snippet.png&quot; alt=&quot;Snippet of the current post in writing&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;a class=&quot;zola-anchor&quot; href=&quot;#prerequisites&quot; aria-label=&quot;Anchor link for: prerequisites&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Text editor of choice, prefferably with Markdown syntax support.&lt;&#x2F;li&gt;
&lt;li&gt;Pandoc installed. Install on NixOS in shell with:&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;$ nix-shell -p pandoc texlive.combined.scheme-small&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;a class=&quot;zola-anchor&quot; href=&quot;#usage&quot; aria-label=&quot;Anchor link for: usage&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Write your content in a .md file, and when you are ready to create a PDF from your .md, then run:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;$ pandoc your_document.md -o your_pdf.pdf&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;It will then parse the markdown and ouput a formatted PDF.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;markdown-to-pdf&#x2F;markdown-pdf-snippet.png&quot; alt=&quot;Snippet of the current post as PDF&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;table-of-content&quot;&gt;Table of Content&lt;a class=&quot;zola-anchor&quot; href=&quot;#table-of-content&quot; aria-label=&quot;Anchor link for: table-of-content&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Adding a Table of Content is as simple as adding –toc to the command, you can also have the sections automatically numbered using –number-sections.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;$ pandoc your_document.md -o your_pdf.pdf --toc --number-sections&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;markdown-to-pdf&#x2F;markdown-number-sections.png&quot; alt=&quot;Snippet of the TOC&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;a class=&quot;zola-anchor&quot; href=&quot;#references&quot; aria-label=&quot;Anchor link for: references&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Pandoc can also make handling references a breeze. Both footnotes, references to charts and images, as well as creating a reference sheet.&lt;&#x2F;p&gt;
&lt;p&gt;To do this you also need pandoc-crossref installed. To install with pandoc in shell on NixOS:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;$ nix-shell -p pandoc texlive.combined.scheme-small haskellPackages.pandoc-crossref&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;To handle the references we need to create and specify a bibliography file, we do this by inserting &lt;code&gt;bibliography: references.bib&lt;&#x2F;code&gt; to the top of the file.&lt;&#x2F;p&gt;
&lt;p&gt;It will now look something like so:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;---
title: your_title
date: 26&amp;#x2F;03&amp;#x2F;2024
bibliography: references.bib
---
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The references.bib file being a standard BibTeX file, making it easy to import references. You can then cite from text by writing &lt;code&gt;[@reference]&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;To generate the PDF with citations and references, run:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;$ pandoc your_document.md -o your_pdf.pdf --toc --number-sections --filter pandoc-crossref --citeproc&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;It will both replace &lt;code&gt;[@reference]&lt;&#x2F;code&gt; with the information in the bibliography, as well as create a reference list at the end of the document.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;charts&quot;&gt;Charts&lt;a class=&quot;zola-anchor&quot; href=&quot;#charts&quot; aria-label=&quot;Anchor link for: charts&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Using pandoc-plot you can generate plots directly from your markdown code using Python, R, ect. To install with the other libraries run:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;$ nix-shell -p pandoc texlive.combined.scheme-small haskellPackages.pandoc-crossref haskelPackages.pandoc-plot&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;You will also need a python environment with matplotlib and numpy to generate the plots in the following code.&lt;&#x2F;p&gt;
&lt;p&gt;You can then insert code like the following:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;import json
import matplotlib.pyplot as plt

with open(&amp;quot;sentiment_data.json&amp;quot;) as file:
    data = json.load(file)

html_css_data = data[&amp;quot;html_css_data&amp;quot;]
javascript_frontend_data = data[&amp;quot;javascript_frontend_data&amp;quot;]
javascript_backend_data = data[&amp;quot;javascript_backend_data&amp;quot;]
c_sharp_data = data[&amp;quot;c_sharp_data&amp;quot;]
datalagring_data = data[&amp;quot;datalagring_data&amp;quot;]
asp_net_data = data[&amp;quot;asp_net_data&amp;quot;]

datasets = [
    (&amp;quot;HTML&amp;#x2F;CSS&amp;quot;, html_css_data, &amp;quot;#1f77b4&amp;quot;),
    (&amp;quot;JavaScript Frontend&amp;quot;, javascript_frontend_data, &amp;quot;#ff7f0e&amp;quot;),
    (&amp;quot;JavaScript Backend&amp;quot;, javascript_backend_data, &amp;quot;#2ca02c&amp;quot;),
    (&amp;quot;C#&amp;quot;, c_sharp_data, &amp;quot;#d62728&amp;quot;),
    (&amp;quot;Datalagring&amp;quot;, datalagring_data, &amp;quot;#9467bd&amp;quot;),
    (&amp;quot;ASP.NET&amp;quot;, asp_net_data, &amp;quot;#8c564b&amp;quot;),
]

fig, ax1 = plt.subplots(figsize=(10, 5))

for name, data, color in datasets:
    x = [d[&amp;quot;x&amp;quot;] for d in data]
    y = [d[&amp;quot;y&amp;quot;] for d in data]
    ax1.plot(x, y, marker=&amp;quot;o&amp;quot;, linestyle=&amp;quot;-&amp;quot;, color=color, label=name, alpha=0.7)
    ax1.plot(x, y, linestyle=&amp;quot;-&amp;quot;, color=color, alpha=0.3, linewidth=4)

ax1.set_xlabel(&amp;quot;Week&amp;quot;)
ax1.set_ylabel(&amp;quot;Sentiment&amp;quot;)
ax1.set_title(&amp;quot;Sentiment over Time&amp;quot;)
ax1.grid(True)
ax1.legend(loc=&amp;quot;upper left&amp;quot;)

plt.setp(ax1.get_xticklabels(), rotation=45, ha=&amp;quot;right&amp;quot;, rotation_mode=&amp;quot;anchor&amp;quot;)

plt.tight_layout()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And run with:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;$ pandoc your_document.md -o your_pdf.pdf --toc --number-sections --filter pandoc-plot --filter pandoc-crossref --citeproc&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;This code is from a recent project of mine. Which resulted in this plot:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;blog.stark.pub&#x2F;posts&#x2F;markdown-to-pdf&#x2F;sentimentTime.png&quot; alt=&quot;Plot example&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;csl-style&quot;&gt;CSL style&lt;a class=&quot;zola-anchor&quot; href=&quot;#csl-style&quot; aria-label=&quot;Anchor link for: csl-style&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;You can of course change the style of the citations generated. There is a large &lt;a rel=&quot;noreferrer&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;citation-style-language&#x2F;styles&quot;&gt;repository&lt;&#x2F;a&gt; of styles availible. To use, download the .csl file and place it in the base directory, then you can run:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;$ pandoc your_document.md -o your_pdf.pdf --toc --number-sections --csl=&quot;your_csl.csl&quot; --filter pandoc-plot --filter pandoc-crossref --citeproc&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;a class=&quot;zola-anchor&quot; href=&quot;#summary&quot; aria-label=&quot;Anchor link for: summary&quot; style=&quot;visibility: hidden;&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The end result is a PDF that’s nicely formatted, with a table of contents, citations, charts and a reference list.&lt;&#x2F;p&gt;
&lt;p&gt;I personally feel that I’ve just so far scratched the surface of what’s possible to do with Pandoc. But so far I’m very satisfied, this abstracts away a lot of the unpleasurable things with writing.&lt;&#x2F;p&gt;
</content>
	</entry>
</feed>