ElasticSearch и поиск наоборот. Percolate API


    Вопрос умной категоризации чего-либо встаёт остро при разработке очень многих сайтов. Конечно, всегда можно отдать это на заполнение человеку и результат поначалу будет куда лучше машинного, но, что если категоризировать нужно в реальном времени сотни и тысячи «товаров».
    Придется отдать это на откуп машине. Тут вариантов не так много, а написание собственного ИИ для 99.9% задач пустая трата времени.

    Заинтересовавшимся как это решить с помощью ElasticSearch прошу под кат.


    Если вы еще не знакомы с ElastiSearch, то советую прекрасную статью «Быстрый полнотекстовый поиск ElasticSearch» от пользователя brujeo

    Общая идея


    В SmartProgress мы реализовали категоризацию в виде групп, которые объединяют цели пользователей в общие группы по интересам. Но как соотнести эти группы (которых уже более 100) с пользовательской целью так, чтобы ему на выбор было предложено максимум 3 группы и при этом они были максимально релевантные цели?

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

    Предположим, что у нас есть категория «Программирование на Ruby on Rails», тогда поисковый запрос по правилам simple query string будет выглядеть примерно так:
    Ruby | RoR | "Ruby on Rails" | "программирование Ruby"~4 | "вставать на рельсы" -php -java -net
    Немного поясню запрос: | — или, "..." — вхождения всего словосочетания, ~N — возможно разбавка словосочетания N словами

    Если нужно найти все «товары»(в нашем случае цели), которые подходят под такой запрос, то достаточно просто выполнить поиск. А если надо найти все категории под определённый товар? Тут приходит на помощь Percolate API

    Percolate API


    Признаюсь честно, моё знакомство с ElasticS началось именно с этой «фишки», до этого я работал только со сфинксом, но он не умеет делать обратный поиск.
    Поэтому, прочитав документацию, я толком не понял что это такое и как с этим работать, а информации в гугле, особенно по версии >1.X, было крайне мало. Но упорство победило (на >5 странице гугла есть жизнь).

    Я попробую объяснить на пальцах как это работает:
    1. Мы создаём индекс или берём существующий
    2. В него добавляется документ(ы) с особым типом .percolator, любым уникальным id и c body в виде нашего запроса (пример ниже)
    3. Далее мы делаем запрос к _percolate и смотрим к каким категориям подходит «товар»


    Рабочий пример


    Давайте попробуем это в действии:

    Создаём индекс «test» (без mapping, он нам не понадобится)
    curl -XPUT 'http://localhost:9200/test'


    Создаём .percolator
    
    curl -XPUT 'http://localhost:9200/test/.percolator/simple-search' -d '
    {
       "query" : {
             "simple_query_string" : {
                "query" : "Ruby | RoR | \"Ruby on Rails\" | \"программирование Ruby\"~4 | \"вставать на рельсы\" -php -java -net",
                "analyzer" : "simple",
                "fields" : ["name^5", "description"],
                "default_operator" : "and"
             }
       },
       "language" : "ru",
    }'
    


    Подробнее:
    test — Индекс
    .percolator — Тип
    simple-search — ID (может быть как int так и string)
    "query" — поиск
    simple_query_string — Функция для поиска. Полный список
    "fields" : ["name^5", "description"] — тут мы указали по каким полям идёт поиск, и указали коэффициент 5 для поля «name», т.к. обычно там самая важная информация. Подробнее.
    "active" : 1 — Дополнительные параметры, не обязательные, может быть несколько любого типа, применяются в фильтрации результата.

    По сути .percolator это такой же объект, как и любой другой в индексе, поэтому к нему также можно применять mapping.

    Ищем:
    
    curl -XPOST 'http://localhost:9200/test/category/_percolate?pretty' -d '
    {
       "doc" : {
           "name" : "Изучить Ruby on Rails максимально быстро",
          "description" : "Я хочу программировать на Ruby"
       },
       "filter" : {
          "term" : {
             "language" : "ru"
          }
       }
    }'
    


    Ответ:
    
    {
      "took" : 5,
      "_shards" : {
        "total" : 5,
        "successful" : 5,
        "failed" : 0
      },
      "total" : 1,
      "matches" : [ {
        "_index" : "test",
        "_id" : "simply-search"
      } ]
    }
    

    Совпадений может быть несколько

    Этот способ прекрасно подходит, если вы по какой-то причине не хотите переносить всю информацию в ElasticS. Или хотите потестить ваш percolator

    Поиск по существующим данным (ElasticS>=v1.0)

    Давайте добавим 1 запись в индекс test
    
    curl -XPUT 'http://localhost:9200/test/category/1' -d '
    {
          "name" : "Изучить Ruby on Rails максимально быстро",
          "description" : "Я хочу программировать на Ruby"
    }'
    


    И посмотри к каким категориям эта запись подходит:
    
    curl -XGET 'http://localhost:9200/test/category/1/_percolate?pretty'
    


    Ответ:
    
    {
      "took" : 4,
      "_shards" : {
        "total" : 5,
        "successful" : 5,
        "failed" : 0
      },
      "total" : 1,
      "matches" : [ {
        "_index" : "test",
        "_id" : "simply-search"
      } ]
    }
    


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

    Ложка дёгтя в бочке мёда


    К сожалению не всё так замечательно, как выглядит со стороны. Да, это работает, но есть минусы:
    • Все .percolator хранятся в оперативной памяти
    • Каждый документ индексируется в оперативной памяти
    • Время выполнения линейно количеству .percolator индексов

    Да, они поддерживают репликацию, как любой объект ElasticSearch, но тем не менее применять этот механизм нужно крайне осторожно.

    Пару простых советов как избежать out of memory:
    1. Если вашу выборку можно сделать более простым методом, например, использую macth/ bool query то используйте это, query язык довольно медленный относительно обычного сравнения значений
    2. Используйте фильтры, сужайте поиск настолько, насколько вам это позволяет логика приложения, это сэкономит вам немного памяти
    3. Не создавайте слишком много .percolator индексов, если у Вас предполагается тысячи таких индексов, значит вам стоит пересмотреть вашу логику или запастись оперативной памятью


    Полезная информация

    • Whats new in percolator — прекрасная презентация от разработчиков ES, которая очень доходчиво объясняет суть технологии
    • Percolator API — страница официальной документации
    • DHC — REST HTTP API Client — замечательный плагин для Google chrome позволяющий быстро и удобно общаться с ES


    Еще моя статья по ES — ElasticSearch — агрегация данных

    P.S. Я не являюсь гуру ES, поэтому рад любым замечаниям и дополнениям.

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

    Нужны ли еще статьи по ElasticSearch

    • 72,6%Нужны по более простым вещам (CRUD, настройка, ...)167
    • 74,4%Нужны по более сложным вещам171
    • 0,9%Не нужны2
    SmartProgress
    Сервис постановки и достижения целей

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 11

      0
      Если я правильно понял задачу, в сфинксе можно создать индекс с этими вот перко-штуками и загонять в поиск название «изделия» по этому индексу. Я так делаю поиск «подобных»: товаров, статей, только в моем случае отдельный индекс не требуется.
        0
        И при этом поиск идёт не просто по тексту, а используется query язык? Интересно, а можно пример, т.к. я такой возможность в сфинксе не нашел и в доках собственно то же. Хотя вполне возможно, что просмотрел
          0
          «query язык» — это, я так понимаю, Extended query syntax, всякие OR, MAYBE, NOT?
          Давайте разберем по порядку:
          "Ruby | RoR | \"Ruby on Rails\" | \"программирование Ruby\"~4 | \"вставать на рельсы\" -php -java -net",
          

          В доке сфинкса будет так:
          'Ruby RoR Ruby on Rails программирование Ruby  вставать на рельсы',
          

          Никаких условий, как видите, тут нет, но это не беда.
          Дальше ищем изделие «Изучить Ruby on Rails максимально быстро»
          query = u'Изучить Ruby on Rails максимально быстро'
          s.SetMatchMode(sp.SPH_MATCH_EXTENDED2)
          s.SetRankingMode(sp.SPH_RANK_SPH04)
          s.SetFilterRange('@weight', 0, 40, True) # Об этом чуть ниже
          results = s.Query(query, index='keywords')
          

          Теперь вопрос: если в query нет никаких php, java, net найдется ли дока?
          Допустим, в какой-нибудь залетной доке вы получите свой net. Для этого есть s.SetFilterRange('@weight', 0, 40, True) — это фильтр веса, можно обрезать всякие мало-мальские совпадения. Т.е. все зависит от того, как вы это настроите. В эластике он тоже есть, но мой опыт говорит, что он не работает как ожидается.
          Ну и еще, вы сами сказали, да и я согласен: у вас так или иначе будут ложные срабатывания.
          У эластика есть percolator, у сфинкса лучше поиск по русскому. Эластика — хорошо работает с токенами, т.е. с тем, что не надо склонять, но можно рассматривать как порядок символов, поэтому будут шикарные результаты с английским текстом и никакой с русским. В одном проекте я так намучился с настройкой этих поисковиков, что в итоге оставил оба. Эластик работал с агрегационными запросами (фасетки), а сфинкс как «поисковик».
          В вашем случае, мне кажется, конечный счет будет равный.
            0
            В целом согласен, хотя есть нюансы:
            В доке сфинкса будет так:

            Нет, увы не так, например:
            Вставать на лыжи
            Вставать рано утром и программировать

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

            Перенести мой запрос на язык сфинкса можно только использовав Extended query syntax, а он учитываться при прямом поиске не будет.

            Насчет русской морфологии, то скажем честно и сфинкс хромает, для этого у нас все русские слова проходят через PhpMorphy и мы вообще не используем русский stemming, хотя это и не относится к теме)

            P.S. Особенно щекотливая тема похудения и наоборот. Группа набора весе (бодибилдинг) и похудения (сбросить вес) постоянно дают ложны срабатывания на сфинксе, а на эластике не будут.
              0
              Взяв небольшую группу запросов можно получить весьма точные результаты. Взяв еще одну и подкорректировав алгоритмы, можно получить весьма неплохие результаты. Взяв еще десять групп, легко получить кашу.
              Поэтому вы правы в том, что исходите из своей задачи. Я вот уже второй день бьюсь над сфинксом, здесь потяну, тут хорошо, там потяну, здесь все плохо, но эластика совсем бы не справилась.
        +1
        /cast summon shodan
          0
          summon сработал, а вопрос-то в чем? :)

          ну да, и percolate тоже у нас пока нету — вероятно, через некоторое время будет
          0
          Здорово, что эластик набирает популярность.
          Я делал проект, где percolate — основняя фича. Исходники открыты.
          Буду рад пообщаться.
            0
            Читал вашу статью, жаль что вы там не рассмотрели подробно техническую часть, мне бы очень помогло, пришлось ковыряться самостоятельно)
            Как вы решаете вопрос с нагрузками, я так понимаю, что у вас уже довольно много percolator индексов? Интересно ваш опыт в этом плане.
            0
            Почему-то после выполнения второго запроса из примера (Создаем .percolator) вываливается ошибка

            {"error":{"root_cause":[{"type":"mapper_parsing_exception","reason":"failed to parse"}],"type":"mapper_parsing_exception","reason":"failed to parse","caused_by":{"type":"json_parse_exception","reason":"Unexpected character ('}' (code 125)): was expecting either valid name character (for unquoted name) or double-quote (for quoted) to start field name\n at [Source: org.elasticsearch.common.io.stream.InputStreamStreamInput@6c53773d; line: 12, column: 2]"}},"status":400}%
            
              0
              Оу, в конце точка с запятой

                 "language" : "ru",
              }'
              


              btw спасибо за статью. Знакомлюсь :)

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

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