Неполнотекстовый поиск: специфичные возможности Elasticsearch для сложных задач

image

Привет всем, меня зовут Андрей, и я разработчик. Давным-давно — кажется, в прошлую пятницу — у нашей команды был проект, где понадобился поиск по ингредиентам, входящим в состав продуктов. Допустим, в состав колбасы. В самом начале проекта от поиска требовалось не много: показать все рецепты, в которых нужный ингредиент содержится в определенном количестве; повторить для N ингредиентов.

Однако в дальнейшем количество продуктов и ингредиентов планировалось значительно увеличить, а поиск должен был не только справляться с возрастающим объемом данным, но и предоставить дополнительные опции — например, автоматическое составление описания продукта по его превалирующим ингредиентам.

Требования

  • Создать поиск на Elacsticsearch по базе из 50 000 документов минимум.
  • Обеспечить высокую скорость ответа на запросы – менее 300 мс.
  • Добиться того, чтобы запросы имели небольшой объем, и сервис был доступен даже в условиях самого плохого мобильного интернета.
  • Сделать логику поиска максимально интуитивной с точки зрения UX. Речь шла по сути о том, что интерфейс будет отражать логику поиска — и наоборот.
  • Снизить до минимума количество прослоек между элементами системы для более высокой производительности и уменьшения количества зависимостей.
  • Предусмотреть возможность в любой момент дополнить алгоритм новыми условиями (например, автоматической генерацией описания продукта).
  • Сделать дальнейшую поддержку поисковой части проекта максимально простой и удобной.

Мы решили не торопиться и начать с простого.

В первую очередь, мы сохранили все ингредиенты состава продукта в базе данных, получив на первых порах 10 000 записей. К сожалению, уже при таком размере поиск по базе занимал слишком много времени, даже с учетом использования join-ов и индексов. А в ближайшее время количество записей должно было превысить 50 000. К тому же заказчик настаивал на использовании Elasticsearch (далее — ES), поскольку сталкивался с этим инструментом и, судя по всему, испытывал к нему теплые чувства. Мы до этого не работали с ES, но знали о его преимуществах и были согласны с этим выбором, так как, например, планировалось, что у нас будут часто появляться новые записи (по разным оценкам от 50 до 500 в день), которые нужно будет сразу же выдавать пользователю.

От прослоек на уровне драйверов мы решили отказаться и просто использовать REST-запросы, поскольку синхронизация с базой делается только в момент создания документа и больше не нужна. Это было еще одно преимущество — вплоть до отправки поисковых запросов напрямую в ES из браузера.

Мы собрали первый прототип, в котором перенесли структуру из базы данных (PostgreSQL) в документы ES:

{"mappings" : {
	"recipe" : {
		"_source"    : {
			"enabled" : true
		},
		"properties" : {
			"recipe_id"     : {"type" : "integer"},
			"recipe_name"   : {"type" : "text"},
			"ingredients"   : {
				"type" : "nested",
				"properties": {
					"ingredient_id":	"integer",
					"ingredient_name":	"string",
					"manufacturer_id":	"integer",
					"manufacturer_name":	"string",
					"percent": 		"float"
				}
			}
		}
	}
}}

На основе этого маппинга получается примерно следующий документ (показать рабочий из проекта не можем по причине NDA):

{
	"recipe_id": 1,
	"recipe_name": "AAA & BBB",
	"ingredients": [
		{
			"ingredient_id": 1,
			"ingredient_name": "AAA",
			"manufacturer_id": 3,
			"manufacturer_name": "Manufacturer 3",
			"percent": 1
		},
		{
			"ingredient_id": 2,
			"ingredient_name": "BBB",
			"manufacturer_id": 4,
			"manufacturer_name": "Manufacturer 4",
			"percent": 3
		}
	]
}

Все это делалось с использованием пакета Elasticsearch PHP. Расширения для Laravel (Elastiquent, Laravel Scout и т.д.) решили не использовать по одной причине — заказчик требовал высокой производительности, вплоть до того, как уже говорилось выше, что «300 мс для запроса это много». А все пакеты для Laravel выступали лишним оверхедом и замедляли работу. Можно было бы делать напрямую на Guzzle, но мы решили не впадать в крайности.

Сначала простейший поиск по рецептам сделали прямо на массивах. Да, это все было вынесено в конфигурационные файлы, но запрос все равно получался слишком большим. Поиск проходил по вложенным документам (те самые ingredients), по булевым выражениям с использованием «should» и «must», также действовала директива обязательного прохода по вложенным документам — в итоге запрос занимал от ста строк, а его объем был от трех килобайт.

Не стоит забывать и про требования к скорости и размеру ответа — к тому моменту ответы в API были отформатированы таким образом, чтобы увеличить объем полезной информации: ключи в каждом json-объекте были сокращены до одной буквы. Поэтому запросы в ES размером в несколько килобайт становились непозволительной роскошью.

И еще мы на тот момент поняли, что строить гигантские запросы в виде ассоциативных массивов на PHP это какая-то лютая наркомания. К тому же контроллеры становились абсолютно нечитаемыми, смотрите сами:

public function searchSimilar()
{
	/*...*/

	$conditions[] = [
		"nested" =>  [
			"path" =>  "ingredients",
			"score_mode" => "max",
			"query" =>  [
				"bool" =>  [
					"must" =>  [
						["term" => ["ingredients.ingredient_id" => $ingredient_id]],
						["range" => ["ingredients.percent"=>[
							"lte"=>$percent + 5,
							"gte"=>$percent - 5
						]]]
					]
				]
			]
		]
	];

	$parameters['body']['query']['bool']['should'][0]['bool']['should'] = $conditions;

	/*...*/

	$equal_conditions[] = [
		"nested" =>  [
			"path" =>  "flavors",
			"query" =>  [
				"bool" =>  [
					"must" =>  [
						["term" => ["ingredients.percent" => $percent]]
					]
				]
			]
		]
	];

	$parameters['body']['query']['bool']['should'][1]['bool']['must'] = $equal_conditions; 

	/*...*/

	return $this->client->search($parameters);
}

Лирическое отступление: когда речь зашла о nested-полях в документе, оказалось, что мы не можем выполнить запрос вида:

"query": {
	"bool": {
		"nested": {
			"bool": {
				"should": [ ... ]
			}
		}
	}
}

по одной простой причине — нельзя выполнять мультипоиск внутри nested-фильтра. Поэтому пришлось делать таким образом:

"query": {
	"bool": {
		"should": [
			{"nested": {
				"path":  "flavors",
				"score_mode": "max",
				"query": {
					"bool": { ... }
				}
			}}
		]
	}
}

т.е. сначала объявлялся массив условий should, а внутри каждого условия вызывался поиск по nested-полю. С точки зрения Elasticsearch это является более правильным и логичным. В итоге мы и сами увидели, что это логично, когда добавляли дополнительные условия поиска.

И здесь мы открыли для себя гугл шаблоны, встроенные в ES. Выбор пал на Mustache — довольно удобный logic-less шаблонизатор. В него можно было вынести все тело запроса и все передаваемые данные практически без изменений, в результате чего конечный запрос приобрел вид:

{ "template": "template1", "params": params{} } 

Тело шаблона получилось довольно скромным и читаемым — только JSON и директивы самого Mustache. Шаблон хранится в самом Elasticsearch и вызывается по имени.

