ElasticSearch Painless: ошибка использования векторных функций в циклах for

Я столкнулся с тем, что кажется ошибкой в ​​​​Painless: если используется векторная функция, скажем, l2norm(), результат остается таким же, как и в первой итерации. Я использую безболезненный скрипт для оценки функции, надеюсь, приведенный ниже запрос прольет свет. Я использую исключение, чтобы увидеть, какое значение имеет каждая итерация, и каждый раз это оценка первого вектора. Я знаю это, потому что пару раз перебирал параметры, и каждый раз счет зависал на первом. Итак, я думаю, что происходит то, что функция l2norm() (и все векторные функции?!) являются экземплярами объекта, которые могут быть созданы только один раз? Если бы это было так, что бы обойти?

Ссылка на обсуждение ES: https://discuss.elastic.co/t/painless-bug-using-for-loops-and-vector-functions/267263

    {
        "query": {
                "nested": {
                        "path": "media",
                        "query": {
                                "function_score": {
                                        "boost_mode": "replace",
                                        "query": {
                                                "bool": {                                                       
                                                        "filter": [{
                                                                "exists": {
                                                                        "field": "media.full_body_dense_vector"
                                                                }
                                                        }]
                                                }
                                        },
                                        "functions": [{
                                                "script_score": {
                                                        "script": {
                                                                "source": "if (params.filterVectors.size() > 0 && params.filterCutOffScore >= 0) {\n  for (int i=0; i < params.filterVectors.size();i++) {\n    def c = params.filterVectors[i];  double euDistance =  l2norm(c, doc['media.full_body_dense_vector']);\n  if (i==1) { throw new Exception(euDistance + ''); }        \n      }\n     return 1.0f;",
                                                                "params": {
                                                                      "filterVectors":[
[1.0,2.0,3.0],[0.1,0.4,0.5]
                                                                        ],
                                                                        "filterCutOffScore": 1.04
                                                                },
                                                                "lang": "painless"
                                                        }
                                                }
                                        }]
                                }
                        }
                }
        },
        "size": 500,
        "from": 0,
        "track_scores": true
}

person Jan Vansteenlandt    schedule 15.03.2021    source источник


Ответы (2)


В то время как l2norm является статическим методом, она точно не должна вести себя как чистая функция!

Я немного исследовал, и кажется, что есть только ошибка на уровне цикла. Когда вы вызываете l2norm вне цикла либо с параметризованными, либо с жестко закодированными векторами, результаты всегда будут разными — как и должно быть. Но не в цикле for (я также тестировал цикл while - тот же результат). Вот минимальный воспроизводимый пример, который можно использовать для сообщения об ошибке на github:

"script": {
  "source": """
      def field = doc['media.full_body_dense_vector'];
      
      def hardcodedVectors = [ [1,2,3], [0.1,0.4,0.5] ];
      
      def noLoopDistances = [
        l2norm(hardcodedVectors[0], field),
        l2norm(hardcodedVectors[1], field)
      ];
      
      def hardcodedDistances = [];
      for (vector in hardcodedVectors) {
        double euDistance = l2norm(vector, field);
        hardcodedDistances.add(euDistance);
      }
      
      def parametrizedDistances = [];
      for (vector in params.filterVectors) {
        double euDistance = l2norm(vector, field);
        parametrizedDistances.add(euDistance);
      }
      
      def comparisonMap = [
        "no-loop": noLoopDistances,
        "hardcoded": hardcodedDistances,
        "parametrized": parametrizedDistances
      ];
      
      Debug.explain(comparisonMap);
     """,
  "params": {
    "filterVectors": [ [1,2,3], [0.1,0.4,0.5] ]
  },
  "lang": "painless"
}

который дает

{
  "no-loop":[             
    8.558621384311845,    // <-- the only time two different l2norm calls behave correctly
    11.071133967619906
  ],
  "parametrized":[
    8.558621384311845,
    8.558621384311845
  ],
  "hardcoded":[
    8.558621384311845,
    8.558621384311845
  ]
}

Это говорит мне о том, что дело не в кэширование во время выполнения, а скорее что-то еще, что должно быть исследовано командой Elastic.

Обходной путь на данный момент состоит в том, чтобы продолжать использовать параметризованные векторы, но вместо циклического выполнения проверок, подобных каменному веку:

if (params.filterVectors.length == 0) {
  // default to something
} else if (params.filterVectors.length == 1) {
  // call l2norm once
} else if (params.filterVectors.length == 2) {
  // call l2norm twice, separately
}

P.S. Выбросить new Exception() для отладки Painless — это нормально. Использование Debug.explain еще лучше по причинам, описанным в этой подглаве< моего Руководства по Elasticsearch.

person Joe Sorocin    schedule 16.03.2021
comment
Большое спасибо, Джо! Я действительно переворачивал каждый камень, чтобы убедиться, что я не воображаю проблему, я уже создал тикет об ошибке, а также тикет, чтобы разрешить нулевые значения для полей плотности_вектора, прямо сейчас вам нужно написать сценарий удаления, потому что вы не можете назначить пустой массив или ноль. Ссылка: github.com/elastic/elasticsearch/issues/70437 - person Jan Vansteenlandt; 16.03.2021
comment
Я заметил вашу книгу, включает ли она функцию оценки, которая учитывает несколько блоков (например, векторную оценку, сопоставление терминов, текстовый запрос, выталкивающий 1 оценку?) Возвращаясь к вопросу, это действительно глупо, но поскольку это, вероятно, ошибка, у меня нет другого варианта, мой массив векторов является переменным, поэтому я просто распечатываю их все, как если бы цикл for проходил через них... ужасный код... но он работает, и другого варианта нет :). Ваш if/else будет работать, но длина варьируется. - person Jan Vansteenlandt; 16.03.2021
comment
Хорошо, я проголосовал за ваш выпуск GH! Книга включает в себя множество материалов, касающихся подсчета очков, фильтрации терминов, постоянных оценок (1) и т. д., но, честно говоря, ничего о векторах. Это широкий взгляд на несколько тем, все из которых перечислены на главной странице. Дать ему шанс! - person Joe Sorocin; 16.03.2021

Во-первых, спасибо Джо за то, что он подтвердил, что мне не приснилось, и это действительно ошибка. Во-вторых, прекрасная команда ElasticSearch рассмотрела проблему и подтвердила, что это ошибка, поэтому ответ на этот пост: ссылка на Github Issue, чтобы в будущем люди могли отслеживать, в какой версии ElasticSearch исправлено это поведение.

person Jan Vansteenlandt    schedule 18.03.2021
comment
Рад подтвердить это ???? Эй, Ян, бессовестный плаг — загляни в мой Руководство, в котором есть пара глав о довольно продвинутых сценариях. варианты использования, которые могут быть вам полезны! - person Joe Sorocin; 18.03.2021