Как не убить слабый сервер: минимизируем размер индекса, аккуратно индексируем и ищем. Затем переносим Typesense на отдельный сервер, подключаемся по Tailscale-IP, настраиваем разные ключи (admin/search-only), делаем первичную заливку и инкрементальные обновления по сети.
Главная идея: в индекс только то, по чему ищем или фильтруем.
Уже сделано:
content (сниппет), а не полный текст;facet: true только те, что реально нужны (tags, published_at, popularity).Дополнительно:
facet: true поле удорожает индекс по памяти и поиску;tags и published_year).В скрипте initial_index_all() (Часть 2):
BATCH_SIZE до 200–500;time.sleep до 0.2–0.5 секунд;Пример:
# initial_index.py (фрагмент) BATCH_SIZE = 300 # было 1000 SLEEP_BETWEEN_BATCHES = 0.3 # ... index_batch(docs) last_id = rows[-1]["id"] total += len(rows) print(f"Проиндексировано страниц: {total}") time.sleep(SLEEP_BETWEEN_BATCHES)
Кратко (без углубления):
timeout не слишком маленький, чтобы запросы поиска успевали./search:В идеале, поиск - это один HTTP-запрос к Typesense и один render_template.
Теперь схема такая:
[Пользователь] -> [Flask + MySQL (сервер A)] -> (Tailscale VPN) -> [Typesense (сервер B)]
100.x.x.x.Плюсы:
Предположим:
100.64.0.10;8108.Просто меняем конфиг клиента:
# ts_client_remote.py import typesense TYPESENSE_API_KEY = "SUPER_SECRET_ADMIN_KEY" # admin или search-only, см. ниже client = typesense.Client({ "nodes": [ { "host": "100.64.0.10", # Tailscale IP сервера Typesense "port": "8108", "protocol": "http", # внутри VPN https обычно не обязателен } ], "api_key": TYPESENSE_API_KEY, "connection_timeout_seconds": 2, })
Дальше:
ts_client импортируем ts_client_remote;client.collections[...] продолжают работать как раньше, только теперь ходят по VPN.Важно:
Лучше всего:
/search).Скрипт, который с помощью admin-ключа создаёт ограниченный ключ:
# create_search_only_key.py from ts_client_remote import client # здесь client с admin-ключом def create_search_only_key(): """ Создаём ключ, который: - имеет доступ только на чтение; - только к коллекции 'pages'; - может выполнять только search-запросы. """ # описание прав зависит от версии Typesense, но идея такая: params = { "description": "Search-only key for pages collection (Flask app)", "actions": ["documents:search"], # только поиск "collections": ["pages"], # только коллекция pages # опционально ограничения по времени и IP # "expires_at": 1767225600, # unix timestamp # "value": "...", # можно задать своё значение, если нужно } key = client.keys.create(params) print(key) if __name__ == "__main__": create_search_only_key()
После запуска получишь JSON с новым ключом вида:
{ "id": 3, "value": "SEARCH_ONLY_KEY_XXX", "actions": ["documents:search"], "collections": ["pages"], ... }
value - тот ключ, который вставляем в Flask.# ts_client_search.py import typesense TYPESENSE_SEARCH_API_KEY = "SEARCH_ONLY_KEY_XXX" client = typesense.Client({ "nodes": [ { "host": "100.64.0.10", # Tailscale IP Typesense-сервера "port": "8108", "protocol": "http", } ], "api_key": TYPESENSE_SEARCH_API_KEY, "connection_timeout_seconds": 2, })
Во всех Flask-частях (/search, фасеты, подсказки и т.д.) используем ts_client_search.client.В индексирующих скриптах (initial_index.py, reindex_single_page.py, настройка синонимов, overrides) оставляем admin-клиент.
Тут есть два варианта:
Проще и логичнее - вариант 1: изменяем наши индексирующие скрипты (Часть 2), чтобы они использовали удалённый ts_client_remote.
Пример: в initial_index.py меняем импорт:
# initial_index.py (замена клиента) from ts_client_remote import client as ts_client
Всё остальное остаётся прежним:
fetch_batch тянет из локального MySQL;index_batch отправляет документы в Typesense по Tailscale.Если сеть не очень быстрая:
BATCH_SIZE, например, до 100–200;Теперь нужно поддерживать Typesense в актуальном состоянии, когда:
Самый точный вариант:
Пример интеграции при сохранении страницы:
# services/search_index.py from ts_client_remote import client as ts_client from mapper import row_to_typesense_doc # как в Части 2 from db import get_connection COLLECTION_NAME = "pages" def reindex_page(page_id: int): conn = get_connection() try: with conn.cursor() as cursor: cursor.execute( """ SELECT id, slug, title, content, tags, published_at, popularity FROM pages WHERE id = %s """, (page_id,), ) row = cursor.fetchone() finally: conn.close() if not row: # Страницы нет в БД — удалим её из Typesense, если была try: ts_client.collections[COLLECTION_NAME].documents[str(page_id)].delete() except Exception: # если в индексе её уже нет — игнорируем pass return doc = row_to_typesense_doc(row) ts_client.collections[COLLECTION_NAME].documents.upsert(doc)
В обработчике сохранения (например, в админке):
# где-то в коде после commit в MySQL reindex_page(page_id)
updated_atЕсли в таблице pages есть поле updated_at, можно:
search_sync или просто файл с отметкой последней синхронизации.SELECT * FROM pages WHERE updated_at > last_sync_time;
upsert в Typesense.Схема скрипта:
# incremental_sync.py from datetime import datetime from db import get_connection from ts_client_remote import client as ts_client from mapper import row_to_typesense_doc COLLECTION_NAME = "pages" BATCH_SIZE = 200 def get_last_sync_time() -> datetime: # тут можно читать из отдельной таблицы или файла. # Для примера возьмём "очень старое" время. return datetime(1970, 1, 1) def set_last_sync_time(dt: datetime): # аналогично — сохраняем куда-то (таблица, файл, etc.) pass def incremental_sync(): last_sync = get_last_sync_time() conn = get_connection() try: with conn.cursor() as cursor: # берём только изменённые после last_sync cursor.execute( """ SELECT id, slug, title, content, tags, published_at, popularity, updated_at FROM pages WHERE updated_at > %s ORDER BY updated_at ASC """, (last_sync,), ) rows = cursor.fetchall() finally: conn.close() if not rows: print("Нет изменений.") return docs = [row_to_typesense_doc(r) for r in rows] ts_client.collections[COLLECTION_NAME].documents.import_( docs, {"action": "upsert"}, batch_size=BATCH_SIZE, ) new_last_sync = max(r["updated_at"] for r in rows) set_last_sync_time(new_last_sync) print(f"Синхронизировано {len(rows)} документов, last_sync = {new_last_sync}") if __name__ == "__main__": incremental_sync()
Имеет смысл выносить Typesense на отдельный сервер, если:
Можно оставить всё на одном сервере, если: