From e34ed6c5970a47f57fa2e83022aeec656c42c3e8 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Fri, 3 Jul 2026 19:44:12 -0400 Subject: [PATCH] feat(ebook): add phrase matching display and update search result structure --- python/ebook_search/api/static/style.css | 22 +++++++++++++++++++ .../api/templates/partials/results.html | 8 +++++++ python/ebook_search/search.py | 11 +++++++--- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/python/ebook_search/api/static/style.css b/python/ebook_search/api/static/style.css index 4644985..4eac52e 100644 --- a/python/ebook_search/api/static/style.css +++ b/python/ebook_search/api/static/style.css @@ -309,6 +309,28 @@ button:hover { font-variant-numeric: tabular-nums; } +.phrase-matches { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: baseline; + margin: 10px 0 0; + font-size: 0.78rem; +} + +.phrase-matches-label { + color: var(--muted); + font-weight: 600; +} + +.phrase-match { + padding: 3px 10px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 999px; + color: var(--accent); +} + /* Runtime — developer diagnostics, hidden unless dev mode is on */ .runtime { display: none; diff --git a/python/ebook_search/api/templates/partials/results.html b/python/ebook_search/api/templates/partials/results.html index 2e0b7a1..7f6af52 100644 --- a/python/ebook_search/api/templates/partials/results.html +++ b/python/ebook_search/api/templates/partials/results.html @@ -82,6 +82,14 @@ {% endif %} + {% if result.matched_phrases %} +

+ boosted by + {% for phrase in result.matched_phrases %} + {{ phrase }} + {% endfor %} +

+ {% endif %} {% endfor %} diff --git a/python/ebook_search/search.py b/python/ebook_search/search.py index b266c2a..cd9ca1a 100644 --- a/python/ebook_search/search.py +++ b/python/ebook_search/search.py @@ -23,7 +23,7 @@ from python.ebook_search.embeddings import MODEL_DIMENSIONS, embed_query, get_em from python.ebook_search.protected_phrases.lib import ( HydratedPhraseMatch, detect_protected_phrases_for_query, - phrase_hit_counts_for_chunks, + phrase_hits_for_chunks, ) from python.ebook_search.rerank import rerank_chunks from python.ebook_search.timing import RuntimeStep, timed_result @@ -58,6 +58,7 @@ class SearchResult: fused_score: float | None = None rerank_score: float | None = None phrase_hit_count: int = 0 + matched_phrases: tuple[str, ...] = () source_author: str | None = None chapter_title: str | None = None page_label: str | None = None @@ -201,20 +202,24 @@ def apply_phrase_mention_boosts( chunk_ids = [candidate.chunk_id for candidate in candidates] try: with Session(engine) as session: - hit_counts = phrase_hit_counts_for_chunks(session, chunk_ids=chunk_ids, phrase_ids=phrase_ids) + phrase_hits = phrase_hits_for_chunks(session, chunk_ids=chunk_ids, phrase_ids=phrase_ids) except SQLAlchemyError as error: logger.warning("ebook_phrase_boost_unavailable error=%s", error) return candidates - if not hit_counts: + if not phrase_hits: return candidates + hit_counts = { + chunk_id: sum(hit.mention_count for hit in chunk_hits) for chunk_id, chunk_hits in phrase_hits.items() + } boosted = [ replace( candidate, score=candidate.score + (hit_counts.get(candidate.chunk_id, 0) * phrase_hit_boost), fused_score=boosted_fused_score(candidate, hit_counts.get(candidate.chunk_id, 0), phrase_hit_boost), phrase_hit_count=hit_counts.get(candidate.chunk_id, 0), + matched_phrases=tuple(hit.phrase_text for hit in phrase_hits.get(candidate.chunk_id, ())), rank_source=phrase_rank_source(candidate.rank_source, hit_counts.get(candidate.chunk_id, 0)), ) for candidate in candidates