loops - 轻松循环 ElasticSearch 文档源数组
问题描述
对于网上商店中的产品,我有以下 ElasticSearch 数据结构:
{
"_index": "vue_storefront_catalog_1_product_1617378559",
"_type": "_doc",
"_source": {
"configurable_children": [
{
"price": 49.99,
"special_price": 34.99,
"special_from_date": "2020-11-27 00:00:00",
"special_to_date": "2020-11-30 23:59:59",
"stock": {
"qty": 0,
"is_in_stock": false,
"stock_status": 0
}
}
{
"price": 49.99,
"special_price": null,
"special_from_date": null,
"special_to_date": null,
"stock": {
"qty": 0,
"is_in_stock": false,
"stock_status": 0
}
}
]
}
使用以下映射:
{
"vue_storefront_catalog_1_product_1614928276" : {
"mappings" : {
"properties" : {
"configurable_children" : {
"properties" : {
"price" : {
"type" : "double"
},
"special_from_date" : {
"type" : "date",
"format" : "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
},
"special_price" : {
"type" : "double"
},
"special_to_date" : {
"type" : "date",
"format" : "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
},
}
}
}
}
}
}
我创建了一个 Elasticsearch 查询以仅过滤掉正在销售的产品,这意味着:special_price 必须低于价格,并且当前日期必须在 special_from_date 和 special_to_date 之间。
这是我创建的无痛脚本:
boolean hasSale = false;
long timestampNow = new Date().getTime();
if (doc.containsKey('configurable_children.special_from_date') && !doc['configurable_children.special_from_date'].empty) {
long timestampSpecialFromDate = doc['configurable_children.special_from_date'].value.toInstant().toEpochMilli();
if (timestampSpecialFromDate > timestampNow) {
hasSale = false;
}
} else if (doc.containsKey('configurable_children.special_to_date') && !doc['configurable_children.special_to_date'].empty) {
long timestampSpecialToDate = doc['configurable_children.special_to_date'].value.toInstant().toEpochMilli();
if (timestampSpecialToDate < timestampNow) {
hasSale = false;
}
} else if (doc.containsKey('configurable_children.stock.is_in_stock') && doc['configurable_children.stock.is_in_stock'].value == false) {
hasSale = false;
} else if (1 - (doc['configurable_children.special_price'].value / doc['configurable_children.price'].value) > params.fraction) {
hasSale = true;
}
return hasSale
只要其中一个可配置的_children 满足成为销售产品的标准,这就会返回产品。这是不正确的,因为我需要遍历整个 opconfigurable_children 来确定它是否是销售产品。我怎样才能确保所有的孩子都被纳入计算?带循环?
这是乔在答案中建议的新查询:
GET vue_storefront_catalog_1_product/_search
{
"query": {
"function_score": {
"query": {
"match_all": {}
},
"functions": [
{
"script_score": {
"script": {
"source": """
int allEntriesAreTrue(def arrayList) {
return arrayList.stream().allMatch(Boolean::valueOf) == true ? 1 : 0
}
ArrayList childrenAreMatching = [];
long timestampNow = params.timestampNow;
ArrayList children = params._source['configurable_children'];
if (children == null || children.size() == 0) {
return allEntriesAreTrue(childrenAreMatching);
}
for (config in children) {
if (!config.containsKey('stock')) {
childrenAreMatching.add(false);
continue;
} else if (!config['stock']['is_in_stock']
|| config['special_price'] == null
|| config['special_from_date'] == null
|| config['special_to_date'] == null) {
childrenAreMatching.add(false);
continue;
}
if (config['special_from_date'] != null && config['special_to_date'] != null) {
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
def from_millis = sf.parse(config['special_from_date']).getTime();
def to_millis = sf.parse(config['special_to_date']).getTime();
if (!(timestampNow >= from_millis && timestampNow <= to_millis)) {
childrenAreMatching.add(false);
continue;
}
}
def sale_fraction = 1 - (config['special_price'] / config['price']);
if (sale_fraction <= params.fraction) {
childrenAreMatching.add(false);
continue;
}
childrenAreMatching.add(true);
}
return allEntriesAreTrue(childrenAreMatching);
""",
"params": {
"timestampNow": 1617393889567,
"fraction": 0.1
}
}
}
}
],
"min_score": 1
}
}
}
响应如下:
{
"took" : 15155,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2936,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [... hits here ...]
}
}
知道为什么查询需要大约 15 秒吗?
解决方案
for
你的直觉是对的——如果你想检查所有的数组列表对象,你需要使用一个循环。
现在,在我进入迭代方面之前,有一件关于 Elasticsearch 中的数组的重要知识。当它们未定义为nested
时,它们的内容将被展平,并且各个键/值对之间的关系将丢失。因此,您绝对应该像这样调整映射:
{
"vue_storefront_catalog_1_product_1614928276" : {
"mappings" : {
"properties" : {
"configurable_children" : {
"type": "nested", <---
"properties" : {
"price" : {
"type" : "double"
},
...
}
}
}
}
}
}
并重新索引您的数据,以确保将configurable_children
它们视为单独的独立实体。
一旦它们被映射为nested
,您将能够仅检索那些与您的脚本条件匹配的孩子:
POST vue_storefront_catalog_1_product_1614928276/_search
{
"_source": "configurable_children_that_match",
"query": {
"nested": {
"path": "configurable_children",
"inner_hits": {
"name": "configurable_children_that_match"
},
"query": {
"bool": {
"must": [
{
"script": {
"script": {
"source": """
boolean hasSale = false;
long timestampNow = new Date().getTime();
if (doc.containsKey('configurable_children.special_from_date') && !doc['configurable_children.special_from_date'].empty) {
long timestampSpecialFromDate = doc['configurable_children.special_from_date'].value.toInstant().toEpochMilli();
if (timestampSpecialFromDate > timestampNow) {
return false
}
}
if (doc.containsKey('configurable_children.special_to_date') && !doc['configurable_children.special_to_date'].empty) {
long timestampSpecialToDate = doc['configurable_children.special_to_date'].value.toInstant().toEpochMilli();
if (timestampSpecialToDate < timestampNow) {
return false
}
}
if (doc.containsKey('configurable_children.stock.is_in_stock') && doc['configurable_children.stock.is_in_stock'].value == false) {
return false
}
if (1 - (doc['configurable_children.special_price'].value / doc['configurable_children.price'].value) > params.fraction) {
hasSale = true;
}
return hasSale
""",
"params": {
"fraction": 0.1
}
}
}
}
]
}
}
}
}
}
这里需要注意两点:
- 查询的
inner_hits
属性允许您让 Elasticsearch 知道您只对那些真正匹配的子项感兴趣。否则,将全部退回。在参数中指定时,将跳过原始的完整 JSON 文档源,仅返回命名的。nested
configurable_children
_source
inner_hits
- 由于 ES 的分布式特性,不推荐使用 java 的
new Date()
. 我已经解释了它背后的原因,我对How to get current time as unix timestamp for script use的回答。您会看到我在此答案底部的查询中使用了参数化。now
继续前进,重要的是要提到嵌套对象在内部表示为单独的子文档。
这一事实的一个副作用是,一旦您进入nested
查询的上下文中,您就无法访问同一文档的其他嵌套子项。
为了缓解这种情况,习惯上定期保持嵌套子项同步,这样当您将对象的属性之一展平以在顶层使用时,您可以使用简单地迭代相应的文档值。这种展平通常是通过我在如何使用过滤器脚本在弹性搜索中迭代嵌套数组的答案中copy_to
说明的功能来完成的?
例如,在您的特定用例中,这意味着您将copy_to
在字段上使用该字段,该字段stock.is_in_stock
会产生一个比对象数组列表更易于使用的顶级布尔数组列表。
到目前为止一切顺利,但您仍然会缺少一种基于special_dates
.
现在,无论您处理的是常规字段类型nested
还是常规object
字段类型,params._source
在常规脚本查询中访问都不适用于 ES,因为v6.4
.
但是,仍然有一种类型的查询确实支持迭代_source
- 输入function_score
查询。
如您的问题所述,您
..需要遍历整套
configurable_children
以确定它是否是销售产品..
话虽如此,下面是我的查询的工作方式:
- 该
function_score
查询通常会生成一个自定义计算分数,但它可以在 的帮助下min_score
用作布尔是/否过滤器以排除configurable_children
不满足特定条件的文档。 - 在
configurable_children
迭代过程中,每个循环都会附加一个布尔值childrenAreMatching
,然后将其传递给allEntriesAreTrue
辅助函数,如果它们是则返回 1,否则返回 0。 - 解析日期并与参数化的日期进行比较
now
;也是fraction
比较的。如果在任何时候某个条件失败,则循环跳转到下一次迭代。
POST vue_storefront_catalog_1_product_1614928276/_search
{
"query": {
"function_score": {
"query": {
"match_all": {}
},
"functions": [
{
"script_score": {
"script": {
"source": """
// casting helper
int allEntriesAreTrue(def arrayList) {
return arrayList.stream().allMatch(Boolean::valueOf) == true ? 1 : 0
}
ArrayList childrenAreMatching = [];
long timestampNow = params.timestampNow;
ArrayList children = params._source['configurable_children'];
if (children == null || children.size() == 0) {
return allEntriesAreTrue(childrenAreMatching);
}
for (config in children) {
if (!config['stock']['is_in_stock']
|| config['special_price'] == null
|| config['special_from_date'] == null
|| config['special_to_date'] == null) {
// nothing to do here...
childrenAreMatching.add(false);
continue;
}
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
def from_millis = sf.parse(config['special_from_date']).getTime();
def to_millis = sf.parse(config['special_to_date']).getTime();
if (!(timestampNow >= from_millis && timestampNow <= to_millis)) {
// not in date range
childrenAreMatching.add(false);
continue;
}
def sale_fraction = 1 - (config['special_price'] / config['price']);
if (sale_fraction <= params.fraction) {
// fraction condition not met
childrenAreMatching.add(false);
continue;
}
childrenAreMatching.add(true);
}
// need to return a number because it's a script score query
return allEntriesAreTrue(childrenAreMatching);
""",
"params": {
"timestampNow": 1617393889567,
"fraction": 0.1
}
}
}
}
],
"min_score": 1
}
}
}
总而言之,只有那些都 configurable_children
满足指定条件的文件才会被退回。
PS如果您从这个答案中学到了一些东西并想了解更多信息,我在我的Elasticsearch Handbook中专门用了一章来介绍 ES 脚本。
推荐阅读
- django - 对于单个元素的查询集,索引 a (query_set[0]) 的第 0 个元素和 query_set.first() 给出不同的值 - Django 2.2.7
- php - PHP 没有加载扩展
- swift - 将 utf8 字符串转换为表情符号,Swift
- blockchain - 基材可以用于非货币应用吗?
- android - 如何从 Android 上打开的活动中删除 KEEP_SCREEN_ON 标志
- java - 在 Selenium Java 中使用数据测试 id 查找元素
- javascript - React Native 在选项卡导航器的子项中添加第二个选项卡导航器
- flutter - 如何在 Flutter 中模拟一个块,并发出状态以响应来自被测小部件的事件
- mysql - 如何在一行中比较同一商品的 2 个最新价格?
- python-3.x - 通过 Pillow 模块提取 exif 数据时的 Python 错误:无效的继续字节