Improve sparse and structured catalog matching

This commit is contained in:
Fiden committed 2026-08-07 18:31:59 +03:00
1 parent 2982161374
commit 42f8f07207
8 files changed
+739 -20

No files matched your search

+185
View File
@@ -0,0 +1,185 @@
from __future__ import annotations
from decimal import Decimal
from pathlib import Path
from typing import Callable
import pytest
from app.catalog import CatalogItem, RawCatalogItem, load_raw_catalog
from app.matcher import CatalogMatcher
CATALOG_ITEMS = load_raw_catalog(
Path(__file__).resolve().parents[1] / "catalog_excel.csv"
)
@pytest.mark.parametrize("item", CATALOG_ITEMS, ids=lambda item: item.sku)
def test_full_catalog_name_retrieves_its_own_sku(
matcher: CatalogMatcher, item: RawCatalogItem
) -> None:
result = matcher.match(item.name)
assert item.sku in {candidate.sku for candidate in result.candidates}
def test_every_canonical_product_type_returns_only_its_own_items(
matcher: CatalogMatcher,
) -> None:
items_by_sku = {item.sku: item for item in matcher.items}
product_types = {item.product_type for item in matcher.items}
for product_type in product_types:
result = matcher.match(product_type)
assert result.candidates, product_type
assert all(
items_by_sku[candidate.sku].product_type == product_type
for candidate in result.candidates
), product_type
def test_every_catalog_model_code_returns_only_matching_items(
matcher: CatalogMatcher,
) -> None:
items_by_sku = {item.sku: item for item in matcher.items}
model_codes = {
model
for item in matcher.items
for model in item.attributes.model_codes
}
for model_code in model_codes:
result = matcher.match(model_code)
assert result.candidates, model_code
assert all(
model_code in items_by_sku[candidate.sku].attributes.model_codes
for candidate in result.candidates
), model_code
def test_every_catalog_brand_returns_only_its_own_items(
matcher: CatalogMatcher,
) -> None:
items_by_sku = {item.sku: item for item in matcher.items}
brands = {
item.attributes.brand
for item in matcher.items
if item.attributes.brand is not None
}
for brand in brands:
result = matcher.match(brand)
assert result.candidates, brand
assert all(
items_by_sku[candidate.sku].attributes.brand == brand
for candidate in result.candidates
), brand
def _number_text(value: Decimal) -> str:
return format(value, "f")
def _assert_query_returns_only(
matcher: CatalogMatcher,
query: str,
predicate: Callable[[CatalogItem], bool],
) -> None:
items_by_sku = {item.sku: item for item in matcher.items}
result = matcher.match(query)
assert result.candidates, query
assert all(
predicate(items_by_sku[candidate.sku]) for candidate in result.candidates
), query
def test_every_catalog_millimetre_value_is_searchable_without_product_name(
matcher: CatalogMatcher,
) -> None:
values = {
value
for item in matcher.items
for value in (
*item.attributes.mm_values,
*((item.attributes.dimensions[0],) if item.attributes.dimensions else ()),
*((item.attributes.thread_length_mm,) if item.attributes.thread_length_mm else ()),
)
}
for value in values:
_assert_query_returns_only(
matcher,
f"{_number_text(value)} мм",
lambda item, expected=value: (
expected in item.attributes.mm_values
or bool(
item.attributes.dimensions
and item.attributes.dimensions[0] == expected
)
or item.attributes.thread_length_mm == expected
),
)
def test_every_catalog_dimension_is_searchable_without_product_name(
matcher: CatalogMatcher,
) -> None:
dimensions = {
item.attributes.dimensions
for item in matcher.items
if item.attributes.dimensions
}
for dimension in dimensions:
query = "x".join(_number_text(value) for value in dimension)
_assert_query_returns_only(
matcher,
query,
lambda item, expected=dimension: (
len(item.attributes.dimensions) >= len(expected)
and item.attributes.dimensions[: len(expected)] == expected
),
)
@pytest.mark.parametrize(
("field", "suffix"),
[
("voltage_v", " В"),
("power_w", " Вт"),
("thread_size", ""),
("grit", ""),
("profile", ""),
("sds_type", ""),
("teeth", " зубьев"),
],
)
def test_every_scalar_catalog_attribute_is_searchable_without_product_name(
matcher: CatalogMatcher,
field: str,
suffix: str,
) -> None:
values = {
getattr(item.attributes, field)
for item in matcher.items
if getattr(item.attributes, field) is not None
}
for value in values:
if field == "thread_size":
query = f"M{_number_text(value)}"
elif field == "grit":
query = f"P{value}"
else:
value_text = _number_text(value) if isinstance(value, Decimal) else str(value)
query = f"{value_text}{suffix}"
_assert_query_returns_only(
matcher,
query,
lambda item, expected=value: getattr(item.attributes, field) == expected,
)