Files
gitea 16d35c16dc Szkielet aplikacji trójwarstwowej (prezentacja / logika / dane)
Trzy niezależne usługi FastAPI komunikujące się przez HTTP/JSON, każda
zna tylko adres warstwy bezpośrednio pod nią:

- presentation (:8000) — strona WWW + formularz
- logic (:8001) — reguły biznesowe, pośrednik
- data (:8002) — wyszukiwanie danych za interfejsem DataProvider

Warstwa danych: czytanie setek plików .xlsx z wykrywaniem nagłówka i
mapowaniem układu kolumn na schemat kanoniczny, z 4-poziomowym cache
(schemat L1, Parquet L2, wyniki zapytań L3, odwrócony indeks L4) i
unieważnianiem po odcisku pliku. Gotowa ścieżka migracji do SQL
(ingest/to_sql.py + SqlDataProvider, przełączane przez DATA_PROVIDER).

Zawiera docker-compose, Makefile, generator danych przykładowych.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:35:09 +02:00

69 lines
2.8 KiB
Python

"""Odwrócony indeks: wartość kanonicznego klucza -> które pliki/arkusze ją mają.
POZIOM 4 (i najważniejszy przy skali) — pozwala NIE skanować setek plików przy
każdym zapytaniu. Budujemy w SQLite indeks: dla wybranych kluczy (np. name, id,
symbol) zapisujemy, w którym pliku/arkuszu występuje dana wartość. Wyszukiwanie
najpierw pyta indeks (jeden szybki SELECT), a otwiera tylko trafione pliki.
Ten SQLite indeks jest jednocześnie POMOSTEM do pełnej migracji SQL — rozbudowa
go o wszystkie kolumny = de facto baza danych (patrz ingest/to_sql.py).
"""
from __future__ import annotations
import sqlite3
from pathlib import Path
class InvertedIndex:
def __init__(self, cache_dir: Path) -> None:
self._db = sqlite3.connect(str(cache_dir / "index.db"), check_same_thread=False)
self._db.execute(
"""
CREATE TABLE IF NOT EXISTS entries (
key TEXT, -- kanoniczne pole, np. 'name'
value TEXT, -- znormalizowana (lower) wartość
file TEXT, -- ścieżka pliku
sheet TEXT
)
"""
)
self._db.execute("CREATE INDEX IF NOT EXISTS ix_kv ON entries(key, value)")
self._db.execute(
"CREATE TABLE IF NOT EXISTS files (file TEXT PRIMARY KEY, fingerprint TEXT)"
)
self._db.commit()
def file_fingerprint(self, file: str) -> str | None:
cur = self._db.execute("SELECT fingerprint FROM files WHERE file=?", (file,))
row = cur.fetchone()
return row[0] if row else None
def reindex_file(self, file: str, fingerprint: str, rows: list[tuple[str, str, str]]) -> None:
"""rows: lista (key, value, sheet) dla jednego pliku."""
self._db.execute("DELETE FROM entries WHERE file=?", (file,))
self._db.executemany(
"INSERT INTO entries(key, value, file, sheet) VALUES (?,?,?,?)",
[(k, v.lower(), file, sheet) for (k, v, sheet) in rows],
)
self._db.execute(
"INSERT OR REPLACE INTO files(file, fingerprint) VALUES (?,?)", (file, fingerprint)
)
self._db.commit()
def lookup(self, key: str, value: str, exact: bool) -> list[tuple[str, str]]:
"""Zwraca listę (file, sheet) kandydatów do przeszukania."""
if exact:
cur = self._db.execute(
"SELECT DISTINCT file, sheet FROM entries WHERE key=? AND value=?",
(key, value.lower()),
)
else:
cur = self._db.execute(
"SELECT DISTINCT file, sheet FROM entries WHERE key=? AND value LIKE ?",
(key, f"%{value.lower()}%"),
)
return [(r[0], r[1]) for r in cur.fetchall()]
def count_files(self) -> int:
return self._db.execute("SELECT COUNT(*) FROM files").fetchone()[0]