Все уроки
ДанныеПродвинутый13 мин чтенияПолезно знать

Elasticsearch

Полнотекстовый поиск и аналитика на инвертированном индексе, распределённом по шардам.

  • Inverted index
  • Analyzers
  • Shards
  • OpenSearch
Шаг 1/5
Поисковый движок хранит inverted index: как указатель в конце книги — каждое слово ведёт к документам, где оно встречается.

За 30 секунд

Elasticsearch — распределённый поисковый движок на базе Apache Lucene. С помощью анализаторов он превращает текст в термы и хранит их в инвертированном индексе, поэтому может находить и ранжировать подходящие документы за миллисекунды среди миллионов записей. Он также умеет агрегации, логи и — с недавних пор — векторный и гибридный поиск. Актуальная мажорная линейка — 9.x; OpenSearch — его open-source форк.

Не в моём ежедневном стеке — я понимаю принципы и изучу детали, когда это понадобится проекту.

Почему это важно

  • Пользователи ждут поиска, который прощает опечатки, понимает словоформы и ставит лучший результат первым. SQL-запрос с LIKE не справится с этим на объёме; инвертированный индекс с оценкой релевантности справится.
  • Это основа многих стеков логирования и observability (Elastic Stack), где быстрая фильтрация и агрегация огромных объёмов важнее транзакций.
  • Команды выбирают его, когда нужен полный контроль над поиском: свои анализаторы для каждого языка, синонимы, правила бустинга и фасетная навигация — в своей инфраструктуре или как managed-сервис.
  • Он контрастирует с hosted-сервисами поиска вроде Algolia: больше работы по настройке и эксплуатации, но больше гибкости, нет оплаты за каждый поиск и можно держать данные в своей инфраструктуре.

Как это работает

01Идея: инвертированный индекс

Предметный указатель в книге перечисляет слова и страницы, на которых они встречаются. Инвертированный индекс делает то же для документов: для каждого терма он хранит список ID документов, где он встречается, плюс позиции и частоты. Чтобы найти «red running shoes», Elasticsearch берёт три коротких списка и пересекает или объединяет их, вместо того чтобы читать каждый документ. Релевантность по умолчанию считается по BM25: терм весит больше, если он редок в индексе и част в документе, и меньше — в очень длинных полях. Elasticsearch работает в near real-time: новые документы становятся доступны для поиска после refresh, который по умолчанию происходит каждую секунду.

  • Индекс: логическая коллекция документов с одним mapping, как таблица.
  • Документ: JSON-объект, единица индексации и поиска.
  • Терм: нормализованный токен, полученный анализатором; например, «running» становится «run».

02Mapping и анализаторы

Mapping определяет тип каждого поля. Самое важное различие — text против keyword. Поле text анализируется: разбивается на токены, приводится к нижнему регистру, возможно, стеммится, поэтому оно хорошо для полнотекстового поиска, но не для сортировки или точных фильтров. Поле keyword хранится как одно точное значение — подходит для фильтров, сортировки и агрегаций (бренд, статус, SKU). Часто поле маппят обоими способами через подполе. Анализатор — это pipeline: character filters, tokenizer и token filters (lowercase, стоп-слова, стемминг, синонимы). Выбирайте языковые анализаторы (english, german, russian…) для каждого поля. Задавайте mapping явно: dynamic mapping угадывает типы и часто ошибается.

json
PUT /products
{
  "settings": { "number_of_shards": 1, "number_of_replicas": 1 },
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "name":       { "type": "text", "analyzer": "english",
                      "fields": { "raw": { "type": "keyword" } } },
      "brand":      { "type": "keyword" },
      "price":      { "type": "scaled_float", "scaling_factor": 100 },
      "in_stock":   { "type": "boolean" },
      "created_at": { "type": "date" }
    }
  }
}

03Запросы: query context и filter context

Поиск использует Query DSL — язык на JSON. Запрос bool комбинирует условия: must и should влияют на оценку релевантности, а filter и must_not только включают или исключают. Фильтры кэшируются и не считают score, поэтому каждое условие «да/нет» (в наличии, бренд, диапазон цены) кладите в filter, а must оставляйте для того, что ввёл пользователь. multi_match ищет по нескольким полям и позволяет бустить одно из них, например name выше description. fuzziness: "AUTO" прощает опечатки. Агрегации выполняются в том же запросе и строят фасеты, например количество по брендам, так что один вызов возвращает и результаты, и фильтры для сайдбара.

