Проблема с быстродействием при запросе c "fuzziness"

День добрый.

Исторически сложилось, что есть 18 одинаковых индексов. В каждом хранятся статьи на своем языке.
18 индексов == 18 В каждом индексе хранятся свои документы.

  "mappings": {
    "article": {"_meta": {"model": "WWW\SearchBundle\Document\Article" },
      "_all": {"auto_boost": true },
      "properties": {
        "keywords": {"index": "not_analyzed","type": "string"},
        "description": {"type": "string",
          "fields": {
            "language": {"type": "string"}
          }
        },
        "title": {"boost": 10,"type": "string",
          "fields": {
            "total": {"index": "not_analyzed", "type": "string"},
            "language": {"type": "string"}
          }
        }
      }
    }

Запросы все одинаковые, это типа поисковика:

 {
  "query": {
    "filtered": {
      "query": {
        "dis_max": {
          "queries": [
            {
              "constant_score": {
                "query": {
                  "multi_match": {
                    "type": "phrase",
                    "query": "donald trump us election",
                    "fields": [
                      "title"
                    ],
                    "fuzziness": 0,
                    "slop": 2
                  }
                },
                "boost": 5
              }
            },
            {
              "constant_score": {
                "query": {
                  "multi_match": {
                    "type": "phrase",
                    "query": "donald trump us election",
                    "fields": [
                      "description^5",
                      "keywords^0.25"
                    ],
                    "fuzziness": 0,
                    "slop": 2
                  }
                },
                "boost": 2
              }
            },
            {
              "constant_score": {
                "query": {
                  "multi_match": {
                    "type": "best_fields",
                    "query": "donald trump us election",
                    "fields": [
                      "title^2",
                      "description^0.2",
                      "keywords^0.25"
                    ],
                    "fuzziness": 1
                  }
                },
                "boost": 0.001
              }
            }
          ]
        }
      }
    }
  },
  "rescore": [
    {
      "query": {
        "rescore_query": {
          "function_score": {
            "functions": [
              {
                "gauss": {
                  "publishedAt": {
                    "origin": "2016-09-01T10:30:55-04:00",
                    "scale": "365d",
                    "decay": 0.5
                  }
                },
                "weight": 1
              }
            ]
          }
        },
        "rescore_query_weight": 1,
        "query_weight": 1,
        "score_mode": "multiply"
      },
      "window_size": 100000
    }
  ],
}

проблемы присутствуют в двух местах:

  1. функция rescore. Если запрос популярный, то могут возвращаться порядка 1 000 000 результатов, которые нужно обработать.
  2. Третий подзапрос с "fuzziness": 1

Включение и отключение "fuzziness": 1 или 0 вызывает броски в быстродействии в 2 раза. При включении нечеткого поиска запрос выполняется примерно 400мс. при отключении - 200мс

замена rescore на сортировку тоже увеличивает быстродействие в 2 раза. для популярных выражений. Для редких статей - влияния не оказывает естественно.

Задача, ускорить запросы раз в 10. Ну или хотябы чтобы меньше 100мс уйти.
Думал сделать фильтрацию результатов, однако не могу, потому что подзапросы возвращают несортированные данные и если резать результаты до 10000, то получается совсем грустно. а если больше - то особого смысла нет. все равно тупит.

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

В данный момент по совокупности в индексах примерно 5 000 000 документов. Самый большой индекс русский (1 200 000 документов ).

Какая версия и как время запроса изменяется, если временно изменить gauss на linear?

Версия 1.7.3
Делаю запрос по "donald trump us election"
"total": 1242827,
для gauss - скорость запроса 511ms
для linear - 450ms

заменяю блок "rescope" на

  "sort": [
    "_score",
    {
      "publishedAt": {
        "order": "desc",
        "ignore_unmapped": true
      }
    }
  ],

получается 270ms

Еще пара вопросов. А что получается если function_score из rescore в основной запрос переместить?

{
  "query": {
    "function_score": {
      "query": {
        "filtered": {
          "query": {
            "dis_max": {
             ...
            }
          }
        }
      },
      "functions": [
        {
          "gauss": {
            "publishedAt": {
              "origin": "2016-09-01T10:30:55-04:00",
              "scale": "365d",
              "decay": 0.5
            }
          },
          "weight": 1
        }
      ],
      "boost_mode": "multiply"
    }
  }
}

Тоже самое с linear вместо gauss?

Какие диски используются, сколько физической памяти на машинах и какой размер heap-a?

Игорь, огромное спасибо за ваши отклики.

При переносе в основной запрос, скорость увеличивается для
gauss в основном запросе получается примерно 280мс, для linear 250мс

на машине 32Gb памяти, хип менял от 2 до 31Gb, сейчас стоит 16Gb
Если смотреть в top, то больше 2,5 гигабайт памяти не отъедается вообще.

Диски
ATA-9: INTEL SSDSC2BB480H4, D2010380, max UDMA/133
две штуки в зеркале.

Но активность на дисковой подсистемы равна нулю после первых 2-3 запросов.

Процессор
Intel(R) Xeon(R) CPU E5-1630 v3 @ 3.70GHz (fam: 06, model: 3f, stepping: 02)

Визуально, когда делаешь даже один запрос, нагрузка на процессоре в топе на секунду-другую прыгает до 40-70 процентов.
Если просто из скрипта начать курлом дергать последовательно один и тот же запрос то диски стоят, на процессоре 300% нагрузки, load average = 4.

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

Если elasticsearch heap не использует, то имеет смысл у него его отнять и отдать OS под файловый кэш.

Давайте вот что еще попробуем. Запустите, опять ваш скрипт, желательно на нескольких потоках чтобы на ноде постоянно что-то искалось, и пока он работает, выполните вот эту команду 10 раз с интервалом в 2 сек и выложите куда-нибудь результат

curl "localhost:9200/_nodes/hot_threads?threads=100"    

Ok.
Вот что еще заметил. После переноса
Сначала запустил оригинальный запросв function_score в основной запрос, явно ухудшилась релевантность, выросла скорость, но увеличилась нагрузка на процессор. примерно в 2 раза. Как я понимаю, в этом случае у эластика получается параллелить расчет функции, за счет этого и получается ускорение.
Решил провести эксперимент, заодно и получить статистику по тредам.
Сначал запускался оригинальный запрос с rescore.
10 потоков оп 10000 запросов в каждом потоке.
LA=15, загрузка проца 780% (на восьми ядрах) по ядрам 97% user, 1% на system, 0% wait
данные по тредам: https://drive.google.com/file/d/0B30qqGOh6OSuQUk3YjNKUEFiR2s/view?usp=sharing

Потом запускался запрос с перенесенной function_score
теже 10 потоков, циклы по 10000 запросов
Структура нагрузки практически идентичная, тольк user 95% system 3%
данные по тредам https://drive.google.com/file/d/0B30qqGOh6OSueVFJQVdFbW9vWk0/view?usp=sharing

Не берусь судить, но сдается мне проблема в том, что подзапросы возвращают очень много статей, и по всем по ним, нужно расчитать новый вес

Вот вопрос, а нельзя ли ограничить каким либо образом количество выдаваемых статей одним подзапросом, чтобы они возвращали не 1 200 000 как сейчас по популярным запросам, а скажем каждый ограничивался 1000 самых релевантных?

Ничего в глаза в hot_threads не бросается, к сожалению.

Вы сказали, что у вас 18 индесков, а сколько шард в каждом индексе?

Вот вопрос, а нельзя ли ограничить каким либо образом количество выдаваемых статей одним подзапросом, чтобы они возвращали не 1 200 000 как сейчас по популярным запросам, а скажем каждый ограничивался 1000 самых релевантных?

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

А вы тоже самое под 2.x не пробовали?

В каждом индексе по 5 шард.

Попробую объяснить в терминах SQL:
Пусть есть таблица

CREATE TABLE large_table (
  id BIGINT PRIMARY KEY,
  doc_id BIGINT,
  word_field VARCHAR(255),
  relevance int
);
create INDEX wf_idx ON large_table(word_field);

Таблица реально большая, скажем миллиардов 15 записей и поле word_field может повторяться многократно. Предположим, что мы берем с нескольких больших новостных ресурсов статьи, делим их на слова, и кортеж идентификатор, ид документа, слово, и некий вес, закидываем в эту таблицу.
Специально утрирую только для демонстрации.
После чего делаем запрос по самым популярным словам "и", "в", "не"

SELECT * FROM (
  SELECT id,word_field,relevance FROM large_table WHERE word_field = 'и'
     UNION
  SELECT id,word_field,relevance FROM large_table WHERE word_field = 'в'
     UNION
  SELECT id,word_field,relevance FROM large_table WHERE word_field = 'не'
) tmp_table
ORDER BY relevance DESC
LIMIT 20

выполняться он будет просто ну ОЧЕНЬ долго. поскольку каждый из подзапросов вернет нам по паре миллионов строк, и все это счастье нужно пропустить через UNION, отсортировать, и выбрять первые.
Если же в основном запросе будут вычислимые поля, или вместо SORT будет какой нибудь OUTER APPLY то это дело вообще превращается в жуткие пипец.

Решается просто, хотя запрос вроде бы сложнее:

SELECT * FROM (
                SELECT * FROM (SELECT id,word_field,relevance FROM large_table WHERE word_field = 'и' ORDER BY relevance DESC LIMIT 1000)
                UNION
                SELECT * FROM (SELECT id,word_field,relevance FROM large_table WHERE word_field = 'в' ORDER BY relevance DESC LIMIT 1000)
                UNION
                SELECT * FROM (SELECT id,word_field,relevance FROM large_table WHERE word_field = 'не' ORDER BY relevance DESC LIMIT 1000)
              ) tmp_table
ORDER BY relevance
LIMIT 20

Этот запрос отработает в сотни раз быстрее, и с ВЫСОКОЙ степенью вероятности вернем мне такой же результат.

Мне кажется, что моя проблема, сродни тому, что я только что описал.
У меня тормоза на популярных запросах, когда каждый из запросов внутри dis_max возвращает огромное количество документов, к которым эластик должен применить рескор (или функцию как вы предлагали) с достаточно тяжелыми расчетами.
А вот если бы каждый из подзапросов ограничить выдачей 1000 наиболее релевантных статей, все стало бы сильно веселей.
По непопулярным словам то у меня поиск идет очень быстро.

На 2.x не пробовал. Затруднительно по организационным причинам.

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

   0123456789........
и  0000101110........
в  1100001100........
не 1110001000........

В этом примере, "и" был в документах 4, 6, 7 и 8, а "в" в документах 0, 1, 6 и 7. Когда мы ищем по всем 3-м полям, мы грузим все 3 карты и выполняем между ними логическую операцию "или" и получаем 1110101110....., по мере получения этой биткарты мы начинаем грузить поле сортировки (или расчитывать score) и грузить их значения в очередь с приоритетом, после того как мы прошли все биткарты в очереди остаются записи с самыми высокими значениями ключа сортировки. Эти записи и возвращаются пользователю. При rescore, мы сначала делаем все тоже самое, а потом пересчитываем score для все записей в окне rescore.

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

Ну, на мой взгляд, как раз это побитовое сравнение и есть нечто сродни юниону.
Хорошо. Управлять размером очереди, в которую помещаются значения с данными после расчета приоритета возможно?
Можно ли ограничить длину этой очереди?

Я попытался сделать так:

{
  "query": {
    "filtered": {
      "query": {
        "dis_max": {
          "queries": [
            {
              .....
            },
            {
              .....
            },
            {
              .....
            }
          ]
        }
      },
      "filter": {
        "limit": {
          "value": 3000
        }
      }
    }
  },
  "rescore": [
    {
      .....
    }
  ],
  "from": 0,
  "size": 20
}

Но получаю просто первые 3000 подходящих записей, а не 3000 записей с наивысшим score.

Неужели ничего нельзя сделать?

Да, эта очередь всегда имеет размер from+size на каждой шарде - то есть в вашем примере - 20.

Поиск по фразам и fuzziness - сложные операции, которые требуют времени. Можно попробовать 2.x, уменьшить количество шард для маленьких индексов, уменьшить количество индексов, увеличить количество нод в кластере.

Может быть, если вы хотите сначала видеть последние результаты, то разделить индексы тоже? Индексов будет больше. Например - последний день, последняя неделя, и все остальное. Если большинство поисков будет вам давать то что нужно уже на самом мелком индексе то остальные можно не опрашивать. И данных меньше смотреть.

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

p.s. день может быть и год, и месяц, смотря какие данные.