Skip to content
PПромтбук
RUEN
04Архитектура

Реализация поиска: PG full-text vs MeiliSearch vs Algolia vs Elasticsearch

Критерии выбора engine, индексация, ranking, faceting, multi-language, как мерить релевантность.

Действуй как senior search engineer. Выбери и спроектируй search для корпуса {{corpus_size}}, паттерны запросов {{query_patterns}}, языки {{languages}}.

1. Decision matrix: какой engine

EngineБрать когдаНЕ брать когда
PostgreSQL FTS (tsvector/tsquery)< 1M документов, простые запросы, уже есть PG, нет typo toleranceНужен typo tolerance / faceting / instant search / много языков с native ranking
MeiliSearch100K-50M, typo tolerance из коробки, нужен self-hosted, простая операцияСложная агрегация / nested / vector search крупного масштаба
TypesenseАналог Meili, чуть лучше с faceting, instant searchТо же что Meili
Algolia< 50M, нужно "просто работает", готовы платить, prefix-search первоклассный, instant search$$$ при росте; data sovereignty (US-hosted); ограничения на сложные queries
Elasticsearch / OpenSearch> 10M, сложная агрегация, нужна гибкость scoring, log analytics одновременноМаленький проект (операционная сложность непропорциональна)
Vector DB (pgvector / Qdrant / Pinecone)Semantic search, RAG, embeddingsLexical/keyword search в чистом виде

Гибрид (recommended на средних масштабах): PG для structured filtering + dedicated search engine для full-text. Или Meili + pgvector для lexical + semantic.

2. Что измерить ДО выбора

Не выбирай engine "по ощущениям". Замерь:

  • Object count сейчас и через 12 месяцев (с темпом роста).
  • Avg document size — 200 байт title? 50KB body? меняет storage план.
  • Query QPS — 10/s? 10000/s? влияет на need в шардинге.
  • p95 latency target — instant search хочет < 50ms; "найти и подождать" терпит 500ms.
  • % queries с typo — если > 10%, FTS без typo tolerance даст плохой UX.
  • Index update lag tolerance — real-time (< 1s) или eventual (несколько минут)?

3. Индексация

Стратегии

  • Live indexing (write-through): любой write в primary store → событие в search engine. Через outbox pattern + worker, не из request thread.
  • Batch reindex (eventual): cron / job каждые N минут читает изменённое (updated_at > last_seen) и upsert'ит в индекс. Проще, но lag.
  • Full reindex — для schema changes / новых полей. Должен быть online (новый индекс параллельно, alias swap), не downtime.

Что хранить в индексе

  • Searchable fields — text, отдельно по полям с весами (title × 3, body × 1).
  • Filter fieldsstatus, category_id, price для facets / refinements.
  • Display fields — то что нужно показать в результатах без обращения в primary DB (snippet, thumbnail_url). Денормализация — это норма для search index.
  • Sort fieldscreated_at, popularity. Не сортируй на лету по непроиндексированному полю.

Atomic reindex без downtime

1. Create index v2 (новая schema / analyzer)
2. Backfill: stream все docs из primary DB → v2
3. Live writes идут в **оба** v1 и v2
4. Когда v2 caught up — swap alias `current → v2`
5. Stop writes в v1, drop v1 через N часов

4. Ranking

Дефолтный BM25 — хороший старт, но почти всегда надо bias'ить:

  • Field weightstitle^3, tags^2, body^1.
  • Function score / boosting — recent (decay function по created_at), popular (views), premium content (boost: 1.5).
  • Personalization (если есть): user history → re-rank top-N после initial retrieval, не в самом BM25.

Не оптимизируй ranking на основе ощущений. См. п. 7 — мерь.

5. Faceting

  • Counts on filtered set: "Brands (12), Categories (8)" обновляются по результатам текущего фильтра.
  • Multi-select within facet — OR внутри, AND между (brand IN (Nike, Adidas) AND price < 100).
  • Facet limit — top-N по count, "show more" подгружает остальное (не присылай 50K брендов сразу).

PG FTS — facets надо считать отдельным query, медленно. Meili/Algolia/ES — нативно быстро.

6. Multi-language

  • Per-language analyzer — stemming / stopwords специфичны для языка. Английский Porter stemmer на русском тексте даёт мусор.
  • Detection vs тэгинг — храни lang поле явно (определи при загрузке через franc / cld3), не пытайся определять на лету.
  • Стратегии индекса:
    • One index per language — чисто, легко тюнить, но запрос "поверх языков" сложнее.
    • Single index, multi-fieldtitle_ru, title_en с разными analyzer'ами в одном документе. Проще операционно.
  • Кросс-язычный поиск — embeddings (semantic): "running shoes" находит "беговые кроссовки". Lexical здесь не работает.

7. Как мерить релевантность

Без измерения — нет работы над ranking, есть гадание.

Offline (готовая разметка)

  • Набор (query, expected top results) — 50-200 пар, размеченных вручную или из логов "что юзер кликнул".
  • Метрики: NDCG@10, MRR, Recall@K, Precision@K.
  • Запускай после каждого изменения ranking → не ушли ли метрики вниз.

Online (на живых пользователях)

  • CTR на позициюclicks_at_position_i / impressions. Если CTR на топ-3 падает после изменения — стало хуже.
  • Zero-result rate — % запросов с 0 результатами. > 10% — проблема (либо корпус мал, либо typo handling плох).
  • Refinement / abandonment rate — юзер переформулировал / ушёл без клика. Высокий — релевантность плоха.
  • A/B-тест для крупных изменений ranking — двух недель достаточно для значимых сигналов.

Quality канарейка

  • Smoke set из ~20 "контрольных" queries прогоняется на каждый deploy. Если "best coffee maker" перестал возвращать ваш топ-product — alert.

8. Operational concerns

  • Backup / restore time — снапшоты индекса. Для ES — snapshot to S3. Для Meili — file copy. Тест restore раз в квартал.
  • Index size budget — индекс обычно 30-100% от primary data. Запланируй disk + monitoring.
  • Hot data vs cold — для > 100M доков — partition по времени (ES ILM).

Формат вывода

## Choice
**Engine:** <chosen>
**Why:** <2-3 bullets tied to corpus_size / query_patterns / languages>
**Alternatives considered:** <table compact>

## Index schema
```json
{ ... }

Ranking config

  • Field weights: ...
  • Boosts: ...

Indexing strategy

<live / batch / hybrid, latency target>

Quality measurement plan

  • Offline set: <N queries, кто размечает, какие метрики>
  • Online: <CTR @position, zero-result rate, A/B>

Migration plan (если меняем с существующего)

<5-7 шагов с rollback>


## Anti-patterns

- ❌ Elasticsearch для < 100K документов — оверкилл, ops cost > benefit.
- ❌ PG FTS для > 10M + typo tolerance — будет медленно и плохо.
-`LIKE '%query%'` как "поиск" в production — full table scan каждый раз.
- ❌ Reindex с downtime — пользователи видят пустоту во время миграции.
- ❌ Без quality measurement — каждое изменение ranking — лотерея, регрессии незаметны.
- ❌ Same analyzer для всех языков — русский ищется как набор латинских символов.
- ❌ Real-time index updates через synchronous write из request thread — write latency растёт + search падает с primary.
- ❌ Хранить только id в индексе и hydrate из БД для каждого hit — теряешь весь смысл search engine.
- ❌ Без zero-result monitoring — % "пусто" растёт незаметно, юзеры уходят.
- ❌ A/B-тест ranking на 100 юзерах — стат-незначимо, шум.
К подразделу «Архитектура»
Похожие промты