Skip to content

v2.0.0 — Local search page, full-stack pagination, and a complete UI rebuild

Latest

Choose a tag to compare

@github-actions github-actions released this 18 Aug 23:39
· 29 commits to main since this release

108 commits, 152 files changed (+34,563 / −2,702), 8 PRs merged (#7#11, #15#18) since v1.6.1.

facetmark 2.0 is the "it just works" release. The biggest gap in 1.x was that facetmark serve started a server with no page to open — the extension needed a manual npm run build + load, docs/landing/ was read-only, and /docs was Swagger. For someone who doesn't write code, the project had no entry point after install. 2.0 fills that gap, and moves retrieval pagination, configuration, and administration onto HTTP at the same time.


✨ New Features

1. Local search page — GET /app

facetmark serve now ships a complete search UI — one HTML shell, one stylesheet, a handful of ES modules. No bundler, no Node build step, no new Python dependency.

  • Two views: search (using #11's offset/depth pagination, with depth pinned from the first page) and a library overview (/stats, answering "is my index built yet?").
  • Bilingual (EN/ZH) + light/dark theme: all strings live in static/strings.json; theme follows the system, manually toggleable, persisted to localStorage.
  • GET /app/boot: the sole endpoint for the pairing token. Returns the token only when the TCP peer is a loopback address and the Host header is a loopback literal — the second check blocks DNS rebinding. Falls back to manual paste otherwise.
  • GET /app/static/*: Starlette StaticFiles mount; ETag and 304 come for free.
  • The startup banner now prints open the search page: .../app.

2. Full-stack pagination (offset + depth)

"Only 20 results" was not one ceiling but four stacked: a page size, a missing offset, a candidate pool that conflated "how deep to retrieve" with "how many to return", and a reranker whose cost grew linearly with page size. 2.0 separates all four.

  • offset and depth plumbed through every surface: search() / quick_search() / HTTP /search /quick / MCP search_bookmarks / CLI (-o/--offset, --depth) / browser extension / karakeep bridge.
  • Six new response fields: limit, offset, depth, total, has_more, depth_capped — all report actual values, not echoes of the request.
  • depth is an explicit parameter because RRF is only depth-stable with a single facet. Deepening the pool to reach page 2 lets page 2 disagree with page 1. The fix is to pin depth: the response reports depth, the client sends it back, and every page is a slice of the same ranking.
  • Browser extension moves from a hardcoded api.search(q, 20) to "load more".
  • db.in_chunks(): batches IN (...) lookups in groups of 900 to avoid the 999-variable limit in distribution-built SQLite.

3. HTTP admin API

Import, index, and configure are all available over HTTP (new src/facetmark/admin.py). No CLI required — the browser settings page can import bookmarks, trigger indexing, and edit configuration.

4. File-based configuration

  • Read and write settings from <data_dir>/config.toml (new src/facetmark/configfile.py), with safe write-back.
  • Retrieval ceilings — MAX_PAGE_SIZE (200), MAX_CANDIDATE_DEPTH (2000), RERANK_DEPTH (20) — moved from scattered le= annotations into configuration. Over-limit is truncation, not rejection, and the truncated values are reported in the response.

5. Facet contribution reporting

Search responses now report each facet's contribution to the fused score, making "why this ranking" explainable.


🎨 UI & Documentation

Web UI — full rebuild

  • Apple system fonts + a wisteria palette; browns, coffees, dark greens and near-blacks removed.
  • Hand-drawn dashed visual language; all 8 views redrawn — white-card grids replaced with layering, hue, and dashed frames.
  • New setup screen and settings screen; every ranking shows "why this result".
  • Typographic scale pushed into the app UI, capsule navigation, macOS-style code windows.
  • color-scheme synced with the manual theme (light/dark paired), fixing UA widgets rendering dark when the system is dark but the user picked light.

Site & README

  • Site rebuilt as three bilingual pages (home / quickstart / guide) with a scroll-narrative landing page.
  • README fully rewritten (EN + ZH): centered hero, tree-directory TOC, GitHub Admonitions, horizontal Mermaid flowcharts, Star History chart.
  • New guide sections for Web UI and pagination; home page adds eight screenshots (search / library × EN/ZH × light/dark).
  • docs/ adds config.html, guide.html, quickstart.html, webui.html, integrations.html, measured.html (EN + ZH each).

🐛 Fixes

  • Empty results on chat-only libraries: when the embeddings endpoint 404s, search no longer returns an empty page for every query. search() auto-degrades to lexical facets and reports degraded_from with the replaced facet names.
  • synthesize empty answers: when all source excerpts are empty (pages imported but not yet indexed), the model is no longer called — the call is guaranteed to fail and costs money. Returns an explicit gap: "none of the sources have any indexed text; run facetmark index".
  • Unlabeled title-only summaries: pages whose body wasn't extracted get summaries inferred from title + URL, previously indistinguishable from real summaries. New basis column (body / title / karakeep); inferred summaries carry a badge, and /synthesize annotates them in the prompt and gap.
  • .gitignore swallowing wheel contents: line 23's *.html ate src/facetmark/web/index.html, so the build succeeded, the server started, and /app 404'd. Added !src/facetmark/web/*.html negation, backed by a wheel CI job.
  • Contrast: new --ink-mute token; rank numbers, footers and small text are ≥ 4.5:1 on every background — measured, not asserted in a comment.
  • Settings page: domain lists are now stored, uploads no longer drop titles, env-locked fields can no longer be edited.
  • Mobile: fifth view was unreachable, Escape didn't clear search — fixed.

⚠️ Behavior Changes

  • Rerank depth is no longer tied to page size: capped by RERANK_DEPTH (20), so per-page rerank cost is independent of page size. On reranking profiles (E, fused), pages beyond 20 items keep fusion order in the tail. This may affect relevance; the impact has not been measured.
  • CANDIDATES_PER_FACET changes from "pool size" to "pool floor": every retrieval is at least this deep, so a 5-row request still reports an honest total.
  • First-page depth changes from max(3 × limit, 30) to max(candidates_per_facet, 3 × window).
  • MCP search_bookmarks drops the static le on limit (default remains 10 — that's a context-window budget, not a recall ceiling).
  • karakeep bridge: retrieval window ceiling moves from literal 500 to MAX_CANDIDATE_DEPTH; truncated now reads the pipeline's has_more directly.

🔒 Security

  • Removed an accidentally committed personal bookmark export (favorites_2026_8_4.html, 1,710 HREF= entries with folders and timestamps) from HEAD and added a named .gitignore entry. This changes HEAD only, not history — the file is still reachable in old commits.
  • DNS rebinding protection on /app/boot (loopback address + loopback Host double check).

🧪 Testing & CI

  • Python tests 1188 → 1514 (+326), extension tests 16 → 28.
  • CI adds a webui job (node --test on pure-function modules) and a wheel job (builds the wheel and asserts web/index.html and three other files are inside).
  • scripts/browser_check.py: CI actually opens the page and looks — seven bugs were reintroduced one by one and all caught.
  • New star-history workflow (updates the Star History chart every Monday).
  • Site adds structural parity tests between EN and ZH content, plus "committed HTML equals re-rendered output".
  • Fixed a flaky Windows timing test (test_requests_to_one_host_are_spaced_apart): lowered the per-gap floor from 0.02 s to 0.01 s to tolerate Windows' ~15 ms timer resolution.

📦 Install & Upgrade

pip install facetmark==2.0.0

Or from source:

pip install -e '.[dev]'
facetmark import BOOKMARKS.html
facetmark index
facetmark serve          # then open .../app in your browser

Full Changelog: v1.6.1...v2.0.0