Painless 简明教程

编辑

为了说明 Painless 的工作原理,让我们加载一些曲棍球统计数据到一个 Elasticsearch 索引中

PUT hockey/_bulk?refresh
{"index":{"_id":1}}
{"first":"johnny","last":"gaudreau","goals":[9,27,1],"assists":[17,46,0],"gp":[26,82,1],"born":"1993/08/13"}
{"index":{"_id":2}}
{"first":"sean","last":"monohan","goals":[7,54,26],"assists":[11,26,13],"gp":[26,82,82],"born":"1994/10/12"}
{"index":{"_id":3}}
{"first":"jiri","last":"hudler","goals":[5,34,36],"assists":[11,62,42],"gp":[24,80,79],"born":"1984/01/04"}
{"index":{"_id":4}}
{"first":"micheal","last":"frolik","goals":[4,6,15],"assists":[8,23,15],"gp":[26,82,82],"born":"1988/02/17"}
{"index":{"_id":5}}
{"first":"sam","last":"bennett","goals":[5,0,0],"assists":[8,1,0],"gp":[26,1,0],"born":"1996/06/20"}
{"index":{"_id":6}}
{"first":"dennis","last":"wideman","goals":[0,26,15],"assists":[11,30,24],"gp":[26,81,82],"born":"1983/03/20"}
{"index":{"_id":7}}
{"first":"david","last":"jones","goals":[7,19,5],"assists":[3,17,4],"gp":[26,45,34],"born":"1984/08/10"}
{"index":{"_id":8}}
{"first":"tj","last":"brodie","goals":[2,14,7],"assists":[8,42,30],"gp":[26,82,82],"born":"1990/06/07"}
{"index":{"_id":39}}
{"first":"mark","last":"giordano","goals":[6,30,15],"assists":[3,30,24],"gp":[26,60,63],"born":"1983/10/03"}
{"index":{"_id":10}}
{"first":"mikael","last":"backlund","goals":[3,15,13],"assists":[6,24,18],"gp":[26,82,82],"born":"1989/03/17"}
{"index":{"_id":11}}
{"first":"joe","last":"colborne","goals":[3,18,13],"assists":[6,20,24],"gp":[26,67,82],"born":"1990/01/30"}

从 Painless 访问 Doc Values

编辑

文档值可以通过名为 docMap 来访问。

例如,以下脚本计算球员的总进球数。此示例使用强类型的 intfor 循环。

GET hockey/_search
{
  "query": {
    "function_score": {
      "script_score": {
        "script": {
          "lang": "painless",
          "source": """
            int total = 0;
            for (int i = 0; i < doc['goals'].length; ++i) {
              total += doc['goals'][i];
            }
            return total;
          """
        }
      }
    }
  }
}

或者,你可以使用脚本字段而不是函数评分来做同样的事情

GET hockey/_search
{
  "query": {
    "match_all": {}
  },
  "script_fields": {
    "total_goals": {
      "script": {
        "lang": "painless",
        "source": """
          int total = 0;
          for (int i = 0; i < doc['goals'].length; ++i) {
            total += doc['goals'][i];
          }
          return total;
        """
      }
    }
  }
}

以下示例使用 Painless 脚本按球员的姓氏和名字的组合对球员进行排序。使用 doc['first'].valuedoc['last'].value 访问名字和姓氏。

GET hockey/_search
{
  "query": {
    "match_all": {}
  },
  "sort": {
    "_script": {
      "type": "string",
      "order": "asc",
      "script": {
        "lang": "painless",
        "source": "doc['first.keyword'].value + ' ' + doc['last.keyword'].value"
      }
    }
  }
}

缺失的键

编辑

如果文档中缺少该字段,则 doc['myfield'].value 会抛出异常。

对于更动态的索引映射,你可以考虑编写一个 catch 等式

if (!doc.containsKey('myfield') || doc['myfield'].empty) { return "unavailable" } else { return doc['myfield'].value }

缺失的值

编辑

要检查文档是否缺少值,可以调用 doc['myfield'].size() == 0

使用 Painless 更新字段

编辑

你还可以轻松地更新字段。你可以使用 ctx._source.<field-name> 访问字段的原始源。

首先,让我们通过提交以下请求来查看球员的源数据

GET hockey/_search
{
  "query": {
    "term": {
      "_id": 1
    }
  }
}

要将球员 1 的姓氏更改为 hockey,只需将 ctx._source.last 设置为新值

POST hockey/_update/1
{
  "script": {
    "lang": "painless",
    "source": "ctx._source.last = params.last",
    "params": {
      "last": "hockey"
    }
  }
}

