8 moduli leaf portati verbatim da ChironeWp3 con rename psdwp3→tht: - vendor/thoth_lsh (MinHash/LSH, leaf puro datasketch+tqdm) + VENDORED.md - lshindex/ (build/save/load/query, dipende vendor + LshConfig) - sqlcheck/ (validate_sql, leaf ExecutionConfig+mschema) - execute/ + execute/warnings (run_controlled/explain, leaf sqlglot+sqlalchemy) - rest/execute + rest/explain (REST variants, dipendono execute+rest.client) - ctetest (CTE test records, leaf sqlglot+pydantic) - report (validation report rendering, dipende execute+sqlcheck) - datamart (stub NotImplementedError) Verifica: import smoke catena completa OK, pytest 109 passed. Deps (datasketch, sqlglot, sqlalchemy, pydantic, requests, tqdm) già in pyproject. VENDORED.md neutralizzato (riferimenti PsdWp3→Thoth).
83 lines
2.9 KiB
Python
83 lines
2.9 KiB
Python
# Vendored from thoth_sqldb2 (thoth-dbmanager 0.7.4) — lsh/core.py
|
|
# Copyright 2025 Marco Pancotti — Apache License 2.0
|
|
# Modifiche locali documentate in VENDORED.md.
|
|
"""Core LSH (MinHash) per la ricerca di valori simili nei campi del database."""
|
|
|
|
import logging
|
|
from typing import Dict, List, Tuple
|
|
|
|
from datasketch import MinHash, MinHashLSH
|
|
from tqdm import tqdm
|
|
|
|
|
|
def create_minhash(signature_size: int, string: str, n_gram: int) -> MinHash:
|
|
m = MinHash(num_perm=signature_size)
|
|
for d in [string[i : i + n_gram] for i in range(len(string) - n_gram + 1)]:
|
|
m.update(d.encode("utf8"))
|
|
return m
|
|
|
|
|
|
# Token che, se presenti nel nome di una colonna, la marcano come "name-like":
|
|
# nomi propri di persone/enti, i cui valori sono utili al value-matching e quindi
|
|
# mai da escludere dall'indice LSH. Bilingue EN/IT (modifica locale, vedi VENDORED.md).
|
|
# Per substring coprono le forme flesse: "nome" -> cognome/soprannome,
|
|
# "name" -> surname/username, "denominazion" -> denominazione, ecc.
|
|
NAME_LIKE_TOKENS: tuple[str, ...] = (
|
|
"name",
|
|
"nome",
|
|
"nominativ",
|
|
"denominazion",
|
|
"ragione_sociale",
|
|
)
|
|
|
|
|
|
def skip_column(
|
|
column_name: str,
|
|
column_values: List[str],
|
|
max_total_chars: int = 50000,
|
|
max_avg_length: int = 20,
|
|
name_tokens: tuple[str, ...] = NAME_LIKE_TOKENS,
|
|
) -> bool:
|
|
lowered = column_name.lower()
|
|
if any(token in lowered for token in name_tokens):
|
|
return False
|
|
sum_of_lengths = sum(len(value) for value in column_values)
|
|
average_length = sum_of_lengths / len(column_values)
|
|
return (sum_of_lengths > max_total_chars) and (average_length > max_avg_length)
|
|
|
|
|
|
def jaccard_similarity(m1: MinHash, m2: MinHash) -> float:
|
|
return m1.jaccard(m2)
|
|
|
|
|
|
def create_lsh_index(
|
|
unique_values: Dict[str, Dict[str, List[str]]],
|
|
signature_size: int,
|
|
n_gram: int,
|
|
threshold: float,
|
|
verbose: bool = True,
|
|
) -> Tuple[MinHashLSH, Dict[str, Tuple[MinHash, str, str, str]]]:
|
|
lsh = MinHashLSH(threshold=threshold, num_perm=signature_size)
|
|
minhashes: Dict[str, Tuple[MinHash, str, str, str]] = {}
|
|
total = sum(
|
|
len(column_values)
|
|
for table_values in unique_values.values()
|
|
for column_values in table_values.values()
|
|
)
|
|
logging.info("Total unique values: %s", total)
|
|
progress_bar = tqdm(total=total, desc="Creating LSH") if verbose else None
|
|
|
|
for table_name, table_values in unique_values.items():
|
|
for column_name, column_values in table_values.items():
|
|
for idx, value in enumerate(column_values):
|
|
minhash = create_minhash(signature_size, value, n_gram)
|
|
minhash_key = f"{table_name}_{column_name}_{idx}"
|
|
minhashes[minhash_key] = (minhash, table_name, column_name, value)
|
|
lsh.insert(minhash_key, minhash)
|
|
if progress_bar:
|
|
progress_bar.update(1)
|
|
|
|
if progress_bar:
|
|
progress_bar.close()
|
|
return lsh, minhashes
|