from __future__ import annotations from tools.retrieval.publication import DEFAULT_PUBLISHED_ONLY import sqlite3 from pathlib import Path from typing import Any from tools.sync.index_state import rag_snapshot from tools.retrieval.retrieval_contract import result_fields, source_references # Tieto importy sú zámerne verejné. # Zachovávajú spätnú kompatibilitu pre testy # a evaluačné skripty, ktoré ich importujú # z tools.rag.search_utils. from tools.rag.search_core import ( ANY_TERM_RRF_WEIGHT, BM25_SQL, BM25_WEIGHTS, DEFAULT_CANDIDATE_MULTIPLIER, FTS_RRF_WEIGHT, MIN_CANDIDATES, MIN_STEM_PREFIX_LENGTH, RRF_K, STRATEGY_PRIORITY, VECTOR_RRF_WEIGHT, WORD_RE, add_fts_metadata, add_vector_metadata, build_match_queries, database_bool, diversify_results, embedding_index_info, exact_match_bonus, fuse_hybrid_results, load_labels, load_result_labels, make_source_url, normalize_for_compare, parse_heading_paths, query_contains_title, query_tokens, quote_fts_token, run_fts_query, run_vector_query, tokens_match_for_title, verify_search_schema, ) @rag_snapshot def search_database( db_file: Path, query: str, limit: int = 10, published_only: bool = DEFAULT_PUBLISHED_ONLY, max_per_document: int = 3, *, question_plan=None, ) -> dict[str, Any]: if limit < 1 or max_per_document < 0: raise ValueError("Limit musí byť kladný a max_per_document nezáporné.") if not db_file.exists(): raise FileNotFoundError("Databáza neexistuje: " f"{db_file}") clean_query = query.strip() if not clean_query: return { **result_fields(status="no_answer", sources=[], reason="empty_query"), "engine": ("hybrid_fts5_embeddings"), "strategies": [], "results": [], } match_queries = build_match_queries(clean_query) candidate_limit = max( MIN_CANDIDATES, (limit * DEFAULT_CANDIDATE_MULTIPLIER), ) with sqlite3.connect( db_file, timeout=5.0, ) as conn: conn.row_factory = sqlite3.Row conn.execute("PRAGMA query_only = ON") verify_search_schema(conn) from tools.rag.search_scope import prepare_search_scope scope = prepare_search_scope(conn, db_file, clean_query, published_only=published_only, question_plan=question_plan) if scope.failure: return scope.failure # ------------------------- # FTS5 kandidáti # ------------------------- fts_candidates: list[dict[str, Any]] = [] fts_capped = False used_strategies: list[str] = [] for ( strategy, match_query, ) in match_queries: rows = run_fts_query( conn, match_query, candidate_limit + 1, published_only, eligible_chunk_ids=scope.eligible_ids, ) if not rows: continue fts_capped = len(rows) > candidate_limit rows = rows[:candidate_limit] for row in rows: row["strategy"] = strategy fts_candidates = rows used_strategies = [strategy] # Používa sa prvá stratégia, # ktorá vráti výsledky. break fts_results = add_fts_metadata( conn, clean_query, scope.project_rows(fts_candidates), ) # ------------------------- # Embedding kandidáti # ------------------------- vector_candidates = run_vector_query( conn, clean_query, candidate_limit + 1, published_only, eligible_chunk_ids=scope.eligible_ids, ) vector_capped = len(vector_candidates) > candidate_limit vector_candidates = vector_candidates[:candidate_limit] vector_results = add_vector_metadata( conn, scope.project_rows(vector_candidates), ) # ------------------------- # Hybrid fusion # ------------------------- hybrid_results = fuse_hybrid_results( fts_results, vector_results, ) # ------------------------- # Document diversification # ------------------------- final_results = diversify_results( hybrid_results, limit, max_per_document, ) reasons = [] if fts_candidates and fts_capped: reasons.append("candidate_limit") if vector_capped: reasons.append("candidate_limit") eligible = diversify_results(hybrid_results, max(1, len(hybrid_results)), max_per_document) if len(eligible) < len(hybrid_results): reasons.append("per_document_limit") if len(eligible) > limit: reasons.append("result_limit") return { **result_fields( status="ok" if final_results else "no_answer", sources=source_references(final_results), reason=None if final_results else "no_matching_sources", truncation_reasons=reasons, ), "engine": ("hybrid_fts5_embeddings"), "strategies": (used_strategies), "results": (final_results), }