ガイド

検索

検索の実行、フィルタの適用、結果のソート、リレーションのインクルード、データの集計の方法を学びます。

Orionは、ソート、フィルタリング、キーワード検索、集計、インクルードを備えた包括的な検索機能をAPIエンドポイントに提供します。

// (POST) https://myapp.com/api/posts/search
{
    "scopes" : [
        {"name" : "active"},
        {"name" : "whereCategory", "parameters" : ["my-category"]}
    ],
    "filters" : [
        {"field" : "created_at", "operator" : ">=", "value" : "2020-01-01"},
        {"field" : "options->visible", "operator" : ">=", "value" : true},
        {"type" : "or", "field" : "meta.source_id", "operator" : "in", "value" : [1,2,3]}
    ],
    "search" : {
        "value" : "Example post"
    },
    "sort" : [
        {"field" : "name", "direction" : "asc"},
        {"field" : "options->key", "direction" : "asc"},
        {"field" : "meta.priority", "direction" : "desc"}
    ],
    "aggregates": [
        {
          "relation": "tags",
          "type": "count",
          "filters": [
            {"field" : "tags.created_at", "operator" : ">=", "value" : "2020-01-01"}
          ]
        }
    ],
    "includes": [
        {
          "relation": "tags",
          "filters": [
            {"field" : "tags.created_at", "operator" : ">=", "value" : "2020-01-01"}
          ]
        }
    ]
}
クエリ制約が適用される順序は、ペイロード内のプロパティの順序には依存しません。常に次の順序で適用されます: scopes -> filters -> search -> sort -> includes -> aggregates

フィルタリング

データをフィルタリングする方法には、クエリスコープとフィルタの2つがあります。

クエリスコープの使用を推奨します。クエリ制約がAPI側(モデルのスコープメソッド)にカプセル化されるため、制約が変更されてもフロントエンド(エンドクライアント)側の変更が不要になるからです。

一方、フィルタは、Eloquentクエリビルダで行うのと同じように、クエリ制約を非常に柔軟に適用できる方法を提供します。

スコープの適用

まず、API経由で公開するスコープの一覧をコントローラーに設定する必要があります。

<?php

namespace App\Http\Controllers\Api;

use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    ...

    /**
     * 利用可能なクエリスコープのリスト。
     *
     * @return array
     */
    public function exposedScopes() : array
    {
        return ['active', 'whereCategory'];
    }

    ...
}

1つまたは複数のスコープを使って実際にデータをフィルタリングするには、検索エンドポイントにリクエストを送り、ペイロードにスコープのnameとそのparametersを持つscopesプロパティを含めます。

// (POST) https://myapp.com/api/posts/search
{
    "scopes" : [
        {"name" : "active"},
        {"name" : "whereCategory", "parameters" : ["my-category"]}
    ],
}

フィルタの適用

クエリ制約をきめ細かく制御する必要がある場合は、フィルタを使用するほうが適していることがあります。スコープの公開と同様に、フィルタで使用できるフィールドをホワイトリストに登録する必要があります。

<?php

namespace App\Http\Controllers\Api;

use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    ...

    /**
    * フィルタリングに使用される属性。
    *
    * @return array
    */
    public function filterableBy() : array
    {
        return ['id', 'title', 'options->visible', 'user.id', 'meta.source_id', 'created_at'];
    }

    ...
}

検索エンドポイントへのリクエストにfiltersプロパティを含めます。

// (POST) https://myapp.com/api/posts/search
{
    "filters" : [
        {"field" : "created_at", "operator" : ">=", "value" : "2020-01-01"},
        {"field" : "options->visible", "operator" : ">=", "value" : true},
        {"type" : "or", "field" : "meta.source_id", "operator" : "in", "value" : [1,2,3]}
    ]
}

上記の例からわかるように、各フィルタ記述子はtype(省略可能)、fieldoperatorvalueのプロパティで構成されます。

fieldプロパティの値は、ホワイトリストに登録された属性のいずれかです。

typeプロパティ(デフォルトはand)は、複数のフィルタを組み合わせる際の論理演算子として機能し、andまたはorのいずれかを指定できます。内部的には、フィルタの適用にクエリビルダのwhereメソッドとorWhereメソッドのどちらを使用するかを決定します。

operatorプロパティには、サポートされている比較演算のいずれかを指定する必要があります。

'<', '<=', '>', '>=', '=', '!=', 'like', 'not like', 'ilike', 'not ilike', 'in', 'not in', 'all in', 'any in'

これらの演算子(all inany inを除く)は、Eloquentのクエリビルダ->where('<some field>', '<operator>', '<value>')の呼び出しに通常渡す演算子とまったく同じものです。

次の演算子はjson / jsonbカラムでの使用を想定しており、実質的にwhereJsonContains制約を適用します。

'all in', 'any in'

all inany inの違いは、all inを適用した場合、エンティティが結果に含まれるためには指定したすべての値がカラム内に存在する必要があるのに対し、any inでは少なくとも1つの値が存在すればよいという点です。

最後に重要なのがvalueです。これは、指定した比較条件を満たすために属性が持つべき実際の値です。

ネストしたフィルタ

フィルタを「グループ」で適用したい場合は、ネストしたフィルタの機能が役立ちます。

// (POST) https://myapp.com/api/posts/search
{
  "filters" : [
      {"field" : "created_at", "operator" : ">=", "value" : "2020-01-01"},
      {"type": "or", "nested" : [
        {"field" : "options->visible", "operator" : "=", "value" : true},
        {"type" : "and", "field" : "meta.source_id", "operator" : "in", "value" : [1,2,3]}
      ]}
  ]
}

上記のリクエストでは、created_atフィールドが2020-01-01以上である、またはoptions->visibleフィールドがtrueに等しくかつmeta.source_idフィールドの値が配列[1,2,3]に含まれる場合に、エンティティがAPIから返されます。

上記の条件は、擬似言語で次のように表現できます。

(created_at >= "2020-01-01") OR (options->visible = true AND meta.source_id IN [1,2,3])

ネストしたフィルタはいくらでも追加できますが、ネストしたフィルタが増えるごとにデータベースへのクエリ全体の「複雑さ」が増すため、機能の過度な使用を防ぐ目的で、深さはデフォルトで1に制限されています。

この制限を引き上げる必要がある場合は、orion.php設定ファイルのsearch.max_nested_depthを変更します。
ドット記法を使って他の属性と同様にホワイトリストに登録するだけで、リレーションの属性に基づいて結果をフィルタリングできます。 上記の例では、user.idmeta.source_idがそのような属性の例です。

多対多リレーションのリソースは、ピボット値でもフィルタリングできます。pivot.<field>記法を使用してください。<field>はピボットテーブル上のフィールドです。

「アロー」記法を使って他の属性と同様にホワイトリストに登録することで、jsonフィールド内の値に基づいて結果をフィルタリングすることも可能です。 上記の例では、options->visibleがそのような属性の1つです。

キーワード検索

この種の検索は、たとえばWebサイトの検索入力機能として、「Laravel is awesome」というフレーズを含むすべてのブログ記事を見つける、といった場面で一般的に使われるものです。 まず、検索の対象となるフィールドの一覧を定義する必要があります。

検索はデフォルトで大文字と小文字を区別します。この動作を変更するには、orion.php設定のsearch.case-sensitivefalseに設定します。
<?php

namespace App\Http\Controllers\Api;

use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    ...

    /**
     * 検索に使用される属性。
     *
     * @return array
     */
    public function searchableBy() : array
    {
        return ['title', 'description', 'options->key', 'user.name'];
    }

    ...
}

検索エンドポイントへのリクエストにsearchプロパティを含めます。

// (POST) https://myapp.com/api/posts/search
{
     "search" : {
        "value" : "Laravel is awesome",
        "case_sensitive": false // (デフォルト: true)
    },
}
orion.php設定でsearch.case-sensitivetrueに設定されている場合でも、リクエストにcase_sensitive: falseフィールドを指定することで、大文字と小文字を区別しない検索を実行できます。

現時点では、検索は指定されたすべてのフィールドに対するデータベースクエリによって実行されます。

AlgoliaおよびElasticSearchのサポートも予定されています。

ドット記法を使って他の属性と同様にホワイトリストに登録するだけで、リレーションの属性に対して検索を実行できます。 上記の例では、user.nameがそのような属性の1つです。

「アロー」記法を使って他の属性と同様にホワイトリストに登録することで、jsonフィールド内の値に対して検索を実行することも可能です。 上記の例では、options->keyがそのような属性の1つです。

ソート

フィルタ用のフィールドをホワイトリストに登録するのと同様に、ソート用のフィールドも指定する必要があります。

<?php

namespace App\Http\Controllers\Api;

use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    ...

    /**
     * ソートに使用される属性。
     *
     * @return array
     */
    public function sortableBy() : array
    {
         return ['id', 'name', 'options->key', 'meta.priority'];
    }

    ...
}

検索エンドポイントへのリクエストにsearchプロパティを含めます。

// (POST) https://myapp.com/api/posts/search
{
    "sort" : [
        {"field" : "name", "direction" : "asc"},
        {"field" : "options->key", "direction" : "asc"},
        {"field" : "meta.priority", "direction" : "desc"}
    ]
}

各ソート記述子はfielddirectionのプロパティで構成されます。

fieldプロパティの値はホワイトリストに登録された属性のいずれかで、directionascまたはdescのいずれかです。

ドット記法を使って他の属性と同様にホワイトリストに登録するだけで、リレーションの属性に基づいて結果をソートできます。 上記の例では、meta.priorityがそのような属性の1つです。

多対多リレーションのリソースは、ピボット値でもソートできます。pivot.<field>記法を使用してください。<field>はピボットテーブル上のフィールドです。

