Elasticsearch:汇总数据表

在进行大数据分析时,我们会经常使用数据表格来展示数据。数据表格可以用来展示原始的数据。这是数据可以来自于 source。在很多的时候,我们更希望这些数据是一些聚合的数据表格,比如如下的数据表格:

在 Kibana 中,我们很容易通过可视化工具生成我们所需要的表格。我们可以参考我之前的文章 “在 Kibana 中的四种表格制作方式”。在今天的文章中,我将介绍如何使用 Elasticsearch API 通过搜索的方法生成我们想要的数据,并可以在自己的应用中进行可视化。

 

简单的表格

在我之前的文章 “在 Kibana 中的四种表格制作方式” 里,针对第一种情况,当我们创建表格时实际上是显示了文档的某些字段而已。分页请求对于构建简单的表格,列表,网格非常有用。

为了保持以需求为导向,你应该旨在将响应 hits 的内容限制为仅 “实际上” 需要填充这些组件的那些属性。 这样,你可以通过 includes 参数指定应返回哪些字段。 类似地,你可以通过 “excludes” 参数来控制哪些内容可以跳过。 也支持通配符:

POST index_name/_search
{
  "size": 24,
  "from": 24,
  "query": { ... },
  "_source": {
    "includes": ["id", "price", "photos"],
    "excludes": ["irrelevant_field", "internal_*"]
  }
}

然后,返回的 hits 将用作表行,其属性作为每个列。在我之前的文章 “Elasticsearch:从搜索中获取选定的字段” 有详细的描述。我们也可以使用 fields 来实现:

POST index_name/_search
{
  "size": 24,
  "from": 24,
  "query": { ... },
  "fields": ["id", "price", "photos"],
  "_source": false
}

目前在最新的 Kibana 的 Discover 的设计中,里面的数据展示都是使用  fields 方法来获取字段的值。

我们还可以对属性的值进行后处理,以便无需进行任何进一步的操作即可将其呈现在前端(例如,日期格式,应用折扣或将布尔值转换为表情符号),比如:

POST my_index/_search
{
  ...
  "script_fields": {
    "emoji_booleans": {
      "script": {
        "source": "doc['boolean_field'].value == true ? \"✅\" : \"❌\"" 
      }
    }
  }
} 

在上面,我们通过 doc values 对布尔值进一步进行处理。通过这样的处理,我们可以直接在客户端进行展示。有关 script fields 的更多介绍,请参阅我之前的文章 “Elasticsearch:Script fields 及其调试”。

 

更为高级的报表

在上面,我们只是使用简单的针对 source 或者针对 source 进行简单处理从而得出的报表。在实际的使用中,我们更希望能够提炼出更为使用者容易理解的洞察数据,比如对数据的聚合。让我们想象一下一个典型的电子商务情况-分页且分面的电子商店运行后,管理层将要求:

  • 总和,
  • 平均值和中位数,
  • 点击率和转化率,
  • 和其他汇总指标。

如果你已经阅读了我之前的文章  “在 Kibana 中的四种表格制作方式”,你可以发现在使用 table 可视化,TSVB 以及 Lens 来进行数据表格的展示时,它们里面都使用了 aggretation。那么在我们时间的应用开发中,我们该如何使用 Elasticsearch API 来实现这个呢?

 

使用场景 - 价格范围指标

管理层想知道

  • 在 $0 < x ≤ $25,$25 < x ≤ $100 和 $ > 100 范围内售出了多少产品
  • 并在每个类别中 “产生了多少收入”

鉴于你的文档结构如下:

POST products/_doc
{
  "category": "phone_case",
  "price": 20,
  "status": "sold"
}

POST products/_doc
{
  "category": "powerbank",
  "price": 99,
  "status": "sold"
}

针对上面的聚合,我们很容易联系到 filter 聚合。说起来,这也是当初我在 Elasticsearch  认证工程师中的考题之一呢 :)

所需的表应如下所示:

显然,在上面,我们可以通过 range filter 聚合,并针对每个 filter 聚合进行子聚合,并得出它们的指标。

 

解决方案

我们使用如下的方案:

POST products/_search
{
  "size": 0,
  "aggs": {
    "by_range": {
      "filter": {
        "term": {
          "status": "sold"
        }
      },
      "aggs": {
        "1.row|0_to_25": {
          "filter": {
            "range": {
              "price": {
                "gt": 0,
                "lte": 25
              }
            }
          },
          "aggs": {
            "1.sold_count": {
              "cardinality": {
                "field": "_id"
              }
            },
            "2.revenue": {
              "sum": {
                "field": "price"
              }
            }
          }
        },
        "2.row|26_to_100": {
          ... // same inner aggs as above
        },
        "3.row|101+": {
          ... // same inner aggs as above
        }
      }
    }
  }
} 

在上面,我们首先针对所有的文档过滤 sold,也就是卖出的产品。然后,使用 range filter 把每个价钱范围里的文档进行统计。 接着我们再进行子聚合得出总的价钱(sum)和已经卖出的个数(cardinality)。

然后,可以很容易地对响应进行后处理以形成表格,因为从 “n.row | xyz” 中提取了行名,并从各个子聚合中提取了列名。

如果大家对这个还不是很熟的话,那么请使用 Kibana 来创建一个表格:

在上面,我使用了 Kibana 自带的 kibana_sample_data_ecommerce 索引来做展示。在上面,我们创建了一个 table 可视化。我们想了解它是如何工作的,我们可以点击上面的 Inspect:

点击上面的 View: Data,

在上面,我们可以清楚地看到它是如何实现的。它在查询时使用的语句和我们的稍有不同,但是整体架构还是基本一样的。

 

更为简洁的选择

上述方法需要复制粘贴单元格级别的子聚合定义,对于某些人而言,至少在 [DRY 原则上],这是一种不合常规的方式。

可以通过分别声明 filters 以使它们共享相同的子聚合来跳过复制粘贴。 用于此巧妙技巧的聚合工具称为 filters。上面的查询可以重新修改为:

POST products/_search
{
  "size": 0,
  "aggs": {
    "by_range": {
      "filter": {
        "term": {
          "status": "sold"
        }
      },
      "aggs": {
        "price_ranges": {
          "filters": {
            "filters": {
              "1.row|0_to_25": {
                "range": {
                  "price": {
                    "gt": 0,
                    "lte": 25
                  }
                }
              },
              "2.row|26_to_100": {
                "range": {
                  ...
                }
              },
              "3.row|101+": {
                "range": {
                  ...
                }
              }
            }
          },
          "aggs": {
            "1.sold_count": {
              "cardinality": {
                "field": "_id"
              }
            },
            "2.revenue": {
              "sum": {
                "field": "price"
              }
            }
          }
        }
      }
    }
  }
}

上面的搜索结果与一堆独立的过滤器聚合值相同。在本例子中,我使用了简单的 filter 来进行桶聚合,在实际的使用中,你可以依据自己的情况选择合适的桶聚合。请参阅我的另外一篇文章 “Elasticsearch:透彻理解 Elasticsearch 中的 Bucket aggregation”。我们可以使用同样的技巧针对数据做不同的指标聚合。

已标记关键词 清除标记
相关推荐