Как получить нужную релевантность результатов в поиске по multivalue полю?

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

Проблема у меня в том, что elastic рассматривает поле со множеством значений (массив) как строку, и релевантность результатов поиска считается как суммарная релевантность по всему массиву, а не как релевантность одного конкретного совпавшего элемента массива.

Ниже сильно упрощеный пример.

Создаём индекс
curl -XDELETE 'http://localhost:9200/tests'
curl -XPUT 'http://localhost:9200/tests' -d'{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_analyzer": {
          "type": "custom",
          "tokenizer": "edge_ngram_tokenizer",
          "filter": ["lowercase", "asciifolding"]
        }
      },
      "tokenizer": {
        "edge_ngram_tokenizer": {
          "type": "edgeNGram",
          "min_gram": "3",
          "max_gram": "12",
          "token_chars": ["letter", "digit"]
        }
      }
    }
  },
  "mappings": {
    "test": {
      "properties": {
        "name": {
          "type": "string",
          "analyzer": "my_analyzer"
        }
      }
    }
  }
}'


Заполняем значениями
curl -XPOST 'http://localhost:9200/tests/test' -d'{ "id": 1, "name": ["text"] }'
curl -XPOST 'http://localhost:9200/tests/test' -d'{ "id": 2, "name": ["text", "text"] }'


Ищем
curl -XGET 'http://localhost:9200/tests/test/_search' -d'{
  "query": {
    "match": {
      "name": "text"
    }
  }
}'


Результаты
{
  "took": 0,
  "timed_out": false,
  "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 },
  "hits": {
    "total": 2,
    "max_score": 0.7911257,
    "hits": [{
      "_index": "tests",
      "_type": "test",
      "_id": "AWOtIL2gdpqdbX7hdDXg",
      "_score": 0.7911257,
      "_source": { "id": 2, "name": [ "text", "text" ] }
    }, {
      "_index": "tests",
      "_type": "test",
      "_id": "AWOtIL0ldpqdbX7hdDXf",
      "_score": 0.51623213,
      "_source": { "id": 1, "name": [ "text" ] }
    }]
  }
}


В результате имеем у id: 2 релевантность 0.7911257, а у id: 1 релевантность 0.51623213.
Мне нужно по запросу получить обе строки, и получить у них обязательно одинаковую релевантность.

Что делать?

Моя проблема происходит из-за того, что в production варианте кода я разбиваю название на несколько подполей, которые анализируются разными analyzer'ами, по которым затем идёт сложный dis_max поиск с разными весовыми коэффициентами на каждое из подполей (буст на полное совпадение, буст на совпадение начала стоки, поиск по частичному совпадению т.д.).

Я знаю два решения проблемы, но оба мне не подходят. Возможно, есть ещё какие-то варианты?

1. Когда названий мало, их можно хранить в отдельных полях name_0, name_1, name_2 и т.д.
При поиске делать dis_max запрос с tie_breaker: 0 и с релевантностью всё будет было хорошо.
"query": {
  "dis_max": {
    "queries": [
      { "match": { "name_0": "text" } },
      { "match": { "name_1": "text" } },
      { "match": { "name_2": "text" } }
    ],
    "tie_breaker": 0,
    "boost": 1
  }
}


2. Можно хранить в эластике по одной записи на каждое название
curl -XPOST 'http://localhost:9200/tests/test' -d'{ "product_id": 1, "name": "text" }'
curl -XPOST 'http://localhost:9200/tests/test' -d'{ "product_id": 2, "name": "text" }'
curl -XPOST 'http://localhost:9200/tests/test' -d'{ "product_id": 2, "name": "text" }'

В таком варианте полученные результаты приходится дополнительно агрегировать по product_id, и в этом случае получаем проблемы с пагинацией результатов и с дальнейшим агрегированием результатов.
  • Вопрос задан
  • 1006 просмотров
Решения вопроса 1
morr
@morr Автор вопроса
Отвечу сам на свой вопрос.

Если хочется хранить массив названий в одном поле, то к "name" полю можно добавить "index_options": "docs", и количество вхождений слова перестанет влиять на релевантность.
Так же можно добавить "norms": { "enabled": false } и общая длина строки перестанет влиять на релевантность.
Но всё это костыли, которые не помогут для более сложных случаев, когда затем вам захочется сделать разные хитрые комбинации повышения/понижения релевантности для разных ситуаций.
Например, с массивом названий не выйдет сделать буст релевантности при полном совпадении одного из названий с искомой фразой.

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

Для понимания того, как считается релевантность очень помогла статья Theory Behind Relevance Scoring.

Так же для себя обнаружил, что на релевантность влияет, насколько часто встречается слово во всём индексе https://www.elastic.co/guide/en/elasticsearch/guid... И влияет оно порой очень сильно. Поэтому при реализации поиска по названиям idf фактор ранжирования стоит в обязательном порядке отключать https://stackoverflow.com/questions/33208587/elast... (работает в версии эластика начиная с 6.2)
Ответ написан
Комментировать
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы