The goal

This is the second chapter of the Ahmad-AI build. The first chapter was about safety — making an agent that refuses to fabricate citations. This chapter is about capability — making an agent that can actually find the right verse, in the right language, with the right intent.

The product: a browser-based Islamic knowledge assistant. Ask it "what does the Quran say about patience" and it returns the canonical sabr verses. Ask it "حور العين" in Arabic and it returns the classical hoor passages. Ask it "namaz" in Urdu and it returns prayer hadiths. Ask it "2:255" and it returns Ayat-ul-Kursi exactly.

Under the hood: Streamlit for the UI, ChromaDB for vector storage, a multilingual sentence-transformer for embeddings, and 56,998 indexed texts spanning six canonical Quran translations and nine major Hadith collections.

Getting from zero to "working" was four weeks. Getting from "working" to "actually good" was six more bugs, most of which I built into the system myself.

What's under the hood

The architecture is deliberately simple:

The first real design decision was to not combine everything into one search — because Quran verses and Hadith passages have completely different structures, formats, and metadata needs. Sharing a vector space between them would have averaged the two.

Week 1-2: data pipeline, then a hard lesson in version drift

The Quran side went smoothly. I pulled a canonical dataset, extracted verses, translations, and metadata, and indexed 6,236 entries without incident. The Hadith side, 50,762 entries from nine collections, went equally smoothly — until I tried to read the databases back.

Both vector databases had been created across a range of ChromaDB library versions as I iterated. Individually, each version worked. But a database written by one version and read by another triggered a hard Rust-level panic — range start index 10 out of range for slice of length 9 — deep inside the ChromaDB SQLite backend, before any Python exception handler could catch it. The panic was thrown from the compiled bindings, not from Python, so it killed the whole process.

The fix was a full rebuild of both databases under one locked version. But the deeper lesson was about how silently libraries change formats between minor releases, and how "it works on my machine" quietly becomes "it works on my machine running exactly this version of this library and no other."

ChromaDB 1.0 was a major structural change from 0.5 — different SQLite schema, different default distance metric, different collection metadata format. Every bug in the rest of this log traces back to that one version boundary in one way or another.

Week 3: nine hundred megabytes of Hadith, and a power cut

Indexing 50,762 Hadiths with a 384-dimensional embedding model produced a database just under a gigabyte and took about 20 minutes of solid CPU time. Halfway through the first attempt, the power in my neighborhood cut.

The ChromaDB write path is transaction-safe, so nothing was corrupted — but the collection was only 59.4% populated when the process died. I wrote a short resume script that read the set of already-indexed IDs directly from the collection, diffed it against the full dataset, and only embedded the missing 20,612 rows. Twelve minutes later the collection was whole.

This is the kind of thing that looks obvious in retrospect and never occurs to you until the first time it happens. Every long-running indexing job I write now starts with a checkpoint pattern baked in from minute one.

Week 4: the multilingual upgrade — and a rebuild, and another rebuild

English-only semantic search worked. But it didn't understand Islamic terminology. Ask about "hoor" and it returned generic Paradise verses. Ask about "tahajjud" and it returned general prayer hadiths. Ask about "namaz" and it fell back to whatever English keyword matched.

The fix, in theory, was simple: swap the English-only embedding model for a multilingual one. Same dimension count, same API, same pipeline. In practice, this triggered four separate classes of bug that took an entire day to untangle.

Bug 01 · Environment

The Python version that was silently used

ChromaDB's underlying HNSW library has no prebuilt wheels for Python 3.12 on Windows. When the virtual environment was recreated, Windows silently picked Python 3.12 instead of 3.11, and the vector store refused to initialize. Every subsequent error looked like a database problem but was actually an environment problem.

Lesson: pin the Python version in the venv creation command explicitly. Never trust the default.

Bug 02 · Dependencies

A protobuf version conflict that broke two libraries at once

Streamlit needed protobuf <6. A transitive dependency of the observability stack needed protobuf ≥6.33. Installing either one broke the other. The fix was to pin Streamlit to an older minor version that accepted protobuf 5.x, satisfying both constraints at the boundary.

Lesson: pin ranges, not exact versions, in requirements.txt. Let pip find the compatible intersection; forcing versions creates false conflicts.

Bug 03 · Database semantics

L2 distance vs. cosine distance — same numbers, different meaning