json
GET /products/_search
{
  "query": {
    "bool": {
      "must":   { "multi_match": { "query": "red runing shoes",
                  "fields": ["name^3", "description"], "fuzziness": "AUTO" } },
      "filter": [
        { "term":  { "in_stock": true } },
        { "range": { "price": { "lte": 120 } } }
      ]
    }
  },
  "aggs": { "brands": { "terms": { "field": "brand", "size": 10 } } },
  "size": 20
}

04Кластер, шарды и реплики

Индекс делится на primary-шарды, каждый из которых — самостоятельный индекс Lucene, и у каждого primary могут быть реплики на других узлах. Шарды — это способ, которым Elasticsearch распределяет данные и распараллеливает запросы; реплики дают failover и дополнительную ёмкость на чтение. Запрос расходится на одну копию каждого шарда, каждый возвращает свои лучшие результаты, а координирующий узел их объединяет. Число primary-шардов фиксируется при создании индекса (изменить его можно через split, shrink или reindex), поэтому рассчитывайте его от ожидаемого объёма: рекомендация Elastic — примерно 10–50 GB на шард и не больше примерно 200 миллионов документов. Слишком много крошечных шардов тратят память; несколько огромных долго восстанавливаются.

  • Здоровье кластера green: все шарды назначены; yellow: не хватает реплик; red: отсутствует primary.
  • Роли узлов: master-eligible, data (hot, warm, cold, frozen), ingest, coordinating, ml.
  • Для продакшен-кластера используйте три master-eligible узла, чтобы избежать раздвоения решений.

05Безопасная индексация из приложения

Ваша основная база остаётся источником истины; Elasticsearch — производная копия для поиска. Индексируйте документы пачками через _bulk API, а не одним запросом на документ. Используйте ID из базы как _id в Elasticsearch, чтобы переиндексация была идемпотентной. Для изменений либо ставьте задачу индексации в очередь после каждой записи (outbox или очередь делают это надёжным), либо используйте change data capture. Всегда читайте и пишите через alias, а не через настоящее имя индекса: когда mapping нужно изменить, создайте новый индекс, переиндексируйте в него и атомарно переключите alias. Безопасность включена по умолчанию с 8.0 (TLS и аутентификация), поэтому используйте API-ключ только с теми привилегиями, которые нужны приложению.

json
POST /_bulk
{ "index": { "_index": "products", "_id": "1001" } }
{ "name": "Trail running shoe", "brand": "acme", "price": 89.9, "in_stock": true }
{ "index": { "_index": "products", "_id": "1002" } }
{ "name": "Road running shoe", "brand": "acme", "price": 99.0, "in_stock": false }
 
POST /_aliases
{ "actions": [
  { "remove": { "index": "products_v1", "alias": "products_live" } },
  { "add":    { "index": "products_v2", "alias": "products_live" } }
] }

06Лицензии, OpenSearch и managed-варианты

В 2021 году Elastic перешёл с Apache 2.0 на SSPL и Elastic License, а AWS форкнул последнюю открытую версию в OpenSearch, которым теперь управляет OpenSearch Software Foundation под крылом Linux Foundation. В 2024 году Elastic добавил AGPLv3 как вариант лицензии для исходного кода. У проектов общие корни и большая часть Query DSL, но они разошлись в функциях, плагинах и клиентских библиотеках, поэтому выберите один и используйте его собственный клиент. Варианты хостинга: Elastic Cloud (включая serverless-предложение), Amazon OpenSearch Service или self-managed кластеры на VM или в Kubernetes (с оператором ECK для Elasticsearch).

Когда применять

  • Поиск по товарам, документам или сайту, где нужны ранжирование по релевантности, устойчивость к опечаткам, синонимы и фасеты.
  • Аналитика логов, метрик и событий, где нужно фильтровать и агрегировать большие объёмы почти в реальном времени.
  • Автодополнение и search-as-you-type по большим каталогам.
  • Гибридный поиск, объединяющий релевантность по ключевым словам и векторное сходство в одном движке.

Когда не стоит

  • Как основная база для транзакционных данных: в нём нет многодокументных транзакций и он eventually consistent.
  • Для небольшого каталога, где полнотекстовый поиск PostgreSQL или hosted-сервис закрывают задачу с гораздо меньшими затратами на эксплуатацию.
  • Тяжёлые реляционные JOIN-ы или требования строгой согласованности.

Альтернативы

  • OpenSearch

    Форк под Apache 2.0; похожий API, сильная интеграция с AWS, своя экосистема плагинов.

  • Algolia

    Hosted поиск как сервис с очень быстрой настройкой и отличной устойчивостью к опечаткам; цена растёт с количеством записей и запросов.

  • Meilisearch / Typesense

    Лёгкие open-source поисковые движки, которые просто запустить; подходят для малых и средних наборов данных.

  • PostgreSQL full-text search

    tsvector и GIN-индексы прямо в вашей базе; для многих приложений достаточно без второй системы.

Лучшие практики

  1. Задавайте явный mapping (dynamic: strict или false) и осознанно маппьте строки как text, keyword или оба варианта.
  2. Кладите условия «да/нет» в filter context, чтобы они кэшировались и не влияли на score.
  3. Всегда обращайтесь к индексам через alias, чтобы переиндексация и изменения mapping проходили без простоя.
  4. Держите шарды примерно 10–50 GB, используйте один primary-шард для маленьких индексов и избегайте тысяч крошечных шардов.
  5. Индексируйте через _bulk API и используйте ID из базы как _id для идемпотентных обновлений.
  6. Для логов используйте data streams и index lifecycle management (ILM), чтобы старые данные уходили на более дешёвые уровни и удалялись по расписанию.
  7. Не отключайте безопасность, используйте API-ключи с ограниченными правами и регулярно делайте snapshot в объектное хранилище.

Частые ошибки

  • Полагаться на dynamic mapping, который создаёт text плюс keyword для каждой строки и непоследовательно маппит числа или даты.
  • Использовать глубокую пагинацию через from и size на тысячи страниц вместо search_after с point in time.
  • Считать Elasticsearch источником истины и терять данные, которых больше нигде нет.
  • Создавать новый индекс на каждый день для каждого клиента и в итоге получить десятки тысяч шардов.
  • Выполнять term-запрос по полю text, который не срабатывает, потому что сохранённые термы проанализированы и приведены к нижнему регистру.

Глубже

07Сегменты, refresh, flush и слияния

Внутри каждого шарда Lucene записывает данные в неизменяемые сегменты. Новые документы попадают в буфер в памяти и в translog (журнал упреждающей записи). Refresh (по умолчанию раз в 1 секунду, и только для индексов, по которым недавно искали) превращает буфер в новый доступный для поиска сегмент; flush фиксирует сегменты на диске и обрезает translog. Поскольку сегменты неизменяемы, обновление на деле — это удаление плюс вставка, а удалённые документы физически убираются только когда фоновые слияния объединяют мелкие сегменты в крупные. Для массовой загрузки поставьте refresh_interval в -1 и реплики в 0, загрузите, затем верните оба значения — это может многократно ускорить индексацию. Избегайте частых обновлений отдельных документов с одними и теми же ID: они создают много крошечных сегментов и удалённых документов.

json
PUT /products_v2/_settings
{ "index": { "refresh_interval": "-1", "number_of_replicas": 0 } }
 
// ...run the bulk load, then:
PUT /products_v2/_settings
{ "index": { "refresh_interval": "1s", "number_of_replicas": 1 } }
POST /products_v2/_refresh

08Векторный, семантический и гибридный поиск

Elasticsearch 8 и 9 поддерживают поля dense_vector с приближённым kNN-поиском на графах HNSW и квантизацией (int8, int4 и BBQ — бинарная квантизация) для экономии памяти. Тип поля semantic_text идёт дальше: вы подключаете inference endpoint, а Elasticsearch при индексации сам разбивает текст на чанки и создаёт эмбеддинги. Retrievers позволяют объединить обычный BM25-запрос с kNN-запросом через reciprocal rank fusion (RRF), который сливает два ранжированных списка по позициям, а не по сырым оценкам. Гибридный поиск обычно лучше каждого метода по отдельности: ключевые слова ловят точные SKU и названия, векторы — смысл. Измеряйте на размеченном наборе запросов до и после.

json
GET /products/_search
{
  "retriever": { "rrf": { "retrievers": [
    { "standard": { "query": { "match": { "name": "waterproof hiking boots" } } } },
    { "knn": { "field": "name_vector", "k": 20, "num_candidates": 100,
               "query_vector_builder": { "text_embedding": {
                 "model_id": "my-embedding-endpoint", "model_text": "waterproof hiking boots" } } } }
  ] } }
}

09Глубокая пагинация и стабильная прокрутка

Пагинация через from + size заставляет каждый шард собрать from + size результатов, а координатор — отсортировать их все, поэтому страница 500 очень дорогая; именно поэтому лимит по умолчанию (index.max_result_window) — 10,000 результатов. Для бесконечной прокрутки у пользователей или экспорта используйте search_after с сортировкой, которая заканчивается уникальным tiebreaker, вместе с point in time (PIT), чтобы выборка не сдвигалась, пока приходят новые документы. Старый scroll API всё ещё доступен, но для глубокой пагинации больше не рекомендуется. Для экспорта очень больших наборов результатов рассмотрите параллельные sliced PIT-запросы.

10Эксплуатация кластера: память, уровни и snapshot

Давайте JVM heap не больше примерно половины RAM и держитесь ниже порога compressed pointers (около 30 GB); остальное использует файловый кэш ОС, на который Lucene сильно опирается. Последние версии подбирают размер heap автоматически, и обычно это правильно. Для данных, привязанных ко времени, используйте уровни hot, warm, cold и frozen вместе с ILM, чтобы старые индексы переезжали на более дешёвое хранилище и в итоге удалялись. Регулярно делайте snapshot в объектное хранилище (S3, GCS, Azure) через snapshot lifecycle management; snapshot инкрементальны и являются вашим настоящим бэкапом, потому что реплики не защищают от удаления индекса. Обновляйтесь rolling restart в пределах мажорной версии и проверяйте deprecation API перед мажорными обновлениями.

Реальные ситуации

01

Поиск ничего не находит по точному SKU

Проблема

Пользователи вставляют в поиск SKU вроде «AB-1234-XL» и не получают результатов, хотя товар существует. SKU замаплен как поле text со стандартным анализатором.

Как решить

Стандартный анализатор разбивает SKU по дефисам и приводит к нижнему регистру, поэтому точная строка никогда не существует как один терм. Добавьте подполе keyword (sku.raw) с lowercase normalizer, переиндексируйте за alias и в поисковый запрос добавьте условие should с term-запросом по sku.raw с высоким бустом, рядом с обычным multi_match. При желании распознавайте ввод, похожий на SKU, регулярным выражением в приложении и сначала делайте точный поиск. Добавьте регрессионный тест с реальными SKU в набор тестов релевантности, чтобы проблема не вернулась после будущих изменений mapping.

02

Кластер стал red после падения узла

Проблема

В кластере из трёх узлов у индексов было ноль реплик ради экономии диска. Ночью один узел вышел из строя, и несколько индексов теперь red, в продакшене ошибки поиска.

Как решить

Red означает, что у primary-шарда нет доступной копии, поэтому его данные недоступны, пока узел не вернётся или вы не восстановите их. Верните узел, если диск цел; иначе восстановите затронутые индексы из последнего snapshot или перестройте их из исходной базы, если Elasticsearch — производный индекс. Используйте cluster allocation explain API, чтобы понять, почему шарды не назначены. После этого поставьте минимум одну реплику на все продакшен-индексы, добавьте дисковое пространство или удаление через ILM, чтобы за это заплатить, настройте snapshot lifecycle management и ставьте алерт на здоровье кластера yellow, а не только red.

03

Медленный фасетный поиск по большому каталогу

Проблема

Поиск с пятью фасетами занимает 1.5 секунды на индексе из десяти миллионов товаров. Профилирование показывает, что больше всего времени уходит на агрегации и scoring.

Как решить

Перенесите все выбранные фасеты в filter context, чтобы они кэшировались и не участвовали в scoring. Используйте поля keyword с doc_values для terms-агрегаций и держите их size небольшим. Для фасетов, которые должны показывать количество с учётом других выборов, используйте post_filter, чтобы агрегации выполнялись по более широкому набору только один раз. Проверьте размер шардов: индекс из 30 шардов по 2 GB без нужды разветвляет запросы, так что сожмите его до меньшего числа шардов. Включите request cache для повторяющихся запросов только с агрегациями. Подтверждайте каждое изменение через profile API и сравнивайте p95 до и после на воспроизведении реальных запросов.

