最近公司的 APP 使用 ES 搜索功能时遇到一个需求 —— 需要搜索出来的数据中只包含某个商户下的商品,且这些商品的库存都不为 0。

首先我们搜索得到的文档格式简化后如下:


也就是说,此时,我的需求的搜索条件是:merchant_id=11,且 stock 不为 0。如果按照需求来说,上面截图里的这个商品是不符合条件的,也就是不应该被搜索出来。但是如果按照 es 一般的 filter 写法去写,filter 部分的写法应该是如下 ——

"filter":[
    { "terms":
            {"sku_list.merchant_id":[11]}
    },
    {"range":
            {"sku_list.stock":{"gte":1}}
    }
]
但这种写法会导致那个商品依然会被搜出来,原因是 elasticsearch(lucene)使用的库没有内部对象的概念,因此内部对象被扁平化为一个简单的字段名称和值列表。也就是说,上面的商品文档,在 es 中会被转换为

{
"id": [ **** ],
"name": [ ****],
"title": [ **** ],
"post": [ **** ] ,
"sku_list.sku_id": [ 100, 101, 102],
"sku_list.stock": [ 2,0,3 ],
"sku_list.merchant_id": [ 10, 11, 12 ],
"sku_list.sale_price": [ 128.00 ]
}
所以 sku_list 里的 stock 跟 merchant_id 不再具有关联关系,因为整合后的对象满足了上面两个条件,所以可以被搜索出来。
要解决这个问题,我们需要对 es 的映射(mapping)进行一些小改动,将 sku_list 的 type 改为 nested(嵌套数据类型,引用其他地方的一个说法:在内部,嵌套对象将数组中的每个对象索引为单独的隐藏文档,这意味着可以独立于其他对象查询每个嵌套对象)。nested 类型是对象数据类型的专用版本,它允许对象数组以可以彼此独立查询的方式进行索引。


那么如何将 sku_list 改为 nested 类型,又尽可能不影响线上已有的功能呢?
首先,可以查看当前索引下的所有映射以及映射类型。

curl -X GET "http://domain/my_index/_mapping"(domain是es所在服务器ip+端口,my_index换成对应的索引名称)
查看后发现 sku_list 对应的索引类型是 text。但是 es 不允许直接修改或删除一个字段类型,所以通用的修改字段类型的解决办法是 ——

采用 reindex 的方法实现,就是创建一个新的 mapping,里面的字段类型按照新的类型定义,然后使用 reindex 的方法把原来的数据拷贝到新的 index 下面

1、创建新的索引 my_index_new
curl -X PUT "http://domain/my_index_new?pretty"
2、将索引的默认字段数调大(默认是 1000,一般不需要调,但因为我们的文档比较复杂,字段数使用超过了 1000,所以必须调整)
curl -X PUT "http://domain/my_index_new/_settings" -d '{"index.mapping.total_fields.limit": "3000"}'
3、将 sku_list 的字段类型设置为 nested(注意这一步一定要再下一步同步数据前完成,不然同步完数据后,sku_list 的字段类型又会被设置成了默认的 text,就又无法再更改了)
curl -X POST "http://domain/my_index_new/_mapping" -d '{"properties": {"sku_list": {"type":"nested"}}}'  
4、将老数据同步到新数据
curl -X POST "http://domain/_reindex" -d '{ "source": {    "index": "my_index"  },  "dest": {    "index": "my_index_new"  }} '
如果原先数据量较大,第三步花的时间会比较长,需要耐心等待其同步完成。

5、删除原先索引(其实如果业务允许的话,可以直接在业务侧将索引改为 my_index_new,老的索引就不删除,放着以防后面出现问题可以及时切换)
curl -X DELETE "http://domain/my_index"
6、设置原索引的别名(如果业务侧不方便改 es 接口处的索引,那只能通过 4、5 这种方法来确保原索引名正常使用,我们是直接用新的索引名称,老的不去动它)
curl -X POST "http://domain/_aliases" -d '{ "actions": [{"add":{"index":"my_index_new","alias":"my_index"}}]} '
如果上述步骤都正常完成,此时再查看索引的 mapping,会发现 sku_list 已经是我们需要的 nested 类型了。

此时就可以使用 nested 的语法结构来构造查询语句。

{"query":{"bool":{"must":[{"dis_max":{"queries":[]}},{"nested":{"path":"sku_list","query":{"bool":{"must":[{"terms":{"sku_list.merchant_id":[11]}},{"range":{"sku_list.stock":{"gte":1}}}]}}}}],"filter":[]}}}
只展示了 nested 部分的结构,其他的可以根据实际情况替换。其中,nested 里的 path 就是我们修改了类型的字段,另外就是注意 nested 在 filter 里使用貌似会报错,所以需要写在 must 查询条件里。

另外可以通过浏览器直接访问 domain/_cat/indices?v,看到新的索引的信息,确认新索引的文档内存大小是否跟老的一致。

————————————————