The scoring code assumed ChromaDB returned cosine distance (range 0-2) and computed similarity as 1 - distance/2. But the collections had been created with the default L2 metric, whose range depends on the vectors' norms. When I switched to a model that produced unnormalized embeddings, every L2 distance grew by 3-6x, and the formula floored every score to 0%.

The fix was to rebuild both collections with hnsw:space: "cosine" explicitly, and re-index. Cosine distance has a fixed 0-2 range regardless of vector norm, so the original formula works again.

Lesson: always check the distance metric on the collection, not just the score formula in the code. They can disagree silently.

Bug 04 · Model mismatch

The wrong model was encoding the query

Even with cosine space, scores stayed at 0%. The databases had been indexed correctly with the multilingual model — I verified by re-encoding a stored verse and confirming a 1.0000 similarity — but the query was being re-encoded by ChromaDB's internal default model, which is English-only. The two vector spaces were completely separate.

The fix was to stop passing query_texts to ChromaDB and start passing query_embeddings instead, computed with the same model used at index time.

Lesson: never let a library silently choose the embedding model for you. If you control the index model, control the query model too.

Bug 05 · Data, not code

The Hadith corpus didn't contain what we were searching for

After all four environment fixes, Arabic queries like حور العين finally returned the correct Quran verses. But Hadith results for "hoor" still returned hadiths about pious wives and greetings — never the classical Hoor al-Ayn narrations. I queried the dataset directly for the Arabic word حور and found exactly five hits — all of them as part of narrator names, none of them as the subject of the hadith.

The dataset was simply missing that content. Not a bug in the code, not a failure of the model — a limitation of the source. The fix, for a future iteration, is to supplement with a Hadith collection that has stronger eschatology coverage.

Lesson: investigate the data before blaming the algorithm. A semantic search system can only find what's actually there.

Bug 06 · Deployment

The URLs that pointed to the wrong folders

Windows treats D:\Ahmad-AI and D:\ahmad-ai as the same folder — but the ChromaDB Rust bindings compute path hashes byte-for-byte, so the casing mattered. Virtual environments created at the lowercase path silently mis-resolved when the working directory used uppercase. The fix was to standardize every path reference, every config file, and every venv on one canonical casing.

Lesson: on Windows, casing is not decorative. Any tool that stores paths as bytes needs consistent casing everywhere.

Where it stands now

The system runs locally, exposes itself through a Streamlit UI, is password-gated, and can be shared publicly through a Cloudflare Tunnel without opening any inbound ports on the machine. It is deployed, multilingual, and access-controlled.

56,998
Indexed Islamic Texts
<10ms
Exact Citation Lookup
~0.8s
Semantic Search Latency
3
Languages Tested

Sample verified outputs from the deployed system:

What I'd tell another engineer building something like this

Pin the Python version and every library that touches your data persistence layer. ChromaDB, SQLite bindings, and vector databases in general are not interchangeable across minor versions. The upgrade path is a rebuild, not a migration.

Test the model in isolation before committing to a rebuild. I could have caught the multilingual model bug in ten minutes with a standalone similarity script — instead I caught it after two full rebuilds of both databases.

When scores collapse to zero, check three things in order: the distance metric, the query embedding model, and the stored embedding model. Ninety percent of "semantic search is broken" bugs live in one of those three.

Investigate data before blaming code. The Hadith coverage gap looked like a semantic search failure. It was actually a source dataset limitation. Only querying the database directly revealed the truth.

Keep the old version working until the new one is proven. Having both vector stores live in parallel — English-only and multilingual — meant every failed multilingual attempt was a two-line config revert, not a full disaster recovery.

What's next

Two immediate improvements are queued. First, cleaning the isnad (narrator chain) prefix out of Hadith text before indexing — the chain sometimes takes up more space in the embedding than the actual content, which dilutes semantic matching across all queries. Second, supplementing the Hadith corpus with a collection that has stronger coverage of Paradise, eschatology, and Hoor narrations, so queries about those topics return Hadith results instead of only Quran results.

Longer term, the plan is a second chapter of this same system — a business-data assistant built on the same agent framework, connected to a live ERP database, sharing the same UX and deployment pipeline. Same pattern, different domain.

This is where it actually stands. Not a demo. Not a proof of concept. A working, deployed, multilingual system, tested against real queries in three languages.


Curious about the earlier chapter — how the guardrails against hallucination were built? Read Chapter 01 →