Compare commits
15 Commits
feature/se
...
feature/im
| Author | SHA1 | Date | |
|---|---|---|---|
| 06c275cc40 | |||
| 92051184f5 | |||
| c048f20734 | |||
| af6b0b8098 | |||
| 94ba04ff56 | |||
| 9fb266e8ac | |||
| f045233a50 | |||
| 6a187760db | |||
| 90298fa6cc | |||
| 503cb1f074 | |||
| 4d276e0776 | |||
| 298191173e | |||
| 81b085dd9d | |||
| b6ad392ab7 | |||
| bb5edf3ae2 |
16
CHANGELOG.md
16
CHANGELOG.md
@@ -2,6 +2,22 @@
|
|||||||
|
|
||||||
## Не выпущено
|
## Не выпущено
|
||||||
|
|
||||||
|
- Backend обновлён до `0.10.1`: поисковая выдача по регистрации больше не
|
||||||
|
ограничивается только тремя заранее выбранными нормативными актами.
|
||||||
|
- Backend обновлён до `0.10.0`: поисковый API использует общий планировщик
|
||||||
|
запросов, расширяет аббревиатуры ОсОО/ООО/ЖЧК и для запроса о регистрации
|
||||||
|
приоритизирует положение КР 2026 года, закон об обществах и ГК КР.
|
||||||
|
- Backend обновлён до `0.9.4`: из поисковых фрагментов убраны технические маркеры
|
||||||
|
подсветки OpenSearch.
|
||||||
|
- Backend обновлён до `0.9.3`: исправлен локальный индекс production Search API,
|
||||||
|
ошибки поиска показываются в общем status-блоке, а панель сохранения и footer
|
||||||
|
выстроены в обычном порядке документа.
|
||||||
|
- Backend обновлён до `0.9.2`: внутренняя лаборатория получила визуальный
|
||||||
|
polish-проход и короткий повторно запускаемый tutorial для новых проверяющих.
|
||||||
|
- Backend обновлён до `0.9.1`: все статусы из нормализованного корпуса доступны
|
||||||
|
как отдельные фильтры, варианты конституционного закона и указа сопоставляются
|
||||||
|
с подтверждённой юридической силой, неизвестные типы документов сохраняются
|
||||||
|
под общим фильтром без вывода юридической силы.
|
||||||
- Уточнено, что внутренняя лаборатория оценивает выдачу собственного OpenSearch;
|
- Уточнено, что внутренняя лаборатория оценивает выдачу собственного OpenSearch;
|
||||||
ЦБД Минюста служит источником для подтверждения документов, а прежние Excel/PDF
|
ЦБД Минюста служит источником для подтверждения документов, а прежние Excel/PDF
|
||||||
бланки удалены.
|
бланки удалены.
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ Telegram-бот — только часть рабочего окружения
|
|||||||
## Текущее состояние
|
## Текущее состояние
|
||||||
|
|
||||||
Сейчас реализованы Telegram-бот-секретарь версии `0.2.2` и backend версии
|
Сейчас реализованы Telegram-бот-секретарь версии `0.2.2` и backend версии
|
||||||
`0.9.0`: добавлена фильтрация поиска по юридической силе.
|
`0.10.1`. Поиск расширяет аббревиатуры организационно-правовых форм и поднимает
|
||||||
|
положение о регистрации 2026 года, закон об обществах и ГК КР для запросов об
|
||||||
|
открытии ОсОО/ЖЧК.
|
||||||
|
|
||||||
| Компонент | Версия | Состояние |
|
| Компонент | Версия | Состояние |
|
||||||
|---|---:|---|
|
|---|---:|---|
|
||||||
| Telegram-бот | `0.2.2` | на Synology работает `0.2.1`; обновление после слияния |
|
| Telegram-бот | `0.2.2` | на Synology работает `0.2.1`; обновление после слияния |
|
||||||
| Backend | `0.9.0` | фильтр поиска по юридической силе и черновик тематического каталога |
|
| Backend | `0.10.1` | расширение терминов и приоритет действующих актов о регистрации ОсОО/ЖЧК |
|
||||||
| Frontend | — | ещё не создан |
|
| Frontend | — | ещё не создан |
|
||||||
| Сбор и обработка правовых данных | `0.8.0` | добавлены relevance set и оценка выдачи |
|
| Сбор и обработка правовых данных | `0.8.0` | добавлены relevance set и оценка выдачи |
|
||||||
| RAG и база знаний | — | ещё не созданы |
|
| RAG и база знаний | — | ещё не созданы |
|
||||||
@@ -58,4 +60,4 @@ python3 -m unittest discover -s tools/telegram-bot -v
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Акылдаш · Telegram-бот v0.2.2 · Backend v0.9.0 · Frontend — не создан
|
Акылдаш · Telegram-бот v0.2.2 · Backend v0.10.1 · Frontend — не создан
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Backend Акылдаш
|
# Backend Акылдаш
|
||||||
|
|
||||||
Версия: `0.9.0`
|
Версия: `0.10.1`
|
||||||
|
|
||||||
Первая backend-область проекта — загрузка правовых документов из официального
|
Первая backend-область проекта — загрузка правовых документов из официального
|
||||||
Open Data API ЦБД Минюста Кыргызской Республики. Код расположен в
|
Open Data API ЦБД Минюста Кыргызской Республики. Код расположен в
|
||||||
@@ -240,4 +240,4 @@ PYTHONPATH=backend python3 -m search.query "ЖЧК ачуу тартиби" --la
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Акылдаш · Backend v0.9.0 · Frontend — не создан
|
Акылдаш · Backend v0.10.1 · Frontend — не создан
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from datetime import datetime, timezone
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Iterable
|
from typing import Callable, Iterable
|
||||||
|
|
||||||
APP_VERSION = "0.9.0"
|
APP_VERSION = "0.9.3"
|
||||||
API_BASE_URL = "https://cbd.minjust.gov.kg/api/v1/OpenData/"
|
API_BASE_URL = "https://cbd.minjust.gov.kg/api/v1/OpenData/"
|
||||||
LANGUAGES = {"Rus": "ru", "Kyr": "ky"}
|
LANGUAGES = {"Rus": "ru", "Kyr": "ky"}
|
||||||
IMAGE_LANGUAGES = {"Russian": "ru", "Kyrgyz": "ky"}
|
IMAGE_LANGUAGES = {"Russian": "ru", "Kyrgyz": "ky"}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from pathlib import Path
|
|||||||
from typing import Callable
|
from typing import Callable
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
APP_VERSION = "0.9.0"
|
APP_VERSION = "0.9.3"
|
||||||
SCHEMA_VERSION = "1"
|
SCHEMA_VERSION = "1"
|
||||||
NORMALIZER_VERSION = "1.0.0"
|
NORMALIZER_VERSION = "1.0.0"
|
||||||
LANGUAGES = ("ru", "ky")
|
LANGUAGES = ("ru", "ky")
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ from pathlib import Path
|
|||||||
|
|
||||||
from search.minjust_opensearch import APP_VERSION, request_json
|
from search.minjust_opensearch import APP_VERSION, request_json
|
||||||
from search.catalog import CATALOGS, labels
|
from search.catalog import CATALOGS, labels
|
||||||
|
from search.query import build_search_query
|
||||||
from search.reviews import ReviewSnapshots, ReviewStore
|
from search.reviews import ReviewSnapshots, ReviewStore
|
||||||
|
|
||||||
|
|
||||||
API_VERSION = "v1"
|
API_VERSION = "v1"
|
||||||
SEARCH_ALGORITHM_VERSION = "search-1"
|
SEARCH_ALGORITHM_VERSION = "search-3"
|
||||||
LANGUAGES = {"ru", "ky"}
|
LANGUAGES = {"ru", "ky"}
|
||||||
CODE = re.compile(r"^[0-9]+$")
|
CODE = re.compile(r"^[0-9]+$")
|
||||||
MAX_PAGE_SIZE = 100
|
MAX_PAGE_SIZE = 100
|
||||||
@@ -139,7 +140,7 @@ class Api:
|
|||||||
"from": (page - 1) * page_size,
|
"from": (page - 1) * page_size,
|
||||||
"size": page_size + 1,
|
"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"],
|
"_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"},
|
"collapse": {"field": "document_code"},
|
||||||
"highlight": {"fields": {f"text_{language}": {"number_of_fragments": 1}}},
|
"highlight": {"fields": {f"text_{language}": {"number_of_fragments": 1}}},
|
||||||
}
|
}
|
||||||
@@ -163,7 +164,8 @@ class Api:
|
|||||||
raise ApiError(502, "search backend returned an incomplete result")
|
raise ApiError(502, "search backend returned an incomplete result")
|
||||||
highlight = hit.get("highlight", {}).get(f"text_{language}", [])
|
highlight = hit.get("highlight", {}).get(f"text_{language}", [])
|
||||||
legal_force = source.get("legal_force_code")
|
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}
|
snippet = highlight[0].replace("<em>", "").replace("</em>", "") if highlight else None
|
||||||
|
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": snippet}
|
||||||
|
|
||||||
def filters(self, query: dict[str, list[str]]) -> dict:
|
def filters(self, query: dict[str, list[str]]) -> dict:
|
||||||
language = one(query, "language") or "ru"
|
language = one(query, "language") or "ru"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""Immutable v1 search catalogs."""
|
"""Immutable v1 search catalogs."""
|
||||||
|
|
||||||
DOCUMENT_TYPES = {
|
DOCUMENT_TYPES = {
|
||||||
"constitution": ("Конституция", "Конституция"), "constitutional_law": ("Конституционный Закон", "Конституциалык Мыйзам"), "code": ("Кодекс", "Кодекс"), "law": ("Закон", "Мыйзам"), "decree": ("Указ", "Жарлык"), "resolution": ("Постановление", "Токтом"), "order": ("Распоряжение", "Распоряжение"), "instruction": ("Инструкция", "Инструкция"), "rules": ("Правила", "Правила"), "procedure": ("Порядок", "Порядок"), "provision": ("Положение", "Жобо"), "regulation": ("Регламент", "Регламент"), "charter": ("Устав", "Жобо (Устав)"), "program": ("Программа", "Программа"), "plan": ("План", "План"), "strategy": ("Стратегия", "Стратегия"), "concept": ("Концепция", "Концепция"), "doctrine": ("Доктрина", "Доктрина"), "agreement": ("Соглашение", "Соглашение"), "declaration": ("Декларация", "Декларация"), "registry": ("Реестр", "Реестр"), "norms": ("Нормативы", "Нормативы"), "model": ("Модель", "Модель"), "matrix": ("Матрица", "Матрица"), "study": ("Исследование", "Исследование"), "report": ("Доклад", "Доклад"), "principles": ("Основные принципы", "Основные принципы"), "unspecified": ("Не указан", "Көрсөтүлгөн эмес"),
|
"constitution": ("Конституция", "Конституция"), "constitutional_law": ("Конституционный Закон", "Конституциалык Мыйзам"), "code": ("Кодекс", "Кодекс"), "law": ("Закон", "Мыйзам"), "decree": ("Указ", "Жарлык"), "resolution": ("Постановление", "Токтом"), "order": ("Распоряжение", "Распоряжение"), "instruction": ("Инструкция", "Инструкция"), "rules": ("Правила", "Правила"), "procedure": ("Порядок", "Порядок"), "provision": ("Положение", "Жобо"), "regulation": ("Регламент", "Регламент"), "charter": ("Устав", "Жобо (Устав)"), "program": ("Программа", "Программа"), "plan": ("План", "План"), "strategy": ("Стратегия", "Стратегия"), "concept": ("Концепция", "Концепция"), "doctrine": ("Доктрина", "Доктрина"), "agreement": ("Соглашение", "Соглашение"), "declaration": ("Декларация", "Декларация"), "registry": ("Реестр", "Реестр"), "norms": ("Нормативы", "Нормативы"), "model": ("Модель", "Модель"), "matrix": ("Матрица", "Матрица"), "study": ("Исследование", "Исследование"), "report": ("Доклад", "Доклад"), "principles": ("Основные принципы", "Основные принципы"), "other": ("Прочие документы", "Башка документтер"), "unspecified": ("Не указан", "Көрсөтүлгөн эмес"),
|
||||||
}
|
}
|
||||||
STATUSES = {"active": ("Действует", "Күчүндө"), "repealed": ("Утратил силу", "Күчүн жоготту"), "unspecified": ("Не указан", "Көрсөтүлгөн эмес")}
|
STATUSES = {"active": ("Действует", "Күчүндө"), "repealed": ("Утратил силу", "Күчүн жоготту"), "not_yet_effective": ("Не вступил в силу", "Күчүнө кире элек"), "canceled": ("Отменено", "Жокко чыгарылды"), "inactive": ("Не действует", "Күчүндө эмес же Колдонулбайт"), "terminated": ("Прекратило действие", "Күчүн токтотту же Колдонуу токтотулган"), "suspended": ("Действие приостановлено", "Күчүнө кирүүсү токтотулган"), "unspecified": ("Не указан", "Көрсөтүлгөн эмес")}
|
||||||
AUTHORITIES = {"president": ("Президент", "Президент"), "parliament": ("Органы законодательной власти", "Мыйзам чыгаруу бийлик органдары"), "cabinet": ("Правительство и Кабинет Министров", "Өкмөт жана Министрлер Кабинети"), "ministries_and_committees": ("Министерства и государственные комитеты", "Министрликтер жана мамлекеттик комитеттер"), "administrative_agencies": ("Административные ведомства", "Административдик ведомстволор"), "national_bank": ("Национальный банк", "Улуттук банк"), "other_state_bodies": ("Иные государственные органы", "Башка мамлекеттик органдар"), "local_representative_bodies": ("Представительные органы местного самоуправления", "Жергиликтүү өз алдынча башкаруунун өкүлчүлүктүү органдары"), "other": ("Прочие органы", "Башка органдар")}
|
AUTHORITIES = {"president": ("Президент", "Президент"), "parliament": ("Органы законодательной власти", "Мыйзам чыгаруу бийлик органдары"), "cabinet": ("Правительство и Кабинет Министров", "Өкмөт жана Министрлер Кабинети"), "ministries_and_committees": ("Министерства и государственные комитеты", "Министрликтер жана мамлекеттик комитеттер"), "administrative_agencies": ("Административные ведомства", "Административдик ведомстволор"), "national_bank": ("Национальный банк", "Улуттук банк"), "other_state_bodies": ("Иные государственные органы", "Башка мамлекеттик органдар"), "local_representative_bodies": ("Представительные органы местного самоуправления", "Жергиликтүү өз алдынча башкаруунун өкүлчүлүктүү органдары"), "other": ("Прочие органы", "Башка органдар")}
|
||||||
LEGAL_FORCE_LEVELS = {
|
LEGAL_FORCE_LEVELS = {
|
||||||
"constitutional": ("Конституционный уровень", "Конституциялык деңгээл"),
|
"constitutional": ("Конституционный уровень", "Конституциялык деңгээл"),
|
||||||
@@ -19,6 +19,12 @@ LEGAL_FORCE_BY_DOCUMENT_TYPE = {
|
|||||||
"resolution": "subordinate",
|
"resolution": "subordinate",
|
||||||
}
|
}
|
||||||
CATALOGS = {"document_type": DOCUMENT_TYPES, "status": STATUSES, "authority": AUTHORITIES, "legal_force": LEGAL_FORCE_LEVELS}
|
CATALOGS = {"document_type": DOCUMENT_TYPES, "status": STATUSES, "authority": AUTHORITIES, "legal_force": LEGAL_FORCE_LEVELS}
|
||||||
|
DOCUMENT_TYPE_ALIASES = {
|
||||||
|
("Положение", "Положение"): "provision",
|
||||||
|
("Конституционный закон", "Конституционный закон"): "constitutional_law",
|
||||||
|
("Указ", "Жарлыгы"): "decree",
|
||||||
|
("устав", None): "charter",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def labels(category: str, code: str) -> dict[str, str]:
|
def labels(category: str, code: str) -> dict[str, str]:
|
||||||
@@ -33,9 +39,14 @@ def source_code(category: str, value: dict | None) -> str:
|
|||||||
pair = ((value or {}).get("ru"), (value or {}).get("ky"))
|
pair = ((value or {}).get("ru"), (value or {}).get("ky"))
|
||||||
if pair == (None, None):
|
if pair == (None, None):
|
||||||
return "unspecified"
|
return "unspecified"
|
||||||
|
if category == "document_type" and pair in DOCUMENT_TYPE_ALIASES:
|
||||||
|
return DOCUMENT_TYPE_ALIASES[pair]
|
||||||
for code, expected in CATALOGS[category].items():
|
for code, expected in CATALOGS[category].items():
|
||||||
if pair == expected or category == "document_type" and code == "provision" and pair == ("Положение", "Положение"):
|
if pair == expected:
|
||||||
return code
|
return code
|
||||||
|
# ponytail: uncurated source types share one filter; add reviewed codes when separate filtering is needed.
|
||||||
|
if category == "document_type":
|
||||||
|
return "other"
|
||||||
raise ValueError(f"Unmapped {category} catalog value: {pair!r}")
|
raise ValueError(f"Unmapped {category} catalog value: {pair!r}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from typing import Iterator
|
|||||||
|
|
||||||
from search.catalog import authority_codes, legal_force_code, source_code
|
from search.catalog import authority_codes, legal_force_code, source_code
|
||||||
|
|
||||||
APP_VERSION = "0.9.0"
|
APP_VERSION = "0.10.1"
|
||||||
LANGUAGES = {"ru", "ky"}
|
LANGUAGES = {"ru", "ky"}
|
||||||
DEFAULT_MAPPING = Path(__file__).with_name("minjust-fragments-index.json")
|
DEFAULT_MAPPING = Path(__file__).with_name("minjust-fragments-index.json")
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,60 @@ import urllib.parse
|
|||||||
from search.minjust_opensearch import APP_VERSION, request_json
|
from search.minjust_opensearch import APP_VERSION, request_json
|
||||||
|
|
||||||
|
|
||||||
|
LEGAL_FORM_ALIASES = {
|
||||||
|
"ru": {
|
||||||
|
"осоо": ("ооо", "общество с ограниченной ответственностью"),
|
||||||
|
"ооо": ("осоо", "общество с ограниченной ответственностью"),
|
||||||
|
"общество с ограниченной ответственностью": ("осоо", "ооо"),
|
||||||
|
"общества с ограниченной ответственностью": ("осоо", "ооо"),
|
||||||
|
},
|
||||||
|
"ky": {
|
||||||
|
"жчк": ("жоопкерчилиги чектелген коом",),
|
||||||
|
"жоопкерчилиги чектелген коом": ("жчк",),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
COMPANY_REGISTRATION_DOCUMENTS = (("230044970", 3000), ("667", 2000), ("4", 1000))
|
||||||
|
|
||||||
|
|
||||||
|
def query_variants(language: str, query: str) -> list[str]:
|
||||||
|
variants = [query]
|
||||||
|
for alias, expansions in LEGAL_FORM_ALIASES[language].items():
|
||||||
|
pattern = re.compile(rf"(?<!\w){re.escape(alias)}(?!\w)", re.IGNORECASE)
|
||||||
|
if pattern.search(query):
|
||||||
|
variants.extend(pattern.sub(expansion, query) for expansion in expansions)
|
||||||
|
return list(dict.fromkeys(variants))
|
||||||
|
|
||||||
|
|
||||||
def company_registration_clauses(language: str, query: str) -> list[dict]:
|
def company_registration_clauses(language: str, query: str) -> list[dict]:
|
||||||
patterns = {
|
non_registration_context = {
|
||||||
"ru": (r"\bкак\s+откры\w*\s+осоо\b", r"\b(порядок|процедура)\s+откры\w*\s+осоо\b", r"\bкак\s+зарегистр\w*\s+осоо\b"),
|
"ru": (r"\bсч[её]т\w*", r"\bбанк\w*", r"\bфилиал\w*", r"\bпредставительств\w*"),
|
||||||
"ky": (r"\bжчк\s+ач\w*\s+тартиби\b", r"\bжчк\s+кантип\s+ач\w*\b"),
|
"ky": (r"\bбанк\w*", r"\bэсеп\w*", r"\bфилиал\w*", r"\bөкүлчүлүк\w*"),
|
||||||
}[language]
|
}[language]
|
||||||
if not any(re.search(pattern, query.casefold()) for pattern in patterns):
|
normalized_query = query.casefold()
|
||||||
|
if any(re.search(pattern, normalized_query) for pattern in non_registration_context):
|
||||||
|
return []
|
||||||
|
|
||||||
|
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.format(form=forms), normalized_query) for pattern in patterns):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
status = {"ru": "Действует", "ky": "Күчүндө"}[language]
|
status = {"ru": "Действует", "ky": "Күчүндө"}[language]
|
||||||
@@ -35,28 +83,32 @@ def company_registration_clauses(language: str, query: str) -> list[dict]:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [clause("230044970", 2000), clause("667", 1000)]
|
return [clause(code, boost) for code, boost in COMPANY_REGISTRATION_DOCUMENTS]
|
||||||
|
|
||||||
|
|
||||||
|
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}
|
||||||
|
return {"bool": planned}
|
||||||
|
|
||||||
|
|
||||||
def build_search_body(language: str, query: str, top_k: int) -> bytes:
|
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({
|
return json.dumps({
|
||||||
"size": top_k,
|
"size": top_k,
|
||||||
"track_total_hits": False,
|
"track_total_hits": False,
|
||||||
"_source": ["document_code"],
|
"_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"},
|
"collapse": {"field": "document_code"},
|
||||||
}, ensure_ascii=False).encode()
|
}, ensure_ascii=False).encode()
|
||||||
|
|
||||||
|
|||||||
@@ -5,89 +5,143 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Оценка поисковой выдачи · Акылдаш</title>
|
<title>Оценка поисковой выдачи · Акылдаш</title>
|
||||||
<style>
|
<style>
|
||||||
:root { color-scheme: light; font: 16px/1.5 system-ui, sans-serif; color: #17202a; background: #f5f7fa; }
|
:root { color-scheme: light; font: 16px/1.5 system-ui, sans-serif; color: #17202a; background: #eef2f5; --ink: #17202a; --muted: #5d6b78; --line: #dbe2e8; --surface: #fff; --surface-muted: #f7f9fb; --brand: #0f5b78; --brand-dark: #0a4258; --accent: #e7b84b; --focus: #1769aa; }
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
body { margin: 0; }
|
body { margin: 0; color: var(--ink); background: radial-gradient(circle at 85% -10%, #dcecf0 0, transparent 35rem), #eef2f5; }
|
||||||
.review__skip { position: absolute; inset-block-start: 0; inset-inline-start: -10000px; padding: .5rem 1rem; background: #fff; }
|
.review__skip { position: absolute; inset-block-start: 0; inset-inline-start: -10000px; padding: .5rem 1rem; background: var(--surface); }
|
||||||
.review__skip:focus { inset-inline-start: 1rem; z-index: 2; }
|
.review__skip:focus { inset-inline-start: 1rem; z-index: 2; }
|
||||||
.review__header, .review__main { max-width: 1440px; margin: auto; padding-inline: 1rem; }
|
.review__header, .review__main { max-width: 1320px; margin: auto; padding-inline: clamp(1rem, 3vw, 3rem); }
|
||||||
.review__header { padding-block: 1.5rem 1rem; }
|
.review__header { padding-block: clamp(2rem, 5vw, 4rem) 1.5rem; }
|
||||||
.review__main { padding-block-end: 8rem; }
|
.review__header h1 { max-width: 18ch; margin: .35rem 0 .75rem; font-size: clamp(2rem, 4vw, 3.5rem); line-height: 1.05; letter-spacing: -.04em; }
|
||||||
.review__search { display: flex; flex-wrap: wrap; align-items: end; gap: .75rem; padding: 1rem; background: #fff; border: 1px solid #d9e0e7; border-radius: .75rem; }
|
.review__header p { max-width: 58ch; margin: 0; color: var(--muted); font-size: 1.05rem; }
|
||||||
.review__field { display: grid; gap: .25rem; min-width: 12rem; flex: 1; }
|
.review__header-top { display: flex; justify-content: space-between; gap: 1rem; align-items: start; }
|
||||||
.review__field--small { flex: 0 0 7rem; min-width: 7rem; }
|
.review__eyebrow { display: inline-flex; align-items: center; gap: .5rem; color: var(--brand); font-size: .75rem; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; }
|
||||||
|
.review__eyebrow::before { content: ''; width: .55rem; height: .55rem; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 4px rgb(231 184 75 / .2); }
|
||||||
|
.review__main { padding-block-end: 2rem; }
|
||||||
|
.review__search { display: grid; grid-template-columns: minmax(18rem, 1fr) 9rem 9rem auto; align-items: end; gap: .75rem; padding: 1.25rem; background: var(--surface); border: 1px solid var(--line); border-radius: 1rem; box-shadow: 0 1.25rem 3rem rgb(24 42 54 / .08); }
|
||||||
|
.review__field { display: grid; gap: .35rem; min-width: 0; }
|
||||||
|
.review__field span, .review__actions label { color: var(--muted); font-size: .8rem; font-weight: 700; }
|
||||||
|
.review__field--small { min-width: 0; }
|
||||||
input, select, textarea, button { font: inherit; }
|
input, select, textarea, button { font: inherit; }
|
||||||
input, select, textarea { border: 1px solid #8d9aaa; border-radius: .4rem; padding: .6rem .7rem; background: #fff; }
|
input, select, textarea { width: 100%; border: 1px solid #b8c4ce; border-radius: .6rem; padding: .7rem .8rem; background: var(--surface); color: var(--ink); }
|
||||||
input:focus-visible, select:focus-visible, textarea:focus-visible, button:focus-visible { outline: 3px solid #1769aa; outline-offset: 2px; }
|
input::placeholder, textarea::placeholder { color: #87939e; }
|
||||||
button { cursor: pointer; border: 1px solid #536273; border-radius: .4rem; padding: .6rem .9rem; background: #fff; color: #17202a; }
|
input:focus-visible, select:focus-visible, textarea:focus-visible, button:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
|
||||||
button:hover { background: #edf3f8; }
|
button { cursor: pointer; border: 1px solid #b8c4ce; border-radius: .6rem; padding: .7rem 1rem; background: var(--surface); color: var(--ink); font-weight: 700; transition-property: background-color, border-color, color, scale; transition-duration: 150ms; transition-timing-function: cubic-bezier(.2, 0, 0, 1); }
|
||||||
.review__button--primary { background: #145a86; color: #fff; border-color: #145a86; }
|
button:hover { background: #edf4f7; border-color: #7d9aaa; }
|
||||||
.review__button--primary:hover { background: #0e4669; }
|
button:active { scale: .96; }
|
||||||
.review__status { min-height: 1.7rem; margin-block: .75rem; }
|
button:disabled { cursor: wait; opacity: .55; }
|
||||||
|
.review__button--primary { background: var(--brand); color: #fff; border-color: var(--brand); }
|
||||||
|
.review__button--primary:hover { background: var(--brand-dark); border-color: var(--brand-dark); }
|
||||||
|
.review__status { min-height: 1.7rem; margin-block: 1rem; color: var(--muted); }
|
||||||
.review__status--error { color: #9b1c1c; }
|
.review__status--error { color: #9b1c1c; }
|
||||||
.review__workspace { display: grid; grid-template-columns: minmax(22rem, 1fr) minmax(24rem, 1.1fr); gap: 1rem; align-items: start; }
|
.review__workspace { display: grid; grid-template-columns: minmax(24rem, .9fr) minmax(28rem, 1.1fr); gap: 1.25rem; align-items: start; }
|
||||||
.review__results, .review__document { background: #fff; border: 1px solid #d9e0e7; border-radius: .75rem; padding: 1rem; }
|
.review__results, .review__document { min-width: 0; background: var(--surface); border: 1px solid var(--line); border-radius: 1rem; padding: clamp(1rem, 2vw, 1.5rem); box-shadow: 0 .75rem 2rem rgb(24 42 54 / .05); }
|
||||||
.review__results-list { display: grid; gap: 1rem; margin: 0; padding: 0; list-style: none; }
|
.review__panel-heading { display: flex; justify-content: space-between; gap: 1rem; align-items: baseline; margin-block-end: 1rem; }
|
||||||
.review__result { border-block-start: 1px solid #d9e0e7; padding-block-start: 1rem; }
|
.review__panel-heading h2 { margin: 0; font-size: 1.1rem; letter-spacing: -.01em; }
|
||||||
.review__result:first-child { border-block-start: 0; padding-block-start: 0; }
|
.review__panel-hint { color: var(--muted); font-size: .8rem; }
|
||||||
.review__result-title { display: flex; gap: .5rem; align-items: baseline; width: 100%; text-align: start; font-weight: 700; border: 0; padding: 0; color: #124f78; }
|
.review__results-list { display: grid; gap: .75rem; margin: 0; padding: 0; list-style: none; }
|
||||||
.review__rank { flex: 0 0 auto; color: #526272; font-variant-numeric: tabular-nums; }
|
.review__result { padding: 1rem; border: 1px solid var(--line); border-radius: .75rem; background: var(--surface-muted); transition-property: background-color, border-color, box-shadow, scale; transition-duration: 150ms; transition-timing-function: cubic-bezier(.2, 0, 0, 1); }
|
||||||
.review__meta, .review__snippet { margin-block: .35rem; color: #526272; }
|
.review__result:hover, .review__result--selected { border-color: #8eb3c1; background: #f1f8fa; box-shadow: 0 .5rem 1rem rgb(15 91 120 / .08); }
|
||||||
|
.review__result:first-child { border-block-start: 1px solid var(--line); }
|
||||||
|
.review__result-title { display: flex; gap: .65rem; align-items: baseline; width: 100%; text-align: start; font-weight: 800; border: 0; padding: 0; background: none; color: var(--brand-dark); }
|
||||||
|
.review__result-title:hover { background: none; border-color: transparent; color: var(--brand); }
|
||||||
|
.review__rank { flex: 0 0 auto; color: var(--brand); font-variant-numeric: tabular-nums; }
|
||||||
|
.review__meta, .review__snippet { margin-block: .5rem; color: var(--muted); }
|
||||||
|
.review__meta { font-size: .8rem; }
|
||||||
.review__snippet { overflow-wrap: anywhere; }
|
.review__snippet { overflow-wrap: anywhere; }
|
||||||
.review__rating { display: flex; flex-wrap: wrap; gap: .45rem; margin-block: .65rem; padding: 0; border: 0; }
|
.review__rating { display: grid; grid-template-columns: repeat(4, 1fr); gap: .4rem; margin-block: 1rem .75rem; padding: 0; border: 0; }
|
||||||
.review__rating legend { width: 100%; font-weight: 600; }
|
.review__rating legend { width: 100%; margin-block-end: .35rem; font-size: .8rem; font-weight: 700; color: var(--muted); grid-column: 1 / -1; }
|
||||||
.review__rating label { min-width: 3.5rem; text-align: center; }
|
.review__rating label { min-width: 0; padding: .45rem .25rem; border: 1px solid #c5d0d8; border-radius: .5rem; color: var(--muted); text-align: center; font-size: .8rem; cursor: pointer; }
|
||||||
.review__rating input { accent-color: #145a86; }
|
.review__rating label:has(input:checked) { border-color: var(--brand); background: var(--brand); color: #fff; }
|
||||||
.review__comment { width: 100%; min-height: 4rem; resize: vertical; }
|
.review__rating label:hover { border-color: #7d9aaa; }
|
||||||
.review__document { position: sticky; inset-block-start: 1rem; min-height: 20rem; }
|
.review__rating label:has(input:focus-visible) { outline: 3px solid var(--focus); outline-offset: 2px; }
|
||||||
|
.review__rating input { position: absolute; opacity: 0; inline-size: 1px; block-size: 1px; }
|
||||||
|
.review__comment { min-height: 4rem; resize: vertical; background: var(--surface); }
|
||||||
|
.review__document { position: sticky; inset-block-start: 1rem; min-height: 24rem; }
|
||||||
|
.review__document-meta { margin-block-end: 1rem; padding-block-end: 1rem; border-block-end: 1px solid var(--line); color: var(--muted); }
|
||||||
.review__document-body { max-width: 75ch; overflow-wrap: anywhere; }
|
.review__document-body { max-width: 75ch; overflow-wrap: anywhere; }
|
||||||
.review__document-body img { max-width: 100%; height: auto; }
|
.review__document-body img { max-width: 100%; height: auto; }
|
||||||
.review__actions { position: fixed; inset-block-end: 0; inset-inline: 0; padding: .75rem 1rem; background: rgb(255 255 255 / .96); border-block-start: 1px solid #d9e0e7; text-align: end; }
|
.review__actions { max-width: 1320px; margin: 1.5rem auto 0; padding-inline: clamp(1rem, 3vw, 3rem); }
|
||||||
dialog { max-width: min(56rem, calc(100% - 2rem)); max-height: calc(100% - 2rem); border: 1px solid #8d9aaa; border-radius: .75rem; padding: 1rem; }
|
.review__actions-inner { display: flex; align-items: end; gap: .75rem; padding: 1rem; background: var(--surface); border: 1px solid var(--line); border-radius: 1rem; box-shadow: 0 .75rem 2rem rgb(24 42 54 / .05); }
|
||||||
|
.review__actions label { display: grid; gap: .35rem; flex: 1; color: var(--muted); font-size: .8rem; font-weight: 700; }
|
||||||
|
.review__actions label:last-of-type { flex: 2; }
|
||||||
|
dialog { max-width: min(56rem, calc(100% - 2rem)); max-height: calc(100% - 2rem); border: 1px solid var(--line); border-radius: 1rem; padding: 1.5rem; box-shadow: 0 2rem 5rem rgb(10 20 30 / .2); }
|
||||||
dialog::backdrop { background: rgb(10 20 30 / .5); }
|
dialog::backdrop { background: rgb(10 20 30 / .5); }
|
||||||
.review__dialog-close { float: inline-end; }
|
.review__dialog-close { float: inline-end; }
|
||||||
|
.review__empty { padding: 3rem 1rem; color: var(--muted); text-align: center; }
|
||||||
|
.review__empty strong { display: block; margin-block-end: .35rem; color: var(--ink); }
|
||||||
|
.review__tour-spotlight { position: fixed; z-index: 1; pointer-events: none; border: 4px solid var(--accent); border-radius: .75rem; box-shadow: 0 0 0 100vmax rgb(10 30 40 / .58), 0 0 0 8px rgb(231 184 75 / .25); }
|
||||||
|
.review__tour { width: min(34rem, calc(100% - 2rem)); padding: 0; color: var(--ink); }
|
||||||
|
.review__tour::backdrop { background: transparent; }
|
||||||
|
.review__tour-content { padding: clamp(1.25rem, 4vw, 2rem); }
|
||||||
|
.review__tour-kicker { margin: 0 0 .5rem; color: var(--brand); font-size: .75rem; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; }
|
||||||
|
.review__tour h2 { margin: 0 0 .65rem; font-size: clamp(1.35rem, 3vw, 1.75rem); line-height: 1.1; letter-spacing: -.02em; }
|
||||||
|
.review__tour p { margin: 0; color: var(--muted); }
|
||||||
|
.review__tour-footer { display: flex; justify-content: space-between; gap: .75rem; align-items: center; margin-block-start: 1.5rem; }
|
||||||
|
.review__tour-progress { color: var(--muted); font-size: .8rem; }
|
||||||
|
.review__tour-actions { display: flex; gap: .5rem; }
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
|
.review__search { grid-template-columns: 1fr 1fr; }
|
||||||
|
.review__search .review__field:first-child { grid-column: 1 / -1; }
|
||||||
.review__workspace { grid-template-columns: 1fr; }
|
.review__workspace { grid-template-columns: 1fr; }
|
||||||
|
.review__main { padding-block-end: 2rem; }
|
||||||
.review__document { display: none; }
|
.review__document { display: none; }
|
||||||
.review__search { align-items: stretch; }
|
.review__document--tour-visible { display: block; }
|
||||||
.review__field, .review__field--small { flex-basis: 100%; }
|
.review__search button { grid-column: 1 / -1; }
|
||||||
.review__search button { width: 100%; }
|
.review__actions { padding-inline: 1rem; }
|
||||||
|
.review__actions-inner { flex-wrap: wrap; }
|
||||||
|
.review__actions label { flex: 1 1 14rem !important; }
|
||||||
|
.review__actions button { flex: 1 1 100%; }
|
||||||
}
|
}
|
||||||
|
@media (max-width: 480px) { .review__search { grid-template-columns: 1fr; } .review__search .review__field:first-child, .review__search button { grid-column: auto; } .review__rating label { font-size: .72rem; } }
|
||||||
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; } }
|
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; } }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<a class="review__skip" href="#results">Перейти к результатам</a>
|
<a class="review__skip" href="#results">Перейти к результатам</a>
|
||||||
<header class="review__header"><h1>Оценка поисковой выдачи</h1><p>Проверяйте результаты нашего OpenSearch по практическим юридическим запросам.</p></header>
|
<header class="review__header">
|
||||||
|
<div class="review__header-top"><div class="review__eyebrow">Внутренняя лаборатория</div><button id="tutorial-open" type="button">Как это работает</button></div>
|
||||||
|
<h1>Проверка качества поиска</h1><p>Помогите проверить, насколько первые результаты отвечают на юридический запрос.</p>
|
||||||
|
</header>
|
||||||
<main class="review__main" id="review">
|
<main class="review__main" id="review">
|
||||||
<form class="review__search" id="search-form">
|
<form class="review__search" id="search-form">
|
||||||
<label class="review__field">Запрос<input id="query" name="q" maxlength="500" required autocomplete="off"></label>
|
<label class="review__field">Юридический запрос<input id="query" name="q" maxlength="500" required autocomplete="off" placeholder="Например, как открыть ОсОО?"></label>
|
||||||
<label class="review__field review__field--small">Язык<select id="language" name="language"><option value="ru">Русский</option><option value="ky">Кыргызский</option></select></label>
|
<label class="review__field review__field--small">Язык запроса<select id="language" name="language"><option value="ru">Русский</option><option value="ky">Кыргызский</option></select></label>
|
||||||
<label class="review__field review__field--small">Результатов<select id="page-size" name="page_size"><option>10</option><option>20</option></select></label>
|
<label class="review__field review__field--small">Результатов<select id="page-size" name="page_size"><option>10</option><option>20</option></select></label>
|
||||||
<button class="review__button--primary" type="submit">Найти</button>
|
<button class="review__button--primary" type="submit">Показать результаты</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="review__status" id="status" role="status" aria-live="polite"></div>
|
<div class="review__status" id="status" role="status" aria-live="polite"></div>
|
||||||
<div class="review__workspace">
|
<div class="review__workspace">
|
||||||
<section class="review__results" aria-labelledby="results-heading"><h2 id="results-heading">Результаты</h2><ol class="review__results-list" id="results"></ol></section>
|
<section class="review__results" aria-labelledby="results-heading"><div class="review__panel-heading"><h2 id="results-heading">Результаты поиска</h2><span class="review__panel-hint">Оцените каждый документ</span></div><ol class="review__results-list" id="results"><li class="review__empty"><strong>Начните с запроса</strong>Введите юридический вопрос выше, чтобы увидеть результаты.</li></ol></section>
|
||||||
<section class="review__document" aria-labelledby="document-heading"><h2 id="document-heading">Документ</h2><div id="document-meta">Выберите результат, чтобы открыть текст.</div><article class="review__document-body" id="document-body"></article></section>
|
<section class="review__document" id="document-panel" aria-labelledby="document-heading"><div class="review__panel-heading"><h2 id="document-heading">Просмотр документа</h2><span class="review__panel-hint">Откройте результат</span></div><div class="review__document-meta" id="document-meta">Выберите название в списке, чтобы прочитать документ.</div><article class="review__document-body" id="document-body"></article></section>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<div class="review__actions"><label>Проверяющий <input id="reviewer" maxlength="120" autocomplete="name"></label> <label>Общий комментарий <input id="overall-comment" maxlength="4000"></label> <button class="review__button--primary" id="save" type="button">Сохранить оценку</button></div>
|
<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>
|
<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__status review__status--error" id="error" role="alert" aria-live="assertive"></div>
|
<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.9.0 · Внутренняя лаборатория релевантности</footer>
|
<footer class="review__header">Акылдаш · Backend v0.10.1 · Внутренняя лаборатория релевантности</footer>
|
||||||
<script>
|
<script>
|
||||||
const DRAFT_KEY = 'akyldash-search-review-draft';
|
const DRAFT_KEY = 'akyldash-search-review-draft';
|
||||||
|
const TOUR_KEY = 'akyldash-search-review-tour-seen';
|
||||||
|
const TOUR_STEPS = [
|
||||||
|
{ target: 'query', title: 'Начните с юридического вопроса', text: 'Введите практический запрос, например «как открыть ОсОО?», выберите язык и нажмите «Показать результаты».', label: 'Шаг 1 из 4' },
|
||||||
|
{ target: 'results', title: 'Смотрите порядок выдачи', text: 'Документы расположены в том порядке, в котором их нашёл поиск. Номер слева — позиция результата.', label: 'Шаг 2 из 4' },
|
||||||
|
{ target: 'document-panel', title: 'Читайте документ перед оценкой', text: 'Нажмите на название результата. Справа откроется текст документа и ссылка на официальный источник.', label: 'Шаг 3 из 4' },
|
||||||
|
{ target: 'save', title: 'Сохраните свою разметку', text: 'Поставьте оценку каждому просмотренному документу, добавьте комментарии и сохраните результат внизу страницы.', label: 'Шаг 4 из 4' }
|
||||||
|
];
|
||||||
const state = { results: [], token: '', selected: null, query: '' };
|
const state = { results: [], token: '', selected: null, query: '' };
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
const setStatus = (text) => { $('status').textContent = text; $('error').textContent = ''; };
|
const setStatus = (text, error = false) => { $('status').textContent = text; $('status').classList.toggle('review__status--error', error); $('status').setAttribute('role', error ? 'alert' : 'status'); $('status').setAttribute('aria-live', error ? 'assertive' : 'polite'); };
|
||||||
const setError = (text) => { $('error').textContent = text; };
|
const setError = (text) => setStatus(text, true);
|
||||||
const escapeText = (value) => value == null ? '' : String(value);
|
const escapeText = (value) => value == null ? '' : String(value);
|
||||||
const readDraft = () => { try { return JSON.parse(localStorage.getItem(DRAFT_KEY) || 'null'); } catch (_) { return null; } };
|
const readDraft = () => { try { return JSON.parse(localStorage.getItem(DRAFT_KEY) || 'null'); } catch (_) { return null; } };
|
||||||
|
const hasSeenTour = () => { try { return localStorage.getItem(TOUR_KEY) === '1'; } catch (_) { return false; } };
|
||||||
|
const markTourSeen = () => { try { localStorage.setItem(TOUR_KEY, '1'); } catch (_) {} };
|
||||||
const saveDraft = () => { if (!state.token) return; const results = [...$('results').children].map((item, index) => ({rank: index + 1, code: item.dataset.code, rating: item.querySelector('input:checked')?.value ?? null, comment: item.querySelector('textarea').value})); try { localStorage.setItem(DRAFT_KEY, JSON.stringify({query: state.query, language: $('language').value, pageSize: $('page-size').value, reviewer: $('reviewer').value, overallComment: $('overall-comment').value, results})); } catch (_) {} };
|
const saveDraft = () => { if (!state.token) return; const results = [...$('results').children].map((item, index) => ({rank: index + 1, code: item.dataset.code, rating: item.querySelector('input:checked')?.value ?? null, comment: item.querySelector('textarea').value})); try { localStorage.setItem(DRAFT_KEY, JSON.stringify({query: state.query, language: $('language').value, pageSize: $('page-size').value, reviewer: $('reviewer').value, overallComment: $('overall-comment').value, results})); } catch (_) {} };
|
||||||
const applyDraft = () => { const draft = readDraft(); if (!draft || draft.query !== state.query || draft.language !== $('language').value || draft.pageSize !== $('page-size').value) return; $('reviewer').value = draft.reviewer || ''; $('overall-comment').value = draft.overallComment || ''; (draft.results || []).forEach((saved) => { const item = [...$('results').children].find((candidate) => candidate.dataset.rank === String(saved.rank) && candidate.dataset.code === saved.code); if (!item) return; if (saved.rating != null) { const input = item.querySelector(`input[value="${CSS.escape(String(saved.rating))}"]`); if (input) input.checked = true; } item.querySelector('textarea').value = saved.comment || ''; }); };
|
const applyDraft = () => { const draft = readDraft(); if (!draft || draft.query !== state.query || draft.language !== $('language').value || draft.pageSize !== $('page-size').value) return; $('reviewer').value = draft.reviewer || ''; $('overall-comment').value = draft.overallComment || ''; (draft.results || []).forEach((saved) => { const item = [...$('results').children].find((candidate) => candidate.dataset.rank === String(saved.rank) && candidate.dataset.code === saved.code); if (!item) return; if (saved.rating != null) { const input = item.querySelector(`input[value="${CSS.escape(String(saved.rating))}"]`); if (input) input.checked = true; } item.querySelector('textarea').value = saved.comment || ''; }); };
|
||||||
function renderResults() {
|
function renderResults() {
|
||||||
$('results').replaceChildren();
|
$('results').replaceChildren();
|
||||||
|
if (!state.results.length) { $('results').innerHTML = '<li class="review__empty"><strong>Результатов пока нет</strong>Введите другой запрос и попробуйте снова.</li>'; return; }
|
||||||
state.results.forEach((result, index) => {
|
state.results.forEach((result, index) => {
|
||||||
const item = document.createElement('li'); item.className = 'review__result'; item.dataset.rank = index + 1; item.dataset.code = result.code;
|
const item = document.createElement('li'); item.className = 'review__result'; item.dataset.rank = index + 1; item.dataset.code = result.code;
|
||||||
const title = document.createElement('button'); title.type = 'button'; title.className = 'review__result-title'; title.innerHTML = `<span class="review__rank">#${index + 1}</span><span></span>`; title.lastElementChild.textContent = escapeText(result.name || 'Название не указано'); title.addEventListener('click', () => openDocument(index, title));
|
const title = document.createElement('button'); title.type = 'button'; title.className = 'review__result-title'; title.innerHTML = `<span class="review__rank">#${index + 1}</span><span></span>`; title.lastElementChild.textContent = escapeText(result.name || 'Название не указано'); title.addEventListener('click', () => openDocument(index, title));
|
||||||
@@ -101,14 +155,21 @@
|
|||||||
applyDraft();
|
applyDraft();
|
||||||
}
|
}
|
||||||
async function openDocument(index, trigger) {
|
async function openDocument(index, trigger) {
|
||||||
const result = state.results[index]; state.selected = trigger; setStatus('Загрузка документа…');
|
const result = state.results[index]; state.selected = trigger; [...$('results').children].forEach((item) => item.classList.remove('review__result--selected')); trigger.closest('.review__result')?.classList.add('review__result--selected'); setStatus('Загрузка документа…');
|
||||||
try { const response = await fetch(`/documents/${encodeURIComponent(result.code)}/editions/${encodeURIComponent(result.edition)}?language=${encodeURIComponent($('language').value)}`); if (!response.ok) throw new Error('Документ недоступен'); const payload = await response.json(); const content = payload.content[$('language').value]; const meta = $('document-meta'); meta.textContent = [result.name, result.status, result.date_adopted].filter(Boolean).join(' · '); const source = document.createElement('a'); source.href = `https://cbd.minjust.gov.kg/${encodeURIComponent(result.code)}/edition/${encodeURIComponent(result.edition)}/${encodeURIComponent($('language').value)}`; source.target = '_blank'; source.rel = 'noreferrer'; source.textContent = ' Официальный источник'; meta.append(source); $('document-body').innerHTML = content.html; $('dialog-heading').textContent = result.name || 'Документ'; $('dialog-body').innerHTML = content.html; if (matchMedia('(max-width: 800px)').matches) $('document-dialog').showModal(); setStatus('Документ загружен.'); } catch (error) { setError('Не удалось загрузить документ. Повторите попытку.'); }
|
try { const response = await fetch(`/documents/${encodeURIComponent(result.code)}/editions/${encodeURIComponent(result.edition)}?language=${encodeURIComponent($('language').value)}`); if (!response.ok) throw new Error('Документ недоступен'); const payload = await response.json(); const content = payload.content[$('language').value]; const meta = $('document-meta'); meta.textContent = [result.name, result.status, result.date_adopted].filter(Boolean).join(' · '); const source = document.createElement('a'); source.href = `https://cbd.minjust.gov.kg/${encodeURIComponent(result.code)}/edition/${encodeURIComponent(result.edition)}/${encodeURIComponent($('language').value)}`; source.target = '_blank'; source.rel = 'noreferrer'; source.textContent = ' Официальный источник'; meta.append(source); $('document-body').innerHTML = content.html; $('dialog-heading').textContent = result.name || 'Документ'; $('dialog-body').innerHTML = content.html; if (matchMedia('(max-width: 800px)').matches) $('document-dialog').showModal(); setStatus('Документ загружен.'); } catch (error) { setError('Не удалось загрузить документ. Повторите попытку.'); }
|
||||||
}
|
}
|
||||||
$('dialog-close').addEventListener('click', () => { $('document-dialog').close(); if (state.selected) state.selected.focus(); });
|
$('dialog-close').addEventListener('click', () => { $('document-dialog').close(); if (state.selected) state.selected.focus(); });
|
||||||
|
let tutorialStep = 0; let tutorialReturnFocus = null;
|
||||||
|
function clearTourTarget() { $('tutorial-spotlight').hidden = true; $('document-panel').classList.remove('review__document--tour-visible'); }
|
||||||
|
function renderTutorial() { const step = TOUR_STEPS[tutorialStep]; const target = $(step.target); if (step.target === 'document-panel' && getComputedStyle(target).display === 'none') target.classList.add('review__document--tour-visible'); target.scrollIntoView({block: 'nearest', inline: 'nearest'}); const bounds = target.getBoundingClientRect(); $('tutorial-spotlight').style.cssText = `inset-block-start: ${Math.max(8, bounds.top - 4)}px; inset-inline-start: ${Math.max(8, bounds.left - 4)}px; inline-size: ${bounds.width + 8}px; block-size: ${bounds.height + 8}px;`; $('tutorial-spotlight').hidden = false; $('tutorial-heading').textContent = step.title; $('tutorial-text').textContent = step.text; $('tutorial-progress').textContent = step.label; $('tutorial-next').textContent = tutorialStep === TOUR_STEPS.length - 1 ? 'Понятно' : 'Далее'; }
|
||||||
|
function closeTutorial() { clearTourTarget(); $('tutorial-dialog').close(); markTourSeen(); tutorialReturnFocus?.focus(); }
|
||||||
|
function openTutorial() { tutorialReturnFocus = document.activeElement; tutorialStep = 0; renderTutorial(); $('tutorial-dialog').showModal(); $('tutorial-next').focus(); }
|
||||||
|
$('tutorial-open').addEventListener('click', openTutorial); $('tutorial-next').addEventListener('click', () => { if (tutorialStep === TOUR_STEPS.length - 1) closeTutorial(); else { tutorialStep += 1; renderTutorial(); } }); $('tutorial-skip').addEventListener('click', closeTutorial); $('tutorial-dialog').addEventListener('cancel', (event) => { event.preventDefault(); closeTutorial(); });
|
||||||
$('search-form').addEventListener('submit', async (event) => { event.preventDefault(); const query = $('query').value.trim(); if (!query) return; state.query = query; setStatus('Поиск выполняется…'); $('save').disabled = true; try { const params = new URLSearchParams({q: query, language: $('language').value, page_size: $('page-size').value}); const response = await fetch(`/search?${params}`); if (!response.ok) throw new Error(); const payload = await response.json(); state.results = payload.results; state.token = payload.review_token; renderResults(); setStatus(state.results.length ? `Найдено результатов: ${state.results.length}.` : `По запросу «${query}» ничего не найдено. Измените запрос.`); } catch (error) { state.results = []; state.token = ''; renderResults(); setError('Не удалось выполнить поиск. Повторите поиск.'); } finally { $('save').disabled = false; } });
|
$('search-form').addEventListener('submit', async (event) => { event.preventDefault(); const query = $('query').value.trim(); if (!query) return; state.query = query; setStatus('Поиск выполняется…'); $('save').disabled = true; try { const params = new URLSearchParams({q: query, language: $('language').value, page_size: $('page-size').value}); const response = await fetch(`/search?${params}`); if (!response.ok) throw new Error(); const payload = await response.json(); state.results = payload.results; state.token = payload.review_token; renderResults(); setStatus(state.results.length ? `Найдено результатов: ${state.results.length}.` : `По запросу «${query}» ничего не найдено. Измените запрос.`); } catch (error) { state.results = []; state.token = ''; renderResults(); setError('Не удалось выполнить поиск. Повторите поиск.'); } finally { $('save').disabled = false; } });
|
||||||
document.addEventListener('input', saveDraft); document.addEventListener('change', saveDraft);
|
document.addEventListener('input', saveDraft); document.addEventListener('change', saveDraft);
|
||||||
$('save').addEventListener('click', async () => { if (!state.token) { setError('Сначала выполните поиск.'); return; } const reviewer = $('reviewer').value.trim(); if (!reviewer) { $('reviewer').focus(); setError('Укажите проверяющего.'); return; } const results = [...$('results').children].map((item, index) => ({rank: index + 1, code: item.dataset.code, rating: item.querySelector('input:checked') ? Number(item.querySelector('input:checked').value) : null, comment: item.querySelector('textarea').value})); $('save').disabled = true; setStatus('Сохранение выполняется…'); try { const response = await fetch('/search-reviews', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({review_token: state.token, reviewer, overall_comment: $('overall-comment').value, results})}); if (!response.ok) throw new Error(); const payload = await response.json(); localStorage.removeItem(DRAFT_KEY); setStatus(`Оценка сохранена · № ${payload.id}.`); } catch (error) { setError('Не удалось сохранить. Проверьте подключение и повторите.'); } finally { $('save').disabled = false; } });
|
$('save').addEventListener('click', async () => { if (!state.token) { setError('Сначала выполните поиск.'); return; } const reviewer = $('reviewer').value.trim(); if (!reviewer) { $('reviewer').focus(); setError('Укажите проверяющего.'); return; } const results = [...$('results').children].map((item, index) => ({rank: index + 1, code: item.dataset.code, rating: item.querySelector('input:checked') ? Number(item.querySelector('input:checked').value) : null, comment: item.querySelector('textarea').value})); $('save').disabled = true; setStatus('Сохранение выполняется…'); try { const response = await fetch('/search-reviews', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({review_token: state.token, reviewer, overall_comment: $('overall-comment').value, results})}); if (!response.ok) throw new Error(); const payload = await response.json(); localStorage.removeItem(DRAFT_KEY); setStatus(`Оценка сохранена · № ${payload.id}.`); } catch (error) { setError('Не удалось сохранить. Проверьте подключение и повторите.'); } finally { $('save').disabled = false; } });
|
||||||
const draft = readDraft(); if (draft) { $('query').value = draft.query || ''; $('language').value = draft.language || 'ru'; $('page-size').value = draft.pageSize || '20'; $('reviewer').value = draft.reviewer || ''; $('overall-comment').value = draft.overallComment || ''; }
|
const draft = readDraft(); if (draft) { $('query').value = draft.query || ''; $('language').value = draft.language || 'ru'; $('page-size').value = draft.pageSize || '20'; $('reviewer').value = draft.reviewer || ''; $('overall-comment').value = draft.overallComment || ''; }
|
||||||
|
if (!hasSeenTour()) setTimeout(openTutorial, 350);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -9,10 +9,22 @@ from contextlib import closing
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from search.catalog import legal_force_code, labels, source_code
|
||||||
from search.minjust_opensearch import bulk_batches, document_codes, export_bulk, load_bulk, request_json, switch_alias
|
from search.minjust_opensearch import bulk_batches, document_codes, export_bulk, load_bulk, request_json, switch_alias
|
||||||
|
|
||||||
|
|
||||||
class MinjustOpenSearchTest(unittest.TestCase):
|
class MinjustOpenSearchTest(unittest.TestCase):
|
||||||
|
def test_unknown_document_type_keeps_source_label_and_has_no_inferred_force(self):
|
||||||
|
value = {"ru": "Решение", "ky": "Решение"}
|
||||||
|
|
||||||
|
self.assertEqual(source_code("document_type", value), "other")
|
||||||
|
self.assertEqual(labels("document_type", "other"), {"ru": "Прочие документы", "ky": "Башка документтер"})
|
||||||
|
self.assertIsNone(legal_force_code(value))
|
||||||
|
self.assertEqual(source_code("status", {"ru": "Не вступил в силу", "ky": "Күчүнө кире элек"}), "not_yet_effective")
|
||||||
|
self.assertEqual(legal_force_code({"ru": "Конституционный закон", "ky": "Конституционный закон"}), "constitutional")
|
||||||
|
self.assertEqual(legal_force_code({"ru": "Указ", "ky": "Жарлыгы"}), "subordinate")
|
||||||
|
self.assertEqual(source_code("document_type", {"ru": "устав"}), "charter")
|
||||||
|
|
||||||
def test_switches_alias_atomically_after_successful_load(self):
|
def test_switches_alias_atomically_after_successful_load(self):
|
||||||
with patch("search.minjust_opensearch.request_json", return_value={"acknowledged": True}) as request:
|
with patch("search.minjust_opensearch.request_json", return_value={"acknowledged": True}) as request:
|
||||||
switch_alias("http://127.0.0.1:9200/", "akyldash-fragments-v2", "akyldash-fragments-current")
|
switch_alias("http://127.0.0.1:9200/", "akyldash-fragments-v2", "akyldash-fragments-current")
|
||||||
|
|||||||
@@ -29,13 +29,26 @@ class SearchApiTest(unittest.TestCase):
|
|||||||
with patch("search.api.request_json", return_value=response) as request:
|
with patch("search.api.request_json", return_value=response) as request:
|
||||||
status, payload = api.handle("GET", "/search?q=%D0%B7%D0%B0%D0%BA%D0%BE%D0%BD&language=ru&page=2&page_size=5&status=active")
|
status, payload = api.handle("GET", "/search?q=%D0%B7%D0%B0%D0%BA%D0%BE%D0%BD&language=ru&page=2&page_size=5&status=active")
|
||||||
self.assertEqual(status, 200)
|
self.assertEqual(status, 200)
|
||||||
self.assertEqual(payload["results"][0]["snippet"], "<em>Закон</em>")
|
self.assertEqual(payload["results"][0]["snippet"], "Закон")
|
||||||
body = json.loads(request.call_args.args[2])
|
body = json.loads(request.call_args.args[2])
|
||||||
self.assertEqual((body["from"], body["size"]), (5, 6))
|
self.assertEqual((body["from"], body["size"]), (5, 6))
|
||||||
self.assertIn({"term": {"status_code": "active"}}, body["query"]["bool"]["filter"])
|
self.assertIn({"term": {"status_code": "active"}}, body["query"]["bool"]["filter"])
|
||||||
with self.assertRaisesRegex(ApiError, "within 10000 results"):
|
with self.assertRaisesRegex(ApiError, "within 10000 results"):
|
||||||
api.handle("GET", "/search?q=x&page=100&page_size=100")
|
api.handle("GET", "/search?q=x&page=100&page_size=100")
|
||||||
|
|
||||||
|
def test_production_api_uses_registration_intent_boosts(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
api = self.make_api(Path(temporary))
|
||||||
|
response = {"hits": {"hits": []}}
|
||||||
|
with patch("search.api.request_json", return_value=response) as request:
|
||||||
|
api.handle("GET", "/search?q=%D0%BA%D0%B0%D0%BA+%D0%BE%D1%82%D0%BA%D1%80%D1%8B%D1%82%D1%8C+%D0%9E%D1%81%D0%9E%D0%9E&language=ru")
|
||||||
|
body = json.loads(request.call_args.args[2])
|
||||||
|
planned = body["query"]["bool"]["must"]["bool"]
|
||||||
|
self.assertEqual(planned["minimum_should_match"], 1)
|
||||||
|
self.assertNotIn("filter", planned)
|
||||||
|
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_document_editions_openapi_and_validation(self):
|
def test_document_editions_openapi_and_validation(self):
|
||||||
with tempfile.TemporaryDirectory() as temporary:
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
api = self.make_api(Path(temporary))
|
api = self.make_api(Path(temporary))
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from search.evaluate_relevance import evaluate, load_queries
|
from search.evaluate_relevance import evaluate, load_queries
|
||||||
from search.query import build_search_body
|
from search.query import COMPANY_REGISTRATION_DOCUMENTS, build_search_body, query_variants
|
||||||
|
|
||||||
|
|
||||||
class SearchRelevanceTest(unittest.TestCase):
|
class SearchRelevanceTest(unittest.TestCase):
|
||||||
@@ -37,7 +37,15 @@ class SearchRelevanceTest(unittest.TestCase):
|
|||||||
body = json.loads(build_search_body("ru", "трудовой договор", 10))
|
body = json.loads(build_search_body("ru", "трудовой договор", 10))
|
||||||
self.assertFalse(body["track_total_hits"])
|
self.assertFalse(body["track_total_hits"])
|
||||||
self.assertEqual(body["collapse"], {"field": "document_code"})
|
self.assertEqual(body["collapse"], {"field": "document_code"})
|
||||||
self.assertEqual(body["query"]["bool"]["must"]["multi_match"]["type"], "cross_fields")
|
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["operator"], "AND")
|
||||||
|
|
||||||
|
def test_legal_form_abbreviations_expand_in_both_directions(self):
|
||||||
|
self.assertIn("как открыть общество с ограниченной ответственностью", query_variants("ru", "как открыть ОсОО"))
|
||||||
|
self.assertIn("регистрация осоо", [variant.casefold() for variant in query_variants("ru", "регистрация общества с ограниченной ответственностью")])
|
||||||
|
self.assertIn("жчк ачуу тартиби", [variant.casefold() for variant in query_variants("ky", "жоопкерчилиги чектелген коом ачуу тартиби")])
|
||||||
|
|
||||||
def test_company_registration_intent_boosts_current_documents(self):
|
def test_company_registration_intent_boosts_current_documents(self):
|
||||||
for language, query, status in (
|
for language, query, status in (
|
||||||
@@ -45,13 +53,15 @@ class SearchRelevanceTest(unittest.TestCase):
|
|||||||
("ky", "ЖЧК ачуу тартиби", "Күчүндө"),
|
("ky", "ЖЧК ачуу тартиби", "Күчүндө"),
|
||||||
):
|
):
|
||||||
body = json.loads(build_search_body(language, query, 10))
|
body = json.loads(build_search_body(language, query, 10))
|
||||||
search_query = body["query"]["bool"]
|
search_query = body["query"]["bool"]["must"]["bool"]
|
||||||
self.assertEqual(search_query["minimum_should_match"], 1)
|
self.assertEqual(search_query["minimum_should_match"], 1)
|
||||||
boosts = [clause["constant_score"] for clause in search_query["should"][1:]]
|
self.assertNotIn("filter", search_query)
|
||||||
self.assertEqual([item["boost"] for item in boosts], [2000, 1000])
|
self.assertTrue(all(clause.get("multi_match", {}).get("operator") == "OR" for clause in search_query["should"] if "multi_match" in clause))
|
||||||
|
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(
|
self.assertEqual(
|
||||||
[item["filter"]["bool"]["filter"][0]["term"]["document_code"] for item in boosts],
|
[item["filter"]["bool"]["filter"][0]["term"]["document_code"] for item in boosts],
|
||||||
["230044970", "667"],
|
[code for code, _ in COMPANY_REGISTRATION_DOCUMENTS],
|
||||||
)
|
)
|
||||||
self.assertTrue(all(item["filter"]["bool"]["filter"][1] == {"term": {f"status_{language}": status}} for item in boosts))
|
self.assertTrue(all(item["filter"]["bool"]["filter"][1] == {"term": {f"status_{language}": status}} for item in boosts))
|
||||||
|
|
||||||
@@ -60,6 +70,8 @@ class SearchRelevanceTest(unittest.TestCase):
|
|||||||
("ru", "ОсОО зарегистрирован?"),
|
("ru", "ОсОО зарегистрирован?"),
|
||||||
("ru", "кто зарегистрировал ОсОО"),
|
("ru", "кто зарегистрировал ОсОО"),
|
||||||
("ru", "как открыть счет ОсОО"),
|
("ru", "как открыть счет ОсОО"),
|
||||||
|
("ru", "как открыть ОсОО банковский счет"),
|
||||||
|
("ru", "как открыть ОсОО банковский счёт"),
|
||||||
("ru", "как открыть филиал ОсОО"),
|
("ru", "как открыть филиал ОсОО"),
|
||||||
("ru", "как создать договор для ОсОО"),
|
("ru", "как создать договор для ОсОО"),
|
||||||
("ru", "порядок создания логотипа ОсОО"),
|
("ru", "порядок создания логотипа ОсОО"),
|
||||||
@@ -69,7 +81,8 @@ class SearchRelevanceTest(unittest.TestCase):
|
|||||||
("ky", "ЖЧК кантип логотип түзүү"),
|
("ky", "ЖЧК кантип логотип түзүү"),
|
||||||
):
|
):
|
||||||
body = json.loads(build_search_body(language, query, 10))
|
body = json.loads(build_search_body(language, query, 10))
|
||||||
self.assertNotIn("should", body["query"]["bool"])
|
clauses = body["query"]["bool"]["must"]["bool"]["should"]
|
||||||
|
self.assertFalse(any("constant_score" in clause for clause in clauses))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ class SearchReviewTest(unittest.TestCase):
|
|||||||
page = api.review_page()
|
page = api.review_page()
|
||||||
self.assertIn("Оценка поисковой выдачи", page)
|
self.assertIn("Оценка поисковой выдачи", page)
|
||||||
self.assertIn("akyldash-search-review-draft", page)
|
self.assertIn("akyldash-search-review-draft", page)
|
||||||
|
self.assertIn("tutorial-dialog", page)
|
||||||
|
self.assertIn("Как это работает", page)
|
||||||
|
self.assertNotIn('id="error"', page)
|
||||||
self.assertIn("localStorage", page)
|
self.assertIn("localStorage", page)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -39,4 +39,4 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Акылдаш · Telegram-бот v0.2.2 · Backend v0.9.0 · Frontend — не создан
|
Акылдаш · Telegram-бот v0.2.2 · Backend v0.10.1 · Frontend — не создан
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
- Telegram-бот: `0.2.2`
|
- Telegram-бот: `0.2.2`
|
||||||
- Telegram-бот на Synology: `0.2.1`
|
- Telegram-бот на Synology: `0.2.1`
|
||||||
- Backend: `0.9.0`
|
- Backend: `0.10.1`
|
||||||
- Frontend: не создан
|
- Frontend: не создан
|
||||||
|
|
||||||
## Краткий итог
|
## Краткий итог
|
||||||
@@ -262,4 +262,4 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Акылдаш · Telegram-бот v0.2.2 · Backend v0.9.0 · Frontend — не создан
|
Акылдаш · Telegram-бот v0.2.2 · Backend v0.10.1 · Frontend — не создан
|
||||||
|
|||||||
Reference in New Issue
Block a user