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) + new searchui.py (pure helpers) + i18n.py (new chrome keys). Runs as the existing knowledgebase.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 container kb-qdrant (127.0.0.1:6333, collection manuals). App does sys.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 global QCLIENT=QdrantClient(host=127.0.0.1, port=6333); main() warms the embedder once (embed.embed_query("warmup")) + torch.set_num_threads(4) before serve(). Model is a lazy singleton so import app stays 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 mkdocs site/. 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

PieceWhat 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 /searchSearch-first UI (SEARCH_HTML, dark theme): autofocus id=kb-search, debounced fetch, type badges (manual/wiring/article), vehicle filter, keyboard nav ↑/↓/Enter, i18n chrome.
/browseServes the mkdocs index (the old manual browse tree, kept reachable).
/<path:path> catch-allStill 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.pyNew 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-ai to 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/search returns 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.py import and run inside the Flask process (~1.6 GB RAM), instead of standing up a separate localhost search service. Lazy singleton keeps import app cheap; the warmup call in main() 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_index is 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 /browse pages entirely now that this is the default front door.

Status

  • v1 shipped + verified live on telep-mainframe (2026-07-29): /, /search, /api/search, /browse all 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; backups app.py.bak-search-fe, app.py.bak-search-ui.
  • Next (phase 2): answer synthesis (cited), hybrid semantic+BM25 fusion, retire lunr on /browse.