Шпаргалка

Здоровье кластера
GET /_cluster/health?pretty
Список индексов с размерами
GET /_cat/indices?v&s=store.size:desc
Проверить, как анализируется текст
POST /products/_analyze { "field": "name", "text": "Running Shoes" }
Узнать, почему шард не назначен
GET /_cluster/allocation/explain
Переиндексировать в новый индекс
POST /_reindex { "source": { "index": "products_v1" }, "dest": { "index": "products_v2" } }
Атомарное переключение alias
POST /_aliases { "actions": [ { "remove": {...} }, { "add": {...} } ] }
Профилировать медленный запрос
GET /products/_search { "profile": true, "query": { ... } }
Открыть point in time
POST /products/_pit?keep_alive=1m
Создать API-ключ
POST /_security/api_key { "name": "app-search", "role_descriptors": { ... } }

Вопросы и ответы

Q1В чём разница между полями text и keyword?

Поле text анализируется при индексации: анализатор разбивает его на токены, приводит к нижнему регистру и может удалить стоп-слова или выполнить стемминг, так что «Running Shoes» становится термами run и shoe. Это то, что нужно для полнотекстового поиска. Поле keyword хранится как одно точное значение — это нужно для фильтров, сортировки и агрегаций, например бренда или статуса. Term-запрос по полю text обычно не срабатывает, потому что ищет точное непроанализированное значение. Типичный паттерн — поле text с подполем keyword, чтобы можно было и искать, и сортировать или строить фасеты.

Q2Как Elasticsearch ранжирует результаты?

По умолчанию используется BM25 — вероятностная модель релевантности. Для каждого терма запроса учитываются частота терма в документе (больше — лучше, но с убывающей отдачей), обратная частота документов (редкие термы весят больше) и длина поля (совпадение в коротком заголовке весит больше, чем в длинном тексте). Оценки условий из must и should складываются. Релевантность настраивается бустами полей, function_score или rank_feature для бизнес-сигналов вроде популярности, а также синонимами. Условия filter на score не влияют. Для семантической релевантности можно добавить векторный поиск и объединить оба списка через reciprocal rank fusion.

Q3Сколько шардов должно быть у индекса?

Меньше, чем думает большинство. Каждый шард — это индекс Lucene со своими накладными расходами на память и файлы, и каждый поиск затрагивает одну копию каждого шарда. Рекомендация Elastic — шарды примерно от 10 до 50 GB и умеренное общее число шардов на узел. Каталогу в несколько гигабайт нужен один primary-шард и одна реплика. Для растущих логов используйте data streams с rollover по размеру или возрасту, чтобы каждый backing-индекс автоматически получал разумный размер. Если ошиблись, индекс можно разделить (split), сжать (shrink) или переиндексировать за alias.

Q4Как изменить mapping без простоя?

Изменить тип существующего поля на месте нельзя, потому что данные уже проиндексированы в этом виде. Безопасный процесс такой: создать новый индекс (products_v2) с новым mapping, переиндексировать из старого индекса или из исходной базы, держать оба синхронными во время переключения (например, писать в оба или проигрывать изменения), а затем атомарно перенести alias с v1 на v2 одним запросом _aliases. Приложения всегда работают с alias, поэтому ничего не замечают. Старый индекс оставьте на короткое окно для отката, затем удалите. Добавлять новые поля можно без переиндексации.

Q5Elasticsearch — это база данных?

Он надёжно хранит данные и поддерживает запросы, но я бы не использовал его как основную базу для бизнес-записей. В нём нет многодокументных транзакций, записи видны только после refresh, а некоторые операции, например изменение mapping, требуют переиндексации откуда-то. Проблемы кластера также могут сделать восстановление сложнее, чем восстановление реляционного бэкапа. Надёжный паттерн — хранить источник истины в PostgreSQL, MySQL или MongoDB и считать Elasticsearch производным индексом, который можно перестроить в любой момент. Для логов и observability, где данные только дописываются, он приемлем как основное хранилище вместе со snapshot.

Q6Elasticsearch или hosted-сервис вроде Algolia?

