As-built completion record for phase 2 of the knowledgebase semantics work: fully-local semantic
SECTION clustering over the phase-1 vectors plus a read-only web explorer (“Szemantika”) folded
into the existing knowledgebase Flask app. No LLM. Result: 194 clusters (post boilerplate filter),
24 cross-SOURCE (span ≥ 2), 631 noise singletons from ~1248 section vectors mean-pooled out of 3252
chunk vectors. This is the retrieval-bundle foundation for phase 3 (claude -p consolidated generation),
which is still NOT started.
Design in 2026-07-24-kb-semantics-phase2-design, task-by-task plan in 2026-07-24-kb-semantics-phase2-plan — this note is the completion record and the hard-won knowledge, not a restatement of either. Builds on 2026-07-24-kb-vectorize-complete (phase 1) and reads the same 2026-07-24-knowledgebase library.
For Agents
Code:
/home/levander/kb-vectors/on telep-mainframe (reuses the phase-1 venv, runs aslevander). New modules:sectionvecs.py,labels.py,cluster.py,kbclust.py(+ tests). CLI:kbclust.py build/list/show <id>. Pipeline: scroll all 3252 chunk vectors from Qdrant collectionmanuals(READ-ONLY) → group by(manual_id, page)→ L2-normalized mean-pool → ~1248 SECTION vectors →HDBSCAN(min_cluster_size=2, min_samples=1, metric="euclidean")on unit-norm vectors (≈ cosine) → representative-heading labels →clusters.json. Artifact:/home/levander/kb-vectors/clusters.json, regenerated on demand viakbclust.py build(thread-capped). Explorer:GET /semantics+GET /api/clustersadded to the existingknowledgebaseFlask app (/home/levander/knowledgebase/app.py) — NO new always-on service. Live athttps://knowledgebase.taild4189d.ts.net/semantics(Hungarian UI “Szemantika”). Dashboard tile 🧭 Szemantika onhome.taild4189d.ts.net. Ranking: clusters sorted by cross-SOURCE span (dedup targets surface first).
What was built
- Section-level clustering pipeline (
sectionvecs.py+cluster.py+labels.py): pulls the phase-1 chunk vectors out of Qdrant, mean-pools each(manual_id, page)group into one L2-normalized SECTION vector (3252 chunks → ~1248 sections), then HDBSCAN-clusters them into topics that recur across manuals. kbclust.pyCLI:build(regenerateclusters.json, prints section/cluster/cross-source/noise counts),list(id / span / size / label),show <id>(members with manual_id + heading + page_url).- Web explorer
/semantics+ JSON/api/clusterson the existing knowledgebase app — each cluster shows its label +N forrásokbadge + member sections linking straight into the KB pages. Cross-source clusters first. Read-only file read ofclusters.json, no user input into paths. - Dashboard tile 🧭 Szemantika on the
home.taild4189d.ts.netportal → the explorer.
Result (the real proof)
- 194 clusters after the boilerplate filter (22 dropped), 24 cross-SOURCE (span ≥ 2 distinct top-level source folders), 631 noise singletons.
- Top clusters are genuine cross-make overlaps: WINDOW REGULATOR spanning chevy-tracker + suzuki-vitara/5door + workshop; ENGINE COOLING; PARKING BRAKE; THROTTLE BODY. These are exactly the “same topic covered in N manuals” dedup targets phase 3 will consolidate.
Why these choices
- SECTION as the cluster unit, not chunk — cluster whole procedures/topics, not passages; one
NNN-*.mdKB page = one section, mean-pooled from its chunk vectors. - Local heading labels, pluggable — the label is the representative (nearest-centroid) section’s
heading, behind a single
make_labelseam. A futureclaude -prelabel pass drops in there for nicer titles without touching the clustering. See the tip below. - Explorer folded into the existing knowledgebase app — deliberately NO new always-on service on the
power-flaky box (see 2026-07-24-psu-load-resets and the mains die-off). It’s just a route on an
already-running app; if
clusters.jsonis missing it shows an empty state, not an error. - Ranked by cross-SOURCE span — dedup targets (topics covered by the most distinct sources) surface first, which is the whole point of the phase.
Hard-won gotchas
HDBSCAN's
min_samplesdefaults tomin_cluster_size— leaving it default made ALL data noise (0 clusters)With
min_cluster_size=2andmin_samplesleft unset, HDBSCAN usesmin_samples = min_cluster_size = 2, which routed every section to the noise bucket → 0 clusters. You must setmin_samples=1explicitly formin_cluster_size=2to actually form clusters. This looked like “the data has no structure” but was purely the default-coupling of the two knobs.
"Cross-manual" span was inflated because
manual_idis<folder>/<manual>and the kick-fix archive is ONE source split into ~25 topic-subfoldersRanking by distinct
manual_idmade the kick-fix archive look like ~25 “different manuals” all covering the same topic — inflating span. Fix: define span by distinct TOP-LEVEL source folder (source_of = manual_id.split("/", 1)[0]), so kick-fix collapses to 1 source while genuinely separate manuals (suzuki-vitara/workshopvssuzuki-vitara/5door-supplement) still count distinctly. Keep the fullsource_manualslist for display, but rank by(span = distinct sources, #manuals, size).
- Boilerplate pollution of the top ranks. Span-ranking first surfaced cover/title pages (“SUZUKI”) and
generic headings (“GENERAL DESCRIPTION”, “DIAGNOSIS”) — they appear in every manual so they span the most
sources. Added an
is_boilerplate()label stoplist filter → 22 clusters dropped, all genuinely generic. The heading-based labels are only as good as the OCR headings — which is exactly why the label layer is pluggable for a futureclauderelabel. - 51% noise (631/1248) is EXPECTED and correct for a dedup goal. Singleton sections unique to one manual aren’t duplicated knowledge, so HDBSCAN routing them to noise is the right answer, not a bug. Don’t chase “reduce the noise” — the noise bucket is the not-duplicated set.
Read-only scroll discipline + the pluggable-label seam
The whole pipeline only scrolls points OUT of Qdrant and writes a local
clusters.json— it neverupserts /deletes / creates. Verified in review. This means clustering can be re-run freely against the livemanualscollection with zero risk to the phase-1 index. Separately,make_label(rep_meta, source_manuals)is the one seam where a futureclaude -prelabel replaces heading-based titles — same signature, no change to clustering or the explorer.
Ops
- Regenerate on demand:
kbclust.py build(thread-capped per the PSU lesson — 2026-07-24-psu-load-resets) after the corpus changes; the explorer just re-readsclusters.json. - Read-only over Qdrant + KB docs — cannot harm either.
- No new always-on service; the explorer is a route on the already-running knowledgebase app.
Roadmap position
- Phase 1 (vectorize) — DONE (2026-07-24-kb-vectorize-complete): 3252 chunk vectors in Qdrant
manuals. - Phase 2 (this — cluster + explore) — DONE.
- Phase 3 (
claude -pconsolidated generation) — NOT started.clusters.jsonis its retrieval bundle: each cross-source cluster = the exact set of sources to handclaudefor a topic. Phase 3 needs Claude Code installed + logged-in (subscription) on the box — still not done. - Optional bridge: a
claude -prelabel pass behindmake_labelfor nicer cluster titles.
Related
- 2026-07-24-kb-semantics-phase2-design — the design this implements (cluster unit, HDBSCAN, ranking, seams)
- 2026-07-24-kb-semantics-phase2-plan — the task-by-task implementation plan
- 2026-07-24-kb-vectorize-complete — phase 1: the chunk vectors this clusters
- 2026-07-24-knowledgebase — the source library whose per-section pages become sections + the app the explorer folds into
- 2026-07-24-psu-load-resets — why the explorer is a route, not a new service, and why builds are thread-capped
- telep-mainframe — the host
- SESSION-HANDOVER
- homelab
- LOG
- TOPICS