deploy / deploy (push) Successful in 30s
search_kb_rows had no relevance floor: any exact-token hit, however generic, scored above zero and could win as the top/only result. A caller utterance as thin as a bare "да" (confirming the language) could exact-match that same common word inside an unrelated FAQ article's body and get returned as "the" answer, which then got read back almost verbatim — this is what surfaced live as the AI unprompted launching into a voucher-activation explanation right after the customer confirmed Russian, having said nothing else. tokenize_kb_text now drops a curated set of RU/KZ greetings, confirmations, pronouns, and particles. A stopword-only query naturally falls through to the existing "no query tokens -> no results" path instead of returning a coincidental match; genuine single-content-word queries (e.g. "ваучер") are unaffected. Applies to every channel that calls _kb_search (voice, Telegram, WhatsApp), not just voice.
189 lines
7.1 KiB
Python
189 lines
7.1 KiB
Python
import json
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from services.shared.kb_search import normalize_kb_text, search_kb_rows
|
|
|
|
|
|
def _u(value: str) -> str:
|
|
return value.encode("ascii").decode("unicode_escape")
|
|
|
|
|
|
def _row(*, row_id: int, title: str, body: str, tags: list[str]):
|
|
return SimpleNamespace(
|
|
id=row_id,
|
|
title=title,
|
|
body=body,
|
|
tags_json=json.dumps(tags, ensure_ascii=False),
|
|
)
|
|
|
|
|
|
def test_normalize_kb_text_collapses_punctuation_and_yo():
|
|
value = _u(r"\u0401\u0436,\u0438\u043a!!! \u0433\u0440\u0430\u0444\u0438\u043a?")
|
|
assert normalize_kb_text(value) == _u(r"\u0435\u0436 \u0438\u043a \u0433\u0440\u0430\u0444\u0438\u043a")
|
|
|
|
|
|
def test_search_kb_rows_matches_long_phrase_to_graphik_article():
|
|
rows = [
|
|
_row(
|
|
row_id=1,
|
|
title=_u(r"\u0413\u0440\u0430\u0444\u0438\u043a \u0440\u0430\u0431\u043e\u0442\u044b"),
|
|
body=_u(r"\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0433\u043e\u0440\u043e\u0434 \u0438\u043b\u0438 \u0444\u0438\u043b\u0438\u0430\u043b."),
|
|
tags=[_u(r"\u0440\u0435\u0436\u0438\u043c \u0440\u0430\u0431\u043e\u0442\u044b")],
|
|
),
|
|
_row(
|
|
row_id=2,
|
|
title=_u(r"\u0422\u0430\u0440\u0438\u0444\u044b"),
|
|
body=_u(r"\u0423\u0442\u043e\u0447\u043d\u0438\u0442\u0435 \u0443\u0441\u043b\u0443\u0433\u0443 \u0438\u043b\u0438 \u0442\u0430\u0440\u0438\u0444."),
|
|
tags=[_u(r"\u0446\u0435\u043d\u0430")],
|
|
),
|
|
]
|
|
|
|
results = search_kb_rows(
|
|
rows,
|
|
_u(r"\u0445\u043e\u0447\u0443 \u0443\u0437\u043d\u0430\u0442\u044c \u0433\u0440\u0430\u0444\u0438\u043a \u0440\u0430\u0431\u043e\u0442\u044b"),
|
|
limit=5,
|
|
)
|
|
|
|
assert results[0].title == rows[0].title
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("query", "expected_title"),
|
|
[
|
|
(
|
|
_u(r"\u0430\u0434\u0440\u0435\u0441\u0430"),
|
|
_u(r"\u0410\u0434\u0440\u0435\u0441 \u0438 \u0444\u0438\u043b\u0438\u0430\u043b"),
|
|
),
|
|
(
|
|
_u(r"\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u0443"),
|
|
_u(r"\u0421\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441 \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u043c"),
|
|
),
|
|
(
|
|
_u(r"\u0437\u0430\u044f\u0432\u043a\u0438"),
|
|
_u(r"\u0421\u0442\u0430\u0442\u0443\u0441 \u0437\u0430\u044f\u0432\u043a\u0438"),
|
|
),
|
|
],
|
|
)
|
|
def test_search_kb_rows_matches_close_word_forms(query: str, expected_title: str):
|
|
rows = [
|
|
_row(
|
|
row_id=1,
|
|
title=_u(r"\u0410\u0434\u0440\u0435\u0441 \u0438 \u0444\u0438\u043b\u0438\u0430\u043b"),
|
|
body=_u(r"\u041d\u0430\u0437\u043e\u0432\u0438\u0442\u0435 \u0433\u043e\u0440\u043e\u0434."),
|
|
tags=[_u(r"\u0430\u0434\u0440\u0435\u0441")],
|
|
),
|
|
_row(
|
|
row_id=2,
|
|
title=_u(r"\u0421\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441 \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u043c"),
|
|
body=_u(r"\u0421\u043a\u0430\u0436\u0438\u0442\u0435, \u0435\u0441\u043b\u0438 \u043d\u0443\u0436\u0435\u043d \u0447\u0435\u043b\u043e\u0432\u0435\u043a."),
|
|
tags=[_u(r"\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440")],
|
|
),
|
|
_row(
|
|
row_id=3,
|
|
title=_u(r"\u0421\u0442\u0430\u0442\u0443\u0441 \u0437\u0430\u044f\u0432\u043a\u0438"),
|
|
body=_u(r"\u041d\u0443\u0436\u0435\u043d \u043d\u043e\u043c\u0435\u0440 \u0437\u0430\u044f\u0432\u043a\u0438."),
|
|
tags=[_u(r"\u0437\u0430\u044f\u0432\u043a\u0430")],
|
|
),
|
|
]
|
|
|
|
results = search_kb_rows(rows, query, limit=5)
|
|
|
|
assert results[0].title == expected_title
|
|
|
|
|
|
def test_search_kb_rows_prefers_title_and_tags_over_body_only_mentions():
|
|
rows = [
|
|
_row(
|
|
row_id=1,
|
|
title=_u(r"\u0421\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441 \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u043c"),
|
|
body=_u(r"\u0415\u0441\u043b\u0438 \u043d\u0443\u0436\u0435\u043d \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440, \u0441\u043a\u0430\u0436\u0438\u0442\u0435 \u043e\u0431 \u044d\u0442\u043e\u043c."),
|
|
tags=[_u(r"\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440")],
|
|
),
|
|
_row(
|
|
row_id=2,
|
|
title=_u(r"\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u0441 \u0443\u0441\u043b\u0443\u0433\u043e\u0439"),
|
|
body=_u(r"\u0412 \u0441\u043b\u043e\u0436\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0437\u0432\u043e\u043d\u043e\u043a \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u043d \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u0443."),
|
|
tags=[_u(r"\u043e\u0448\u0438\u0431\u043a\u0430")],
|
|
),
|
|
]
|
|
|
|
results = search_kb_rows(rows, _u(r"\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440"), limit=5)
|
|
|
|
assert results[0].title == rows[0].title
|
|
|
|
|
|
def test_search_kb_rows_ignores_stopword_only_query():
|
|
# A caller confirming the language ("да") should never surface an
|
|
# unrelated FAQ article just because that article's body happens to
|
|
# contain the word "да" somewhere in ordinary prose.
|
|
rows = [
|
|
_row(
|
|
row_id=1,
|
|
title="Активация ваучера",
|
|
body="Да, подтвердите СМС с номера 1414 командой 21*1.",
|
|
tags=["ваучер"],
|
|
),
|
|
]
|
|
|
|
assert search_kb_rows(rows, "да", limit=5) == []
|
|
assert search_kb_rows(rows, "Хорошо", limit=5) == []
|
|
|
|
|
|
def test_search_kb_rows_still_matches_real_content_word_amid_fillers():
|
|
rows = [
|
|
_row(
|
|
row_id=1,
|
|
title="Активация ваучера",
|
|
body="Подтвердите СМС с номера 1414 командой 21*1.",
|
|
tags=["ваучер"],
|
|
),
|
|
_row(
|
|
row_id=2,
|
|
title="График работы",
|
|
body="Филиалы работают с 9 до 18.",
|
|
tags=["график"],
|
|
),
|
|
]
|
|
|
|
results = search_kb_rows(rows, "да, у меня вопрос про ваучер", limit=5)
|
|
|
|
assert results
|
|
assert results[0].title == "Активация ваучера"
|
|
|
|
|
|
def test_search_kb_rows_single_real_word_query_still_matches():
|
|
rows = [
|
|
_row(
|
|
row_id=1,
|
|
title="Активация ваучера",
|
|
body="Подтвердите СМС с номера 1414 командой 21*1.",
|
|
tags=["ваучер"],
|
|
),
|
|
]
|
|
|
|
results = search_kb_rows(rows, "ваучер", limit=5)
|
|
|
|
assert results
|
|
assert results[0].title == "Активация ваучера"
|
|
|
|
|
|
def test_search_kb_rows_breaks_ties_by_newer_id():
|
|
older = _row(
|
|
row_id=10,
|
|
title="KB Freshness",
|
|
body="Freshness article body",
|
|
tags=["freshkb"],
|
|
)
|
|
newer = _row(
|
|
row_id=11,
|
|
title="KB Freshness",
|
|
body="Freshness article body",
|
|
tags=["freshkb"],
|
|
)
|
|
|
|
results = search_kb_rows([older, newer], "freshkb", limit=5)
|
|
|
|
assert [item.id for item in results[:2]] == [11, 10]
|