Elasticsearch — поисковый движок с json rest api, использующий Lucene и написанный на Java. Описание всех преимуществ этого движка доступно на официальном сайте. Далее по тексту будем называть Elasticsearch как ES.
Подобные движки используются при сложном поиске по базе документов. Например, поиск с учетом морфологии языка или поиск по geo координатам.
В этой статье я расскажу про основы ES на примере индексации постов блога. Покажу как фильтровать, сортировать и искать документы.
Чтобы не зависеть от операционной системы, все запросы к ES я буду делать с помощью CURL. Также есть плагин для google chrome под названием sense.
По тексту расставлены ссылки на документацию и другие источники. В конце размещены ссылки для быстрого доступа к документации. Определения незнакомых терминов можно прочитать в глоссарии.

Установка ES


Для этого нам сначала потребуется Java. Разработчики рекомендуют установить версии Java новее, чем Java 8 update 20 или Java 7 update 55.
Дистрибутив ES доступен на сайте разработчика. После распаковки архива нужно запустить bin/elasticsearch. Также доступны пакеты для apt и yum. Есть официальный image для docker. Подробнее об установке.
После установки и запуска проверим работоспособность:
# для удобства запомним адрес в переменную
#export ES_URL=$(docker-machine ip dev):9200
export ES_URL=localhost:9200

curl -X GET $ES_URL

Нам придет приблизительно такой ответ:
{
  "name" : "Heimdall",
  "cluster_name" : "elasticsearch",
  "version" : {
    "number" : "2.2.1",
    "build_hash" : "d045fc29d1932bce18b2e65ab8b297fbf6cd41a1",
    "build_timestamp" : "2016-03-09T09:38:54Z",
    "build_snapshot" : false,
    "lucene_version" : "5.4.1"
  },
  "tagline" : "You Know, for Search"
}

Индексация


Добавим пост в ES:
# Добавим документ c id 1 типа post в индекс blog.
# ?pretty указывает, что вывод должен быть человеко-читаемым.

curl -XPUT "$ES_URL/blog/post/1?pretty" -d'
{
  "title": "Веселые котята",
  "content": "<p>Смешная история про котят<p>",
  "tags": [
    "котята",
    "смешная история"
  ],
  "published_at": "2014-09-12T20:44:42+00:00"
}'

ответ сервера:
{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "created" : false
}

ES автоматически создал индекс blog и тип post. Можно провести условную аналогию: индекс — это база данных, а тип — таблица в этой БД. Каждый тип имеет свою схему — mapping, также как и реляционная таблица. Mapping генерируется автоматически при индексации документа:
# Получим mapping всех типов индекса blog
curl -XGET "$ES_URL/blog/_mapping?pretty"

В ответе сервера я добавил в комментариях значения полей проиндексированного документа:
{
  "blog" : {
    "mappings" : {
      "post" : {
        "properties" : {
          /* "content": "<p>Смешная история про котят<p>", */ 
          "content" : {
            "type" : "string"
          },
          /* "published_at": "2014-09-12T20:44:42+00:00" */
          "published_at" : {
            "type" : "date",
            "format" : "strict_date_optional_time||epoch_millis"
          },
          /* "tags": ["котята", "смешная история"] */
          "tags" : {
            "type" : "string"
          },
          /*  "title": "Веселые котята" */
          "title" : {
            "type" : "string"
          }
        }
      }
    }
  }
}

Стоит отметить, что ES не делает различий между одиночным значением и массивом значений. Например, поле title содержит просто заголовок, а поле tags — массив строк, хотя они представлены в mapping одинаково.
Позднее мы поговорим о маппинге более подобно.

Запросы


Извлечение документа по его id:


# извлечем документ с id 1 типа post из индекса blog
curl -XGET "$ES_URL/blog/post/1?pretty"

{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "_source" : {
    "title" : "Веселые котята",
    "content" : "<p>Смешная история про котят<p>",
    "tags" : [ "котята", "смешная история" ],
    "published_at" : "2014-09-12T20:44:42+00:00"
  }
}

В ответе появились новые ключи: _version и _source. Вообще, все ключи, начинающиеся с _ относятся к служебным.
Ключ _version показывает версию документа. Он нужен для работы механизма оптимистических блокировок. Например, мы хотим изменить документ, имеющий версию 1. Мы отправляем измененный документ и указываем, что это правка документа с версией 1. Если кто-то другой тоже редактировал документ с версией 1 и отправил изменения раньше нас, то ES не примет наши изменения, т.к. он хранит документ с версией 2.
Ключ _source содержит тот документ, который мы индексировали. ES не использует это значение для поисковых операций, т.к. для поиска используются индексы. Для экономии места ES хранит сжатый исходный документ. Если нам нужен только id, а не весь исходный документ, то можно отключить хранение исходника.
Если нам не нужна дополнительная информация, можно получить только содержимое _source:
curl -XGET "$ES_URL/blog/post/1/_source?pretty"

