<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ᗩᒪᕮ᙭ ᑕᗩᗰᗩᑕᕼO ᑕᗩSTIᒪᕼO</title>
    <description>The latest articles on DEV Community by ᗩᒪᕮ᙭ ᑕᗩᗰᗩᑕᕼO ᑕᗩSTIᒪᕼO (@alexccastilho).</description>
    <link>https://dev.to/alexccastilho</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4108962%2Fc770b3e8-973d-4cb9-8770-b11156e85575.jpg</url>
      <title>DEV Community: ᗩᒪᕮ᙭ ᑕᗩᗰᗩᑕᕼO ᑕᗩSTIᒪᕼO</title>
      <link>https://dev.to/alexccastilho</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alexccastilho"/>
    <language>en</language>
    <item>
      <title>I built an offline document indexer, and Ollama taught me two things I did not expect</title>
      <dc:creator>ᗩᒪᕮ᙭ ᑕᗩᗰᗩᑕᕼO ᑕᗩSTIᒪᕼO</dc:creator>
      <pubDate>Fri, 04 Sep 2026 03:28:41 +0000</pubDate>
      <link>https://dev.to/alexccastilho/i-built-an-offline-document-indexer-and-ollama-taught-me-two-things-i-did-not-expect-4ho5</link>
      <guid>https://dev.to/alexccastilho/i-built-an-offline-document-indexer-and-ollama-taught-me-two-things-i-did-not-expect-4ho5</guid>
      <description>&lt;p&gt;I had a folder with a few hundred scanned documents I could not upload anywhere. Confidential, mostly images of paper with no text layer, useless for search. Opening them one by one was the only way to find anything.&lt;/p&gt;

&lt;p&gt;So I built the step that comes before the AI assistant, rather than trying to replace it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;You point it at a folder. It scans, runs OCR only on the pages that have no text layer (it checks per file and skips the rest), slices oversized PDFs, reads every page, and classifies each one into an item with a type, a date where one exists, an author and a summary.&lt;/p&gt;

&lt;p&gt;Then it writes four Markdown files next to your originals: an index of every item, a chronological timeline, a review report naming every gap and failure it hit, and a set of instructions ready to paste into an AI project. The originals are never touched, everything lands in a separate output folder.&lt;/p&gt;

&lt;p&gt;Classification runs on an open model through Ollama, on the machine itself. The host is fixed to 127.0.0.1 in code and never read from configuration, which was the property I actually needed. There is also a deterministic rules engine that uses no model at all, for machines that cannot run one.&lt;/p&gt;

&lt;p&gt;Stack is Python 3.12, FastAPI, HTMX, SQLite and Ollama. 456 tests, CI on Windows, interface in English, Portuguese and Spanish. GPL-3.&lt;/p&gt;

&lt;p&gt;Two things surprised me while building it, and they are probably more useful to you than the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;num_gpu: -1&lt;/code&gt; does not mean "use all the GPU"
&lt;/h2&gt;

&lt;p&gt;I had a card with several GB of VRAM free while the model ran at CPU speed for the layers that had spilled into RAM. I was passing &lt;code&gt;options.num_gpu = -1&lt;/code&gt;, which reads like "use as much GPU as possible".&lt;/p&gt;

&lt;p&gt;It does not do that. It hands the decision to Ollama's own scheduler, which is deliberately conservative. It sizes the KV cache for &lt;code&gt;OLLAMA_NUM_PARALLEL&lt;/code&gt; concurrent requests, four by default on the versions I tested, which is four times the cache a single threaded classifier will ever use. Then it keeps a safety margin on top and rounds down to a whole number of layers.&lt;/p&gt;

&lt;p&gt;Two independent things fixed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server settings, not request options.&lt;/strong&gt; &lt;code&gt;num_gpu&lt;/code&gt; is per request, but the sizes that decide how much fits are per &lt;em&gt;server&lt;/em&gt; environment variables, read once by &lt;code&gt;ollama serve&lt;/code&gt; at startup. One loaded model, one parallel slot, flash attention on and a quantised KV cache freed enough VRAM for several more layers, with no effect on output. The cache holds attention state, not the answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compute the layer count yourself.&lt;/strong&gt; Read &lt;code&gt;block_count&lt;/code&gt; and &lt;code&gt;embedding_length&lt;/code&gt; from the model metadata, work out the per layer cost at your context size, and pass an explicit &lt;code&gt;num_gpu&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One trap that cost me an afternoon: measure free VRAM with nothing loaded. If you measure while the model you are sizing is already resident, you budget against a number that already includes it. You ask for fewer layers than fit, Ollama reloads smaller, less is free next time, and you ratchet a model that fit right off the GPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The bigger model lost
&lt;/h2&gt;

&lt;p&gt;I benchmarked five models over the same windows of a 31 page document, every one running fully on an 8 GB card:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Type field filled&lt;/th&gt;
&lt;th&gt;s/window&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;qwen3.5:4b&lt;/td&gt;
&lt;td&gt;3.0 GB&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;30.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemma4:e4b&lt;/td&gt;
&lt;td&gt;3.1 GB&lt;/td&gt;
&lt;td&gt;79.5%&lt;/td&gt;
&lt;td&gt;38.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3.5:9b&lt;/td&gt;
&lt;td&gt;5.3 GB&lt;/td&gt;
&lt;td&gt;79.5%&lt;/td&gt;
&lt;td&gt;86.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3:8b&lt;/td&gt;
&lt;td&gt;5.4 GB&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;108.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;granite4.2:8b&lt;/td&gt;
&lt;td&gt;5.7 GB&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;116.9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 4b beat the 9b of its own family, on quality and on speed at the same time. Describing a page is reading and format discipline, not deep reasoning, so size buys nothing for this task and costs a lot of time. It is the default now.&lt;/p&gt;

&lt;p&gt;There was a related bug worth mentioning. Models with a thinking channel dump the whole answer into &lt;code&gt;thinking&lt;/code&gt; and return an empty &lt;code&gt;response&lt;/code&gt; when you do not explicitly disable it. My code only read &lt;code&gt;response&lt;/code&gt;, so every window came back with zero items. Sending &lt;code&gt;think: false&lt;/code&gt; fixed it, and reading &lt;code&gt;thinking&lt;/code&gt; as a fallback covers the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am asking for
&lt;/h2&gt;

&lt;p&gt;People running it on hardware that is not mine. It was built and tested on essentially one machine, so I am certain things break on other GPUs, other Windows versions and file formats that never passed through here. An issue, a PR, or just a note saying what went wrong all help.&lt;/p&gt;

&lt;p&gt;Two limits, so nobody wastes an afternoon. It is Windows only and structurally so, since it reads WMI, the registry and Performance Counters directly. And the quality score it reports measures the engine's declared self confidence and how completely it filled the fields, not whether it was right. An engine can score 100 and still misfile a document. That is written into the code that computes the score, not just the README.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/alexccastilho/gclaude-indexer" rel="noopener noreferrer"&gt;https://github.com/alexccastilho/gclaude-indexer&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ollama</category>
      <category>python</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
