我怎样才能在无痛脚本 Elasticsearch 5.3 中做到这一点

How can I do this in painless script Elasticsearch 5.3

我们正在尝试复制此 ES 插件 https://github.com/MLnick/elasticsearch-vector-scoring。原因是 AWS ES 不允许安装任何自定义插件。该插件只是做点积和余弦相似度,所以我猜想在 painless 脚本中复制它应该非常简单。看起来 groovy 脚本在 5.0 中已被弃用。

这是插件的源代码。

    /**
     * @param params index that a scored are placed in this parameter. Initialize them here.
     */
    @SuppressWarnings("unchecked")
    private PayloadVectorScoreScript(Map<String, Object> params) {
        params.entrySet();
        // get field to score
        field = (String) params.get("field");
        // get query vector
        vector = (List<Double>) params.get("vector");
        // cosine flag
        Object cosineParam = params.get("cosine");
        if (cosineParam != null) {
            cosine = (boolean) cosineParam;
        }
        if (field == null || vector == null) {
            throw new IllegalArgumentException("cannot initialize " + SCRIPT_NAME + ": field or vector parameter missing!");
        }
        // init index
        index = new ArrayList<>(vector.size());
        for (int i = 0; i < vector.size(); i++) {
            index.add(String.valueOf(i));
        }
        if (vector.size() != index.size()) {
            throw new IllegalArgumentException("cannot initialize " + SCRIPT_NAME + ": index and vector array must have same length!");
        }
        if (cosine) {
            // compute query vector norm once
            for (double v: vector) {
                queryVectorNorm += Math.pow(v, 2.0);
            }
        }
    }

    @Override
    public Object run() {
        float score = 0;
        // first, get the ShardTerms object for the field.
        IndexField indexField = this.indexLookup().get(field);
        double docVectorNorm = 0.0f;
        for (int i = 0; i < index.size(); i++) {
            // get the vector value stored in the term payload
            IndexFieldTerm indexTermField = indexField.get(index.get(i), IndexLookup.FLAG_PAYLOADS);
            float payload = 0f;
            if (indexTermField != null) {
                Iterator<TermPosition> iter = indexTermField.iterator();
                if (iter.hasNext()) {
                    payload = iter.next().payloadAsFloat(0f);
                    if (cosine) {
                        // doc vector norm
                        docVectorNorm += Math.pow(payload, 2.0);
                    }
                }
            }
            // dot product
            score += payload * vector.get(i);
        }
        if (cosine) {
            // cosine similarity score
            if (docVectorNorm == 0 || queryVectorNorm == 0) return 0f;
            return score / (Math.sqrt(docVectorNorm) * Math.sqrt(queryVectorNorm));
        } else {
            // dot product score
            return score;
        }
    }

我试图从从索引中获取一个字段开始。但我收到错误。

这是我索引的形状。

我已经启用delimited_payload_filter

"settings" : {
    "analysis": {
            "analyzer": {
               "payload_analyzer": {
                  "type": "custom",
                  "tokenizer":"whitespace",
                  "filter":"delimited_payload_filter"
                }
      }
    }
 }

我有一个名为 @model_factor 的字段来存储向量。

{
    "movies" : {
        "properties" : {
            "@model_factor": {
                            "type": "text",
                            "term_vector": "with_positions_offsets_payloads",
                            "analyzer" : "payload_analyzer"
                     }
        }
    }
}

这是文件的形状

{
    "@model_factor":"0|1.2 1|0.1 2|0.4 3|-0.2 4|0.3",
    "name": "Test 1"
}

这是我使用脚本的方式

{
    "query": {
        "function_score": {
            "query" : {
                "query_string": {
                    "query": "*"
                }
            },
            "script_score": {
                "script": {
                    "inline": "def termInfo = doc['_index']['@model_factor'].get('1', 4);",
                    "lang": "painless",
                    "params": {
                        "field": "@model_factor",
                        "vector": [0.1,2.3,-1.6,0.7,-1.3],
                        "cosine" : true
                    }
                }
            },
            "boost_mode": "replace"
        }
    }
}

这是我得到的错误。

"failures": [
      {
        "shard": 2,
        "index": "test",
        "node": "ShL2G7B_Q_CMII5OvuFJNQ",
        "reason": {
          "type": "script_exception",
          "reason": "runtime error",
          "caused_by": {
            "type": "wrong_method_type_exception",
            "reason": "wrong_method_type_exception: cannot convert MethodHandle(List,int)int to (Object,String)String"
          },
          "script_stack": [
            "termInfo = doc['_index']['@model_factor'].get('1',4);",
            "              ^---- HERE"
          ],
          "script": "def termInfo = doc['_index']['@model_factor'].get('1',4);",
          "lang": "painless"
        }
      }
    ]

问题是如何访问索引字段以在无痛脚本中获取 @model_factor

选项 1

由于@model_factor 是一个 text 字段,在无痛脚本中,可以访问它,在映射中设置 fielddata=true。所以映射应该是:

{
    "movies" : {
        "properties" : {
            "@model_factor": {
                            "type": "text",
                            "term_vector": "with_positions_offsets_payloads",
                            "analyzer" : "payload_analyzer",
                            "fielddata" : true
                     }
        }
    }
}

