指南

搜索

了解如何执行搜索、应用过滤、对结果排序、包含关联以及聚合数据。

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

过滤

过滤数据有两种方式——查询作用域(scope)和过滤器(filter)。

推荐使用查询作用域,因为查询约束封装在 API 内部(模型上的 scope 方法),当这些约束发生变化时,前端(终端客户端)无需做任何改动。

而过滤器则提供了非常灵活的查询约束应用方式,就像你通过 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'];
    }

    ...
}

若要实际使用一个或多个作用域来过滤数据,请向 search 端点发送请求,并在请求体中包含 scopes 属性,指定作用域的 name 及其 parameters

// (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'];
    }

    ...
}

在发送到 search 端点的请求中包含 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)作为组合多个过滤器的逻辑运算符,可以是 andor。在底层,它决定应用过滤器时在查询构造器上使用 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 只要求至少有一个值存在。

最后同样重要的是 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)的值进行过滤。只需使用 pivot.<field> 表示法,其中 <field> 是中间表上的字段。

也可以基于 json 字段内部的值来过滤结果,只需使用「箭头」表示法将它们与其他属性一起加入白名单即可。 上面示例中的 options->visible 就是这类属性。

关键词搜索

这类搜索是你通常会用到的功能,例如网站上的搜索框,用于查找所有包含「Laravel is awesome」这一短语的博客文章。 首先,你需要定义执行搜索时所涉及的字段列表:

搜索默认区分大小写。你可以通过在 orion.php 配置中将 search.case-sensitive 设为 false 来改变此行为。
<?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 端点的请求中包含 search 属性:

// (POST) https://myapp.com/api/posts/search
{
     "search" : {
        "value" : "Laravel is awesome",
        "case_sensitive": false // (默认值:true)
    },
}
即使 orion.php 配置中的 search.case-sensitive 设为 true,你也可以通过在请求中提供 case_sensitive: false 字段来执行不区分大小写的搜索。

目前,搜索是通过数据库查询在所有指定字段上执行的。

AlgoliaElasticSearch 的支持也在计划之中。

你可以对关联的属性执行搜索,只需使用点号表示法将它们与其他属性一起加入白名单即可。 上面示例中的 user.name 就是这类属性。

也可以对 json 字段内部的值执行搜索,只需使用「箭头」表示法将它们与其他属性一起加入白名单即可。 上面示例中的 options->key 就是这类属性。

排序

与为过滤器加入字段白名单的方式类似,排序字段也需要事先指定:

<?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 端点的请求中包含 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 属性的值就是白名单中的某个属性,direction 则是 ascdesc

你可以基于关联的属性对结果排序,只需使用点号表示法将它们与其他属性一起加入白名单即可。 上面示例中的 meta.priority 就是这类属性。

多对多关联的资源还可以按其中间表(pivot)的值进行排序。只需使用 pivot.<field> 表示法,其中 <field> 是中间表上的字段。

也可以基于 json 字段内部的值对结果排序,只需使用「箭头」表示法将它们与其他属性一起加入白名单即可。 上面示例中的 options->key 就是这类属性。

聚合

要使用聚合,需要将相关的关联和/或字段加入白名单。

可用的聚合有以下几种: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'];
    }

    ...
}

在发送到 search 端点的请求中包含 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'];
    }

    ...
}

在发送到 search 端点的请求中包含 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 方法中加入白名单。