КатегорииElasticsearchУроки

Elasticsearch — Урок 6.4 Релевантность

Традиционная база данных обычно содержит структурированные данные. Запрос в базе данных ограничивает данные в зависимости от разных условий, заданных пользователем. Каждое условие в запросе оценивается как true/false, а строки, которые не удовлетворяют условиям, устраняются. Однако полнотекстовый поиск намного сложнее. Данные не структурированы или, по крайней мере, являются таковыми.

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

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

Давайте запросим example6 индекс, который мы создали в начале этого урока. Рассмотрим простой запрос:

POST example6/_search
 {
   "query": {
     "match": {
       "product_name" : "кожаная куртка"
     }
   }
 }

Ответ запроса выглядит следующим образом:

{
   ....
   "hits": {
     "total": 3,
     "max_score": 0.5753642,
     "hits": [
       {
         "_index": "example6",
         "_type": "product",
         "_id": "1",
         "_score": 0.5753642,
         "_source": {
           "product_name": "Мужская качественная кожаная куртка",
           "description": "Лучший выбор. Всесезонная кожаная куртка",
           "unit_price": 79.99,
           "reviews": 250,
           "release_date": "2016-08-16"
         }
       },
       {
         "_index": "example6",
         "_type": "product",
         "_id": "2",
         "_score": 0.2876821,
         "_source": {
           "product_name": "Мужская водостойкая куртка",
           "description": "Обеспечивает комфорт во время езды на велосипеде",
           "unit_price": 69.99,
           "reviews": 5,
           "release_date": "2017-03-02"
         }
       },
       {
         "_index": "example6",
         "_type": "product",
         "_id": "3",
         "_score": 0.2876821,
         "_source": {
           "product_name": "Куртка женская шерстяная",
           "description": "Согреет вас зимой",
           "unit_price": 59.99,
           "reviews": 10,
           "release_date": "2018-01-15"
         }
       }
     ]
   }
}

Из предыдущего ответа видно, что каждый документ содержит _score со следующими оценаками:

ID Наименование товара Оценка
1 Мужская качественная кожаная куртка 0.5753642
2 Мужская водостойкая куртка 0.2876821
3 Куртка женская шерстяная 0.2876821

Мы видим, что документ с ID 1 оценивается чуть выше документов 2 и 3 . Оценка рассчитывается с использованием BM25 алгоритма подобия. По умолчанию результаты сортируются с использованием _score значений.

На очень высоком уровне BM25 вычисляется оценка на основе следующего:

  • Как часто термин появляется в документе — временная частота ( tf )
  • Насколько распространен термин для всех документов — частота обратного документа ( idf )
  • Документы, содержащие все или большинство условий запроса, оцениваются выше, чем документы, который содержат меньше условий
  • Нормализация основана на длине документа, более короткие документы оцениваются лучше, чем более длинные

Чтобы узнать больше о том, как работает алгоритм подобия BM25, перейдите на страницу https://en.wikipedia.org/wiki/Okapi_BM25 .

Не каждый запрос нуждается в релевантности. Вы можете искать документы, которые точно соответствуют значению, например статусу или поиску документов в заданном диапазоне. Elasticsearch позволяет комбинировать как структурированный, так и полнотекстовый поиск в одном запросе. Запрос Elasticsearch может быть выполнен в контексте запроса или в контексте фильтра. В контексте запроса релевантность _score вычисляется для каждого документа, соответствующего запросу. В контексте фильтра все результаты, соответствующие запросу, возвращаются со значением по умолчанию для релевантности 1; мы обсудим более подробно в следующем разделе.

Поиск против фильтрации

По умолчанию, когда запрос выполняется, оценка релевантности рассчитывается для каждого документа. При запуске структурированного запроса (например, возраста, равного 50), или запроса термина в непроанализированном поле (например, пол = мужской), нам не нужно рассчитывать релевантность. Поскольку эти запросы просто отвечают на вопрос да/нет. Вычисление оценки релевантности для каждого результата может быть дорогостоящей операцией. Запустив запрос в контексте фильтра, мы просим Elasticsearch не оценивать результаты.

Балл релевантности, рассчитанный для запроса, применяется только к текущему контексту запроса и не может быть повторно использован. Как мы обсуждали в предыдущем разделе, оценка основана на токенах и их частоте в документе, из-за чего запросы не кэшируются. С другой стороны, фильтры могут автоматически кэшироваться. Чтобы запустить запрос в контексте фильтра, мы должны обернуть запрос в constant_score, как показано здесь:

POST example6/_search
 {
   "query": {
     "constant_score": {
       "filter": {
         "term" : {
           "product_name" : "куртк"
         }
       }
     }
   }
 }

В результате релевантность (_score) документов будет одинаковой равной 1. Запрос выполняется в контексте фильтра и может быть кэширован. Кеширование мы обсудим подробнее чуть позже. Мы также можем запускать запросы, требующие подсчета релевантности так и не требующие в одном запросе. Для этого будем использовать bool, как способ объединения различных запросов:

POST example6/_search
 {
   "query": {
     "bool": {
       "must": [
         {
           "match": { #Query context
             "product_name": "куртка"
           }
         },
         {
           "constant_score": { #Filter context
             "filter": {
               "range": {
                 "unit_price": {
                   "lt": "100"
                 }
               }
             }
           }
         }
       ]
     }
   }
 }

В предыдущем запросе запрос match выполниться в контексте поиска, а запрос диапазона выполняется в контексте фильтра.

Как повысить релевантность на основе одного поля

Хотя алгоритм релевантности по умолчанию (BM25) работает в большинстве случаев, он не является панацеей. Рейтинг основан исключительно на содержании документа. Иногда рейтинг, основанный только на содержании, может быть недостаточным. Например, для пользователя, ищущего ресторан, наряду с его предпочтениями, нам также может потребоваться указать расстояние от ресторана от текущего местоположения пользователя. Рестораны ближе к пользователю оцениваются лучше, чем те, которые нет. В мире электронной коммерции мы хотим учитывать как цену, так и рейтинг продукта при оценке. Возможно, пользователь может пойти на компромисс по цене продукта, учитывая хорошие рейтинги и так далее.

Так же, как мы использовали constant_score, чтобы не оценивать результаты запроса, мы можем использовать function_score для оценки результатов на основе поля или с использованием функций, указанных в запросе. Структура function_score следующая:

{
   "query": {
     "function_score": {
       "query": {},
       "functions": [],
       "filter": {},
       "field_value_factor": {}
     }
   }
 }

Используя field_value_factor, мы можем увеличить счет, используя одно поле в документе. В следующем примере мы хотим, чтобы продукты с лучшими обзорами отображались выше в списке. Наряду с полнотекстовым расчетом релевантности мы можем сказать, чтобы Elasticsearch учитывал количество отзывов при расчете оценки. Чем выше количество обзоров, тем лучше оценка документа. Смотри ниже:

POST example6/_search
 {
   "query": {
     "function_score": {
       "query": { 
         "match": {
           "product_name" : "куртка"
         }
       },
       "field_value_factor": { 
         "field": "reviews"
       }
     }
   }
 }

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

{
   ....
   "hits": {
     "total": 3,
     "max_score": 71.920525,
     "hits": [
       {
         "_index": "example6",
         "_type": "product",
         "_id": "1",
         "_score": 71.920525,
         "_source": {
           "product_name": "Мужская качественная кожаная куртка",
           "description": "Лучший выбор. Всесезонная кожаная куртка",
           "unit_price": 79.99,
           "reviews": 250,
           "release_date": "2016-08-16"
         }
       },
       {
         "_index": "example6",
         "_type": "product",
         "_id": "3",
         "_score": 2.8768208,
         "_source": {
           "product_name": "Куртка женская шерстяная",
           "description": "Согреет вас зимой",
           "unit_price": 59.99,
           "reviews": 10,
           "release_date": "2018-01-15"
         }
       },
       {
         "_index": "example6",
         "_type": "product",
         "_id": "2",
         "_score": 1.4384104,
         "_source": {
           "product_name": "Мужская водостойкая куртка",
           "description": "Обеспечивает комфорт во время езды на велосипеде",
           "unit_price": 69.99,
           "reviews": 5,
           "release_date": "2017-03-02"
         }
       }
     ]
   }
 }

Вы можете видеть из предыдущего ответа, что документ с 250 отзывами оценивается намного выше, чем документ с 10 отзывами.

В следующем запросе мы будем использовать field_value_factor с коэффициентом 0.25. factor контролирует влияние количества отзывов на оценку. По умолчанию он равен 1:

POST example6/_search
 {
   "query": {
     "function_score": {
       "query": {
         "match": {
           "product_name": {
             "query": "куртка"
           }
         }
       },
       "field_value_factor": {
         "field": "reviews",
         "factor": "0.25"
       }
     }
   }
 }

Оценка рассчитывается по следующей формуле:

score = _score + (factor * reviews)

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

Чтобы лучше нормализовать эффект от количества отзывов, мы также можем использовать modifier (модификатор), например логарифмическую функцию. Запрос с модификатором показан здесь:

POST example6/_search
 {
   "query": {
     "function_score": {
       "query": {
         "match": {
           "product_name": {
             "query": "куртка"
           }
         }
       },
       "field_value_factor": {
         "field": "reviews",
         "modifier": "log1p"
       }
     }
   }
 }

Оценки в данном случае:

{_id: 1,  _score: 0.69034314, …}

{_id: 3,  _score: 0.29959002, …}

{_id: 2,  _score: 0.22386017, …}

 

Вы можете видеть из ответа, что документ с идентификатором 1 имеет наивысший балл, так как он имеет больше всех отзывов. Вы также можете учитывать как цену, так и рейтинг продукта при расчете релевантности. Elasticsearch Query DSL поддерживает индивидуальный подсчет или повышение индивидуальных запросов. Усиление запросов позволяет рассчитывать релевантность на основе различных сигналов, таких как цена или расстояние от местоположения пользователя.

Как повысить оценку

В этом разделе мы обсудим, как использовать запрос bool для объединения разных запросов и управление релевантностью индивидуальных запросов. По умолчанию для расчета итогового балла добавляются оценки всех запросов. Документы должны соответствовать всем must запросам. Чем больше should запросов совпадает с документом, тем более актуальным является документ. Например, мы ищем куртки по цене $100 и по крайней мере 25 отзывами.  Чем больше предложений должно соответствовать документу, тем выше оценка. Куртки, которые дороже $100 или имеют обзоров меньше, чем 25 могут появляться в результатах, но они очень низкие по сравнению с куртками, которые соответствуют одному или обоим условиям. Здесь показан запрос bool для ранжирования товаров на основе should:

#Boosting queries using bool
POST example6/_search
 {
   "query": {
     "bool": {
       "must": [
         {
           "match": {
             "product_name": "куртка"
           }
         }
       ],
       "should": [
         {
           "range": {
             "unit_price": {
               "lt": 100
             }
           }
         },
         {
           "range": {
             "reviews": {
               "gte": 25
             }
           }
         }
       ]
     }
   }
 }
Оценки:

{_id: 1,  _score: 2.287682, …}

{_id: 2,  _score: 1.287682, …}

{_id: 3,  _score: 1.287682, …}

Запрос диапазона — это структурированный запрос. Для каждого запроса диапазона, который соответствует документу, 1 балл добавляются к окончательному счету. В предыдущем примере как цена, так и количество отзывов вносят свой вклад в оценку. Например, если больше отзывов более важно, чем цена, мы можем увеличить вес отзывов.  Используяboost мы можем контролировать, насколько сильное влияние может внести в итоговую оценку каждый запрос:

POST example/_search
 {
   "query": {
     "bool": {
       "must": [
         {
           "match": {
             "product_name": "куртка"
           }
         }
       ],
       "should": [
         {
           "range": {
             "unit_price": {
               "lt": 100,
               "boost": 0.5
             }
           }
         },
         {
           "range": {
             "reviews": {
               "gte": 25,
               "boost": 2
             }
           }
         }
       ]
     }
   }
 }
Оценки:

{_id: 1,  _score: 2.787682, «unit_price»: 79.99,  «reviews»: 250, …}

{_id: 2,  _score: 0.78768206, «unit_price»: 69.99,  «reviews»: 5, …}

{_id: 3,  _score: 0.78768206, «unit_price»: 59.99,  «reviews»: 10, …}

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

Как повысить релевантность с помощью функций распада

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

Мы можем начать с определения запросов диапазона для разных временных интервалов и обернуть их в bool. Предполагая, что выпущенные в прошлом году товары менее важны, чем выпущенные в текущем году, в следующем запросе товары, выпущенные в текущем году, повышаются на 1, товары, выпущенные в прошлом году, зададим 0.5:

POST example6/_search
 {
   "query" : {
     "bool": {
       "should": [
         {
           "range": {
             "release_date": {
               "gte": "now/y",
               "boost": 1
             }
           }
         },
         {
           "range": {
             "release_date": {
               "gte": "now-1y/y",
               "lte": "now/y", 
               "boost": "0.5"
             }
           }
         }
       ]
     }
   }
 }