Algolia за день даёт отличную устойчивость к опечаткам, мгновенные результаты и продуманную панель управления, и вам никогда не нужно держать серверы. Компромиссы — цена, растущая с количеством записей и поисковых запросов, меньший контроль над внутренностями ранжирования и данные у третьей стороны. Elasticsearch, self-managed или в Elastic Cloud, требует больше настройки и работы над релевантностью, но даёт свои анализаторы, агрегации для аналитики, векторный поиск и предсказуемые затраты на инфраструктуру на масштабе. Для небольшого или среднего магазина с маленькой командой hosted-поиск часто прагматичный выбор; для больших каталогов, сложных фасетов или логов выигрывают Elasticsearch или OpenSearch.

Вопросы уровня senior

S1Как держать Elasticsearch синхронным с реляционной базой на масштабе?

Двойная запись в пути запроса хрупкая: коммит в базу может пройти, а вызов индекса — упасть. Я предпочитаю асинхронный pipeline. Либо приложение пишет строку outbox в той же транзакции, а воркер индексирует из outbox, либо инструмент change data capture вроде Debezium читает binlog или WAL и публикует изменения, которые потребляет индексатор. Индексатор использует ID из базы как _id, внешнее версионирование (version_type external с updated_at или порядковым номером), чтобы события не по порядку не перезаписывали более новые данные, и _bulk для пропускной способности. Я мониторю отставание и держу задачу полной переиндексации за alias для аварийного восстановления и изменений mapping. Компромисс — eventual consistency в несколько секунд, которую поиск обычно терпит.

S2Как измерять и улучшать релевантность поиска?

Релевантность — продуктовая метрика, и относиться к ней нужно соответственно. Соберите judgement list: несколько сотен реальных запросов из логов с документами, которые должны быть наверху, оценённые людьми, знающими предметную область. С помощью rank evaluation API посчитайте метрики вроде nDCG или MRR для текущей конфигурации. Затем меняйте по одной вещи: анализаторы, синонимы, бусты полей, function_score с популярностью или маржой, либо добавление векторного retriever с RRF. Перезапустите оценку и оставляйте изменения, которые её улучшают и не ломают известные запросы. В онлайне отслеживайте click-through, долю запросов без результатов и уходы с поиска, а крупные изменения проверяйте A/B-тестами. Без такого цикла настройка релевантности превращается в мнения, и каждое исправление ломает другой запрос.

S3Elasticsearch или OpenSearch для нового проекта в 2026 году?

Оба — зрелые движки на Lucene с похожими базовыми API, так что решение в основном про экосистему, лицензию и хостинг. Elasticsearch быстрее развивает функции вроде semantic_text, retrievers и векторной квантизации, а Elastic Cloud — сильный managed-вариант; его исходники доступны под AGPL, SSPL или Elastic License. OpenSearch — Apache 2.0 под управлением фонда, тесно интегрирован с AWS и имеет свои плагины для безопасности, алертинга и векторного поиска. Если вы глубоко в AWS и хотите managed-сервис с permissive-лицензией, OpenSearch — естественный выбор. Если нужны новейшие поисковые функции или observability-стек Elastic, выбирайте Elasticsearch. Не смешивайте их клиенты: API разошлись достаточно, чтобы вызывать тонкие баги.

Глоссарий

Инвертированный индекс
Отображение каждого терма на список документов, где он встречается; именно это делает текстовый поиск быстрым.
Анализатор
Pipeline, превращающий текст в термы: character filters, tokenizer и token filters.
Mapping
Схема индекса: имена полей, типы и анализаторы.
Шард
Часть индекса, сама по себе полноценный индекс Lucene; primary хранят данные, реплики копируют их.
BM25
Формула релевантности по умолчанию, основанная на частоте терма, его редкости и длине поля.
Refresh
Операция, которая делает новые проиндексированные документы доступными для поиска, по умолчанию раз в секунду.
Alias
Стабильное имя, указывающее на один или несколько индексов; используется для замены индексов без простоя.
ILM
Index lifecycle management: автоматический rollover, перенос по уровням и удаление индексов с данными по времени.
RRF
Reciprocal rank fusion: слияние ранжированных списков по позициям, используется для гибридного поиска по ключевым словам и векторам.

Есть идея продукта?Давайте создадим его.

Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.