As-built completion record for v1 of the knowledgebase’s new front door: a server-side semantic
search page that replaces mkdocs-material’s slow client-side lunr search. Type a natural-language
question → the best manual sections ranked by meaning (existing Qdrant + bge-large), snippets, deep
links, vehicle filter, type badges — ~54 ms warm end-to-end. Runs in-process in the existing
knowledgebase Flask app (no new service). Verified live on telep-mainframe today. v1 is
retrieval only (ranked sections), NOT RAG synthesis.
Design in 2026-07-29-kb-semantic-search-frontend-design, task-by-task plan in 2026-07-29-kb-semantic-search-frontend-plan — this note is the completion record and the hard-won knowledge, not a restatement of either.
For Agents
Code:
/home/levander/knowledgebase/app.py(extended) + newsearchui.py(pure helpers) +i18n.py(new chrome keys). Runs as the existingknowledgebase.service(Flask + waitress @127.0.0.1:8092,Restart=always). Backups:app.py.bak-search-fe,app.py.bak-search-ui. Backend reused unchanged:/home/levander/kb-vectors/search.py::search(client, q, limit, folder, manual)+embed.py(bge-large CPU singleton) over Qdrant containerkb-qdrant(127.0.0.1:6333, collectionmanuals). App doessys.path.insert(0, "/home/levander/kb-vectors"). Deps installed INTO the app venv (they lived in kb-vectors venv before): torch 2.13.0+cpu (from the pytorch CPU index, NO CUDA), sentence-transformers 5.6.1, qdrant-client 1.18.0, transformers 5.14.1, numpy 2.5.1. Startup: module globalQCLIENT=QdrantClient(host=127.0.0.1, port=6333);main()warms the embedder once (embed.embed_query("warmup")) +torch.set_num_threads(4)beforeserve(). Model is a lazy singleton soimport appstays cheap (model NOT loaded at import). Routes:GET /api/search(JSON),/+/search(search-first UI),/browse(mkdocs index), catch-all/<path:path>still serves the mkdocssite/.site()is no longer decorated with/. State: v1 shipped + verified live; 141 unit tests pass; non-destructive (Qdrant/docs/site/clusters untouched).
Why this was built — the lunr search was fundamentally unfixable
mkdocs-material's client-side lunr search is unfixable in-config for a large corpus
The KB’s only search was mkdocs-material’s client-side lunr index, built in the browser on every visit — a 5.4 MB / 6683-section index that hung on “Initializing search”. Community mkdocs-material removed
prebuild_index(it’s Insiders-only), so the index can’t be pre-built. Dropping the Hungarian search language to shrink it either doubles the index (reconfigure_search: false→ 13366 docs) or removes the HU site entirely. There is no config path to a fast client-side search at this scale. The only real fix was replacing it with a server backend — which is what this is.
The semantic backend already existed (2026-07-24-kb-vectorize-complete: kb-vectors/search.py +
embed.py bge-large singleton over Qdrant manuals). The frontend just exposes it as the primary
way in. The app venv previously lacked the ML deps — installing CPU-torch into the app venv let it
run in-process (no new always-on service), at ~1.6 GB RAM, fine on this box.
What shipped
| Piece | What it does |
|---|---|
GET /api/search?q=&folder=&manual=&limit= | JSON {took_ms, count, hits:[{heading, manual_id, vehicle, page_url, snippet, score, type}]}. Calls kbsearch.search() over Qdrant, then dedupe-by-page + snippet/highlight + type classify + vehicle label. Qdrant/embedder failure → 503. |
/ and /search | Search-first UI (SEARCH_HTML, dark theme): autofocus id=kb-search, debounced fetch, type badges (manual/wiring/article), vehicle filter, keyboard nav ↑/↓/Enter, i18n chrome. |
/browse | Serves the mkdocs index (the old manual browse tree, kept reachable). |
/<path:path> catch-all | Still serves the mkdocs site/ pages unchanged. |
searchui.py (new) | Pure helpers: dedupe_by_page, make_snippet, highlight (HTML-escaped, XSS-safe), classify, vehicle_label. Plus its tests. |
i18n.py | New chrome keys added with hu + en parity. |
The site() view lost its @app.route("/") decorator so / renders the new UI instead of the mkdocs
index; the catch-all keeps every existing manual/consolidated/semantics page serving.
Results / performance (verified live)
- Warm query ~54 ms end-to-end (embed + Qdrant + render).
- Real semantic wins (not keyword matches):
- “why won’t the fuel pump prime” → “Pump is dead, now what” (0.728)
- “fuel cut controller” → FUEL CUT SYSTEM carburetor page (0.726)
- “stop light switch” → Stop Light Switch Adjustment (0.681)
- Filter by folder works; wiring hits deep-link to
/…/…-wiring…/(the UI appends#kinyert-huzalozasi-adatok-aito land on the extracted section from 2026-07-29-kb-wiring-extraction-v1-complete). - Memory: embedder ~1.63 GB RSS of 64 GB (50 GB free).
- 141 unit tests pass. Non-destructive (Qdrant / docs /
site// clusters untouched).
Graceful degradation verified
Stopping Qdrant →
/api/searchreturns 503, but the home page and manual pages still 200 and the service didn’t crash (NRestarts=0). Restoring Qdrant → search back. The mkdocs site is independent of the search backend by design.
Key decisions / gotchas
The backend already existed — the win was exposing it in-process
No new service, no re-embedding. Installing CPU-torch into the app venv (matching kb-vectors’ approach, NO CUDA) is what let
search.pyimport and run inside the Flask process (~1.6 GB RAM), instead of standing up a separate localhost search service. Lazy singleton keepsimport appcheap; the warmup call inmain()pays the model-load cost once at startup, not on the first query.
Community mkdocs-material has NO
prebuild_index— client-side lunr is a dead end at scale
prebuild_indexis Insiders-only. Dropping the HU search lang either doubles the index (reconfigure_search: false→ 13366 docs) or removes the HU site. For a 6683-section corpus the client-side index (5.4 MB) will always hang on “Initializing search” — the fix is a server backend, full stop.
v1 = retrieval only, NOT RAG synthesis (ranked sections). Phase-2 candidates:
- Answer synthesis via claude over the top hits, cited (the “assistant” tier).
- Hybrid semantic + keyword (BM25) fusion for exact-term recall.
- Retire the mkdocs lunr search on
/browsepages entirely now that this is the default front door.
Status
- v1 shipped + verified live on telep-mainframe (2026-07-29):
/,/search,/api/search,/browseall live; warm ~54 ms; graceful degradation confirmed; 141 tests pass. - In-process in
knowledgebase.service— no new service; ~1.63 GB RSS. - Non-destructive: Qdrant / docs /
site// clusters unchanged; backupsapp.py.bak-search-fe,app.py.bak-search-ui. - Next (phase 2): answer synthesis (cited), hybrid semantic+BM25 fusion, retire lunr on
/browse.
Related
- 2026-07-29-kb-semantic-search-frontend-design — the design this implements (search-first, retrieval-only, phase-2 levers)
- 2026-07-29-kb-semantic-search-frontend-plan — the task-by-task plan (Task 0 in-process import GO/NO-GO)
- 2026-07-24-kb-vectorize-complete — the semantic backend this exposes (
search.py+ bge-large over Qdrantmanuals) - 2026-07-29-kb-wiring-extraction-v1-complete — wiring hits deep-link to its
#kinyert-huzalozasi-adatok-aisection - 2026-07-24-knowledgebase — the mkdocs library whose slow lunr search this replaces as the front door
- telep-mainframe — the host (12900K + RTX 3080; 64 GB RAM); the app runs on the CPU
- SESSION-HANDOVER
- homelab
- LOG
- TOPICS