你还可以向文档添加字段。例如,此脚本添加一个包含球员昵称 *hockey* 的新字段。

POST hockey/_update/1
{
  "script": {
    "lang": "painless",
    "source": """
      ctx._source.last = params.last;
      ctx._source.nick = params.nick
    """,
    "params": {
      "last": "gaudreau",
      "nick": "hockey"
    }
  }
}

日期

编辑

日期字段被公开为 ZonedDateTime,因此它们支持诸如 getYeargetDayOfWeek 之类的方法,或者例如使用 getMillis 获取自 epoch 以来的毫秒数。要在脚本中使用这些方法,请省略 get 前缀,并继续将方法的其余部分转换为小写。例如,以下内容返回每个曲棍球运动员的出生年份

GET hockey/_search
{
  "script_fields": {
    "birth_year": {
      "script": {
        "source": "doc.born.value.year"
      }
    }
  }
}

正则表达式

编辑

默认情况下启用正则表达式,因为设置 script.painless.regex.enabled 有一个新选项 limited,这是默认设置。这默认使用正则表达式,但限制正则表达式的复杂性。看似无害的正则表达式可能具有惊人的性能和堆栈深度行为。但它们仍然是非常强大的工具。此外,与之前的 limited 一样,该设置可以设置为 true,这将启用正则表达式而不限制它们。要自己启用它们,请在 elasticsearch.yml 中设置 script.painless.regex.enabled: true

Painless 对正则表达式的本地支持具有语法结构

  • /pattern/:模式字面量创建模式。这是在 Painless 中创建模式的唯一方法。/ 内的模式只是 Java 正则表达式。有关更多信息,请参见 模式标志
  • =~:find 运算符返回一个 boolean 值,如果文本的子序列匹配,则返回 true,否则返回 false
  • ==~:match 运算符返回一个 boolean 值,如果文本匹配,则返回 true,如果不匹配,则返回 false

使用 find 运算符 (=~) 你可以更新所有姓氏中包含 "b" 的曲棍球运动员

POST hockey/_update_by_query
{
  "script": {
    "lang": "painless",
    "source": """
      if (ctx._source.last =~ /b/) {
        ctx._source.last += "matched";
      } else {
        ctx.op = "noop";
      }
    """
  }
}

使用 match 运算符 (==~) 你可以更新所有名字以辅音开头并以元音结尾的曲棍球运动员

POST hockey/_update_by_query
{
  "script": {
    "lang": "painless",
    "source": """
      if (ctx._source.last ==~ /[^aeiou].*[aeiou]/) {
        ctx._source.last += "matched";
      } else {
        ctx.op = "noop";
      }
    """
  }
}

你可以直接使用 Pattern.matcher 获取 Matcher 实例并删除他们姓氏中的所有元音

POST hockey/_update_by_query
{
  "script": {
    "lang": "painless",
    "source": "ctx._source.last = /[aeiou]/.matcher(ctx._source.last).replaceAll('')"
  }
}

Matcher.replaceAll 只是对 Java MatcherreplaceAll 方法的调用,因此它支持 $1\1 进行替换

POST hockey/_update_by_query
{
  "script": {
    "lang": "painless",
    "source": "ctx._source.last = /n([aeiou])/.matcher(ctx._source.last).replaceAll('$1')"
  }
}

如果你需要对替换进行更多控制,可以在具有 Function<Matcher, String>CharSequence 上调用 replaceAll,该函数构建替换。这不支持 $1\1 来访问替换,因为你已经拥有对匹配器的引用,并且可以使用 m.group(1) 获取它们。

在构建替换的函数内调用 Matcher.find 是不友好的,并且很可能会破坏替换过程。

这将使曲棍球运动员姓氏中的所有元音都变为大写

POST hockey/_update_by_query
{
  "script": {
    "lang": "painless",
    "source": """
      ctx._source.last = ctx._source.last.replaceAll(/[aeiou]/, m ->
        m.group().toUpperCase(Locale.ROOT))
    """
  }
}

或者你可以使用 CharSequence.replaceFirst 将他们姓氏中的第一个元音变为大写

POST hockey/_update_by_query
{
  "script": {
    "lang": "painless",
    "source": """
      ctx._source.last = ctx._source.last.replaceFirst(/[aeiou]/, m ->
        m.group().toUpperCase(Locale.ROOT))
    """
  }
}

注意:以上所有 _update_by_query 示例确实都需要一个 query 来限制它们拉回的数据。虽然你 可以 使用一个 脚本查询,但它不如使用任何其他查询高效,因为脚本查询无法使用倒排索引来限制它们必须检查的文档。