В предыдущем запросе now/y задает текущий год. Использование bool для оценки на основе интервалов не очень практично. Товары, выпущенные в январе прошлого года, оцениваются так же как выпущенные в декабре прошлого года. Независимо от месяца выпуска товара, позиция товара не измениться в рамках года. Чтобы рассчитать более точный балл для числовых значений, можно использовать даты и функции распада.

Функции распада работают так, выбирается начальное значение и далее оценка уменьшает,  поскольку значение удаляется от начала координат. Например, пользователь ищет ресторан поблизости. Мы можем использовать его текущее местоположение в качестве источника и оценивать рестораны на основе расстояния от источника. Используя функции распада, оценка постепенно уменьшается, а не внезапно падает.

В предыдущих разделах мы использовали количество отзывов для расчета оценки релевантности; в этом разделе мы будем использовать дату выпуска вместе с количеством отзывов, чтобы рассчитать оценку. Чтобы использовать функции распада, мы должны указать начало и масштаб. Параметр scale определяет скорость, с которой оценка уменьшается при удалении от начала координат, как показано ниже:

POST example6/_search
 {
   "query": {
     "function_score": {
       "functions": [
         {
           "gauss": {
             "release_date": {
               "origin": "now",
               "scale": "180d"
             }
           }
         }
       ]
     }
   }
 }

Обратите внимание, как мы использовали now как начальное значение и 180d как масштаб. Elasticsearch —  автоматически преобразует их в соответствующие даты.

Принимая во внимание, что новые товары выпускаются не чаще 6 месяц, мы устанавливаем масштаб на 180 дней. Продукт, выпущенный рядом с текущей датой, получает оценку 1, а продукт, выпущенный за шесть месяцев до этого, получает оценку 0.5. В предыдущем запросе мы использовали функцию gauss. В зависимости от того, как вы хотите, чтобы оценка уменьшалась, будь то постепенная, непрерывная кривая или внезапное падение, вы можете выбирать между линейными, экспоненциальными или гауссовыми типами функций.

Затем, вместе с датой выпуска, мы хотим добавить функцию по цене. Например, пользователь пытается найти товары, которые оцениваются по цене $50 и которые недавно выпущены. С помощью function_score запросим группу функций для цены и даты выпуска:

POST example6/_search
{
  "query": {
    "function_score": {
      "functions": [
        {
          "gauss": {
            "unit_price": {
              "origin": "50",
              "scale": "15"
            }
          }
        },
        {
          "gauss": {
            "release_date": {
              "origin": "now",
              "scale": "180d"
            }
          }
        }
      ]
    }
  }
}

Оценка:

{_id: 3, unit_price: 59.99, release_date: «2018-01-15», _score: 0.6897885}

{_id: 2, unit_price: 69.99, release_date: «2017-03-02», _score: 0.014727486}

{_id: 1, unit_price: 79.99, release_date: «2016-08-16», _score: 0.000057596884}

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

 POST example6/_search
 {
   "query": {
     "function_score": {
       "query": {
         "match": {
           "product_name": {
             "query": "куртка"
           }
         }
       },
       "functions": [
         {
           "gauss": {
             "unit_price": {
               "origin": "50",
               "scale": "15"
             }
           },
           "weight": 1
         },
         {
           "gauss": {
             "release_date": {
               "origin": "now",
               "scale": "180d"
             }
           },
           "weight": 2
         }
       ]
     }
   }
 }

Оценки:

{_id: 3, unit_price: 59.99, release_date: «2018-01-15», _score: 0.3968744}

{_id: 2, unit_price: 69.99, release_date: «2017-03-02», _score: 0.008472911}

{_id: 1, unit_price: 79.99, release_date: «2016-08-16», _score: 0.000033134653}

В зависимости от того, что более важно, вы можете влиять на факторы, которые способствуют оптимальному результату.

Rescoring

Для запроса, который соответствует большому количеству документов, подсчет оценок релевантности всех документов может быть довольно дорогостоящим. Чтобы уменьшить эту стоимость и повысить точность, Elasticsearch позволяет вам снимать только верхнюю часть N документов без подсчета всех документов, соответствующих запросу. Например, вы хотите, чтобы расстояние было одним из факторов, влияющих на показатель релевантности, но запрос геоданных, который используется для расчета расстояния, является дорогостоящим. Вы можете использовать основной запрос для расчета оценки всех документов, а затем использовать rescore для расчета лучших N документов на основе расстояния. Предположим, мы хотим получить 10 лучших продуктов на основе отзывов. Здесь показан запрос на рассылку, основанный на скрипте:

POST example6/_search
 {
   "query": {
     "match": {
       "product_name": {
         "query": "куртка"
       }
     }
   },
   "rescore": {
     "window_size": 10,
     "query": {
       "rescore_query": {
         "function_score": {
           "script_score": {
             "script": {
               "inline": "Math.log(params['_source']['reviews'])"
             }
           }
         }
       },
       "query_weight": 0.5,
       "rescore_query_weight": 1.0
     }
   }
 }

В предыдущем запросе в rescore применяется логарифмическая функция для все 10 (в нашем случае 3, потому как их всего три) документов.

Все документы, соответствующие исходному запросу, оцениваются с использованием BM25 алгоритма подобия по умолчанию. Далее, для документов, указанных в window_size(10), выполняется запрос функции log, а влияние запроса rescore определяется с помощью query_weight и rescore_query_weight. Вес по умолчанию 1 для обоих параметров. Ответ на предыдущий запрос выглядит следующим образом:

{
   ....
   "hits": {
     "total": 3,
     "max_score": 5.6653023,
     "hits": [
       {
         "_index": "example6",
         "_type": "product",
         "_id": "1",
         "_score": 5.6653023,
         "_source": {
           "product_name": "Мужская качественная кожаная куртка",
           "description": "Лучший выбор. Всесезонная кожаная куртка",
           "unit_price": 79.99,
           "reviews": 250,
           "release_date": "2016-08-16"
         }
       },
       {
         "_index": "example6",
         "_type": "product",
         "_id": "3",
         "_score": 2.4464262,
         "_source": {
           "product_name": "Куртка женская шерстяная",
           "description": "Согреет вас зимой",
           "unit_price": 59.99,
           "reviews": 10,
           "release_date": "2018-01-15"
         }
       },
       {
         "_index": "example6",
         "_type": "product",
         "_id": "2",
         "_score": 1.753279,
         "_source": {
           "product_name": "Мужская водостойкая куртка",
           "description": "Обеспечивает комфорт во время езды на велосипеде",
           "unit_price": 69.99,
           "reviews": 5,
           "release_date": "2017-03-02"
         }
       }
     ]
   }
 }

Оценка релевантности отладка

Чтобы отлаживать, как рассчитывается значение релевантности для запроса, вы можете использовать параметр explain во время поиска, как показано здесь:

POST example6/_search?explain=true
 {
   "query": {
     "match": {
       "description" : "куртка"
     }
   }
 }

Ответ на запрос будет содержать объяснение расчета:

   "hits": {
        "total": 1,
        "max_score": 0.2876821,
        "hits": [{
            "_shard": "[example6][3]",
            "_node": "eHrZsLVVSWGHXDvISRU4cw",
            "_index": "example6",
            "_type": "product",
            "_id": "1",
            "_score": 0.2876821,
            "_source": {
                "product_name": "Мужская качественная кожаная куртка",
                "description": "Лучший выбор. Всесезонная кожаная куртка",
                "unit_price": 79.99,
                "reviews": 250,
                "release_date": "2016-08-16"
            },
            "_explanation": {
                "value": 0.2876821,
                "description": "weight(description:куртк in 0) [PerFieldSimilarity], result of:",
                "details": [{
                    "value": 0.2876821,
                    "description": "score(doc=0,freq=1.0 = termFreq=1.0\n), product of:",
                    "details": [{
                            "value": 0.2876821,
                            "description": "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:",
                            "details": [{
                                    "value": 1,
                                    "description": "docFreq",
                                    "details": []
                                },
                                {
                                    "value": 1,
                                    "description": "docCount",
                                    "details": []
                                }
                            ]
                        },
                        {
                            "value": 1,
                            "description": "tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:",
                            "details": [{
                                    "value": 1,
                                    "description": "termFreq=1.0",
                                    "details": []
                                },
                                {
                                    "value": 1.2,
                                    "description": "parameter k1",
                                    "details": []
                                },
                                {
                                    "value": 0.75,
                                    "description": "parameter b",
                                    "details": []
                                },
                                {
                                    "value": 5,
                                    "description": "avgFieldLength",
                                    "details": []
                                },
                                {
                                    "value": 5,
                                    "description": "fieldLength",
                                    "details": []
                                }
                            ]
                        }
                    ]
                }]
            }
        }]
    }

Попробуйте поэкспериментировать c разными запросами с параметром explain, и попробуйте разобраться как происходит расчет.

Чтобы узнать больше о том, как BM25 работает, перейдите на страницу https://ru.wikipedia.org/wiki/Okapi_BM25

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *