feat: initial microservices project structure
Services: - api: FastAPI gateway with JWT auth, async endpoints, WebSocket - worker-gpu: CUDA sentence-transformers, FAISS IVFFlat, Ollama LLM - worker-indexer: Winnowing+MinHash plagiarism detection, PDF/DOCX extraction - worker-notifier: SMTP email notifications - worker-gost: GOST 7.1-2003 and GOST R 7.0.5-2008 formatting Infrastructure: - docker-compose.yml (production) + docker-compose.dev.yml (hot reload) - Nginx reverse proxy + WebSocket support - PostgreSQL 16 with Alembic migrations - Elasticsearch 8 with Russian/English analyzers - MinIO, RabbitMQ, Redis, Ollama Frontend: - React 18 + Vite + TypeScript + TailwindCSS + Zustand + React Query v5 - 9 pages: Home, Search, Cabinet, Task, Check, Bibliography, Pricing, Login, Register Scripts: - Parser stubs: OpenAlex, КиберЛенинка, arXiv (Phase 0 - to be filled) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
120
services/worker-indexer/app/algorithms/winnowing.py
Normal file
120
services/worker-indexer/app/algorithms/winnowing.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""Алгоритм Winnowing для fingerprinting текстов.
|
||||
|
||||
Winnowing — алгоритм выбора минимального хэша в скользящем окне.
|
||||
Используется для нахождения точных и частичных совпадений текстов.
|
||||
"""
|
||||
|
||||
import xxhash
|
||||
|
||||
|
||||
def get_ngrams(tokens: list[str], k: int = 5) -> list[str]:
|
||||
"""
|
||||
Сформировать n-граммы из токенов.
|
||||
|
||||
Args:
|
||||
tokens: Список слов
|
||||
k: Размер n-граммы
|
||||
|
||||
Returns:
|
||||
Список n-грамм в виде строк
|
||||
"""
|
||||
if len(tokens) < k:
|
||||
return []
|
||||
return [" ".join(tokens[i : i + k]) for i in range(len(tokens) - k + 1)]
|
||||
|
||||
|
||||
def hash_ngram(ngram: str) -> int:
|
||||
"""
|
||||
Хэшировать n-грамму через xxHash (быстро, детерминированно).
|
||||
|
||||
Args:
|
||||
ngram: n-грамма для хэширования
|
||||
|
||||
Returns:
|
||||
64-битный хэш
|
||||
"""
|
||||
return xxhash.xxh64(ngram.encode("utf-8")).intdigest()
|
||||
|
||||
|
||||
def winnow(text: str, k: int = 5, window: int = 4) -> set[int]:
|
||||
"""
|
||||
Алгоритм Winnowing для построения fingerprint документа.
|
||||
|
||||
Шаги:
|
||||
1. Токенизация (lowercase)
|
||||
2. Построение k-грамм
|
||||
3. Хэширование k-грамм
|
||||
4. Скользящее окно — выбор минимального хэша в каждой позиции
|
||||
|
||||
Args:
|
||||
text: Исходный текст
|
||||
k: Размер k-граммы (n-gram)
|
||||
window: Размер скользящего окна
|
||||
|
||||
Returns:
|
||||
Множество отобранных хэшей (fingerprint)
|
||||
"""
|
||||
tokens = text.lower().split()
|
||||
|
||||
if len(tokens) < k:
|
||||
return set()
|
||||
|
||||
ngrams = get_ngrams(tokens, k)
|
||||
hashes = [hash_ngram(ng) for ng in ngrams]
|
||||
|
||||
if not hashes:
|
||||
return set()
|
||||
|
||||
# Скользящее окно — выбираем минимальный хэш в каждом окне
|
||||
fingerprint: set[int] = set()
|
||||
prev_min_idx = -1
|
||||
|
||||
for i in range(len(hashes) - window + 1):
|
||||
window_hashes = hashes[i : i + window]
|
||||
min_val = min(window_hashes)
|
||||
min_idx = i + window_hashes.index(min_val)
|
||||
|
||||
# Добавляем только если это новый минимум или позиция изменилась
|
||||
if min_idx != prev_min_idx:
|
||||
fingerprint.add(min_val)
|
||||
prev_min_idx = min_idx
|
||||
|
||||
return fingerprint
|
||||
|
||||
|
||||
def jaccard_similarity(fp_a: set[int], fp_b: set[int]) -> float:
|
||||
"""
|
||||
Коэффициент Жаккара для двух fingerprint'ов.
|
||||
|
||||
Args:
|
||||
fp_a: Fingerprint документа A
|
||||
fp_b: Fingerprint документа B
|
||||
|
||||
Returns:
|
||||
Коэффициент сходства от 0.0 до 1.0
|
||||
"""
|
||||
if not fp_a or not fp_b:
|
||||
return 0.0
|
||||
|
||||
intersection = len(fp_a & fp_b)
|
||||
union = len(fp_a | fp_b)
|
||||
|
||||
return intersection / union if union > 0 else 0.0
|
||||
|
||||
|
||||
def compute_similarity(text_a: str, text_b: str, k: int = 5, window: int = 4) -> float:
|
||||
"""
|
||||
Вычислить схожесть двух текстов через Winnowing.
|
||||
|
||||
Args:
|
||||
text_a: Первый текст
|
||||
text_b: Второй текст
|
||||
k: Размер k-граммы
|
||||
window: Размер окна Winnowing
|
||||
|
||||
Returns:
|
||||
Схожесть от 0.0 до 1.0
|
||||
"""
|
||||
fp_a = winnow(text_a, k=k, window=window)
|
||||
fp_b = winnow(text_b, k=k, window=window)
|
||||
return jaccard_similarity(fp_a, fp_b)
|
||||
Reference in New Issue
Block a user