diff --git a/CHANGELOG.md b/CHANGELOG.md index 7318e0a..28cfe6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ - Добавлен план внутреннего интерфейса оценки поисковой выдачи юристами. - Завершён Search API v1: стабильные справочники, валидный OpenAPI, безопасная пагинация и проверка актуальных редакций в локальном OpenSearch. +- Добавлен фильтр Search API v1 по укрупнённой юридической силе; полная + иерархия GeneralClassifiers опубликована как черновой каталог вне API v1. - Добавлено безопасное переключение alias на новую версию поискового индекса после полной загрузки; checkpoint защищает возобновление загрузки от смены alias. diff --git a/README.md b/README.md index deb2842..26a0911 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,12 @@ Telegram-бот — только часть рабочего окружения ## Текущее состояние Сейчас реализованы Telegram-бот-секретарь версии `0.2.2` и backend версии -`0.8.3`: исправлена безопасность production Docker build context. +`0.9.0`: добавлена фильтрация поиска по юридической силе. | Компонент | Версия | Состояние | |---|---:|---| | Telegram-бот | `0.2.2` | на Synology работает `0.2.1`; обновление после слияния | -| Backend | `0.8.3` | исправлена безопасность production build context | +| Backend | `0.9.0` | фильтр поиска по юридической силе и черновик тематического каталога | | Frontend | — | ещё не создан | | Сбор и обработка правовых данных | `0.8.0` | добавлены relevance set и оценка выдачи | | RAG и база знаний | — | ещё не созданы | @@ -58,4 +58,4 @@ python3 -m unittest discover -s tools/telegram-bot -v --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.8.3 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.9.0 · Frontend — не создан diff --git a/backend/README.md b/backend/README.md index 50c246e..2463aeb 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,6 +1,6 @@ # Backend Акылдаш -Версия: `0.8.3` +Версия: `0.9.0` Первая backend-область проекта — загрузка правовых документов из официального Open Data API ЦБД Минюста Кыргызской Республики. Код расположен в @@ -240,4 +240,4 @@ PYTHONPATH=backend python3 -m search.query "ЖЧК ачуу тартиби" --la --- -Акылдаш · Backend v0.8.3 · Frontend — не создан +Акылдаш · Backend v0.9.0 · Frontend — не создан diff --git a/backend/ingestion/minjust_cbd.py b/backend/ingestion/minjust_cbd.py index 2556d2e..7cbac08 100644 --- a/backend/ingestion/minjust_cbd.py +++ b/backend/ingestion/minjust_cbd.py @@ -21,7 +21,7 @@ from datetime import datetime, timezone from pathlib import Path from typing import Callable, Iterable -APP_VERSION = "0.8.3" +APP_VERSION = "0.9.0" API_BASE_URL = "https://cbd.minjust.gov.kg/api/v1/OpenData/" LANGUAGES = {"Rus": "ru", "Kyr": "ky"} IMAGE_LANGUAGES = {"Russian": "ru", "Kyrgyz": "ky"} diff --git a/backend/normalization/minjust_cbd.py b/backend/normalization/minjust_cbd.py index 25366b9..90ead3b 100644 --- a/backend/normalization/minjust_cbd.py +++ b/backend/normalization/minjust_cbd.py @@ -23,7 +23,7 @@ from pathlib import Path from typing import Callable from urllib.parse import urlsplit -APP_VERSION = "0.8.3" +APP_VERSION = "0.9.0" SCHEMA_VERSION = "1" NORMALIZER_VERSION = "1.0.0" LANGUAGES = ("ru", "ky") diff --git a/backend/search/api.py b/backend/search/api.py index 9a6183d..dc8b666 100644 --- a/backend/search/api.py +++ b/backend/search/api.py @@ -72,6 +72,7 @@ def openapi() -> dict: {"name": "document_type", "in": "query", "schema": {"type": "string"}}, {"name": "status", "in": "query", "schema": {"type": "string"}}, {"name": "authority", "in": "query", "schema": {"type": "string"}}, + {"name": "legal_force", "in": "query", "schema": {"type": "string"}}, {"name": "date_from", "in": "query", "schema": {"type": "string", "format": "date"}}, {"name": "date_to", "in": "query", "schema": {"type": "string", "format": "date"}}, {"name": "sort", "in": "query", "schema": {"type": "string", "enum": ["relevance", "date"]}}, @@ -121,7 +122,7 @@ class Api: if sort not in {"relevance", "date"}: raise ApiError(400, "sort must be relevance or date") filters: list[dict] = [{"term": {"language": language}}, {"term": {"is_current_edition": True}}] - fields = {"document_type": "document_type_code", "status": "status_code", "authority": "authority_codes"} + fields = {"document_type": "document_type_code", "status": "status_code", "authority": "authority_codes", "legal_force": "legal_force_code"} for parameter, field in fields.items(): value = one(query, parameter) if value: @@ -137,7 +138,7 @@ class Api: body = { "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", "date_adopted", "number"], + "_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"}}}}, "collapse": {"field": "document_code"}, "highlight": {"fields": {f"text_{language}": {"number_of_fragments": 1}}}, @@ -161,13 +162,14 @@ class Api: if not isinstance(source, dict) or not source.get("document_code"): raise ApiError(502, "search backend returned an incomplete result") highlight = hit.get("highlight", {}).get(f"text_{language}", []) - return {"code": source["document_code"], "edition": source.get("edition_code"), "name": source.get(f"document_name_{language}"), "type": source.get(f"document_type_{language}"), "status": source.get(f"status_{language}"), "date_adopted": source.get("date_adopted"), "number": source.get("number"), "snippet": highlight[0] if highlight else None} + legal_force = source.get("legal_force_code") + return {"code": source["document_code"], "edition": source.get("edition_code"), "name": source.get(f"document_name_{language}"), "type": source.get(f"document_type_{language}"), "status": source.get(f"status_{language}"), "legal_force": labels("legal_force", legal_force)[language] if legal_force else None, "date_adopted": source.get("date_adopted"), "number": source.get("number"), "snippet": highlight[0] if highlight else None} def filters(self, query: dict[str, list[str]]) -> dict: language = one(query, "language") or "ru" if language not in LANGUAGES: raise ApiError(400, "language must be ru or ky") - fields = {"document_types": ("document_type", "document_type_code"), "statuses": ("status", "status_code"), "authorities": ("authority", "authority_codes")} + fields = {"document_types": ("document_type", "document_type_code"), "statuses": ("status", "status_code"), "authorities": ("authority", "authority_codes"), "legal_forces": ("legal_force", "legal_force_code")} body = {"size": 0, "query": {"term": {"is_current_edition": True}}, "aggs": {name: {"terms": {"field": pair[1], "size": 1000}, "aggs": {"documents": {"cardinality": {"field": "document_code", "precision_threshold": 40000}}}} for name, pair in fields.items()}} response = self.query_opensearch(body) try: diff --git a/backend/search/catalog.py b/backend/search/catalog.py index 9fcbad2..4f66bca 100644 --- a/backend/search/catalog.py +++ b/backend/search/catalog.py @@ -5,7 +5,20 @@ DOCUMENT_TYPES = { } STATUSES = {"active": ("Действует", "Күчүндө"), "repealed": ("Утратил силу", "Күчүн жоготту"), "unspecified": ("Не указан", "Көрсөтүлгөн эмес")} AUTHORITIES = {"president": ("Президент", "Президент"), "parliament": ("Органы законодательной власти", "Мыйзам чыгаруу бийлик органдары"), "cabinet": ("Правительство и Кабинет Министров", "Өкмөт жана Министрлер Кабинети"), "ministries_and_committees": ("Министерства и государственные комитеты", "Министрликтер жана мамлекеттик комитеттер"), "administrative_agencies": ("Административные ведомства", "Административдик ведомстволор"), "national_bank": ("Национальный банк", "Улуттук банк"), "other_state_bodies": ("Иные государственные органы", "Башка мамлекеттик органдар"), "local_representative_bodies": ("Представительные органы местного самоуправления", "Жергиликтүү өз алдынча башкаруунун өкүлчүлүктүү органдары"), "other": ("Прочие органы", "Башка органдар")} -CATALOGS = {"document_type": DOCUMENT_TYPES, "status": STATUSES, "authority": AUTHORITIES} +LEGAL_FORCE_LEVELS = { + "constitutional": ("Конституционный уровень", "Конституциялык деңгээл"), + "legislative": ("Законодательный уровень", "Мыйзам деңгээли"), + "subordinate": ("Подзаконный уровень", "Мыйзам алдындагы деңгээл"), +} +LEGAL_FORCE_BY_DOCUMENT_TYPE = { + "constitution": "constitutional", + "constitutional_law": "constitutional", + "code": "legislative", + "law": "legislative", + "decree": "subordinate", + "resolution": "subordinate", +} +CATALOGS = {"document_type": DOCUMENT_TYPES, "status": STATUSES, "authority": AUTHORITIES, "legal_force": LEGAL_FORCE_LEVELS} def labels(category: str, code: str) -> dict[str, str]: @@ -49,3 +62,7 @@ def authority_codes(paths: list[dict]) -> list[str]: else: codes.add("other") return sorted(codes) or ["other"] + + +def legal_force_code(document_type: dict | None) -> str | None: + return LEGAL_FORCE_BY_DOCUMENT_TYPE.get(source_code("document_type", document_type)) diff --git a/backend/search/minjust-fragments-index.json b/backend/search/minjust-fragments-index.json index 1cc47b8..d464a6c 100644 --- a/backend/search/minjust-fragments-index.json +++ b/backend/search/minjust-fragments-index.json @@ -23,6 +23,7 @@ "document_type_ru": { "type": "keyword" }, "document_type_ky": { "type": "keyword" }, "document_type_code": { "type": "keyword" }, + "legal_force_code": { "type": "keyword" }, "status_ru": { "type": "keyword" }, "status_ky": { "type": "keyword" }, "status_code": { "type": "keyword" }, diff --git a/backend/search/minjust_opensearch.py b/backend/search/minjust_opensearch.py index cef6db6..254965d 100644 --- a/backend/search/minjust_opensearch.py +++ b/backend/search/minjust_opensearch.py @@ -15,9 +15,9 @@ import urllib.request from pathlib import Path from typing import Iterator -from search.catalog import authority_codes, source_code +from search.catalog import authority_codes, legal_force_code, source_code -APP_VERSION = "0.8.3" +APP_VERSION = "0.9.0" LANGUAGES = {"ru", "ky"} DEFAULT_MAPPING = Path(__file__).with_name("minjust-fragments-index.json") @@ -76,6 +76,7 @@ def search_document(document: dict, fragment: dict, expected: tuple[str, str, st "document_type_ru": localized(document.get("type"), "ru"), "document_type_ky": localized(document.get("type"), "ky"), "document_type_code": source_code("document_type", document.get("type")), + "legal_force_code": legal_force_code(document.get("type")), "status_ru": localized(document.get("status"), "ru"), "status_ky": localized(document.get("status"), "ky"), "status_code": source_code("status", document.get("status")), diff --git a/backend/search/review.html b/backend/search/review.html index 1cdc6ec..0e032d4 100644 --- a/backend/search/review.html +++ b/backend/search/review.html @@ -75,7 +75,7 @@

Документ

- +