/* search_similar.mustache */
{
	"query": {
		"bool": {
			"should":  [
				{"bool": {
					"minimum_should_match":  {{ minimumShouldMatch }},
					"should":  [
					{{#ingredientsList}} //особенность mustache в том что здесь проверка на непустой объект ingredientsList
						{{#ingredients}}   //а здесь та же деректива является проходом по массиву ingredients
						{"nested": {
							"path":  "ingredients",
							"score_mode": "max",
							"query": {
								"bool": {
									"must":  [
										{"term": {"ingredients.flavor_id": {{ id }} }},
										{"range": {"ingredients.percent" : {
											"lte": {{ lte }},
											"gte": {{ gte }}
										}}}
									]
								}
							}
						}}
						{{^isLast}},{{/isLast}} // флаг последнего элемента
						{{/ingredients}}
					{{/ingredientsList}}
					]
				}}
			]
		}
	}
}

/* запрос */
{
	"template": "search_similar",
	"params": {
		"minimumShouldMatch": 1,
		"ingredientsList": {
			"ingredients": [
				{"id": 1, "lte": 10, "gte": 5, "isLast": true }
			]
		}
	}
}

В итоге на выходе мы получили шаблон, в который мы просто передавали массив необходимых ингредиентов. По логике запрос мало отличался от, условно, следующего:

SELECT * 
FROM ingredients 
LEFT JOIN recipes ON recipes.id = ingredient.recipe_id 
WHERE ingredients.id in (1,2,3) 
AND 
ingredients.id not in (4,5,6) 
AND ingredients.percent BETWEEN 10.0 AND 20.0

но отрабатывал он быстрее, и это была готовая основа под дальнейшие запросы.

Здесь нам кроме поиска по процентам понадобилось еще несколько типов операций: поиск по названию среди ингредиентов, групп и названий рецептов; поиск по id ингредиента с учетом допуска его содержания в рецепте; тот же запрос, но с подсчетом результатов по четырем условиям (впоследствии был переделан под другую задачу), а также финальный запрос.

В запросе требовалась следующая логика: для каждого ингредиента есть пять тэгов, которые относят его к какой-либо группе. Условно, свинина и говядина относятся к мясу, а курица и индейка — к птице. Каждый из тэгов расположен на своем уровне. Исходя из этих тэгов, мы могли создавать условное описание для рецепта, что позволяло нам генерировать дерево поиска и/или описания автоматически. Например, колбаса мясо-молочная со специями, ливерная с соей, куриная халяль. В одном рецепте может быть несколько ингредиентов с одним тэгом. Это позволяло не набивать цепочку тэгов руками — исходя из состава рецепта, мы уже могли однозначно его описать. Изменилась и структура вложенного документа:

{
	"ingredient_id": 1,
	"ingredient_name": "AAA",
	"manufacturer_id": 3,
	"manufacturer_name": "Manufacturer 3",
	"percent": 1,
	"level_1": 2,
	"level_2": 4,
	"level_3": 6,
	"level_4": 7,
	"level_5": 12
}

Также была необходимость задать поиск по условию «чистоты» рецепта. Например, нам требовался рецепт, где не было бы ничего кроме говядины, соли и перца. Тогда мы должны были отсеивать рецепты, где на первом уровне была бы только говядина, а на втором — только специи (первый тэг у специй был нулевым). Здесь пришлось схитрить: поскольку mustache является шаблоном без логики, ни о каких расчетах речи идти не могло; здесь требовалось внедрить в запрос часть скрипта на встроенном скриптовом языке ES — Painless. Его синтаксис максимально приближен к Java, так что трудностей не возникло. В итоге у нас был Mustache-шаблон, генерирующий JSON, в котором часть расчетов, а именно сортировка и фильтрация были реализованы на Painless:

"filter": [
	{{#levelsList}}
		{{#levels}}
		{"script": {
			"script": "
				int total=0;
				for (ingredient in params._source.ingredients){
					if ([0,{{tag}}].contains(ingredient.level_{{id}}))
						total+=1;
				}
				return (total==params._source.ingredients.length);
			"
		}}
		{{^isLast}},{{/isLast}}
		{{/levels}}
	{{/levelsList}}
]

Здесь и далее тело скрипта отформатировано для читаемости, в запросах переносы строк использовать нельзя.

К тому времени мы убрали допуск содержания ингредиента и нашли узкое место — мы могли посчитать колбасу говяжьей только потому, что там встречается этот ингредиент. Тогда мы добавили — все на тех же Painless-скриптах — фильтрацию по условию того, что данный ингредиент должен превалировать в составе:

"filter": [
	{"script":{
		"script": "
			double nest=0,rest=0;
			for (ingredient in params._source.ingredients){
				if([{{#tags}}{{tagId}}{{^isLast}},{{/isLast}}{{/tags}}].contains(flavor.level_{{tags.0.levelId}})){
					nest+= ingredient.percent;
				}else{
					if (ingredient.percent>rest){rest = ingredient.percent}
				}
			}
			return(nest>=rest);
		"
	}}
]

Как можно заметить, для этого проекта в Elasticsearch не хватало многих вещей, поэтому их пришлось собирать из «подручных средств». Но это не удивительно — проект достаточно нетипичен для машины, которая используется для полнотекстового поиска.

На одной из промежуточных стадий проекта нам понадобилась следующая вещь: вывести список всех доступных групп ингредиентов и количество позиций в каждой. Здесь вскрылась та же проблема, что и в превалирующем запросе: из 10 000 рецептов на основе содержимого генерировалось около 10 групп. Однако суммарно в этих группах оказывалось порядка 40 000 рецептов, что вообще не соответствовало действительности. Тогда мы начали копать в сторону параллельных запросов.

Первым запросом мы получали список всех групп, находящихся на первом уровне без числа вхождений. После чего генерировали мультизапрос: на каждую группу строился запрос на получение реального количества рецептов по принципу превалирующего процента. Все эти запросы собирались в один и отправлялись в Elasticsearch. Время ответа по общему запросу было равным времени обработки самого медленного запроса. Bulk-агрегация позволяла распараллелить их. Подобная логика (просто группировкой по условию в запросе) в SQL занимала примерно в 15 раз больше времени.

/* первый запрос */
$params = config('elastic.params');
$params['body'] = config('elastic.top_list');

return (Elastic::getClient()->search($params))['aggregations']['tags']['buckets'];

/* второй запрос */

После этого нам понадобилось в рамках одного запроса оценить:

  1. сколько для текущего состава доступно рецептов;
  2. какие еще ингредиенты мы можем добавить в состав (иногда мы добавляли ингредиент и получали пустую выборку);
  3. какие ингредиенты среди выбранных мы можем пометить как единственные на данном уровне.

Исходя из задачи, мы объединили логику последнего полученного запроса на список рецепта и логику получения точных чисел из списка всех доступных групп:

/* аггрегация */

"aggs" : {	// аггрегация по количеству доступных тэгов
	"tags" :{	// вернет количество совпадений
		"terms" :{
			"field"   : "ingredients.level_{{ level }}",
			"order"   : {"_term" : "asc"},
			"exclude" : [ {{#exclude}}{{ id }},{{/exclude}} 0]
		},
		"aggs": {
			"reverse_nested": {}
		}	// вернет количество реальных документов, а не совпадений
	}
}

/* обобщеный запрос */

foreach ($not_only as $element) {
	$parameters['body'][] = config('elastic.params');
	$parameters['body'][] = self::getParamsBody(
		$body,
		collect($only->all())->push($element),
		$max_level,
		0,
		0
	);
}

/* Основной запрос */
$parameters['body'][] = config('elastic.params');
$parameters['body'][] = self::getParamsBody(
	$body,
	$only,
	$max_level,
	$from,
	$size')
);

/* Количество параллельных потоков поиска */
$parameters['max_concurrent_searches'] = 1 + $not_only->count();

return (Elastic::getClient()->msearchTemplate($parameters))['responses'];

В итоге мы получили запрос, находящий все необходимые рецепты и их полное количество (оно бралось из response[«hits»][«total»]). Для простоты этот запрос записывался на последнем месте списка.

Дополнительно через агрегацию мы получали все id ингредиентов для следующего уровня. Для каждого из ингредиентов, не помеченных как «единственный», мы создавали запрос, где помечали его соответствующим образом, а затем просто считали количество найденных документов. Если оно было больше нуля, то ингредиент считался доступным для присвоения ключа «единственный». Думаю, здесь вы и без меня можете восстановить весь шаблон, который у нас получился на выходе:

{
	"from": {{ from }},
	"size": {{ size }},
	"query": {
		"bool": {
			"must":  [
				{{#ingredientTags}}
					{{#tagList}}
					{"bool": {
						"should":  [
							{"term": {"level_{{ levelId }}": {{ tagId }} }}
						]
					}}
					{{^isLast}},{{/isLast}}
					{{/tagList}}
				{{/ingredientTags}}
			],
			"filter": [
				{"script":{
					"script": "
						double nest=0,rest=0;
						for(ingredient in params._source. ingredients){
							if([{{#tags}}{{tagId}}{{^isLast}},{{/isLast}}{{/tags}}].contains(ingredient.level_{{tags.0.levelId}})){
								nest+= ingredient.percent;
							}else{
								if (ingredient.percent>rest){
									rest= ingredient.percent
								}
							}
						}
						return(nest>=rest);
					"
				}}
				{{#levelsList}},
					{{#levels}}
						{"script": {
							"script": "
								int total=0;
								for(ingredient in params._source.ingredients){
									if ([0,{{tag}}].contains(ingredient.level_{{id}}))
										total+=1;
								}
								return (total==params._source.ingredients.length);
							"
						}}
						{{^isLast}},{{/isLast}}
					{{/levels}}
				{{/levelsList}}
			]
		}
	},
	"aggs" : {
		"tags" :{
			"terms" :{
				"field"   : "ingredients.level_{{ level }}",
				"order"   : {"_term" : "asc"},
				"exclude" : [ {{#exclude}}{{ id }},{{/exclude}} 0]
			},
			"aggs": {
				"reverse_nested": {}
			}
		}
	},
	"sort": [
		{"_score": {"order": "desc"}}
	]
}

Часть из этого вороха шаблонов и запросов мы, естественно, кешируем (как, например, страницу всех доступных групп с количеством доступных рецептов), что добавляет нам немного производительности на главной странице. Это решение позволило добиться того, что данные для главной собираются за 50 мс.

Результаты проекта

Мы реализовали поиск по базе как минимум из 50 000 документов на Elasticsearch, который позволяет искать ингредиенты в составе продуктов и получать описание продукта по содержащимся в нем ингредиентам. Скоро эта база вырастет примерно в шесть раз (данные как раз подготавливаются), поэтому мы вполне довольны и своими результатами, и Elasticsearch как инструментом поиска.

К вопросу о производительности — мы уложились в требования проекта, и сами рады тому, что время ответа на запрос в среднем составляет 250-300 мс.

Через три месяца после начала работы с Elasticsearch он уже не кажется таким запутанным и непривычным. А плюсы шаблонизации очевидны: если мы видим, что запрос снова становится слишком большим, мы просто переносим дополнительную логику в шаблон и снова отправляем серверу исходный запрос практически без изменений.

«Всего хорошего и спасибо за рыбу!» (с)

P.S. В последний момент нам понадобилась еще и сортировка по русским символам в названии. И тут выяснилось, что Elasticsearch не воспринимает русский алфавит адекватно. Условная колбаса «Ультра мега свинина 9000 калорий» превращалась внутри сортировки просто в «9000» и оказывалась в конце списка. Как оказалось, эта проблема довольно просто решается преобразованием русских символов в unicode-нотацию вида u042B.
Поделиться публикацией
Комментарии 36
    +1
    В ES нет натуральной сортировки. Доберетесь до нее — вот плагин для форка. Будете админить кластер(ы) — вот кроссплатформенный десктопный клиент.
      0
      Спасибо, буду иметь в виду. А есть киллер-фичи по сравнению с кибаной?
        0
        Kibana это несколько иное, как по способу установки, так и по паттернам использования. Мы используем и то и другое для разных задач.
        0
        Респект за линки!!!
        +9
        Разговор идёт о всего-то 50к документов, а у вас навёрнуто столько… Толи и у вас индексов нет, толи вместо сервера raspberry pi. Тот же postgre, при самой простой настройке и грамотных запросах выдавал бы результаты за сотню миллисекунд, со встроенным полнотекстовым поиском. а написание обвязки на php без ларавела(можно было бы люмен взять или на симфони сделать микросервис) сэкономило бы ещё 150 миллисекунд. Короче, странно все это
          +6
          Да и вообще, 300мс — я бы категорически не назвал высокой скоростью, а уж для простенького джойника по 50к «быстро» вообще должно быть где-то в диапазоне 5-15мс…
            –1
            толи вместо сервера raspberry pi

            Ты почти нас раскусил. Просто сейчас работаем «на минималках», скоро заказчик будет растить сервера, тогда и нагрузка подрастет, и результаты чуть адекватнее будут. А обвязку из-под фреймворка не стали выносить по ряд других причин.
            +1
            я, возможно, повторюсь, но 50 тыс записей это семечки для эластика. На нормальной машине он способен выдавать и более объемные данные. Тем более структура у вас не то, чтобы сложная. К примеру частный случай использование эластика — хранение логов. Текстовой поиск по достаточно большому объему логов (около 2 Тб) занимает не так уж и много времени, учитывая, что мы используем кибану и по факту никто и никогда ничего не замерял(У нас редко когда вылазит ошибка кибаны о том, что реквест отвалился по таймауту в 300 ms).
              0
              50к это была своего рода вводная для проекта. Ждем когда набьется база, до 1-2 млн, там уже будет критично
                0
                50к это была своего рода вводная для проекта. Ждем когда набьется база, до 1-2 млн, там уже будет критично


                Для современного железа и современного софта — это те же семечки.
                При грамотном использовании этого софта.

                Если бы речь шла о 1-2 миллиарда, а не миллиона — другой вопрос.
                А миллионы — только звучит страшно…
                0
                я вас поправлю: на нормальном сервере не очень сложные запросы эластик нормально отрабатывает и на 25 террабайтах на сервер.

                  0
                  можно пойти и дальше) 25 Тб не предел. Я просто привел пример по логам. Очевидно, что Эластик используется и с большим объемом данных
                    0
                    Практически, это предел даже для логов. В ластике by-design есть требование — открытый индекс держит в памяти минимум 0,2% от объема индекса на диске. Причем это тот минимум когда отключены все фичи. Реально на практике меньше 0,5% я не видал.
                    Это приводит к тому что для 25ТБ индексов надо иметь ОЗУ 125 гиг только чтобы открыть эти индексы. Итого на серваке с 256 Гб ОЗУ все это работает в режиме «еле ворочается» — секунда на запрос. А если учесть насколько JAVA плохо работает с объемом HEAP за 32 гига, то приходиться изгаляться с кучей экземпляров…
                    И повторюсь, это для достаточно простых операций.

                    Выгружать/загружать индексы по требованию — тоже не решение: в момент загрузки индекса — кластер краснеет.

                    Я предлагал для elastic решение по «заморозке» индекса и их прозрачной загрузке выгрузке, но команда его разработки отказалась сказав «вам просто надо больше нод/памяти».
                      0
                      Это приводит к тому что для 25ТБ индексов надо иметь ОЗУ 125 гиг только чтобы открыть эти индексы. Итого на серваке с 256 Гб ОЗУ все это работает в режиме «еле ворочается» — секунда на запрос.


                      Ребята на конференции Highload утверждают, что как раз таки наоборот.
                      youtu.be/y5OJSIC5yE8

                      У них действительно была проблема с тем, что одиночный сервер должен быть очень жирным. В их случае — нереально жирным.

                      Но! Эта проблема была на Sphinx, который не поддерживает кластеризацию.

                      И они перешли на Elastic как раз именно потому, чтобы избавиться от ограничения огромных аппаратных требований на 1 машину. И размазать свою задачу по кластеру.

                      Да, они потеряли в производительности (ибо Sphinx быстрее). Это было осознано, так как они получили возможность масштабироваться и расти дальше.
                        0
                        Ребята на Хайлоад не нюхали жизни… Я как раз сейчас говорю про уже размазанную задачу. Я говорю про эээ метакластер на базе elasticsearch на 200+ серверов общим размеров 5 ПЕТАбайт. И кластер на 200 серверов рулится очень плохо, и я бы предпочел чтобы там было только 100 а лучше 50 серверов. Причем из этих 5 ПБ «горячими» являются только 50 ТБ… Т.е. для нормально работы по горячим данным мне хватило бы 500 Гиг ОЗУ на весь кластер, а приходится набивать 25 террабайт озу… только чтобы открыть эти индексы которые не нужны…
                          0
                          Ребята на Хайлоад не нюхали жизни…


                          Это вы про ivi-то? Ну-ну.

                          Я говорю про эээ метакластер на базе elasticsearch на 200+ серверов общим размеров 5 ПЕТАбайт.


                          Вот тут немножко не понял. Вы же раньше писали про другой порядок чисел? Вот же ваши слова:

                          Это приводит к тому что для 25ТБ индексов надо иметь ОЗУ 125 гиг только чтобы открыть эти индексы.


                          То ли я чего то не понял?

                          Ну а то, что при работе с 5 ПЕТАбайтами разработчик системы будет не простой пользователь/потребитель готовой СУБД, а было бы неплохо чтобы и разбирался досканально и проектировали архитектуру под задачу — совершенно естественно, имхо.
                            0
                            Это вы про ivi-то? Ну-ну.

                            Угу именно про ivi. Не спорю — ребята прошареные, только хайлоад бывает сильно по разному хай.

                            Вы же раньше писали про другой порядок чисел?

                            25ТБ индекса имелось в виду именно в пересчете на 1 сервер хранения конечно. Т.е. 25ТБ/сервер именно, из за того что Ластик довольно таки неплохо масштабируется. И именно потому что хайлоад. Смысл писать «у нас хайлоад, у нас миллион пользователей в сутки»? Вы пишите, а сколько при этом у вас железа. А то миллион пользователей в сутки на 1 сервер и на 100 серверов — абсолютно разные вещи даже по спектру возникающих задач, проблем и путей их решения.

                            Ну а то, что при работе с 5 ПЕТАбайтами разработчик системы будет не простой пользователь/потребитель готовой СУБД, а было бы неплохо чтобы и разбирался досканально и проектировали архитектуру под задачу — совершенно естественно, имхо.


                            На самом деле любая задача которая претендует на хайлоад уже требует «досконально разобраться». Решения «из коробки» типа elastic или postgres годны только так попробовать понюхать… И для более менее серьезного использования требуют анализа и допиливания в лучшем случае конфигов.

                            Говоря про 25 ТБ/сервер для эластика -я немного лукавлю. Порог был 5-10ТБ/сервер после которого я решил что проще будет «допилить» эластик…
                            но 10-25ТБ порог «а давайте рискнем с допиленным ластиком» не превзошел «давайте добавим оперативы».
                            И следующим порогом >25 ТБ/сервер стало — давайте уже напишем собственную «СУБД».
                +1
                5 лет я был фанатом Elasticsearch и использовал его во всех проектах (и иногда совершенно невероятным образом). В текущем тоже начал использовать для suggest search, и снова начались грабли с синхронизацией данных базы и кластеров elastic + никуда не деть тот момент, что elastic это внешний сервис, и к нему нужно обратится, получить результат, а потом уже обратится к базе. В итоге я убрал elastic из текущего проекта полностью, переписав поиск на PosgreSQL + ts_vectors. В нужных моделях создал триггеры на уровне БД которые создают вектора, и по ним произвожу поиск. По бенчмаркам я выиграл в скорости в 4ре раза, а код стал проще и надежнее. Ну и не нужно платить за elastic! Может конечно это все зависит от объема БД, и когда нибудь производительность с elastic победит — но во-первых, не факт что БД этого проекта когда нибудь достигнет таких объемов, во-вторых — вернуть на проект его всегда можно (и довольно быстро). Короче говоря — я стал более аккуратно использовать elastic, и намного больше углубился в изучение родных возможностей Postgre.
                  0
                  О, интересно. Спасибо, буду иметь эту возможность в виду.
                    0
                    Вообще-то elasticsearch бесплатен. А так, наличие 2 БД это всегда архитектурная проблема, Эластик тут не при чем. Мы вот сделали наоборот 5 лет назад и весьма успешно для наших кейсов.
                      0
                      Он условно бесплатен. Функционал x-pack не бесплатен. Поддержание серверов не бесплатно. В production особенно. На одном проекте пользуемся их клаудом found.elastic.co и поверьте, он уж вообще совсем не бесплатен. И даже при этом мы через год встретились с переполнением памяти и отказами в запросах, что подняло цену еще в два раза…
                        0
                        Кластер, построенный на elasticsearch был и есть бесплатен. То что Вы докупили сервисов и поддержки, не делает ПО платным. Более того, elasticsearch еще и open source. Можете сами компилить и сервер, и плагины к нему.
                          0
                          Конечно могу. Но мое время, или время человека который все будет настраивать, также не бесплатно. Говоря о что то не нужно платить за elastic, я имел ввиду все в комплексе.
                            0
                            Вы ввели читателей в заблуждение, что нужно платить за elastic. Мы разобрались теперь.
                              0
                              Ну незнай, ваше время на
                              и намного больше углубился в изучение родных возможностей Postgre.
                              тоже не бесплатно.

                              По факту — какие то возможности лучше реализованы «из коробки» в ластике, для этих же фич к Постгресу необходимо ляпать жуткие костыли например партиционирование и федеративные запросы, с другой стороны некоторые банальнейшие задачи для Постгреса типа «проапдейтить поле по уникальному значению doc_id» — это костыли для ластика.

                              Как правило если у вас возникает выбор между Реляционный PostgreSQL или нереляционный FTS Elastic — вывод — как таковая транзакционность, нормализация и другие плюшки Postgres вам не нужны, а надо вам масштабирование, гибкая документоориентированная модель и полнотекстовый поиск.
                              А то что вы не можете склониться в сторону Ластика — значит что масштабы у вас так себе и пока еще справляется вертикальная модель, что документы ваши не настолько гибкие и структура понятна, а полнотекстовый поиск довольно ограничен. В итоге вы можете сделать как реализацию на постгресе так и на ластике, при этом в обоих случаях придется писать костыли.
                                0
                                Но мое время, или время человека который все будет настраивать, также не бесплатно. Говоря о что то не нужно платить за elastic, я имел ввиду все в комплексе.


                                Тогда вашу фразу можно отнести к вообще любому ПО.
                                Она попросту вводит в заблуждение.

                                Но мое время, или время человека который все будет настраивать, также не бесплатно.


                                А Postgres будет сам доплачивать за то, что с ним разбираются?
                              0
                              Все таки elastic бесплатен, а с недавних пор и некоторая часть x-pack по-тихоньку открывается
                            0
                            Каждый создатель велосипеда хвалит свой велосипед, но, когда доходит дело до расширения, внезапно обнаруживается, что документации по велосипеду нет, а его автор уволился. И тогда новый специалист принимает решение выкинуть и написать все на известной технологии, по которой есть и документация и на рынке специалистов потом найти легко.
                            Пишите статью, было бы интересно увидеть ваше решение.
                              0
                              Ну я бы не назвал это велосипедом. Это вполне себе задокументированный функционал (https://postgrespro.ru/docs/postgrespro/9.5/datatype-textsearch www.postgresql.org/docs/9.6/static/datatype-textsearch.html). Просто порог вхождения (для меня) был немного выше. В свое время для меня было гораздо проще настроить elastisearch чем ковырять SQL. Сейчас разобравшись я понимаю, что написание многоуровневых json запросов в elastic (по сложным много параметровым выборкам) было на самом то деле сложнее чем реализация на PostgreSQL. Я не говорю что elastic это плохо. Это хорошо и круто. Но не всегда нужно.
                                0
                                На самом деле термвектора в постгрессе это «недоэластик» и по сути весь FTS завязанный на обработке этих векторов приходиться писать самостоятельно… Те же токенизаторы, анализаторы. Да и алгоритмы выбора и ранжирования документов тоже… Но с другой это имеет право на жизнь если вам ничего кроме элементарного поиска не нужно, и гораздо более важна например задача целостности данных, или возможность апдейтов.
                                  0
                                  Так и есть. Я ведь и не призываю отказываться от elastic. Я просто привожу пример того, что иногда он не нужен. К сожалению, мой недостаток знаний в свое время привел к тому, что elastic я пихал везде, пока это не вылезло мне боком именно с целостностью данных. Если бы знал о альтернативах — я сэкономил бы много времени. В принципе это и был главный посыл моего комментария.
                              0
                              И да.Есть один проект на Ruby с частыми выборками из Elastic и он просто катасрофичен по затратам памяти. В продакшн съедает 1 Гб, и просит еще. Тот же функционал на Postgres снизил потребление до 256 Мб. А триггерный функционал на ресурсы базы SQL если и повлиял, то незаметно.
                              0
                              Все отлично и спасибо за статью.

                              P.S.:
                              Деготь такой:

                              Для подобной задачи ElasticSearch — избыточен.

                              1) Elastic написан на Java. Что вызывает некоторую, гм… требовательность к оперативной памяти.

                              2) В Elastic отлично реализована работа в кластере на множестве серверов. Завел кластер Elastic — и дальше индекс как-то там сам внутри распределяется и устойчиво работает.

                              Так вот — п. 2) нужен для серьезнейших и нагруженнейших решений. И благодаря великолепной работе в кластере Elastic'у можно простить п. 1).

                              Но для задачи:

                              Создать поиск на Elacsticsearch по базе из 50 000 документов минимум.
                              Обеспечить высокую скорость ответа на запросы – менее 300 мс.


                              Лучше подошел бы куда как более легковесный и куда как более шустрый SphinxSearch.
                                0
                                На самом деле странный выбор в пользу Ластика вместо постгресса.

                                30 тысяч записей для постгресса это не много, впрочем как и для ластика. Но при этом Слон предсказуем и по скорости и по расходу памяти.
                                Собственно сами когда-то делали выбор:
                                + Постгрес предсказуем по расходу ОЗУ, который не зависит от объема БД.
                                — Ластик — тоже предсказуем — вынь да положь ему от 0,2 — 0,5% от объема индекса на диске даже если выключить все фичи причем это «work as designed» github.com/elastic/elasticsearch/issues/10869 в нашем случае это приводит к пределу 25 ТБ индекса на сервер из которых 24 ТБ — «холодные».
                                + Ластик хорошо масштабируется ( раньше я сказал бы что шикарно, но сейчас только хорошо) 5-7 нод — смело. 20-40 нод с осторожностью… Больше 40 только если у вас мало индексов или они редко создаются — уничтожаются. Ибо синхронизация метаинформации между кандидатами в мастера для большого числа индексов (а точнее шардов) может занимать существенное время.
                                — Постгрес масштабируется от слова никак. Т.е. там есть механизмы master-slave чутка ущербный by-design но работает. Механизм multimaster находится в зачаточном состоянии (я про версию 9 говорю), но с помощью костылей и палок получается заставить работать кластер из 3 узлов в режиме мультимастера… но это хардкор.
                                + Постгрес — это хорошая, взрослая, реляционная СУБД MVCC-типа. С транзакциями!
                                — Ластик в таком режиме — NoSQL система eventualy consistensy, если вам нужен «не точный поиск то оно вам самое то» а вот если вам понадобился поиск точный, да еще и например с сортировкой, да еще и с повторяющимися вариантами — подумайте дважды. Сделать можно, но местами ни разу не тривиально.

                                Ну и прочее, прочее прочее…
                                Мое мнение отталкиваться от производительности при выборе elastic/postgre ни разу не правильно.
                                  0
                                  И еще мы на тот момент поняли, что строить гигантские запросы в виде ассоциативных массивов на PHP это какая-то лютая наркомания. К тому же контроллеры становились абсолютно нечитаемыми, смотрите сами:


                                  На своем проекте мы создавали репозитории + классы для каждого запроса и не было проблем с массивами для запросов.

                                  ну и как то странно хранить запросы в контроллерах, все таки это не обязанность контроллера заниматься поиском в эластике.
                                    –1
                                    Мы в самом начале не совсем понимали как это можно разрулить. Поэтому получался всякий разный костылесипед. Сейчас это уже утряслось.

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое