"""Поиск по каталогу: полнотекстовый запрос с учётом морфологии.""" from __future__ import annotations import json import re from .textutil import stem SAFE = re.compile(r"[^\w\-Ѐ-ӿ]+", re.U) # Веса колонок для bm25: заголовок и ключевые слова важнее тела документа. BM25_WEIGHTS = (10.0, 8.0, 4.0, 1.0, 0.8, 2.0) def build_query(text: str) -> str: """Превращает пользовательский запрос в выражение FTS5. Каждое слово ищется и как есть, и по основе с префиксом — чтобы «инвертор» находил «инверторы» и «инвертора». """ phrases = re.findall(r'"([^"]+)"', text) rest = re.sub(r'"[^"]*"', " ", text) parts = ['"%s"' % SAFE.sub(" ", p).strip() for p in phrases if p.strip()] for raw in rest.split(): neg = raw.startswith("-") word = SAFE.sub("", raw.lstrip("-+")).strip() if len(word) < 2: continue st = stem(word) variants = {'"%s"' % word.lower()} if st and st != word.lower(): variants.add('"%s"*' % st) else: variants.add('"%s"*' % word.lower()) clause = "(" + " OR ".join(sorted(variants)) + ")" parts.append(("NOT " + clause) if neg else clause) return " AND ".join(p for p in parts if p).replace("AND NOT", "NOT") def search(con, query: str, *, limit: int = 20, ext: str | None = None, folder: str | None = None, lang: str | None = None, year_from: int | None = None, kind: str | None = None) -> list[dict]: fts_query = build_query(query) if not fts_query: return [] sql = [ "SELECT d.id, d.title, d.author, d.year, d.pages, d.ext, d.lang, d.kind,", " d.rel_path, d.path, d.folder, d.description, d.keywords, d.theses,", " d.scanned, u.keywords AS user_keywords,", " snippet(docs_fts, 3, '«', '»', ' … ', 14) AS snip,", " bm25(docs_fts, %s) AS rank" % ", ".join(str(w) for w in BM25_WEIGHTS), "FROM docs_fts JOIN docs d ON d.id = docs_fts.rowid", " LEFT JOIN user_keywords u ON u.path = d.path", "WHERE docs_fts MATCH ?", ] params: list = [fts_query] if ext: sql.append("AND d.ext = ?") params.append("." + ext.lower().lstrip(".")) if folder: sql.append("AND d.folder LIKE ?") params.append("%" + folder + "%") if lang: sql.append("AND d.lang = ?") params.append(lang) if kind: sql.append("AND d.kind = ?") params.append(kind) if year_from: sql.append("AND d.year >= ?") params.append(year_from) sql.append("ORDER BY rank LIMIT ?") params.append(limit) rows = con.execute("\n".join(sql), params).fetchall() out = [] for r in rows: d = dict(r) d["keywords"] = json.loads(d.get("keywords") or "[]") d["theses"] = json.loads(d.get("theses") or "[]") d["user_keywords"] = json.loads(d.get("user_keywords") or "[]") d["score"] = round(-d.pop("rank"), 2) out.append(d) return out