mirror of
https://github.com/migatu/astrololo.git
synced 2026-07-14 13:34:38 +00:00
16d35c16dc
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>
69 lines
2.8 KiB
Python
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]
|