feat(ebook): add phrase matching display and update search result structure
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user