- PVSM.RU - https://www.pvsm.ru -

Переходим от MongoDB Full Text к ElasticSearch

В своем прошлом посте [1], с анонсом Google Chrome расширения для Likeastore [2], я упомянул тот факт, что в качестве поискового индекса мы начали использовать ElasticSeach [3]. Именно ElasticSeach дал достаточно хорошую производительность и качество поиска, после которого было принято решение, выпустить расширение к хрому.

В этом посте, я расскажу о том, что использование связки MongoDB + ElasticSeach, есть крайне эффективное NoSQL решение, и о том, как перейти на ElasticSearch, если у вас уже есть MongoDB.

Немного истории

Функциональность поиска, суть нашего приложения. Возможность найти что-то быстро среди тысячи своих лайков, было то, ради чего мы начали этот проект.

Глубоких знаний теории полнотекстового поиска у нас небыло, и в качестве первого подхода мы решили попробовать MongoDB Full Text Search. Не смотря на то, что в версии 2.4 full text является экспериментальной фичей, он заработал довольно хорошо. Поэтому, на некоторое время мы его оставили, переключившись на более актуальные задачи.

Время шло и база данных росла. Коллекция, по которой мы проводим индексацию начала набирать определенный размер. Начиная от размера в 2 миллионов документов, я стал замечать общее снижение производительности приложения. Проявлялось это в виде долгого открытия первой страницы, и крайне медленного поиска.

Все это время я присматривался к специализированным поисковым хранилищам, как ElasticSearch [3], Solr [4] или Shpinx [5]. Но как это часто былает в стартапе, пока «гром не грянет, мужик не перепишет».

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

Быстрый анализ показал, что большая часть HTTP реквестов отваливается с 504, после обращений к MongoDB. Мы хостимся на MongoHQ, но при попытке открыть консоль мониторинга, ровным счетом ничего не выходило. База была нагружена до самого предела. После того, как консоль таки удалось открыть я видел, что Locked % уходить в заоблачные 110 — 140% и держится там, не собираясь уходить вниз.

Сервис, который собирает пользовательские лайки, делает довольно много insert'ов, и каждый такой insert влечет за собой ре-калькуляцию полнотекстового индекса, это дорогая операция, и достигая определенных ограничений (в том числе и по ресурсам сервера), мы просто уперлись в ее предел.

Сбор данных пришлось отключить, полнотекстовый индекс удалить. После перезапуска, сервиса Locked index не превышал 0.7%, но если пользователь пытался что-то поискать, нам пришлось отвечать неудобным «sorry, search is on maintenance»…

Пробуем ElasticSeach локально

Я решил посмотреть, что представляет из себя Elastic, попробовав его на своей машине. Для того рода экспериментов, всегда был (есть, и надеюсь будет) vagrant [6].

ElasticSeach написан на Java, и требует соответвующего рантайма.

> sudo apt-get update
> sudo apt-get install openjdk-6-jre
> sudo add-apt-repository ppa:webupd8team/java
> sudo apt-get install oracle-java7-installer

После чего можно проверить, все ли нормально, запустив

> java --version

Сам Elastic устанавливается крайне просто. Я рекомендую установку из Debian пакета, так как в таком виде его проще сконфигурировать для запуска как сервис, а не как процесс.

> wget https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.1.1.deb
> dpkg -i elasticsearch-1.1.1.deb

После этого, он готов к запуску,

> sudo update-rc.d elasticsearch defaults 95 10
> sudo /etc/init.d/elasticsearch start

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

{
  "ok" : true,
  "status" : 200,
  "name" : "Xavin",
  "version" : {
    "number" : "1.1.1",
    "build_hash" : "36897d07dadcb70886db7f149e645ed3d44eb5f2",
    "build_timestamp" : "2014-05-05T12:06:54Z",
    "build_snapshot" : false,
    "lucene_version" : "4.5.1"
  },
  "tagline" : "You Know, for Search"
}

Полное развертывание занимает около 10 минут.

Теперь что-то надо сохранить и поискать

ElasticSearch это интерфейс, построенный поверх технологии Lucene [7]. Это без преувеличений сложнейшая технология, отточенная годами, в которую было вложено тысячи трудо-часов высококлассных инженеров. Elastic, делает эту технологию доступной простым смертным, и делает это очень хорошо.

Я нахожу некоторые параллели, между Elastic'ом и CounchDB — тоже HTTP API, тоже schemaless, таже ориентация на документы.

После быстрой установки, я потратил много времени читая мануал [8], смотря релевантные видосы [9], пока не понял, как можно сохранить документ в индекс и как запустить самый простой поисковой запрос.

На этом этапе я использовал голый curl, точно также, как это показанно в документации.

Но долго тренироваться «на кошках», не интересно. Я сделал дамп продакшн MongoDB базы, и теперь мне нужно было перегнать всю свою коллекцию из MongoDB в ElasticSearch индекс.

Для данной миграции, я сделал небольшой тул — elaster [10]. Elaster, это node.js приложение, которое стриммит заданную коллекцию MongoDB, в ElasticSearch, предватительно создав нужный индекс и инициализировав его маппингом.

Процесс этот не очень быстрый (есть пару идей, для улучшения elaster) но примерно через 40 минут, все записи из MongoDB были в ElasticSearch, можно пробовать искать.

image

Создание поискового запроса

Query DSL [11], язык построения запросов к Elastic. По синтаксису это обычный JSON, но вот делать эффективный запрос, это опыт, практика и знания. Я признаюсь чесно их еще не достиг, поэтому моя первая попытка выглядела вот так:

function fullTextItemSearch (user, query, paging, callback) {
  if (!query) {
      return callback(null, { data: [], nextPage: false });
  }

  var page = paging.page || 1;

  elastic.search({
      index: 'items',
      from: (page - 1) * paging.pageSize,
      size: paging.pageSize,
      body: {
          query: {
              filtered: {
                  query: {
                      'query_string': {
                          query: query
                      },
                  },
                  filter: {
                      term: {
                          user: user.email
                      }
                  }
              }
          }
      }
  }, function (err, resp) {
      if (err) {
          return callback(err);
      }

      var items = resp.hits.hits.map(function (hit) {
          return hit._source;
      });

      callback(null, {data: items, nextPage: items.length === paging.pageSize});
  });
}

Это фильтрованный запрос, с пейджингом, для выдачи результатов для одного пользователя. Когда я попробовал его запустить, я был поражен как крут Elastic. Время выполнения запроса 30-40мс, и даже без каких-то тонких настроек, я был доволен результатами выдачи!

Помимо этого, в состав ElasticSeach входит Hightligh API, для подсветки результатов. Расширив запрос, до такого вида

	elastic.search({
		index: 'items',
		from: (page - 1) * paging.pageSize,
		size: paging.pageSize,
		body: {
			query: {
				filtered: {
					query: {
						'query_string': {
							query: query
						},
					},
					filter: {
						term: {
							user: user.email
						}
					}
				},
			},
			highlight: {
				fields: {
					description: { },
					title: { },
					source: { }
				}
			}
		}

В респонсе на него (объект hit) будет содержатся вложенный объект highlight, c HTML-ом, готовым для использовании на фронт-енде, что дает возможность делать примерно такое,

image

Модификация кода приложения

После того как базовый поиск заработал, необходимо сделать так, чтобы все новые данные, которые приходят в MongoDB (как основное хранилище) «перетекали» в ElasticSearch.

Для этого, существуют специальные плагины, т.н. rivers. Их довольно много, под разные базы данных. Для MongoDB, самый широко использованный находится тут [12].

River для MongoDB работает по принципу мониторинга oplog из локальной базы, и трансформации oplog событий в ElasticSeach команды. В теории все просто. На практике, мне не удалось завести этот плагин с MongoHQ (скорее всего проблема кризны моих рук, ибо в интернете полно описаний успешных использований).

Но в моем случае, оказалось гораздо проще пойти по другому пути. Так как у меня только одна коллекция, в которую есть только insert и find, мне проще было модифицировать код приложения, таким образом, что сразу после insert`а в MongoDB, я делаю bulk комманду в ElasticSeach.

	async.waterfall([
		readUser,
		executeConnector,
		findNew,
		saveToMongo,
		saveToEleastic,
		saveState
	], function (err, results) {
  });

Функция, saveToElastic

		var commands = [];
		items.forEach(function (item) {
			commands.push({'index': {'_index': 'items', '_type': 'item', '_id': item._id.toString()}});
			commands.push(item);
		});

		elastic.bulk({body: commands}, callback);

Не исключаю, что в более сложных сценариях использование river будет более оправдано.

Разворачиваем в продакшине

После того, как локальные эксперимент был завершен, нужно было все это развернуть в продакшине.

Для этого я создал новый дроплет на Digital Ocean [13] (2 CPU, 2 GB, 40 GB SDD) и по сути провел с ним все манипуляции описанные выше — установить ElasticSeach, установить node.js и git, установить elaster и запустить миграцию данных.

Как только новый инстанс был поднят и инициализирован данными, я перезапустил сервисы сбора данных и Likeastore API, уже с кодом модифицированным под Elastic. Все сработало очень гладко и в продакшине уже никаких сюрпризов небыло.

Результаты

Сказать то, что я доволен переходом на ElasticSearch это ничего не сказать. Это действительно одна из не многих технологий, которая работает «из коробки».

Elastic открыл возможность создания расширения к браузеру, для быстрого поиска, а также возможности создания расширенного поиска (по датам, типа контента и т.д.)

Тем не менее, я еще полный нуб, в этой технологии. Тот фидбек, который мы получили с момента перехода на Elastic и выпуска расширения, явно указывает на то, что необходимы улучшения. Если кто-то готов поделится опытом, буду очень рад.

MongoDB наконец-то задышала свободно, Locked % держится на уровне 0.1% и не стремится вверх, что делает приложение действительно отзывчивым.

Если вы все еще используете MongoDB Full Text, то надеюсь этот пост вдохновит вас на переход к ElasticSeach.

Автор: alexbeletsky

Источник [14]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/nosql/60014

Ссылки в тексте:

[1] посте: http://habrahabr.ru/company/likeastore/blog/222515/

[2] Likeastore: https://likeastore.com

[3] ElasticSeach: http://elasticsearch.com

[4] Solr: http://lucene.apache.org/solr/

[5] Shpinx: http://sphinxsearch.com/

[6] vagrant: http://www.vagrantup.com/

[7] Lucene: http://lucene.apache.org/core/

[8] мануал: http://www.elasticsearch.org/guide/

[9] видосы: http://www.elasticsearch.org/videos/

[10] elaster: https://github.com/likeastore/elaster

[11] Query DSL: http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html

[12] тут: https://github.com/richardwilly98/elasticsearch-river-mongodb

[13] Digital Ocean: https://www.digitalocean.com/?refcode=de56d081b272

[14] Источник: http://habrahabr.ru/post/223109/