antimoda1 commited on
Commit ·
2aaeb1b
1
Parent(s): 8199364
remove copy-paste
Browse files- lemmatizer.py +92 -0
- retrieval.py +7 -75
- tests/test_lemmatization.py +9 -67
- texts/реформа2012.md +1 -1
lemmatizer.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Общий модуль для лемматизации на русском языке с поддержкой пользовательских терминов.
|
| 4 |
+
Используется в retrieval.py и test_lemmatization.py
|
| 5 |
+
"""
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
import spacy
|
| 8 |
+
from vocabulary.parse_vocabulary import parse_vocabulary
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class RussianLemmatizer:
|
| 12 |
+
"""
|
| 13 |
+
Русский лемматизатор на основе spaCy с поддержкой топонимов.
|
| 14 |
+
Содержит общую логику для всех компонентов системы.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
def __init__(self, load_toponyms: bool = True):
|
| 18 |
+
"""
|
| 19 |
+
Инициализация лемматизатора.
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
load_toponyms: загружать ли топонимы из файла при инициализации
|
| 23 |
+
"""
|
| 24 |
+
print(" Загрузка русской модели spaCy...")
|
| 25 |
+
try:
|
| 26 |
+
self.nlp = spacy.load("ru_core_news_sm")
|
| 27 |
+
except OSError:
|
| 28 |
+
print(" ⚠️ Модель ru_core_news_sm не найдена, скачиваю...")
|
| 29 |
+
import subprocess
|
| 30 |
+
subprocess.check_call(["python", "-m", "spacy", "download", "ru_core_news_sm"])
|
| 31 |
+
self.nlp = spacy.load("ru_core_news_sm")
|
| 32 |
+
|
| 33 |
+
self.toponims = {}
|
| 34 |
+
|
| 35 |
+
if load_toponyms:
|
| 36 |
+
self._register_terms()
|
| 37 |
+
|
| 38 |
+
def _register_terms(self):
|
| 39 |
+
"""
|
| 40 |
+
Загружает термины из vocabulary/vocabulary.md и регистрирует их в spaCy
|
| 41 |
+
как custom component для исправления лемм.
|
| 42 |
+
"""
|
| 43 |
+
# Находим файл vocabulary относительно этого модуля
|
| 44 |
+
vocab_file = Path(__file__).parent / "vocabulary" / "vocabulary.md"
|
| 45 |
+
|
| 46 |
+
if not vocab_file.exists():
|
| 47 |
+
print(f" ⚠️ Файл {vocab_file} не найден, управление терминами пропущено")
|
| 48 |
+
return
|
| 49 |
+
|
| 50 |
+
# Парсим словарь термин -> описание
|
| 51 |
+
vocab_dict = parse_vocabulary(str(vocab_file))
|
| 52 |
+
|
| 53 |
+
# Создаём словарь для быстрого поиска (приводим к нижнему регистру)
|
| 54 |
+
self.toponims = {term.lower(): term.lower() for term in vocab_dict.keys()}
|
| 55 |
+
|
| 56 |
+
print(f" Загружено {len(self.toponims)} терминов из vocabulary.md")
|
| 57 |
+
|
| 58 |
+
# Добавляем custom component для обработки терминов после лемматизации
|
| 59 |
+
@self.nlp.component("fix_terms")
|
| 60 |
+
def fix_terms(doc):
|
| 61 |
+
"""Компонент для исправления лемм терминов и их форм"""
|
| 62 |
+
for token in doc:
|
| 63 |
+
lemma_lower = token.lemma_.lower()
|
| 64 |
+
|
| 65 |
+
# Проверяем прямое совпадение
|
| 66 |
+
if lemma_lower in self.toponims:
|
| 67 |
+
token.lemma_ = self.toponims[lemma_lower]
|
| 68 |
+
return doc
|
| 69 |
+
|
| 70 |
+
# Добавляем компонент после лемматизатора
|
| 71 |
+
if "fix_terms" not in self.nlp.pipe_names:
|
| 72 |
+
self.nlp.add_pipe("fix_terms", after="lemmatizer")
|
| 73 |
+
|
| 74 |
+
def tokenize_text(self, text: str) -> list[str]:
|
| 75 |
+
"""
|
| 76 |
+
Лемматизация текста для русского языка (spaCy).
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
text: текст для лемматизации
|
| 80 |
+
|
| 81 |
+
Returns:
|
| 82 |
+
list: список лемм, исключая пунктуацию
|
| 83 |
+
"""
|
| 84 |
+
doc = self.nlp(text.lower())
|
| 85 |
+
|
| 86 |
+
# Извлекаем леммы, пропускаем пунктуацию
|
| 87 |
+
lemmas = []
|
| 88 |
+
for token in doc:
|
| 89 |
+
if not token.is_punct and token.lemma_.strip(): # Пропускаем пунктуацию и пробелы
|
| 90 |
+
lemmas.append(token.lemma_)
|
| 91 |
+
|
| 92 |
+
return lemmas
|
retrieval.py
CHANGED
|
@@ -6,10 +6,9 @@ from rank_bm25 import BM25Okapi
|
|
| 6 |
import warnings
|
| 7 |
warnings.filterwarnings('ignore')
|
| 8 |
|
| 9 |
-
import spacy
|
| 10 |
-
|
| 11 |
from _1_get_documents import load_and_process_data
|
| 12 |
-
from _2_splitting import
|
|
|
|
| 13 |
# from _3_chunking import RussianEmbedder
|
| 14 |
|
| 15 |
from sentence_transformers import CrossEncoder
|
|
@@ -23,18 +22,9 @@ class Retrieval:
|
|
| 23 |
print("Инициализация RAG системы...")
|
| 24 |
self.device = "cuda" if use_gpu and torch.cuda.is_available() else "cpu"
|
| 25 |
|
| 26 |
-
# Инициализация лемматизатора для русского языка
|
| 27 |
-
print("
|
| 28 |
-
|
| 29 |
-
self.nlp = spacy.load("ru_core_news_sm")
|
| 30 |
-
except OSError:
|
| 31 |
-
print(" ⚠️ Модель ru_core_news_sm не найдена, скачиваю...")
|
| 32 |
-
import subprocess
|
| 33 |
-
subprocess.check_call(["python", "-m", "spacy", "download", "ru_core_news_sm"])
|
| 34 |
-
self.nlp = spacy.load("ru_core_news_sm")
|
| 35 |
-
|
| 36 |
-
# Загружаем топонимы и регистрируем их как исключения для лемматизации
|
| 37 |
-
self._register_toponyms()
|
| 38 |
|
| 39 |
# Загружаем и обрабатываем данные
|
| 40 |
print("1. Загрузка данных из JSON...")
|
|
@@ -60,7 +50,6 @@ class Retrieval:
|
|
| 60 |
tuple: (chunks, docs_metadata, paragraph_metadata, chunk_dates)
|
| 61 |
где chunk_dates - список ((start_year, end_year), ...) для каждого чанка
|
| 62 |
"""
|
| 63 |
-
splitter = Splitter()
|
| 64 |
chunks = []
|
| 65 |
docs_metadata = []
|
| 66 |
paragraph_metadata = []
|
|
@@ -107,68 +96,11 @@ class Retrieval:
|
|
| 107 |
print(f" Из {len(set(paragraph_metadata))} абзацев в {doc_id + 1} документах")
|
| 108 |
return chunks, docs_metadata, paragraph_metadata, chunk_dates
|
| 109 |
|
| 110 |
-
def get_all_chunks(self, doc_id):
|
| 111 |
-
return [chunk for chunk, x in zip(self.chunks, self.docs_metadata, strict=True) if x == doc_id]
|
| 112 |
-
|
| 113 |
-
def _register_toponyms(self):
|
| 114 |
-
"""
|
| 115 |
-
Загружает топонимы из vocabulary/toponims.txt и регистрирует их в spaCy
|
| 116 |
-
как custom component для исправления лемм.
|
| 117 |
-
"""
|
| 118 |
-
toponims_file = Path(__file__).parent / "vocabulary" / "toponims.txt"
|
| 119 |
-
|
| 120 |
-
if not toponims_file.exists():
|
| 121 |
-
print(f" ⚠️ Файл {toponims_file} не найден, управление топонимами пропущено")
|
| 122 |
-
return
|
| 123 |
-
|
| 124 |
-
# Читаем топонимы
|
| 125 |
-
with open(toponims_file, 'r', encoding='utf-8') as f:
|
| 126 |
-
self.toponims = {line.strip().lower(): line.strip().lower() for line in f if line.strip()}
|
| 127 |
-
|
| 128 |
-
print(f" Загружено {len(self.toponims)} топонимов")
|
| 129 |
-
|
| 130 |
-
# Добавляем custom component для обработки топонимов после лемматизации
|
| 131 |
-
@self.nlp.component("fix_toponyms")
|
| 132 |
-
def fix_toponyms(doc):
|
| 133 |
-
"""Компонент для исправления лемм топонимов и их форм"""
|
| 134 |
-
for token in doc:
|
| 135 |
-
lemma_lower = token.lemma_.lower()
|
| 136 |
-
|
| 137 |
-
# Проверяем прямое совпадение
|
| 138 |
-
if lemma_lower in self.toponims:
|
| 139 |
-
token.lemma_ = self.toponims[lemma_lower]
|
| 140 |
-
else:
|
| 141 |
-
# Проверяем, может ли это быть формой топонима
|
| 142 |
-
# Для каждого топонима проверяем, совпадает ли начало слова
|
| 143 |
-
for toponim in self.toponims:
|
| 144 |
-
if lemma_lower.startswith(toponim[:len(lemma_lower)-2]) and len(lemma_lower) > len(toponim) - 3:
|
| 145 |
-
# Похоже, это форма топонима (например, "дягилев" -> "дягилево")
|
| 146 |
-
token.lemma_ = toponim
|
| 147 |
-
break
|
| 148 |
-
return doc
|
| 149 |
-
|
| 150 |
-
# Добавляем компонент после лемматизатора
|
| 151 |
-
if "fix_toponyms" not in self.nlp.pipe_names:
|
| 152 |
-
self.nlp.add_pipe("fix_toponyms", after="lemmatizer")
|
| 153 |
-
|
| 154 |
def _prepare_bm25(self):
|
| 155 |
"""Подготавливаем BM25 индекс для ключевого поиска"""
|
| 156 |
# Токенизация для BM25
|
| 157 |
-
tokenized_chunks = [self.
|
| 158 |
return BM25Okapi(tokenized_chunks)
|
| 159 |
-
|
| 160 |
-
def _tokenize_text(self, text: str) -> list[str]:
|
| 161 |
-
"""Лемматизация текста для русского языка (spaCy)"""
|
| 162 |
-
# Используем spaCy для обработки текста и получения лемм
|
| 163 |
-
doc = self.nlp(text.lower())
|
| 164 |
-
|
| 165 |
-
# Извлекаем леммы, пропускаем пунктуацию
|
| 166 |
-
lemmas = []
|
| 167 |
-
for token in doc:
|
| 168 |
-
if not token.is_punct and token.lemma_.strip(): # Пропускаем пунктуацию и пробелы
|
| 169 |
-
lemmas.append(token.lemma_)
|
| 170 |
-
|
| 171 |
-
return lemmas
|
| 172 |
|
| 173 |
def rerank_search(self, query: str) -> list[dict]:
|
| 174 |
"""
|
|
@@ -188,7 +120,7 @@ class Retrieval:
|
|
| 188 |
|
| 189 |
def bm25_search(self, query: str) -> list:
|
| 190 |
# 2. Ключевой поиск (BM25)
|
| 191 |
-
tokenized_query = self.
|
| 192 |
return self.bm25.get_scores(tokenized_query)
|
| 193 |
|
| 194 |
def filter_by_year_range(self, indices: list[int], year_range: tuple[int, int]) -> list[int]:
|
|
|
|
| 6 |
import warnings
|
| 7 |
warnings.filterwarnings('ignore')
|
| 8 |
|
|
|
|
|
|
|
| 9 |
from _1_get_documents import load_and_process_data
|
| 10 |
+
from _2_splitting import parse_year_metadata, years_overlap, YEAR_OLD, YEAR_NEW
|
| 11 |
+
from lemmatizer import RussianLemmatizer
|
| 12 |
# from _3_chunking import RussianEmbedder
|
| 13 |
|
| 14 |
from sentence_transformers import CrossEncoder
|
|
|
|
| 22 |
print("Инициализация RAG системы...")
|
| 23 |
self.device = "cuda" if use_gpu and torch.cuda.is_available() else "cpu"
|
| 24 |
|
| 25 |
+
# Инициализация лемматизатора для русского языка
|
| 26 |
+
print(" Инициализация лемматизатора...")
|
| 27 |
+
self.lemmatizer = RussianLemmatizer(load_toponyms=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
# Загружаем и обрабатываем данные
|
| 30 |
print("1. Загрузка данных из JSON...")
|
|
|
|
| 50 |
tuple: (chunks, docs_metadata, paragraph_metadata, chunk_dates)
|
| 51 |
где chunk_dates - список ((start_year, end_year), ...) для каждого чанка
|
| 52 |
"""
|
|
|
|
| 53 |
chunks = []
|
| 54 |
docs_metadata = []
|
| 55 |
paragraph_metadata = []
|
|
|
|
| 96 |
print(f" Из {len(set(paragraph_metadata))} абзацев в {doc_id + 1} документах")
|
| 97 |
return chunks, docs_metadata, paragraph_metadata, chunk_dates
|
| 98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
def _prepare_bm25(self):
|
| 100 |
"""Подготавливаем BM25 индекс для ключевого поиска"""
|
| 101 |
# Токенизация для BM25
|
| 102 |
+
tokenized_chunks = [self.lemmatizer.tokenize_text(chunk) for chunk in self.chunks]
|
| 103 |
return BM25Okapi(tokenized_chunks)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
def rerank_search(self, query: str) -> list[dict]:
|
| 106 |
"""
|
|
|
|
| 120 |
|
| 121 |
def bm25_search(self, query: str) -> list:
|
| 122 |
# 2. Ключевой поиск (BM25)
|
| 123 |
+
tokenized_query = self.lemmatizer.tokenize_text(query)
|
| 124 |
return self.bm25.get_scores(tokenized_query)
|
| 125 |
|
| 126 |
def filter_by_year_range(self, indices: list[int], year_range: tuple[int, int]) -> list[int]:
|
tests/test_lemmatization.py
CHANGED
|
@@ -6,7 +6,6 @@
|
|
| 6 |
2. Лемматизацию обычных слов (включая беглые гласные)
|
| 7 |
3. Обработку букв Е и Ё
|
| 8 |
"""
|
| 9 |
-
import re
|
| 10 |
import sys
|
| 11 |
from pathlib import Path
|
| 12 |
from dataclasses import dataclass
|
|
@@ -14,8 +13,8 @@ from dataclasses import dataclass
|
|
| 14 |
# Добавляем родительскую директорию в path
|
| 15 |
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 16 |
|
| 17 |
-
import spacy
|
| 18 |
from rank_bm25 import BM25Okapi
|
|
|
|
| 19 |
|
| 20 |
|
| 21 |
# ============================================================================
|
|
@@ -40,6 +39,7 @@ TESTS_TOPONIMS = [
|
|
| 40 |
# TestSearch(word='Дягилева', sentence='Примерно в 1969 году запускаются поезда от станции Рязани-1 до Дягилева и Леска - пара рейсов в день, но всё же.'),
|
| 41 |
TestSearch(word='Леске', sentence='Примерно в 1969 году запускаются поезда от станции Рязани-1 до Дягилева и Леска - пара рейсов в день, но всё же.'),
|
| 42 |
TestSearch(word="Дашково-Песочню", sentence="31 маршрутка ходила из Дашково-Песочни в Дягилево."),
|
|
|
|
| 43 |
#TestSearch(word="Песочни", sentence="31 маршрутка ходила из Дашково-Песочни в Дягилево."),
|
| 44 |
#TestSearch(word='Дашково-Песочня', sentence='То есть его продлили дальше в Песочню, и он стал охватывать так называемый основной транспортный коридор города.'),
|
| 45 |
]
|
|
@@ -47,7 +47,7 @@ TESTS_TOPONIMS = [
|
|
| 47 |
# тесты для лемматизации обычных слов. Стемминг здесь сломается!
|
| 48 |
TESTS_LEMMATIZATION = [
|
| 49 |
TestSearch(word='хорошая', sentence='И там в лучшем случае было 20-23 машины где-то так.'),
|
| 50 |
-
TestSearch(word='
|
| 51 |
]
|
| 52 |
|
| 53 |
# буквы Е и Ё в топонимах и не только
|
|
@@ -64,79 +64,21 @@ class RussianBM25:
|
|
| 64 |
"""BM25 с лемматизацией для русского языка (spaCy с топонимами)"""
|
| 65 |
|
| 66 |
def __init__(self, documents: list[str]):
|
| 67 |
-
#
|
| 68 |
-
|
| 69 |
-
self.nlp = spacy.load("ru_core_news_sm")
|
| 70 |
-
except OSError:
|
| 71 |
-
print("Модель ru_core_news_sm не найдена, скачиваю...")
|
| 72 |
-
import subprocess
|
| 73 |
-
subprocess.check_call(["python", "-m", "spacy", "download", "ru_core_news_sm"])
|
| 74 |
-
self.nlp = spacy.load("ru_core_news_sm")
|
| 75 |
|
| 76 |
-
|
| 77 |
-
self._register_toponyms()
|
| 78 |
-
|
| 79 |
-
tokenized_docs = [self._tokenize_text(doc) for doc in documents]
|
| 80 |
self.bm25 = BM25Okapi(tokenized_docs)
|
| 81 |
self.documents = documents
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
"""Загружает топонимы из vocabulary/toponims.txt и регистрирует их в spaCy"""
|
| 85 |
-
toponims_file = Path(__file__).parent.parent / "vocabulary" / "toponims.txt"
|
| 86 |
-
|
| 87 |
-
if not toponims_file.exists():
|
| 88 |
-
print(f" Файл {toponims_file} не найден, управление топонимами пропущено")
|
| 89 |
-
return
|
| 90 |
-
|
| 91 |
-
# Читаем топонимы
|
| 92 |
-
with open(toponims_file, 'r', encoding='utf-8') as f:
|
| 93 |
-
self.toponims = {line.strip().lower(): line.strip().lower() for line in f if line.strip()}
|
| 94 |
-
|
| 95 |
-
print(f" Загружено {len(self.toponims)} топонимов из {toponims_file.name}")
|
| 96 |
-
|
| 97 |
-
# Добавляем custom component для обработки топонимов после лемматизации
|
| 98 |
-
@self.nlp.component("fix_toponyms")
|
| 99 |
-
def fix_toponyms(doc):
|
| 100 |
-
"""Компонент для исправления лемм топо��имов и их форм"""
|
| 101 |
-
for token in doc:
|
| 102 |
-
lemma_lower = token.lemma_.lower()
|
| 103 |
-
|
| 104 |
-
# Проверяем прямое совпадение
|
| 105 |
-
if lemma_lower in self.toponims:
|
| 106 |
-
token.lemma_ = self.toponims[lemma_lower]
|
| 107 |
-
else:
|
| 108 |
-
# Проверяем, может ли это быть формой топонима
|
| 109 |
-
# Для каждого топонима проверяем, совпадает ли начало слова
|
| 110 |
-
for toponim in self.toponims:
|
| 111 |
-
if lemma_lower.startswith(toponim[:len(lemma_lower)-2]) and len(lemma_lower) > len(toponim) - 3:
|
| 112 |
-
# Похоже, это форма топонима (например, "дягилев" -> "дягилево")
|
| 113 |
-
token.lemma_ = toponim
|
| 114 |
-
break
|
| 115 |
-
return doc
|
| 116 |
-
|
| 117 |
-
# Добавляем компонент после лемматизатора
|
| 118 |
-
if "fix_toponyms" not in self.nlp.pipe_names:
|
| 119 |
-
self.nlp.add_pipe("fix_toponyms", after="lemmatizer")
|
| 120 |
-
|
| 121 |
-
def _tokenize_text(self, text: str) -> list[str]:
|
| 122 |
-
"""Лемматизация текста для русского языка (spaCy)"""
|
| 123 |
-
# Используем spaCy для обработки текста и получения лемм
|
| 124 |
-
doc = self.nlp(text.lower())
|
| 125 |
-
|
| 126 |
-
# Извлекаем леммы, пропускаем пунктуацию
|
| 127 |
-
lemmas = []
|
| 128 |
-
for token in doc:
|
| 129 |
-
if not token.is_punct and token.lemma_.strip(): # Пропускаем пунктуацию и пробелы
|
| 130 |
-
lemmas.append(token.lemma_)
|
| 131 |
-
|
| 132 |
-
return lemmas
|
| 133 |
|
| 134 |
def search(self, query: str) -> list[tuple[int, float]]:
|
| 135 |
"""
|
| 136 |
Поиск по запросу.
|
| 137 |
Returns: список (индекс_документа, релевантность_score)
|
| 138 |
"""
|
| 139 |
-
tokenized_query = self.
|
| 140 |
scores = self.bm25.get_scores(tokenized_query)
|
| 141 |
|
| 142 |
# Возвращаем индексы документов, отсортированные по релевантности
|
|
|
|
| 6 |
2. Лемматизацию обычных слов (включая беглые гласные)
|
| 7 |
3. Обработку букв Е и Ё
|
| 8 |
"""
|
|
|
|
| 9 |
import sys
|
| 10 |
from pathlib import Path
|
| 11 |
from dataclasses import dataclass
|
|
|
|
| 13 |
# Добавляем родительскую директорию в path
|
| 14 |
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 15 |
|
|
|
|
| 16 |
from rank_bm25 import BM25Okapi
|
| 17 |
+
from lemmatizer import RussianLemmatizer
|
| 18 |
|
| 19 |
|
| 20 |
# ============================================================================
|
|
|
|
| 39 |
# TestSearch(word='Дягилева', sentence='Примерно в 1969 году запускаются поезда от станции Рязани-1 до Дягилева и Леска - пара рейсов в день, но всё же.'),
|
| 40 |
TestSearch(word='Леске', sentence='Примерно в 1969 году запускаются поезда от станции Рязани-1 до Дягилева и Леска - пара рейсов в день, но всё же.'),
|
| 41 |
TestSearch(word="Дашково-Песочню", sentence="31 маршрутка ходила из Дашково-Песочни в Дягилево."),
|
| 42 |
+
TestSearch(word='Канищева', sentence='А тут нас извечная проблема, связи Ворошиловки с районом, точнее сказать с Приокским, Канищево, где находятся важные объекты, причем часть расположена в Приокском, часть в Канищево.')
|
| 43 |
#TestSearch(word="Песочни", sentence="31 маршрутка ходила из Дашково-Песочни в Дягилево."),
|
| 44 |
#TestSearch(word='Дашково-Песочня', sentence='То есть его продлили дальше в Песочню, и он стал охватывать так называемый основной транспортный коридор города.'),
|
| 45 |
]
|
|
|
|
| 47 |
# тесты для лемматизации обычных слов. Стемминг здесь сломается!
|
| 48 |
TESTS_LEMMATIZATION = [
|
| 49 |
TestSearch(word='хорошая', sentence='И там в лучшем случае было 20-23 машины где-то так.'),
|
| 50 |
+
TestSearch(word='Петли', sentence='И вместо трёх петель осталась одна маленькая в Горроще.') # беглая Е - чередование в корне
|
| 51 |
]
|
| 52 |
|
| 53 |
# буквы Е и Ё в топонимах и не только
|
|
|
|
| 64 |
"""BM25 с лемматизацией для русского языка (spaCy с топонимами)"""
|
| 65 |
|
| 66 |
def __init__(self, documents: list[str]):
|
| 67 |
+
# Инициализируем лемматизатор
|
| 68 |
+
self.lemmatizer = RussianLemmatizer(load_toponyms=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
+
tokenized_docs = [self.lemmatizer.tokenize_text(doc) for doc in documents]
|
|
|
|
|
|
|
|
|
|
| 71 |
self.bm25 = BM25Okapi(tokenized_docs)
|
| 72 |
self.documents = documents
|
| 73 |
+
# Для обратной совместимости с тестами
|
| 74 |
+
self.nlp = self.lemmatizer.nlp
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
def search(self, query: str) -> list[tuple[int, float]]:
|
| 77 |
"""
|
| 78 |
Поиск по запросу.
|
| 79 |
Returns: список (индекс_документа, релевантность_score)
|
| 80 |
"""
|
| 81 |
+
tokenized_query = self.lemmatizer.tokenize_text(query)
|
| 82 |
scores = self.bm25.get_scores(tokenized_query)
|
| 83 |
|
| 84 |
# Возвращаем индексы документов, отсортированные по релевантности
|
texts/реформа2012.md
CHANGED
|
@@ -64,7 +64,7 @@
|
|
| 64 |
|
| 65 |
Но транспортная реформа 2011 года это предполагал не только отмены, но еще и изменения маршрутов. Теперь мы перейдем к ним и надо заметить, что часть изменений была действительно воплощена в жизнь. К сожалению, я нашел только лишь изменения, которые касаются маршрутов автоколонны 1310. И большая часть их была действительно выполнена. Например, шестой автобус был продлен с завода Центролит до Братиславского, а значительная часть маршрутов была перенаправлена с Круиза на Глобус. При этом четвертый автобус планировали перенаправить с Глобуса до Новоселов, 60. И это единственный пункт изменения маршрутов автобуса в автоколонне, который выполнен так и не был. Но зато вместо четвертого автобуса до Новоселов, 60 стал ездить 16-й. По маршрутным же такси я нашел лишь список этих самых маршрутов, которые планировалось изменить. А вот как - я так и не нашел никакой информации.
|
| 66 |
|
| 67 |
-
Ну и также, помимо отмен изменений, в реформе 2011 года планировались и новые маршруты. Один маршрут предполагалось отдать автоколонне 1310, а три остальных планировались отдать коммерческим переводчикам. Теперь же мы их также рассмотрим. Начну с автобусного номер 25. Должен был пройти от октябрьского городка по улице Чкалова, Московскому шоссе через Приокск и Канищево в Недостоево. Довольно странный маршрут, но в целом и сам по себе интересный. Единственное, что мне непонятно, как он должен был пройти из Приокского в Канищево: по улице Молодцова, по улице Станкозаводской или же вообще через троллейбусное депо номер 2. Так что тут тоже непонятно. Что ж до маршрутов, кто тут все три варианта довольно интересны. Начну с маршрута от поселка Божатково до площади Победы. Во-первых, где развернутся на площади Победы, а во-вторых, это же, по сути, сокращенный 13 автобус. Да, я понимаю, что Михайловского шоссе до площади Победы максимальный пассажиропоток, а потому там в идеале нужна какая-то дублирующая, скажем так, вспомогательная даже маршрутка. Но, зная Оризанские реалии, мне что-то подсказывает, что эта маршрутка ехала бы, не помогая автобусу, а перед ним, собирая всех пассажиров. Следующий маршрут вообще имеет какую-то странную схему, потому что тут он едет сначала по северной окружной, потом возвращается на улицу Каширина и Солнечную, потом опять едет на северную окружную дорогу в поселок Борки. Какой у него точный маршрут? Непонятно. Возможно, это не так хотели показать, что в одну сторону поедет по Солнечной, а в другую по Каширина, но лично мне это показалось непонятно. И третий маршрут с поселка Ворошиловки до завода ТКПО. Этот маршрут все-таки через некоторое время попытались ввести под номером 46, но проходил ли он в таком виде, непонятно. Скорее всего, он так ни разу и не вышел на линию. Так что в плане новых маршрутов было сделано целое ничего, поскольку реформу официально все-таки отменили.
|
| 68 |
|
| 69 |
Я думаю, вы заметили, что практически ничего я до этого не говорил про троллейбус. И в том, что показали людям в конце 2011 года, действительно так же не было упоминаний про троллейбус. Но фактически в реформе его упоминания было. А именно как раз-таки была открыта новая троллейбусная линия по проезду Шабулина, улицам Бирюзовой и Интернациональной, по которой пустили два новых маршрута: четвертый из Московского ваконящего и восемнадцатый из конечьего в центре. Если четвертый маршрут был пущен вполне себе логично, да, вы сейчас скажете, что у него такой пиковый пассажиропоток, и в итоге вне часа пик он часто ездит пустым, но все-таки этот маршрут более-менее востребован. Что же с восемнадцатым троллейбусом, то тут не все так однозначно. Я как человек из будущего считаю, что восемнадцатый троллейбус нужно было пускать не в центр, а в Горрощу до центрального парка или до улицы Строителей. В тех условиях, когда предполагалось обменить 36 автобус и 50 маршрутку, имевшие подобные трассировку, такой троллейбус был бы более разумен. И можно было эту отмену вполне себе протокнуть под предлогом, что маршрутка заменяется на вместительный троллейбус. Но не свершилось. В позднее это выйдет боком еще для восьмого маршрута, который потом начнут резко менять, а ведь начнется все с отмены того самого восемнадцатого маршрута.
|
| 70 |
|
|
|
|
| 64 |
|
| 65 |
Но транспортная реформа 2011 года это предполагал не только отмены, но еще и изменения маршрутов. Теперь мы перейдем к ним и надо заметить, что часть изменений была действительно воплощена в жизнь. К сожалению, я нашел только лишь изменения, которые касаются маршрутов автоколонны 1310. И большая часть их была действительно выполнена. Например, шестой автобус был продлен с завода Центролит до Братиславского, а значительная часть маршрутов была перенаправлена с Круиза на Глобус. При этом четвертый автобус планировали перенаправить с Глобуса до Новоселов, 60. И это единственный пункт изменения маршрутов автобуса в автоколонне, который выполнен так и не был. Но зато вместо четвертого автобуса до Новоселов, 60 стал ездить 16-й. По маршрутным же такси я нашел лишь список этих самых маршрутов, которые планировалось изменить. А вот как - я так и не нашел никакой информации.
|
| 66 |
|
| 67 |
+
Ну и также, помимо отмен изменений, в реформе 2011 года планировались и новые маршруты. Один маршрут предполагалось отдать автоколонне 1310, а три остальных планировались отдать коммерческим переводчикам. Теперь же мы их также рассмотрим. Начну с автобусного номер 25. Должен был пройти от октябрьского городка по улице Чкалова, Московскому шоссе через Приокский и Канищево в Недостоево. Довольно странный маршрут, но в целом и сам по себе интересный. Единственное, что мне непонятно, как он должен был пройти из Приокского в Канищево: по улице Молодцова, по улице Станкозаводской или же вообще через троллейбусное депо номер 2. Так что тут тоже непонятно. Что ж до маршрутов, кто тут все три варианта довольно интересны. Начну с маршрута от поселка Божатково до площади Победы. Во-первых, где развернутся на площади Победы, а во-вторых, это же, по сути, сокращенный 13 автобус. Да, я понимаю, что Михайловского шоссе до площади Победы максимальный пассажиропоток, а потому там в идеале нужна какая-то дублирующая, скажем так, вспомогательная даже маршрутка. Но, зная Оризанские реалии, мне что-то подсказывает, что эта маршрутка ехала бы, не помогая автобусу, а перед ним, собирая всех пассажиров. Следующий маршрут вообще имеет какую-то странную схему, потому что тут он едет сначала по северной окружной, потом возвращается на улицу Каширина и Солнечную, потом опять едет на северную окружную дорогу в поселок Борки. Какой у него точный маршрут? Непонятно. Возможно, это не так хотели показать, что в одну сторону поедет по Солнечной, а в другую по Каширина, но лично мне это показалось непонятно. И третий маршрут с поселка Ворошиловки до завода ТКПО. Этот маршрут все-таки через некоторое время попытались ввести под номером 46, но проходил ли он в таком виде, непонятно. Скорее всего, он так ни разу и не вышел на линию. Так что в плане новых маршрутов было сделано целое ничего, поскольку реформу официально все-таки отменили.
|
| 68 |
|
| 69 |
Я думаю, вы заметили, что практически ничего я до этого не говорил про троллейбус. И в том, что показали людям в конце 2011 года, действительно так же не было упоминаний про троллейбус. Но фактически в реформе его упоминания было. А именно как раз-таки была открыта новая троллейбусная линия по проезду Шабулина, улицам Бирюзовой и Интернациональной, по которой пустили два новых маршрута: четвертый из Московского ваконящего и восемнадцатый из конечьего в центре. Если четвертый маршрут был пущен вполне себе логично, да, вы сейчас скажете, что у него такой пиковый пассажиропоток, и в итоге вне часа пик он часто ездит пустым, но все-таки этот маршрут более-менее востребован. Что же с восемнадцатым троллейбусом, то тут не все так однозначно. Я как человек из будущего считаю, что восемнадцатый троллейбус нужно было пускать не в центр, а в Горрощу до центрального парка или до улицы Строителей. В тех условиях, когда предполагалось обменить 36 автобус и 50 маршрутку, имевшие подобные трассировку, такой троллейбус был бы более разумен. И можно было эту отмену вполне себе протокнуть под предлогом, что маршрутка заменяется на вместительный троллейбус. Но не свершилось. В позднее это выйдет боком еще для восьмого маршрута, который потом начнут резко менять, а ведь начнется все с отмены того самого восемнадцатого маршрута.
|
| 70 |
|