特徴量エンジンKaskadaリファレンス~クエリ

# はじめに

先日、以下の記事を発表しました。

https://qiita.com/yoshiyuki_kono/items/7dc7bdad5e02ff986f5d

新しいテクノロジーに触れる時はいつでも楽しいものです。

まだまだ勉強中ですが、公式ドキュメントの学習過程の記録として、以下の記事をまとめてみました。

本稿情報のソースとして、下記ドキュメントを参照ください。

https://kaskada.io/docs-site/kaskada/main/developing/queries.html

# Kaskadaクエリ概要
Kaskada クエリは、イベントに対する計算を簡単に記述できるように設計された Fenl と呼ばれる言語で作成されます。
以下は、クエリ言語Fenlの機能と構文の概要です。

## レコード

すべてのクエリはレコードを返します。レコードによって、1 つ以上の値を 1 つの行にグループ化できます。

また、`{key: value, key2: value2}`という構文を使用してレコードを作成できます。

## テーブルの内容の表示とフィルタリング

Kaskada クエリは、単純な式を構成することによって構築されます。すべての式はタイムラインを返します。

```javascript
Purchase | when(Purchase.amount > 10)
```
この例では、式`Purchase`(すべての購入イベントのタイムライン) から始めて、 `when()`を使用してフィルタリングしています。その結果、　`Purchase`の`amount`が 10 を超える購入イベントのタイムラインが作成されます。

## ステートフルな集約
イベントを集約して、任意の時点での値を観察できる連続タイムラインを生成することができます。
```javascript
{ max_verified_review_to_date: Review.stars | when(Review.verified) | max() }
```



この例では、まず`Review`イベントのタイムラインをフィルタリングして検証済み(`verified`)のレビューのみを含め、次に`max()`集計を使用してフィルタリングされた結果を集計します。結果として得られるタイムラインには、各時点での評価(`stars`)の最大数が示されます。

## 自動結合
すべての式はエンティティに関連付けられているため、テーブルと式を自動的に結合できます。これにより、Kaskadaではエンティティ同士の結合のための冗長な定型コード（SQLにおける結合条件）を排除することができます。

```javascript
{ purchases_per_page_view: count(Purchase) / count(Pageview) }
```
ここでは、集計関数`count()`を使用して、各時点までの購入数を同じ時点までのページビュー数で割っています。その結果、各ユーザーのページビューごとの購入が時間の経過とともにどのように変化するかを示すタイムラインが作成されます。`Purchase`と`Pageview`テーブルは両方とも同じエンティティ（ユーザー）のデータを表しているため、そのまま簡単に組み合わせることができます。

## イベントベースのウィンドウ処理
時間の経過とともにイベントを収集し、他のイベントと比較して集計することができます。順序付けられた集計により、時間的な相互作用を簡単に説明できます。

```javascript
{
  pageviews_since_last_purchase:  count(Pageview, window=since(Purchase)),
  spend_since_last_review:        count(Purchase, window=since(Review)),
}
```

デフォルトでは、集計はタイムラインの最初から適用されますが、ここでは、`since()` ウィンドウ関数を使用して、購入またはレビューがそれぞれ発生するたびに集計がリセットされるように設定しています。

## パイプライン化された操作
パイプ構文を使用すると、複数の操作を連鎖させることができます。この構文により、行いたい操作の発想したのと同じ、自然な順序で記述することができます。

```javascript
{
  largest_spend_over_2_purchases: purchase.amount
  | when(Purchase.category == "food")
  | sum(window=sliding(2, Purchase.category == "food")) # Inner aggregation
  | max()                                               # Outer aggregation
}
```

### 行ジェネレーター

グループ化された集計とは異なり、 ここで使われている`daily()`のようなティック ジェネレーターなどはタイムライン上にイベントとしてにデータがない場合でも行を生成します。

そのため、イベントデータを時系列データにピボットすることができます。

```javascript
{
  signups_per_hour: count(Signups, window=since(daily()))
  | when(daily())
  | mean()
}
```

### 連続表現

タイムラインは、「離散(discrete)」 (瞬間的な値またはイベント) または「連続(continuous)」 (ステートフルな「集計」によって生成された値) のいずれかです。連続タイムライン、任意の時点での集計の値、を使用して、イベントソースから計算された「集計」を表現することができます。

```javascript
let product_average = Review.stars
| with_key(Review.product_id)
| mean()

in { average_product_review: product_average | lookup(Purchase.product_id)) }
```

この例では、この`lookup()`関数は、別のエンティティに対して、計算された値を観察するために使用されます。

ここでは、購入された製品（別のエンティティ）を「ルックアップ」し、平均レビューの値を観察します。

## ネイティブタイムトラベル

値を時間内で前方に (後方ではなく) シフトすると、時間的リークのリスクを冒さずに、さまざまな時間的コンテキストを組み合わせることができます。値をシフトすると、「現在」の値と過去の値を簡単に比較できます。
```javascript
let purchases_now = count(Purchase)
let purchases_yesterday =
   purchases_now | shift_by(days(1))

in { purchases_in_last_day: purchases_now - purchases_yesterday }
```

変数 `product_average` は、製品の ID をエンティティとして使用して平均レビューを計算します。

この例では、 `purchases_now`によって生成されたタイムラインを取得し、`shift_by()`関数を使用して時間を 1 日進めます。次に、シフトされていない元の値からシフトされた値を減算します。

### 組み合わせの例

最後に、これまで紹介した様々な表現を組み合わせた例を紹介します。

```javascript
# How many big purchases happen each hour and where?
let cadence = hourly()
# Anything can be named and re-used
let hourly_big_purchases = Purchase
| when(Purchase.amount > 10)
# Filter anywhere
| count(window=since(cadence))
# Aggregate anything
| when(cadence)
# No choosing between “when” & “having”

in {hourly_big_purchases}
# Records are just another type
| extend({
  # …modify them sequentially
  last_visit_region: last(Pageview.region)
})
```