diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b09603..b6a096a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Не выпущено +- Backend обновлён до `0.10.0`: поисковый API использует общий планировщик + запросов, расширяет аббревиатуры ОсОО/ООО/ЖЧК и распознаёт намерение регистрации. - Backend обновлён до `0.9.4`: из поисковых фрагментов убраны технические маркеры подсветки OpenSearch. - Backend обновлён до `0.9.3`: исправлен локальный индекс production Search API, diff --git a/README.md b/README.md index 4c05167..9bbc213 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,13 @@ Telegram-бот — только часть рабочего окружения ## Текущее состояние Сейчас реализованы Telegram-бот-секретарь версии `0.2.2` и backend версии -`0.9.4`. В поисковых фрагментах скрыты технические маркеры подсветки OpenSearch. +`0.10.0`. Поиск расширяет аббревиатуры организационно-правовых форм и поднимает +основные нормы о создании и регистрации ОсОО/ЖЧК для запросов об открытии. | Компонент | Версия | Состояние | |---|---:|---| | Telegram-бот | `0.2.2` | на Synology работает `0.2.1`; обновление после слияния | -| Backend | `0.9.4` | в поисковых фрагментах скрыты технические маркеры подсветки OpenSearch | +| Backend | `0.10.0` | расширение терминов и учет намерения при поиске регистрации ОсОО/ЖЧК | | Frontend | — | ещё не создан | | Сбор и обработка правовых данных | `0.8.0` | добавлены relevance set и оценка выдачи | | RAG и база знаний | — | ещё не созданы | @@ -58,4 +59,4 @@ python3 -m unittest discover -s tools/telegram-bot -v --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.9.4 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.10.0 · Frontend — не создан diff --git a/backend/README.md b/backend/README.md index 86177b3..7ffac86 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,6 +1,6 @@ # Backend Акылдаш -Версия: `0.9.4` +Версия: `0.10.0` Первая backend-область проекта — загрузка правовых документов из официального Open Data API ЦБД Минюста Кыргызской Республики. Код расположен в @@ -240,4 +240,4 @@ PYTHONPATH=backend python3 -m search.query "ЖЧК ачуу тартиби" --la --- -Акылдаш · Backend v0.9.4 · Frontend — не создан +Акылдаш · Backend v0.10.0 · Frontend — не создан diff --git a/backend/search/api.py b/backend/search/api.py index 24aa61f..cf41637 100644 --- a/backend/search/api.py +++ b/backend/search/api.py @@ -12,11 +12,12 @@ from pathlib import Path from search.minjust_opensearch import APP_VERSION, request_json from search.catalog import CATALOGS, labels +from search.query import build_search_query from search.reviews import ReviewSnapshots, ReviewStore API_VERSION = "v1" -SEARCH_ALGORITHM_VERSION = "search-1" +SEARCH_ALGORITHM_VERSION = "search-2" LANGUAGES = {"ru", "ky"} CODE = re.compile(r"^[0-9]+$") MAX_PAGE_SIZE = 100 @@ -139,7 +140,7 @@ class Api: "from": (page - 1) * page_size, "size": page_size + 1, "_source": ["document_code", "edition_code", "document_name_ru", "document_name_ky", "document_type_ru", "document_type_ky", "status_ru", "status_ky", "legal_force_code", "date_adopted", "number"], - "query": {"bool": {"filter": filters, "must": {"multi_match": {"query": text, "fields": [f"document_name_{language}", f"text_{language}"], "type": "cross_fields"}}}}, + "query": {"bool": {"filter": filters, "must": build_search_query(language, text)}}, "collapse": {"field": "document_code"}, "highlight": {"fields": {f"text_{language}": {"number_of_fragments": 1}}}, } diff --git a/backend/search/minjust_opensearch.py b/backend/search/minjust_opensearch.py index 2e9aeaf..a4751e2 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.9.4" +APP_VERSION = "0.10.0" 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 e108dfc..4548d7e 100644 --- a/backend/search/query.py +++ b/backend/search/query.py @@ -10,12 +10,52 @@ import urllib.parse from search.minjust_opensearch import APP_VERSION, request_json +LEGAL_FORM_ALIASES = { + "ru": { + "осоо": ("ооо", "общество с ограниченной ответственностью"), + "ооо": ("осоо", "общество с ограниченной ответственностью"), + "общество с ограниченной ответственностью": ("осоо", "ооо"), + "общества с ограниченной ответственностью": ("осоо", "ооо"), + }, + "ky": { + "жчк": ("жоопкерчилиги чектелген коом",), + "жоопкерчилиги чектелген коом": ("жчк",), + }, +} +COMPANY_REGISTRATION_DOCUMENTS = ("667", "4") + + +def query_variants(language: str, query: str) -> list[str]: + variants = [query] + for alias, expansions in LEGAL_FORM_ALIASES[language].items(): + pattern = re.compile(rf"(? list[dict]: - patterns = { - "ru": (r"\bкак\s+откры\w*\s+осоо\b", r"\b(порядок|процедура)\s+откры\w*\s+осоо\b", r"\bкак\s+зарегистр\w*\s+осоо\b"), - "ky": (r"\bжчк\s+ач\w*\s+тартиби\b", r"\bжчк\s+кантип\s+ач\w*\b"), + forms, patterns = { + "ru": ( + r"(?:осоо|ооо|обществ\w*\s+с\s+ограниченной\s+ответственностью)", + ( + r"\bкак\s+(?:откры\w*|созда\w*|зарегистр\w*)\s+{form}\b", + r"\b(?:порядок|процедура)\s+(?:открыти\w*|создани\w*|регистраци\w*)\s+{form}\b", + r"\b(?:государственн\w+\s+)?регистраци\w*\s+{form}\b", + r"\bсоздани\w*\s+{form}\b", + ), + ), + "ky": ( + r"(?:жчк|жоопкерчилиги\s+чектелген\s+коом)", + ( + r"\bжчк\s+ач\w*\s+тартиби\b", + r"\bжчк\s+кантип\s+ач\w*\b", + r"\b{form}\s+(?:ачуу|ачуунун|каттоо|каттоонун)\b", + r"\b(?:ачуу|каттоо)\s+{form}\b", + ), + ), }[language] - if not any(re.search(pattern, query.casefold()) for pattern in patterns): + if not any(re.search(pattern.format(form=forms), query.casefold()) for pattern in patterns): return [] status = {"ru": "Действует", "ky": "Күчүндө"}[language] @@ -35,28 +75,34 @@ def company_registration_clauses(language: str, query: str) -> list[dict]: } } - return [clause("230044970", 2000), clause("667", 1000)] + return [clause(COMPANY_REGISTRATION_DOCUMENTS[0], 2000), clause(COMPANY_REGISTRATION_DOCUMENTS[1], 1000)] + + +def build_search_query(language: str, query: str) -> dict: + intent_clauses = company_registration_clauses(language, query) + full_text = [ + { + "multi_match": { + "query": variant, + "fields": [f"document_name_{language}^4", f"text_{language}"], + "type": "cross_fields", + "operator": "OR" if intent_clauses else "AND", + } + } + for variant in query_variants(language, query) + ] + planned = {"should": [*full_text, *intent_clauses], "minimum_should_match": 1} + if intent_clauses: + planned["filter"] = [{"terms": {"document_code": list(COMPANY_REGISTRATION_DOCUMENTS)}}] + return {"bool": planned} def build_search_body(language: str, query: str, top_k: int) -> bytes: - full_text = { - "multi_match": { - "query": query, - "fields": [f"document_name_{language}", f"text_{language}"], - "type": "cross_fields", - } - } - clauses = company_registration_clauses(language, query) - bool_query = {"filter": {"term": {"language": language}}} - if clauses: - bool_query.update({"should": [full_text, *clauses], "minimum_should_match": 1}) - else: - bool_query["must"] = full_text return json.dumps({ "size": top_k, "track_total_hits": False, "_source": ["document_code"], - "query": {"bool": bool_query}, + "query": {"bool": {"filter": [{"term": {"language": language}}, {"term": {"is_current_edition": True}}], "must": build_search_query(language, query)}}, "collapse": {"field": "document_code"}, }, ensure_ascii=False).encode() diff --git a/backend/search/review.html b/backend/search/review.html index 4f7864f..3907d2b 100644 --- a/backend/search/review.html +++ b/backend/search/review.html @@ -119,7 +119,7 @@