Compare commits
3 Commits
main
...
feature/ta
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d393d38bb | |||
| 2c43c7c9ad | |||
| 14e86ccdd8 |
@@ -2,6 +2,8 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
- Backend обновлён до `0.10.3`: запрос о налоге на прибыль поднимает действующий
|
||||
Налоговый кодекс; точное совпадение фразы дополнительно усиливает результат.
|
||||
- Backend обновлён до `0.10.2`: действующие документы ранжируются выше, а
|
||||
утратившие силу — после остальных; из лаборатории удалён пустой блок фрагмента.
|
||||
- Backend обновлён до `0.10.1`: поисковая выдача по регистрации больше не
|
||||
|
||||
@@ -10,14 +10,13 @@ Telegram-бот — только часть рабочего окружения
|
||||
## Текущее состояние
|
||||
|
||||
Сейчас реализованы Telegram-бот-секретарь версии `0.2.2` и backend версии
|
||||
`0.10.2`. Поиск расширяет аббревиатуры организационно-правовых форм и поднимает
|
||||
положение о регистрации 2026 года, закон об обществах и ГК КР для запросов об
|
||||
открытии ОсОО/ЖЧК.
|
||||
`0.10.3`. Поиск расширяет аббревиатуры организационно-правовых форм и поднимает
|
||||
действующий Налоговый кодекс для запросов о налоге на прибыль.
|
||||
|
||||
| Компонент | Версия | Состояние |
|
||||
|---|---:|---|
|
||||
| Telegram-бот | `0.2.2` | на Synology работает `0.2.1`; обновление после слияния |
|
||||
| Backend | `0.10.2` | расширение терминов и приоритет действующих актов о регистрации ОсОО/ЖЧК |
|
||||
| Backend | `0.10.3` | приоритет кодекса для темы налога на прибыль; тематический поиск ОсОО/ЖЧК |
|
||||
| Frontend | — | ещё не создан |
|
||||
| Сбор и обработка правовых данных | `0.8.0` | добавлены relevance set и оценка выдачи |
|
||||
| RAG и база знаний | — | ещё не созданы |
|
||||
@@ -60,4 +59,4 @@ python3 -m unittest discover -s tools/telegram-bot -v
|
||||
|
||||
---
|
||||
|
||||
Акылдаш · Telegram-бот v0.2.2 · Backend v0.10.2 · Frontend — не создан
|
||||
Акылдаш · Telegram-бот v0.2.2 · Backend v0.10.3 · Frontend — не создан
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Backend Акылдаш
|
||||
|
||||
Версия: `0.10.2`
|
||||
Версия: `0.10.3`
|
||||
|
||||
Первая backend-область проекта — загрузка правовых документов из официального
|
||||
Open Data API ЦБД Минюста Кыргызской Республики. Код расположен в
|
||||
@@ -240,4 +240,4 @@ PYTHONPATH=backend python3 -m search.query "ЖЧК ачуу тартиби" --la
|
||||
|
||||
---
|
||||
|
||||
Акылдаш · Backend v0.10.2 · Frontend — не создан
|
||||
Акылдаш · Backend v0.10.3 · Frontend — не создан
|
||||
|
||||
@@ -17,7 +17,7 @@ from search.reviews import ReviewSnapshots, ReviewStore
|
||||
|
||||
|
||||
API_VERSION = "v1"
|
||||
SEARCH_ALGORITHM_VERSION = "search-4"
|
||||
SEARCH_ALGORITHM_VERSION = "search-5"
|
||||
LANGUAGES = {"ru", "ky"}
|
||||
CODE = re.compile(r"^[0-9]+$")
|
||||
MAX_PAGE_SIZE = 100
|
||||
|
||||
@@ -17,7 +17,7 @@ from typing import Iterator
|
||||
|
||||
from search.catalog import authority_codes, legal_force_code, source_code
|
||||
|
||||
APP_VERSION = "0.10.2"
|
||||
APP_VERSION = "0.10.3"
|
||||
LANGUAGES = {"ru", "ky"}
|
||||
DEFAULT_MAPPING = Path(__file__).with_name("minjust-fragments-index.json")
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ LEGAL_FORM_ALIASES = {
|
||||
},
|
||||
}
|
||||
COMPANY_REGISTRATION_DOCUMENTS = (("230044970", 3000), ("667", 2000), ("4", 1000))
|
||||
TAX_PROFIT_DOCUMENTS = (("112340", 3000),)
|
||||
|
||||
|
||||
def query_variants(language: str, query: str) -> list[str]:
|
||||
@@ -34,6 +35,26 @@ def query_variants(language: str, query: str) -> list[str]:
|
||||
return list(dict.fromkeys(variants))
|
||||
|
||||
|
||||
def document_priority_clauses(language: str, documents: tuple[tuple[str, int], ...]) -> list[dict]:
|
||||
status = {"ru": "Действует", "ky": "Күчүндө"}[language]
|
||||
return [
|
||||
{
|
||||
"constant_score": {
|
||||
"filter": {
|
||||
"bool": {
|
||||
"filter": [
|
||||
{"term": {"document_code": code}},
|
||||
{"term": {f"status_{language}": status}},
|
||||
]
|
||||
}
|
||||
},
|
||||
"boost": boost,
|
||||
}
|
||||
}
|
||||
for code, boost in documents
|
||||
]
|
||||
|
||||
|
||||
def company_registration_clauses(language: str, query: str) -> list[dict]:
|
||||
non_registration_context = {
|
||||
"ru": (r"\bсч[её]т\w*", r"\bбанк\w*", r"\bфилиал\w*", r"\bпредставительств\w*"),
|
||||
@@ -66,40 +87,35 @@ def company_registration_clauses(language: str, query: str) -> list[dict]:
|
||||
if not any(re.search(pattern.format(form=forms), normalized_query) 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 {
|
||||
"constant_score": {
|
||||
"filter": {
|
||||
"bool": {
|
||||
"filter": [
|
||||
{"term": {"document_code": document_code}},
|
||||
{"term": {f"status_{language}": status}},
|
||||
]
|
||||
}
|
||||
},
|
||||
"boost": boost,
|
||||
}
|
||||
}
|
||||
return document_priority_clauses(language, COMPANY_REGISTRATION_DOCUMENTS)
|
||||
|
||||
return [clause(code, boost) for code, boost in COMPANY_REGISTRATION_DOCUMENTS]
|
||||
|
||||
def tax_profit_clauses(language: str, query: str) -> list[dict]:
|
||||
if language != "ru" or not re.search(r"\bналог\w*\s+(?:на\s+)?прибыл\w*\b", query.casefold()):
|
||||
return []
|
||||
# ponytail: exact topic anchor; expand only with verified tax-code sources.
|
||||
return document_priority_clauses(language, TAX_PROFIT_DOCUMENTS)
|
||||
|
||||
|
||||
def build_search_query(language: str, query: str) -> dict:
|
||||
intent_clauses = company_registration_clauses(language, query)
|
||||
registration_clauses = company_registration_clauses(language, query)
|
||||
intent_clauses = [*registration_clauses, *tax_profit_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",
|
||||
"operator": "OR" if registration_clauses else "AND",
|
||||
}
|
||||
}
|
||||
for variant in query_variants(language, query)
|
||||
]
|
||||
planned = {"should": [*full_text, *intent_clauses], "minimum_should_match": 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}
|
||||
return {"bool": planned}
|
||||
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
<div class="review__actions"><div class="review__actions-inner"><label>Ваше имя или псевдоним<input id="reviewer" maxlength="120" autocomplete="name" placeholder="Например, Айжан К."></label> <label>Комментарий к выдаче <input id="overall-comment" maxlength="4000" placeholder="Что важно учесть? (необязательно)"></label> <button class="review__button--primary" id="save" type="button">Сохранить разметку</button></div></div>
|
||||
<dialog id="document-dialog"><button class="review__dialog-close" id="dialog-close" type="button">Закрыть</button><h2 id="dialog-heading">Документ</h2><div class="review__document-body" id="dialog-body"></div></dialog>
|
||||
<div class="review__tour-spotlight" id="tutorial-spotlight" hidden aria-hidden="true"></div><dialog class="review__tour" id="tutorial-dialog" aria-labelledby="tutorial-heading" aria-describedby="tutorial-text"><div class="review__tour-content"><p class="review__tour-kicker">Короткое знакомство</p><h2 id="tutorial-heading"></h2><p id="tutorial-text"></p><div class="review__tour-footer"><span class="review__tour-progress" id="tutorial-progress"></span><div class="review__tour-actions"><button id="tutorial-skip" type="button">Пропустить</button><button class="review__button--primary" id="tutorial-next" type="button">Далее</button></div></div></div></dialog>
|
||||
<footer class="review__header">Акылдаш · Backend v0.10.2 · Внутренняя лаборатория релевантности</footer>
|
||||
<footer class="review__header">Акылдаш · Backend v0.10.3 · Внутренняя лаборатория релевантности</footer>
|
||||
<script>
|
||||
const DRAFT_KEY = 'akyldash-search-review-draft';
|
||||
const TOUR_KEY = 'akyldash-search-review-tour-seen';
|
||||
|
||||
@@ -49,6 +49,16 @@ class SearchApiTest(unittest.TestCase):
|
||||
self.assertTrue(any("constant_score" in clause for clause in planned["should"]))
|
||||
self.assertTrue(any("общество с ограниченной ответственностью" in clause.get("multi_match", {}).get("query", "") for clause in planned["should"]))
|
||||
|
||||
def test_production_api_boosts_tax_code_for_profit_tax_query(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
api = self.make_api(Path(temporary))
|
||||
with patch("search.api.request_json", return_value={"hits": {"hits": []}}) as request:
|
||||
api.handle("GET", "/search?q=%D0%BD%D0%B0%D0%BB%D0%BE%D0%B3+%D0%BD%D0%B0+%D0%BF%D1%80%D0%B8%D0%B1%D1%8B%D0%BB%D1%8C&language=ru")
|
||||
body = json.loads(request.call_args.args[2])
|
||||
planned = body["query"]["bool"]["must"]["bool"]
|
||||
anchors = [clause["constant_score"]["filter"]["bool"]["filter"][0] for clause in planned["should"] if "constant_score" in clause]
|
||||
self.assertIn({"term": {"document_code": "112340"}}, anchors)
|
||||
|
||||
def test_search_sorts_repealed_documents_after_other_statuses(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
api = self.make_api(Path(temporary))
|
||||
|
||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from search.evaluate_relevance import evaluate, load_queries
|
||||
from search.query import COMPANY_REGISTRATION_DOCUMENTS, build_search_body, query_variants
|
||||
from search.query import COMPANY_REGISTRATION_DOCUMENTS, TAX_PROFIT_DOCUMENTS, build_search_body, query_variants
|
||||
|
||||
|
||||
class SearchRelevanceTest(unittest.TestCase):
|
||||
@@ -40,7 +40,9 @@ class SearchRelevanceTest(unittest.TestCase):
|
||||
self.assertIn({"term": {"is_current_edition": True}}, body["query"]["bool"]["filter"])
|
||||
lexical_query = body["query"]["bool"]["must"]["bool"]["should"][0]["multi_match"]
|
||||
self.assertEqual(lexical_query["type"], "cross_fields")
|
||||
self.assertEqual(lexical_query["fields"], ["document_name_ru^4", "text_ru"])
|
||||
self.assertEqual(lexical_query["operator"], "AND")
|
||||
self.assertTrue(any("match_phrase" in clause for clause in body["query"]["bool"]["must"]["bool"]["should"]))
|
||||
|
||||
def test_legal_form_abbreviations_expand_in_both_directions(self):
|
||||
self.assertIn("как открыть общество с ограниченной ответственностью", query_variants("ru", "как открыть ОсОО"))
|
||||
@@ -56,7 +58,8 @@ class SearchRelevanceTest(unittest.TestCase):
|
||||
search_query = body["query"]["bool"]["must"]["bool"]
|
||||
self.assertEqual(search_query["minimum_should_match"], 1)
|
||||
self.assertNotIn("filter", search_query)
|
||||
self.assertTrue(all(clause.get("multi_match", {}).get("operator") == "OR" for clause in search_query["should"] if "multi_match" in clause))
|
||||
lexical = [clause["multi_match"] for clause in search_query["should"] if "multi_match" in clause]
|
||||
self.assertTrue(all(clause["type"] == "cross_fields" and clause["operator"] == "OR" for clause in lexical))
|
||||
boosts = [clause["constant_score"] for clause in search_query["should"] if "constant_score" in clause]
|
||||
self.assertEqual([item["boost"] for item in boosts], [boost for _, boost in COMPANY_REGISTRATION_DOCUMENTS])
|
||||
self.assertEqual(
|
||||
@@ -65,6 +68,20 @@ class SearchRelevanceTest(unittest.TestCase):
|
||||
)
|
||||
self.assertTrue(all(item["filter"]["bool"]["filter"][1] == {"term": {f"status_{language}": status}} for item in boosts))
|
||||
|
||||
def test_profit_tax_intent_prioritizes_the_current_tax_code(self):
|
||||
for query in ("налог на прибыль", "налога на прибыль организаций", "налогообложение прибыли"):
|
||||
body = json.loads(build_search_body("ru", query, 10))
|
||||
clauses = body["query"]["bool"]["must"]["bool"]["should"]
|
||||
boosts = [clause["constant_score"] for clause in clauses if "constant_score" in clause]
|
||||
self.assertEqual([item["boost"] for item in boosts], [boost for _, boost in TAX_PROFIT_DOCUMENTS])
|
||||
self.assertEqual([item["filter"]["bool"]["filter"][0]["term"]["document_code"] for item in boosts], ["112340"])
|
||||
self.assertTrue(all(item["filter"]["bool"]["filter"][1] == {"term": {"status_ru": "Действует"}} for item in boosts))
|
||||
|
||||
for query in ("прибыль организации", "налог на доходы", "налог на имущество"):
|
||||
body = json.loads(build_search_body("ru", query, 10))
|
||||
clauses = body["query"]["bool"]["must"]["bool"]["should"]
|
||||
self.assertFalse(any("constant_score" in clause for clause in clauses))
|
||||
|
||||
def test_company_registration_intent_ignores_non_procedural_queries(self):
|
||||
for language, query in (
|
||||
("ru", "ОсОО зарегистрирован?"),
|
||||
|
||||
@@ -39,4 +39,4 @@
|
||||
|
||||
---
|
||||
|
||||
Акылдаш · Telegram-бот v0.2.2 · Backend v0.10.2 · Frontend — не создан
|
||||
Акылдаш · Telegram-бот v0.2.2 · Backend v0.10.3 · Frontend — не создан
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Статус проекта
|
||||
|
||||
Последняя проверка: 2026-09-14
|
||||
Последняя проверка: 2026-09-16
|
||||
Назначение документа: быстро восстановить контекст проекта для участников команды и будущих агентов.
|
||||
|
||||
- Telegram-бот: `0.2.2`
|
||||
- Telegram-бот на Synology: `0.2.1`
|
||||
- Backend: `0.10.2`
|
||||
- Backend: `0.10.3`
|
||||
- Frontend: не создан
|
||||
|
||||
## Краткий итог
|
||||
@@ -17,6 +17,29 @@
|
||||
оценки юристов и зафиксировать baseline. ЦБД Минюста служит источником для
|
||||
проверки документов и редакций, а не системой для сравнения поисковой выдачи.
|
||||
|
||||
### Точка остановки на 2026-09-16
|
||||
|
||||
- В работе ветка `feature/tax-profit-search-intent` (коммиты `14e86cc` и
|
||||
`2c43c7c`): тематический запрос «налог на прибыль» поднимает действующий НК
|
||||
КР; для обычных запросов сохранён `cross_fields`, добавлено усиление точной
|
||||
фразы. Нужна дальнейшая проверка на контрольном наборе запросов.
|
||||
- Независимое ревью полного diff завершено: блокирующих замечаний нет.
|
||||
Backend-тесты: 33 пройдены, 1 пропущен; `git diff --check` чистый.
|
||||
- PR для ветки пока не создан. `gitea-api` не может разрешить
|
||||
`git.vakanaut.ru` из WSL. Пользователь сообщил, что сайт открывается в
|
||||
Windows-браузере; в WSL не разрешается также `example.com`. Последняя
|
||||
проверенная настройка WSL DNS: `nameserver 10.255.255.254` в
|
||||
`/etc/resolv.conf`. Несколько часов назад, по словам пользователя, Gitea API
|
||||
работал. DNS не меняли, WSL не перезапускали.
|
||||
- Текущий локальный backend-контейнер при последней проверке был `0.10.2`;
|
||||
изменения ветки (`0.10.3`) не развёрнуты. Не деплоить без отдельного запроса.
|
||||
- В рабочем дереве есть постороннее пользовательское изменение `.gitignore`;
|
||||
не включать его в коммиты и не удалять.
|
||||
- Следующий шаг: после восстановления DNS повторить `gitea-api`, проверить
|
||||
наличие PR и создать его только если PR для ветки ещё нет; затем продолжить
|
||||
ревью/проверки. Сначала диагностировать кратковременный сбой DNS в WSL, не
|
||||
менять сетевые настройки и не перезапускать WSL без учёта возможного простоя.
|
||||
|
||||
## Уже сделано
|
||||
|
||||
### Telegram
|
||||
@@ -262,4 +285,4 @@
|
||||
|
||||
---
|
||||
|
||||
Акылдаш · Telegram-бот v0.2.2 · Backend v0.10.2 · Frontend — не создан
|
||||
Акылдаш · Telegram-бот v0.2.2 · Backend v0.10.3 · Frontend — не создан
|
||||
|
||||
Reference in New Issue
Block a user