Typesense это современный, быстрый и открытый поисковый движок, предназначенный для добавления поиска в приложениях и веб-сайтах. Он разработан с акцентом на простоту использования, скорость работы, релевантность результатов и гибкость. Typesense часто рассматривается как альтернатива Elasticsearch и Algolia, но с более простой настройкой и более дружелюбным интерфейсом.
Он даёт:
Важно: Typesense имеет локализации токенизации и для русского языка - достаточно указать locale: "ru" у нужных строковых полей в схеме коллекции.
Typesense распространяется под лицензией GNU General Public License v3.0. Это означает, что вы можете свободно использовать, модифицировать и распространять его. Скачать бинарник можно по ссылке https://typesense.org/downloads
Цель: сайт на Flask + Bootstrap + MySQL (~100 000 страниц). MySQL остаётся источником истины, Typesense - быстрый поисковый индекс.
Минимальная архитектура на одном сервере:
[Пользователь браузер]
|
v
[Flask-приложение]
| \
| \ (поиск)
| \
v v
[MySQL] [Typesense]
(данные) (поисковый индекс)
Typesense хранит поисковый индекс в RAM, а сырой документ - на диске. Память ≈ 2–3× размер только тех полей, по которым вы ищете/фильтруете.
Допустим, в индекс идёт:
title: ~100 байтcontent_snippet: ~500 байт (обрезанный текст/аннотация)tags: ~50 байтurl: ~100 байтИтого ~750 байт ≈ 0,75 KB на документ.На 100 000 документов: 100000 × 0,75 KB ≈ 75 MB исходных данных.
По формуле 2–3× нужно ~150–225 MB RAM под Typesense-индекс. Даже с запасом 512 MB под Typesense вы вписываетесь.
Практически:
Минимально комфортный сервер для Flask+MySQL+Typesense:
Проще всего поднять Typesense в Docker-контейнере. Typesense - это единый бинарник, официально рекомендуют запускать через Docker или пакеты.
sudo mkdir -p /var/lib/typesense-data docker run -d --name typesense \ -p 8108:8108 \ -v /var/lib/typesense-data:/data \ typesense/typesense:28.0 \ --data-dir /data \ --api-key=SUPER_SECRET_ADMIN_KEY \ --enable-cors
Что здесь происходит:
-p 8108:8108 - публикуем порт API (по умолчанию 8108).-v /var/lib/typesense-data:/data - постоянное хранилище индекса и данных.--api-key=... - задаём админский API-ключ (обязательно меняем на свой).--enable-cors - если потом планируете прямые запросы из браузера (в нашем случае поиск пойдёт через Flask, так что можно и не включать).Проверка:
curl http://localhost:8108/health # ожидаем: {"ok":true}
Устанавливаем официальный Python-клиент (последняя версия 1.3.0, совместима с Typesense ≥ v26).
pip install typesense
# init_typesense_client.py import typesense TYPESENSE_ADMIN_API_KEY = "SUPER_SECRET_ADMIN_KEY" client = typesense.Client({ "nodes": [ { "host": "127.0.0.1", # тот же сервер "port": "8108", "protocol": "http", } ], "api_key": TYPESENSE_ADMIN_API_KEY, "connection_timeout_seconds": 2, }) def check_health(): """ Проверяем, что сервер Typesense жив. """ health = client.health.retrieve() # запрос к /health print(health) # ожидаем {'ok': True} if __name__ == "__main__": check_health()
typesense.Client, указывая список нод (пока одна).health.retrieve() ходит на /health и возвращает JSON со статусом.pages под сайтУ нас есть MySQL-таблица (упрощённо):
CREATE TABLE pages ( id INT PRIMARY KEY AUTO_INCREMENT, slug VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL, content MEDIUMTEXT NOT NULL, tags VARCHAR(255) NULL, published_at DATETIME NOT NULL, popularity INT NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
В Typesense мы создадим коллекцию pages примерно с такими полями:
id - числовой идентификатор (будет ключом документа);title - заголовок (поиск, locale: "ru");slug / url - строка, по которой будем отдавать страницу;content - текст (поиск, locale: "ru");tags - массив строк, используется как фасет (фильтры);published_at - UNIX-timestamp (целое, сортировка/фильтрация);popularity - число для сортировки по популярности.Typesense поддерживает типы string, int32, int64, float, bool и массивы строк/чисел.
pages с поддержкой русского языкаОсновная фишка - указать locale: "ru" для текстовых полей, где нам важен русский. Typesense имеет кастомизации токенизатора для ru, включая морфологические особенности.
# create_pages_collection.py import typesense TYPESENSE_ADMIN_API_KEY = "SUPER_SECRET_ADMIN_KEY" client = typesense.Client({ "nodes": [ { "host": "127.0.0.1", "port": "8108", "protocol": "http", } ], "api_key": TYPESENSE_ADMIN_API_KEY, "connection_timeout_seconds": 2, }) schema = { "name": "pages", "fields": [ # PK документа, по нему будем обновлять/удалять {"name": "id", "type": "int32"}, # Заголовок страницы (русский язык, полнотекстовый) {"name": "title", "type": "string", "locale": "ru"}, # URL / slug для генерации ссылки {"name": "slug", "type": "string"}, # Краткий текст (сниппет), по которому тоже ищем {"name": "content", "type": "string", "locale": "ru"}, # Теги — массив строк, используется как фасет (фильтры по темам) {"name": "tags", "type": "string[]", "facet": True}, # Время публикации в виде unix-timestamp (секунды) {"name": "published_at", "type": "int64", "facet": True}, # Популярность/рейтинг для сортировки (просмотры/клики) {"name": "popularity", "type": "int32", "facet": True}, ], # Поле сортировки по умолчанию (если не указать sort_by при поиске) "default_sorting_field": "popularity", } def create_collection(): # На всякий случай удалим старую коллекцию (в dev-среде) try: client.collections["pages"].delete() print("Старая коллекция 'pages' удалена.") except Exception: # Если нет коллекции — ничего страшного pass created = client.collections.create(schema) print("Создана коллекция:", created) if __name__ == "__main__": create_collection()
Что тут важно:
locale: "ru" для title и content - включает русскоязычную токенизацию.facet: True у tags, published_at, popularity - позже сможем строить фильтры и фасеты.default_sorting_field - базовая сортировка, если клиент не задаёт свою.Позже, в продвинутых частях, сюда можно будет добавить:
stem: true - базовое стеммирование (walk / walked-style для поддерживаемых языков);infix: true - поиск по подстрокам;Сделаем простой маршрут, который проверяет связь Flask => Typesense. Это поможет отловить сетевые/ключевые ошибки на раннем этапе.
# app.py (фрагмент) from flask import Flask, jsonify import typesense app = Flask(__name__) TYPESENSE_ADMIN_API_KEY = "SUPER_SECRET_ADMIN_KEY" typesense_client = typesense.Client({ "nodes": [ { "host": "127.0.0.1", # для удалённого сервера позже будет Tailscale IP "port": "8108", "protocol": "http", } ], "api_key": TYPESENSE_ADMIN_API_KEY, "connection_timeout_seconds": 2, }) @app.route("/typesense-health") def typesense_health(): """ Простой health-check для Typesense. Удобно подвесить под мониторинг или использовать руками. """ try: health = typesense_client.health.retrieve() # {'ok': True} при успехе return jsonify({"ok": bool(health.get("ok", False))}), 200 except Exception as e: return jsonify({"ok": False, "error": str(e)}), 500 if __name__ == "__main__": app.run(debug=True)
{ "ok": true }.