{
  "title" : "Веселые котята",
  "content" : "<p>Смешная история про котят<p>",
  "tags" : [ "котята", "смешная история" ],
  "published_at" : "2014-09-12T20:44:42+00:00"
}

Также можно выбрать только определенные поля:
# извлечем только поле title
curl -XGET "$ES_URL/blog/post/1?_source=title&pretty"

{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "_source" : {
    "title" : "Веселые котята"
  }
}

Давайте проиндексируем еще несколько постов и выполним более сложные запросы.
curl -XPUT "$ES_URL/blog/post/2" -d'
{
  "title": "Веселые щенки",
  "content": "<p>Смешная история про щенков<p>",
  "tags": [
    "щенки",
    "смешная история"
  ],
  "published_at": "2014-08-12T20:44:42+00:00"
}'

curl -XPUT "$ES_URL/blog/post/3" -d'
{
  "title": "Как у меня появился котенок",
  "content": "<p>Душераздирающая история про бедного котенка с улицы<p>",
  "tags": [
    "котята"
  ],
  "published_at": "2014-07-21T20:44:42+00:00"
}'

Сортировка


# найдем последний пост по дате публикации и извлечем поля title и published_at
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
  "size": 1,
  "_source": ["title", "published_at"],
  "sort": [{"published_at": "desc"}]
}'

{
  "took" : 8,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : null,
    "hits" : [ {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "1",
      "_score" : null,
      "_source" : {
        "title" : "Веселые котята",
        "published_at" : "2014-09-12T20:44:42+00:00"
      },
      "sort" : [ 1410554682000 ]
    } ]
  }
}

Мы выбрали последний пост. size ограничивает кол-во документов в выдаче. total показывает общее число документов, подходящих под запрос. sort в выдаче содержит массив целых чисел, по которым производится сортировка. Т.е. дата преобразовалась в целое число. Подробнее о сортировке можно прочитать в документации.

Фильтры и запросы


ES с версии 2 не различает фильты и запросы, вместо этого вводится понятие контекстов.
Контекст запроса отличается от контекста фильтра тем, что запрос генерирует _score и не кэшируется. Что такое _score я покажу позже.

Фильтрация по дате


Используем запрос range в контексте filter:
# получим посты, опубликованные 1ого сентября или позже
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
  "filter": {
    "range": {
      "published_at": { "gte": "2014-09-01" }
    }
  }
}'

Фильтрация по тегам


Используем term query для поиска id документов, содержащих заданное слово:
# найдем все документы, в поле tags которых есть элемент 'котята'
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
  "_source": [
    "title",
    "tags"
  ],
  "filter": {
    "term": {
      "tags": "котята"
    }
  }
}'

{
  "took" : 9,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "1",
      "_score" : 1.0,
      "_source" : {
        "title" : "Веселые котята",
        "tags" : [ "котята", "смешная история" ]
      }
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "3",
      "_score" : 1.0,
      "_source" : {
        "title" : "Как у меня появился котенок",
        "tags" : [ "котята" ]
      }
    } ]
  }
}

Полнотекстовый поиск


Три наших документа содержат в поле content следующее:
  • <p>Смешная история про котят<p>
  • <p>Смешная история про щенков<p>
  • <p>Душераздирающая история про бедного котенка с улицы<p>

Используем match query для поиска id документов, содержащих заданное слово:
# source: false означает, что не нужно извлекать _source найденных документов
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
  "_source": false,
  "query": {
    "match": {
      "content": "история"
    }
  }
}'

{
  "took" : 13,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 0.11506981,
    "hits" : [ {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "2",
      "_score" : 0.11506981
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "1",
      "_score" : 0.11506981
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "3",
      "_score" : 0.095891505
    } ]
  }
}

Однако, если искать "истории" в поле контент, то мы ничего не найдем, т.к. в индексе содержатся только оригинальные слова, а не их основы. Для того чтобы сделать качественный поиск, нужно настроить анализатор.
Поле _score показывает релевантность. Если запрос выпоняется в filter context, то значение _score всегда будет равно 1, что означает полное соответствие фильтру.

Анализаторы


