From 84b82465620a750aad9aa82df6a7ad85c591138f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=90=D0=B1=D0=B0=D0=B9?= =?UTF-8?q?=D0=B4=D1=83=D0=BB=D0=B8=D0=BD?= Date: Tue, 10 Mar 2026 16:33:39 +0300 Subject: [PATCH] Init --- .env.example | 13 + .gitignore | 2 + README.md | 141 +++++++++ requirements.txt | 6 + supabase.py | 443 ++++++++++++++++++++++++++ wiki_auth.py | 36 +++ wiki_check_slugs.py | 78 +++++ wiki_embeddings.py | 171 ++++++++++ wiki_sync.py | 737 +++++++++++++++++++++++++++++++++++++++++++ wiki_tree_crawler.py | 146 +++++++++ yandex_wiki.py | 125 ++++++++ 11 files changed, 1898 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 supabase.py create mode 100644 wiki_auth.py create mode 100644 wiki_check_slugs.py create mode 100644 wiki_embeddings.py create mode 100644 wiki_sync.py create mode 100644 wiki_tree_crawler.py create mode 100644 yandex_wiki.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2b19bb7 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Яндекс OAuth-токен (работает для Трекера и Вики) +YT=y0_... +ORG_ID=7405124 + +# Supabase +SUPABASE_HOST=aws-1-eu-north-1.pooler.supabase.com +SUPABASE_PORT=5432 +SUPABASE_USER=postgres.xeakxxnriopsmaxdioke +SUPABASE_PASSWORD=YmyO2rmahTkfdV97 +SUPABASE_DB=postgres + +# OpenAI +OPENAI_API_KEY=sk-... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d50a09f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +__pycache__/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..559cc29 --- /dev/null +++ b/README.md @@ -0,0 +1,141 @@ +# wiki_embedding + +Семантический поиск по страницам Яндекс Вики через pgvector + OpenAI embeddings. + +## Что делает + +- Обходит раздел «Управление Аналитики» Яндекс Вики через API и Playwright +- Сохраняет содержимое страниц в Supabase (PostgreSQL) +- Генерирует векторные embeddings через OpenAI `text-embedding-3-small` +- Позволяет делать семантический поиск по ~700 страницам на русском и английском + +## Стек + +| Компонент | Технология | +|-----------|-----------| +| Хранилище | Supabase (PostgreSQL 17) | +| Векторный поиск | pgvector, cosine similarity (`<=>`) | +| Embeddings | OpenAI `text-embedding-3-small` (1536 dims) | +| Wiki API | Яндекс Вики API v1 (`api.wiki.yandex.net/v1`) | +| Браузерный краулер | Playwright + Chromium headless | +| Язык | Python 3.12 | + +## Структура файлов + +``` +wiki_embedding/ +├── wiki_sync.py # Главный скрипт синхронизации (API → Supabase → embeddings) +├── wiki_embeddings.py # Генерация embeddings и семантический поиск +├── yandex_wiki.py # Краулер через Яндекс Вики API v1 +├── wiki_tree_crawler.py # Playwright-краулер для страниц с {% tree %} +├── wiki_check_slugs.py # Проверка покрытия ROOT_SLUGS +├── wiki_auth.py # Сохранение браузерной сессии для Playwright +├── supabase.py # SupabaseManager — подключение и операции с БД +├── requirements.txt # Зависимости +└── .env # Credentials (не коммитить) +``` + +## Настройка + +### 1. Установить зависимости + +```bash +pip install -r requirements.txt +playwright install chromium +``` + +### 2. Создать .env + +``` +# Яндекс OAuth-токен (работает для Трекера и Вики) +YT=y0_... +ORG_ID=7405124 + +# Supabase +SUPABASE_HOST=aws-1-eu-north-1.pooler.supabase.com +SUPABASE_PORT=5432 +SUPABASE_USER=postgres.xeakxxnriopsmaxdioke +SUPABASE_PASSWORD=... +SUPABASE_DB=postgres + +# OpenAI +OPENAI_API_KEY=sk-... +``` + +### 3. Сохранить браузерную сессию (один раз) + +```bash +python wiki_auth.py +``` + +Откроется браузер — залогинься в wiki.yandex.ru, нажми Enter. + +## Запуск + +### Полная синхронизация + +```bash +python wiki_sync.py +``` + +Обходит все 642 slug-а из `ROOT_SLUGS`, обновляет `wiki_pages`, генерирует embeddings. + +### Обнаружить новые страницы через Playwright + +```bash +python wiki_tree_crawler.py +``` + +Открывает страницы с `{% tree %}` в headless-браузере, находит дочерние slug-и которых нет в базе. + +### Проверить покрытие + +```bash +python wiki_check_slugs.py +``` + +Показывает какие `{% tree %}` страницы не покрыты ROOT_SLUGS. + +### Семантический поиск (из кода) + +```python +import sys +sys.path.insert(0, '/Users/at/code/wiki_embedding') +from supabase import SupabaseManager +from wiki_embeddings import search + +db = SupabaseManager() +db.connect() +results = search(db, 'твой запрос', limit=5) +for r in results: + print(r['similarity'], r['title']) + print(r['content_text'][:500]) +db.close() +``` + +Similarity > 0.5 — хорошее совпадение. + +## Cron (ежедневная синхронизация) + +``` +0 3 * * * cd /Users/at/code/wiki_embedding && /usr/bin/python3 wiki_sync.py >> logs/wiki_sync.log 2>&1 +``` + +## Как устроен поиск + +1. При индексировании каждая страница → вектор через OpenAI (title + content) +2. При поиске запрос → вектор тем же способом +3. pgvector находит страницы с минимальным косинусным расстоянием (`<=>`) +4. Возвращается `1 - distance` как similarity (0..1) + +Точный поиск без индекса (IVFFlat не используется — при < 10k векторов даёт плохие результаты). + +## Схема БД + +```sql +-- Содержимое страниц +wiki_pages (id, slug, title, page_type, modified_at, content_hash, value JSONB) + +-- Векторные embeddings +wiki_embeddings (id, slug, title, content_text, content_hash, embedding vector(1536)) +``` diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..21450c9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +openai==2.17.0 +playwright==1.58.0 +psycopg2-binary==2.9.11 +python-dotenv==1.2.1 +requests==2.32.5 +pendulum==3.1.0 diff --git a/supabase.py b/supabase.py new file mode 100644 index 0000000..4ba0240 --- /dev/null +++ b/supabase.py @@ -0,0 +1,443 @@ +""" +Модуль для работы с Supabase (PostgreSQL) +""" +import os +from pathlib import Path +import psycopg2 +from dotenv import load_dotenv + +load_dotenv(Path(__file__).parent / '.env') + + +class SupabaseManager: + """Менеджер для работы с Supabase""" + + def __init__(self): + """Инициализация подключения к Supabase""" + self.host = os.getenv('SUPABASE_HOST') + self.port = os.getenv('SUPABASE_PORT') + self.user = os.getenv('SUPABASE_USER') + self.password = os.getenv('SUPABASE_PASSWORD') + self.database = os.getenv('SUPABASE_DB') + + self.conn = None + self.cursor = None + + def connect(self): + """Подключение к базе данных""" + try: + self.conn = psycopg2.connect( + host=self.host, + port=self.port, + user=self.user, + password=self.password, + database=self.database, + sslmode='require', + ) + self.cursor = self.conn.cursor() + + # Проверка версии + self.cursor.execute('SELECT version();') + db_version = self.cursor.fetchone() + print(f"Подключение к Supabase успешно!\nВерсия PostgreSQL: {db_version[0]}") + return True + except Exception as e: + print(f"✗ Ошибка подключения к Supabase: {e}") + return False + + def close(self): + """Закрытие соединения""" + if self.cursor: + self.cursor.close() + if self.conn: + self.conn.close() + print("✓ Соединение с базой данных закрыто") + + # ============================================================================ + # ПРОВЕРКА ТАБЛИЦ + # ============================================================================ + + def table_exists(self, table_name): + """Проверка существования таблицы""" + self.cursor.execute(""" + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = %s + ); + """, (table_name,)) + return self.cursor.fetchone()[0] + + def table_has_data(self, table_name): + """Проверка, что таблица не пустая""" + self.cursor.execute(f"SELECT EXISTS (SELECT 1 FROM {table_name} LIMIT 1);") + return self.cursor.fetchone()[0] + + # ============================================================================ + # СОЗДАНИЕ ТАБЛИЦ + # ============================================================================ + + def create_employee_table(self): + """Создание таблицы employee""" + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS employee ( + id SERIAL PRIMARY KEY, + pg_load_dttm TIMESTAMPTZ NOT NULL, + yatracker_employee_id TEXT NOT NULL, + value JSONB NOT NULL + ); + """) + self.conn.commit() + print("Таблица 'employee' создана") + + def create_tasks_table(self): + """Создание таблицы tasks""" + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS tasks ( + id SERIAL PRIMARY KEY, + pg_load_dttm TIMESTAMPTZ NOT NULL, + queue TEXT, + task_key TEXT NOT NULL, + task_title TEXT, + status_name TEXT, + created_at_dttm TIMESTAMPTZ, + updated_at_dttm TIMESTAMPTZ, + status_start_dttm TIMESTAMPTZ, + value JSONB NOT NULL + ); + """) + self.conn.commit() + print("Таблица 'tasks' создана") + + def create_employee_info_table(self): + """Создание таблицы employee_info""" + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS employee_info ( + id SERIAL PRIMARY KEY, + pg_load_dttm TIMESTAMPTZ NOT NULL, + column_d TEXT, + column_n TEXT + ); + """) + self.conn.commit() + print("Таблица 'employee_info' создана") + + # ============================================================================ + # ЗАГРУЗКА ДАННЫХ + # ============================================================================ + + def load_employee_data(self, employee_df): + """Загрузка данных сотрудников в базу""" + if len(employee_df) > 0: + print("\nЗагружаем данные в таблицу 'employee'...") + for _, row in employee_df.iterrows(): + self.cursor.execute(""" + INSERT INTO employee (pg_load_dttm, yatracker_employee_id, value) + VALUES (%s, %s, %s::jsonb) + """, (row['pg_load_dttm'], row['yatracker_employee_id'], row['value'])) + self.conn.commit() + print(f"✓ Загружено {len(employee_df)} записей в таблицу 'employee'") + else: + print("\nНет данных для загрузки в таблицу 'employee'") + + def load_tasks_data(self, tasks_df): + """Загрузка данных задач в базу""" + if len(tasks_df) > 0: + print("\nЗагружаем данные в таблицу 'tasks'...") + for _, row in tasks_df.iterrows(): + self.cursor.execute(""" + INSERT INTO tasks (pg_load_dttm, queue, task_key, task_title, status_name, + created_at_dttm, updated_at_dttm, status_start_dttm, value) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb) + """, ( + row['pg_load_dttm'], + row['queue'], + row['task_key'], + row['task_title'], + row['status_name'], + row['created_at_dttm'], + row['updated_at_dttm'], + row['status_start_dttm'], + row['value'] + )) + self.conn.commit() + print(f"✓ Загружено {len(tasks_df)} записей в таблицу 'tasks'") + else: + print("\nНет данных для загрузки в таблицу 'tasks'") + + def load_employee_info_data(self, employee_info_df): + """Загрузка данных employee_info в базу""" + if len(employee_info_df) > 0: + print("\nЗагружаем данные в таблицу 'employee_info'...") + for _, row in employee_info_df.iterrows(): + self.cursor.execute(""" + INSERT INTO employee_info (pg_load_dttm, column_d, column_n) + VALUES (%s, %s, %s) + """, ( + row['pg_load_dttm'], + row['column_d'], + row['column_n'] + )) + self.conn.commit() + print(f"✓ Загружено {len(employee_info_df)} записей в таблицу 'employee_info'") + else: + print("\nНет данных для загрузки в таблицу 'employee_info'") + + # ============================================================================ + # ОСНОВНАЯ ЛОГИКА ПРОВЕРКИ И ЗАГРУЗКИ + # ============================================================================ + + def check_and_prepare_tables(self): + """ + Проверка существования таблиц и создание при необходимости + + Returns: + dict: Словарь с флагами наличия данных для каждой таблицы + """ + tables_status = {} + + # Проверяем таблицу employee + employee_exists = self.table_exists('employee') + employee_has_data = False + + if employee_exists: + employee_has_data = self.table_has_data('employee') + if employee_has_data: + print("Таблица 'employee' существует и содержит данные. Пропускаем загрузку.") + else: + print("Таблица 'employee' существует, но пустая.") + else: + self.create_employee_table() + + tables_status['employee'] = employee_has_data + + # Проверяем таблицу tasks + tasks_exists = self.table_exists('tasks') + tasks_has_data = False + + if tasks_exists: + tasks_has_data = self.table_has_data('tasks') + if tasks_has_data: + print("Таблица 'tasks' существует и содержит данные. Пропускаем загрузку.") + else: + print("Таблица 'tasks' существует, но пустая.") + else: + self.create_tasks_table() + + tables_status['tasks'] = tasks_has_data + + # Проверяем таблицу employee_info + employee_info_exists = self.table_exists('employee_info') + employee_info_has_data = False + + if employee_info_exists: + employee_info_has_data = self.table_has_data('employee_info') + if employee_info_has_data: + print("Таблица 'employee_info' существует и содержит данные. Пропускаем загрузку.") + else: + print("Таблица 'employee_info' существует, но пустая.") + else: + self.create_employee_info_table() + + tables_status['employee_info'] = employee_info_has_data + + return tables_status + + def load_all_data(self, tables_status, employee=None, tasks=None, employee_info=None): + """ + Загрузка всех данных в базу + + Args: + tables_status (dict): Статус таблиц из check_and_prepare_tables + employee (pd.DataFrame): Данные сотрудников + tasks (pd.DataFrame): Данные задач + employee_info (pd.DataFrame): Информация о сотрудниках + """ + if employee is not None and not tables_status['employee']: + self.load_employee_data(employee) + elif tables_status['employee']: + print("\nТаблица 'employee' уже содержит данные, пропускаем загрузку") + + if tasks is not None and not tables_status['tasks']: + self.load_tasks_data(tasks) + elif tables_status['tasks']: + print("\nТаблица 'tasks' уже содержит данные, пропускаем загрузку") + + if employee_info is not None and not tables_status['employee_info']: + self.load_employee_info_data(employee_info) + elif tables_status['employee_info']: + print("\nТаблица 'employee_info' уже содержит данные, пропускаем загрузку") + + # ============================================================================ + # ПОЛУЧЕНИЕ ДАННЫХ ДЛЯ АНАЛИЗА + # ============================================================================ + + def get_tasks_for_analysis(self, task_keys=None): + """ + Получение задач для анализа с описанием и исполнителем + + Args: + task_keys (list): Список ключей задач для фильтрации (опционально) + + Returns: + list: Список словарей с данными задач + """ + if task_keys: + # Формируем строку с ключами для SQL IN + keys_str = ", ".join([f"'{key}'" for key in task_keys]) + query = f""" + SELECT * + FROM + (SELECT + t.queue, + t.task_key, + t.task_title, + t.value -> 'description' as description, + t.value -> 'assignee' ->> 'display' as assignee, + row_number() over (partition by task_key order by updated_at_dttm desc) as rn + FROM tasks t + WHERE task_key IN ({keys_str})) T1 + WHERE rn = 1; + """ + else: + query = """ + SELECT + t.queue, + t.task_key, + t.task_title, + t.value -> 'description' as description, + t.value -> 'assignee' ->> 'display' as assignee + FROM tasks t + ORDER BY t.updated_at_dttm DESC + LIMIT 100; + """ + + self.cursor.execute(query) + columns = [desc[0] for desc in self.cursor.description] + rows = self.cursor.fetchall() + + tasks = [] + for row in rows: + task_dict = dict(zip(columns, row)) + tasks.append(task_dict) + + return tasks + + # ============================================================================ + # WIKI PAGES + # ============================================================================ + + def create_wiki_pages_table(self): + """Создание таблицы wiki_pages""" + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS wiki_pages ( + id SERIAL PRIMARY KEY, + pg_load_dttm TIMESTAMPTZ NOT NULL, + slug TEXT NOT NULL, + wiki_page_id INTEGER, + title TEXT, + page_type TEXT, + modified_at TIMESTAMPTZ, + content_hash TEXT NOT NULL, + value JSONB NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_wiki_pages_slug + ON wiki_pages (slug); + CREATE INDEX IF NOT EXISTS idx_wiki_pages_load + ON wiki_pages (pg_load_dttm DESC); + """) + self.conn.commit() + print("Таблица 'wiki_pages' создана") + + def get_latest_hashes(self) -> dict[str, str]: + """Получить последний content_hash для каждого slug.""" + self.cursor.execute(""" + SELECT DISTINCT ON (slug) slug, content_hash + FROM wiki_pages + ORDER BY slug, pg_load_dttm DESC; + """) + return {row[0]: row[1] for row in self.cursor.fetchall()} + + def upsert_wiki_pages(self, pages: list[dict]) -> dict: + """ + Вставить новые и изменённые страницы. + + Логика: сравниваем content_hash с последним сохранённым. + Если хеш совпадает — пропускаем. Если новый или изменился — вставляем. + + Returns: + dict: {'inserted': int, 'unchanged': int} + """ + import json + + if not pages: + return {'inserted': 0, 'unchanged': 0} + + if not self.table_exists('wiki_pages'): + self.create_wiki_pages_table() + + latest_hashes = self.get_latest_hashes() + + inserted = 0 + unchanged = 0 + + for page in pages: + slug = page['slug'] + new_hash = page['content_hash'] + stored_hash = latest_hashes.get(slug) + + if stored_hash == new_hash: + unchanged += 1 + continue + + value = { + 'slug': slug, + 'wiki_page_id': page.get('wiki_page_id'), + 'title': page.get('title'), + 'page_type': page.get('page_type'), + 'modified_at': str(page.get('modified_at') or ''), + 'content': page.get('content', ''), + } + + self.cursor.execute(""" + INSERT INTO wiki_pages + (pg_load_dttm, slug, wiki_page_id, title, page_type, modified_at, content_hash, value) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s::jsonb) + """, ( + page['pg_load_dttm'], + slug, + page.get('wiki_page_id'), + page.get('title'), + page.get('page_type'), + page.get('modified_at'), + new_hash, + json.dumps(value, ensure_ascii=False), + )) + inserted += 1 + + self.conn.commit() + return {'inserted': inserted, 'unchanged': unchanged} + + def execute_sql_file(self, sql_file_path): + """ + Выполнение SQL запроса из файла + + Args: + sql_file_path (str): Путь к файлу с SQL запросом + + Returns: + list: Список словарей с результатами + """ + with open(sql_file_path, 'r', encoding='utf-8') as f: + query = f.read() + + self.cursor.execute(query) + columns = [desc[0] for desc in self.cursor.description] + rows = self.cursor.fetchall() + + results = [] + for row in rows: + result_dict = dict(zip(columns, row)) + results.append(result_dict) + + return results diff --git a/wiki_auth.py b/wiki_auth.py new file mode 100644 index 0000000..0b94627 --- /dev/null +++ b/wiki_auth.py @@ -0,0 +1,36 @@ +""" +Сохранить сессию wiki.yandex.ru для последующего использования Playwright. + +Запуск (один раз): + python wiki_auth.py + +Откроется браузер — залогинься в wiki.yandex.ru, затем нажми Enter в терминале. +Cookies сохранятся в wiki_auth.json. +""" +from playwright.sync_api import sync_playwright +from pathlib import Path + +AUTH_FILE = Path(__file__).parent / 'wiki_auth.json' + + +def main(): + with sync_playwright() as p: + browser = p.chromium.launch(headless=False) + context = browser.new_context() + page = context.new_page() + + print('Открываю wiki.yandex.ru...') + page.goto('https://wiki.yandex.ru') + + print() + print('Залогинься в браузере, затем вернись сюда и нажми Enter.') + input('Нажми Enter когда будешь залогинен: ') + + context.storage_state(path=str(AUTH_FILE)) + browser.close() + + print(f'✓ Сессия сохранена в {AUTH_FILE}') + + +if __name__ == '__main__': + main() diff --git a/wiki_check_slugs.py b/wiki_check_slugs.py new file mode 100644 index 0000000..b8532e6 --- /dev/null +++ b/wiki_check_slugs.py @@ -0,0 +1,78 @@ +""" +Проверка покрытия ROOT_SLUGS: находит страницы в wiki_pages с {% tree %}, +которые не добавлены в ROOT_SLUGS — у них могут быть необнаруженные дочерние страницы. + +Запуск: + python wiki_check_slugs.py +""" +import sys +from pathlib import Path +from dotenv import load_dotenv + +load_dotenv(Path(__file__).parent / '.env') + +sys.path.insert(0, str(Path(__file__).parent)) +from supabase import SupabaseManager + +# ROOT_SLUGS из wiki_sync.py — поддерживай синхронно +from wiki_sync import ROOT_SLUGS + + +def main(): + db = SupabaseManager() + if not db.connect(): + sys.exit(1) + + try: + # Все slug-и в базе + db.cursor.execute("SELECT slug FROM wiki_pages ORDER BY slug") + all_slugs = {row[0] for row in db.cursor.fetchall()} + + # Страницы с {% tree %} — у них могут быть необнаруженные дети + db.cursor.execute(""" + SELECT slug, value->>'title' AS title + FROM wiki_pages + WHERE value->>'content' LIKE '%{%% tree%%}%' + ORDER BY slug + """) + tree_pages = db.cursor.fetchall() + + finally: + db.close() + + root_slugs_set = set(ROOT_SLUGS) + + # Страницы с {% tree %}, которых нет в ROOT_SLUGS + missing_from_roots = [(s, t) for s, t in tree_pages if s not in root_slugs_set] + + # ROOT_SLUGS которых нет в базе (удалены или никогда не синхронизировались) + missing_from_db = root_slugs_set - all_slugs + + print(f'Всего страниц в wiki_pages: {len(all_slugs)}') + print(f'Страниц с {{% tree %}}: {len(tree_pages)}') + print(f'ROOT_SLUGS: {len(ROOT_SLUGS)}') + print() + + if missing_from_roots: + print('⚠️ Страницы с {% tree %}, НЕ добавленные в ROOT_SLUGS:') + print(' (у них могут быть дочерние страницы, которые краулер не найдёт)') + print() + for slug, title in missing_from_roots: + print(f' + {slug}') + print(f' «{title}»') + print(f' https://wiki.yandex.ru/{slug}') + print() + else: + print('✓ Все страницы с {% tree %} уже есть в ROOT_SLUGS') + + if missing_from_db: + print('⚠️ ROOT_SLUGS которых НЕТ в базе (не были синхронизированы или удалены):') + for slug in sorted(missing_from_db): + print(f' - {slug}') + print() + else: + print('✓ Все ROOT_SLUGS присутствуют в базе') + + +if __name__ == '__main__': + main() diff --git a/wiki_embeddings.py b/wiki_embeddings.py new file mode 100644 index 0000000..c6d7725 --- /dev/null +++ b/wiki_embeddings.py @@ -0,0 +1,171 @@ +""" +Семантический поиск по Яндекс Вики через OpenAI embeddings + pgvector. +""" +import re +import os +from pathlib import Path +from dotenv import load_dotenv +from openai import OpenAI + +load_dotenv(Path(__file__).parent / '.env') + +EMBED_MODEL = 'text-embedding-3-small' # 1536 dims, быстро и дёшево + +_client = None + + +def _openai() -> OpenAI: + global _client + if _client is None: + _client = OpenAI(api_key=os.getenv('OPENAI_API_KEY')) + return _client + + +def clean_wiki_markup(text: str) -> str: + """Убрать wiki-разметку, оставить чистый текст для embedding.""" + if not text: + return '' + # {% ... %} блоки + text = re.sub(r'\{%[^%]*%\}', '', text) + # [[ссылки]] → оставить текст после | + text = re.sub(r'\[\[([^\]|]*)\|([^\]]*)\]\]', r'\2', text) + text = re.sub(r'\[\[([^\]]*)\]\]', r'\1', text) + # ((url текст)) → текст + text = re.sub(r'\(\(https?://\S+\s+([^)]+)\)\)', r'\1', text) + text = re.sub(r'\(\(https?://\S+\)\)', '', text) + # Markdown-разметка + text = re.sub(r'\*{1,3}([^*]+)\*{1,3}', r'\1', text) + text = re.sub(r'_{1,2}([^_]+)_{1,2}', r'\1', text) + # Таблицы и спец-символы wiki + text = re.sub(r'[#|]{2,}', '\n', text) + text = re.sub(r'^\s*[#>]+\s*', '', text, flags=re.MULTILINE) + # Лишние пробелы + text = re.sub(r'\n{3,}', '\n\n', text) + return text.strip() + + +def embed_text(text: str) -> list[float]: + """Получить embedding для текста через OpenAI API.""" + import openai as _openai_module + # Начинаем с 15000 символов, при ошибке обрезаем вдвое + limit = 15000 + while limit >= 1000: + try: + resp = _openai().embeddings.create(model=EMBED_MODEL, input=text[:limit]) + return resp.data[0].embedding + except _openai_module.BadRequestError as e: + if 'maximum context length' in str(e): + limit = limit // 2 + continue + raise + raise ValueError(f'Не удалось уложиться в лимит токенов даже при 1000 символах') + + +def embed_page(page: dict) -> list[float]: + """Сгенерировать embedding для страницы (title + content).""" + title = page.get('title', '') + content = clean_wiki_markup(page.get('content', '') or '') + combined = f'{title}\n\n{content}' + return embed_text(combined) + + +def upsert_embeddings(db, pages: list[dict]) -> dict: + """ + Сгенерировать и сохранить embeddings для новых/изменённых страниц. + + Пропускает страницы, у которых content_hash не изменился. + + Returns: + dict: {'embedded': int, 'skipped': int} + """ + import json + import pendulum + + if not pages: + return {'embedded': 0, 'skipped': 0} + + # Получить текущие хеши из wiki_embeddings + db.cursor.execute('SELECT slug, content_hash FROM wiki_embeddings') + stored = {row[0]: row[1] for row in db.cursor.fetchall()} + + embedded = 0 + skipped = 0 + + for page in pages: + slug = page['slug'] + new_hash = page['content_hash'] + content = page.get('content', '') or '' + + if not content.strip(): + skipped += 1 + continue + + if stored.get(slug) == new_hash: + skipped += 1 + continue + + print(f' ↑ embedding: {slug}') + content_text = clean_wiki_markup(content) + vector = embed_page({'title': page.get('title', ''), 'content': content}) + + db.cursor.execute(""" + INSERT INTO wiki_embeddings + (pg_load_dttm, slug, title, content_text, content_hash, embedding) + VALUES (%s, %s, %s, %s, %s, %s) + ON CONFLICT (slug) DO UPDATE SET + pg_load_dttm = EXCLUDED.pg_load_dttm, + title = EXCLUDED.title, + content_text = EXCLUDED.content_text, + content_hash = EXCLUDED.content_hash, + embedding = EXCLUDED.embedding + """, ( + pendulum.now('Europe/Moscow'), + slug, + page.get('title', ''), + content_text, + new_hash, + json.dumps(vector), + )) + db.conn.commit() + embedded += 1 + + return {'embedded': embedded, 'skipped': skipped} + + +def search(db, query: str, limit: int = 5) -> list[dict]: + """ + Семантический поиск по wiki_embeddings. + + Args: + db: подключённый SupabaseManager + query: текстовый запрос на любом языке + limit: кол-во результатов + + Returns: + Список {'slug', 'title', 'similarity', 'content_text'} + """ + import json + + query_vec = embed_text(query) + + db.cursor.execute(""" + SELECT + slug, + title, + content_text, + 1 - (embedding <=> %s::vector) AS similarity + FROM wiki_embeddings + ORDER BY embedding <=> %s::vector + LIMIT %s + """, (json.dumps(query_vec), json.dumps(query_vec), limit)) + + rows = db.cursor.fetchall() + return [ + { + 'slug': row[0], + 'title': row[1], + 'content_text': row[2], + 'similarity': round(float(row[3]), 4), + } + for row in rows + ] diff --git a/wiki_sync.py b/wiki_sync.py new file mode 100644 index 0000000..7dab863 --- /dev/null +++ b/wiki_sync.py @@ -0,0 +1,737 @@ +""" +Ежедневная синхронизация Яндекс Вики → Supabase. + +Запуск: + python wiki_sync.py + +Cron (каждый день в 03:00): + 0 3 * * * cd /Users/at/python/tracker-checker && /usr/bin/python3 wiki_sync.py >> logs/wiki_sync.log 2>&1 +""" +import os +import sys +from pathlib import Path +from dotenv import load_dotenv +import pendulum + +from yandex_wiki import crawl +from supabase import SupabaseManager +from wiki_embeddings import upsert_embeddings + +load_dotenv(Path(__file__).parent / '.env') + +# ── Настройки ──────────────────────────────────────────────────────────────── + +API_KEY = os.getenv('API_KEY') or os.getenv('YT') +ORG_ID = os.getenv('ORG_ID') or os.getenv('ORG') + +# Корневые разделы для обхода — добавляй нужные slug-и +ROOT_SLUGS = [ + # Корневой раздел + 'upravlenie-analitiki', + # Процессы УА (дети рендерятся через {% tree %}, добавляй slug вручную когда откроешь в браузере) + 'upravlenie-analitiki/processy-ua', + # Продукты УА и все найденные подразделы + 'upravlenie-analitiki/produkty-ua', + 'upravlenie-analitiki/bdd', + 'upravlenie-analitiki/analiticheskaja-podderzhka', + 'upravlenie-analitiki/onboarding', + 'upravlenie-analitiki/antifraud', + 'upravlenie-analitiki/research-analytics', + 'upravlenie-analitiki/dlja-sotrudnikov-ua', + 'upravlenie-analitiki/operacionnaja-analitika', + 'upravlenie-analitiki/biznes-analitika', + 'upravlenie-analitiki/produktovaja-analitika-kpa', + 'upravlenie-analitiki/produktovaja-analitika-lojalnosti', + 'upravlenie-analitiki/logirovanie-mp', + 'upravlenie-analitiki/produktovaja-analitika-kpo', + # Добавленные вручную по URL + 'upravlenie-analitiki/platforma-po-av-testam', + 'upravlenie-analitiki/kak-podat-zajavku-analitikam', + 'upravlenie-analitiki/put-novichka', + 'upravlenie-analitiki/dutykotiki', + 'upravlenie-analitiki/processy-ua/dq-process', + 'upravlenie-analitiki/processy-ua/emk-process', + 'upravlenie-analitiki/processy-ua/process-revju-issledovatelskix-zadach', + 'upravlenie-analitiki/platforma-po-av-testam/testirovanie', + # Аналитическая поддержка (дети через {% tree %}) + 'upravlenie-analitiki/analiticheskaja-podderzhka/ab-testy', + 'upravlenie-analitiki/analiticheskaja-podderzhka/komanda', + 'upravlenie-analitiki/analiticheskaja-podderzhka/dashbordy', + 'upravlenie-analitiki/analiticheskaja-podderzhka/issledovanija-i-logi', + 'upravlenie-analitiki/analiticheskaja-podderzhka/ad-hoc', + # Добавлено автоматически через wiki_tree_crawler.py + 'upravlenie-analitiki/antifraud/bonusy-nachislennye-sotrudnikami-vruchnuju', + 'upravlenie-analitiki/antifraud/bonusy-za-jekologicheskie-akcii', + 'upravlenie-analitiki/antifraud/frod-na-kso-krazhi', + 'upravlenie-analitiki/antifraud/frod-ot-ujazvimosti-mp-v-funkcionalnosti-skanirujj', + 'upravlenie-analitiki/antifraud/frod-s-fejjkovymi-zakazami', + 'upravlenie-analitiki/antifraud/frod-v-chaevyx', + 'upravlenie-analitiki/antifraud/frod-v-limitax', + 'upravlenie-analitiki/antifraud/frod-v-limitax/frod-v-limitax.-ruchnye-ogranichenija-menedzherov-', + 'upravlenie-analitiki/antifraud/frod-v-otmenjonnyx-zakazax', + 'upravlenie-analitiki/antifraud/frod-v-otmenjonnyx-zakazax/zakazy-cherez-storonnie-marketplejjsy', + 'upravlenie-analitiki/antifraud/frod-v-programmax-lojalnosti', + 'upravlenie-analitiki/antifraud/frod-v-programmax-lojalnosti/primenenie-kupona-s-limitom-ot-summy-n-rublejj-k-m', + 'upravlenie-analitiki/antifraud/frod-v-programmax-lojalnosti/xaljavshhiki', + 'upravlenie-analitiki/antifraud/frod-v-referalnojj-programme', + 'upravlenie-analitiki/antifraud/frod-v-rejjtingax-i-otzyvax.-klientskijj-put-posta', + 'upravlenie-analitiki/antifraud/frod-v-vozvratax', + 'upravlenie-analitiki/antifraud/frod-v-vozvratax/dop.materialy', + 'upravlenie-analitiki/antifraud/jekonomika-proekta-antifrod', + 'upravlenie-analitiki/antifraud/jekonomika-proekta-antifrod/jekonomika-antifroda-v-chasti-limitov', + 'upravlenie-analitiki/antifraud/jekonomika-proekta-antifrod/kak-schitat-soxranjonnye-antifrodom-sredstva', + 'upravlenie-analitiki/antifraud/jekonomika-proekta-antifrod/primenenie-kuponov-na-pervyjj-zakaz-bolee-1-raza', + 'upravlenie-analitiki/antifraud/jekonomika-proekta-antifrod/soxranennye-dengi-v-chasti-vozvratov-2.0', + 'upravlenie-analitiki/antifraud/klasterizacija-i-klassifikacija-klientov-v-vozvrat', + 'upravlenie-analitiki/antifraud/lzhenovichki', + 'upravlenie-analitiki/antifraud/lzhenovichki/analitika-vremeni-zhizni-lzhe-novichkov', + 'upravlenie-analitiki/antifraud/massovost-vozvratov-pervyx-zakazov-po-partnerskojj', + 'upravlenie-analitiki/antifraud/raschet-marzhinalnosti-po-froderam', + 'upravlenie-analitiki/antifraud/rasskazhi-o-frode', + 'upravlenie-analitiki/antifraud/skanirovanie-tovarov-s-dvojjnym-shtrixkodom', + 'upravlenie-analitiki/antifraud/summarnye-dannye-za-24-jj-god-po-obemu-vozvratovbo', + 'upravlenie-analitiki/bdd/blok-analiticheskaja-kultura-i-standarty', + 'upravlenie-analitiki/bdd/blok-dannye', + 'upravlenie-analitiki/bdd/blok-infrastruktura', + 'upravlenie-analitiki/bdd/manifest-bdd', + 'upravlenie-analitiki/bdd/porazgonjat', + 'upravlenie-analitiki/bdd/porazgonjat/perechen-slozhnyx-proektov', + 'upravlenie-analitiki/bi-analitiki', + 'upravlenie-analitiki/bi-analitiki/arxiv', + 'upravlenie-analitiki/bi-analitiki/arxivacija-otchetov', + 'upravlenie-analitiki/bi-analitiki/kritichnost-dashbordov---standart', + 'upravlenie-analitiki/bi-analitiki/obeshhanija-komandy-bi', + 'upravlenie-analitiki/bi-analitiki/onbording-novichkov', + 'upravlenie-analitiki/bi-analitiki/opisanie-otchetov', + 'upravlenie-analitiki/bi-analitiki/plany-razvitija', + 'upravlenie-analitiki/bi-analitiki/poleznye-resursy', + 'upravlenie-analitiki/bi-analitiki/self-servisy-ua-vv-self-service', + 'upravlenie-analitiki/bi-analitiki/standarty-komandy-bi', + 'upravlenie-analitiki/bi-analitiki/vnutrennjaja-dokumentacija', + 'upravlenie-analitiki/bi-analitiki/xjendover-dashbordov-v-bi-komandu-ot-analitikov-ua', + 'upravlenie-analitiki/biznes-analitika/zadachi-1', + 'upravlenie-analitiki/dlja-sotrudnikov-ua/benefity-dlja-sotrudnikov', + 'upravlenie-analitiki/dlja-sotrudnikov-ua/dlja-tex-kto-rabotaet-vne-rf', + 'upravlenie-analitiki/dlja-sotrudnikov-ua/instrukcija-po-voinskomu-uchetu', + 'upravlenie-analitiki/dlja-sotrudnikov-ua/poleznye-ssylki-dlja-obuchenija', + 'upravlenie-analitiki/dlja-sotrudnikov-ua/uxod-v-otpusk-i-na-bolnichnyjj', + 'upravlenie-analitiki/dutykotiki/rabota-dezhurnogo', + 'upravlenie-analitiki/dutykotiki/rabota-dezhurnogo/obuchenie-po-ponjatnomu-tekstu', + 'upravlenie-analitiki/dutykotiki/rabota-dezhurnogo/rabota-s-jatrekerom', + 'upravlenie-analitiki/dutykotiki/rabota-dezhurnogo/shablon-dlja-stati-po-vygruzke', + 'upravlenie-analitiki/dutykotiki/segmentacii', + 'upravlenie-analitiki/dutykotiki/segmentacii/segmentacija-clc', + 'upravlenie-analitiki/dutykotiki/vygruzki', + 'upravlenie-analitiki/dutykotiki/vygruzki/11', + 'upravlenie-analitiki/dutykotiki/vygruzki/1c-napitki', + 'upravlenie-analitiki/dutykotiki/vygruzki/2f70ef29029b', + 'upravlenie-analitiki/dutykotiki/vygruzki/3-gruppa-roznicy-pokazateli-lp-i-abonement', + 'upravlenie-analitiki/dutykotiki/vygruzki/414dcd3c2b4c', + 'upravlenie-analitiki/dutykotiki/vygruzki/4814eb73fc5e', + 'upravlenie-analitiki/dutykotiki/vygruzki/6a6a82721362', + 'upravlenie-analitiki/dutykotiki/vygruzki/6ff40875fcba', + 'upravlenie-analitiki/dutykotiki/vygruzki/a1e2856653b5', + 'upravlenie-analitiki/dutykotiki/vygruzki/adresa-kontragentov-postavshhikov', + 'upravlenie-analitiki/dutykotiki/vygruzki/aktualnyjj-spisok-pozicijj-texnologa', + 'upravlenie-analitiki/dutykotiki/vygruzki/analitika-novyx-torgovyx-tochek', + 'upravlenie-analitiki/dutykotiki/vygruzki/analitika-po-magazinu-7514', + 'upravlenie-analitiki/dutykotiki/vygruzki/analitika-po-produktam-iz-rastitelnojj-linejjki', + 'upravlenie-analitiki/dutykotiki/vygruzki/analitika-po-zakazam-na-zavtra', + 'upravlenie-analitiki/dutykotiki/vygruzki/analitika-proniknovenija-kategorijj-v-chek', + 'upravlenie-analitiki/dutykotiki/vygruzki/analiz-aktivnyx-darkstorov-po-otgruzkam-kontragent', + 'upravlenie-analitiki/dutykotiki/vygruzki/analiz-aktivnyx-darkstorov-po-otgruzkam-kontragent-19-11', + 'upravlenie-analitiki/dutykotiki/vygruzki/analiz-kommunikacijj-dlja-olesi-kashicynojj-b2b', + 'upravlenie-analitiki/dutykotiki/vygruzki/analiz-ottoka', + 'upravlenie-analitiki/dutykotiki/vygruzki/analiz-pokupatelejj-assortimenta-podgruppy-morozhe', + 'upravlenie-analitiki/dutykotiki/vygruzki/apteki-kol-vo-gen.zakazov-vyruchka-s-vozvratami-i-', + 'upravlenie-analitiki/dutykotiki/vygruzki/asr-po-kartam', + 'upravlenie-analitiki/dutykotiki/vygruzki/assortiment-1s-napitki-alkogol', + 'upravlenie-analitiki/dutykotiki/vygruzki/assortiment-kategorii-supermarket-s-prinadlezhnost', + 'upravlenie-analitiki/dutykotiki/vygruzki/assortiment-tovarov', + 'upravlenie-analitiki/dutykotiki/vygruzki/avgmedianchecks', + 'upravlenie-analitiki/dutykotiki/vygruzki/b2b---klienty-s-dvumja-i-bolee-zakazami', + 'upravlenie-analitiki/dutykotiki/vygruzki/barista-i-dr.dolzhnosti-kotorye-rabotali-s-01.01.2', + 'upravlenie-analitiki/dutykotiki/vygruzki/bonus-bags', + 'upravlenie-analitiki/dutykotiki/vygruzki/bonusnye-karty-sotrudnikov', + 'upravlenie-analitiki/dutykotiki/vygruzki/bonusnye-karty-zapolnivshie-formu-registracii-dlja', + 'upravlenie-analitiki/dutykotiki/vygruzki/bonusy-za-utilizaciju-s-telefonami-i-kartami-pokup', + 'upravlenie-analitiki/dutykotiki/vygruzki/ceny-tovarov-dobrojj-polki', + 'upravlenie-analitiki/dutykotiki/vygruzki/chastota-pokupok-tovarov-v-kategorii-suxofrukty', + 'upravlenie-analitiki/dutykotiki/vygruzki/cheki-s-nds-i-bez-nds-po-partnerskojj-programme-s-', + 'upravlenie-analitiki/dutykotiki/vygruzki/dannye-dlja-rolika-o-vv---otkrytye-tt-novye-goroda', + 'upravlenie-analitiki/dutykotiki/vygruzki/dannye-kogorty-novichkov-po-partnjorskojj-programm', + 'upravlenie-analitiki/dutykotiki/vygruzki/dannye-o-pokupkax-v-magazinax-po-kartamtelefonam', + 'upravlenie-analitiki/dutykotiki/vygruzki/dannye-po-nomenklature', + 'upravlenie-analitiki/dutykotiki/vygruzki/dannye-po-pokupateljam-dobrojj-polki', + 'upravlenie-analitiki/dutykotiki/vygruzki/dannye-po-prodazham-alkogolja-offlajjn', + 'upravlenie-analitiki/dutykotiki/vygruzki/dannye-za-god', + 'upravlenie-analitiki/dutykotiki/vygruzki/data-degustacii-i-prodazhi-po-ee-itogam', + 'upravlenie-analitiki/dutykotiki/vygruzki/daty-podkljuchenija-tt-k-polochnomu-ili-67-grupp-r', + 'upravlenie-analitiki/dutykotiki/vygruzki/dinamika-kart-dlja-kategorii-mjasnye-delikatesy.ko', + 'upravlenie-analitiki/dutykotiki/vygruzki/dinamika-prodazh-desertnojj-i-xlebnojj-kategorii', + 'upravlenie-analitiki/dutykotiki/vygruzki/dlja-informirovanija-roznicy', + 'upravlenie-analitiki/dutykotiki/vygruzki/dolja-tovarov-indilavki-v-korzine-ambassadora', + 'upravlenie-analitiki/dutykotiki/vygruzki/dolja-zakazov-s-suxojj-polkojj-zamorozkojj-sgorjac', + 'upravlenie-analitiki/dutykotiki/vygruzki/dostavka-4-chasa-i-dachnyjj-jekspress---nomera-tel', + 'upravlenie-analitiki/dutykotiki/vygruzki/dovozy', + 'upravlenie-analitiki/dutykotiki/vygruzki/e-maily-tt-gde-est-kassiry', + 'upravlenie-analitiki/dutykotiki/vygruzki/file-grudki-indejjki', + 'upravlenie-analitiki/dutykotiki/vygruzki/frov-vygruzka-vyruchki-po-dnjam', + 'upravlenie-analitiki/dutykotiki/vygruzki/good-polka', + 'upravlenie-analitiki/dutykotiki/vygruzki/httpsbdo.kaiten.ruspace63173card35882806', + 'upravlenie-analitiki/dutykotiki/vygruzki/informacija-o-tt', + 'upravlenie-analitiki/dutykotiki/vygruzki/jekstremalnye-obrashhenija', + 'upravlenie-analitiki/dutykotiki/vygruzki/kart-po-testeram', + 'upravlenie-analitiki/dutykotiki/vygruzki/karty---novichki', + 'upravlenie-analitiki/dutykotiki/vygruzki/karty-b2b-s-limitom', + 'upravlenie-analitiki/dutykotiki/vygruzki/karty-i-zakazy-po-dobrym-pokupkam', + 'upravlenie-analitiki/dutykotiki/vygruzki/karty-po-dobrojj-polke-i-jeko', + 'upravlenie-analitiki/dutykotiki/vygruzki/karty-polzovatelejj-u-kogo-byla-vkljuchena-nastroj', + 'upravlenie-analitiki/dutykotiki/vygruzki/karty-uchastnikov-akcii-knopsy', + 'upravlenie-analitiki/dutykotiki/vygruzki/kategorii-osnovnye-metriki', + 'upravlenie-analitiki/dutykotiki/vygruzki/kogorta-partnjorov-partnjorskojj-programmy-razdele', + 'upravlenie-analitiki/dutykotiki/vygruzki/kol-vo-onlajjnoflajjn-polzovatelejj-po-asr', + 'upravlenie-analitiki/dutykotiki/vygruzki/kol-vo-unikalnyx-kart-pokupatelejj-kosmetiki', + 'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-chekov-kulinarija-vv', + 'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-chekov-s-kso-po-rjadu-magazinov', + 'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-chekov-v-razreze-kass', + 'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-obrashhenijj-iz-arxiva', + 'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-obrashhenijj-po-tegam-smena-adresa-i-o', + 'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-pokupok-torty-pirozhnye-s-13-po-15-fev', + 'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-prodazh-36161-i-35085', + 'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-tt-i-novye-goroda-prisutstvija-po-kodu', + 'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-tt-po-okrugam', + 'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-uchastnikov-blagotvoritelnojj-akcii', + 'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-unikalnyx-kart', + 'upravlenie-analitiki/dutykotiki/vygruzki/kommentarii-dlja-sborshhika.-frov-vesovye', + 'upravlenie-analitiki/dutykotiki/vygruzki/kommentarii-i-zhaloby-k-zakazam-c-14-po-20-avgusta', + 'upravlenie-analitiki/dutykotiki/vygruzki/kommentarii-k-zakazu', + 'upravlenie-analitiki/dutykotiki/vygruzki/kommentarii-o-proizvoditeljax', + 'upravlenie-analitiki/dutykotiki/vygruzki/korzina-pokupatelejj-po-adresu-dostavki', + 'upravlenie-analitiki/dutykotiki/vygruzki/limity-arendy-v-1s-finansy---operacii---registr-sv', + 'upravlenie-analitiki/dutykotiki/vygruzki/metod-avtorizacii-na-kso-kassa-samoobsluzhivanija', + 'upravlenie-analitiki/dutykotiki/vygruzki/molochnye-kategorii-po-vozrastam', + 'upravlenie-analitiki/dutykotiki/vygruzki/morozhenoe', + 'upravlenie-analitiki/dutykotiki/vygruzki/nachislennye-bonusy--cheki', + 'upravlenie-analitiki/dutykotiki/vygruzki/nochnaja-dostavka-zakaz-i-dostavka-osushhestvljali', + 'upravlenie-analitiki/dutykotiki/vygruzki/nomenklatura-chestnyjj-znak-za-2023-g', + 'upravlenie-analitiki/dutykotiki/vygruzki/nomenklatura-veganstvo--prodakt-menedzher', + 'upravlenie-analitiki/dutykotiki/vygruzki/nomenklatury-novinki', + 'upravlenie-analitiki/dutykotiki/vygruzki/nomera-bonusnyx-kart', + 'upravlenie-analitiki/dutykotiki/vygruzki/nomera-bonusnyx-kart-projavljajushhix-aktivnost-v-', + 'upravlenie-analitiki/dutykotiki/vygruzki/nomera-i-daty-dogovorov-b2b', + 'upravlenie-analitiki/dutykotiki/vygruzki/nomera-kart-po-vozrastam-18-24-25-34', + 'upravlenie-analitiki/dutykotiki/vygruzki/nomera-oshibok-oplat-v-obrashhenijax-pokupatelejj', + 'upravlenie-analitiki/dutykotiki/vygruzki/nomerov-bonusnyx-kart-po-tovaru-tvorog-detskijj-ja', + 'upravlenie-analitiki/dutykotiki/vygruzki/nomerov-kuratorov-i-pomoshhnikov-dostavki', + 'upravlenie-analitiki/dutykotiki/vygruzki/novichki', + 'upravlenie-analitiki/dutykotiki/vygruzki/novichki-dlja-darkstorov-gruppy-3', + 'upravlenie-analitiki/dutykotiki/vygruzki/novichki-oflajjn-i-novichki-seti', + 'upravlenie-analitiki/dutykotiki/vygruzki/novichki-v-onlajjne--top-10-v-pervom-cheke-novichk', + 'upravlenie-analitiki/dutykotiki/vygruzki/novinki-v-razbivke-po-mesjacam-kol-vo-tt-i-kol-vo-', + 'upravlenie-analitiki/dutykotiki/vygruzki/obogashhenie-vygruzki-dannymi-tt', + 'upravlenie-analitiki/dutykotiki/vygruzki/obrashhenija-33-tipa-ot-neavtorizovannyx-polzovate', + 'upravlenie-analitiki/dutykotiki/vygruzki/obrashhenija-po-degustacii-v-magazine', + 'upravlenie-analitiki/dutykotiki/vygruzki/obrashhenija-po-moloku', + 'upravlenie-analitiki/dutykotiki/vygruzki/obrashhenija-po-nomeram-kart', + 'upravlenie-analitiki/dutykotiki/vygruzki/obrashhenija-po-syru', + 'upravlenie-analitiki/dutykotiki/vygruzki/obrashhenija-po-upakovke-i-jetiketkam', + 'upravlenie-analitiki/dutykotiki/vygruzki/obrashhenija-pokupatelejj-po-kategorijam-v-razreze', + 'upravlenie-analitiki/dutykotiki/vygruzki/obratnaja-svjaz-po-zootovaram', + 'upravlenie-analitiki/dutykotiki/vygruzki/opredelenie-sotrudnikane-sotrudnika-po-nomeru-kart', + 'upravlenie-analitiki/dutykotiki/vygruzki/otchet-po-medikamentam', + 'upravlenie-analitiki/dutykotiki/vygruzki/otchjot-po-otdelu-bez-upakovki-za-janvar-ijul-2024', + 'upravlenie-analitiki/dutykotiki/vygruzki/otdel-bez-upakovki-srednijj-chek', + 'upravlenie-analitiki/dutykotiki/vygruzki/otgruzki-s-tt-klientov-b2b', + 'upravlenie-analitiki/dutykotiki/vygruzki/otsrochka-platezha-po-ka', + 'upravlenie-analitiki/dutykotiki/vygruzki/ottokklienty-8048', + 'upravlenie-analitiki/dutykotiki/vygruzki/otzyvy-pokupatelejj-na-cenu', + 'upravlenie-analitiki/dutykotiki/vygruzki/parsing-tt-po-geozonam', + 'upravlenie-analitiki/dutykotiki/vygruzki/partnjory-partnjorskojj-programmy-region', + 'upravlenie-analitiki/dutykotiki/vygruzki/perekljuchenie-pozicijj', + 'upravlenie-analitiki/dutykotiki/vygruzki/peresechenie-auditorijj-po-lukumu', + 'upravlenie-analitiki/dutykotiki/vygruzki/peresechenie-po-pokupkam-vafel-sladosti', + 'upravlenie-analitiki/dutykotiki/vygruzki/pokazateli-po-proektu-indilavka', + 'upravlenie-analitiki/dutykotiki/vygruzki/pokupateli-mango', + 'upravlenie-analitiki/dutykotiki/vygruzki/pokupateli-restoranov', + 'upravlenie-analitiki/dutykotiki/vygruzki/pokupki-u-partnjorov', + 'upravlenie-analitiki/dutykotiki/vygruzki/pokupki-v-indilavke-posle-festivalja', + 'upravlenie-analitiki/dutykotiki/vygruzki/pokupki-vo-vv-u-kart-kotorye-pokupali-na-obed.ru', + 'upravlenie-analitiki/dutykotiki/vygruzki/polzovateli-dlja-achivki', + 'upravlenie-analitiki/dutykotiki/vygruzki/polzovateli-mobilnogo-prilozhenija-kotorye-ne-sove', + 'upravlenie-analitiki/dutykotiki/vygruzki/poschitat-kolichestvo-stolovyx-priborov-v-zakazax-', + 'upravlenie-analitiki/dutykotiki/vygruzki/poslednie-cheki-batarejjki--bonusy-za-utilizaciju', + 'upravlenie-analitiki/dutykotiki/vygruzki/postavshhiki-i-stoimost-pechatnojj-upakovki', + 'upravlenie-analitiki/dutykotiki/vygruzki/postavshhiki-piva', + 'upravlenie-analitiki/dutykotiki/vygruzki/povtornye-pokupki-indilavka', + 'upravlenie-analitiki/dutykotiki/vygruzki/povtornye-zakazy-iz-partnjorki-s-primenenie-promok', + 'upravlenie-analitiki/dutykotiki/vygruzki/pp-vv-analiz-reklamy-esh-uchis-tvori', + 'upravlenie-analitiki/dutykotiki/vygruzki/prichiny-vozvratov-po-segmentam', + 'upravlenie-analitiki/dutykotiki/vygruzki/privlechjonnye-novichki-v-indilavku', + 'upravlenie-analitiki/dutykotiki/vygruzki/procentnoe-sootnoshenie-shtatnyx-sotrudnikov-i-aut', + 'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-bytovojj-ximii-na-rozliv', + 'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-deserta', + 'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-kulinarii-i-kulinarii-rp-na-2-tt', + 'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-kulinarii-vv-po-nedeljam', + 'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-lavka-vkusa---otbornoe', + 'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-onlajjn-v-moskve', + 'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-po-jajjcu-v-period-pasxi', + 'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-spisanija-zc-po-tov.pozicijam-kotorye-vyv', + 'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-termostakana-za-poslednie-polgoda', + 'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-tovarov-aptek', + 'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-tovarov-dobraja-polka-so-skidkami', + 'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-tovarov-so-stavkojj-nds-10', + 'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-tovarov-supermarketa-na-vajjldberriz-wild', + 'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-trex-tovarov-za-poslednijj-god', + 'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-tt-bez-uchjota-v-cheklajjnax-tovarov-iz-k', + 'upravlenie-analitiki/dutykotiki/vygruzki/promokod-krasota--info-po-istochnikam-po-promokoda', + 'upravlenie-analitiki/dutykotiki/vygruzki/proschet-vozvratnosti-i-chastoty-pokupok-unikalnyx', + 'upravlenie-analitiki/dutykotiki/vygruzki/proverit-kartochki-pokupatelejj-na-nalichie-zakazo', + 'upravlenie-analitiki/dutykotiki/vygruzki/proverka-sebestoimosti-komplektov', + 'upravlenie-analitiki/dutykotiki/vygruzki/raschet-sr-kolva-zakazov', + 'upravlenie-analitiki/dutykotiki/vygruzki/rasshirennaja-vygruzka-po-zakazam-b2b-klientov', + 'upravlenie-analitiki/dutykotiki/vygruzki/razmernost-upakovok-skju', + 'upravlenie-analitiki/dutykotiki/vygruzki/segmentacija-onlajjnoflajjn-po-kartam', + 'upravlenie-analitiki/dutykotiki/vygruzki/shtrixkody-na-nomenklatury', + 'upravlenie-analitiki/dutykotiki/vygruzki/skidka-kontragentov.-sbor-bazy', + 'upravlenie-analitiki/dutykotiki/vygruzki/skoroportjashhijjsja-assortiment-i-ego-postavshhik', + 'upravlenie-analitiki/dutykotiki/vygruzki/sneki-vygruzka-kart-dlja-gruppy-tovarov-dlja-vozra', + 'upravlenie-analitiki/dutykotiki/vygruzki/sootnesti-nomera-telefonov-i-nomera-kart-vv', + 'upravlenie-analitiki/dutykotiki/vygruzki/sootnoshenie-chisla-klientov-v-razreze-asr', + 'upravlenie-analitiki/dutykotiki/vygruzki/sostav-chekov-v-kotoryx-est-tovary-iz-kategorii-ku', + 'upravlenie-analitiki/dutykotiki/vygruzki/sostavy-i-postavshhiki-dlja-napitkov-i-alkogolja', + 'upravlenie-analitiki/dutykotiki/vygruzki/sostavy-produkcii', + 'upravlenie-analitiki/dutykotiki/vygruzki/sotrudniki-kotorye-ustroilis-do-01.01.2013-i-rabot', + 'upravlenie-analitiki/dutykotiki/vygruzki/spisanija-po-degustacii-texnolog-proverka-kachestv', + 'upravlenie-analitiki/dutykotiki/vygruzki/spisanija-s-tt-i-onlajjn-prodazhi-po-murmansku', + 'upravlenie-analitiki/dutykotiki/vygruzki/spisaniyapostavki', + 'upravlenie-analitiki/dutykotiki/vygruzki/spisok-aktualnojj-nomenklatury', + 'upravlenie-analitiki/dutykotiki/vygruzki/spisok-nomenklatury-po-opredelennomu-prodakt-mened', + 'upravlenie-analitiki/dutykotiki/vygruzki/spisok-tovarov-dlja-mobilnogo-prilozhenija-eatfit', + 'upravlenie-analitiki/dutykotiki/vygruzki/spisok-tt-s-priznakom-kafe', + 'upravlenie-analitiki/dutykotiki/vygruzki/spisok-zakazov-iz-torgovojj-tochki-7087ds-bbr-239', + 'upravlenie-analitiki/dutykotiki/vygruzki/sposob-prigotovlenija-v-lichnom-kabinete-postavshh', + 'upravlenie-analitiki/dutykotiki/vygruzki/sravnenie-prodazh-dvux-vidov-makaron', + 'upravlenie-analitiki/dutykotiki/vygruzki/srednee-i-mediannoe-kolichestvo-chekov-v-moskve-i-', + 'upravlenie-analitiki/dutykotiki/vygruzki/srednee-kol-vo-tovara-v-cheke', + 'upravlenie-analitiki/dutykotiki/vygruzki/srednee-kolichestvo-paketov-v-zakazax-partnera-sbe', + 'upravlenie-analitiki/dutykotiki/vygruzki/srednjaja-stoimost-pokupki-i-chastota-pokupki-xurm', + 'upravlenie-analitiki/dutykotiki/vygruzki/srednjaja-summa-realizovannyx-tovarov--summa-ostat', + 'upravlenie-analitiki/dutykotiki/vygruzki/statistika-po-kolichestvu-zakazov-kategorii-gorjac', + 'upravlenie-analitiki/dutykotiki/vygruzki/statistika-po-prodazham', + 'upravlenie-analitiki/dutykotiki/vygruzki/statistika-po-slitym-kartam', + 'upravlenie-analitiki/dutykotiki/vygruzki/statistika-sbora-paketov-i-kryshek', + 'upravlenie-analitiki/dutykotiki/vygruzki/statusy-postavshhikov', + 'upravlenie-analitiki/dutykotiki/vygruzki/summa-bonusov-po-nomeram-kart', + 'upravlenie-analitiki/dutykotiki/vygruzki/temperatura-xranenija-tovarov-kategorii-alkogol', + 'upravlenie-analitiki/dutykotiki/vygruzki/top-100-po-kolichestvu-onlajjn-zakazov.-tt-ne-v-zo', + 'upravlenie-analitiki/dutykotiki/vygruzki/top-5-oxlazhdenki-vkusmil', + 'upravlenie-analitiki/dutykotiki/vygruzki/top-50-detskix-tovarov', + 'upravlenie-analitiki/dutykotiki/vygruzki/top-darkov-dlja-achivki-blagotvoritelnosti', + 'upravlenie-analitiki/dutykotiki/vygruzki/top-pozicijj-v-prodazhax-k-ng-2023', + 'upravlenie-analitiki/dutykotiki/vygruzki/top-tovarov-po-vozrastam', + 'upravlenie-analitiki/dutykotiki/vygruzki/top-tovarov-s-maksimalnymi-vozvratami', + 'upravlenie-analitiki/dutykotiki/vygruzki/top-tovarov-v-pervom-cheke-karty-klienta', + 'upravlenie-analitiki/dutykotiki/vygruzki/top-tovary-55', + 'upravlenie-analitiki/dutykotiki/vygruzki/top-tovary-geo', + 'upravlenie-analitiki/dutykotiki/vygruzki/tt-kafe', + 'upravlenie-analitiki/dutykotiki/vygruzki/tt-po-moskve-i-sankt-peterburgu', + 'upravlenie-analitiki/dutykotiki/vygruzki/tt-s-dostavkojj', + 'upravlenie-analitiki/dutykotiki/vygruzki/unikalnye-pokupateli-tovara-dobryjj-prjanik', + 'upravlenie-analitiki/dutykotiki/vygruzki/unikalnyu-karty-po-tipu-zhaloby-33-4-s-razbivkojj', + 'upravlenie-analitiki/dutykotiki/vygruzki/vajjtstory-gde-est-sborka-zakazov-jelektronnaja-po', + 'upravlenie-analitiki/dutykotiki/vygruzki/ves-tovara-do-i-posle-sborki', + 'upravlenie-analitiki/dutykotiki/vygruzki/vitrinyproekty-po-vsem-tt', + 'upravlenie-analitiki/dutykotiki/vygruzki/vozvraty-pokupki-i-obrashhenija-pokupatelejj-po-se', + 'upravlenie-analitiki/dutykotiki/vygruzki/vozvraty-s-jekspress-dostavkojj-po-servisam', + 'upravlenie-analitiki/dutykotiki/vygruzki/vtorsyre-zakaz-uslugi-s-dostavkojj', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-18-24-letnix', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-analitiki', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-auditorii', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-cen-za-period-v-dinamike', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-dannyx-po-onlajjn-zakazam-v-adresax-s-jek', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-dannyx-po-palletam', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-dannyx-predoplaty', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-daty-rozhdenija-i-pola-klientov', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-dlja-sokrashhenija-zatrat-vremeni-linejjn', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-dolejj-kazhdogo-puti-v-korzine-pokupatelj', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-email-texnologov', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-informacii-po-nalichiju-statusa-xrupkoe', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-jeko--i-nejeko--pokupatelejj', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-kart-po-spisku-tt-kto-ne-zakazyval-dostav', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-kolichestva-offlajjn-pokupatelejj-po-regi', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-kolichestva-pokupatelejj-na-lp', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-kuratorov-kurerov-i-kassirov-komplektovsh', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-nomenklatury', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-nomerov-telefonov-i-kart-stavili-negativn', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-nomerov-telefonov-ottok', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-nomerov-telefonov-po-kartam', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-obrashhenijj-otzyvov-i-rejjtinga-sp-vvgo', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-obrashhenijj-po-programme-lojalnosti-novi', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-obrashhenijj-po-samovyvozu', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-planogramm-kafe', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-po-kartam-kategorija-non-fud', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-pokupatelejj-indilavki-za-2024-god', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-postavshhikov', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-poterjannye-pokupateli-po-syru-rossijjsko', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-prinadlezhnosti-k-stmnestm-tovara-s-privj', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-raspredelenija-so-skladov-sumka-kofe-na-2', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-realizacijj-restorannyx-zakazov-jur.licam', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-skju-iz-1s-mjasnye-delikatesy.-kolbasy', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-srednjaja-vyruchka-i-srednee-kol-vo-cheko', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-strok-k-zakazam', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-telefonov-arendodatelejj-mikromarket-stat', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-texnologov-po-proizvoditeljupostavshhiku', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-top-50-tovarov-iz-top-20-kategorijj', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-top-tovarov', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-tovarov-darkstorov', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-tovarov-s-opredelennym-sostavom', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-v-formate-parket-chekov-novichkov', + 'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-zakazov-slotovyx-jekspress-na-blizhajjshi', + 'upravlenie-analitiki/dutykotiki/vygruzki/vyruchka-i-kolichestvo-v-kategorii-mjaso-i-zamoroz', + 'upravlenie-analitiki/dutykotiki/vygruzki/vyruchka-i-kolichestvo-vegan-tovarov', + 'upravlenie-analitiki/dutykotiki/vygruzki/vyruchka-po-dnjam-i-ostatki-po-chasam', + 'upravlenie-analitiki/dutykotiki/vygruzki/xarakteristika-nomenklatury-i-id-xarakteristiki', + 'upravlenie-analitiki/dutykotiki/vygruzki/zadacha-arbuznyx-kommentariev', + 'upravlenie-analitiki/dutykotiki/vygruzki/zakaz-po-telefonu', + 'upravlenie-analitiki/dutykotiki/vygruzki/zakazy-dlja-nochlezhki', + 'upravlenie-analitiki/dutykotiki/vygruzki/zakazy-oformlennye-cherez-servis-samovyvoz-s-tovar', + 'upravlenie-analitiki/dutykotiki/vygruzki/zakazy-oformlennyjj-vne-grafika-raboty-servisa-dar', + 'upravlenie-analitiki/dutykotiki/vygruzki/zakazy-partnjorskojj-programmy-sovershjonnye-s-tov', + 'upravlenie-analitiki/dutykotiki/vygruzki/zakazy-po-lsk-fjeshn-ooo', + 'upravlenie-analitiki/dutykotiki/vygruzki/zapros-bonusnyx-kart-po-tt', + 'upravlenie-analitiki/dutykotiki/vygruzki/zapros-kolichestva-chekov-v-torgovyx-tochkax', + 'upravlenie-analitiki/dutykotiki/vygruzki/zaprosy-mvd-i-drugix-gos.organov-po-pokupateljam', + 'upravlenie-analitiki/dutykotiki/vygruzki/zhaloby-na-neprijatnyjj-zapax-v-tt', + 'upravlenie-analitiki/dutykotiki/vygruzki/zheltyjj-cennik-po-partneru-jae', + 'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike', + 'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/ab-testirovanie', + 'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/bagi', + 'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/blagotvoritelnost-vo-vkusvill', + 'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/formulirovka-zaprosa-na-vygruzku', + 'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/issledovanija', + 'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/poleznye-sovety-dlja-power-bi', + 'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/postanovka-zadachi-analitiku', + 'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/prioritezacija-zadach', + 'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/segmentacija-lifestyle', + 'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/texbot', + 'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/vremja-vypolnenija-zadach', + 'upravlenie-analitiki/kak-podat-zajavku-analitikam/kak-ispolzovat-shablony-dlja-zadach', + 'upravlenie-analitiki/logirovanie-mp/aa55c98a3fdf', + 'upravlenie-analitiki/logirovanie-mp/biznes-trebovanija-k-sisteme-logirovanija', + 'upravlenie-analitiki/logirovanie-mp/boli-logov', + 'upravlenie-analitiki/logirovanie-mp/boli-svjazannye-s-logami', + 'upravlenie-analitiki/logirovanie-mp/cartlogs', + 'upravlenie-analitiki/logirovanie-mp/cartlogs/korzina', + 'upravlenie-analitiki/logirovanie-mp/cartlogs/oplaty-5-4', + 'upravlenie-analitiki/logirovanie-mp/celi-proekta-logirovanie-2.0', + 'upravlenie-analitiki/logirovanie-mp/korzina', + 'upravlenie-analitiki/logirovanie-mp/proektlogirovaniya', + 'upravlenie-analitiki/logirovanie-mp/proektlogirovaniya/itogicustdev', + 'upravlenie-analitiki/logirovanie-mp/proektlogirovaniya/metrics', + 'upravlenie-analitiki/logirovanie-mp/proektlogirovaniya/opisanie-novogo-processa-legirovanija', + 'upravlenie-analitiki/logirovanie-mp/proektlogirovaniya/scenarii-vzaimodejjstvija-mezhdu-uchastnikami-proc', + 'upravlenie-analitiki/logirovanie-mp/proektlogirovaniya/zony-otvetstvennosti-i-kompetencii-uchastnikov-pro', + 'upravlenie-analitiki/logirovanie-mp/razdel-1', + 'upravlenie-analitiki/logirovanie-mp/razdel-1/istorija-versijj', + 'upravlenie-analitiki/logirovanie-mp/semanticheskaja-model', + 'upravlenie-analitiki/logirovanie-mp/vitriny-logov', + 'upravlenie-analitiki/logirovanie-mp/vitriny-logov/primer-specificheskaja-vitrina-s-uzkospecializirov', + 'upravlenie-analitiki/logirovanie-mp/vitriny-logov/razdel-2', + 'upravlenie-analitiki/logirovanie-mp/vnedrenie-izmenenijj-v-tekushhijj-process-sozdanij', + 'upravlenie-analitiki/logirovanie-mp/voprosy-po-chernovomu-arxitekturnomu-resheniju', + 'upravlenie-analitiki/onboarding/dostupy', + 'upravlenie-analitiki/onboarding/dostupy/6849a4eb2845', + 'upravlenie-analitiki/onboarding/formy-jatrekera', + 'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami', + 'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/1s', + 'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/analiticheskie-instrumenty', + 'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/full-power-bi-desktop', + 'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/instruction-ytracker', + 'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/instrukcija-po-nastrojjke-vs-code', + 'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/instrukcija-po-podkljucheniju-clickhouse-v-dbeave', + 'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/nastrojjka-gitlab', + 'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/perenos-s-kajjtena-katja', + 'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/podkljuchenie-cherez-jupyterhub', + 'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/poisk-informacii-po-bd', + 'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/pro-moduli-access', + 'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/sozdanie-ssh-kljucha', + 'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/sql-docs', + 'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/ssrs-i-power-bi-otchety', + 'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/superset', + 'upravlenie-analitiki/onboarding/kak-my-vedjom-analiticheskie-zadachi', + 'upravlenie-analitiki/onboarding/manifest-produktovojj-analitiki', + 'upravlenie-analitiki/onboarding/manifest-produktovojj-analitiki/komanda-i-vzaimodejjstvie', + 'upravlenie-analitiki/onboarding/manifest-produktovojj-analitiki/process-raboty-nad-zadachami', + 'upravlenie-analitiki/onboarding/oformlenie-postojannogo-propuska-v-ofis-vv', + 'upravlenie-analitiki/onboarding/opisanie-obshhego-analiticheskogo-shablona-opisani', + 'upravlenie-analitiki/onboarding/stati-pro-dannye-vv', + 'upravlenie-analitiki/onboarding/stati-pro-dannye-vv/produkty-lojalnosti-kratkijj-gajjd', + 'upravlenie-analitiki/onboarding/statja-onbordinga-dlja-novichka', + 'upravlenie-analitiki/onboarding/upravlenie-analitiki', + 'upravlenie-analitiki/operacionnaja-analitika/analitika-b2b', + 'upravlenie-analitiki/operacionnaja-analitika/analitika-darkkitchen', + 'upravlenie-analitiki/operacionnaja-analitika/analitika-mikroservisa-roznicy', + 'upravlenie-analitiki/operacionnaja-analitika/analitika-partnerov-apteki-restorany-cvety-etc', + 'upravlenie-analitiki/operacionnaja-analitika/analitika-poslednejj-mili', + 'upravlenie-analitiki/operacionnaja-analitika/analitika-sborki', + 'upravlenie-analitiki/operacionnaja-analitika/raspredelenie-urz-ostatki', + 'upravlenie-analitiki/operacionnaja-analitika/revju-2025', + 'upravlenie-analitiki/platforma-po-av-testam/arxiv', + 'upravlenie-analitiki/platforma-po-av-testam/draft-monitoring-sostojanija-platformy', + 'upravlenie-analitiki/platforma-po-av-testam/oshibki', + 'upravlenie-analitiki/platforma-po-av-testam/testirovanie/onbording', + 'upravlenie-analitiki/platforma-po-av-testam/testirovanie/reglamenty-testovojj-dokumentacii', + 'upravlenie-analitiki/platforma-po-av-testam/testirovanie/regress', + 'upravlenie-analitiki/platforma-po-av-testam/testirovanie/restapi', + 'upravlenie-analitiki/platforma-po-av-testam/testirovanie/test-plan-pokrytija-kriticheskogo-funkcionala-ab-t', + 'upravlenie-analitiki/platforma-po-av-testam/testirovanie/web-funkcional---kalendar', + 'upravlenie-analitiki/platforma-po-av-testam/validacija-ab-platformy', + 'upravlenie-analitiki/processy-ua/emk-process/primer-zapolnennogo-shablona-dlja-revju-pa', + 'upravlenie-analitiki/processy-ua/emk-process/process-revju-pa', + 'upravlenie-analitiki/produktovaja-analitika-kpa/8a1c0312ec94', + 'upravlenie-analitiki/produktovaja-analitika-kpa/8a1c0312ec94/0d4e3c3b36ec', + 'upravlenie-analitiki/produktovaja-analitika-kpa/8a1c0312ec94/klasterizacija-torgovyx-tochek.-podxod-razvitija', + 'upravlenie-analitiki/produktovaja-analitika-kpa/8a1c0312ec94/kojefficienty-sezonnosti-i-razvedyvatelnyjj-analiz', + 'upravlenie-analitiki/produktovaja-analitika-kpa/8a1c0312ec94/metriki-jeffektivnosti', + 'upravlenie-analitiki/produktovaja-analitika-kpa/8a1c0312ec94/okupaemost-tt', + 'upravlenie-analitiki/produktovaja-analitika-kpa/8a1c0312ec94/otcheta-dlja-podbora-pervichnojj-matricy-tovarov-d', + 'upravlenie-analitiki/produktovaja-analitika-kpa/8a1c0312ec94/razlichija-v-sezonnosti-i-v-chekax-budnivyxodnye-i', + 'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e', + 'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/43eb2cbbfe12', + 'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/5-na-fr', + 'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/8325b4e9d0f1', + 'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/ac709a764736', + 'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/dopniki-po-unikalnosti-i-jekskljuzivnosti', + 'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/dorabotka-dashborda-peregovornaja-komanija', + 'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/e58a3ba2ba0b', + 'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/jeffektivnost-termochexlov-rc-dark', + 'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/peredacha-skriptov-olja', + 'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/rejjting-postavshhika', + 'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/shablon-dlja-issledovanijj-ua', + 'upravlenie-analitiki/produktovaja-analitika-kpa/metriki-i-skripty-kpa', + 'upravlenie-analitiki/produktovaja-analitika-kpa/metriki-i-skripty-kpa/chasto-ispolzuemye-tablicy-i-predstavlenija', + 'upravlenie-analitiki/produktovaja-analitika-kpa/metriki-i-skripty-kpa/metriki', + 'upravlenie-analitiki/produktovaja-analitika-kpa/metriki-i-skripty-kpa/metriki-gp', + 'upravlenie-analitiki/produktovaja-analitika-kpa/metriki-i-skripty-kpa/poleznye-skripty', + 'upravlenie-analitiki/produktovaja-analitika-kpa/raznye-ceny-i-cenoobrazovanie', + 'upravlenie-analitiki/produktovaja-analitika-kpa/statistika-frov', + 'upravlenie-analitiki/produktovaja-analitika-kpa/zadachi', + 'upravlenie-analitiki/produktovaja-analitika-kpa/zadachi/0b2881f238dd', + 'upravlenie-analitiki/produktovaja-analitika-kpa/zadachi/15fe987417a3', + 'upravlenie-analitiki/produktovaja-analitika-kpa/zadachi/6fb599d9485e', + 'upravlenie-analitiki/produktovaja-analitika-kpa/zadachi/d567d5309fec', + 'upravlenie-analitiki/produktovaja-analitika-kpa/zadachi/vygruzki', + 'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti', + 'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/chaevye', + 'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/cheki', + 'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/chestnyjj-znak', + 'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/eshpay', + 'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/kassy', + 'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/krepkijj-alkogol', + 'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/ks', + 'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/lojalnost-i-onlajjn-kassa', + 'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/oborudovanie', + 'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/onlajjn-oplaty', + 'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/oplata-na-meste', + 'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/podarochnye-karty', + 'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/pravila-postanovki-zadach-analitikam', + 'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/privjazki-ka', + 'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/spravochnik-platezhnyjj-put', + 'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/spravochnik-platezhnyjj-put---logi-kurera', + 'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/vozvraty', + 'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/vygruzki-dlja-issledovateljj', + 'upravlenie-analitiki/produktovaja-analitika-kpo/general-team', + 'upravlenie-analitiki/produktovaja-analitika-kpo/general-team/dq-project', + 'upravlenie-analitiki/produktovaja-analitika-kpo/general-team/issledovanie-otkuda-polzovateli-nachinajut-sobirat', + 'upravlenie-analitiki/produktovaja-analitika-kpo/general-team/istochniki-dlja-rascheta-metrik-otchetov-su-i-obes', + 'upravlenie-analitiki/produktovaja-analitika-kpo/general-team/opisanija-verxneurovnevyx-otchetov', + 'upravlenie-analitiki/produktovaja-analitika-kpo/general-team/pererabotka-verxneurovnevojj-otchetnosti', + 'upravlenie-analitiki/produktovaja-analitika-kpo/general-team/plany-2024n1', + 'upravlenie-analitiki/produktovaja-analitika-kpo/lidery-pa-kpo', + 'upravlenie-analitiki/produktovaja-analitika-kpo/lidery-pa-kpo/plany-i-itogi', + 'upravlenie-analitiki/produktovaja-analitika-kpo/lidery-pa-kpo/sink-liderov-pa-kpo', + 'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija', + 'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/adzhendy-vstrech', + 'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/novyjj-process-logirovanija', + 'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/onbording', + 'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/prioritizacija-zadach-po-rice', + 'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/process-provedenija-av-testov', + 'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/processy-pa-kpo', + 'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/proekty', + 'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/python-pakety', + 'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/rabota-s-logami', + 'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/reestr-dashbordov', + 'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/reestr-ispolzuemyx-na-238kx-tablic-vitrin-vjux', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-avtorizacii-i-profilja', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-avtorizacii-i-profilja/ab-testy', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-avtorizacii-i-profilja/dashbordy', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-avtorizacii-i-profilja/issledovanija', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-ccg', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-ccg/komandaccg', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-ccg/plany-i-celi', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-ccg/processy-korzina', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-ccg/produktovaja-analitika-geoservisov', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-ccg/produktovaja-analitika-korziny', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-kataloga', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-kataloga/jekran-podderzhki', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-kataloga/katalog', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-kataloga/neaktualnoe', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-kataloga/obshhee-kataloga', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-kataloga/produktovaja-analitika-kartochki-tovara', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-kataloga/rejjtingi-i-otzyvy', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-lojalnosti', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-lojalnosti/dokumentirovanie-zadach', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-lojalnosti/kommunikacii', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-lojalnosti/lojalnost', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-lojalnosti/plany-2024', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-lojalnosti/proekty', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-lojalnosti/shablony', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-lojalnosti/spisok-del-serezhi', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-lojalnosti/vitriny-danny', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-offlajjnery-v-mp', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-offlajjnery-v-mp/magaziny', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-offlajjnery-v-mp/processy', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-platezhnyx-putejj', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-platezhnyx-putejj/chaevye', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-platezhnyx-putejj/onlajjn-oplaty', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-platezhnyx-putejj/oplaty-na-meste', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-platezhnyx-putejj/vozvraty', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/ab-poisk', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/dokumentacija-biblioteki-dlja-ab-testov', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/kak-rabotaet-poisk', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/logi-poiska-xyunja', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/metriki-poisk', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/onbording-v-komandu-poiska', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/produkty-komandy-analitiki-poiska', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/proekt-novaja-konkurencija', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/researches-poisk', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/search-dashes', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/vitriny-dannyx-poisk', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/8ff2daf94a28', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/ab-rekomendacii', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/algoritmicheskie-podborki', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/dashbordy-rekomendacii', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/issledovanija-rekomendacii', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/logi-rekomendacii', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/metriki-rekomendacii', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/onbording-v-komandu-analitiki-rekomendacijj', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/problemy', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/rekomendacii-v-mp', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/tovary-v-rekomendacijax', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/vygruzki', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/ab-tests', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/appsflyer', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/bjeklog-gipotez', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/dokumentacija-sajjta', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/jandeks.metrika', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/logirovanie-sajjta', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/logirovanie-sajjta-slova', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/logirovanie-sajjta-slova-f6de9f', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/metriki-sajjta', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/otchety-komandy-sajjta', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/test-vkusvill-plan', + 'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/zadachi', + 'upravlenie-analitiki/produktovaja-analitika-kpo/proekty-pa-kpo', + 'upravlenie-analitiki/produktovaja-analitika-kpo/proekty-pa-kpo/domen-driven-design', + 'upravlenie-analitiki/produktovaja-analitika-kpo/proekty-pa-kpo/logirovanie', + 'upravlenie-analitiki/produktovaja-analitika-kpo/proekty-pa-kpo/novaja-konkurencija-metriki', + 'upravlenie-analitiki/produktovaja-analitika-kpo/proekty-pa-kpo/shablon', + 'upravlenie-analitiki/produktovaja-analitika-kpo/verxneurovnevaja-analitika', + 'upravlenie-analitiki/produktovaja-analitika-kpo/verxneurovnevaja-analitika/zadachi', + 'upravlenie-analitiki/produktovaja-analitika-lojalnosti/4689cbbc6dc5', + 'upravlenie-analitiki/produktovaja-analitika-lojalnosti/dokumentacija-po-zadacham', + 'upravlenie-analitiki/produktovaja-analitika-lojalnosti/globalnye-issledovanija', + 'upravlenie-analitiki/produktovaja-analitika-lojalnosti/komanda', + 'upravlenie-analitiki/produktovaja-analitika-lojalnosti/mexaniki-lojalnosti', + 'upravlenie-analitiki/produktovaja-analitika-lojalnosti/obshhaja-informacija', + 'upravlenie-analitiki/produktovaja-analitika-lojalnosti/otchety', + 'upravlenie-analitiki/produktovaja-analitika-lojalnosti/segmentacii', + 'upravlenie-analitiki/produktovaja-analitika-lojalnosti/slovar-metrik', + 'upravlenie-analitiki/put-novichka/kak-podat-zajavku-na-dostupy', + 'upravlenie-analitiki/put-novichka/perevod-sotrudnika', + 'upravlenie-analitiki/put-novichka/pomoshh-v-provedenii-onboarding', + 'upravlenie-analitiki/put-novichka/vzaimodejjstvie-upravlenija-analitiki-ua-i-upravle', + 'upravlenie-analitiki/research-analytics/analiz-jeffektivnosti-otveta-na-otzyv', + 'upravlenie-analitiki/research-analytics/assistent-antifrod-dose-na-klienta', + 'upravlenie-analitiki/research-analytics/avtootvety-na-gorjachejj-linii', + 'upravlenie-analitiki/research-analytics/b2b-containers', + 'upravlenie-analitiki/research-analytics/busting-tovarov-sgorjacha', + 'upravlenie-analitiki/research-analytics/favorite-in-tap-bar', + 'upravlenie-analitiki/research-analytics/finansovaja-model-fudomatov-obedy', + 'upravlenie-analitiki/research-analytics/gen-ai', + 'upravlenie-analitiki/research-analytics/gen-ai/copilot-vygruzki', + 'upravlenie-analitiki/research-analytics/gen-ai/genai-bt', + 'upravlenie-analitiki/research-analytics/gen-ai/rag', + 'upravlenie-analitiki/research-analytics/modelirovanie-blokirovki-postavshhikov', + 'upravlenie-analitiki/research-analytics/nps', + 'upravlenie-analitiki/research-analytics/ocenka-jeffektivnosti-soobshhenija-ob-izmenenijax-', + 'upravlenie-analitiki/research-analytics/oec-metrika', + 'upravlenie-analitiki/research-analytics/poleznye-bazovye-materialy', + 'upravlenie-analitiki/research-analytics/prognozirovanie-pl-obedy', + 'upravlenie-analitiki/research-analytics/snjatie-tovara-s-prodazhi', + 'upravlenie-analitiki/research-analytics/zajavka-na-otrabotku-kachestva', +] + +MAX_DEPTH = 4 # Максимальная глубина обхода ссылок +DELAY = 0.3 # Пауза между запросами (сек), чтобы не перегружать API + +# ───────────────────────────────────────────────────────────────────────────── + + +def main(): + started_at = pendulum.now(tz='Europe/Moscow') + print(f'[{started_at.to_datetime_string()}] Запуск wiki_sync') + + if not API_KEY or not ORG_ID: + print('✗ Не заданы API_KEY / ORG_ID в .env') + sys.exit(1) + + # 1. Обход Wiki + pages = crawl( + root_slugs=ROOT_SLUGS, + api_token=API_KEY, + org_id=ORG_ID, + max_depth=MAX_DEPTH, + delay=DELAY, + ) + + if not pages: + print('Страниц не найдено. Завершение.') + return + + # 2. Сохранение в Supabase + db = SupabaseManager() + if not db.connect(): + sys.exit(1) + + try: + result = db.upsert_wiki_pages(pages) + print( + f'\n✓ Страницы сохранены:\n' + f' Новых / изменённых: {result["inserted"]}\n' + f' Без изменений: {result["unchanged"]}\n' + f' Всего страниц: {len(pages)}' + ) + + # 3. Генерация embeddings для новых/изменённых страниц + print('\nГенерируем embeddings...') + emb_result = upsert_embeddings(db, pages) + print( + f'✓ Embeddings:\n' + f' Сгенерировано: {emb_result["embedded"]}\n' + f' Без изменений: {emb_result["skipped"]}' + ) + finally: + db.close() + + finished_at = pendulum.now(tz='Europe/Moscow') + duration = (finished_at - started_at).in_seconds() + print(f'[{finished_at.to_datetime_string()}] Готово за {duration}с') + + +if __name__ == '__main__': + main() diff --git a/wiki_tree_crawler.py b/wiki_tree_crawler.py new file mode 100644 index 0000000..58779ac --- /dev/null +++ b/wiki_tree_crawler.py @@ -0,0 +1,146 @@ +""" +Обход {% tree %} страниц Яндекс Вики через Playwright. + +Находит дочерние страницы, которые не видны через API (рендерятся динамически). +Возвращает список slug-ов которых нет в wiki_pages — их нужно добавить в ROOT_SLUGS. + +Запуск: + python wiki_tree_crawler.py +""" +import re +import time +from pathlib import Path +from dotenv import load_dotenv + +load_dotenv(Path(__file__).parent / '.env') + +from playwright.sync_api import sync_playwright + +AUTH_FILE = Path(__file__).parent / 'wiki_auth.json' +WIKI_BASE = 'https://wiki.yandex.ru' +DELAY = 0.5 # пауза между запросами + + +def slug_from_url(url: str) -> str | None: + """Извлечь slug из URL wiki.yandex.ru/slug/...""" + match = re.match(r'https?://wiki\.yandex\.ru/([^?#]+?)/?$', url) + if not match: + return None + slug = match.group(1).strip('/') + # Отфильтровать служебные страницы + if any(slug.startswith(p) for p in ['.files', 'homepage/', 'cdp-']): + return None + return slug + + +def get_child_slugs(page, parent_slug: str) -> list[str]: + """Открыть страницу и собрать все ссылки на дочерние страницы.""" + url = f'{WIKI_BASE}/{parent_slug}' + page.goto(url, wait_until='networkidle', timeout=30000) + time.sleep(DELAY) + + # Ищем ссылки в левом навигационном дереве и в содержимом страницы + links = page.eval_on_selector_all( + 'a[href]', + 'els => els.map(el => el.href)' + ) + + child_slugs = [] + for link in links: + slug = slug_from_url(link) + if slug and slug.startswith(parent_slug + '/') and slug != parent_slug: + child_slugs.append(slug) + + return list(set(child_slugs)) + + +def find_tree_pages_in_db() -> list[tuple[str, str]]: + """Получить из Supabase все страницы с {% tree %} в контенте.""" + import sys + sys.path.insert(0, str(Path(__file__).parent)) + from supabase import SupabaseManager + + db = SupabaseManager() + db.connect() + db.cursor.execute(""" + SELECT slug, value->>'title' AS title + FROM wiki_pages + WHERE value->>'content' LIKE '%{%% tree%%}%' + ORDER BY slug + """) + rows = db.cursor.fetchall() + db.close() + return rows + + +def get_all_known_slugs() -> set[str]: + """Все slug-и которые уже есть в wiki_pages.""" + import sys + sys.path.insert(0, str(Path(__file__).parent)) + from supabase import SupabaseManager + + db = SupabaseManager() + db.connect() + db.cursor.execute("SELECT slug FROM wiki_pages") + slugs = {row[0] for row in db.cursor.fetchall()} + db.close() + return slugs + + +def main(): + if not AUTH_FILE.exists(): + print(f'✗ Файл авторизации не найден: {AUTH_FILE}') + print(' Сначала запусти: python wiki_auth.py') + return + + ROOT_PREFIX = 'upravlenie-analitiki' + + print('Загружаем список страниц с {% tree %} из Supabase...') + all_tree_pages = find_tree_pages_in_db() + tree_pages = [ + (s, t) for s, t in all_tree_pages + if s == ROOT_PREFIX or s.startswith(ROOT_PREFIX + '/') + ] + known_slugs = get_all_known_slugs() + print(f'Страниц с {{% tree %}} (всего): {len(all_tree_pages)}, в разделе УА: {len(tree_pages)}') + print() + + new_slugs = [] + + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + context = browser.new_context(storage_state=str(AUTH_FILE)) + page = context.new_page() + + for slug, title in tree_pages: + print(f'→ {slug} «{title}»') + try: + children = get_child_slugs(page, slug) + new = [s for s in children if s not in known_slugs] + print(f' Найдено дочерних: {len(children)}, новых: {len(new)}') + for s in sorted(children): + marker = ' ← NEW' if s in new else '' + print(f' {s}{marker}') + new_slugs.extend(new) + except Exception as e: + print(f' ✗ Ошибка: {e}') + print() + + browser.close() + + new_slugs = list(set(new_slugs)) + + if new_slugs: + print('=' * 60) + print(f'Найдено {len(new_slugs)} новых slug-ов для ROOT_SLUGS:') + print() + for s in sorted(new_slugs): + print(f" '{s}',") + print() + print('Добавь их в ROOT_SLUGS в wiki_sync.py и запусти wiki_sync.py') + else: + print('✓ Новых страниц не найдено') + + +if __name__ == '__main__': + main() diff --git a/yandex_wiki.py b/yandex_wiki.py new file mode 100644 index 0000000..16eedb8 --- /dev/null +++ b/yandex_wiki.py @@ -0,0 +1,125 @@ +""" +Модуль для обхода страниц Yandex Wiki API +""" +import re +import time +import hashlib +import requests +import pendulum + + +WIKI_API = 'https://api.wiki.yandex.net/v1' + + +def _make_headers(api_token: str, org_id: str) -> dict: + return { + 'Authorization': f'OAuth {api_token}', + 'X-Org-Id': org_id, + } + + +def get_page(slug: str, api_token: str, org_id: str) -> dict | None: + """Получить страницу по slug. Возвращает None если не найдена.""" + try: + resp = requests.get( + f'{WIKI_API}/pages', + headers=_make_headers(api_token, org_id), + params={'slug': slug, 'fields': 'content,attributes'}, + timeout=15, + ) + if resp.status_code == 200: + return resp.json() + return None + except requests.RequestException as e: + print(f' ✗ Ошибка при запросе slug={slug}: {e}') + return None + + +def extract_slugs_from_content(content: str) -> list[str]: + """Извлечь slug-ссылки из wiki-разметки страницы.""" + slugs = [] + + # [[slug]] и [[slug|текст]] + slugs += re.findall(r'\[\[([^\]|#]+)', content) + + # ((https://wiki.yandex.ru/slug текст)) или ((https://wiki.yandex.ru/slug)) + url_matches = re.findall(r'https://wiki\.yandex\.ru/([^\s)#]+)', content) + slugs += url_matches + + # Нормализуем: убираем пробелы и слэши по краям + result = [] + for s in slugs: + s = s.strip().strip('/') + if s and not s.startswith('http') and len(s) < 200: + result.append(s) + + return result + + +def content_hash(content: str) -> str: + """MD5-хеш содержимого страницы для определения изменений.""" + return hashlib.md5(content.encode('utf-8')).hexdigest() + + +def crawl( + root_slugs: list[str], + api_token: str, + org_id: str, + max_depth: int = 4, + delay: float = 0.3, +) -> list[dict]: + """ + Обойти дерево страниц начиная с root_slugs. + + Args: + root_slugs: Список стартовых slug-ов (например ['upravlenie-analitiki']) + api_token: OAuth-токен + org_id: ID организации + max_depth: Максимальная глубина обхода + delay: Пауза между запросами в секундах + + Returns: + Список словарей с данными страниц + """ + dt_load = pendulum.now(tz='Europe/Moscow') + visited: set[str] = set() + pages: list[dict] = [] + + def _crawl(slug: str, depth: int): + if depth > max_depth or slug in visited: + return + visited.add(slug) + + print(f' {" " * depth}→ {slug}') + page = get_page(slug, api_token, org_id) + time.sleep(delay) + + if not page: + return + + raw_content = page.get('content', '') + attrs = page.get('attributes', {}) + + modified_at = attrs.get('modified_at') or attrs.get('updated_at') + + pages.append({ + 'pg_load_dttm': dt_load, + 'slug': page.get('slug', slug), + 'wiki_page_id': page.get('id'), + 'title': page.get('title', ''), + 'page_type': page.get('page_type', ''), + 'modified_at': modified_at, + 'content': raw_content, + 'content_hash': content_hash(raw_content), + }) + + # Рекурсивно обходим ссылки + for child_slug in extract_slugs_from_content(raw_content): + _crawl(child_slug, depth + 1) + + print(f'Начинаем обход wiki, корневые разделы: {root_slugs}') + for root in root_slugs: + _crawl(root, 0) + + print(f'✓ Обход завершён. Найдено страниц: {len(pages)}') + return pages