evaluation

This commit is contained in:
Ján Pták 2026-08-14 22:58:43 +02:00
parent 0a3d898b2c
commit e4e5c77afb
10 changed files with 146565 additions and 117 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,11 @@ WORD_RE = re.compile(
re.UNICODE,
)
QUOTED_PHRASE_RE = re.compile(
r'[„“”"]([^„“”"]+)[„“”"]',
re.UNICODE,
)
BM25_WEIGHTS = (
0.0,
@ -45,6 +50,13 @@ FTS_RRF_WEIGHT = 1.0
VECTOR_RRF_WEIGHT = 1.5
ANY_TERM_RRF_WEIGHT = 0.25
# Ak FTS kandidát obsahuje veľmi silný
# lexikálny signál, napríklad meno osoby
# alebo presný názov práce, nesmie byť
# potlačený iba embeddingovým výsledkom.
LEXICAL_ANCHOR_MIN_BONUS = 4.5
LEXICAL_ANCHOR_RRF_WEIGHT = 2.0
STRATEGY_PRIORITY = {
"all_terms": 3,
@ -104,6 +116,42 @@ def query_tokens(
return tokens
def quoted_query_phrases(
query: str,
) -> list[str]:
phrases: list[str] = []
seen: set[str] = set()
for match in QUOTED_PHRASE_RE.finditer(
query
):
phrase = normalize_for_compare(
match.group(1)
)
if not phrase:
continue
# Jednoslovné výrazy ako "RAG"
# alebo "chatbot" sú príliš široké
# na silný lexikálny anchor.
if len(phrase.split()) < 2:
continue
if phrase in seen:
continue
phrases.append(
phrase
)
seen.add(
phrase
)
return phrases
def quote_fts_token(
token: str,
*,
@ -633,6 +681,25 @@ def exact_match_bonus(
bonus = 0.0
# Pri otázkach typu:
#
# Kto sa venuje práci s názvom
# „Systém získavania informácií
# v slovenskom jazyku“?
#
# je celý názov práce veľmi silný
# lexikálny signál, aj keď title
# dokumentu je meno študenta.
for phrase in quoted_query_phrases(
query
):
if (
phrase in title
or phrase in text
):
bonus += 6.0
break
if title == normalized_query:
bonus += 6.0
@ -781,9 +848,8 @@ def add_fts_metadata(
-bm25_score,
)
score = (
base_score
+ exact_match_bonus(
match_bonus = (
exact_match_bonus(
query,
item,
tags,
@ -791,6 +857,11 @@ def add_fts_metadata(
)
)
score = (
base_score
+ match_bonus
)
heading_paths = (
parse_heading_paths(
item
@ -943,6 +1014,49 @@ def add_vector_metadata(
return results
def lexical_bonus_from_fts_result(
item: dict[str, Any],
) -> float:
fts_score = item.get(
"score"
)
bm25_score = item.get(
"bm25_score"
)
if (
fts_score is None
or bm25_score is None
):
return 0.0
try:
score_value = float(
fts_score
)
bm25_value = float(
bm25_score
)
except (
TypeError,
ValueError,
):
return 0.0
base_score = max(
0.0,
-bm25_value,
)
return max(
0.0,
score_value - base_score,
)
def fuse_hybrid_results(
fts_results: list[
dict[str, Any]
@ -1002,6 +1116,33 @@ def fuse_hybrid_results(
else FTS_RRF_WEIGHT
)
lexical_bonus = (
lexical_bonus_from_fts_result(
item
)
)
# Silný lexikálny anchor má prednosť
# pred čisto sémantickou podobnosťou.
#
# Typické prípady:
# - meno osoby je priamo title dokumentu,
# - názov práce je uvedený v úvodzovkách,
# - query obsahuje celý názov dokumentu.
#
# Toto rieši najmä situáciu, keď
# any_term FTS nájde správneho Bogdana
# na 1. mieste, ale pôvodná váha 0.25
# ho po RRF vytlačila mimo Top 5.
if (
lexical_bonus
>= LEXICAL_ANCHOR_MIN_BONUS
):
fts_weight = max(
fts_weight,
LEXICAL_ANCHOR_RRF_WEIGHT,
)
scores[
chunk_id
] += (