or operation

以下内容基于 Elasticsearch 2.0.1 。

首先是 or filter 已经变成了 bool ,它是专门用来合并 queries ,支持 or、and、not 这类操作符的,大概相当于 ()(?).

当然还涉及到搜索结果评分等,但是这里不提这些。

现在遇到的情景大概就是想搜索多个空格分隔的关键字,任何一个 field 满足任一个关键字则匹配成功,返回所有匹配成功的结果和它们的 highlight 字段。

首先是对搜索内容做一点微不足道的处理:

const queries = 'some keywords'.trim().split(/\s+/).map((query) => query.lowerCase());
const queryString = queries.join(' OR ');

保持所有 query 包裹在 bool 中,把上面的两个变量用在 POST body 中,curl 同理:

{
"index": "index",
"from": 11,
"size": 20,
"_source": false,
"body": {
"query": {
"bool": {
"should": [
{
"terms": {
"tags": queries,
},
},
{
"query_string": {
"query": queryString,
"fields": ["name^5", "name.analyzed^5", "email", "phone", "content"],
"analyzer": "whitespace",
},
},
],
"minimum_should_match": 1,
}
},
"highlight": {
"fields": {
"content": { fragment_size: 18, number_of_fragments: 1 },
"name: {},
"name.analyzed": {},
"tags": {},
"email": {},
"phone": {}
}
}
}
}

note: minimum_should_match may not be available in some version of elasticsearch.

es 升级

Download

Migration

Run

从 2.0 -> 5.2 整体来说没有想象中那么困难,当然原本的环境就是单点大法…所以过程比较顺畅,有一些小坑,错误都比较明显。

es 从单机到集群

Background

Migration

先说结论,最终是没有实现 zero downtime。参考了几个不停机迁移方案以及现有的业务模型和数据规模,感觉过程实现成本和风险远远大于短暂的停机,所以选择了在凌晨重启服务加入了两个新的节点构成集群并在 data nodes 之间同步数据,修改配置和加入集群期间服务不可用大概一分钟多,新的 data node 同步百G级数据完成大概用了4个小时。这一分钟左右没有出现更新操作(凌晨原因),新增操作在后续通过脚本做了补充。

关于不停机迁移的方法也看了几个现成的方法诸如

https://blog.engineering.ticketbis.com/elasticsearch-cluster-migration-without-downtime/
https://thoughts.t37.net/migrating-a-130tb-cluster-from-elasticsearch-2-to-5-in-20-hours-with-0-downtime-and-a-rollback-39b4b4f29119

多半离不开版本号和更新时间这两个字段,迁移过程区分迁移前的数据和迁移过程中新增的数据,在迁移完毕时把新数据再同步一次,这就要求数据只有 create 没有 update 操作在迁移过程中发生,感觉不太适合除了自增类型的例如日志以外的其他业务逻辑。

关于集群,需要至少3个 master eligible node 以防止脑裂,并把其中两个设置为 data node 承载数据,在磁盘的扩充过程中给每个节点挂载数据盘,方便日后扩容。

以其中一个节点为例,几个比较重要的配置是

discovery.zen.ping.unicast.hosts: ["10.66.90.77:9300", "10.66.91.150:9300", "10.30.55.37:9300"]
network.host: ["10.30.55.37", _local_]

作为有限且小规模的集群,为 discovery module 指定广播发现节点的私有地址列表,同时允许各个节点在本机的访问调试.

Summary

总之如果不是追求绝对的不停机,并且不愿意增加额外的字段或改变自己的逻辑,es提供的加入集群方式来做迁移数据在我看来是相当简便又稳定的一种方式。

后续应该会在众多开源产品中选择一种 monitor 让集群和服务的状态更透明、报警和响应更及时。

script language

从 2.0.1 升级到 5.1.1 有一阵子了,今天发现了一个存在比较久的问题,就是诸如 update 包括 bulk update 操作,不能被正常的执行。问题集中在那些在 body 中使用 script 的 query,而直接全文更新的则没有问题。例如,对 list 类型的 document 进行局部更新:

POST index/type/id/_update
{
"script": "ctx._source.tags -= tag",
"params" : {
"tag" : "blue"
}
}

这样的使用在 2.0.1 中是没问题的,然而在 5.x 中却报错:Variable [tag] is not defined. 无法执行这个 script,后来发现了 default lang 不再是 groovy 而变成了 painless ,而 painless 的取值需要携带 key ,即 params.tag 这样才可以正常找到值。那么 groovy 为什么不再是 default 还被新版本中标记为 deprecated 了呢……要知道 groovy 之前替换掉 mvel 的理由是足够快而且简单……

先说一下什么是 sandboxed language

沙盒是在受限的安全环境中运行应用程序的一种做法,这种做法是要限制授予应用程序的代码访问权限。

像 groovy 和 JavaScript 这类脚本语言它们本身都不是 sandboxed,它们可以做很多系统级别的不止是读写、网络请求的操作,这样就给基于 JAVA 并且在运行中默认开启 The Security Manager 的 elasticsearch 带来很大的安全隐患,比如在脚本中随便加一句 infinite loop ,服务器可能就表现成拒绝访问的状态,所以在之前的版本中 elasticsearch 为 groovy 加入了沙盒控制一些权限,然而后面由于权限限制不够,还是出现了一些问题23333。

虽然 5.x 仍然内置 groovy ,但是考虑到 elasticsearch script 的来源,可以是 inline、store 、还有 file,前两个就不说了,一个是 query 中直接写进去的像我上面的例子,另一个也是以数据的形式存在某个 cluster state 的 _script 节点下,而 file 的形式是配置在 elasticsearch 的 config 文件夹中,所以从安全的角度,elasticsearch 5.x 只对 file script 默认允许执行 groovy 。

至于开头的 query ,如果不考虑安全性,例如默认我们的 elasticsearch 运行与一个相对隔离的环境下,如果还想用 inline groovy ,就可以为 groovy 单独开启一个配置 script.engine.groovy.inline: true 或者更宽泛的,针对所有 inline script 的配置script.inline: true,那么为上面的 script 声明一下 lang 就可以成功执行了(在 Python 和 Node.js 包中拼 dict 和 object 也是一样):

POST index/type/id/_update
{
"script" :{
"inline": "ctx._source.tags -= tag",
"lang": "groovy",
"params" : {
"tag" : "blue"
}
}
}

等 painless 相对稳定了,直接切换过去就可以了,毕竟语法都类似,而且还安全。

upsert element to exist document

一个已经存在的 document 可能有一个 tags 的 element ,它是一个 Array 形态,现在我们想 upsert 某个 tag 进去。

这种 array 的操作通常是用 script 操作的,于是很直观地用到文档中的 upsert:

{
"script": {
"inline": "ctx._source.tags += tag",
"lang": "groovy",
"params": {
"tag": "皮皮虾"
}
},
"upsert": {
"tags": ["皮皮虾"]
}
}

然而 script 总是执行而 upsert 不执行,原因是 document 已经存在了,这个 upsert 只是针对当 document 不存在时,所以还是要把逻辑做在 script 中:

{
"script": {
"inline": "if (ctx._source.tags) {ctx._source.tags += tag;} else {ctx._source.tags = [tag]}",
"lang": "groovy",
"params": {
"tag": "皮皮虾"
}
}
}