「アロー」記法を使って他の属性と同様にホワイトリストに登録することで、jsonフィールド内の値に基づいて結果をソートすることも可能です。 上記の例では、options->keyがそのような属性の1つです。

集計

集計を利用するには、必要なリレーションやフィールドをホワイトリストに登録する必要があります。

利用可能な集計はcountavgsumminmaxexistsです。

<?php

namespace App\Http\Controllers\Api;

use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    ...

    /**
    * リソースで集約が許可されているリレーションとフィールド。
    *
    * @return array
    */
    public function aggregates() : array
    {
        return ['user', 'user.team', 'user.profile', 'meta'];
    }

    ...
}

ワイルドカードを使用して、可能なリレーションやフィールドをすべて定義する手間を減らすこともできます。

<?php

namespace App\Http\Controllers\Api;

use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    ...

    /**
    * リソースで集約が許可されているリレーションとフィールド。
    *
    * @return array
    */
    public function aggregates() : array
    {
        return ['user.*', 'meta'];
    }

    ...
}

検索エンドポイントへのリクエストにaggregatesプロパティを含めます。

// (POST) https://myapp.com/api/posts/search
{
    "aggregates" : [
        {"type" : "count", "relation" : "tags"},
        {"type" : "exists", "relation" : "tags"},
        {"type" : "avg", "relation" : "tags", "field": "stars"},
        {"type" : "sum", "relation" : "tags", "field": "stars"},
        {"type" : "min", "relation" : "tags", "field": "stars"},
        {"type" : "max", "relation" : "tags", "field": "stars"}
    ]
}
countexistsの集計は他の集計とは動作が異なり、relationフィールドのみを必要とすることに注意してください。

フィルタの適用

集計にフィルタを指定することもできます。ネストしたフィルタもサポートされています。

{
  "aggregates": [
    {
      "relation": "tags",
      "type": "count",
      "filters": [
        {"field" : "tags.created_at", "operator" : ">=", "value" : "2020-01-01"}
      ]
    },
    {
      "relation": "tags",
      "field": "stars",
      "type": "avg",
      "filters": [
        {"field" : "tags.created_at", "operator" : ">=", "value" : "2020-01-01"},
        {"nested": [
          {"field": "tags.id", "operator": "=", "value": 1},
          {"field": "tags.id", "operator": ">", "value": 10, "type": "or"}
        ]}
      ]
    }
  ]
}
フィルタはコントローラーのfilterableByメソッドでホワイトリストに登録されている必要があります。

インクルード

返されるリソースと一緒にリレーションを含めたい場合があります。集計と同様に、インクルードも最初にホワイトリストに登録する必要があります。

<?php

namespace App\Http\Controllers\Api;

use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    ...

    /**
    * リソースと一緒に含めることが許可されているリレーション。
    *
    * @return array
    */
    public function includes() : array
    {
        return ['user', 'user.team', 'user.profile', 'meta'];
    }

    ...
}

ワイルドカードを使用して、可能なリレーションをすべて定義する手間を減らすこともできます。

<?php

namespace App\Http\Controllers\Api;

use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    ...

    /**
    * リソースと一緒に含めることが許可されているリレーション。
    *
    * @return array
    */
    public function includes() : array
    {
        return ['user.*', 'meta'];
    }

    ...
}

検索エンドポイントへのリクエストにincludesプロパティを含めます。

// (POST) https://myapp.com/api/posts/search
{
    "includes" : [
        {"relation" : "tags", "limit" : 10},
        {"relation" : "comments"}
    ]
}
limitフィールドを指定することで、返されるリレーションのエンティティ数を制限できます。

常にインクルードされるリレーション

クエリパラメータで渡さなくてもデフォルトでリレーションをロードするには、alwaysIncludesメソッドを使用します。

<?php

namespace App\Http\Controllers\Api;

use Orion\Http\Controllers\Controller;

class PostsController extends Controller
{
    ...

    /**
    * リソースと一緒にデフォルトで読み込まれるリレーション。
    *
    * @return array
    */
    public function alwaysIncludes() : array
    {
        return ['user', 'meta'];
    }

    ...
}
includeメソッドとは異なり、alwaysIncludesメソッドはワイルドカードをサポートしていません。

フィルタの適用

インクルードにフィルタを指定することもできます。ネストしたフィルタもサポートされています。

{
  "includes": [
    {
      "relation": "comments",
      "filters": [
        {"field" : "comments.created_at", "operator" : ">=", "value" : "2020-01-01"}
      ]
    },
    {
      "relation": "tags",
      "filters": [
        {"field" : "tags.created_at", "operator" : ">=", "value" : "2020-01-01"},
        {"nested": [
          {"field": "tags.id", "operator": "=", "value": 1},
          {"field": "tags.id", "operator": ">", "value": 20, "type": "or"}
        ]}
      ]
    }
  ]
}
フィルタはコントローラーのfilterableByメソッドでホワイトリストに登録されている必要があります。