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;
|
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 — developer diagnostics, hidden unless dev mode is on */
|
||||||
.runtime {
|
.runtime {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -82,6 +82,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dl>
|
</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>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ol>
|
</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 (
|
from python.ebook_search.protected_phrases.lib import (
|
||||||
HydratedPhraseMatch,
|
HydratedPhraseMatch,
|
||||||
detect_protected_phrases_for_query,
|
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.rerank import rerank_chunks
|
||||||
from python.ebook_search.timing import RuntimeStep, timed_result
|
from python.ebook_search.timing import RuntimeStep, timed_result
|
||||||
@@ -58,6 +58,7 @@ class SearchResult:
|
|||||||
fused_score: float | None = None
|
fused_score: float | None = None
|
||||||
rerank_score: float | None = None
|
rerank_score: float | None = None
|
||||||
phrase_hit_count: int = 0
|
phrase_hit_count: int = 0
|
||||||
|
matched_phrases: tuple[str, ...] = ()
|
||||||
source_author: str | None = None
|
source_author: str | None = None
|
||||||
chapter_title: str | None = None
|
chapter_title: str | None = None
|
||||||
page_label: 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]
|
chunk_ids = [candidate.chunk_id for candidate in candidates]
|
||||||
try:
|
try:
|
||||||
with Session(engine) as session:
|
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:
|
except SQLAlchemyError as error:
|
||||||
logger.warning("ebook_phrase_boost_unavailable error=%s", error)
|
logger.warning("ebook_phrase_boost_unavailable error=%s", error)
|
||||||
return candidates
|
return candidates
|
||||||
|
|
||||||
if not hit_counts:
|
if not phrase_hits:
|
||||||
return candidates
|
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 = [
|
boosted = [
|
||||||
replace(
|
replace(
|
||||||
candidate,
|
candidate,
|
||||||
score=candidate.score + (hit_counts.get(candidate.chunk_id, 0) * phrase_hit_boost),
|
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),
|
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),
|
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)),
|
rank_source=phrase_rank_source(candidate.rank_source, hit_counts.get(candidate.chunk_id, 0)),
|
||||||
)
|
)
|
||||||
for candidate in candidates
|
for candidate in candidates
|
||||||
|
|||||||
Reference in New Issue
Block a user