feat(ebook): add phrase matching display and update search result structure

This commit is contained in:
2026-07-24 11:38:50 -04:00
parent 11b5d5db3c
commit 34e7823517
3 changed files with 38 additions and 3 deletions
+22
View File
@@ -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;
@@ -82,6 +82,14 @@
</div>
{% endif %}
</dl>
{% if result.matched_phrases %}
<p class="phrase-matches">
<span class="phrase-matches-label">boosted by</span>
{% for phrase in result.matched_phrases %}
<span class="phrase-match">{{ phrase }}</span>
{% endfor %}
</p>
{% endif %}
</li>
{% endfor %}
</ol>
+8 -3
View File
@@ -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