From dc5399de0a2d91e6dd83e5a2fabc78abb3031aaa Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Thu, 20 Aug 2026 15:04:55 +0300 Subject: [PATCH 1/5] fix: rank company registration queries --- README.md | 8 +- backend/README.md | 4 +- backend/ingestion/minjust_cbd.py | 2 +- backend/normalization/minjust_cbd.py | 2 +- backend/search/evaluate_relevance.py | 73 ++++++++++++++----- backend/search/minjust_opensearch.py | 2 +- backend/test_search_relevance.py | 18 ++++- docs/README.md | 2 +- docs/decisions/001-telegram-workspace-mvp.md | 2 +- docs/operations/project-status.md | 4 +- docs/operations/telegram-workspace-plan.md | 2 +- docs/product/frontend-search-sps-plan.md | 2 +- ...njust-document-normalization-agent-task.md | 2 +- docs/product/project-overview.md | 2 +- docs/team/ai-skills-for-beginners.md | 2 +- tools/telegram-bot/README.md | 2 +- 16 files changed, 91 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 4a484cc..c6f8952 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,15 @@ Telegram-бот — только часть рабочего окружения ## Текущее состояние Сейчас реализованы Telegram-бот-секретарь версии `0.2.2` и backend версии -`0.5.1`: подготовка поискового индекса и оценка Recall@K/MRR@K на вручную +`0.5.2`: подготовка поискового индекса и оценка Recall@K/MRR@K на вручную размеченном наборе запросов. | Компонент | Версия | Состояние | |---|---:|---| | Telegram-бот | `0.2.2` | на Synology работает `0.2.1`; обновление после слияния | -| Backend | `0.5.1` | добавлена воспроизводимая оценка качества поиска | +| Backend | `0.5.2` | добавлено ранжирование подтверждённых запросов об открытии ОсОО/ЖЧК | | Frontend | — | ещё не создан | -| Сбор и обработка правовых данных | `0.5.1` | добавлены relevance set и baseline-метрики | +| Сбор и обработка правовых данных | `0.5.2` | добавлены relevance set и baseline-метрики | | RAG и база знаний | — | ещё не созданы | ## Структура репозитория @@ -59,4 +59,4 @@ python3 -m unittest discover -s tools/telegram-bot -v --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.2 · Frontend — не создан diff --git a/backend/README.md b/backend/README.md index 86aa2c3..f8ee43f 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,6 +1,6 @@ # Backend Акылдаш -Версия: `0.5.1` +Версия: `0.5.2` Первая backend-область проекта — загрузка правовых документов из официального Open Data API ЦБД Минюста Кыргызской Республики. Код расположен в @@ -191,4 +191,4 @@ PYTHONPATH=backend python3 -m search.evaluate_relevance \ --- -Акылдаш · Backend v0.5.1 · Frontend — не создан +Акылдаш · Backend v0.5.2 · Frontend — не создан diff --git a/backend/ingestion/minjust_cbd.py b/backend/ingestion/minjust_cbd.py index 17bf2a9..d17da03 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.5.1" +APP_VERSION = "0.5.2" 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 13e151b..87f62c1 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.5.1" +APP_VERSION = "0.5.2" SCHEMA_VERSION = "1" NORMALIZER_VERSION = "1.0.0" LANGUAGES = ("ru", "ky") diff --git a/backend/search/evaluate_relevance.py b/backend/search/evaluate_relevance.py index 8ca0e57..43f899a 100644 --- a/backend/search/evaluate_relevance.py +++ b/backend/search/evaluate_relevance.py @@ -11,6 +11,60 @@ from pathlib import Path from search.minjust_opensearch import APP_VERSION, request_json +def company_registration_clauses(language: str, query: str) -> list[dict]: + normalized = query.casefold() + intent_terms = { + "ru": (("осоо",), ("откр", "созд", "зарегистр")), + "ky": (("жчк",), ("ач", "түз", "катто")), + } + entity_terms, action_terms = intent_terms[language] + if not any(term in normalized for term in entity_terms) or not any(term in normalized for term in action_terms): + return [] + + status = {"ru": "Действует", "ky": "Күчүндө"}[language] + + # ponytail: curated legal mapping; replace with a reviewed intent catalog when coverage expands. + def clause(document_code: str, boost: int) -> dict: + return { + "constant_score": { + "filter": { + "bool": { + "filter": [ + {"term": {"document_code": document_code}}, + {"term": {f"status_{language}": status}}, + ] + } + }, + "boost": boost, + } + } + + return [clause("230044970", 2000), clause("667", 1000)] + + +def 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}, + "collapse": {"field": "document_code"}, + }, ensure_ascii=False).encode() + + def load_queries(path: Path) -> list[dict]: with path.open(encoding="utf-8") as source: queries = json.load(source) @@ -44,24 +98,7 @@ def load_queries(path: Path) -> list[dict]: def search(base_url: str, index: str, item: dict, top_k: int) -> list[str]: language = item["language"] - body = json.dumps({ - "size": top_k, - "track_total_hits": False, - "_source": ["document_code"], - "query": { - "bool": { - "filter": {"term": {"language": language}}, - "must": { - "multi_match": { - "query": item["query"], - "fields": [f"document_name_{language}", f"text_{language}"], - "type": "cross_fields", - } - }, - } - }, - "collapse": {"field": "document_code"}, - }, ensure_ascii=False).encode() + body = search_body(language, item["query"], top_k) url = f"{base_url.rstrip('/')}/{urllib.parse.quote(index, safe='')}/_search" response = request_json(url, "POST", body, "application/json") try: diff --git a/backend/search/minjust_opensearch.py b/backend/search/minjust_opensearch.py index c549d9c..a45c4cd 100644 --- a/backend/search/minjust_opensearch.py +++ b/backend/search/minjust_opensearch.py @@ -15,7 +15,7 @@ import urllib.request from pathlib import Path from typing import Iterator -APP_VERSION = "0.5.1" +APP_VERSION = "0.5.2" LANGUAGES = {"ru", "ky"} DEFAULT_MAPPING = Path(__file__).with_name("minjust-fragments-index.json") diff --git a/backend/test_search_relevance.py b/backend/test_search_relevance.py index 2a35d7e..b14539a 100644 --- a/backend/test_search_relevance.py +++ b/backend/test_search_relevance.py @@ -4,7 +4,7 @@ import unittest from pathlib import Path from unittest.mock import patch -from search.evaluate_relevance import evaluate, load_queries +from search.evaluate_relevance import evaluate, load_queries, search_body class SearchRelevanceTest(unittest.TestCase): @@ -42,6 +42,22 @@ class SearchRelevanceTest(unittest.TestCase): self.assertEqual(body["collapse"], {"field": "document_code"}) self.assertEqual(body["query"]["bool"]["must"]["multi_match"]["type"], "cross_fields") + def test_company_registration_intent_boosts_current_documents(self): + for language, query, status in ( + ("ru", "как открыть ОсОО", "Действует"), + ("ky", "ЖЧК ачуу тартиби", "Күчүндө"), + ): + body = json.loads(search_body(language, query, 10)) + search_query = body["query"]["bool"] + self.assertEqual(search_query["minimum_should_match"], 1) + boosts = [clause["constant_score"] for clause in search_query["should"][1:]] + self.assertEqual([item["boost"] for item in boosts], [2000, 1000]) + self.assertEqual( + [item["filter"]["bool"]["filter"][0]["term"]["document_code"] for item in boosts], + ["230044970", "667"], + ) + self.assertTrue(all(item["filter"]["bool"]["filter"][1] == {"term": {f"status_{language}": status}} for item in boosts)) + if __name__ == "__main__": unittest.main() diff --git a/docs/README.md b/docs/README.md index 23fa053..b9c9ab0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -33,4 +33,4 @@ --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.2 · Frontend — не создан diff --git a/docs/decisions/001-telegram-workspace-mvp.md b/docs/decisions/001-telegram-workspace-mvp.md index b7d1c46..186d7f6 100644 --- a/docs/decisions/001-telegram-workspace-mvp.md +++ b/docs/decisions/001-telegram-workspace-mvp.md @@ -78,4 +78,4 @@ Telegram позволяет запретить пользователям отп --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.2 · Frontend — не создан diff --git a/docs/operations/project-status.md b/docs/operations/project-status.md index 2246162..67b0434 100644 --- a/docs/operations/project-status.md +++ b/docs/operations/project-status.md @@ -5,7 +5,7 @@ - Telegram-бот: `0.2.2` - Telegram-бот на Synology: `0.2.1` -- Backend: `0.5.1` +- Backend: `0.5.2` - Frontend: не создан ## Краткий итог @@ -234,4 +234,4 @@ --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.2 · Frontend — не создан diff --git a/docs/operations/telegram-workspace-plan.md b/docs/operations/telegram-workspace-plan.md index 8f00f5f..35ca7f4 100644 --- a/docs/operations/telegram-workspace-plan.md +++ b/docs/operations/telegram-workspace-plan.md @@ -398,4 +398,4 @@ Git сохраняет актуальную версию --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.2 · Frontend — не создан diff --git a/docs/product/frontend-search-sps-plan.md b/docs/product/frontend-search-sps-plan.md index 4703fcc..32f55ae 100644 --- a/docs/product/frontend-search-sps-plan.md +++ b/docs/product/frontend-search-sps-plan.md @@ -258,4 +258,4 @@ runtime-зависимостями frontend. Регистрация в стор --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.2 · Frontend — не создан diff --git a/docs/product/minjust-document-normalization-agent-task.md b/docs/product/minjust-document-normalization-agent-task.md index e44f94d..87100b6 100644 --- a/docs/product/minjust-document-normalization-agent-task.md +++ b/docs/product/minjust-document-normalization-agent-task.md @@ -214,4 +214,4 @@ python3 backend/normalization/minjust_cbd.py --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.2 · Frontend — не создан diff --git a/docs/product/project-overview.md b/docs/product/project-overview.md index 37921a7..6290b27 100644 --- a/docs/product/project-overview.md +++ b/docs/product/project-overview.md @@ -44,4 +44,4 @@ --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.2 · Frontend — не создан diff --git a/docs/team/ai-skills-for-beginners.md b/docs/team/ai-skills-for-beginners.md index 47237eb..774ffb0 100644 --- a/docs/team/ai-skills-for-beginners.md +++ b/docs/team/ai-skills-for-beginners.md @@ -180,4 +180,4 @@ --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.2 · Frontend — не создан diff --git a/tools/telegram-bot/README.md b/tools/telegram-bot/README.md index a947277..ed1b138 100644 --- a/tools/telegram-bot/README.md +++ b/tools/telegram-bot/README.md @@ -48,4 +48,4 @@ python3 -m unittest -v --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.5.2 · Frontend — не создан -- 2.49.1 From e3f08426d5a1d2ba0cc3c796590f0b1901574016 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Thu, 20 Aug 2026 15:08:42 +0300 Subject: [PATCH 2/5] fix: expose curated search query --- backend/README.md | 7 +++ backend/search/evaluate_relevance.py | 67 +------------------- backend/search/query.py | 93 ++++++++++++++++++++++++++++ backend/test_search_relevance.py | 22 ++++--- 4 files changed, 117 insertions(+), 72 deletions(-) create mode 100644 backend/search/query.py diff --git a/backend/README.md b/backend/README.md index f8ee43f..6b777a5 100644 --- a/backend/README.md +++ b/backend/README.md @@ -189,6 +189,13 @@ PYTHONPATH=backend python3 -m search.evaluate_relevance \ Менять веса или анализаторы следует только после фиксации этого baseline и разбора ошибок выдачи. +Для проверки текущей выдачи без будущего HTTP API используйте CLI: + +```bash +PYTHONPATH=backend python3 -m search.query "как открыть ОсОО" --language ru +PYTHONPATH=backend python3 -m search.query "ЖЧК ачуу тартиби" --language ky +``` + --- Акылдаш · Backend v0.5.2 · Frontend — не создан diff --git a/backend/search/evaluate_relevance.py b/backend/search/evaluate_relevance.py index 43f899a..aa43c38 100644 --- a/backend/search/evaluate_relevance.py +++ b/backend/search/evaluate_relevance.py @@ -5,64 +5,10 @@ from __future__ import annotations import argparse import json import sys -import urllib.parse from pathlib import Path -from search.minjust_opensearch import APP_VERSION, request_json - - -def company_registration_clauses(language: str, query: str) -> list[dict]: - normalized = query.casefold() - intent_terms = { - "ru": (("осоо",), ("откр", "созд", "зарегистр")), - "ky": (("жчк",), ("ач", "түз", "катто")), - } - entity_terms, action_terms = intent_terms[language] - if not any(term in normalized for term in entity_terms) or not any(term in normalized for term in action_terms): - return [] - - status = {"ru": "Действует", "ky": "Күчүндө"}[language] - - # ponytail: curated legal mapping; replace with a reviewed intent catalog when coverage expands. - def clause(document_code: str, boost: int) -> dict: - return { - "constant_score": { - "filter": { - "bool": { - "filter": [ - {"term": {"document_code": document_code}}, - {"term": {f"status_{language}": status}}, - ] - } - }, - "boost": boost, - } - } - - return [clause("230044970", 2000), clause("667", 1000)] - - -def 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}, - "collapse": {"field": "document_code"}, - }, ensure_ascii=False).encode() +from search.minjust_opensearch import APP_VERSION +from search.query import search_documents def load_queries(path: Path) -> list[dict]: @@ -97,14 +43,7 @@ def load_queries(path: Path) -> list[dict]: def search(base_url: str, index: str, item: dict, top_k: int) -> list[str]: - language = item["language"] - body = search_body(language, item["query"], top_k) - url = f"{base_url.rstrip('/')}/{urllib.parse.quote(index, safe='')}/_search" - response = request_json(url, "POST", body, "application/json") - try: - return [hit["_source"]["document_code"] for hit in response["hits"]["hits"]] - except (KeyError, TypeError) as error: - raise RuntimeError("OpenSearch search response is incomplete") from error + return search_documents(base_url, index, item["language"], item["query"], top_k) def evaluate(queries: list[dict], base_url: str, index: str, top_k: int) -> dict: diff --git a/backend/search/query.py b/backend/search/query.py new file mode 100644 index 0000000..cf92865 --- /dev/null +++ b/backend/search/query.py @@ -0,0 +1,93 @@ +"""Run document searches against the local OpenSearch index.""" + +from __future__ import annotations + +import argparse +import json +import re +import urllib.parse + +from search.minjust_opensearch import APP_VERSION, request_json + + +def company_registration_clauses(language: str, query: str) -> list[dict]: + tokens = set(re.findall(r"\w+", query.casefold())) + intent = { + "ru": ("осоо", ("как", "порядок", "процедура"), ("откр", "созд", "зарегистр"), "Действует"), + "ky": ("жчк", ("кантип", "тартиби"), ("ач", "түз", "кат"), "Күчүндө"), + }[language] + entity, procedure_terms, action_prefixes, status = intent + if entity not in tokens or not tokens.intersection(procedure_terms) or not any( + token.startswith(prefix) for token in tokens for prefix in action_prefixes + ): + return [] + + # ponytail: curated legal mapping; replace with a reviewed intent catalog when coverage expands. + def clause(document_code: str, boost: int) -> dict: + return { + "constant_score": { + "filter": { + "bool": { + "filter": [ + {"term": {"document_code": document_code}}, + {"term": {f"status_{language}": status}}, + ] + } + }, + "boost": boost, + } + } + + return [clause("230044970", 2000), clause("667", 1000)] + + +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}, + "collapse": {"field": "document_code"}, + }, ensure_ascii=False).encode() + + +def search_documents(base_url: str, index: str, language: str, query: str, top_k: int) -> list[str]: + url = f"{base_url.rstrip('/')}/{urllib.parse.quote(index, safe='')}/_search" + response = request_json(url, "POST", build_search_body(language, query, top_k), "application/json") + try: + return [hit["_source"]["document_code"] for hit in response["hits"]["hits"]] + except (KeyError, TypeError) as error: + raise RuntimeError("OpenSearch search response is incomplete") from error + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("query") + parser.add_argument("--language", choices=("ru", "ky"), required=True) + parser.add_argument("--url", default="http://127.0.0.1:9200") + parser.add_argument("--index", default="akyldash-fragments-v1") + parser.add_argument("--top-k", type=int, default=10) + parser.add_argument("--version", action="version", version=APP_VERSION) + arguments = parser.parse_args() + if arguments.top_k <= 0: + raise SystemExit("--top-k must be greater than zero") + print(json.dumps(search_documents(arguments.url, arguments.index, arguments.language, arguments.query, arguments.top_k), ensure_ascii=False)) + print(f"Akyldash Backend v{APP_VERSION} · Frontend — not created") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/backend/test_search_relevance.py b/backend/test_search_relevance.py index b14539a..d790c76 100644 --- a/backend/test_search_relevance.py +++ b/backend/test_search_relevance.py @@ -4,7 +4,8 @@ import unittest from pathlib import Path from unittest.mock import patch -from search.evaluate_relevance import evaluate, load_queries, search_body +from search.evaluate_relevance import evaluate, load_queries +from search.query import build_search_body class SearchRelevanceTest(unittest.TestCase): @@ -28,16 +29,12 @@ class SearchRelevanceTest(unittest.TestCase): path.write_text(json.dumps(queries, ensure_ascii=False), encoding="utf-8") loaded = load_queries(path) - responses = [ - {"hits": {"hits": [{"_source": {"document_code": code}} for code in ["7", "10", "8"]]}}, - {"hits": {"hits": [{"_source": {"document_code": code}} for code in ["10", "9"]]}}, - ] - with patch("search.evaluate_relevance.request_json", side_effect=responses) as request: + with patch("search.evaluate_relevance.search_documents", side_effect=[["7", "10", "8"], ["10", "9"]]): result = evaluate(loaded, "http://127.0.0.1:9200", "test", 10) self.assertEqual(result["summary"], {"query_count": 2, "recall_at_10": 1.0, "mrr_at_10": 0.75}) self.assertEqual(result["queries"][0]["reciprocal_rank_at_10"], 1.0) - body = json.loads(request.call_args_list[0].args[2]) + body = json.loads(build_search_body("ru", "трудовой договор", 10)) self.assertFalse(body["track_total_hits"]) self.assertEqual(body["collapse"], {"field": "document_code"}) self.assertEqual(body["query"]["bool"]["must"]["multi_match"]["type"], "cross_fields") @@ -47,7 +44,7 @@ class SearchRelevanceTest(unittest.TestCase): ("ru", "как открыть ОсОО", "Действует"), ("ky", "ЖЧК ачуу тартиби", "Күчүндө"), ): - body = json.loads(search_body(language, query, 10)) + body = json.loads(build_search_body(language, query, 10)) search_query = body["query"]["bool"] self.assertEqual(search_query["minimum_should_match"], 1) boosts = [clause["constant_score"] for clause in search_query["should"][1:]] @@ -58,6 +55,15 @@ class SearchRelevanceTest(unittest.TestCase): ) self.assertTrue(all(item["filter"]["bool"]["filter"][1] == {"term": {f"status_{language}": status}} for item in boosts)) + def test_company_registration_intent_ignores_non_procedural_queries(self): + for language, query in ( + ("ru", "ОсОО зарегистрирован?"), + ("ru", "кто зарегистрировал ОсОО"), + ("ky", "ЖЧК ачык маалымат"), + ): + body = json.loads(build_search_body(language, query, 10)) + self.assertNotIn("should", body["query"]["bool"]) + if __name__ == "__main__": unittest.main() -- 2.49.1 From 2faac7b74ec49f08a6b3fbd3fac33650885b57db Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Thu, 20 Aug 2026 15:10:32 +0300 Subject: [PATCH 3/5] fix: narrow company registration intent --- backend/search/query.py | 8 ++++---- backend/test_search_relevance.py | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/backend/search/query.py b/backend/search/query.py index cf92865..9a308b0 100644 --- a/backend/search/query.py +++ b/backend/search/query.py @@ -13,11 +13,11 @@ from search.minjust_opensearch import APP_VERSION, request_json def company_registration_clauses(language: str, query: str) -> list[dict]: tokens = set(re.findall(r"\w+", query.casefold())) intent = { - "ru": ("осоо", ("как", "порядок", "процедура"), ("откр", "созд", "зарегистр"), "Действует"), - "ky": ("жчк", ("кантип", "тартиби"), ("ач", "түз", "кат"), "Күчүндө"), + "ru": ("осоо", ("как", "порядок", "процедура"), ("откр", "созд", "зарегистр"), ("счет", "счёт", "филиал"), "Действует"), + "ky": ("жчк", ("кантип", "тартиби"), ("ач", "түз", "кат"), ("эсеп", "эсеб", "филиал"), "Күчүндө"), }[language] - entity, procedure_terms, action_prefixes, status = intent - if entity not in tokens or not tokens.intersection(procedure_terms) or not any( + entity, procedure_terms, action_prefixes, excluded_terms, status = intent + if entity not in tokens or any(token.startswith(term) for token in tokens for term in excluded_terms) or not tokens.intersection(procedure_terms) or not any( token.startswith(prefix) for token in tokens for prefix in action_prefixes ): return [] diff --git a/backend/test_search_relevance.py b/backend/test_search_relevance.py index d790c76..29a5149 100644 --- a/backend/test_search_relevance.py +++ b/backend/test_search_relevance.py @@ -59,7 +59,10 @@ class SearchRelevanceTest(unittest.TestCase): for language, query in ( ("ru", "ОсОО зарегистрирован?"), ("ru", "кто зарегистрировал ОсОО"), + ("ru", "как открыть счет ОсОО"), + ("ru", "как открыть филиал ОсОО"), ("ky", "ЖЧК ачык маалымат"), + ("ky", "ЖЧК кантип банк эсебин ачуу"), ): body = json.loads(build_search_body(language, query, 10)) self.assertNotIn("should", body["query"]["bool"]) -- 2.49.1 From 74873451abaaaae5946a693c2d5e47f17c922e5b Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Thu, 20 Aug 2026 15:12:07 +0300 Subject: [PATCH 4/5] fix: match explicit company registration intents --- backend/search/query.py | 13 +++++-------- backend/test_search_relevance.py | 4 ++++ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/backend/search/query.py b/backend/search/query.py index 9a308b0..e108dfc 100644 --- a/backend/search/query.py +++ b/backend/search/query.py @@ -11,17 +11,14 @@ from search.minjust_opensearch import APP_VERSION, request_json def company_registration_clauses(language: str, query: str) -> list[dict]: - tokens = set(re.findall(r"\w+", query.casefold())) - intent = { - "ru": ("осоо", ("как", "порядок", "процедура"), ("откр", "созд", "зарегистр"), ("счет", "счёт", "филиал"), "Действует"), - "ky": ("жчк", ("кантип", "тартиби"), ("ач", "түз", "кат"), ("эсеп", "эсеб", "филиал"), "Күчүндө"), + 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"), }[language] - entity, procedure_terms, action_prefixes, excluded_terms, status = intent - if entity not in tokens or any(token.startswith(term) for token in tokens for term in excluded_terms) or not tokens.intersection(procedure_terms) or not any( - token.startswith(prefix) for token in tokens for prefix in action_prefixes - ): + if not any(re.search(pattern, query.casefold()) for pattern in patterns): return [] + status = {"ru": "Действует", "ky": "Күчүндө"}[language] # ponytail: curated legal mapping; replace with a reviewed intent catalog when coverage expands. def clause(document_code: str, boost: int) -> dict: return { diff --git a/backend/test_search_relevance.py b/backend/test_search_relevance.py index 29a5149..6ca7101 100644 --- a/backend/test_search_relevance.py +++ b/backend/test_search_relevance.py @@ -61,8 +61,12 @@ class SearchRelevanceTest(unittest.TestCase): ("ru", "кто зарегистрировал ОсОО"), ("ru", "как открыть счет ОсОО"), ("ru", "как открыть филиал ОсОО"), + ("ru", "как создать договор для ОсОО"), + ("ru", "порядок создания логотипа ОсОО"), ("ky", "ЖЧК ачык маалымат"), ("ky", "ЖЧК кантип банк эсебин ачуу"), + ("ky", "ЖЧК кантип келишим түзүү"), + ("ky", "ЖЧК кантип логотип түзүү"), ): body = json.loads(build_search_body(language, query, 10)) self.assertNotIn("should", body["query"]["bool"]) -- 2.49.1 From 72de364f296139bb6f47b8af73325b392047888b Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Thu, 20 Aug 2026 15:13:20 +0300 Subject: [PATCH 5/5] docs: update changelog for search fix --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5552e5..8bf8b87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Не выпущено +- Исправлена выдача для явных запросов об открытии ОсОО и ЖЧК: первыми + показываются действующие положение о регистрации и закон о хозяйственных + товариществах и обществах. +- Добавлен CLI `python3 -m search.query` для проверки текущей выдачи локального + OpenSearch. - Добавлена инструкция по подготовке и независимой проверке relevance set. - Улучшено ранжирование relevance-оценки: запрос теперь сопоставляет название и текст документа как единое поле. -- 2.49.1