微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

通过 ElasticSearch 轻松循环记录源数组 您的直觉是对的 - 如果您想检查所有数组列表对象,您需要使用 for 循环继续,重要的是要提到嵌套对象在内部表示为单独的子文档但是,仍然有一种类型的查询支持迭代 _source — 输入 function_score 查询

如何解决通过 ElasticSearch 轻松循环记录源数组 您的直觉是对的 - 如果您想检查所有数组列表对象,您需要使用 for 循环继续,重要的是要提到嵌套对象在内部表示为单独的子文档但是,仍然有一种类型的查询支持迭代 _source — 输入 function_score 查询

我有以下用于网上商店产品的 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_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",}
        }
      }
    }
  }
}

我创建了一个 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

只要其中一个可配置的孩子满足销售产品的条件,就会返回产品。这是不正确的,因为我需要遍历整个集合 op configure_children 以确定它是否是销售产品。我怎样才能确保所有孩子都被计算在内?带循环?


这是 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 时,它们的内容 will be flattened 以及各个键/值对之间的关​​系将丢失。因此,您绝对应该像这样调整映射:

{
  "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. inner_hits attributenested query 允许您让 Elasticsearch 知道您只对那些真正匹配的孩子感兴趣。否则,将返回所有 configurable_children。在 _source parameter 中指定时,将跳过原始的完整 JSON 文档源,仅返回命名的 inner_hits
  2. 由于 ES 的分布式特性,不建议使用 java 的 new Date()。我已经向 my answer 解释了它背后的原因。 您会看到我在此答案底部的查询中使用了参数化的 now

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

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

为了缓解这种情况,习惯上定期保持嵌套的子级同步,这样当您将对象的一个​​属性展平以在顶层使用时,您可以使用简单的迭代相应的 doc 值。这种扁平化通常是通过我在 How to get current time as unix timestamp for script usemy answer

中说明的 copy_to 功能完成的

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

到目前为止一切顺利,但您仍然缺少一种基于 special_dates 进行过滤的方法。

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

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

如您的问题所述,您

..需要遍历整个 configurable_children 集以确定它是否是销售产品..

话虽如此,下面是我的查询是如何工作的:

  1. How to iterate through a nested array in elasticsearch with filter script? 通常会生成自定义计算的分数,但它可以在 min_score 的帮助下用作布尔值是/否过滤器,以排除 configurable_children满足一定条件。
  2. configurable_children 被迭代时,每个循环都会将一个布尔值附加到 childrenAreMatching,然后将其传递给 allEntriesAreTrue 助手,如果它们是,则返回 1,否则返回 0 .
  3. 解析日期并与参数化的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
    }
  }
}

总而言之,只有那些 all configurable_children 满足指定条件的文档才会被返回。


附言如果您从这个答案中学到了一些东西并想了解更多信息,我在 function_score query 中专门用了一整章介绍 ES 脚本。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。