feat: improve legal search intent and aliases
This commit is contained in:
@@ -12,11 +12,12 @@ from pathlib import Path
|
||||
|
||||
from search.minjust_opensearch import APP_VERSION, request_json
|
||||
from search.catalog import CATALOGS, labels
|
||||
from search.query import build_search_query
|
||||
from search.reviews import ReviewSnapshots, ReviewStore
|
||||
|
||||
|
||||
API_VERSION = "v1"
|
||||
SEARCH_ALGORITHM_VERSION = "search-1"
|
||||
SEARCH_ALGORITHM_VERSION = "search-2"
|
||||
LANGUAGES = {"ru", "ky"}
|
||||
CODE = re.compile(r"^[0-9]+$")
|
||||
MAX_PAGE_SIZE = 100
|
||||
@@ -139,7 +140,7 @@ class Api:
|
||||
"from": (page - 1) * page_size,
|
||||
"size": page_size + 1,
|
||||
"_source": ["document_code", "edition_code", "document_name_ru", "document_name_ky", "document_type_ru", "document_type_ky", "status_ru", "status_ky", "legal_force_code", "date_adopted", "number"],
|
||||
"query": {"bool": {"filter": filters, "must": {"multi_match": {"query": text, "fields": [f"document_name_{language}", f"text_{language}"], "type": "cross_fields"}}}},
|
||||
"query": {"bool": {"filter": filters, "must": build_search_query(language, text)}},
|
||||
"collapse": {"field": "document_code"},
|
||||
"highlight": {"fields": {f"text_{language}": {"number_of_fragments": 1}}},
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ from typing import Iterator
|
||||
|
||||
from search.catalog import authority_codes, legal_force_code, source_code
|
||||
|
||||
APP_VERSION = "0.9.4"
|
||||
APP_VERSION = "0.10.0"
|
||||
LANGUAGES = {"ru", "ky"}
|
||||
DEFAULT_MAPPING = Path(__file__).with_name("minjust-fragments-index.json")
|
||||
|
||||
|
||||
@@ -10,12 +10,52 @@ import urllib.parse
|
||||
from search.minjust_opensearch import APP_VERSION, request_json
|
||||
|
||||
|
||||
LEGAL_FORM_ALIASES = {
|
||||
"ru": {
|
||||
"осоо": ("ооо", "общество с ограниченной ответственностью"),
|
||||
"ооо": ("осоо", "общество с ограниченной ответственностью"),
|
||||
"общество с ограниченной ответственностью": ("осоо", "ооо"),
|
||||
"общества с ограниченной ответственностью": ("осоо", "ооо"),
|
||||
},
|
||||
"ky": {
|
||||
"жчк": ("жоопкерчилиги чектелген коом",),
|
||||
"жоопкерчилиги чектелген коом": ("жчк",),
|
||||
},
|
||||
}
|
||||
COMPANY_REGISTRATION_DOCUMENTS = ("667", "4")
|
||||
|
||||
|
||||
def query_variants(language: str, query: str) -> list[str]:
|
||||
variants = [query]
|
||||
for alias, expansions in LEGAL_FORM_ALIASES[language].items():
|
||||
pattern = re.compile(rf"(?<!\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]:
|
||||
patterns = {
|
||||
"ru": (r"\bкак\s+откры\w*\s+осоо\b", r"\b(порядок|процедура)\s+откры\w*\s+осоо\b", r"\bкак\s+зарегистр\w*\s+осоо\b"),
|
||||
"ky": (r"\bжчк\s+ач\w*\s+тартиби\b", r"\bжчк\s+кантип\s+ач\w*\b"),
|
||||
forms, patterns = {
|
||||
"ru": (
|
||||
r"(?:осоо|ооо|обществ\w*\s+с\s+ограниченной\s+ответственностью)",
|
||||
(
|
||||
r"\bкак\s+(?:откры\w*|созда\w*|зарегистр\w*)\s+{form}\b",
|
||||
r"\b(?:порядок|процедура)\s+(?:открыти\w*|создани\w*|регистраци\w*)\s+{form}\b",
|
||||
r"\b(?:государственн\w+\s+)?регистраци\w*\s+{form}\b",
|
||||
r"\bсоздани\w*\s+{form}\b",
|
||||
),
|
||||
),
|
||||
"ky": (
|
||||
r"(?:жчк|жоопкерчилиги\s+чектелген\s+коом)",
|
||||
(
|
||||
r"\bжчк\s+ач\w*\s+тартиби\b",
|
||||
r"\bжчк\s+кантип\s+ач\w*\b",
|
||||
r"\b{form}\s+(?:ачуу|ачуунун|каттоо|каттоонун)\b",
|
||||
r"\b(?:ачуу|каттоо)\s+{form}\b",
|
||||
),
|
||||
),
|
||||
}[language]
|
||||
if not any(re.search(pattern, query.casefold()) for pattern in patterns):
|
||||
if not any(re.search(pattern.format(form=forms), query.casefold()) for pattern in patterns):
|
||||
return []
|
||||
|
||||
status = {"ru": "Действует", "ky": "Күчүндө"}[language]
|
||||
@@ -35,28 +75,34 @@ def company_registration_clauses(language: str, query: str) -> list[dict]:
|
||||
}
|
||||
}
|
||||
|
||||
return [clause("230044970", 2000), clause("667", 1000)]
|
||||
return [clause(COMPANY_REGISTRATION_DOCUMENTS[0], 2000), clause(COMPANY_REGISTRATION_DOCUMENTS[1], 1000)]
|
||||
|
||||
|
||||
def build_search_query(language: str, query: str) -> dict:
|
||||
intent_clauses = company_registration_clauses(language, query)
|
||||
full_text = [
|
||||
{
|
||||
"multi_match": {
|
||||
"query": variant,
|
||||
"fields": [f"document_name_{language}^4", f"text_{language}"],
|
||||
"type": "cross_fields",
|
||||
"operator": "OR" if intent_clauses else "AND",
|
||||
}
|
||||
}
|
||||
for variant in query_variants(language, query)
|
||||
]
|
||||
planned = {"should": [*full_text, *intent_clauses], "minimum_should_match": 1}
|
||||
if intent_clauses:
|
||||
planned["filter"] = [{"terms": {"document_code": list(COMPANY_REGISTRATION_DOCUMENTS)}}]
|
||||
return {"bool": planned}
|
||||
|
||||
|
||||
def build_search_body(language: str, query: str, top_k: int) -> bytes:
|
||||
full_text = {
|
||||
"multi_match": {
|
||||
"query": query,
|
||||
"fields": [f"document_name_{language}", f"text_{language}"],
|
||||
"type": "cross_fields",
|
||||
}
|
||||
}
|
||||
clauses = company_registration_clauses(language, query)
|
||||
bool_query = {"filter": {"term": {"language": language}}}
|
||||
if clauses:
|
||||
bool_query.update({"should": [full_text, *clauses], "minimum_should_match": 1})
|
||||
else:
|
||||
bool_query["must"] = full_text
|
||||
return json.dumps({
|
||||
"size": top_k,
|
||||
"track_total_hits": False,
|
||||
"_source": ["document_code"],
|
||||
"query": {"bool": bool_query},
|
||||
"query": {"bool": {"filter": [{"term": {"language": language}}, {"term": {"is_current_edition": True}}], "must": build_search_query(language, query)}},
|
||||
"collapse": {"field": "document_code"},
|
||||
}, ensure_ascii=False).encode()
|
||||
|
||||
|
||||
@@ -119,7 +119,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.9.4 · Внутренняя лаборатория релевантности</footer>
|
||||
<footer class="review__header">Акылдаш · Backend v0.10.0 · Внутренняя лаборатория релевантности</footer>
|
||||
<script>
|
||||
const DRAFT_KEY = 'akyldash-search-review-draft';
|
||||
const TOUR_KEY = 'akyldash-search-review-tour-seen';
|
||||
|
||||
Reference in New Issue
Block a user