The file finder we built because we couldn’t find anything anymore.
A small piece of infrastructure that lives quietly on a laptop and answers questions like “find the lease PDF Shashi sent last summer” in under a second. Built for one user — Aman — because the cost of not finding things had quietly become absurd.
By March 2026, the files had won.
By March 2026, Aman had roughly 1,500 files scattered across three places. A work laptop carrying years of contracts, design docs, screenshots, half-finished essays, scenario files for two different products. A phone, synced via Syncthing into a directory on the laptop — receipts, photos of documents, things friends had forwarded over WhatsApp. And the Samsung Notes Windows app, which is wonderful for capturing things and absolutely terrible for getting them back — everything saved as opaque blobs with filenames like AgADxxx_4321.jpg.
A typical “find the lease PDF Shashi sent last summer” took two to three minutes. Open the laptop’s file manager, guess at a folder, fail, try search, get drowned in cached thumbnails, switch to the Syncthing folder, scroll, give up, open Samsung Notes, scroll, give up, ask Shashi to resend it.
Compounded across a working day, that was about thirty minutes a day lost to the where-did-I-put-that tax. Not catastrophic. Just a slow leak of attention that you stop noticing until you measure it.
So the tool got built. Not because anyone asked for it. Because the math finally tipped.
A Telegram bot that answers in plain English.
The interface is a Telegram chat. You type something like “the lease PDF Shashi sent last summer” or “screenshot of the bank balance from before the trip”, and a moment later the file comes back. If the bot isn’t sure, it asks one short clarifying question. If it’s genuinely got nothing, it says so — quietly, no theatre.
Under the hood: Python backend, a file watcher per source directory that picks up changes in seconds, an extractor pipeline that handles a dozen file types, and a hybrid retrieval stack mixing Gemini embeddings with BM25 keyword scoring, then a bge cross-encoder reranker on the survivors. Per-provider Chroma collections hold the vector indexes — one collection per embedding provider, because vector dimensionality is fixed at collection creation and mixing dims would corrupt the index.
The retrieval surface is a 0.7-vector / 0.3-keyword fixed-weight fusion, not Reciprocal Rank Fusion — we wanted the vector dominance tunable in production, not laundered through rank inversion. Every result row collapses to one row per parent file via a parent_id dedupe, because Gemini embeddings have a 0.5-0.7 baseline cosine on plain English text and the relative ordering after dedupe is the actual signal.
That’s the brief sketch. There are seven independently ablate-able ranking knobs, an edit-history JSONL sidecar that survives full vector-store rebuilds, a free-tier-aware 12-second batch throttle that keeps the system inside the public Gemini quota, and a 13-stage agentic flow on the read path. None of that needs to live here.
If you want the actual architecture — the fusion math, the seven knobs, the sidecar pattern, the cross-provider collection contract — that lives on the memory flagship: Two memory systems we shipped. Both running. This page is the human story. That page is the engineering.
Not a 14-day sprint. Six weeks of evenings.
This was an internal tool, not a client engagement. It got built across roughly six weeks of free-time evenings, in phases, with the corpus and the benchmark growing as the system did.
-
Phase 0–3Indexer + retriever foundation. Walker, extractor dispatcher, chunking strategy (summary chunk at index 0 with filename + folder + head + tail), embedder, Chroma collections, the hybrid fusion math, the 50-query benchmark scaffold. By the end of Phase 3 the system could answer about 70/95.
-
Phase 4–9The seven ranking knobs, one by one. Each knob added because a specific ablation result demanded it. Synonyms, extension boost, wrong-extension penalty, UUID penalty (the one that finally taught the system to ignore cached Telegram blob names), folder-synonym boost, filename-overlap, filename-coverage. Each gated by an env var so you can drop it and re-run the benchmark to see exactly what it earned.
-
Phase 10File watcher + edit-history sidecar — pulled forward. Originally a later phase. Got pulled forward because the indexed corpus kept going stale within a day and the bot would confidently return last week’s version of a document. Watchdog Observer with a 3-second debounce, single worker thread, append-only JSONL sidecar at
{chroma_path}/edits/{file_id}.jsonl. Survives full vector-store rebuilds, which is what made it worth pulling forward. -
OngoingWeekly longitudinal benchmark. The 50-query held-out set gets rerun on Sundays. Score lives in a tiny CSV. Anything that pushes it below 90/95 is a regression worth a Saturday-morning investigation.
Concrete numbers and a quieter morning.
Files indexed across three sources
Laptop, Syncthing-synced phone storage, Samsung Notes Windows app blobs. Twelve file types. One unified search surface.
Top-3 hit rate on the held-out benchmark
Fifty queries collected from real “where did I put that” moments. Of ninety-five attempts across multiple runs and configurations, the right file ranks in the top three on ninety-three.
End-to-end retrieval latency
From hitting send in Telegram to the file landing in chat. Local Chroma plus a cached bge reranker keeps the round-trip tight.
Reclaimed from the search tax
Not measured to a stopwatch — measured to the felt sense of “I can find anything I’ve seen.” That’s the actual lifestyle outcome, and it’s the part that’s hard to put on a metric strip.
The shift is small to describe and large to live with. From “I can’t find anything — I’ll just ask the person to resend it” to “Hey bot, find the lease PDF Shashi sent last summer.” The bot returns the file. The conversation moves on. The morning stays quieter.
Honestly? Mostly the same. With clearer signposting.
The thing we’d ship the same way again is the per-provider Chroma collection design — one collection per embedding provider, because Chroma fixes vector dimensionality at collection creation. LM Studio at 2560 dim and Gemini at 3072 dim live in physically separate collections. The indexer refuses to fall back across providers because writing the wrong-dim vector into a dim-locked collection corrupts it silently. We caught one near-corruption early because the indexer was strict, and a softer system would have lost data.
What we’d do differently: document that contract more loudly. Future-us, returning to the codebase six months from now, will absolutely write a fallback path because it “feels safer” and then wonder why retrieval quality collapsed. The README should have an aggressive paragraph about this, not a polite footnote.
Two other smaller things. The summary-chunk-at-index-0 design caught more queries than the vector math did — people remember filenames and locations more than content. We’d weight that even harder next time, earlier. And the free-tier-aware batch throttle should have been the very first thing built, not a phase-mid retrofit. The quota is the architecture; treat it that way from line one.
If your team has the same file-search pain, we can build you the same kind of thing.
Personal tool today, productized tomorrow. The architecture transplants directly to a team setting — shared corpus, per-user permissions, the same hybrid retrieval surface. Bring the actual pain to the conversation.