Полнотекстовый поиск: почему LIKE и tsvector не хватает
Каждое приложение рано или поздно нуждается в поиске. Сначала хватает WHERE name ILIKE '%query%' — и это работает на тысяче записей. Потом добавляется tsvector в PostgreSQL — и это работает на миллионе. Но когда нужна мгновенная подсказка при вводе (typeahead), толерантность к опечаткам, фасетная фильтрация, синонимы и ранжирование по релевантности — PostgreSQL уже не справляется.
Три движка доминируют в 2026 году: Elasticsearch — ветеран с огромной экосистемой, Meilisearch — минималистичный и быстрый из коробки, Typesense — позиционируется как «Algolia, которую можно захостить самому». Каждый решает задачу полнотекстового поиска, но подход, сложность и производительность радикально отличаются.
Я работал с Elasticsearch на двух e-commerce проектах (каталог товаров, 2M+ документов), Meilisearch подключал к Rails-портфолио для поиска по статьям, Typesense тестировал на проекте с HR-системой для поиска по резюме. Вот честное сравнение.
Архитектура и философия
Elasticsearch — швейцарский нож
Elasticsearch построен на Apache Lucene. Это не просто поисковый движок — это распределённая система для хранения, поиска и аналитики. Кластеры, шарды, реплики, агрегации, пайплайны обработки — всё из коробки.
┌────────────────────────────────────────────┐
│ Elasticsearch Cluster │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Node 1 │ │ Node 2 │ │ Node 3 │ │
│ │ Shard 0P │ │ Shard 1P │ │ Shard 2P │ │
│ │ Shard 1R │ │ Shard 2R │ │ Shard 0R │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└────────────────────────────────────────────┘
Плюс: масштабируется горизонтально на петабайты. Минус: минимальный production-кластер — 3 ноды, каждая хочет 4-8 GB RAM. Для поиска по 10K документов это как нанять самосвал для перевозки рюкзака.
Meilisearch — простота прежде всего
Meilisearch — один бинарник на Rust. Скачал, запустил, работает. Нет кластеров (до версии 1.x), нет шардов, нет конфигурации Lucene. Один процесс, один файл данных.
# Установка и запуск — 2 команды
curl -L https://install.meilisearch.com | sh
./meilisearch --master-key="your-secret-key"
# Готово. Слушает на :7700
Философия: 95% приложений не нужны петабайты и 50-нодовые кластеры. Им нужен быстрый поиск с опечатками, который работает из коробки.
Typesense — баланс между простотой и мощью
Typesense написан на C++, оптимизирован для скорости. Поддерживает кластеризацию (Raft consensus), но может работать как один процесс. Позиционируется как self-hosted альтернатива Algolia.
# Docker
docker run -p 8108:8108 \
-v /data/typesense:/data \
typesense/typesense:27.1 \
--data-dir=/data \
--api-key=your-secret-key \
--enable-cors
Сравнение архитектуры
| Аспект | Elasticsearch | Meilisearch | Typesense |
|---|---|---|---|
| Язык | Java (Lucene) | Rust | C++ |
| Кластеризация | Да (встроенная) | Нет (single node) | Да (Raft) |
| Минимум RAM | 4 GB | 256 MB | 256 MB |
| Хранение | На диске (Lucene segments) | На диске (LMDB) | В памяти + на диске |
| Лицензия | SSPL / Elastic License | MIT | GPL-3.0 |
| Cloud-сервис | Elastic Cloud | Meilisearch Cloud | Typesense Cloud |
Установка и настройка
Elasticsearch
# docker-compose.yml
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.15.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
ports:
- "9200:9200"
volumes:
- es_data:/usr/share/elasticsearch/data
deploy:
resources:
limits:
memory: 4G
volumes:
es_data:
# Проверка
curl http://localhost:9200
# {"name": "...", "version": {"number": "8.15.0"}, ...}
Meilisearch
services:
meilisearch:
image: getmeili/meilisearch:v1.10
ports:
- "7700:7700"
volumes:
- meili_data:/meili_data
environment:
MEILI_MASTER_KEY: "your-master-key-min-16-chars"
MEILI_ENV: production
volumes:
meili_data:
Typesense
services:
typesense:
image: typesense/typesense:27.1
ports:
- "8108:8108"
volumes:
- typesense_data:/data
command: >
--data-dir=/data
--api-key=your-api-key
--enable-cors
volumes:
typesense_data:
Индексация данных
Elasticsearch: mappings и analyzers
# Создание индекса с маппингом
curl -X PUT "localhost:9200/products" -H 'Content-Type: application/json' -d'
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0,
"analysis": {
"analyzer": {
"product_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": ["lowercase", "snowball_ru", "snowball_en", "synonym_filter"]
}
},
"filter": {
"snowball_ru": {
"type": "snowball",
"language": "Russian"
},
"snowball_en": {
"type": "snowball",
"language": "English"
},
"synonym_filter": {
"type": "synonym",
"synonyms": [
"телефон, смартфон, мобильник",
"ноутбук, лэптоп, ноут"
]
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "product_analyzer",
"fields": {
"keyword": { "type": "keyword" },
"suggest": {
"type": "search_as_you_type"
}
}
},
"description": {
"type": "text",
"analyzer": "product_analyzer"
},
"category": { "type": "keyword" },
"brand": { "type": "keyword" },
"price": { "type": "float" },
"in_stock": { "type": "boolean" },
"rating": { "type": "float" },
"created_at": { "type": "date" }
}
}
}'
# Индексация документа
curl -X POST "localhost:9200/products/_doc/1" -H 'Content-Type: application/json' -d'
{
"name": "iPhone 16 Pro Max 256GB",
"description": "Флагманский смартфон Apple с чипом A18 Pro",
"category": "smartphones",
"brand": "Apple",
"price": 1199.99,
"in_stock": true,
"rating": 4.8,
"created_at": "2026-05-01T00:00:00Z"
}'
# Bulk-индексация
curl -X POST "localhost:9200/products/_bulk" -H 'Content-Type: application/x-ndjson' -d'
{"index": {"_id": "2"}}
{"name": "Samsung Galaxy S26 Ultra", "category": "smartphones", "brand": "Samsung", "price": 1099.99, "in_stock": true, "rating": 4.7}
{"index": {"_id": "3"}}
{"name": "MacBook Pro 16 M4 Max", "category": "laptops", "brand": "Apple", "price": 2499.99, "in_stock": false, "rating": 4.9}
'
Elasticsearch требует явного маппинга для production. Без него — dynamic mapping может создать неоптимальные типы. Настройка анализаторов — это отдельная наука: токенизаторы, фильтры, стемминг для каждого языка.
Meilisearch: zero-config индексация
# Создание индекса и добавление документов — один запрос
curl -X POST "localhost:7700/indexes/products/documents" \
-H "Authorization: Bearer your-master-key" \
-H "Content-Type: application/json" \
-d '[
{
"id": 1,
"name": "iPhone 16 Pro Max 256GB",
"description": "Флагманский смартфон Apple с чипом A18 Pro",
"category": "smartphones",
"brand": "Apple",
"price": 1199.99,
"in_stock": true,
"rating": 4.8
},
{
"id": 2,
"name": "Samsung Galaxy S26 Ultra",
"category": "smartphones",
"brand": "Samsung",
"price": 1099.99,
"in_stock": true,
"rating": 4.7
}
]'
Никакого маппинга. Meilisearch автоматически определяет типы полей и настраивает поиск. Для 90% случаев этого достаточно.
# Настройка поисковых полей и фильтров
curl -X PATCH "localhost:7700/indexes/products/settings" \
-H "Authorization: Bearer your-master-key" \
-H "Content-Type: application/json" \
-d '{
"searchableAttributes": ["name", "description", "brand"],
"filterableAttributes": ["category", "brand", "price", "in_stock"],
"sortableAttributes": ["price", "rating"],
"rankingRules": [
"words", "typo", "proximity", "attribute", "sort", "exactness"
],
"typoTolerance": {
"enabled": true,
"minWordSizeForTypos": {
"oneTypo": 4,
"twoTypos": 8
}
}
}'
Typesense: типизированная схема
# Создание коллекции (аналог индекса) с явной схемой
curl -X POST "localhost:8108/collections" \
-H "X-TYPESENSE-API-KEY: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"name": "products",
"fields": [
{"name": "name", "type": "string"},
{"name": "description", "type": "string"},
{"name": "category", "type": "string", "facet": true},
{"name": "brand", "type": "string", "facet": true},
{"name": "price", "type": "float"},
{"name": "in_stock", "type": "bool", "facet": true},
{"name": "rating", "type": "float"}
],
"default_sorting_field": "rating"
}'
# Добавление документов
curl -X POST "localhost:8108/collections/products/documents/import" \
-H "X-TYPESENSE-API-KEY: your-api-key" \
-H "Content-Type: application/json" \
-d '{"id": "1", "name": "iPhone 16 Pro Max 256GB", "description": "Флагманский смартфон Apple", "category": "smartphones", "brand": "Apple", "price": 1199.99, "in_stock": true, "rating": 4.8}
{"id": "2", "name": "Samsung Galaxy S26 Ultra", "description": "Флагман Samsung", "category": "smartphones", "brand": "Samsung", "price": 1099.99, "in_stock": true, "rating": 4.7}'
Typesense требует схему, но она проще чем маппинг Elasticsearch. Нет анализаторов — Typesense автоматически обрабатывает токенизацию и опечатки для 30+ языков.
Поиск: синтаксис и возможности
Elasticsearch: мощный Query DSL
# Полнотекстовый поиск с фильтрацией и агрегациями
curl -X POST "localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "смартфон apple",
"fields": ["name^3", "description", "brand^2"],
"type": "best_fields",
"fuzziness": "AUTO"
}
}
],
"filter": [
{ "term": { "in_stock": true } },
{ "range": { "price": { "gte": 500, "lte": 2000 } } }
]
}
},
"highlight": {
"fields": {
"name": {},
"description": { "fragment_size": 150 }
}
},
"aggs": {
"categories": {
"terms": { "field": "category", "size": 10 }
},
"brands": {
"terms": { "field": "brand", "size": 10 }
},
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{ "to": 500 },
{ "from": 500, "to": 1000 },
{ "from": 1000, "to": 2000 },
{ "from": 2000 }
]
}
},
"avg_rating": {
"avg": { "field": "rating" }
}
},
"sort": [
{ "_score": "desc" },
{ "rating": "desc" }
],
"from": 0,
"size": 20
}'
Elasticsearch Query DSL — самый мощный из трёх. Bool queries, nested queries, functionscore для кастомного ранжирования, scriptscore для динамических формул, aggregations на уровне аналитической базы данных. Но и самый многословный — простой поиск требует 30+ строк JSON.
Meilisearch: минималистичный API
# Поиск — одна строка
curl "localhost:7700/indexes/products/search" \
-H "Authorization: Bearer your-master-key" \
-H "Content-Type: application/json" \
-d '{
"q": "смартфон apple",
"filter": "in_stock = true AND price >= 500 AND price <= 2000",
"facets": ["category", "brand"],
"sort": ["rating:desc"],
"limit": 20,
"offset": 0,
"attributesToHighlight": ["name", "description"],
"highlightPreTag": "<mark>",
"highlightPostTag": "</mark>"
}'
Ответ:
{
"hits": [
{
"id": 1,
"name": "iPhone 16 Pro Max 256GB",
"description": "Флагманский смартфон Apple с чипом A18 Pro",
"brand": "Apple",
"price": 1199.99,
"_formatted": {
"name": "iPhone 16 Pro Max 256GB",
"description": "Флагманский <mark>смартфон</mark> <mark>Apple</mark> с чипом A18 Pro"
}
}
],
"query": "смартфон apple",
"processingTimeMs": 2,
"estimatedTotalHits": 1,
"facetDistribution": {
"category": { "smartphones": 1 },
"brand": { "Apple": 1 }
}
}
2 миллисекунды. Опечатки обрабатываются автоматически: «smatfon» найдёт «смартфон». Фасетная фильтрация встроена.
Typesense: типизированный поиск
curl "localhost:8108/collections/products/documents/search" \
-H "X-TYPESENSE-API-KEY: your-api-key" \
-G \
--data-urlencode "q=смартфон apple" \
--data-urlencode "query_by=name,description,brand" \
--data-urlencode "filter_by=in_stock:true && price:=[500..2000]" \
--data-urlencode "facet_by=category,brand" \
--data-urlencode "sort_by=_text_match:desc,rating:desc" \
--data-urlencode "per_page=20" \
--data-urlencode "highlight_full_fields=name,description"
Typesense использует GET-параметры вместо JSON body (хотя POST тоже работает). Синтаксис фильтрации price:=[500..2000] компактнее чем у Elasticsearch, но менее гибкий.
Толерантность к опечаткам (Typo Tolerance)
Один из главных критериев для пользовательского поиска.
Тест: поиск с опечатками
| Запрос | Elasticsearch | Meilisearch | Typesense |
|---|---|---|---|
| "iphone" (точный) | Да | Да | Да |
| "iphon" (пропущена буква) | Да (fuzziness:AUTO) | Да | Да |
| "iphonee" (лишняя буква) | Да | Да | Да |
| "iphnoe" (перестановка) | Нет* | Да | Да |
| "айфон" (транслитерация) | Нет** | Нет** | Нет** |
| "смартофн" (опечатка в кириллице) | Частично | Да | Да |
*Elasticsearch fuzziness по умолчанию считает edit distance, а не перестановки. Можно настроить через custom analyzer.
**Транслитерация — ни один из трёх не поддерживает из коробки. Нужен synonym filter или custom preprocessor.
Meilisearch и Typesense значительно лучше с опечатками из коробки. Elasticsearch требует тонкой настройки fuzziness, phonetic analysis, character filters.
Фасетная фильтрация
Elasticsearch
{
"aggs": {
"categories": {
"terms": { "field": "category", "size": 50 }
},
"price_stats": {
"stats": { "field": "price" }
},
"brand_with_count": {
"terms": {
"field": "brand",
"size": 20,
"order": { "_count": "desc" }
}
},
"price_histogram": {
"histogram": {
"field": "price",
"interval": 100,
"min_doc_count": 1
}
}
}
}
Elasticsearch aggregations — самые мощные. Histograms, date histograms, nested aggs, pipeline aggs, percentiles, cardinality. Это полноценный аналитический движок.
Meilisearch
{
"facets": ["category", "brand", "in_stock"]
}
Meilisearch отдаёт только facetDistribution — количество документов по каждому значению. Нет гистограмм, нет вложенных фасетов, нет статистики по числовым полям. Для каталога товаров — достаточно. Для аналитики — нет.
Typesense
facet_by=category,brand,price(100:0:2000)
Typesense поддерживает числовые range facets: price(100:0:2000) — гистограмма с шагом 100 от 0 до 2000. Богаче чем Meilisearch, но до Elasticsearch далеко.
Интеграция с Rails
Elasticsearch: гем searchkick
# Gemfile
gem 'searchkick'
# app/models/product.rb
class Product < ApplicationRecord
searchkick language: "russian",
word_start: [:name],
suggest: [:name],
callbacks: :async
scope :search_import, -> { includes(:category, :brand) }
def search_data
{
name: name,
description: description,
category: category.name,
brand: brand.name,
price: price.to_f,
in_stock: in_stock?,
rating: rating.to_f,
created_at: created_at,
}
end
end
# Поиск
results = Product.search(
"смартфон apple",
where: {
in_stock: true,
price: { gte: 500, lte: 2000 },
},
aggs: [:category, :brand],
order: { rating: :desc },
page: 1,
per_page: 20,
highlight: true,
)
results.each do |product|
puts product.name
puts product.search_highlights[:name] # <em>Apple</em> iPhone...
end
# Фасеты
results.aggs["category"] # {"smartphones" => 15, "laptops" => 8, ...}
Searchkick — отличная абстракция. Скрывает сложность Elasticsearch Query DSL за Ruby-интерфейсом. Автоматический реиндекс при save, async callbacks через Sidekiq/SolidQueue, suggest для автокомплита.
Meilisearch: гем meilisearch-rails
# Gemfile
gem 'meilisearch-rails'
# config/initializers/meilisearch.rb
MeiliSearch::Rails.configuration = {
meilisearch_url: ENV.fetch('MEILISEARCH_URL', 'http://localhost:7700'),
meilisearch_api_key: ENV.fetch('MEILISEARCH_KEY'),
}
# app/models/product.rb
class Product < ApplicationRecord
include MeiliSearch::Rails
meilisearch do
attribute :name, :description, :category_name, :brand_name, :price, :rating, :in_stock
searchable_attributes [:name, :description, :brand_name]
filterable_attributes [:category_name, :brand_name, :price, :in_stock]
sortable_attributes [:price, :rating]
ranking_rules [
'words', 'typo', 'proximity', 'attribute', 'sort', 'exactness'
]
end
def category_name
category&.name
end
def brand_name
brand&.name
end
end
# Поиск
results = Product.search(
"смартфон apple",
filter: "in_stock = true AND price >= 500",
sort: ["rating:desc"],
facets: ["category_name", "brand_name"],
limit: 20,
)
results.each do |product|
puts product.name
end
Мeнее мощный чем Searchkick, но проще. Нет нативных highlight в Rails-геме — приходится обращаться к raw response.
Typesense: гем typesense
# Gemfile
gem 'typesense'
# config/initializers/typesense.rb
TYPESENSE_CLIENT = Typesense::Client.new(
nodes: [{
host: ENV.fetch('TYPESENSE_HOST', 'localhost'),
port: ENV.fetch('TYPESENSE_PORT', '8108'),
protocol: 'http',
}],
api_key: ENV.fetch('TYPESENSE_API_KEY'),
connection_timeout_seconds: 5,
)
# app/services/typesense_search.rb
class TypesenseSearch
def self.search_products(query, filters: {}, page: 1, per_page: 20)
search_params = {
q: query,
query_by: 'name,description,brand',
filter_by: build_filter(filters),
facet_by: 'category,brand',
sort_by: '_text_match:desc,rating:desc',
per_page: per_page,
page: page,
highlight_full_fields: 'name,description',
}
TYPESENSE_CLIENT
.collections['products']
.documents
.search(search_params)
end
private
def self.build_filter(filters)
parts = []
parts << "in_stock:true" if filters[:in_stock]
parts << "price:=[#{filters[:min_price]}..#{filters[:max_price]}]" if filters[:min_price]
parts << "category:=#{filters[:category]}" if filters[:category]
parts.join(' && ')
end
end
Typesense не имеет Rails-интеграции уровня Searchkick. Нужно писать сервис вручную. Зато полный контроль над запросами.
Производительность: бенчмарки
Условия теста
- Данные: каталог товаров, 1M документов
- Средний размер документа: ~500 байт (name, description, category, brand, price, rating)
- Машина: 4 ядра, 16 GB RAM, SSD
- Каждый движок в Docker, single-node
- 100 конкурентных клиентов, 10 секунд нагрузки
Скорость индексации
| Операция | Elasticsearch | Meilisearch | Typesense |
|---|---|---|---|
| Индексация 1M документов | 45s | 28s | 22s |
| Индексация 1 документа | 12ms | 8ms | 3ms |
| Обновление 1 документа | 15ms | 10ms | 4ms |
| Удаление 1 документа | 8ms | 5ms | 2ms |
Typesense быстрее всех на индексации благодаря in-memory архитектуре. Meilisearch второй — Rust + LMDB эффективнее Java + Lucene для мелких операций.
Скорость поиска (p50 / p99)
| Запрос | Elasticsearch | Meilisearch | Typesense |
|---|---|---|---|
| Простой поиск (1 слово) | 5ms / 25ms | 2ms / 8ms | 1ms / 5ms |
| Фразовый поиск (3 слова) | 12ms / 45ms | 3ms / 12ms | 2ms / 8ms |
| Поиск + фильтр + фасеты | 18ms / 65ms | 5ms / 18ms | 3ms / 12ms |
| Поиск с опечаткой | 15ms / 55ms | 3ms / 10ms | 2ms / 7ms |
| Typeahead (prefix) | 8ms / 30ms | 1ms / 5ms | 1ms / 4ms |
Typesense и Meilisearch быстрее Elasticsearch в 3-10 раз на типичных поисковых запросах. Elasticsearch компенсирует это мощью агрегаций и гибкостью Query DSL.
Использование ресурсов
| Метрика | Elasticsearch | Meilisearch | Typesense |
|---|---|---|---|
| RAM (1M docs) | 2.8 GB | 450 MB | 800 MB |
| Диск (1M docs) | 1.2 GB | 380 MB | 520 MB |
| CPU (idle) | 3-5% | <1% | <1% |
| CPU (100 qps) | 25% | 8% | 6% |
Elasticsearch потребляет в 3-6 раз больше RAM. Это цена за Lucene, JVM и распределённую архитектуру. Для VPS с 2 GB RAM — Meilisearch или Typesense.
Расширенные возможности
Geo-поиск
Elasticsearch:
json
{
"query": {
"bool": {
"filter": {
"geo_distance": {
"distance": "10km",
"location": { "lat": 55.75, "lon": 37.62 }
}
}
}
},
"sort": [
{ "_geo_distance": { "location": { "lat": 55.75, "lon": 37.62 }, "order": "asc" } }
]
}
Meilisearch:
json
{
"filter": "_geoRadius(55.75, 37.62, 10000)",
"sort": ["_geoPoint(55.75, 37.62):asc"]
}
Typesense:
filter_by=location:(55.75, 37.62, 10 km)
sort_by=location(55.75, 37.62):asc
Все три поддерживают geo-поиск. Синтаксис Meilisearch и Typesense проще.
Синонимы
Elasticsearch:
json
{
"settings": {
"analysis": {
"filter": {
"my_synonyms": {
"type": "synonym",
"synonyms": [
"телефон, смартфон, мобильник",
"ноутбук, лэптоп"
]
}
}
}
}
}
Meilisearch:
bash
curl -X PUT "localhost:7700/indexes/products/settings/synonyms" \
-H "Authorization: Bearer key" \
-H "Content-Type: application/json" \
-d '{
"телефон": ["смартфон", "мобильник"],
"ноутбук": ["лэптоп"]
}'
Typesense:
bash
curl -X PUT "localhost:8108/collections/products/synonyms/phone-synonyms" \
-H "X-TYPESENSE-API-KEY: key" \
-d '{
"synonyms": ["телефон", "смартфон", "мобильник"]
}'
Мультиязычный поиск
| Возможность | Elasticsearch | Meilisearch | Typesense |
|---|---|---|---|
| Стемминг | 30+ языков (анализаторы) | Автоматически | Автоматически |
| Кириллица | Через ICU plugin | Из коробки | Из коробки |
| CJK (китайский, японский) | Через kuromoji/smartcn | Из коробки | Ограниченно |
| Отдельный индекс на язык | Рекомендуется | Не нужно | Не нужно |
Meilisearch и Typesense обрабатывают мультиязычный контент автоматически. Elasticsearch требует отдельных анализаторов для каждого языка — что даёт больше контроля, но и больше работы.
Мониторинг и обслуживание
Elasticsearch
# Здоровье кластера
curl localhost:9200/_cluster/health?pretty
# Статистика индекса
curl localhost:9200/products/_stats?pretty
# Медленные запросы
curl localhost:9200/products/_settings -d '{
"index.search.slowlog.threshold.query.warn": "1s",
"index.search.slowlog.threshold.query.info": "500ms"
}'
Elasticsearch имеет богатейший набор метрик: Kibana для визуализации, APM для трейсинга, ML для аномалий. Целая экосистема мониторинга.
Meilisearch
# Статистика
curl localhost:7700/stats -H "Authorization: Bearer key"
# Задачи (индексация асинхронная)
curl localhost:7700/tasks -H "Authorization: Bearer key"
# Здоровье
curl localhost:7700/health
Meilisearch минималистичен в мониторинге. Встроенный дашборд в web-интерфейсе, базовые метрики через API. Для серьёзного мониторинга — подключение Prometheus через экспортер.
Typesense
# Метрики
curl localhost:8108/metrics.json -H "X-TYPESENSE-API-KEY: key"
# Здоровье
curl localhost:8108/health
# Статистика коллекции
curl localhost:8108/collections/products -H "X-TYPESENSE-API-KEY: key"
Typesense отдаёт Prometheus-совместимые метрики из коробки. Подключение к Grafana — стандартное.
Когда что выбирать
Elasticsearch — когда
- Больше 10M документов и нужна горизонтальная масштабируемость
- Сложные агрегации: гистограммы, pipeline aggs, статистика
- Уже есть ELK-стек (логи + метрики + APM)
- Нужны nested documents, parent-child relationships
- Команда имеет экспертизу в Elasticsearch
- Бюджет на инфраструктуру: минимум 3 ноды × 4 GB RAM
Meilisearch — когда
- Поиск по каталогу, статьям, документации — до 10M документов
- Нужен результат за 2 дня, не за 2 недели
- Ограниченный бюджет (VPS с 1-2 GB RAM)
- Толерантность к опечаткам критична (e-commerce, пользовательский поиск)
- Команда без Elasticsearch-экспертизы
- MVP и стартапы: минимум конфигурации, максимум результата
Typesense — когда
- Нужен self-hosted Algolia: мгновенный typeahead, фасеты, опечатки
- High-availability: встроенная Raft-кластеризация (3 ноды)
- Гео-поиск с сортировкой по расстоянию
- Бюджет между Meilisearch (минимум) и Elasticsearch (максимум)
- Нужна скорость: Typesense последовательно быстрее на бенчмарках
Ни один из трёх — когда
- Меньше 100K записей — PostgreSQL
tsvector+pg_trgmдостаточно - Только точный поиск по полям — обычный
WHERE+ индексы - Поиск по логам — лучше ClickHouse или Loki
- Real-time аналитика — ClickHouse, не Elasticsearch
Итоговый чеклист
Выбор движка
- [ ] Определить объём: < 100K → PostgreSQL, < 10M → Meilisearch/Typesense, > 10M → Elasticsearch
- [ ] Определить требования: опечатки, фасеты, geo, агрегации, мультиязычность
- [ ] Оценить бюджет: Elasticsearch = 12+ GB RAM, Meilisearch/Typesense = 1-2 GB
- [ ] Оценить экспертизу команды: Elasticsearch требует опыта настройки
Интеграция
- [ ] Выбрать гем: searchkick (ES), meilisearch-rails (Meili), typesense-ruby (Typesense)
- [ ] Настроить async-индексацию: не блокировать HTTP-запросы при обновлении индекса
- [ ] Настроить searchable attributes: не индексировать лишние поля
- [ ] Настроить filterable attributes: только поля для фильтрации
Production
- [ ] Бэкапы: snapshots (ES), data dump (Meili), export (Typesense)
- [ ] Мониторинг: query latency, indexing lag, memory usage
- [ ] Rate limiting: защита от search abuse
- [ ] Тестирование: relevance тесты — проверять что поиск находит ожидаемые результаты
- [ ] Fallback: при недоступности поискового движка — fallback на PostgreSQL ILIKE
Комментарии (0)