2

我们正在尝试复制这个 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进行无痛脚本?

4

1 回答 1

6

选项1

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

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

然后可以对访问 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,但在这种情况下,该值是作为术语的向量索引,而不是存储在有效负载中的向量的值。不幸的是,似乎没有办法使用无痛脚本和 doc-values 访问 Token Payload(存储真实矢量索引值的地方)。请参阅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
} 

然后无痛的脚本应该是:

{
    "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 的问题

只要向量维度保持不大,选项 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
}'

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

选项 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"
}

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

因此,使用上面输入向量的相同示例: [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)在映射中禁用,以便在上面的公式中不使用它。idf 部分对于所有文档都是恒定的,因为它们都包含所有术语(所有向量具有相同的维度)。

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

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

选项 3 的问题

需要进行测试。

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

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

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

我将在测试索引中尝试它并用结果编辑这个问题。

于 2017-05-07T20:43:23.617 回答