Skip to content

Blog

Private document search for a small company, fully local

By Saif Qureshi11 min read
  • Semantic search
  • Local AI
  • Privacy

Every small company has the same three questions on repeat. Which contract has that renewal date? Which invoice matches this payment? What did we promise in that email before we signed? The answers are in your own files, spread over a shared drive, a few mailboxes and a folder of scans.

You can get those answers without uploading the files to anyone. This post describes how, for an owner of a company with 5 to 50 people. I built a search engine like this for my own documents (it's called Finer, and part 1 and part 2 of this series cover it with numbers and code). What follows is the same design sized for a small office. I'll say which parts I've built and measured, and which are design advice I haven't tested at office scale.

Key takeaways

  • Local search has two halves: keyword search with SQLite FTS5 for exact words, and a small embedding model for meaning. A fusion step merges their two rankings into one list.
  • Neither half needs a server farm. The keyword index is one file, and small embedding models run on an ordinary CPU.
  • The answer step is optional. If it uses a cloud model, mask identifiers first, and know that pattern-based masking catches IBANs and ID numbers but not names.
  • A SQLite file has no user accounts. Access rules need one index per group or a small search service in front.
  • The honest privacy claim is narrow: fewer parties ever see your documents. Local search is not a compliance certificate.

What "local" means here

Local means three things stay on machines you control: the files, the index built from them, and the search itself. Nobody else holds a copy of the index, and nobody else sees what your staff type into the search box.

It doesn't mean one laptop. An office machine that holds the index, staff who reach it over the office network, and an encrypted backup disk in a drawer is still local. You can list every copy and you can delete any of them.

It also doesn't mean cut off from the internet. You'll download software and a model once. And you may decide to send one hard question to a cloud model, with identifiers masked first. That's an exception you choose, and I'll come back to it.

One thing I won't do in this post is make legal claims. Keeping documents on your own machines means fewer parties ever see them. Whether that satisfies a particular rule or contract is a question for whoever advises you on that.

Start with the files, not the model

Before any software, write down where things live. Contracts are usually in a shared folder by client. Invoices sit in monthly folders or in an accounting tool. Mail is in mailboxes, and somebody has a folder of scans on their desktop.

The index reads those places and never moves anything. Each search result points back to the original file, and staff open that file, not a copy. A small extractor pulls the text out of PDFs, Word files and exported mail and keeps the path next to it.

Two practical snags show up here. Scans have no text, so they're invisible to search until they go through OCR, software that reads the letters off an image. And mail has to be exported or synced to disk before anything can index it. In Finer I solved both on a Mac with Apple's built-in OCR and nightly processing. On Windows or Linux you'd pick other tools, and I haven't tested those.

Keyword search with SQLite FTS5

SQLite is a database that is just one file on your disk. FTS5 is its full-text search extension. It builds a word index over the extracted text, so when you type "INV-2026-041" or "notice period" it returns the documents that contain those words, ranked, with the matching lines.

This half is for anything exact: contract numbers, invoice references, client names, clause titles. If the words are in the file, it finds them, and it's fast.

What it can't do is guess. If the email says "cancel" and the contract says "terminate", keyword search sees two unrelated words. FTS5's built-in tokenizers also don't stem German or split German compounds. In my own test a search for "Vertrag" did not find "Mietvertrag". Part 2 shows that test.

A small embedding model for meaning

An embedding model turns a piece of text into a list of numbers that stands for its meaning. Two passages that say similar things get similar numbers, even with no word in common. So a search for "cancel the contract" can find the paragraph headed "Termination".

The model reads your documents in chunks of a few paragraphs and stores one list of numbers per chunk, next to the keyword index. At search time it does the same for the question and looks for the nearest chunks. All of that happens on your machine.

It doesn't need special hardware. Static embedding models such as the Model2Vec family are built to run on a plain CPU. On my Mac I use Google's EmbeddingGemma 2, which also handles German and English in one index. Its text worker takes about 0.8 GB of memory while loaded. Part 1 has those measurements.

Meaning search finds things that are about the right topic. It does not prove anything. It will return a paragraph that sounds like your question and has the wrong client's name in it. For money and dates, always open the source.

One list from two rankings

Now you have two result lists for every question. Keyword search ranked documents by how well the words fit. Meaning search ranked them by how close the topic is. Staff shouldn't have to look at both.

The method I use to merge them is called reciprocal rank fusion. It ignores the scores and looks only at positions: a document near the top of either list moves up, and one near the top of both moves up most. It comes from a 2009 paper and takes a few lines of code.

Here is what that buys you. Imagine a 12-person services firm, and someone searches "late fee" plus a client's name. Keyword search finds the invoice that has a late fee line. Meaning search finds the email thread where a colleague offered to waive it, which never uses the words "late fee". Both land near the top. You'd want to read both before you call the client.

On my own files the difference was large. Keywords alone scored 0.63 on a standard ranking measure (MRR), and the fused list scored 0.90. That was 28 questions on one folder, with settings tuned on those same questions. Many of them were English questions about German documents, which is hard on keywords. So read it as a direction and test on your own questions.

The answer step is optional

Everything so far gives you a ranked list of documents. An answer step goes further: a language model reads the top passages and writes a short reply with the sources cited.

You don't need it on day one. A good result list already answers "which contract?" and "which invoice?". I'd leave the answer step off until people trust the results.

If you add it, two rules matter. First, every sentence in the answer cites a passage, and if the passages don't contain the answer, the model says so. Second, decide where the model runs. A local model keeps everything inside. A cloud model is bigger, but the passages leave your machine. In Finer the answer feature is off by default, and when I switch it on, a cloud model writes the answer unless I pick the local one.

Mask before anything leaves the machine

If passages do go to a cloud model, mask them first. Masking replaces sensitive values with placeholders before the text is sent, and puts the real values back in the answer on your side.

Be realistic about what masking catches. Values with a fixed shape are easy: IBANs, passport and ID numbers, tax numbers, dates of birth. Pattern rules can find those, and that is what Finer does. Names and addresses have no fixed shape, and my pattern rules do not mask them. Even for the fixed shapes, testing another of my systems with fake data turned up formats the first patterns had missed. A tool that claims to remove every name from a contract deserves a test with your own documents before you believe it.

Three habits make masking hold up. Put it in one place that every outgoing request passes through, so there is no second path around it. Test it with fake data, never real documents. And keep a short list of folders that never go out at all, masked or not. HR files are the obvious first entry. I describe the layered version of this in the company brain series, in the part on rules and privacy.

Access rules: who can find what

Search must not show people more than their folders do. If payroll is closed to most of the team, a search for a colleague's name can't return payroll files to them. That includes the title and the preview line, because a file name alone can give things away.

This is where a single index file runs out. SQLite has no user accounts. Anyone who can open the index file can read every word in it. So for a team you need one of two things. Either build a separate index per access group and let the normal folder permissions decide who can open which one. Or put a small search service on the office machine that knows who is asking and filters the results. The first is simpler, and it's where I'd start.

Don't put the index file on a shared drive for everyone to open directly, either. SQLite's own documentation warns that file locking is unreliable on many network file systems and that this can corrupt the database. One machine owns the index. Everyone else asks that machine.

Three groups cover most small companies: everyone, finance, management. Map each folder to one of them, and review the map when someone joins, changes role or leaves. I haven't run this multi-user setup in an office myself. Finer is a single-user tool, so take this section as design advice.

When local is enough, and when it isn't

Local is enough when the questions are about your own files and the people asking are in the office or on the office network. "Find the clause", "find the invoice", "find the email before we signed" all fit. For a few thousand documents an ordinary computer should do the job. The folder I measured in part 1, a few hundred business documents, runs on one Mac.

It gets harder in four situations.

  • People work remotely with no VPN. Then the index machine isn't reachable, and you need a secure way in before you need search.
  • The documents live only in a cloud tool with no export. You can't index what you can't read. Leave those out and say so, so nobody assumes the search covers them.
  • You want long, polished drafts all day. That is more than I'd ask of a small local model. It's the case for a cloud model behind masking.
  • Outsiders need to search too. An auditor or a law firm can't join your network. Export the specific documents for them and keep a note of what went out.

How I'd start, and a prompt for the first step

Start small: one client folder, one month of invoices, one exported mailbox. Write down ten questions from the last few months whose answers you already know. Index the small set, ask the ten questions, and check that each right document shows up in the top results. Fix what's missing before you add more folders.

Before any of that, map your files. The prompt below asks an AI assistant to draft that map with you. It doesn't touch a file or change a permission. It needs a list of your folders from you, so paste one in or describe them. Go through the result with whoever handles finance and whoever runs the office.

If you'd like help building it, that's the kind of work I do: see hybrid retrieval or get in touch.

Prompt to map your files before indexing
prompt
Goal: draft the scope for a local document search at my company. Do not build anything yet.
Inputs: the folder list I paste below, with a note on what each holds (contracts, invoices, exported mail, scans). The three access groups: everyone, finance, management. Ten questions my team asks often.
Constraints: files stay on our machines and nothing is uploaded to an outside index. Assume keyword search with SQLite FTS5, a small embedding model that runs on a CPU, and a fusion of both rankings. The answer step is off for now. Do not invent tools, prices or dates. If you do not know something about my setup, ask me.
Definition of done: a table that maps every folder to one access group. A list of folders that need OCR or an export first. A list of folders that must never be sent to an outside model. A first test set small enough to index and check in one afternoon. The open questions you still have for me.

My folders:
[paste your folder list here]

Frequently asked questions

Do I need a server or a graphics card?

No. The keyword index is a single file, and small embedding models run on an ordinary CPU. One reasonably recent office computer that stays on is enough to start. Add hardware only if your own tests show slow searches or slow indexing.

What if the contract and the email use different words?

That is what the second half is for. Keyword search finds the exact phrase in the contract. Meaning search finds the email that discusses the same thing in other words. The fused list shows both, and you confirm in the two originals.

Does the team have to learn a query syntax?

No. One search box and plain words. Three habits help: type the client's name, add one exact term when money or a date is involved, and open the original before acting on a result.

Our files are in cloud folders today. Can we still do this?

Yes, if you can sync or export them to a machine you control. Index that copy. If a tool gives you no export, leave those documents out and tell the team the search doesn't cover them.

Is local search automatically compliant with privacy law?

No, and I'm not giving legal advice. The claim I'll stand behind is narrow: with a local index, fewer parties ever see your documents. Access rules, retention and deletion are still your job.

Sources

  1. SQLite FTS5 extension documentation sqlite.org
  2. SQLite: appropriate uses (network file systems and client/server) sqlite.org
  3. Model2Vec: small static embedding models (MinishLab) github.com
  4. Cormack, Clarke, Büttcher: Reciprocal Rank Fusion (SIGIR 2009) research.google

Talk to me

Building an AI product and need someone senior to own the technical side? Book a free 30-minute call.