然后就可以得分了accessing doc-values:

{
    "query": {
        "function_score": {
            "query" : {
                "query_string": {
                    "query": "*"
                }
            },
            "script_score": {
                "script": {
                    "inline": "return Double.parseDouble(doc['@model_factor'].get(1)) * params.vector[1];",
                    "lang": "painless",
                    "params": {
                        "vector": [0.1,2.3,-1.6,0.7,-1.3]
                    }
                }
            },
            "boost_mode": "replace"
        }
    }
}

选项 1 有问题

因此可以访问字段数据值设置 fielddata=true,但在这种情况下,该值是作为术语的向量索引,而不是存储在有效负载中的向量的值。不幸的是,似乎无法使用无痛脚本和文档值访问令牌有效负载(存储实际矢量索引值的位置)。见 source code for elasticsearch and another similar question re: accessing term info.

所以答案是使用无痛脚本无法访问负载。

我也尝试使用简单的模式分词器来存储向量值,但是当访问术语向量值时,顺序没有保留,这可能是插件作者决定使用术语作为的原因一个字符串,然后检索 the position 0 of the vector as the term "0" 然后在有效载荷中找到真正的向量值。

选项 2

一个非常简单的替代方法是在文档中使用 n 个字段,每个字段代表向量中的一个位置,因此在您的示例中,我们有一个 5 dim 向量,其值直接存储在 v0...v4 中,如下所示双:

{
    "@model_factor":"0|1.2 1|0.1 2|0.4 3|-0.2 4|0.3",
    "name": "Test 1",
    "v0" : 1.2,
    "v1" : 0.1,
    "v2" : 0.4,
    "v3" : -0.2,
    "v4" : 0.3
} 

然后无痛脚本应该是:

{
    "query": {
        "function_score": {
            "query" : {
                "query_string": {
                    "query": "*"
                }
            },
            "script_score": {
                "script": {
                    "inline": "return doc['v0'].getValue() * params.vector[0];",
                    "lang": "painless",
                    "params": {
                        "vector": [0.1,2.3,-1.6,0.7,-1.3]
                    }
                }
            },
            "boost_mode": "replace"
        }
    }
}

应该可以很容易地迭代输入向量长度并动态获取字段来计算我为简单起见而写的修改 doc['v0'].getValue() * params.vector[0] 的点积。

选项 2 有问题

只要向量维数不大,方案二是可行的。我认为默认的 Elasticsearch 每个文档的最大字段数是 1000,但 it can be changed 也在 AWS 环境中:

curl -X PUT \
  'https://.../indexName/_settings' \
  -H 'cache-control: no-cache' \
  -H 'content-type: application/json' 
  -d '{
"index.mapping.total_fields.limit": 2000
}'

此外,还应该测试大量文件的脚本速度。 也许在重新评分/重新排名的场景中,这是一个可行的解决方案。

选项 3

第三个选项确实是一个实验,在我看来是最吸引人的。 它尝试利用 Vector Space 模型的内部 Elasticsearch 表示,并且不使用任何脚本来评分,而是重用基于 tf/idf.

的默认相似性评分

位于 Elasticsearch 核心的 Lucene 已经在内部使用与 calculate the similarity score between documents in his Vector Space Model representation of terms as the formula below, taken from the TFIDFSImilarity javadoc 的余弦相似度的修改,显示:

特别是,表示该字段的向量的权重是该字段项的 tf/idf 值。

因此我们可以使用词向量索引文档,使用词向量的索引。如果我们重复 N 次,我们表示向量的值,利用评分公式的 tf 部分。 这意味着向量的域应该在 {1.. Infinite} 正整数域中进行转换和重新缩放。我们从 1 开始,这样我们可以确保所有文档都包含所有术语,这样可以更容易地利用公式。

例如,向量:[21, 54, 45] 可以使用简单的空白分析器和以下值作为文档中的字段进行索引:

{
    "@model_factor" : "0<repeated 21 times> 1<repeated 54 times> 2<repeated 45 times>",
    "name": "Test 1"
}

然后查询,即计算点积,我们提升表示向量索引位置的单个项。

因此使用输入向量上方的相同示例:[45, 1, 1] 将在查询中转换为:

"should": [
        {
          "term": {
            "@model_factor": {
              "value": "0",
              "boost": 45 
            }
          }
        },
        {
          "term": {
            "@model_factor": "1" // boost:1 by default

          }
        },
        {
          "term": {
            "@model_factor": "2"  // boost:1 by default
          }
        }
      ]

norm(t,d) 应该是 disabled in the mapping 这样就不会在上面的公式中使用了。 idf 部分对于所有文档都是不变的,因为它们都包含所有术语(所有向量具有相同的维度)。

queryNorm(q) 对于上面公式中的所有文档都是一样的,所以这不是问题。

coord(q,d) 是常数,因为所有文档都包含所有项。

选项 3 有问题

需要测试。

它仅适用于正数向量,请参阅 math Whosebug 中的此问题以使其也适用于负数。

它与点积不完全相同,但非常接近于基于原始向量找到相似的文档。

大向量维度上的可伸缩性在查询时可能是一个问题,因为这意味着我们需要使用不同的提升来执行 N 个暗项查询。

我将在测试索引中尝试并使用结果编辑此问题。