Web-scale search engine (Google-like). Crawler -> URL frontier (Kafka) -> fetcher -> parser -> bulk indexer -> sharded inverted Lucene index (150 shards × RF 3). Query path: API gateway -> query cache -> spell corrector -> query rewriter -> coordinator (scatter-gather) -> shards (BM25) -> PageRank static scores -> ML re-ranker (LambdaMART). Auto-suggest service backed by in-RAM prefix trie. Semantic search via embedding model + HNSW vector index, fused with BM25 via Reciprocal Rank Fusion. Five scenarios: crawl-and-index a new page, search query (BM25 + ML rerank), auto-suggest while typing, spell correction "googel" -> "google", hybrid vector + lexical retrieval. Two ADRs: inverted index sharding strategy (document-partitioning vs term-partitioning), BM25 vs vector vs hybrid retrieval.
Поисковый движок появляется не только в Google-подобных продуктах. Он нужен маркетплейсу для поиска товаров, корпоративной базе знаний для поиска документов, облачной платформе для поиска логов, соцсети для поиска людей и постов. На интервью это хороший кейс, потому что он соединяет ingestion pipeline, индексирование, sharding, ranking, cache, ML reranking и graceful degradation.
Ключевая идея: поиск не читает исходную OLTP-базу напрямую. Система строит отдельную поисковую проекцию данных: инвертированный индекс, doc values для фильтров, словари терминов, позиции слов, popularity signals и иногда векторные embeddings. Поэтому проектирование поиска всегда начинается с вопроса: какие документы входят в корпус, насколько быстро изменения должны становиться видимыми, какая точность результата важнее latency и как пользовательские сигналы возвращаются в ранжирование.
Практический масштаб для этого кейса: около 1 млрд документов по 10 КБ, индекс 15 ТБ без реплик и примерно 45 ТБ при replication factor 3. Пиковая нагрузка может быть сотни тысяч запросов в секунду, а p95 желательно держать ниже 500 мс. Это заставляет разбивать индекс на шарды, держать координаторы scatter-gather, кэшировать популярные запросы и ограничивать хвостовые задержки медленных shard replicas.
Связанные темы: ::concept{slug="elasticsearch"}, ::concept{slug="sharding"}, ::concept{slug="message-queues"}, ::concept{slug="change-data-capture"}, ::concept{slug="caching-patterns"}.
Ментальная модель поиска: это две почти независимые магистрали. Первая магистраль пишет данные: source DB или crawler публикует изменения, CDC отправляет события в очередь, transformer нормализует документ, bulk indexer кладет его в Lucene/Elasticsearch/OpenSearch. Вторая магистраль читает: пользовательский запрос идет через gateway, query rewriter, cache, coordinator, shard fan-out, merge top-K и reranker.
Инвертированный индекс отвечает на вопрос не "какие документы лежат в таблице", а "в каких документах встречается термин". Для фразы "kafka exactly once" движок находит postings list для каждого термина, пересекает или объединяет списки, учитывает позиции, считает BM25, фильтрует по metadata и возвращает локальный top-K на каждом shard. Coordinator затем сливает локальные top-K в глобальный top-K. Если есть semantic search, отдельный ANN index ищет ближайшие embeddings, а результат смешивается с BM25 через RRF или ML модель.
Важно не путать freshness и consistency. Поиск часто near-real-time: запись в source DB уже успешна, но в поисковом индексе появится через секунды. Для e-commerce это обычно приемлемо, для банковской операции нет. Поэтому поисковый индекс надо рассматривать как read model, а не primary storage.
Диаграмма показывает полный путь от изменения документа до выдачи результата. В ingest path есть CDC, Kafka topic, transformer и bulk indexer. Эта часть объясняет, почему массовая индексация делается батчами, почему очередь сглаживает пики и почему transformer лучше отделять от базы: извлечение текста из PDF, enrichment, language detection и embedding generation могут быть тяжелыми и нестабильными.
Query path показывает gateway, query cache, coordinator, shards и reranker. Coordinator не хранит весь индекс: он знает routing и рассылает запросы на relevant shards. Каждый shard возвращает локальный набор кандидатов. Затем coordinator сливает scores, применяет pagination, facets, snippets и отдает top results. Reranker обычно работает только на top-100 или top-1000, потому что ML scoring всех документов невозможен по latency.
На диаграмме также видны cache и ML-компоненты. Cache полезен для head queries: "iphone", "kafka", "refund policy". Но cache не решает long tail, поэтому shard latency и merge algorithm остаются критичными. ML reranker улучшает качество, но добавляет failure mode: если модель недоступна, система должна уметь вернуть BM25-only результат.
Happy path query учит scatter-gather. Запрос попадает в cache, получает miss, идет к coordinator, рассылается по shard replicas, собирает top-K и возвращает результат. Главный урок: latency определяется не средним shard response, а самым медленным участником fan-out. Если query трогает 100 shards, даже редкие p99 spikes становятся обычным явлением.
Real-time indexing показывает CDC -> queue -> transform -> bulk index. Здесь важно выбирать refresh interval. Чем чаще refresh, тем быстрее документ виден в поиске, но тем выше нагрузка на Lucene segments и merge. Для product search можно принять 1-5 секунд freshness, для log search иногда нужны сотни миллисекунд, для archive search можно индексировать минутами.
Hot shard timeout показывает degraded response. Если один shard завис, coordinator может вернуть partial result с признаком failed shard. Это лучше, чем положить весь поиск, но dangerous для compliance и поиска по security logs: пользователь может не увидеть критический документ. Поэтому продуктово надо явно решить, где partial response допустим.
Hybrid semantic scenario показывает смешивание BM25 и vector search. BM25 хорошо ловит exact terms и фильтры, vector search помогает с синонимами и intent. Но embeddings дороже, сложнее обновляются и хуже объясняются. Обычно hybrid дает лучший UX, но требует offline evaluation и click feedback.
Главный trade-off: качество против задержки. Больше shards, synonyms, personalization и ML reranking повышают recall и relevance, но увеличивают fan-out, CPU и хвостовые задержки. Наоборот, агрессивный cache и маленький candidate set ускоряют выдачу, но могут ухудшить long-tail queries.
Второй trade-off: freshness против write cost. Почти мгновенный refresh создает много маленьких Lucene segments, провоцирует merge pressure и может замедлить поиск. Батчевый bulk indexing эффективнее, но пользователь дольше ждет появления документа.
Третий trade-off: shard size. Большие shards проще администрировать и меньше нагружают cluster state, но медленнее восстанавливаются и дают тяжелые queries. Маленькие shards лучше параллелятся, но создают metadata overhead и повышают вероятность fan-out explosion. Для Lucene часто разумно держать shard десятки гигабайт, а не терабайты.
Четвертый trade-off: denormalization. Поиск быстрее, когда документ содержит все нужные поля, но изменение автора, категории или permissions требует reindex многих документов. Если ACL проверять после поиска, можно получить security leak через count/facet; если встроить ACL в индекс, сложнее обновлять права.
Elasticsearch и OpenSearch используют Lucene, inverted index, segments, refresh, replicas, routing и coordinator nodes. Solr тоже построен на Lucene и часто встречается в enterprise search. Algolia продает managed search с сильным фокусом на latency и ranking UX. Typesense и Meilisearch популярны для более простых product search сценариев.
Google Search намного сложнее: crawler, canonicalization, link graph, spam detection, serving index, ML ranking и огромная инфраструктура freshness. Но базовые идеи совпадают: build index offline/nearline, serve queries from specialized indexes, separate ranking layers.
Для логов похожая архитектура есть в Elasticsearch, Loki, ClickHouse-based search и Splunk. Разница в data model: logs часто append-only, фильтруются по времени и labels, а не по сложному natural language relevance.
Первый anti-pattern: искать полнотекстом в OLTP-базе при большом корпусе и высокой QPS. Postgres tsvector отлично подходит для ранней стадии, но на миллиарде документов и сложном ranking нужна отдельная search serving система.
Второй anti-pattern: считать, что replicas увеличивают ingest throughput. Replica помогает read availability и read QPS, но запись должна пройти primary и replication, а merge cost остается реальным.
Третий anti-pattern: делать offset pagination на глубоких страницах. from=100000&size=10 заставляет shards собирать огромные промежуточные top-N. Для глубокой навигации нужны search_after, cursor или ограничение UX.
Четвертый anti-pattern: не проектировать permissions. Enterprise search без корректного ACL filtering опасен: пользователь может увидеть название, snippet или facet по документу, который не должен знать.
Пятый anti-pattern: смешивать online scoring и offline training без observability. Если клики, impressions и query logs не связаны с ranking version, невозможно понять, улучшила ли модель выдачу.
Не стоит поднимать Elasticsearch, если корпус маленький и требования простые. Для десятков тысяч документов Postgres full-text search с GIN индексом может быть дешевле, надежнее и проще.
Не стоит использовать full-text search как transactional source of truth. Индекс может отставать, пересобираться, терять часть shard response и возвращать eventual view. Истинные права, платежи, inventory и банковские балансы должны жить в primary storage.
Не стоит использовать vector search как замену всем фильтрам. Embeddings хороши для semantic similarity, но плохо заменяют exact filters, ACL, dates, facets и numeric constraints.
Не стоит делать глобальный web-scale crawler для продукта, которому нужен поиск по собственному каталогу. Crawl, dedup, robots.txt, spam и canonicalization резко усложняют систему.
Начать стоит с Lucene inverted index, BM25, segment merge, refresh interval и doc values. Затем изучить Elasticsearch shard allocation, replicas, routing, query cache и aggregations. Для distributed part полезны ::concept{slug="sharding"}, ::concept{slug="replication"} и ::concept{slug="change-data-capture"}. Для качества выдачи читать про learning-to-rank, click models, reciprocal rank fusion и hybrid search. Для reliability смотреть circuit breakers, timeout budget, partial response policy и reindex strategy.