diff --git a/CHANGELOG.md b/CHANGELOG.md index 203696c..04f8060 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Не выпущено +- Backend обновлён до `0.10.4`: многословный поиск получил низковесный OR fallback с требованием совпадения минимум двух терминов; fallback пропускает распознанные названия нормативных актов. - Backend обновлён до `0.10.3`: запрос о налоге на прибыль поднимает действующий Налоговый кодекс; точное совпадение фразы дополнительно усиливает результат. - Backend обновлён до `0.10.2`: действующие документы ранжируются выше, а diff --git a/backend/README.md b/backend/README.md index 9713de9..63b5bda 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,6 +1,6 @@ # Backend Акылдаш -Версия: `0.10.3` +Версия: `0.10.4` Первая backend-область проекта — загрузка правовых документов из официального Open Data API ЦБД Минюста Кыргызской Республики. Код расположен в @@ -240,4 +240,4 @@ PYTHONPATH=backend python3 -m search.query "ЖЧК ачуу тартиби" --la --- -Акылдаш · Backend v0.10.3 · Frontend — не создан +Акылдаш · Backend v0.10.4 · Frontend — не создан diff --git a/backend/search/api.py b/backend/search/api.py index 7659d50..70c2e4a 100644 --- a/backend/search/api.py +++ b/backend/search/api.py @@ -17,7 +17,7 @@ from search.reviews import ReviewSnapshots, ReviewStore API_VERSION = "v1" -SEARCH_ALGORITHM_VERSION = "search-5" +SEARCH_ALGORITHM_VERSION = "search-6" LANGUAGES = {"ru", "ky"} CODE = re.compile(r"^[0-9]+$") MAX_PAGE_SIZE = 100 diff --git a/backend/search/minjust_opensearch.py b/backend/search/minjust_opensearch.py index fa8f9a4..7e3f841 100644 --- a/backend/search/minjust_opensearch.py +++ b/backend/search/minjust_opensearch.py @@ -17,7 +17,7 @@ from typing import Iterator from search.catalog import authority_codes, legal_force_code, source_code -APP_VERSION = "0.10.3" +APP_VERSION = "0.10.4" LANGUAGES = {"ru", "ky"} DEFAULT_MAPPING = Path(__file__).with_name("minjust-fragments-index.json") diff --git a/backend/search/query.py b/backend/search/query.py index 40690e9..9e9873a 100644 --- a/backend/search/query.py +++ b/backend/search/query.py @@ -24,6 +24,42 @@ LEGAL_FORM_ALIASES = { } COMPANY_REGISTRATION_DOCUMENTS = (("230044970", 3000), ("667", 2000), ("4", 1000)) TAX_PROFIT_DOCUMENTS = (("112340", 3000),) +BOILERPLATE_PREFIXES = { + "ru": ( + "как регулируется", "правовое регулирование", "судебная практика", "сроки и ответственность", + "действующая редакция", "последние изменения", "основные положения", "комментарий к акту", + ), + "ky": (), +} +ACT_TITLE_PREFIXES = { + "ru": ( + r"(?:гражданский\s+)?процессуальный\s+кодекс(?:\s+(?:кыргызской\s+республики|кр))?", + r"(?:гражданск\w*|уголовн\w*|налогов\w*|трудов\w*|семейн\w*)\s+кодекс\w*(?:\s+(?:кыргызской\s+республики|кр))?", + r"кодекс\s+о\s+правонарушениях(?:\s+(?:кыргызской\s+республики|кр))?", + r"конституци\w*(?:\s+(?:кыргызской\s+республики|кр))?", + r"(?:гк|ук|нк|коап|гпк|упк|уик)(?:\s+(?:кыргызской\s+республики|кр))?", + r"закон(?:\s+(?:кыргызской\s+республики|кр))?\s+о\s+(?:защит\w*\s+прав\w*\s+потребител\w*|государственн\w+\s+регистраци\w+\s+юридическ\w+\s+лиц|труд\w*)", + ), + "ky": ( + r"кыргыз\w*\s+республикасынын\s+конституци\w*", + r"(?:кыргыз\w*\s+республикасынын\s+)?(?:жарандык|кылмыш\w*|салык|эмгек|үй-бүлө|административдик|жарандык\s+процесстик)\s+кодекс\w*", + r"(?:кыргыз\w*\s+республикасынын\s+)?(?:жк|үк)(?:\s+кр)?", + ), +} + + +def relaxed_query_text(language: str, query: str) -> str: + value = query.strip() + for prefix in BOILERPLATE_PREFIXES[language]: + if value.casefold().startswith(prefix): + value = value[len(prefix):].strip() + break + for pattern in ACT_TITLE_PREFIXES[language]: + match = re.match(rf"\s*{pattern}(?=\s|$|[.,;:!?])", value, re.IGNORECASE) + if match: + value = value[match.end():].lstrip(" \t,;:—-") + break + return value def query_variants(language: str, query: str) -> list[str]: @@ -100,6 +136,7 @@ def tax_profit_clauses(language: str, query: str) -> list[dict]: def build_search_query(language: str, query: str) -> dict: registration_clauses = company_registration_clauses(language, query) intent_clauses = [*registration_clauses, *tax_profit_clauses(language, query)] + variants = query_variants(language, query) full_text = [ { "multi_match": { @@ -109,13 +146,34 @@ def build_search_query(language: str, query: str) -> dict: "operator": "OR" if registration_clauses else "AND", } } - for variant in query_variants(language, query) + for variant in variants ] + relaxed_text = [] + if not registration_clauses: + seen_relaxed = set() + for variant in variants: + value = relaxed_query_text(language, variant) + words = value.split() + if value in seen_relaxed or len(words) < 2 or (value == variant.strip() and len(words) < 3): + continue + if len(words) < 3 and not any(len(word) >= 5 for word in words): + continue + seen_relaxed.add(value) + relaxed_text.append({ + "multi_match": { + "query": value, + "fields": [f"document_name_{language}^4", f"text_{language}"], + "type": "cross_fields", + "operator": "OR", + "minimum_should_match": 2, + "boost": 0.1, + } + }) phrase_matches = [ {"match_phrase": {f"text_{language}": {"query": variant, "boost": 3}}} for variant in query_variants(language, query) ] - planned = {"should": [*full_text, *phrase_matches, *intent_clauses], "minimum_should_match": 1} + planned = {"should": [*full_text, *phrase_matches, *relaxed_text, *intent_clauses], "minimum_should_match": 1} return {"bool": planned} diff --git a/backend/search/review.html b/backend/search/review.html index f5d4316..4ac1cf1 100644 --- a/backend/search/review.html +++ b/backend/search/review.html @@ -117,7 +117,7 @@