Tidqom — Source-linked AI and developer tools
AITid
AI

I Built a Chatbot for My Own Documents in One Evening (Free, Local, No API Key)

A working RAG setup over a folder of PDFs using Ollama and Chroma. Full code, the two mistakes that made my answers wrong, and how I fixed retrieval quality.

Sarah Chen profile photo
July 29, 2026 · 4 min read
I Built a Chatbot for My Own Documents in One Evening (Free, Local, No API Key) — AI

What you get

a Python script that answers questions about a folder of your own PDFs, runs entirely on your machine, costs nothing, and never sends a page to anyone. It took me about three hours including the mistakes. Without the mistakes, forty minutes.

The stack, and why

Related: Midjourney vs DALL-E vs Flux: A Practical Image-Tool Comparison Framework →

Related: Ollama vs LM Studio vs Jan: I Used All Three for a Month →

PieceChoiceWhy not the alternative
Model runtimeOllamaSimplest local API, everything speaks to it
Generation modelqwen3:8bGood instruction following at a size most laptops handle
Embeddingsnomic-embed-textRuns locally, far better than MiniLM on long passages
Vector storeChromaZero setup, persists to disk
ParsingpypdfBoring and reliable

Setup

Related: 9 Free AI Coding Tools Every Developer Should Try in 2026 →

Related: [How to Run a Local LLM on 8GB of RAM (What Actually Works in 2026) →](/article/run-llama-local-8gb-ram-guide)

bash
ollama pull qwen3:8b
ollama pull nomic-embed-text
pip install chromadb pypdf ollama

The ingest script

Related: Sora 2 Review: OpenAI's Video Model Is Finally Useful for Real Work →

Advertisement — In Article

Related: كيف تبني وكيل ذكاء اصطناعي يعمل على جهازك بدون إنترنت (Ollama + MCP) — جرّبته 3 أسابيع (2026) →

python
import os, pypdf, chromadb, ollama

client = chromadb.PersistentClient(path="./store") col = client.get_or_create_collection("docs")

def chunk(text, size=900, overlap=150): out, i = [], 0 while i < len(text): out.append(text[i:i+size]) i += size - overlap return out

for fn in os.listdir("docs"): if not fn.endswith(".pdf"): continue reader = pypdf.PdfReader(f"docs/{fn}") for page_no, page in enumerate(reader.pages): text = (page.extract_text() or "").strip() if len(text) < 50: continue for j, c in enumerate(chunk(text)): emb = ollama.embeddings(model="nomic-embed-text", prompt=c)["embedding"] col.add(ids=[f"{fn}-{page_no}-{j}"], embeddings=[emb], documents=[c], metadatas=[{"source": fn, "page": page_no + 1}]) print("indexed", col.count(), "chunks")

terminal

The query script

Related: كيف تؤتمت عملك بالذكاء الاصطناعي باستخدام n8n مجاناً (دليل عملي خطوة بخطوة 2026) →

python
import chromadb, ollama

col = chromadb.PersistentClient(path="./store").get_collection("docs")

Advertisement — In Article

def ask(q, k=5): emb = ollama.embeddings(model="nomic-embed-text", prompt=q)["embedding"] res = col.query(query_embeddings=[emb], n_results=k) ctx = "\n\n---\n\n".join( f"[{m['source']} p.{m['page']}]\n{d}" for d, m in zip(res["documents"][0], res["metadatas"][0])) prompt = ( "Answer using ONLY the context below. Cite the source and page for each claim. " "If the answer is not in the context, say so plainly.\n\n" f"CONTEXT:\n{ctx}\n\nQUESTION: {q}") return ollama.chat(model="qwen3:8b", messages=[{"role": "user", "content": prompt}])["message"]["content"]

print(ask("What is the refund window in the contract?"))

terminal

That is the whole thing. Run ingest once, then query as often as you like.

Experience log: the two mistakes that wasted my evening

Related: 12 Best AI Tools for Coding in 2026 (Tested for Solo Developers & Freelancers) →

Mistake 1 — chunks that were too small. I started at 300 characters with no overlap because a tutorial said so. Answers were confidently wrong: the retriever pulled a sentence containing "30 days" from an unrelated clause. Moving to 900 characters with 150 overlap fixed roughly 70% of the bad answers immediately. Small chunks retrieve precisely and answer badly, because the model loses the surrounding meaning.

Mistake 2 — no page filter on empty text. Scanned pages returned empty strings, which still got embedded, and empty vectors sat near everything in the index — so garbage kept appearing in results. The len(text) < 50 guard removed it. If your PDFs are scans, you need OCR (ocrmypdf) before any of this works at all.

Third, smaller thing

the first version had no citations, and I could not tell whether an answer was real. Forcing the source-and-page format in the prompt made errors obvious within seconds.

Making the answers better

Related: How to Run an AI Model Locally on Your Own Computer (Step-by-Step 2026 Guide) →

  • Fetch more, then trim. Retrieve 10 chunks, ask the model to discard irrelevant ones, then answer. Slower, clearly more accurate.
  • Keep the filename meaningful. 2026-supplier-contract.pdf gives the model context that scan001.pdf does not.
  • Re-index when documents change. Chroma will not notice on its own; delete by id or rebuild the collection.
  • Cap what you feed in. Five 900-character chunks is roughly 1,200 tokens. Twenty chunks will overflow a small model's context and quality collapses.

When this approach is not the right one

If your question needs to consider the entire document rather than a passage — "summarise the whole report", "how many times is X mentioned" — retrieval is the wrong tool. Feed the full document to a long-context model instead, or count with plain code.

FAQ

How many documents can this handle? Chroma is comfortable into the low hundreds of thousands of chunks on a laptop. Past that, look at pgvector or Qdrant.

Can I use this on Word documents and web pages? Yes — swap pypdf for python-docx or trafilatura. The rest is unchanged.

Does it work in other languages? nomic-embed-text handles multilingual content reasonably; for heavy non-English use, bge-m3 performed better in my tests.

Is this private? Completely, as long as both models are local. Nothing leaves the machine — verify with ss -tunap | grep ollama if you want to be sure.

Advertisement

Related Articles

مقالات ذات صلة — تابع القراءة داخل الموقع

View all in AI

مواضيع مقترحة · Suggested Topics

استكشف مواضيع ومحاور ذات صلة بهذا المقال — روابط داخلية لتعميق قراءتك.

The Daily Pulse

Newsletter delivery is not connected yet. This form only saves your address in this browser; no email is sent.

Get concise, source-linked technology notes without the hype.

Advertisement