ElasticSearch Painless:在 for 循环错误中使用向量函数

ElasticSearch Painless: using vector functions in for loops bug

我 运行 研究了 Painless 中的一个错误,如果使用向量函数,比如 l2norm(),结果与第一次迭代的结果相同。我在函数评分中使用无痛脚本,我希望下面的查询能提供一些启发。我正在使用“异常”来查看每次迭代中的值是什么,每次都是第一个向量的分数。我知道这一点是因为我循环了几次参数,而分数每次都“卡在”第一件事上。所以我认为正在发生的是函数 l2norm() (以及所有向量函数?!)是只能实例化一次的对象实例?如果是这样,有什么解决方法?

Link 到 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
}

虽然 l2norm 是一个 static method,但它的行为当然不应该像一个纯函数!

我调查了一下,似乎只有一个循环级别的错误。当您使用参数化向量或硬编码向量在循环外调用 l2norm 时,结果将始终不同——它们本应如此。但不在 for 循环内(我也测试了 while 循环——同样的结果)。这是可用于 report a bug on 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
  ]
}

这告诉我,这不是 runtime caching 的问题,而是 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 就可以了。由于 this sub-chapter on Debugging of my Elasticsearch Handbook.

中解释的原因,使用 Debug.explain 甚至更好

首先,感谢 Joe 确认我不是凭空想象,这确实是一个错误。其次,可爱的 ElasticSearch 团队一直在对问题进行分类并确认它是一个错误,所以这个 post 的答案是 a link to the Github Issue 所以在未来,人们可以跟踪这个行为是在哪个 ElasticSearch 版本中修补的。