Анализаторы нужны, чтобы преобразовать исходный текст в набор токенов.
Анализаторы состоят из одного Tokenizer и нескольких необязательных TokenFilters. Tokenizer может предшествовать нескольким CharFilters. Tokenizer разбивают исходную строку на токены, например, по пробелам и символам пунктуации. TokenFilter может изменять токены, удалять или добавлять новые, например, оставлять только основу слова, убирать предлоги, добавлять синонимы. CharFilter — изменяет исходную строку целиком, например, вырезает html теги.
В ES есть несколько стандартных анализаторов. Например, анализатор russian.
Воспользуемся api и посмотрим, как анализаторы standard и russian преобразуют строку "Веселые истории про котят":
# используем анализатор standard       
# обязательно нужно перекодировать не ASCII символы
curl -XGET "$ES_URL/_analyze?pretty&analyzer=standard&text=%D0%92%D0%B5%D1%81%D0%B5%D0%BB%D1%8B%D0%B5%20%D0%B8%D1%81%D1%82%D0%BE%D1%80%D0%B8%D0%B8%20%D0%BF%D1%80%D0%BE%20%D0%BA%D0%BE%D1%82%D1%8F%D1%82"

{
  "tokens" : [ {
    "token" : "веселые",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "<ALPHANUM>",
    "position" : 0
  }, {
    "token" : "истории",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "<ALPHANUM>",
    "position" : 1
  }, {
    "token" : "про",
    "start_offset" : 16,
    "end_offset" : 19,
    "type" : "<ALPHANUM>",
    "position" : 2
  }, {
    "token" : "котят",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "<ALPHANUM>",
    "position" : 3
  } ]
}

# используем анализатор russian
curl -XGET "$ES_URL/_analyze?pretty&analyzer=russian&text=%D0%92%D0%B5%D1%81%D0%B5%D0%BB%D1%8B%D0%B5%20%D0%B8%D1%81%D1%82%D0%BE%D1%80%D0%B8%D0%B8%20%D0%BF%D1%80%D0%BE%20%D0%BA%D0%BE%D1%82%D1%8F%D1%82"

{
  "tokens" : [ {
    "token" : "весел",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "<ALPHANUM>",
    "position" : 0
  }, {
    "token" : "истор",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "<ALPHANUM>",
    "position" : 1
  }, {
    "token" : "кот",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "<ALPHANUM>",
    "position" : 3
  } ]
}

Стандартный анализатор разбил строку по пробелам и перевел все в нижний регистр, анализатор russian — убрал не значимые слова, перевел в нижний регистр и оставил основу слов.
Посмотрим, какие Tokenizer, TokenFilters, CharFilters использует анализатор russian:
{
  "filter": {
    "russian_stop": {
      "type":       "stop",
      "stopwords":  "_russian_"
    },
    "russian_keywords": {
      "type":       "keyword_marker",
      "keywords":   []
    },
    "russian_stemmer": {
      "type":       "stemmer",
      "language":   "russian"
    }
  },
  "analyzer": {
    "russian": {
      "tokenizer":  "standard",
      /* TokenFilters */
      "filter": [
        "lowercase",
        "russian_stop",
        "russian_keywords",
        "russian_stemmer"
      ]
      /* CharFilters отсутствуют */
    }
  }
}

Опишем свой анализатор на основе russian, который будет вырезать html теги. Назовем его default, т.к. анализатор с таким именем будет использоваться по умолчанию.
{
  "filter": {
    "ru_stop": {
      "type":       "stop",
      "stopwords":  "_russian_"
    },
    "ru_stemmer": {
      "type":       "stemmer",
      "language":   "russian"
    }
  },
  "analyzer": {
    "default": {
      /* добавляем удаление html тегов */
      "char_filter": ["html_strip"],
      "tokenizer":  "standard",
      "filter": [
        "lowercase",
        "ru_stop",
        "ru_stemmer"
      ]
    }
  }
}

Сначала из исходной строки удалятся все html теги, потом ее разобьет на токены tokenizer standard, полученные токены перейдут в нижний регистр, удалятся незначимые слова и от оставшихся токенов останется основа слова.

Создание индекса


Выше мы описали dafault анализатор. Он будет применяться ко всем строковым полям. Наш пост содержит массив тегов, соответственно, теги тоже будут обработаны анализатором. Т.к. мы ищем посты по точному соответствию тегу, то необходимо отключить анализ для поля tags.
Создадим индекс blog2 с анализатором и маппингом, в котором отключен анализ поля tags:
curl -XPOST "$ES_URL/blog2" -d'
{
  "settings": {
    "analysis": {
      "filter": {
        "ru_stop": {
          "type": "stop",
          "stopwords": "_russian_"
        },
        "ru_stemmer": {
          "type": "stemmer",
          "language": "russian"
        }
      },
      "analyzer": {
        "default": {
          "char_filter": [
            "html_strip"
          ],
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "ru_stop",
            "ru_stemmer"
          ]
        }
      }
    }
  },
  "mappings": {
    "post": {
      "properties": {
        "content": {
          "type": "string"
        },
        "published_at": {
          "type": "date"
        },
        "tags": {
          "type": "string",
          "index": "not_analyzed"
        },
        "title": {
          "type": "string"
        }
      }
    }
  }
}'

Добавим те же 3 поста в этот индекс (blog2). Я опущу этот процесс, т.к. он аналогичен добавлению документов в индекс blog.

Полнотекстовый поиск с поддержкой выражений


Познакомимся с еще одним типом запросов:
# найдем документы, в которых встречается слово 'истории'
# query -> simple_query_string -> query содержит поисковый запрос
# поле title имеет приоритет 3
# поле tags имеет приоритет 2
# поле content имеет приоритет 1
# приоритет используется при ранжировании результатов
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
  "query": {
    "simple_query_string": {
      "query": "истории",
      "fields": [
        "title^3",
        "tags^2",
        "content"
      ]
    }
  }
}'

Т.к. мы используем анализатор с русским стеммингом, то этот запрос вернет все документы, хотя в них встречается только слово 'история'.
Запрос может содержать специальные символы, например:
"\"fried eggs\" +(eggplant | potato) -frittata"

Синтаксис запроса:
+ signifies AND operation
| signifies OR operation
- negates a single token
" wraps a number of tokens to signify a phrase for searching
* at the end of a term signifies a prefix query
( and ) signify precedence
~N after a word signifies edit distance (fuzziness)
~N after a phrase signifies slop amount

# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
  "query": {
    "simple_query_string": {
      "query": "-щенки",
      "fields": [
        "title^3",
        "tags^2",
        "content"
      ]
    }
  }
}'

# получим 2 поста про котиков

Ссылки



PS


Если интересны подобные статьи-уроки, есть идеи новых статей или есть предложения о сотрудничестве, то буду рад сообщению в личку или на почту m.kuzmin+habr@darkleaf.ru.

Комментарии (72)


  1. nikerossxp
    30.03.2016 09:08

    По опыту, на сколько эффективнее выборки MySQL? Или можно как-то объединить: хранение mysql, поиск ES, даст ли это преимущества? Не придумаю никак сравнительный тест.


    1. talbot
      30.03.2016 09:16

      По опыту: можно в MySQL хранить данные (например, контактные данные миллиона человек), а с помощью ElasticSearch организовать нечёткий поиск по именам, фамилиям, номерам телефонов и т.д. ElasticSearch вернёт идентификатор, поиск по ID в MySQL будет уже быстрым.


      1. mkuzmin
        30.03.2016 09:24

        Можно не только хранить ID, но и поля этого человека и уже не обращаться к mysql. Я упоминал об этом в статье.


        1. talbot
          30.03.2016 10:16

          Можно конечно, если нам например нужно вывести какую-то условно «карточку».


    1. mkuzmin
      30.03.2016 09:23
      +2

      Mysql — это надежное хранилище, источник исходных данных, ES — поисковый движок. У них разные задачи.
      В большинстве случаев используют Mysql + ES. Бывают случаи, когда не очень важные данные хранят сразу в ES, например логи.
      ES может забрать на себя нагрузку на чтение с Mysql, т.е. пишем в Mysql, а читаем из ES. ES горизонтально масштабируется значительно проще, чем mysql, т.к. есть автоматический шардинг.
      Надо смотреть на конкретные запросы и вполне возможно, что mysql при выборке по первичному ключу будет быстрее.
      Но нужно понимать, что в ES есть значительно больше типов запросов, чем в mysql, например поиск по geo координатам, с помощью плагинов можно сделать поиск по изображениям.
      ПС. под mysql тут можно понимать любую реляционную БД.


      1. nikerossxp
        30.03.2016 09:31

        Спасибо! Нет ли чего почитать по успешной реализации подобной связки? В Ваших публикациях, к сожалению, такого нет :(
        З.Ы. только начал вкуривать ES.


        1. mkuzmin
          30.03.2016 09:36

          Для начала этого урока будет достаточно. Ваше приложение после записи в mysql будет писать данные в ES. Т.е. взаимодействие идет через приложение.
          Я советую потом прочитать официальную документацию, она хорошо написана и там есть классные примеры.


          1. nikerossxp
            30.03.2016 09:38

            Поковыряемс :)


        1. markusweb
          30.03.2016 14:44

          Я такое делал на рельсах пару лет назад, получилось весьма интересно, работает до сих пор, нареканий нет. Если интересно в контексте Rails, могу поделиться сниппетами.


    1. tkf
      30.03.2016 09:43

      Использую связку mysql + elasticsearch для хранения и поиска по закладкам. На приблизительно 500к записей разница получается приблизительно раз в 10-15. Запись ищется в elasticsearch он возвращает id, а далее mysql достает по id саму запись из базы. Сравнение конечно несколько не правильное, ибо в elastic'е используется стемминг, а в mysql просто поиск по like'у, и теоретически можно попытаться настроить лучше. Вопрос в том нужно ли это. По факту писать фильтры через elasticsearch получается удобней. Из минусов elasticsearch любит память.


      1. M-A-X
        30.03.2016 10:15

        А почему mysql не FULLTEXT INDEX?
        LIKE 'search' или LIKE '%search%'?


        1. tkf
          30.03.2016 10:20

          Используется добрый LIKE '%search%' точнее по факту там чуть похитрее но в целом да ^_^
          Match against не использую, ибо иногда приходится искать маленькие слова, а оно их вроде как не любит, по крайней мере раньше не любило :)


          1. M-A-X
            30.03.2016 10:36

            Оно не любит слова, которые встречаются в 50%+ записей.
            Не любит — это полбеды, эти слова стают стоп-словами.
            Приходится в некоторых случаях их вручную вырезать из запроса.
            Но Match against в те самые 10 раз, а то и более, быстрее за LIKE '%search%'.
            Запросы выполняются порядка миллисекунды.
            Пока остаюсь на Mysql, внешний поиск прикручивать не буду.
            :)


            1. tkf
              30.03.2016 10:59

              ну вот, а еще там стемминг не полноценный :) нет конечно всегда можно вручную преобразовать все слова.
              Еще одним из плюсов elasticsearch'а можно посчитать возможность удобно делать фасетный поиск. А еще elasticsearch неплохо может горизонтально масштабироваться. Не уверен что производительность match against тоже так можно поднимать, но это чисто мысли, ибо пол миллиона записей даже с десятком полей это ни о чем практически в любой бд если не косячить.
              В общем у всех свои задачи :)
              Там где стемминг и фасетный поиск не нужны используется как раз match against, на скорость что характерно жалоб нет. А для закладок использую elastic удобно получается. Так что применимость у всего своя. И если вас все устраивает то внешний поиск действительно не нужен.


      1. pavlick
        30.03.2016 12:50
        +2

        Для эластика есть морфологические плагины, которые больше, чем стемминг, например «люди» и «человек» будут приведены к одному и тому же токену. Посмотрите в сторону плагина russian_morphology, например. Хотя в нем встречаются забавные артефакты. Плюс возможность поиска с расстоянием между словами (с количеством перестановок, если точнее) и другие прелести Lucene.
        Память бывает разная, но все базы данных так или иначе любят память. Например, сейчас в эластике уже можно настроить использование doc_values для необходимых полей, в этом случае куча уже может использоваться меньше при выполнении запросов, а данные будут маппиться в память и при необходимости выкидваться опять же средствами оси.


        1. tkf
          30.03.2016 13:16

          у меня сейчас следующая связка фильтров — 'lowercase', 'russian_morphology', 'english_morphology', 'ru_stopwords', 'ru_stemming', человек — люди вполне неплохо отрабатывает. На тему поиска с расстоянием думаю в будущем подкрутить, когда подсказки к запросам буду делать, пока как то не надо. в основном или поиск по словам или фасеты. Да и эластик сейчас староват надо будет обновлять, но для этого же время надо будет выделить чтобы все переписать :)


          1. pavlick
            30.03.2016 13:49

            так у вас же включен russian_morphology ) Уберите его, и человек/люди расползутся в разные токены.


            1. tkf
              30.03.2016 14:16

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


              1. pavlick
                30.03.2016 14:30

                в изначальном комментарии говорилось, что эластик использует стемминг. Я лишь уточнил, что токенизация не ограничивается стеммингом )


        1. CyberSoft
          30.03.2016 13:27

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


          1. pavlick
            30.03.2016 14:16

            А какая нагрузка индексации и структура кластера? Не замечал заметного снижения производительности при использовании doc_values (как раз-таки на hdd, правда SAS 10K), в моем случае скорость индексации была до 2К/сек (не пиковая, а фактическая), запись производилась на ноды, расположенные на 3 серверах


            1. CyberSoft
              30.03.2016 16:28

              Тестировали даже не на той версии, что сейчас используем, но всё же тогда надо было решить "брать или не брать". На тестовой машине с ES 2.0 doc_values просадил скорость индексации где-то на 15%, решили тогда отказаться. Сейчас наверно лучше, не знаю.
              Кластер большой, к тому же по паре инстансов для обхода "ограничения" размера хипа в 32гб. Скорость 20-25к/с в среднем. Так что суммарно я думаю могло (может и сейчас) сильно просадить индексацию. А ещё сжатие включили, что сейчас оказалось пустой тратой, как мне кажется…
              Сейчас уже подумываем взять обратно doc_values, на другом кластере для меньшего объёма данных.


      1. oxidmod
        30.03.2016 13:25

        использовать ES для поиска имеет смысл в том случае, когда запросы к РСУБД невозможно построить оптимально.
        а примере всеми любимого MySQL и условно кругловакуумного блога. Когда нам нужно отобрать посты нескольких авторов за промежуток времени. MySQL не может использовать индекс для поиска по 2 range условиям. Если у вас пара тысяч записей, то это не страшно. Если у вас пара сотен тысяч — миллионы записей, то MySQL уйдет в себя всерьез и надолго. Кеширование тоже не всегда спасет, ибо параметров может быть сильно больше двух, и в каждом десятки-сотни вариантов (фильтры в интернет магазинах). Количество комбинаций такое, что никакой памяти не хватит.
        Вот тут ES покажет себя во всей красе. Вы можете очень быстро искать по любой комбинации параметров и получить айдишники записей. Если вам нужно показать сколько в магазине товаров с такой комбинацией параметров, то этого уже достаточно(щелкаешь ОЗУ: 1Гб — видишь плашечку: найдено 100 товаров. Ставишь еще галочку возле 2Гб — плашечка сразу показывает: найдено 200 товаров). Если вам нужно показать клиенту список товаров, то тут уже запрос к мускуля по праймари кей. Очень бысто и еффективно. Тут же поможет и кеш по айдишнику товара и репликация мускуля, если мускуль не вытягивает нагрузку на чтение, но то уже совсем другая история


    1. CyberSoft
      30.03.2016 11:14

      У нас ES используется как полноценная БД из-за мастабируемости и распределённого поиска, плюс к этому, за счёт кластера достигается высокая пропускная способность, а также лёгкость добавления ноды. Вроде бы PostgreSQL кластер тоже умеет, но не знаю как у него с вышеперечисленным, не говоря уже про MySQL. Но в ES нет такого мощного SQL, какой он есть в РБД. Так что каждому своё.
      Конечно используем РБД, но скорее для вставки из ES для других приложений, которые используют их для своей работы.


      1. mkuzmin
        30.03.2016 11:22

        Тут нужно быть осторожным, тк ES может потерять данные, особенно в кластерной конфигурации.
        https://aphyr.com/posts/317-jepsen-elasticsearch
        Я делал перевод статьи по ES как NoSQL базу — https://habrahabr.ru/company/percolator/blog/222765/
        Есть даже проект Crate.io. Это что-то вроде sql над es: https://crate.io/a/how-is-crate-data-different-than-elasticsearch/
        Postgresql из коробки не умеет кластер, есть PostgresXL но это немного не о том.


        1. CyberSoft
          30.03.2016 12:05

          А как насчёт 2.х? Мы используем 2.1, а последняя сейчас вообще 2.3.
          По поводу sql, то мы используем ES по назначению — поиск документов, поэтому нет потребности в каких-то сложных выборках или джойнах.


          1. mkuzmin
            30.03.2016 12:11

            Распределенная система подвержена сетевым сбоям и разделению кластера. Что произойдет если кластер разделится на 2 половины и будет происходить запись в обе половины? Это условный пример, и нужно смотреть как ES поведет себя в этой ситуации, наверняка в меньшей части кластера мастер не будет выбран и запись не пойдет.
            Я просто хочу обратить внимание, что с распределенными системами не все так просто. И придется чем-то жертвовать.
            В сети есть много материалов, тот же Aphyr. Можно погуглить материалы про cap теорему.


            1. CyberSoft
              30.03.2016 12:23

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


            1. pavlick
              30.03.2016 13:30

              Можно в настройках настроить минимальное количество мастеров для функционирования кластера, чтобы избежать сплитбрейна


  1. CyberSoft
    30.03.2016 12:04

    промахнулся


  1. olku
    30.03.2016 12:25

    Мы тоже используем ЕС как полноценную базу данных для документов (в терминах NoSQL). Клиенты и админы очень довольны.
    Кстати, ES вместе с Кибаной официально рекомендуются Амазоном. Про предел индексации в 34К в секунду (тот же Кинетик гарантирует 1К в секунду на шард) на ноду тут уже писали.


    1. r4nd0m
      30.03.2016 13:09

      Почему бы не использовать в качестве базы данных базу данных (mongodb). Насколько я понял из докладов, ЕS, как база данных, во всём ему проигрывает, кроме опций поиска. Мы используем их вместе, хранение в mongodb, поиск — в ES.


      1. olku
        30.03.2016 13:12

        Потому что Mongo не Lucene. Нам так надо.


  1. pavlick
    30.03.2016 12:41

    Сейчас уже не совсем правильно называть ES поисковым движком. Это уже больше аналитическая система с широкими возможностями агрегации данных.


    1. vgoloviznin
      30.03.2016 18:38

      Все-таки ES — это поисковый движок, а вот стек ELK — аналитическая система


  1. mrsoul
    30.03.2016 13:03

    Подскажите, а какой алгоритм лучше исопльзовать для следующего — есть большой каталог товаров с параметрами, изначально хранится в mysql. Происходит поиск по базе эластика и в ответ выдаётся какой-либо результат (набор товаров). Как организовать постраничную навигацию по результатам? Средствами эластика или передавать коды товаров в sql-запрос и данные для конкретной страницы брать из mysql?


    1. pavlick
      30.03.2016 13:20

      Если наладить хранение документов в эластике целиком, то необходимость во втором запросе в mysql отпадет


    1. mkuzmin
      30.03.2016 13:23

      ES умеет постраничную выдачу. В этой статье есть секция "Сортировка", в ней используется ключ size.
      Так же с помощью Scroll можно избежать проблемы постраничной выдачи, когда между получением страниц происходит вставка. Т.е. мы выбираем первую страницу, в кто-то добавил документ, который мог быть на первой странице, соответственно все документы съедут и последний документ с первой страницы продублируется и встанет на первое место на вторую страницу. А Scroll делает снимок и новый документ в выдачу не попадет. Надеюсь понятно объяснил)
      https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html


      1. mrsoul
        30.03.2016 13:39

        Да, более-менее, спасибо


      1. pavlick
        30.03.2016 13:41

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


        1. mkuzmin
          30.03.2016 13:44

          Согласен, я упомянул скролл, чтобы просто показать эту возможность.


      1. tgz
        03.04.2016 09:53

        Только этот скролл медленный тормоз, на больших объемах будете ждать сутками.


    1. peterpro
      30.03.2016 15:05

      Кстати, если в магазине предусмотрены фильтрации по нескольким типам признаков я бы сразу порекомендовал обратить внимание на фасеты в Elastic — просто отличная штука!


      1. boston
        30.03.2016 19:42

        fasets начиная со второй версии выпили, теперь только агрегация https://habrahabr.ru/company/smartprogress/blog/227131/


      1. vgoloviznin
        30.03.2016 19:59

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


  1. lostpassword
    30.03.2016 13:21

    У меня такой вопрос: это что же за блог, для индексации которого применяется Elasticsearch? Какая же у вас посещаемость?)


    1. mkuzmin
      30.03.2016 13:24
      +2

      Да это же просто пример на кошечках и собачках)
      Не в посещаемости дело, а в возможностях поиска.


    1. Beowulfenator
      05.04.2016 13:36
      +1

      Например, wordpress.org, в котором на ES сделан поиск по всем блогам, созданным в системе.


  1. InteractiveTechnology
    30.03.2016 14:19
    +2

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


    1. mkuzmin
      30.03.2016 14:36
      +4

      Как вы шардируете данные?
      Заводите новый индекс, через определенный интервал времени?
      Просто на горячую изменить кол-во шардов для индекса невозможно.
      Наверняка используете роутинг, т.к. при увеличении кол-ва данных будет падать скорость поиска.
      Расскажите, пожалуйста, про "правильную готовку".


      1. InteractiveTechnology
        05.04.2016 18:58

        Роутинг вводили, потом выводили с трудом. Не везде его можно использовать, он очень сильно привязывает всё к знанию этого самого роута, а не везде это возможно знать изначально в запросе.
        Система спроектирована так, что у нас есть версионность и разделение по типам, например users у нас как индекс {company}_users_v{1..999} в нём есть типы admin, operator, engineer, abonent и т.п.
        Индексы связаны между собой через alias как {company}_users => {company}_users_*


    1. AlexWinner
      31.03.2016 01:30

      А как это всё переживает разрыв связи между датацентрами?


      1. InteractiveTechnology
        05.04.2016 19:00

        Точно так же как и в любом другом кластере, срабатывает failover.


    1. Beowulfenator
      05.04.2016 13:40

      А у вас бывают очень тяжеловесные запросы, которые тормозят всю систему? Мы все никак не можем решить такую проблему: есть запросы пользовательские, которые должны отрабатываться быстро, а есть админские, которые по минуте выполняются. При этом во время выполнения админских запросов весь индекс тормозит. Как бы так понизить приоритет одним запросам, и поднять другим?


      1. mkuzmin
        05.04.2016 13:46

        можно разнести нагрузку по разным репликам
        например, явно обращаясь к реплике https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-preference.html
        наверняка есть и другие варианты


        1. Beowulfenator
          05.04.2016 14:35

          Пробовал через preference, и при этом все равно необъяснимо тормозит.


      1. InteractiveTechnology
        05.04.2016 15:27

        Для начало нужно понять из-за чего именно такое происходит, сделайте explain или лучше profile, чтобы понять на что уходит время. А после уже будет видно в чём проблема и можно подумать над её решением.


    1. lostpassword
      05.04.2016 13:46

      Напишите пост про боевое применение Эластика, многим очень интересно будет.)


      1. InteractiveTechnology
        05.04.2016 15:29

        Эх, было бы время) я бы с радостью


  1. Flex25
    31.03.2016 21:24

    Объясните мне, дилетанту в области nosql, два момента в отличиях Mongodb, ES и Sphinx:

    1. Что лучше использовать для задач фасетного поиска, например для поиска по параметрам товаров интернет-магазина?

    2. Есть ли в Mongodb полнотекстовый поиск с учетом русской морфологии и с ранжированием результатов? Если да, то зачем тогда использовать ES и Sphinx? Они реально быстрее?

    Давно присматриваюсь к nosql-решениям и никак не могу сделать выбор между Mongodb, ES и Sphinx именно в разрезе этих двух задач: полнотекстовый и фасетный поиски. Интуиция подсказывает выбор в пользу Mongodb как более зрелого, проверенного и быстрого решения. Но это только интуиция.


    1. oxidmod
      31.03.2016 21:57

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


      1. InteractiveTechnology
        05.04.2016 18:02

        про эластик ваша информация устарела, это давно уже полноценная nosql БД, имеет свои плюсы и минусы. Данные там потерять можно точно так же как и в любой другой БД, в которой нет транзакций ;)


        1. oxidmod
          05.04.2016 18:06

          я говорю, не о консистентности. Банально уборщица выдернула шнур и все. Есливам неоткуда просинкать данные по новой, то вы их потеряли. полностью. Редис/монга держат (или периодически скидывают) данные на диске. После включение данные поднимутся с диска и все будет ок.


          1. InteractiveTechnology
            05.04.2016 18:13

            стоп стоп) вы сами себе противоречите. Если у вас кластер редиса, то проблема будет ровно такой же вы потеряете данные за тот промежуток времени снапошотов, которые установлены в настройках. В эластике всё тоже самое, пишется в одну ноду, потом разноситься по другим, если не успел записать (выдернули свет) они просто потеряются как и в случаи с редисом/монгой. В запросе к эластику на запись можно ожидать записи данных и выставить на эту запись обновления индекса мгновенно без задержки, по умолчанию все записи собираются в pool и раз в секунду переиндексируются.


            1. oxidmod
              05.04.2016 18:25

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


    1. grossws
      01.04.2016 03:33

      Если вам нужны фасетки и более-менее нормальная морфология — берите Apache Solr или ElasticSearch. И то, и другое использует поисковый движок Apache Lucene под капотом.

      В случае Solr'а при использовании docValues стоит брать 4.10 или подождать 5.6. Или использовать поля с docValues=false, тогда проблемы нет. Если говорить про интернет-магазин, то вариативность данных для каждой из фасеток, скорее всего, небольшая (сотни-тысячи значений), так что требования по памяти для UnInvertedField кэша будут небольшие.

      Авторизованные данные, имхо, стоит хранить снаружи (mysql, postgres, mongo — куда лучше лягут по структуре), чтобы иметь возможность переиндексировать, экспериментировать с индексами (например, вкрутить кастомную морфологию) и т. п.

      Для нормализации порекомендую попробовать AOT'овскую морфорлогию.


  1. zyrik
    02.04.2016 15:33

    Расскажите, а можно ли в ES делать что-то типа join'а по разным индексам или типам? И как это вообще работает?
    Насколько я понимаю, типичный сценарий для ES следующий. Каждый день (или через другой промежуток времени) создается новый индекс, который содержит данные за данный период времени. Допустим, что я сохраняю логи в ES. Получается, что в терминах ES таблица логов — это отдельный тип (type). Теперь допустим, что мне надо в другом типе сохранять какую-то внешнюю информацию (например маппинг между ip адресом и страной, где размещается сервер логов). Для этого, насколько я понимаю, нужно создать новый тип с этими данные. В этом случае мне интересно:
    1) как организовать join между двумя типами
    2) в случае создания нового индекса, нужно пересоздавать тип маппинга. Как обычно избавляются от избыточной информации в этом случае?


    1. mkuzmin
      02.04.2016 19:03

      Есть parent-child query. Это аналог join.


    1. Beowulfenator
      05.04.2016 13:33

      Нельзя организовать join между двумя типами, потому что данные могут находиться на разных шардах. Обычно в индекс просто записывается избыточная информация. При создании новых индексов можно использовать шаблоны.
      Но в целом, если вы уже взялись за ES, привыкайте к денормализации.


      1. mkuzmin
        05.04.2016 13:35

        Делать join между несколькими шардами нельзя. Но при индексации можно указать parent и оба документа окажутся на одном шарде.


        1. Beowulfenator
          05.04.2016 13:45

          Конечно. И это не единственный способ — в руководстве рекомендуют выбрать один из четырех: join на стороне приложения, денормализация, вложенные документы и parent-child. Каждый хорош для своих целей, но мне показалось, что для исходной задачи ("например маппинг между ip адресом и страной, где размещается сервер логов") проще всего было бы применить денормализацию.


    1. InteractiveTechnology
      05.04.2016 18:07

      В вашем случаи нужно просто перестроить мышление на денормализацию данных и подобные вопросы вообще не станут возникать.