Files
test-search-engine/tests/test_matcher.py
T

402 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import pytest
from app.matcher import CatalogMatcher
@pytest.mark.parametrize(
("message", "sku"),
[
("дрель ударная prowerk pw-750 в наличии?", "INS-0008"),
("кабель шввп 2х0.5 сколько за метр", "KAB-0017"),
("диск пильный 190 на 48 зубьев", "DSK-0034"),
("уровень 1000 мм есть?", "RIN-0024"),
("гайки м10 нужны", "KRP-0020"),
("лента фум 12 мм", "RAS-0060"),
("перчатки нитриловые есть?", "PER-0002"),
("проф труба 20х20 стенка полтора", "TRB-0002"),
("гкл 9.5 сколько лист", "GKL-0001"),
("хомуты пластиковые 4.8х400", "RAS-0014"),
("сдс бур 6 на 110", "BIT-0074"),
("наждачка р120 листами", "RAS-0050"),
("круг зачистной на 125", "DSK-0018"),
("бита ph2 50 мм", "BIT-0005"),
("саморезы по дереву 3.5х45 пачка 200", "SAM-0010"),
("труба профильная 20х20х2", "TRB-0003"),
("ввгнг лс 3х1.5", "KAB-0010"),
("диск отрезной по металлу 115х1.2", "DSK-0002"),
("бур sds 10х210", "BIT-0080"),
("дюбель-гвоздь 6х60", "KRP-0048"),
],
)
def test_unique_queries_are_matched(
matcher: CatalogMatcher, message: str, sku: str
) -> None:
result = matcher.match(message)
assert result.status == "matched"
assert [candidate.sku for candidate in result.candidates] == [sku]
assert 0.0 <= result.candidates[0].confidence <= 1.0
@pytest.mark.parametrize(
"message",
[
"саморезы по металлу 4.2х70",
"труба профильная 35х35",
"кабель ввгнг 4х2.5",
"ушм на 150",
"бита t50",
"шпилька м16 метровая",
"бур 10х220",
"бита ph2 70 мм",
],
)
def test_explicit_attribute_conflicts_are_not_found(
matcher: CatalogMatcher, message: str
) -> None:
result = matcher.match(message)
assert result.status == "not_found"
assert result.candidates == []
@pytest.mark.parametrize(
"message",
[
"нужен кабель",
"дайте дюбелей",
"сверло нужно",
"какие есть диски",
"перфоратор посоветуйте",
"шуруповерт как у макиты, только дешевле",
"шурик на 12в недорогой",
"болгарка на 230 какая есть",
"гкл 12.5",
],
)
def test_under_specified_product_queries_are_ambiguous(
matcher: CatalogMatcher, message: str
) -> None:
result = matcher.match(message)
assert result.status == "ambiguous"
assert 2 <= len(result.candidates) <= 3
def test_unspecified_packaging_remains_ambiguous(matcher: CatalogMatcher) -> None:
result = matcher.match("здравствуйте, есть саморезы гкл 3.5х25?")
assert result.status == "ambiguous"
assert {candidate.sku for candidate in result.candidates} == {
"SAM-0061",
"SAM-0062",
"SAM-0063",
}
def test_package_word_filters_out_kilogram_item(matcher: CatalogMatcher) -> None:
result = matcher.match("саморезы по дереву 4.2 на 75, пачку")
assert result.status == "ambiguous"
assert {candidate.sku for candidate in result.candidates} == {
"SAM-0022",
"SAM-0023",
}
@pytest.mark.parametrize(
"message",
[
"здравствуйте, вы до скольки работаете?",
"можно оплатить картой при получении?",
"где находится ваш магазин",
"статус заказа 4512 подскажите",
"спасибо, заказ получил, все отлично",
"",
],
)
def test_non_product_messages_are_not_found(
matcher: CatalogMatcher, message: str
) -> None:
result = matcher.match(message)
assert result.status == "not_found"
assert result.candidates == []
def test_product_evidence_has_priority_over_service_words(matcher: CatalogMatcher) -> None:
result = matcher.match("статус заказа 4512 и еще нужен бур 8х160")
assert result.status == "matched"
assert result.candidates[0].sku == "BIT-0077"
@pytest.mark.parametrize("brand", ["makita", "макита", "макиты", "макиту"])
def test_makita_transliterations_share_the_same_brand_alias(
matcher: CatalogMatcher, brand: str
) -> None:
result = matcher.match(f"шуруповерт как у {brand}, только дешевле")
assert result.status == "ambiguous"
assert [candidate.sku for candidate in result.candidates] == [
"INS-0017",
"INS-0010",
"INS-0003",
]
def test_cheap_preference_orders_candidates_by_ascending_price(
matcher: CatalogMatcher,
) -> None:
result = matcher.match("шуруповерт как у макиты, только дешевле")
prices_by_sku = {item.sku: item.price for item in matcher.items}
candidate_prices = [
prices_by_sku[candidate.sku] for candidate in result.candidates
]
assert candidate_prices == sorted(candidate_prices)
@pytest.mark.parametrize(
"brand", ["ToolKraft", "toolkraft", "тулкрафт", "тул крафт"]
)
def test_brand_only_toolkraft_query_returns_catalog_items(
matcher: CatalogMatcher, brand: str
) -> None:
result = matcher.match(brand)
assert result.status == "ambiguous"
assert [candidate.sku for candidate in result.candidates] == [
"INS-0022",
"INS-0023",
"INS-0024",
]
def test_unknown_toolkraft_model_does_not_fall_back_to_brand_results(
matcher: CatalogMatcher,
) -> None:
result = matcher.match("ToolKraft TK-999")
assert result.status == "not_found"
assert result.candidates == []
@pytest.mark.parametrize(
("query", "sku"),
[
("Арс-12", "INS-0017"),
("АРС12", "INS-0017"),
("АРС-18", "INS-0018"),
("PW-12", "INS-0010"),
("TK-12", "INS-0024"),
("ТР-12", "INS-0003"),
],
)
def test_model_code_can_omit_only_its_final_letter(
matcher: CatalogMatcher, query: str, sku: str
) -> None:
result = matcher.match(query)
assert result.status == "matched"
assert [candidate.sku for candidate in result.candidates] == [sku]
@pytest.mark.parametrize(
"query",
[
"АРС-1",
"АРС-999",
"PW-90",
"TK-999",
"УШМ АРС-12",
],
)
def test_partial_model_does_not_guess_numeric_or_conflicting_models(
matcher: CatalogMatcher, query: str
) -> None:
result = matcher.match(query)
assert result.status == "not_found"
assert result.candidates == []
def test_complete_neighboring_model_keeps_exact_priority(
matcher: CatalogMatcher,
) -> None:
result = matcher.match("АРС-125")
assert result.status == "matched"
assert [candidate.sku for candidate in result.candidates] == ["INS-0020"]
@pytest.mark.parametrize("query", ["шурпуоверт на 12в", "шураыввавёрт на 12в"])
def test_product_type_survives_typos(matcher: CatalogMatcher, query: str) -> None:
result = matcher.match(query)
assert result.status == "ambiguous"
assert all(candidate.sku.startswith("INS-") for candidate in result.candidates)
@pytest.mark.parametrize(
("query", "skus"),
[
("удар", ("INS-0008", "INS-0009", "INS-0022")),
("ударная", ("INS-0008", "INS-0009", "INS-0022")),
("удар мощный", ("INS-0008", "INS-0009", "INS-0022")),
(
"удар мощно 900 хромированый анус долбить",
("INS-0009", "INS-0016", "INS-0023"),
),
(
"дрель удар мощно 900 хромированый анус долбить",
("INS-0009", "INS-0023", "INS-0002"),
),
],
)
def test_trigram_fallback_finds_product_type_from_sparse_or_noisy_text(
matcher: CatalogMatcher, query: str, skus: tuple[str, ...]
) -> None:
result = matcher.match(query)
assert result.status == "ambiguous"
assert tuple(candidate.sku for candidate in result.candidates) == skus
items_by_sku = {item.sku: item for item in matcher.items}
assert {
items_by_sku[candidate.sku].product_type for candidate in result.candidates
} == {"дрель"}
@pytest.mark.parametrize(
"query",
[
"анус",
"долбить",
"какой адрес",
"аквариум 125 мм",
"<script>alert(1)</script>",
"qwerty asdf zxcv",
],
)
def test_trigram_fallback_rejects_weak_or_accidental_similarity(
matcher: CatalogMatcher, query: str
) -> None:
result = matcher.match(query)
assert result.status == "not_found"
assert result.candidates == []
def test_bare_size_is_parsed_without_unit(matcher: CatalogMatcher) -> None:
assert matcher.match("бита ph2 50").candidates[0].sku == "BIT-0005"
assert matcher.match("диск пильный 190 48 зубьев").candidates[0].sku == "DSK-0034"
def test_unknown_size_does_not_fall_back_to_nearest_product(
matcher: CatalogMatcher,
) -> None:
result = matcher.match("ушм 150")
assert result.status == "not_found"
assert result.candidates == []
@pytest.mark.parametrize(
("query", "sku"),
[
("165 мм 24 зуба", "DSK-0029"),
("165 мм 36 зубов", "DSK-0030"),
("165 мм 48 зубьев", "DSK-0031"),
("190 мм 24 зуба", "DSK-0032"),
("190 мм 36 зубов", "DSK-0033"),
("190 мм 48 зубьев", "DSK-0034"),
("210 мм 24 зуба", "DSK-0035"),
("210 мм 36 зубов", "DSK-0036"),
("210 мм 48 зубьев", "DSK-0037"),
],
)
def test_tooth_count_infers_saw_blade_without_product_name(
matcher: CatalogMatcher, query: str, sku: str
) -> None:
result = matcher.match(query)
assert result.status == "matched"
assert [candidate.sku for candidate in result.candidates] == [sku]
@pytest.mark.parametrize(
("query", "skus"),
[
("24 зуба", ("DSK-0029", "DSK-0032", "DSK-0035")),
("36 зубов", ("DSK-0030", "DSK-0033", "DSK-0036")),
("48 зубьев", ("DSK-0031", "DSK-0034", "DSK-0037")),
],
)
def test_bare_tooth_count_returns_all_matching_saw_blade_sizes(
matcher: CatalogMatcher, query: str, skus: tuple[str, ...]
) -> None:
result = matcher.match(query)
assert result.status == "ambiguous"
assert tuple(candidate.sku for candidate in result.candidates) == skus
def test_unknown_tooth_count_does_not_return_nearest_saw_blade(
matcher: CatalogMatcher,
) -> None:
result = matcher.match("210 мм 25 зубьев")
assert result.status == "not_found"
assert result.candidates == []
@pytest.mark.parametrize(
"query",
[
"пильный диск 190x48t",
"190x48T",
"190x48z",
"190 48t",
"190/48T",
],
)
def test_compact_saw_notation_parses_diameter_and_teeth(
matcher: CatalogMatcher, query: str
) -> None:
result = matcher.match(query)
assert result.status == "matched"
assert [candidate.sku for candidate in result.candidates] == ["DSK-0034"]
def test_bare_t_suffix_infers_saw_blades(matcher: CatalogMatcher) -> None:
result = matcher.match("24T")
assert result.status == "ambiguous"
assert [candidate.sku for candidate in result.candidates] == [
"DSK-0029",
"DSK-0032",
"DSK-0035",
]
@pytest.mark.parametrize(
("query", "status", "skus"),
[
("125 мм", "ambiguous", ("INS-0013", "DSK-0025", "RAS-0054")),
("12 В", "ambiguous", ("INS-0003", "INS-0010", "INS-0024")),
("900 Вт", "ambiguous", ("INS-0009", "INS-0013")),
("M10", "ambiguous", ("KRP-0030", "KRP-0020", "KRP-0009")),
("P120", "ambiguous", ("DSK-0024", "RAS-0050", "RAS-0057")),
("SDS-plus", "ambiguous", ("BIT-0073", "INS-0012")),
("PH2", "ambiguous", ("BIT-0004", "RIN-0010")),
("20x20x2", "matched", ("TRB-0003",)),
("ToolKraft 125 мм", "matched", ("INS-0027",)),
("99999 мм", "not_found", ()),
("аквариум 125 мм", "not_found", ()),
],
)
def test_attribute_only_queries_use_exact_catalog_values(
matcher: CatalogMatcher,
query: str,
status: str,
skus: tuple[str, ...],
) -> None:
result = matcher.match(query)
assert result.status == status
assert tuple(candidate.sku for candidate in result.candidates) == skus