Нормализовать переводы строк в LF

Решение по TD-STYLE-ANALYZERS: LF — инструменты проекта (Python/Node) пишут LF,
CRLF-.sh не работают на Linux CI (sh scripts/ci.sh), большинство файлов уже были
LF. Добавлен .gitattributes (* text=auto eol=lf, бинарные исключения),
.editorconfig переведён на lf, 1029 файлов конвертированы, git add --renormalize.
Из индекса убраны закравшиеся archive/**/__pycache__/*.pyc.
This commit is contained in:
Rustam Khalimov
2026-09-11 19:01:42 +03:00
parent 39c9bdc1b7
commit 713d554dc2
751 changed files with 94507 additions and 94486 deletions
@@ -1,103 +1,103 @@
"""Полнотекстовый поиск DuckDB FTS (по решению — включаем).
DuckDB FTS строит виртуальную таблицу-снимок: индекс пересоздаётся при
старте, раз в сутки по расписанию и вручную (POST /api/admin/fts/rebuild).
Чтобы свежесозданные карточки искались сразу, поиск совмещает FTS с
точечным LIKE-дополнением (см. services/leads.search).
Рабочий синтаксис DuckDB >= 1.0: только PRAGMA create_fts_index
(вариант CREATE VIRTUAL TABLE ... USING fts(...) в этой версии падает).
Поиск — через табличную функцию fts_main_<table>.match_bm25(rowid, query).
"""
from __future__ import annotations
import logging
from ..db import store
log = logging.getLogger("leadradar.fts")
OK_KEY = "ftsOk"
# Источник, колонка-rowid, текстовые колонки (должны совпадать со схемой db.py)
_FTS_TARGETS = [
("leads", ("title", "summary", "source_msg", "contact")),
("messages", ("text",)),
("rejected_msgs", ("text",)),
]
def is_ready() -> bool:
return bool(store.get_setting(OK_KEY))
def _set(ok: bool) -> None:
store.set_setting(OK_KEY, ok)
def install_extension() -> bool:
try:
store.execute("INSTALL fts")
store.execute("LOAD fts")
return True
except Exception as exc: # noqa: BLE001
log.warning("FTS extension unavailable: %s", exc)
return False
def rebuild() -> bool:
"""Пересоздаёт FTS-индексы по leads и messages (overwrite поверх старого).
PRAGMA не поддерживает prepared-параметры, поэтому имена подставляются
напрямую — это внутренние константы из _FTS_TARGETS, не пользовательский ввод.
"""
try:
for table, cols in _FTS_TARGETS:
col_list = ", ".join(f"'{c}'" for c in cols)
store.execute(
f"PRAGMA create_fts_index('{table}', 'id', {col_list}, "
"stemmer='russian', overwrite=1)"
)
_set(True)
log.info("FTS rebuild done")
return True
except Exception as exc: # noqa: BLE001
log.warning("FTS rebuild failed: %s", exc)
_set(False)
return False
def search(query: str, limit: int = 50) -> dict:
"""Возвращает {'leads': [ids], 'messages': [ids], 'rejected': [ids]} по FTS
(пусто при сбое)."""
out: dict = {"leads": [], "messages": [], "rejected": []}
if not is_ready():
return out
q = _sanitize(query)
if not q:
return out
targets = ("leads", "messages", "rejected_msgs")
keys = {"leads": "leads", "messages": "messages", "rejected_msgs": "rejected"}
for table in targets:
try:
# fts_main_<table> создаётся PRAGMA create_fts_index над таблицей
fn = f"fts_main_{table}.match_bm25(id, ?)"
rows = store.query(
f"SELECT id, {fn} AS _s FROM {table} "
f"WHERE {fn} IS NOT NULL ORDER BY _s LIMIT ?",
[q, q, limit * 2],
)
out[keys[table]] = [r["id"] for r in rows]
except Exception as exc: # noqa: BLE001
log.warning("fts %s query failed: %s", table, exc)
return out
def _sanitize(query: str) -> str:
"""Минимальная очистка запроса от служебных символов FTS."""
import re
q = query.strip().lower()
q = re.sub(r'["\'\\()!*+-]', " ", q)
q = " ".join(q.split())
return q[:80]
"""Полнотекстовый поиск DuckDB FTS (по решению — включаем).
DuckDB FTS строит виртуальную таблицу-снимок: индекс пересоздаётся при
старте, раз в сутки по расписанию и вручную (POST /api/admin/fts/rebuild).
Чтобы свежесозданные карточки искались сразу, поиск совмещает FTS с
точечным LIKE-дополнением (см. services/leads.search).
Рабочий синтаксис DuckDB >= 1.0: только PRAGMA create_fts_index
(вариант CREATE VIRTUAL TABLE ... USING fts(...) в этой версии падает).
Поиск — через табличную функцию fts_main_<table>.match_bm25(rowid, query).
"""
from __future__ import annotations
import logging
from ..db import store
log = logging.getLogger("leadradar.fts")
OK_KEY = "ftsOk"
# Источник, колонка-rowid, текстовые колонки (должны совпадать со схемой db.py)
_FTS_TARGETS = [
("leads", ("title", "summary", "source_msg", "contact")),
("messages", ("text",)),
("rejected_msgs", ("text",)),
]
def is_ready() -> bool:
return bool(store.get_setting(OK_KEY))
def _set(ok: bool) -> None:
store.set_setting(OK_KEY, ok)
def install_extension() -> bool:
try:
store.execute("INSTALL fts")
store.execute("LOAD fts")
return True
except Exception as exc: # noqa: BLE001
log.warning("FTS extension unavailable: %s", exc)
return False
def rebuild() -> bool:
"""Пересоздаёт FTS-индексы по leads и messages (overwrite поверх старого).
PRAGMA не поддерживает prepared-параметры, поэтому имена подставляются
напрямую — это внутренние константы из _FTS_TARGETS, не пользовательский ввод.
"""
try:
for table, cols in _FTS_TARGETS:
col_list = ", ".join(f"'{c}'" for c in cols)
store.execute(
f"PRAGMA create_fts_index('{table}', 'id', {col_list}, "
"stemmer='russian', overwrite=1)"
)
_set(True)
log.info("FTS rebuild done")
return True
except Exception as exc: # noqa: BLE001
log.warning("FTS rebuild failed: %s", exc)
_set(False)
return False
def search(query: str, limit: int = 50) -> dict:
"""Возвращает {'leads': [ids], 'messages': [ids], 'rejected': [ids]} по FTS
(пусто при сбое)."""
out: dict = {"leads": [], "messages": [], "rejected": []}
if not is_ready():
return out
q = _sanitize(query)
if not q:
return out
targets = ("leads", "messages", "rejected_msgs")
keys = {"leads": "leads", "messages": "messages", "rejected_msgs": "rejected"}
for table in targets:
try:
# fts_main_<table> создаётся PRAGMA create_fts_index над таблицей
fn = f"fts_main_{table}.match_bm25(id, ?)"
rows = store.query(
f"SELECT id, {fn} AS _s FROM {table} "
f"WHERE {fn} IS NOT NULL ORDER BY _s LIMIT ?",
[q, q, limit * 2],
)
out[keys[table]] = [r["id"] for r in rows]
except Exception as exc: # noqa: BLE001
log.warning("fts %s query failed: %s", table, exc)
return out
def _sanitize(query: str) -> str:
"""Минимальная очистка запроса от служебных символов FTS."""
import re
q = query.strip().lower()
q = re.sub(r'["\'\\()!*+-]', " ", q)
q = " ".join(q.split())
return q[:80]