循环通过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

一旦其中一个可配置儿童满足销售产品的标准,就会返回产品。这是不正确的,因为我需要循环整个可配置儿童集,以确定它是否是销售产品。我如何才能确保所有的孩子都被纳入计算?用一个环?


这是Joe在回答中建议的新查询:

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
}
}
}
}
]
}
}
}
}
}

这里需要注意两件事:

  1. nested查询的inner_hits属性允许您让Elasticsearch知道您只对那些真正匹配的孩子感兴趣。否则,将返回所有configurable_children。当在_source参数中指定时,将跳过原始的完整JSON文档源,并且只返回命名的inner_hits
  2. 由于ES的分布式性质,不建议使用java的new Date()。我已经解释了它背后的原因——我对如何将当前时间作为脚本使用的unix时间戳的回答您会看到我在这个答案底部的查询中使用了一个参数化的now

接下来,重要的是要提到嵌套对象在内部表示为单独的子文档

这个事实的一个副作用是,一旦进入nested查询的上下文,就无法访问同一文档的其他嵌套子级。

为了缓解这种情况,通常会定期保持嵌套的子级同步,这样当您将对象的一个属性展平以在顶层使用时,您可以使用简单的迭代相应的文档值。这种扁平化通常是通过copy_to功能来完成的,我在回答"如何使用过滤器脚本在弹性搜索中迭代嵌套数组?

在您的特定用例中,这意味着,例如,您将在字段stock.is_in_stock上使用copy_to,这将产生一个顶级布尔数组列表,该列表比对象数组列表更容易使用。

到目前为止还不错,但您仍然缺少一种基于special_dates进行筛选的方法

现在,无论您处理的是nested字段类型还是常规object字段类型,自v6.4以来,在常规脚本查询中访问params._source在ES中都不起作用。

然而,仍然有一种类型的查询支持迭代_source——输入function_score查询

正如你的问题所述,你的

。。需要循环查看整套configurable_children,以确定它是否是销售产品。。

话虽如此,以下是我的查询工作原理:

  1. function_score查询通常会生成自定义计算的分数,但在min_score的帮助下,它可以用作布尔型是/否过滤器,以排除configurable_children不满足特定条件的文档
  2. configurable_children被迭代时,每个循环将一个布尔值附加到childrenAreMatching上,然后将其传递到allEntriesAreTrue帮助器上,如果它们是,则返回1,如果不是,则返回0
  3. 对日期进行解析,并将其与参数化的CCD_ 30进行比较;对CCD_ 31也进行了比较。如果在任何时候,某个条件失败,则循环跳到下一次迭代
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
}
}
}

总之,只有Allconfigurable_children满足指定条件的文档才会被返回。


p.S.如果你从这个答案中学到了一些东西,并且想了解更多,我在我的Elasticsearch手册中专门用了一整章来介绍ES脚本。

最新更新