如何在无痛脚本 Elasticsearch 5.3 中执行此操作

toy*_*toy 2 vector elasticsearch elasticsearch-5 elasticsearch-painless

我们正在尝试复制这个 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;
        }
    }
Run Code Online (Sandbox Code Playgroud)

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

这是我的索引的形状。

我已启用 delimited_payload_filter

"settings" : {
    "analysis": {
            "analyzer": {
               "payload_analyzer": {
                  "type": "custom",
                  "tokenizer":"whitespace",
                  "filter":"delimited_payload_filter"
                }
      }
    }
 }
Run Code Online (Sandbox Code Playgroud)

我有一个叫做@model_factor存储向量的字段。

{
    "movies" : {
        "properties" : {
            "@model_factor": {
                            "type": "text",
                            "term_vector": "with_positions_offsets_payloads",
                            "analyzer" : "payload_analyzer"
                     }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是文档的形状

{
    "@model_factor":"0|1.2 1|0.1 2|0.4 3|-0.2 4|0.3",
    "name": "Test 1"
}
Run Code Online (Sandbox Code Playgroud)

这是我如何使用脚本

{
    "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"
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我得到的错误。

"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"
        }
      }
    ]
Run Code Online (Sandbox Code Playgroud)

问题是如何访问索引字段以@model_factor进行无痛脚本编写?

Ton*_*Rad 5

选项1

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

{
    "movies" : {
        "properties" : {
            "@model_factor": {
                            "type": "text",
                            "term_vector": "with_positions_offsets_payloads",
                            "analyzer" : "payload_analyzer",
                            "fielddata" : true
                     }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后可以通过访问 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"
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

选项 1 的问题

因此可以访问字段数据值设置fielddata=true,但在这种情况下,该值是作为术语的向量索引,而不是存储在有效负载中的向量值。不幸的是,似乎无法使用无痛脚本和文档值访问令牌有效负载(存储实际向量索引值的位置)。请参阅elasticsearch源代码和另一个类似的问题:访问术语信息

所以答案是使用无痛脚本是不可能访问有效负载的。

我还尝试使用简单的模式标记器来存储向量值,但是在访问术语向量值时,不会保留顺序,这可能是插件的作者决定使用该术语作为字符串然后检索的原因向量的位置 0 作为术语“0”,然后在有效载荷中找到真正的向量值。

选项 2

一个非常简单的替代方法是在文档中使用 n 个字段,每个字段代表向量中的一个位置,因此在您的示例中,我们有一个 5 个暗向量,其值直接存储在 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
} 
Run Code Online (Sandbox Code Playgroud)

然后无痛脚本应该是:

{
    "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"
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

选项 2 的问题

只要向量维数不大,选项 2 是可行的。我认为每个文档的默认 Elasticsearch 最大字段数是 1000,但它也可以在 AWS 环境中更改

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

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

选项 3

第三个选项真的是一个实验,在我看来是最有趣的。它试图利用向量空间模型的内部 Elasticsearch 表示,并且不使用任何脚本进行评分,而是重用基于 tf/idf 的默认相似度评分。

位于 Elasticsearch 核心的 Lucene 已经在内部使用余弦相似度的修改来计算文档之间的相似度分数,其术语的向量空间模型表示如下公式,取自TFIDFSImilarity javadoc,显示:

在此处输入图片说明

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

所以我们可以用 termvectors 索引一个文档,使用 term 向量的索引。如果我们重复 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"
}
Run Code Online (Sandbox Code Playgroud)

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

因此,使用上面相同的示例,输入向量:[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
          }
        }
      ]
Run Code Online (Sandbox Code Playgroud)

norm(t,d)应该在映射中禁用,这样它就不会在上面的公式中使用。所有文档的 idf 部分都是常量,因为它们都包含所有术语(所有向量具有相同的维度)。

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

coord(q,d)是一个常数,因为所有文档都包含所有术语。

选项 3 的问题

需要进行测试。

它仅适用于正数向量,请参阅math stackoverflow 中的这个问题,使其也适用于负数。

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

在查询时,大向量维度的可扩展性可能是一个问题,因为这意味着我们需要使用不同的 boosting 进行 N 个暗淡词查询。

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