如何编写脚本

编辑

在 Elasticsearch API 中支持脚本的任何地方,语法都遵循相同的模式;您需要指定脚本的语言,提供脚本逻辑(或源代码),并添加传递给脚本的参数。

  "script": {
    "lang":   "...",
    "source" | "id": "...",
    "params": { ... }
  }
lang
指定编写脚本的语言。默认为 painless
source, id
脚本本身,您将其指定为内联脚本的 source 或存储脚本的 id。 使用存储脚本 API 来创建和管理存储的脚本。
params
指定作为变量传递给脚本的任何命名参数。 使用参数而不是硬编码的值来减少编译时间。

编写您的第一个脚本

编辑

Painless 是 Elasticsearch 的默认脚本语言。 它安全、高性能,并且为任何有少量编码经验的人提供了自然的语法。

Painless 脚本的结构是一个或多个语句,并且可以选择在开头有一个或多个用户定义的函数。 一个脚本必须始终至少有一个语句。

Painless 执行 API 提供了使用简单的用户定义参数测试脚本并接收结果的能力。 让我们从一个完整的脚本开始,并回顾它的组成部分。

首先,索引一个包含单个字段的文档,以便我们有一些数据可以使用

resp = client.index(
    index="my-index-000001",
    id="1",
    document={
        "my_field": 5
    },
)
print(resp)
response = client.index(
  index: 'my-index-000001',
  id: 1,
  body: {
    my_field: 5
  }
)
puts response
const response = await client.index({
  index: "my-index-000001",
  id: 1,
  document: {
    my_field: 5,
  },
});
console.log(response);
PUT my-index-000001/_doc/1
{
  "my_field": 5
}

然后,我们可以构建一个操作该字段的脚本,并作为查询的一部分运行评估该脚本。 以下查询使用搜索 API 的 script_fields 参数来检索脚本估值。 这里发生了很多事情,但我们将分解这些组件以单独理解它们。 现在,您只需要了解此脚本获取 my_field 并对其进行操作。

