Support incomplete model suffixes
This commit is contained in:
+36
-1
@@ -44,6 +44,7 @@ _MODEL_RE = re.compile(
|
|||||||
r"(?<![a-zа-я0-9])([a-zа-я]{2,6}-\d+[a-zа-я]?|[a-zа-я]{2,6}\d+[a-zа-я]?)(?![a-zа-я0-9])",
|
r"(?<![a-zа-я0-9])([a-zа-я]{2,6}-\d+[a-zа-я]?|[a-zа-я]{2,6}\d+[a-zа-я]?)(?![a-zа-я0-9])",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
|
_NORMALIZED_MODEL_RE = re.compile(r"^([a-zа-я]{2,6})(\d+)([a-zа-я]?)$", re.IGNORECASE)
|
||||||
|
|
||||||
_PAIR_DIMENSION_TYPES = frozenset(
|
_PAIR_DIMENSION_TYPES = frozenset(
|
||||||
{
|
{
|
||||||
@@ -229,6 +230,31 @@ def _extract_model_codes(normalized: str) -> frozenset[str]:
|
|||||||
return frozenset(codes)
|
return frozenset(codes)
|
||||||
|
|
||||||
|
|
||||||
|
def model_code_matches(query_code: str, candidate_code: str) -> bool:
|
||||||
|
"""Match a complete model or a model with only its final letter omitted.
|
||||||
|
|
||||||
|
Numeric parts remain exact. Thus ``арс12`` may complete to
|
||||||
|
``арс12л``, but it must not match the different model ``арс125``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if query_code == candidate_code:
|
||||||
|
return True
|
||||||
|
|
||||||
|
query_match = _NORMALIZED_MODEL_RE.fullmatch(query_code)
|
||||||
|
candidate_match = _NORMALIZED_MODEL_RE.fullmatch(candidate_code)
|
||||||
|
if query_match is None or candidate_match is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
query_prefix, query_number, query_suffix = query_match.groups()
|
||||||
|
candidate_prefix, candidate_number, candidate_suffix = candidate_match.groups()
|
||||||
|
return (
|
||||||
|
query_prefix == candidate_prefix
|
||||||
|
and query_number == candidate_number
|
||||||
|
and not query_suffix
|
||||||
|
and bool(candidate_suffix)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _detect_sds(normalized: str) -> str | None:
|
def _detect_sds(normalized: str) -> str | None:
|
||||||
flattened = normalized.replace("-", " ")
|
flattened = normalized.replace("-", " ")
|
||||||
if "sds max" in flattened or "сдс макс" in flattened:
|
if "sds max" in flattened or "сдс макс" in flattened:
|
||||||
@@ -645,7 +671,16 @@ def compare_attributes(
|
|||||||
return conflict
|
return conflict
|
||||||
|
|
||||||
if query.model_codes:
|
if query.model_codes:
|
||||||
conflict = require(query.model_codes.issubset(candidate.model_codes), "model")
|
conflict = require(
|
||||||
|
all(
|
||||||
|
any(
|
||||||
|
model_code_matches(query_code, candidate_code)
|
||||||
|
for candidate_code in candidate.model_codes
|
||||||
|
)
|
||||||
|
for query_code in query.model_codes
|
||||||
|
),
|
||||||
|
"model",
|
||||||
|
)
|
||||||
if conflict:
|
if conflict:
|
||||||
return conflict
|
return conflict
|
||||||
|
|
||||||
|
|||||||
+22
-4
@@ -10,6 +10,7 @@ from app.attributes import (
|
|||||||
Compatibility,
|
Compatibility,
|
||||||
ParsedAttributes,
|
ParsedAttributes,
|
||||||
compare_attributes,
|
compare_attributes,
|
||||||
|
model_code_matches,
|
||||||
parse_attributes,
|
parse_attributes,
|
||||||
)
|
)
|
||||||
from app.catalog import CatalogItem, build_catalog, load_raw_catalog
|
from app.catalog import CatalogItem, build_catalog, load_raw_catalog
|
||||||
@@ -109,11 +110,15 @@ class CatalogMatcher:
|
|||||||
product_types = {self.items[index].product_type for index in model_candidates}
|
product_types = {self.items[index].product_type for index in model_candidates}
|
||||||
if len(product_types) == 1:
|
if len(product_types) == 1:
|
||||||
product_type = next(iter(product_types))
|
product_type = next(iter(product_types))
|
||||||
|
exact_model = all(
|
||||||
|
code in self.model_index
|
||||||
|
for code in provisional_attributes.model_codes
|
||||||
|
)
|
||||||
product_match = ProductTypeMatch(
|
product_match = ProductTypeMatch(
|
||||||
canonical=product_type,
|
canonical=product_type,
|
||||||
score=1.0,
|
score=1.0 if exact_model else 0.93,
|
||||||
exact=True,
|
exact=exact_model,
|
||||||
alias="model_code",
|
alias="model_code" if exact_model else "model_code_prefix",
|
||||||
token_start=0,
|
token_start=0,
|
||||||
)
|
)
|
||||||
if product_match is None and provisional_attributes.teeth is not None:
|
if product_match is None and provisional_attributes.teeth is not None:
|
||||||
@@ -393,7 +398,20 @@ class CatalogMatcher:
|
|||||||
def _model_candidates(self, model_codes: frozenset[str]) -> frozenset[int]:
|
def _model_candidates(self, model_codes: frozenset[str]) -> frozenset[int]:
|
||||||
if not model_codes:
|
if not model_codes:
|
||||||
return frozenset()
|
return frozenset()
|
||||||
sets = [self.model_index.get(code, frozenset()) for code in model_codes]
|
|
||||||
|
sets: list[frozenset[int]] = []
|
||||||
|
for query_code in model_codes:
|
||||||
|
exact = self.model_index.get(query_code)
|
||||||
|
if exact:
|
||||||
|
sets.append(exact)
|
||||||
|
continue
|
||||||
|
|
||||||
|
partial_indexes: set[int] = set()
|
||||||
|
for candidate_code, indexes in self.model_index.items():
|
||||||
|
if model_code_matches(query_code, candidate_code):
|
||||||
|
partial_indexes.update(indexes)
|
||||||
|
sets.append(frozenset(partial_indexes))
|
||||||
|
|
||||||
if not sets or any(not indexes for indexes in sets):
|
if not sets or any(not indexes for indexes in sets):
|
||||||
return frozenset()
|
return frozenset()
|
||||||
result = set(sets[0])
|
result = set(sets[0])
|
||||||
|
|||||||
@@ -119,3 +119,4 @@ ToolKraft 125 мм
|
|||||||
дрель удар мощно 900 хромированый анус долбить
|
дрель удар мощно 900 хромированый анус долбить
|
||||||
анус
|
анус
|
||||||
долбить
|
долбить
|
||||||
|
Арс-12
|
||||||
|
|||||||
@@ -177,6 +177,54 @@ def test_unknown_toolkraft_model_does_not_fall_back_to_brand_results(
|
|||||||
assert result.candidates == []
|
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в"])
|
@pytest.mark.parametrize("query", ["шурпуоверт на 12в", "шураыввавёрт на 12в"])
|
||||||
def test_product_type_survives_typos(matcher: CatalogMatcher, query: str) -> None:
|
def test_product_type_survives_typos(matcher: CatalogMatcher, query: str) -> None:
|
||||||
result = matcher.match(query)
|
result = matcher.match(query)
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ MORE_MESSAGE_BASELINE_RESULTS: dict[str, ExpectedResult] = {
|
|||||||
"дрель удар мощно 900 хромированый анус долбить": ("ambiguous", ("INS-0009", "INS-0023", "INS-0002")),
|
"дрель удар мощно 900 хромированый анус долбить": ("ambiguous", ("INS-0009", "INS-0023", "INS-0002")),
|
||||||
"анус": ("not_found", ()),
|
"анус": ("not_found", ()),
|
||||||
"долбить": ("not_found", ()),
|
"долбить": ("not_found", ()),
|
||||||
|
"Арс-12": ("matched", ("INS-0017",)),
|
||||||
}
|
}
|
||||||
|
|
||||||
EXPECTED_RESULTS = VALIDATED_MESSAGE_RESULTS | MORE_MESSAGE_BASELINE_RESULTS
|
EXPECTED_RESULTS = VALIDATED_MESSAGE_RESULTS | MORE_MESSAGE_BASELINE_RESULTS
|
||||||
|
|||||||
Reference in New Issue
Block a user