Elasticsearch - Ищете эффективный способ must_not с идентификаторами
У меня следующая ситуация:
В настоящее время мы осуществляем поиск продуктов с помощью коммерческого решения. Я играю с Elasticsearch, чтобы реализовать наш текущий поиск продуктов с Elasticsearch, который в принципе работает очень хорошо. Но у нас есть одна специальность. У нас есть каталог товаров, насчитывающий около 1 миллиона товаров, но не каждому покупателю разрешено покупать каждый товар. Есть много правил, определяющих, может ли продукт быть куплен покупателем.
Это не просто
Клиент А не имеет права покупать товары продавца А
Или же:
Клиент B не имеет права покупать товары категории B поставщика B.
Это было бы легко.
Чтобы получить эти продукты, которые клиент не имеет права покупать, мы внедрили микросервис / веб-сервис несколько лет назад. Этот веб-сервис возвращает черный список продуктов, просто список номеров продуктов.
Проблема заключается в том, что, если я просто запускаю запрос в Elasticsearch, игнорируя эти продукты из черного списка, я получаю обратно продукты, которые покупателю не разрешено покупать. Если я сделаю запрос только о 10 самых популярных поисковых запросах, может случиться так, что мне не разрешат показывать эти продукты, потому что покупателю не разрешено их покупать. Кроме того, если я использую агрегаты для поставщиков и категорий, я получаю информацию о продавцах и / или категориях, у которых клиент, вероятно, не имеет права покупать.
Что я делал в своем прототипе?
Перед запросом Elasticsearch я запрашиваю черный список продуктов для определенного клиента (и, конечно, кеширую его). После того, как я получил черный список, я запускаю запрос, подобный этому:
{
"query" : {
"bool" : {
"must_not" : [
{
"ids" : {
"values" : [
// Numbers of blacklisted products. Can be thousands!
1234567,
1234568,
1234569,
1234570,
...
]
}
}
],
"should" : [
{
"query" : {
...
}
]
}
}
}
"aggregations" : {
...
}
}
Это работает очень хорошо, но у нас есть клиенты, у которых тысячи продуктов из черного списка. Поэтому, с одной стороны, я боюсь, что сетевой трафик будет слишком высоким, и я понял, что полный запрос Elasticsearch заметно медленнее. Но это зависит в основном от количества перечисленных товаров.
Мой следующий подход состоял в том, чтобы разработать свой собственный построитель запросов Elasticsearch в качестве плагина, который обрабатывает черные списки внутри Elasticsearch. Этот запрос в черный список расширяет AbstractQueryBuilder и использует TermInSetQuery. Таким образом, этот построитель запросов один раз запрашивает черный список данного клиента, кэширует его и создает TermInSetQuery со всеми этими номерами продуктов, занесенными в черный список.
Теперь мой запрос выглядит так:
{
"query" : {
"bool" : {
"must_not" : [
{
"blacklist" : { <-- This is my own query builder
"customer" : 1234567
}
}
],
"should" : [
{
"query" : {
...
}
]
}
}
}
"aggregations" : {
...
}
}
Это решение быстрее и не должно каждый раз отправлять полный список номеров продуктов в черном списке в запросе. Так что у меня нет сетевых накладных расходов. Но запрос все еще заметно медленнее, чем без этого чёрного списка. Я профилировал этот запрос, и я не удивлен, увидев, что мой черный список запросов занимает около 80-90% времени выполнения.
Я думаю, что этот TermInSetQuery работает очень плохо в моем случае. Потому что я предполагаю, что соответствующий процесс сопоставления Lucene с Elasticsearch - это нечто большее, чем просто:
if (blacklistSet.contains(id)) {
continue; // ignore the current search hit.
}
У кого-нибудь из вас есть подсказка для меня, как сделать такой механизм черного списка более производительным?
Есть ли способ перехватить процесс запроса Elasticsearch/Lucene? Может быть, я могу написать свой собственный реальный запрос Lucene вместо использования TermInSetQuery.
Заранее спасибо.
Кристиан
2 ответа
Это не решение, но, возможно, другой подход.
Прежде всего, вот старый пост SO, который может вас заинтересовать. Насколько я знаю, более поздние версии Elasticsearch не вводили / не изменяли что-то более или более подходящее.
Если вы перейдете по ссылке ответа на страницу " Условия запроса", вы найдете очень простой пример.
Теперь вместо кэширования ваших черных списков вы можете создать индекс и сохранить черный список для каждого клиента. Затем вы можете использовать запрос терминов и, в основном, ссылаться на черный список из другого индекса (= кэш черного списка).
Я не знаю частоту обновлений в этих черных списках, так что, возможно, это может быть проблемой. Кроме того, вы должны быть осторожны, чтобы не быть не синхронизированы. Особо следует отметить тот факт, что индексные вставки / обновления по умолчанию видны не сразу. Поэтому вам может потребоваться принудительное обновление при индексировании / обновлении черных списков.
Как я уже сказал, это не может быть решением. Но если это кажется вам возможным, возможно, стоит попробовать сравнить с другими вашими решениями.
Спасибо за советы. На самом деле я хотел избежать индексирования информации в черном списке. Поэтому я решил написать свой собственный плагин черного списка Elasticsearch. Но чем больше я об этом думаю, тем хуже мне не нравится моя идея. Если бы я мог избавиться от своего плагина, мне не пришлось бы поддерживать свой плагин, и было бы проще перейти, например, в облако. Итак, я попробовал несколько вещей.
Тестовый сценарий:
Я создал тестовый индекс со 100000 документов, включая информацию о том, какой клиент не имеет права покупать продукт. Например
{
"id" : "123456"
"description" : "My example products",
...
"blacklist" : [ <lots_of_customer_numbers> ]
}
Кроме того, я создал индекс черного списка с одним документом с черным списком из 10 000 элементов для проверки поиска терминов. (Должен представлять черный список для одного клиента.)
Я использовал существующую установку Elasticsearch версии 5.1.2.
Тест 1:
Черный список игнорируется. Просто запрос по ключевому слову.
"query" : {
"bool" : {
"must" : [
{
"multi_match" : {
"query" : <keyword>,
"fields" : [
"_all"
]
}
}
]
}
}
Тест 2:
Черный список взят в соответствие с must_not и ключевыми словами ids plus. (Примечание. Сервер и клиент находятся на одном хосте. Поэтому у нас нет узкого места в сети.)
"query" : {
"bool" : {
"must" : [
{
"multi_match" : {
"query" : <keyword>,
"fields" : [
"_all"
]
}
}
],
"must_not" : [
{
"ids" : {
"values" : [ <10000_ids> ]
}
}
]
}
}
Тест 3:
Черный список учитывается при поиске терминов плюс ключевое слово.
"query" : {
"bool" : {
"must" : [
{
"multi_match" : {
"query" : <keyword>,
"fields" : [
"_all"
]
}
}
],
"must_not" : [
{
"terms" : {
"blacklist" : {
"index" : "blacklists",
"type" : "blacklist",
"id" : "1234567",
"path" : "items"
}
}
}
]
}
}
Тест 4:
Черный список учитывается с помощью must_not и термина запроса в пределах одного индекса и документов плюс ключевое слово.
"query" : {
"bool" : {
"must" : [
{
"multi_match" : {
"query" : <keyword>,
"fields" : [
"_all"
]
}
}
],
"must_not" : [
{
"term" : {
"blackList" : {
"value" : "1234567"
}
}
}
]
}
}
Я сделал 1000 поисков для каждого тестового сценария. И вот результат:
Тест 1: 3,708мс
Тест 2: 104,775мс
Тест 3: 39 586 мс
Тест 4: 3586 мс
Как вы можете видеть, тест 2 с must_not и ids выполняется медленнее. Тест 3 с поиском терминов выполняется примерно в 11 раз медленнее, чем тест 1.Тест 4 работает немного лучше, чем тест 1.
Я постараюсь, будет ли сценарий теста 3 достаточным для моих реальных потребностей, потому что понять это довольно просто. Если нет, то я должен пойти по сценарию теста 4, который был бы более сложным в моем реальном живом сценарии.
Большое спасибо, еще раз.