vellum MCP is a self-hosted server over a folder of markdown, your agent's memory as plain .md
files you own. Search is the feature everyone expects to be "solved" by a library: bolt on bleve or a vector index and move on.
I never did. Search is a ranked scan over the notes, held in RAM. No engine, no index to build. Bragging about not adding a dependency is a strange flex, so let me show you what that plain scan actually costs, and how one optimization pass later it runs about 5× faster than my first honest cut of it. This is the case for the boring option.
A bundled search engine sounds like the responsible choice, and for a large enough corpus it is. But look at what it actually drags in for a personal note vault:
For a vault of a few thousand notes, that's a lot of moving parts to answer "which notes mention OAuth." The whole content is already in memory. Vellum holds the vault in an in-RAM index it rebuilds from disk in about 50 ms at startup. So search is just a ranked scan over that: walk the notes, score each against the query, sort, return the top slice. No separate index, nothing to warm up, nothing to corrupt.
Here's what that costs on a 2,000-note vault (Apple M4 Pro, warm):
| Query type | Time |
|---|---|
| exact term | 0.38 ms |
| accent-folded, multi-term | 0.74 ms |
| worst-case typo match | 0.39 ms |
Sub-millisecond, every time. Nothing to invalidate except when a file actually changes. The cache keys off each file's mod-time and size, so an edit through the MCP layer, the web editor, or your text editor all invalidate the same way.
The reason I don't miss the library is that writing the matcher myself made it more forgiving, not less. Three things it does that matter for real notes:
Folds diacritics both ways. ukol
finds úkol
; poznamka
finds poznámka
; and the reverse. When half your notes are in a language with accents and you're typing on muscle memory, this is the difference between "found it" and "search is broken."
Tolerates typos, scaled to word length. For a 4 to 7 character word it allows one edit; for 8+ it allows two. So preklapy
finds překlepy
. Short words stay strict (you don't want cat
matching car
), long words get slack where a fat-fingered character is likely.
Ranks by where the match lands. A hit in the title beats a hit in a tag, beats one in the path, beats one buried in the body. And a match at the start of a word beats one mid-word. This is the part a generic engine gets generically right and a hand-written one gets right for your data. The ranking encodes how you actually think about your own notes.
I'm not claiming a linear scan is the answer for everyone. It's the answer for this size of problem. So the search sits behind a Searcher
interface. The day a vault gets big enough that a scan hurts, I can drop a real engine in behind the same seam without touching the callers.
That day hasn't come. Two thousand notes in, a full ranked scan is still sub-millisecond, and honestly it would take an order of magnitude more before the constant factors start to matter. When you do cross that line (tens or hundreds of thousands of documents, or you genuinely need semantic similarity rather than lexical matching), reach for the index. Below it, the index is the heavy option pretending to be the safe one.
"Use a library for search" is good advice that stops being good at small scale, and small scale is where a lot of self-hosted tools live. Before you add the dependency, the index, the warm-up and the embedding pipeline, check whether a scan over what's already in RAM answers in under a millisecond. For a personal knowledge base, it very likely does.
Sometimes the library is the heavy option.
👉 github.com/freema/vellum. MIT, one 24 MB container. Where's your line for "just scan it" vs. "build an index"? I'd put it somewhere north of 50k documents for lexical search. Curious where others draw it.