если объект найден по реквизиту (колонка в FTS), в выдаче показывается только совпавший реквизит с типом значения — в том же формате дерева
This commit is contained in:
@@ -5,6 +5,12 @@
|
|||||||
Формат: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
Формат: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
Версионирование: [Semantic Versioning](https://semver.org/lang/ru/).
|
Версионирование: [Semantic Versioning](https://semver.org/lang/ru/).
|
||||||
|
|
||||||
|
## [0.3.3] - 2026-07-17
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **`query_1c.py search`**: если объект найден по реквизиту (колонка `fields_text` в FTS), в выдаче показывается только совпавший реквизит с типом значения — в том же формате дерева, что и `object --clear`.
|
||||||
|
|
||||||
## [0.3.2] - 2026-07-17
|
## [0.3.2] - 2026-07-17
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Индекс метаданных и модулей конфигураций 1С
|
# Индекс метаданных и модулей конфигураций 1С
|
||||||
|
|
||||||
**Версия:** см. [`VERSION`](VERSION) (текущая: **0.3.2**) · [CHANGELOG](CHANGELOG.md)
|
**Версия:** см. [`VERSION`](VERSION) (текущая: **0.3.3**) · [CHANGELOG](CHANGELOG.md)
|
||||||
**Автор:** Michael BAG · [mk@p7net.ru](mailto:mk@p7net.ru)
|
**Автор:** Michael BAG · [mk@p7net.ru](mailto:mk@p7net.ru)
|
||||||
**Лицензия:** [GNU GPL v3](LICENSE) · [русский перевод (справочно)](LICENSE.ru)
|
**Лицензия:** [GNU GPL v3](LICENSE) · [русский перевод (справочно)](LICENSE.ru)
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ python index_1c.py list
|
|||||||
|
|
||||||
| Команда | Аргумент | Результат | Полезные опции |
|
| Команда | Аргумент | Результат | Полезные опции |
|
||||||
|---------|----------|-----------|----------------|
|
|---------|----------|-----------|----------------|
|
||||||
| `search` | строка | объекты метаданных (FTS) | `-b`, `--type`, `--limit` |
|
| `search` | строка | объекты метаданных (FTS); при совпадении по реквизиту — дерево найденного поля | `-b`, `--type`, `--limit` |
|
||||||
| `object` | имя/синоним объекта **или** реквизита | дерево структуры | `-b`, `--type`, `--clear`, `--limit` |
|
| `object` | имя/синоним объекта **или** реквизита | дерево структуры | `-b`, `--type`, `--clear`, `--limit` |
|
||||||
| `show` | имя объекта | полная карточка JSON | `-b` |
|
| `show` | имя объекта | полная карточка JSON | `-b` |
|
||||||
| `refs` | `Catalog.Customers` / `Contract` | обратные ссылки полей | `-b`, `--limit` (на каждую baseconf) |
|
| `refs` | `Catalog.Customers` / `Contract` | обратные ссылки полей | `-b`, `--limit` (на каждую baseconf) |
|
||||||
|
|||||||
+205
-24
@@ -3,12 +3,15 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .parse_meta import FieldRec, ObjectRec, parse_metadata_xml
|
from .parse_meta import FieldRec, ObjectRec, parse_metadata_xml
|
||||||
|
|
||||||
|
_QUERY_TOKEN_RE = re.compile(r"[\wА-Яа-яЁё]+", flags=re.UNICODE)
|
||||||
|
|
||||||
|
|
||||||
FILL_LABEL = {
|
FILL_LABEL = {
|
||||||
"ShowError": "обяз.",
|
"ShowError": "обяз.",
|
||||||
@@ -43,6 +46,174 @@ def _field_matches(f: dict[str, Any], q: str, qn: str) -> bool:
|
|||||||
return name_cf == qn or syn_cf == qn or qn in syn_parts
|
return name_cf == qn or syn_cf == qn or qn in syn_parts
|
||||||
|
|
||||||
|
|
||||||
|
def _query_tokens(query: str) -> list[str]:
|
||||||
|
return _QUERY_TOKEN_RE.findall(query or "")
|
||||||
|
|
||||||
|
|
||||||
|
def _words_cf(text: str) -> list[str]:
|
||||||
|
return [w.casefold() for w in _QUERY_TOKEN_RE.findall(text or "")]
|
||||||
|
|
||||||
|
|
||||||
|
def _tokens_match_text(tokens: list[str], text: str) -> bool:
|
||||||
|
"""Как FTS search: каждый токен — префикс какого-либо слова в тексте."""
|
||||||
|
if not tokens:
|
||||||
|
return False
|
||||||
|
words = _words_cf(text)
|
||||||
|
if not words:
|
||||||
|
return False
|
||||||
|
for tok in tokens:
|
||||||
|
t = tok.casefold()
|
||||||
|
if not any(w.startswith(t) for w in words):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def object_header_matches_search(row: dict[str, Any], query: str) -> bool:
|
||||||
|
"""Совпадение по имени/синониму/комментарию объекта (не по реквизитам)."""
|
||||||
|
tokens = _query_tokens(query)
|
||||||
|
if not tokens:
|
||||||
|
return False
|
||||||
|
for part in (
|
||||||
|
row.get("name") or "",
|
||||||
|
row.get("full_name") or "",
|
||||||
|
row.get("synonym") or "",
|
||||||
|
row.get("comment") or "",
|
||||||
|
):
|
||||||
|
if _tokens_match_text(tokens, part):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def field_matches_search_query(f: dict[str, Any], query: str) -> bool:
|
||||||
|
"""Совпадение реквизита с поисковым запросом (префикс по словам, как FTS)."""
|
||||||
|
tokens = _query_tokens(query)
|
||||||
|
if not tokens:
|
||||||
|
return False
|
||||||
|
text = " ".join(
|
||||||
|
filter(
|
||||||
|
None,
|
||||||
|
[
|
||||||
|
f.get("kind") or "",
|
||||||
|
f.get("name") or "",
|
||||||
|
f.get("synonym") or "",
|
||||||
|
f.get("comment") or "",
|
||||||
|
f.get("tabular_section") or "",
|
||||||
|
" ".join(f.get("types") or []),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return _tokens_match_text(tokens, text)
|
||||||
|
|
||||||
|
|
||||||
|
def _field_dict_from_db_row(row: sqlite3.Row) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"kind": row["kind"],
|
||||||
|
"name": row["name"],
|
||||||
|
"synonym": row["synonym"] or "",
|
||||||
|
"comment": row["comment"] or "" if "comment" in row.keys() else "",
|
||||||
|
"types": [t.strip() for t in (row["types"] or "").split(",") if t.strip()],
|
||||||
|
"tabular_section": row["tabular_section"] or "",
|
||||||
|
"refs": json.loads(row["refs_json"] or "[]"),
|
||||||
|
"fill_checking": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def find_fields_for_search_query(
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
object_id: int,
|
||||||
|
query: str,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Реквизиты объекта, совпавшие с FTS-запросом search."""
|
||||||
|
rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT kind, name, synonym, comment, types, tabular_section, refs_json
|
||||||
|
FROM fields
|
||||||
|
WHERE object_id = ?
|
||||||
|
ORDER BY tabular_section, kind, name
|
||||||
|
""",
|
||||||
|
(object_id,),
|
||||||
|
)
|
||||||
|
matched: list[dict[str, Any]] = []
|
||||||
|
seen: set[tuple[str, str, str]] = set()
|
||||||
|
for row in rows:
|
||||||
|
fdict = _field_dict_from_db_row(row)
|
||||||
|
if not field_matches_search_query(fdict, query):
|
||||||
|
continue
|
||||||
|
sig = (fdict["tabular_section"], fdict["name"], fdict["kind"])
|
||||||
|
if sig in seen:
|
||||||
|
continue
|
||||||
|
seen.add(sig)
|
||||||
|
matched.append(fdict)
|
||||||
|
return matched
|
||||||
|
|
||||||
|
|
||||||
|
def enrich_matched_fields(obj: dict[str, Any], matched: list[dict[str, Any]]) -> None:
|
||||||
|
"""Подтянуть FillChecking и типы из актуальной структуры объекта."""
|
||||||
|
if not matched or not obj.get("fields"):
|
||||||
|
return
|
||||||
|
by_f = {
|
||||||
|
(f.get("tabular_section") or "", f.get("name") or "", f.get("kind") or ""): f
|
||||||
|
for f in obj["fields"]
|
||||||
|
}
|
||||||
|
for mf in matched:
|
||||||
|
key = (mf.get("tabular_section") or "", mf.get("name") or "", mf.get("kind") or "")
|
||||||
|
src = by_f.get(key)
|
||||||
|
if not src:
|
||||||
|
continue
|
||||||
|
mf["fill_checking"] = src.get("fill_checking") or ""
|
||||||
|
mf["types"] = src.get("types") or mf.get("types")
|
||||||
|
mf["synonym"] = src.get("synonym") or mf.get("synonym")
|
||||||
|
mf["refs"] = src.get("refs") or mf.get("refs")
|
||||||
|
|
||||||
|
|
||||||
|
def load_object_structure_for_row(
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
row: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
match = {
|
||||||
|
"config": row["config"],
|
||||||
|
"full_name": row["full_name"],
|
||||||
|
"path": row.get("path"),
|
||||||
|
"object_type": row.get("object_type"),
|
||||||
|
}
|
||||||
|
if row.get("json"):
|
||||||
|
match["json"] = row["json"]
|
||||||
|
elif row.get("object_id"):
|
||||||
|
jrow = conn.execute(
|
||||||
|
"SELECT json FROM objects WHERE id=?", (row["object_id"],)
|
||||||
|
).fetchone()
|
||||||
|
match["json"] = jrow["json"] if jrow else "{}"
|
||||||
|
else:
|
||||||
|
jrow = conn.execute(
|
||||||
|
"SELECT json FROM objects WHERE config=? AND full_name=?",
|
||||||
|
(row["config"], row["full_name"]),
|
||||||
|
).fetchone()
|
||||||
|
match["json"] = jrow["json"] if jrow else "{}"
|
||||||
|
return load_object_structure(match)
|
||||||
|
|
||||||
|
|
||||||
|
def annotate_search_hits(
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
rows: list[dict[str, Any]],
|
||||||
|
query: str,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Уточнить search: object — по шапке; field — по реквизитам в fields_text."""
|
||||||
|
for row in rows:
|
||||||
|
if object_header_matches_search(row, query):
|
||||||
|
row["match_kind"] = "object"
|
||||||
|
row["matched_fields"] = []
|
||||||
|
continue
|
||||||
|
oid = row.get("object_id")
|
||||||
|
matched = find_fields_for_search_query(conn, int(oid), query) if oid else []
|
||||||
|
if matched:
|
||||||
|
row["match_kind"] = "field"
|
||||||
|
row["matched_fields"] = matched
|
||||||
|
else:
|
||||||
|
row["match_kind"] = "object"
|
||||||
|
row["matched_fields"] = []
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
def find_structure_matches(
|
def find_structure_matches(
|
||||||
conn: sqlite3.Connection,
|
conn: sqlite3.Connection,
|
||||||
query: str,
|
query: str,
|
||||||
@@ -239,12 +410,43 @@ def format_field_line(
|
|||||||
return f"{indent}{branch}`{kind}.{name}`{syn_s} [{fill}] : {types_s}"
|
return f"{indent}{branch}`{kind}.{name}`{syn_s} [{fill}] : {types_s}"
|
||||||
|
|
||||||
|
|
||||||
|
def _select_matched_field_nodes(
|
||||||
|
fields: list[dict[str, Any]],
|
||||||
|
matched_fields: list[dict[str, Any]],
|
||||||
|
) -> tuple[list[dict[str, Any]], set[tuple[str, str, str]], set[str]]:
|
||||||
|
want: set[tuple[str, str, str]] = set()
|
||||||
|
need_ts: set[str] = set()
|
||||||
|
for mf in matched_fields:
|
||||||
|
want.add(
|
||||||
|
(
|
||||||
|
mf.get("kind") or "",
|
||||||
|
mf.get("tabular_section") or "",
|
||||||
|
mf.get("name") or "",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if mf.get("tabular_section") and mf.get("kind") == "Column":
|
||||||
|
need_ts.add(mf["tabular_section"])
|
||||||
|
selected: list[dict[str, Any]] = []
|
||||||
|
for f in fields:
|
||||||
|
key = (
|
||||||
|
f.get("kind") or "",
|
||||||
|
f.get("tabular_section") or "",
|
||||||
|
f.get("name") or "",
|
||||||
|
)
|
||||||
|
if key in want:
|
||||||
|
selected.append(f)
|
||||||
|
elif f.get("kind") == "TabularSection" and f.get("name") in need_ts:
|
||||||
|
selected.append(f)
|
||||||
|
return selected, want, need_ts
|
||||||
|
|
||||||
|
|
||||||
def format_object_tree(
|
def format_object_tree(
|
||||||
obj: dict[str, Any],
|
obj: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
clear: bool = False,
|
clear: bool = False,
|
||||||
matched_fields: list[dict[str, Any]] | None = None,
|
matched_fields: list[dict[str, Any]] | None = None,
|
||||||
match_kind: str = "object",
|
match_kind: str = "object",
|
||||||
|
omit_header: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Дерево реквизитов объекта. clear — только совпадения."""
|
"""Дерево реквизитов объекта. clear — только совпадения."""
|
||||||
lines: list[str] = []
|
lines: list[str] = []
|
||||||
@@ -254,35 +456,14 @@ def format_object_tree(
|
|||||||
header = f"`{obj.get('full_name') or obj.get('object_type') + '.' + obj.get('name')}`{syn_s}"
|
header = f"`{obj.get('full_name') or obj.get('object_type') + '.' + obj.get('name')}`{syn_s}"
|
||||||
if cfg:
|
if cfg:
|
||||||
header = f"[{cfg}] {header}"
|
header = f"[{cfg}] {header}"
|
||||||
lines.append(header)
|
if not omit_header:
|
||||||
|
lines.append(header)
|
||||||
|
|
||||||
fields: list[dict[str, Any]] = list(obj.get("fields") or [])
|
fields: list[dict[str, Any]] = list(obj.get("fields") or [])
|
||||||
|
|
||||||
if clear:
|
if clear:
|
||||||
if match_kind == "field" and matched_fields:
|
if match_kind == "field" and matched_fields:
|
||||||
want: set[tuple[str, str, str]] = set()
|
selected, want, need_ts = _select_matched_field_nodes(fields, matched_fields)
|
||||||
need_ts: set[str] = set()
|
|
||||||
for mf in matched_fields:
|
|
||||||
want.add(
|
|
||||||
(
|
|
||||||
mf.get("kind") or "",
|
|
||||||
mf.get("tabular_section") or "",
|
|
||||||
mf.get("name") or "",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if mf.get("tabular_section") and mf.get("kind") == "Column":
|
|
||||||
need_ts.add(mf["tabular_section"])
|
|
||||||
selected: list[dict[str, Any]] = []
|
|
||||||
for f in fields:
|
|
||||||
key = (
|
|
||||||
f.get("kind") or "",
|
|
||||||
f.get("tabular_section") or "",
|
|
||||||
f.get("name") or "",
|
|
||||||
)
|
|
||||||
if key in want:
|
|
||||||
selected.append(f)
|
|
||||||
elif f.get("kind") == "TabularSection" and f.get("name") in need_ts:
|
|
||||||
selected.append(f)
|
|
||||||
if not selected:
|
if not selected:
|
||||||
lines.append(" (совпавшие реквизиты не найдены в структуре)")
|
lines.append(" (совпавшие реквизиты не найдены в структуре)")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|||||||
+7
-3
@@ -10,6 +10,7 @@ from typing import Any
|
|||||||
|
|
||||||
from . import db as dbmod
|
from . import db as dbmod
|
||||||
from .config import cache_dir
|
from .config import cache_dir
|
||||||
|
from .object_view import annotate_search_hits
|
||||||
|
|
||||||
|
|
||||||
def _fts_query(raw: str) -> str:
|
def _fts_query(raw: str) -> str:
|
||||||
@@ -266,7 +267,8 @@ def search_objects(
|
|||||||
limit: int = 30,
|
limit: int = 30,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
exact_sql = """
|
exact_sql = """
|
||||||
SELECT config, full_name, object_type, name, synonym, comment, path, '' AS snip, -100.0 AS score
|
SELECT config, full_name, object_type, name, synonym, comment, path,
|
||||||
|
id AS object_id, '' AS snip, -100.0 AS score
|
||||||
FROM objects
|
FROM objects
|
||||||
WHERE (name = ? OR full_name = ? OR full_name LIKE ?)
|
WHERE (name = ? OR full_name = ? OR full_name LIKE ?)
|
||||||
"""
|
"""
|
||||||
@@ -286,6 +288,7 @@ def search_objects(
|
|||||||
fetch_n = max(limit * 5, 50)
|
fetch_n = max(limit * 5, 50)
|
||||||
sql = """
|
sql = """
|
||||||
SELECT o.config, o.full_name, o.object_type, o.name, o.synonym, o.comment, o.path,
|
SELECT o.config, o.full_name, o.object_type, o.name, o.synonym, o.comment, o.path,
|
||||||
|
o.id AS object_id,
|
||||||
snippet(objects_fts, 0, '[', ']', '…', 12) AS snip,
|
snippet(objects_fts, 0, '[', ']', '…', 12) AS snip,
|
||||||
bm25(objects_fts, 10.0, 5.0, 2.0, 1.0) AS score
|
bm25(objects_fts, 10.0, 5.0, 2.0, 1.0) AS score
|
||||||
FROM objects_fts
|
FROM objects_fts
|
||||||
@@ -307,7 +310,8 @@ def search_objects(
|
|||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
like = f"%{query}%"
|
like = f"%{query}%"
|
||||||
sql2 = """
|
sql2 = """
|
||||||
SELECT config, full_name, object_type, name, synonym, comment, path, '' AS snip, 0 AS score
|
SELECT config, full_name, object_type, name, synonym, comment, path,
|
||||||
|
id AS object_id, '' AS snip, 0 AS score
|
||||||
FROM objects
|
FROM objects
|
||||||
WHERE full_name LIKE ? OR synonym LIKE ? OR comment LIKE ? OR name LIKE ?
|
WHERE full_name LIKE ? OR synonym LIKE ? OR comment LIKE ? OR name LIKE ?
|
||||||
"""
|
"""
|
||||||
@@ -343,7 +347,7 @@ def search_objects(
|
|||||||
merged.sort(
|
merged.sort(
|
||||||
key=lambda x: (x.get("_boost", 9), x.get("score") if x.get("score") is not None else 0)
|
key=lambda x: (x.get("_boost", 9), x.get("score") if x.get("score") is not None else 0)
|
||||||
)
|
)
|
||||||
return merged[:limit]
|
return annotate_search_hits(conn, merged[:limit], query)
|
||||||
|
|
||||||
|
|
||||||
def search_modules(
|
def search_modules(
|
||||||
|
|||||||
+18
-23
@@ -54,9 +54,11 @@ from index1c.search import ( # noqa: E402
|
|||||||
search_objects,
|
search_objects,
|
||||||
)
|
)
|
||||||
from index1c.object_view import ( # noqa: E402
|
from index1c.object_view import ( # noqa: E402
|
||||||
|
enrich_matched_fields,
|
||||||
find_structure_matches,
|
find_structure_matches,
|
||||||
format_object_tree,
|
format_object_tree,
|
||||||
load_object_structure,
|
load_object_structure,
|
||||||
|
load_object_structure_for_row,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -91,7 +93,21 @@ def cmd_search(args: argparse.Namespace) -> int:
|
|||||||
for r in rows:
|
for r in rows:
|
||||||
syn = f" — {r['synonym']}" if r.get("synonym") else ""
|
syn = f" — {r['synonym']}" if r.get("synonym") else ""
|
||||||
print(f"- [{r['config']}] `{r['full_name']}`{syn}")
|
print(f"- [{r['config']}] `{r['full_name']}`{syn}")
|
||||||
if r.get("snip"):
|
matched = list(r.get("matched_fields") or [])
|
||||||
|
if r.get("match_kind") == "field" and matched:
|
||||||
|
obj = load_object_structure_for_row(conn, r)
|
||||||
|
enrich_matched_fields(obj, matched)
|
||||||
|
block = format_object_tree(
|
||||||
|
obj,
|
||||||
|
clear=True,
|
||||||
|
matched_fields=matched,
|
||||||
|
match_kind="field",
|
||||||
|
omit_header=True,
|
||||||
|
)
|
||||||
|
for line in block.splitlines():
|
||||||
|
if line:
|
||||||
|
print(f" {line}")
|
||||||
|
elif r.get("snip"):
|
||||||
print(f" {r['snip']}")
|
print(f" {r['snip']}")
|
||||||
return 0
|
return 0
|
||||||
finally:
|
finally:
|
||||||
@@ -283,29 +299,8 @@ def cmd_object(args: argparse.Namespace) -> int:
|
|||||||
if i:
|
if i:
|
||||||
print()
|
print()
|
||||||
obj = load_object_structure(m)
|
obj = load_object_structure(m)
|
||||||
# подтянуть FillChecking/типы из свежего XML в matched_fields
|
|
||||||
matched = list(m.get("matched_fields") or [])
|
matched = list(m.get("matched_fields") or [])
|
||||||
if matched and obj.get("fields"):
|
enrich_matched_fields(obj, matched)
|
||||||
by_f = {
|
|
||||||
(
|
|
||||||
f.get("tabular_section") or "",
|
|
||||||
f.get("name") or "",
|
|
||||||
f.get("kind") or "",
|
|
||||||
): f
|
|
||||||
for f in obj["fields"]
|
|
||||||
}
|
|
||||||
for mf in matched:
|
|
||||||
key = (
|
|
||||||
mf.get("tabular_section") or "",
|
|
||||||
mf.get("name") or "",
|
|
||||||
mf.get("kind") or "",
|
|
||||||
)
|
|
||||||
src = by_f.get(key)
|
|
||||||
if src:
|
|
||||||
mf["fill_checking"] = src.get("fill_checking") or ""
|
|
||||||
mf["types"] = src.get("types") or mf.get("types")
|
|
||||||
mf["synonym"] = src.get("synonym") or mf.get("synonym")
|
|
||||||
mf["refs"] = src.get("refs") or mf.get("refs")
|
|
||||||
# точное совпадение объекта (score 0) → clear без дерева;
|
# точное совпадение объекта (score 0) → clear без дерева;
|
||||||
# иначе при найденных реквизитах — clear только по ним
|
# иначе при найденных реквизитах — clear только по ним
|
||||||
kind = m.get("match_kind") or "object"
|
kind = m.get("match_kind") or "object"
|
||||||
|
|||||||
Reference in New Issue
Block a user