人工智能(AI) VS 商业智能(BI) 区别与联系是什么?
REST API 设计:过滤、排序和分页
无论 API 是公开的还是内部使用的,API 设计正在成为 API 产品策略的核心支柱。良好的 API 设计可以改善任何 API 程序的整体开发人员体验 (DX),并且可以提高性能和长期可维护性。
然而,没有标准或官方的 API 设计指南。 RESTful只是一种架构风格。有许多针对 API 设计的初学者 API 指南可供使用,例如本指南。然而,我们没有找到很多关于更高级的过滤和分页的 API 指南,这激发了我们发布这篇文章。
过滤
URL 参数是将基本过滤添加到 REST API 的最简单方法。如果您有一个销售商品的客户端,您可以通过属性名称作为或条件进行过滤 。但是,这仅适用于精确匹配。如果您想要确定价格或日期范围等范围,该怎么办? /items
GET /items?state=active
GET /items?state=active&seller_id=1234
问题是 URL 参数只有一个键和一个值,但过滤器由三个组件组成:
- 属性或字段名称
- eq、lte、gte 等运算符
- 过滤值
有多种方法可以将三个组件编码为 URL 参数键/值。
方括号
对运算符进行编码的一种方法是在键名上使用方括号。例如,将查找价格大于或等于 10,但小于或等于 100 的所有商品。 []
GET /items?price[gte]=10&price[lte]=100
我们可以根据需要拥有任意多个运算符,例如 [lte]、[gte]、[exists]、[regex]、[before] 和 [after]。
LHS 括号在服务器端解析起来有点困难,但为客户端的过滤器值提供了更大的灵活性。无需以不同方式处理特殊字符。
好处
- 方便客户使用。有许多可用的查询字符串解析库可以轻松地将嵌套 JSON 对象编码到方括号中。 qs 就是这样一个自动编码/解码方括号的库:
var qs = require('qs');
var assert = require('assert');
assert.deepEqual(qs.parse('price[gte]=10&price[lte]=100'), {
price: {
gte: 10,
lte: 100
}
});
- 易于在服务器端解析。 URL 参数键包含字段名称和运算符。无需查看 URL 参数值即可轻松(属性名称、运算符)。
GROUP BY
- 当运算符被用作文字过滤器术语时,无需转义过滤器值中的特殊字符。当您的过滤器包含用户可能设置的其他自定义元数据字段时尤其如此。
缺点
- 可能需要在服务器端进行更多工作来解析和分组过滤器。您可能需要编写自定义 URL 参数绑定器或解析器来将查询字符串键拆分为两个部分:字段名称和运算符。然后您需要(属性名称,运营商)。
GROUP BY
- 变量名中的特殊字符可能会很尴尬。您可能需要编写一个自定义绑定器来将查询字符串键拆分为两个部分:字段名称和运算符。
- 难以管理自定义组合过滤器。具有相同属性名称和运算符的多个过滤器会产生隐式 AND。如果 API 用户想要对过滤器进行“或”操作该怎么办?即查找价格小于 10 或大于 100 的所有商品?
冒号
与括号方法类似,您可以设计一个 API 将运算符置于 RHS(而不是 LHS)上。例如,将查找价格大于或等于 10,但小于或等于 100 的所有商品。 GET /items?price=gte:10&price=lte:100
好处
- 在服务器端最容易解析,特别是在不支持重复过滤器的情况下。不需要定制活页夹。许多 API 框架已经可以处理 URL 参数数组。多个价格过滤器将位于同一变量“价格”下,该变量可以是序列或映射。
缺点
- 文字值需要特殊处理。例如,将转换为查找 user_id 大于 100 的所有项目。但是,如果我们想要查找 user_id 等于 gt:100 的所有项目(因为这可能是有效 ID)怎么办?
GET /items?user_id=gt:100
搜索查询参数
如果您需要在端点上进行搜索,您可以直接使用搜索参数添加对过滤器和范围的支持。如果您已经在使用 ElasticSearch 或其他基于 Lucene 的技术,您可以直接支持 Lucene 语法或 ElasticSearch 简单查询字符串。
例如,我们可以搜索包含术语 red chair 且价格大于或等于 10 且小于或等于 100 的商品: GET /items?q=title:red chair AND price:[10 TO 100]
此类 API 可以允许模糊匹配、增强某些术语等等。
好处
- 为API用户提供最灵活的查询
- 后端几乎不需要解析,可以直接传递到搜索引擎或数据库(只是要小心清理输入以确保安全)
缺点
- 对于初学者来说,开始使用 API 比较困难。需要熟悉Lucene语法。
- 全文搜索并不适用于所有资源。例如,模糊性和术语提升对于时间序列指标数据没有意义。
- 需要 URL 百分比编码,这使得使用 cURL 或 Postman 更加复杂。
分页
大多数返回实体列表的端点都需要某种分页。
如果没有分页,简单的搜索可能会返回数百万甚至数十亿的点击量,从而导致额外的网络流量。
分页需要隐式排序。默认情况下,这可能是项目的唯一标识符,但也可以是其他有序字段,例如创建日期。
偏移分页
这是最简单的分页形式。限制/偏移在使用 SQL 数据库的应用程序中变得流行,这些数据库已经将 LIMIT 和 OFFSET 作为 SQL SELECT 语法的一部分。实现限制/偏移分页只需要很少的业务逻辑。
限制/偏移分页看起来像。此查询将返回从第 100 行开始的 20 行。 GET /items?limit=20&offset=100
例子
(假设查询按创建日期降序排序)
- 客户请求最近的项目:
GET /items?limit=20
- 在滚动/下一页上,客户端发出第二个请求
GET /items?limit=20&offset=20
- 在滚动/下一页上,客户端发出第三个请求
GET /items?limit=20&offset=40
作为 SQL 语句,第三个请求如下所示:
SELECT
*
FROM
Items
ORDER BY Id
LIMIT 20
OFFSET 40;
好处
- 最容易实现,除了直接将参数传递给 SQL 查询之外几乎不需要任何编码。
- 服务器上无状态。
- 无论自定义 sort_by 参数如何,都可以工作。
缺点
- 对于大偏移值来说效果不佳。假设您执行一个具有较大偏移值 1000000 的查询。数据库需要扫描并计算从 0 开始的行,并且将跳过(即丢弃数据)前 1000000 行。
- 当新项目插入到表中时不一致(即页面漂移),当我们按最新的优先顺序对项目进行排序时,这一点尤其明显。考虑以下通过减小 Id 进行排序的情况:
- 查询
GET /items?offset=0&limit=15
- 表中添加了 10 个新项目
- 查询 第二个查询将仅返回 5 个新项目,因为添加 10 个新项目会将偏移量向后移动 10 个项目。为了解决这个问题,客户端确实需要为第二个查询偏移 25,但客户端不可能知道插入到表中的其他对象。
GET /items?offset=15&limit=15
GET /items?offset=25&limit=15
- 查询
即使有限制,偏移分页也很容易实现和理解,并且可以用于数据集上限较小的应用程序。
键集分页
键集分页使用最后一页的过滤器值来获取下一组项目。这些列将被索引。
例子
(假设查询按创建日期降序排序)
- 客户请求最近的项目:
GET /items?limit=20
- 在滚动/下一页上,客户端从之前返回的结果中找到最小创建日期 2021-01-20T00:00:00。然后使用日期作为过滤器进行第二个查询:
GET /items?limit=20&created:lte:2021-01-20T00:00:00
- 在滚动/下一页上,客户端从之前返回的结果中找到最小创建日期 2021-01-19T00:00:00。然后使用日期作为过滤器进行第三次查询:
GET /items?limit=20&created:lte:2021-01-19T00:00:00
SELECT
*
FROM
Items
WHERE
created <= '2021-01-20T00:00:00'
ORDER BY Id
LIMIT 20
好处
- 可与现有过滤器配合使用,无需额外的后端逻辑。只需要一个额外的限制URL参数。
- 即使将新项目插入表中,也能保持一致的顺序。按最新的优先排序时效果很好。
- 即使偏移较大,也能保持一致的性能。
缺点
- 分页机制与过滤器和排序紧密耦合。即使不打算使用过滤器,也会强制 API 用户添加过滤器。
- 不适用于低基数字段,例如枚举字符串。
- 使用自定义 sort_by 字段时,API 用户会很复杂,因为客户端需要根据用于排序的字段调整过滤器。
键集分页对于具有单个自然高基数键的数据非常有效,例如可以使用时间戳的时间序列或日志数据。
查找分页
查找分页是键集分页的扩展。通过添加 after_id 或 start_id URL 参数,我们可以消除分页与过滤器和排序的紧密耦合。由于唯一标识符本质上是高基数,因此我们不会遇到与按低基数字段(如状态枚举或类别名称)排序不同的问题。
基于搜索的分页的问题是当需要自定义排序顺序时很难实现。
例子
(假设查询按创建日期升序排序)
- 客户请求最近的项目:
GET /items?limit=20
- 在滚动/下一页上,客户端从之前返回的结果中找到最后一个 id“20”。然后使用它作为起始 id 进行第二次查询:
GET /items?limit=20&after_id=20
- 在滚动/下一页上,客户端从之前返回的结果中找到最后一个 id“40”。然后使用它作为起始 id 进行第三次查询:
GET /items?limit=20&after_id=40
寻求分页可以被提炼成一个子句 where
SELECT
*
FROM
Items
WHERE
Id > 20
LIMIT 20
如果按 id 排序,上面的示例可以正常工作,但如果我们想按电子邮件字段排序怎么办?对于每个请求,后端需要首先获取标识符与 after_id 匹配的项目的电子邮件值。然后,使用该值作为过滤器执行第二个查询。 where
让我们考虑一下查询,后端将需要两个查询。第一个查询可能是使用哈希表进行 O(1) 查找,以获得电子邮件数据透视值。将此输入到第二个查询中,以仅检索电子邮件位于 after_email 之后的项目。我们按电子邮件和 ID 两列进行排序,以确保在两封电子邮件相同的情况下有稳定的排序。这对于较低基数字段至关重要。 GET /items?limit=20&after_id=20&sort_by=email
1.
SELECT
email AS AFTER_EMAIL
FROM
Items
WHERE
Id = 20
2.
SELECT
*
FROM
Items
WHERE
Email >= [AFTER_EMAIL]
ORDER BY Email, Id
LIMIT 20
好处
- 分页逻辑与过滤逻辑没有耦合。
- 即使将新项目插入表中,也能保持一致的顺序。按最新的优先排序时效果很好。
- 即使偏移较大,也能保持一致的性能。
缺点
- 相对于基于偏移或基于键集的分页,后端实现更复杂
- 如果从数据库中删除项目,则 start_id 可能不是有效的 ID。
Seek 分页是一个很好的整体分页策略,也是我们在 Moesif Public API 上实现的。它需要在后端进行更多的工作,但确保不会给 API 的客户端/用户增加额外的复杂性,同时即使在更大的搜索范围内也能保持性能。
排序
与过滤一样,排序对于任何返回大量数据的 API 端点来说都是一项重要功能。如果您要返回用户列表,您的 API 用户可能希望按上次修改日期或电子邮件进行排序。
为了启用排序,许多 API 添加了 sort 或 sort_by URL 参数,该参数可以将字段名称作为值。
然而,良好的 API 设计可以灵活地指定升序或降序。与过滤器一样,指定顺序需要将三个组件编码为键/值对。
格式示例
-
GET /users?sort_by=asc(email)
和GET /users?sort_by=desc(email)
-
GET /users?sort_by=+email
和GET /users?sort_by=-email
-
GET /users?sort_by=email.asc
和GET /users?sort_by=email.desc
-
GET /users?sort_by=email&order_by=asc
和GET /users?sort_by=email&order_by=desc
多列排序
不建议使用排序和顺序不配对的最后一种设计。您最终可能允许按两列或更多列排序:
SELECT
email
FROM
Items
ORDER BY Last_Modified DESC, Email ASC
LIMIT 20
要编码此多列排序,您可以允许多个字段名称,例如
GET /users?sort_by=desc(last_modified),asc(email)
或
GET /users?sort_by=-last_modified,+email
如果排序字段和排序不配对,则需要保留 URL 参数排序;否则,什么排序应该与什么字段名称配对是不明确的。然而,许多服务器端框架在反序列化到映射后可能不会保留顺序。
您还必须确保任何缓存键都考虑 URL 参数排序,但这会给缓存大小带来压力。
结论
良好的 API 设计是开发者体验 (DX) 的关键组成部分。 API 规范可以比许多底层服务器实现更持久,这需要考虑 API 的未来用例。