resp = client.search(
    index="my-index-000001",
    script_fields={
        "my_doubled_field": {
            "script": {
                "source": "doc['my_field'].value * params['multiplier']",
                "params": {
                    "multiplier": 2
                }
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'my-index-000001',
  body: {
    script_fields: {
      my_doubled_field: {
        script: {
          source: "doc['my_field'].value * params['multiplier']",
          params: {
            multiplier: 2
          }
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "my-index-000001",
  script_fields: {
    my_doubled_field: {
      script: {
        source: "doc['my_field'].value * params['multiplier']",
        params: {
          multiplier: 2,
        },
      },
    },
  },
});
console.log(response);
GET my-index-000001/_search
{
  "script_fields": {
    "my_doubled_field": {
      "script": { 
        "source": "doc['my_field'].value * params['multiplier']", 
        "params": {
          "multiplier": 2
        }
      }
    }
  }
}

script 对象

script 源代码

script 是一个标准的 JSON 对象,在 Elasticsearch 中的大多数 API 下定义脚本。 此对象需要 source 来定义脚本本身。 该脚本未指定语言,因此默认为 Painless。

在脚本中使用参数

编辑

Elasticsearch 第一次看到新脚本时,它会编译该脚本并将编译后的版本存储在缓存中。 编译可能是一个繁重的过程。 不要在脚本中硬编码值,而是将它们作为命名的 params 传递。

例如,在前面的脚本中,我们可以直接硬编码值并编写一个看起来不那么复杂的脚本。我们可以只检索 my_field 的第一个值,然后将其乘以 2

"source": "return doc['my_field'].value * 2"

虽然它有效,但这种解决方案非常不灵活。 我们必须修改脚本源代码才能更改乘数,并且每次乘数更改时,Elasticsearch 都必须重新编译脚本。

不要硬编码值,而是使用命名的 params 使脚本灵活,还可以减少脚本运行时编译时间。 现在,您可以在不重新编译 Elasticsearch 脚本的情况下更改 multiplier 参数。

"source": "doc['my_field'].value * params['multiplier']",
"params": {
  "multiplier": 2
}

默认情况下,您可以在 5 分钟内编译多达 150 个脚本。 对于 ingest 上下文,默认的脚本编译速率不受限制。

script.context.field.max_compilations_rate=100/10m

如果您在短时间内编译了太多唯一的脚本,Elasticsearch 将拒绝带有 circuit_breaking_exception 错误的新动态脚本。

缩短脚本

编辑

使用 Painless 原生的语法能力,您可以减少脚本中的冗长,使其更短。 这是一个简单的脚本,我们可以使其更短

resp = client.search(
    index="my-index-000001",
    script_fields={
        "my_doubled_field": {
            "script": {
                "lang": "painless",
                "source": "doc['my_field'].value * params.get('multiplier');",
                "params": {
                    "multiplier": 2
                }
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'my-index-000001',
  body: {
    script_fields: {
      my_doubled_field: {
        script: {
          lang: 'painless',
          source: "doc['my_field'].value * params.get('multiplier');",
          params: {
            multiplier: 2
          }
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "my-index-000001",
  script_fields: {
    my_doubled_field: {
      script: {
        lang: "painless",
        source: "doc['my_field'].value * params.get('multiplier');",
        params: {
          multiplier: 2,
        },
      },
    },
  },
});
console.log(response);
GET my-index-000001/_search
{
  "script_fields": {
    "my_doubled_field": {
      "script": {
        "lang":   "painless",
        "source": "doc['my_field'].value * params.get('multiplier');",
        "params": {
          "multiplier": 2
        }
      }
    }
  }
}

让我们看一下缩短版本的脚本,看看它比之前的迭代版本有哪些改进

resp = client.search(
    index="my-index-000001",
    script_fields={
        "my_doubled_field": {
            "script": {
                "source": "field('my_field').get(null) * params['multiplier']",
                "params": {
                    "multiplier": 2
                }
            }
        }
    },
)
print(resp)
const response = await client.search({
  index: "my-index-000001",
  script_fields: {
    my_doubled_field: {
      script: {
        source: "field('my_field').get(null) * params['multiplier']",
        params: {
          multiplier: 2,
        },
      },
    },
  },
});
console.log(response);
GET my-index-000001/_search
{
  "script_fields": {
    "my_doubled_field": {
      "script": {
        "source": "field('my_field').get(null) * params['multiplier']",
        "params": {
          "multiplier": 2
        }
      }
    }
  }
}

此版本的脚本删除了多个组件并显着简化了语法

  • lang 声明。 由于 Painless 是默认语言,如果您正在编写 Painless 脚本,则无需指定语言。
  • return 关键字。 Painless 会自动使用脚本中的最后一个语句(如果可能)在需要返回值的脚本上下文中生成返回值。
  • get 方法,该方法被括号 [] 替换。 Painless 专门为 Map 类型使用快捷方式,这允许我们使用括号而不是更长的 get 方法。
  • source 语句末尾的分号。 Painless 不要求块的最后一条语句使用分号。 但是,在其他情况下确实需要它们来消除歧义。

在 Elasticsearch 支持脚本的任何地方使用此缩写语法,例如在您创建 运行时字段 时。

存储和检索脚本

编辑

您可以使用存储脚本 API从集群状态存储和检索脚本。 存储的脚本可减少编译时间并加快搜索速度。

与常规脚本不同,存储的脚本要求您使用 lang 参数指定脚本语言。

要创建脚本,请使用创建存储的脚本 API。 例如,以下请求创建一个名为 calculate-score 的存储脚本。

resp = client.put_script(
    id="calculate-score",
    script={
        "lang": "painless",
        "source": "Math.log(_score * 2) + params['my_modifier']"
    },
)
print(resp)
response = client.put_script(
  id: 'calculate-score',
  body: {
    script: {
      lang: 'painless',
      source: "Math.log(_score * 2) + params['my_modifier']"
    }
  }
)
puts response
const response = await client.putScript({
  id: "calculate-score",
  script: {
    lang: "painless",
    source: "Math.log(_score * 2) + params['my_modifier']",
  },
});
console.log(response);
POST _scripts/calculate-score
{
  "script": {
    "lang": "painless",
    "source": "Math.log(_score * 2) + params['my_modifier']"
  }
}

您可以使用获取存储的脚本 API检索该脚本。

resp = client.get_script(
    id="calculate-score",
)
print(resp)
response = client.get_script(
  id: 'calculate-score'
)
puts response
const response = await client.getScript({
  id: "calculate-score",
});
console.log(response);
GET _scripts/calculate-score

要在查询中使用存储的脚本,请在 script 声明中包含脚本 id

resp = client.search(
    index="my-index-000001",
    query={
        "script_score": {
            "query": {
                "match": {
                    "message": "some message"
                }
            },
            "script": {
                "id": "calculate-score",
                "params": {
                    "my_modifier": 2
                }
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'my-index-000001',
  body: {
    query: {
      script_score: {
        query: {
          match: {
            message: 'some message'
          }
        },
        script: {
          id: 'calculate-score',
          params: {
            my_modifier: 2
          }
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "my-index-000001",
  query: {
    script_score: {
      query: {
        match: {
          message: "some message",
        },
      },
      script: {
        id: "calculate-score",
        params: {
          my_modifier: 2,
        },
      },
    },
  },
});
console.log(response);
GET my-index-000001/_search
{
  "query": {
    "script_score": {
      "query": {
        "match": {
            "message": "some message"
        }
      },
      "script": {
        "id": "calculate-score", 
        "params": {
          "my_modifier": 2
        }
      }
    }
  }
}

存储脚本的 id

要删除存储的脚本,请提交删除存储的脚本 API请求。

resp = client.delete_script(
    id="calculate-score",
)
print(resp)
response = client.delete_script(
  id: 'calculate-score'
)
puts response
const response = await client.deleteScript({
  id: "calculate-score",
});
console.log(response);
DELETE _scripts/calculate-score

使用脚本更新文档

编辑

您可以使用更新 API使用指定的脚本更新文档。 脚本可以更新、删除或跳过修改文档。 更新 API 还支持传递部分文档,该文档将合并到现有文档中。

首先,让我们索引一个简单的文档

resp = client.index(
    index="my-index-000001",
    id="1",
    document={
        "counter": 1,
        "tags": [
            "red"
        ]
    },
)
print(resp)
response = client.index(
  index: 'my-index-000001',
  id: 1,
  body: {
    counter: 1,
    tags: [
      'red'
    ]
  }
)
puts response
const response = await client.index({
  index: "my-index-000001",
  id: 1,
  document: {
    counter: 1,
    tags: ["red"],
  },
});
console.log(response);
PUT my-index-000001/_doc/1
{
  "counter" : 1,
  "tags" : ["red"]
}

要增加计数器,您可以提交带有以下脚本的更新请求

resp = client.update(
    index="my-index-000001",
    id="1",
    script={
        "source": "ctx._source.counter += params.count",
        "lang": "painless",
        "params": {
            "count": 4
        }
    },
)
print(resp)
response = client.update(
  index: 'my-index-000001',
  id: 1,
  body: {
    script: {
      source: 'ctx._source.counter += params.count',
      lang: 'painless',
      params: {
        count: 4
      }
    }
  }
)
puts response
const response = await client.update({
  index: "my-index-000001",
  id: 1,
  script: {
    source: "ctx._source.counter += params.count",
    lang: "painless",
    params: {
      count: 4,
    },
  },
});
console.log(response);
POST my-index-000001/_update/1
{
  "script" : {
    "source": "ctx._source.counter += params.count",
    "lang": "painless",
    "params" : {
      "count" : 4
    }
  }
}

类似地,您可以使用更新脚本将标签添加到标签列表中。 因为这只是一个列表,所以即使标签存在也会添加该标签

resp = client.update(
    index="my-index-000001",
    id="1",
    script={
        "source": "ctx._source.tags.add(params['tag'])",
        "lang": "painless",
        "params": {
            "tag": "blue"
        }
    },
)
print(resp)
response = client.update(
  index: 'my-index-000001',
  id: 1,
  body: {
    script: {
      source: "ctx._source.tags.add(params['tag'])",
      lang: 'painless',
      params: {
        tag: 'blue'
      }
    }
  }
)
puts response
const response = await client.update({
  index: "my-index-000001",
  id: 1,
  script: {
    source: "ctx._source.tags.add(params['tag'])",
    lang: "painless",
    params: {
      tag: "blue",
    },
  },
});
console.log(response);
POST my-index-000001/_update/1
{
  "script": {
    "source": "ctx._source.tags.add(params['tag'])",
    "lang": "painless",
    "params": {
      "tag": "blue"
    }
  }
}

您还可以从标签列表中删除标签。 Java Listremove 方法在 Painless 中可用。 它采用您要删除的元素的索引。 为避免可能的运行时错误,您首先需要确保标签存在。 如果列表中包含重复的标签,则此脚本仅删除一个出现项。

resp = client.update(
    index="my-index-000001",
    id="1",
    script={
        "source": "if (ctx._source.tags.contains(params['tag'])) { ctx._source.tags.remove(ctx._source.tags.indexOf(params['tag'])) }",
        "lang": "painless",
        "params": {
            "tag": "blue"
        }
    },
)
print(resp)
response = client.update(
  index: 'my-index-000001',
  id: 1,
  body: {
    script: {
      source: "if (ctx._source.tags.contains(params['tag'])) { ctx._source.tags.remove(ctx._source.tags.indexOf(params['tag'])) }",
      lang: 'painless',
      params: {
        tag: 'blue'
      }
    }
  }
)
puts response
const response = await client.update({
  index: "my-index-000001",
  id: 1,
  script: {
    source:
      "if (ctx._source.tags.contains(params['tag'])) { ctx._source.tags.remove(ctx._source.tags.indexOf(params['tag'])) }",
    lang: "painless",
    params: {
      tag: "blue",
    },
  },
});
console.log(response);
POST my-index-000001/_update/1
{
  "script": {
    "source": "if (ctx._source.tags.contains(params['tag'])) { ctx._source.tags.remove(ctx._source.tags.indexOf(params['tag'])) }",
    "lang": "painless",
    "params": {
      "tag": "blue"
    }
  }
}

您还可以从文档中添加和删除字段。 例如,此脚本添加字段 new_field

resp = client.update(
    index="my-index-000001",
    id="1",
    script="ctx._source.new_field = 'value_of_new_field'",
)
print(resp)
response = client.update(
  index: 'my-index-000001',
  id: 1,
  body: {
    script: "ctx._source.new_field = 'value_of_new_field'"
  }
)
puts response
const response = await client.update({
  index: "my-index-000001",
  id: 1,
  script: "ctx._source.new_field = 'value_of_new_field'",
});
console.log(response);
POST my-index-000001/_update/1
{
  "script" : "ctx._source.new_field = 'value_of_new_field'"
}

相反,此脚本会删除字段 new_field

resp = client.update(
    index="my-index-000001",
    id="1",
    script="ctx._source.remove('new_field')",
)
print(resp)
response = client.update(
  index: 'my-index-000001',
  id: 1,
  body: {
    script: "ctx._source.remove('new_field')"
  }
)
puts response
const response = await client.update({
  index: "my-index-000001",
  id: 1,
  script: "ctx._source.remove('new_field')",
});
console.log(response);
POST my-index-000001/_update/1
{
  "script" : "ctx._source.remove('new_field')"
}

除了更新文档之外,您还可以从脚本内部更改执行的操作。 例如,如果 tags 字段包含 green,则此请求会删除该文档。 否则它不执行任何操作 (noop)

resp = client.update(
    index="my-index-000001",
    id="1",
    script={
        "source": "if (ctx._source.tags.contains(params['tag'])) { ctx.op = 'delete' } else { ctx.op = 'none' }",
        "lang": "painless",
        "params": {
            "tag": "green"
        }
    },
)
print(resp)
response = client.update(
  index: 'my-index-000001',
  id: 1,
  body: {
    script: {
      source: "if (ctx._source.tags.contains(params['tag'])) { ctx.op = 'delete' } else { ctx.op = 'none' }",
      lang: 'painless',
      params: {
        tag: 'green'
      }
    }
  }
)
puts response
const response = await client.update({
  index: "my-index-000001",
  id: 1,
  script: {
    source:
      "if (ctx._source.tags.contains(params['tag'])) { ctx.op = 'delete' } else { ctx.op = 'none' }",
    lang: "painless",
    params: {
      tag: "green",
    },
  },
});
console.log(response);
POST my-index-000001/_update/1
{
  "script": {
    "source": "if (ctx._source.tags.contains(params['tag'])) { ctx.op = 'delete' } else { ctx.op = 'none' }",
    "lang": "painless",
    "params": {
      "tag": "green"
    }
  }
}