RESTful 搜索和搜索结果的 MVC 模式
RESTful MVC Pattern for Searches and Search Results
所以,我确定之前一定有人问过这个问题,但我似乎找不到任何东西。问题是,当我为 Web 应用程序编写搜索功能时,它对我来说从来都不是很合适。
我在 Rails 上使用 Ruby,但我想这个问题适用于您使用 RESTful MVC 模式的任何情况。
假设您有一个要搜索的 资源(例如用户、待办事项等)。一旦应用程序增长,这将不再适用于简单的 LIKE 查询,并且您开始使用索引(例如 Solr、ElasticSearch、Lucene 等)。索引资源也往往是来自资源及其关联对象(用户位置、待办事项创建者……)的复合数据。
我们如何最好地表达这一点?
- 它是否是 /resources (Resource#index) 的 GET?它是主要资源的选择性列表,但实际上它又是一个复合物,如果搜索功能很广泛,它确实会膨胀模型的代码。
- 它是 POST 到 /searches (Search#create) 吗?我们正在创建搜索但没有保存它。相反,它有点被转换成一组 SearchResults。
- 那么,它是 SearchResult 的 GET (SearchResult#show) 吗?但它没有身份证。我想 SearchIndex 是那种模型的数据库,但您不会真正创建 SearchResult,对吧?它更像是一个以 SearchResult#show 结尾的 Search#create,但我也觉得它很奇怪。
通常不建议将 POST
用于 search-operations,因为您会失去 GET
必须提供的所有优势 - 语义、幂等性、安全性(可缓存性)...
许多 RESTful 和 REST-like 系统使用简单的 GET
查询,搜索参数作为 query
或 path
参数允许客户端和 server-based 缓存查询和结果。从 HTTP 1.1 开始。缓存包含 query-parameters 的 GET 请求不是问题,除非正确指定缓存 headers。
但是预定义的查询具有 LIKE
查询的味道,您会尽量避免。特别是 ElasticSearch 允许动态地向类型添加新字段。这可能会引入新的开销来跟上添加新的预定义过滤器以支持对这些字段的查询。因此,根据需要动态添加查询可能是 long 运行 的基本要求。不过,这并不难实现。
包含动态添加的搜索过滤器的 GET /users/12345
查询的示例输出可能因此如下所示:
{
"id": "12345",
"firstName": "Max",
"lastName": "Test",
"_schema": {
"href": "http://example.com/schema/user"
}
"_links": {
"self": {
"href": "/users/12345",
"methods": ["get", "put", "delete"]
},
"curies": [{
"name": "usr",
"href": "http://example.com/docs/rels/{rel}",
"templated": true
}],
"usr:employee": {
"href": "/companies/112233",
"title": "Sample Company",
"type": "application/hal+json"
}
},
"_embedded": {
"usr:address": [
{
"_schema": {
"href": "http://example.com/schema/address"
},
"street" : "Sample Street",
"zip": "...",
"city": "...",
"state": "...",
"location": {
"longitude": "...",
"latitude": "..."
}
"_links": {
"self": {
"href": "/users/12345/address/1",
"_methods": ["get", "post", "put", "delete"],
}
}
}
],
"usr:search": {
"_schema": {
"href": "http://example.com/schema/user_search"
}
"_links": {
"self": {
"href": "/users/12345/search",
"methods: ["post", "delete"]
}
},
"filters": [
"_schema": {
"href": "http://example.com/schema/user_search_filter"
},
"_links": {
"self": {
"href": "/users/12345/search/filters",
"methods: ["get"]
},
"next": {
"href": "/users/12345/search/filters?page=2"
"methods: ["get"]
}
},
{
"byName": {
"query": {
"constant_score": {
"filter": {
"term": {
"name": {
"href": "/users/12345#name"
}
}
}
}
}
"_links": {
"self": {
"href": "/users/12345/search/filter/byName",
"methods": ["get", "put", "delete"],
"_schema": {
"href": "http://example.com/schema/search_byName"
}
"type": "application/hal+json"
}
}
}
},
{
"in20kmDistance" : {
"query": {
"filtered" : {
"query" : {
"match_all" : {}
},
"filter" : {
"geo_distance" : {
"distance" : "20km",
"Location" : {
"lat" : {
"href": "/users/12345/address/location#lat"
},
"lon" : {
"href": "/users/12345/address/location#lon"
}
}
}
}
}
}
}
"_links": {
"self": {
"href": "/users/12345/search/filter/in20kmDistance,
"methods": ["get", "put", "delete"],
"_schema": {
"href": "http://example.com/schema/search_in20kmDistance"
}
"type": "application/hal+json"
}
}
}
},
{
...
}
]
}
}
}
上面的 example-code 包含带有嵌入式地址的用户表示和扩展 JSON HAL 格式的搜索过滤器。由于 RESTful 资源应尽可能 self-explanatory,样本包含 link 到它们的位置和它们的模式,以便 post
和 put
操作也知道服务器可能需要哪些字段。
search
资源充当过滤器的控制器,因为它只允许添加新过滤器或一次删除所有过滤器,而通过调用 GET
实现过滤器页面的迭代在 /users/{userId}/search/filters?page=pageNo
.
一个实际的过滤器现在包含要执行的实际指令 - 在这种情况下,ElasticSearch 查询用户名或当前地址 20 公里距离内的所有内容 - 以及 link执行查询的实际 URI。请注意,ElasticSearch 代码实际上包含 link 到包含实际查询应使用的数据的资源。当然,可以 return 包含实际用户数据的有效 ElasticSearch 查询,甚至 JSON Pointer 而不是数据的 URI——这又是一些实现细节。
这种方法允许在 运行 时间动态添加新查询或更新现有查询,同时保持 GET
语义在 query-time 不变。此外,还可以利用缓存功能,这可能会显着提高性能 - 特别是在用户数据不经常更改的情况下。
然而,这种方法的缺点是,您必须 return 更多有关用户查找的数据。您还可以考虑不 return 嵌入过滤器,并让客户端显式轮询这些过滤器。此外,目前过滤器是通过某个名称添加的,该名称充当键。实际上,这可能会导致 naming-clashes。因此,最终 UUID 更好,但如果人类必须调用这些 URI,也会带走语义,因为 byName
对人类来说肯定比 de305d54-75b4-431b-adb2-eb6b9e546014
更具语义,但这更多是一个实现细节。
所以,我确定之前一定有人问过这个问题,但我似乎找不到任何东西。问题是,当我为 Web 应用程序编写搜索功能时,它对我来说从来都不是很合适。
我在 Rails 上使用 Ruby,但我想这个问题适用于您使用 RESTful MVC 模式的任何情况。
假设您有一个要搜索的 资源(例如用户、待办事项等)。一旦应用程序增长,这将不再适用于简单的 LIKE 查询,并且您开始使用索引(例如 Solr、ElasticSearch、Lucene 等)。索引资源也往往是来自资源及其关联对象(用户位置、待办事项创建者……)的复合数据。
我们如何最好地表达这一点?
- 它是否是 /resources (Resource#index) 的 GET?它是主要资源的选择性列表,但实际上它又是一个复合物,如果搜索功能很广泛,它确实会膨胀模型的代码。
- 它是 POST 到 /searches (Search#create) 吗?我们正在创建搜索但没有保存它。相反,它有点被转换成一组 SearchResults。
- 那么,它是 SearchResult 的 GET (SearchResult#show) 吗?但它没有身份证。我想 SearchIndex 是那种模型的数据库,但您不会真正创建 SearchResult,对吧?它更像是一个以 SearchResult#show 结尾的 Search#create,但我也觉得它很奇怪。
通常不建议将 POST
用于 search-operations,因为您会失去 GET
必须提供的所有优势 - 语义、幂等性、安全性(可缓存性)...
许多 RESTful 和 REST-like 系统使用简单的 GET
查询,搜索参数作为 query
或 path
参数允许客户端和 server-based 缓存查询和结果。从 HTTP 1.1 开始。缓存包含 query-parameters 的 GET 请求不是问题,除非正确指定缓存 headers。
但是预定义的查询具有 LIKE
查询的味道,您会尽量避免。特别是 ElasticSearch 允许动态地向类型添加新字段。这可能会引入新的开销来跟上添加新的预定义过滤器以支持对这些字段的查询。因此,根据需要动态添加查询可能是 long 运行 的基本要求。不过,这并不难实现。
包含动态添加的搜索过滤器的 GET /users/12345
查询的示例输出可能因此如下所示:
{
"id": "12345",
"firstName": "Max",
"lastName": "Test",
"_schema": {
"href": "http://example.com/schema/user"
}
"_links": {
"self": {
"href": "/users/12345",
"methods": ["get", "put", "delete"]
},
"curies": [{
"name": "usr",
"href": "http://example.com/docs/rels/{rel}",
"templated": true
}],
"usr:employee": {
"href": "/companies/112233",
"title": "Sample Company",
"type": "application/hal+json"
}
},
"_embedded": {
"usr:address": [
{
"_schema": {
"href": "http://example.com/schema/address"
},
"street" : "Sample Street",
"zip": "...",
"city": "...",
"state": "...",
"location": {
"longitude": "...",
"latitude": "..."
}
"_links": {
"self": {
"href": "/users/12345/address/1",
"_methods": ["get", "post", "put", "delete"],
}
}
}
],
"usr:search": {
"_schema": {
"href": "http://example.com/schema/user_search"
}
"_links": {
"self": {
"href": "/users/12345/search",
"methods: ["post", "delete"]
}
},
"filters": [
"_schema": {
"href": "http://example.com/schema/user_search_filter"
},
"_links": {
"self": {
"href": "/users/12345/search/filters",
"methods: ["get"]
},
"next": {
"href": "/users/12345/search/filters?page=2"
"methods: ["get"]
}
},
{
"byName": {
"query": {
"constant_score": {
"filter": {
"term": {
"name": {
"href": "/users/12345#name"
}
}
}
}
}
"_links": {
"self": {
"href": "/users/12345/search/filter/byName",
"methods": ["get", "put", "delete"],
"_schema": {
"href": "http://example.com/schema/search_byName"
}
"type": "application/hal+json"
}
}
}
},
{
"in20kmDistance" : {
"query": {
"filtered" : {
"query" : {
"match_all" : {}
},
"filter" : {
"geo_distance" : {
"distance" : "20km",
"Location" : {
"lat" : {
"href": "/users/12345/address/location#lat"
},
"lon" : {
"href": "/users/12345/address/location#lon"
}
}
}
}
}
}
}
"_links": {
"self": {
"href": "/users/12345/search/filter/in20kmDistance,
"methods": ["get", "put", "delete"],
"_schema": {
"href": "http://example.com/schema/search_in20kmDistance"
}
"type": "application/hal+json"
}
}
}
},
{
...
}
]
}
}
}
上面的 example-code 包含带有嵌入式地址的用户表示和扩展 JSON HAL 格式的搜索过滤器。由于 RESTful 资源应尽可能 self-explanatory,样本包含 link 到它们的位置和它们的模式,以便 post
和 put
操作也知道服务器可能需要哪些字段。
search
资源充当过滤器的控制器,因为它只允许添加新过滤器或一次删除所有过滤器,而通过调用 GET
实现过滤器页面的迭代在 /users/{userId}/search/filters?page=pageNo
.
一个实际的过滤器现在包含要执行的实际指令 - 在这种情况下,ElasticSearch 查询用户名或当前地址 20 公里距离内的所有内容 - 以及 link执行查询的实际 URI。请注意,ElasticSearch 代码实际上包含 link 到包含实际查询应使用的数据的资源。当然,可以 return 包含实际用户数据的有效 ElasticSearch 查询,甚至 JSON Pointer 而不是数据的 URI——这又是一些实现细节。
这种方法允许在 运行 时间动态添加新查询或更新现有查询,同时保持 GET
语义在 query-time 不变。此外,还可以利用缓存功能,这可能会显着提高性能 - 特别是在用户数据不经常更改的情况下。
然而,这种方法的缺点是,您必须 return 更多有关用户查找的数据。您还可以考虑不 return 嵌入过滤器,并让客户端显式轮询这些过滤器。此外,目前过滤器是通过某个名称添加的,该名称充当键。实际上,这可能会导致 naming-clashes。因此,最终 UUID 更好,但如果人类必须调用这些 URI,也会带走语义,因为 byName
对人类来说肯定比 de305d54-75b4-431b-adb2-eb6b9e546014
更具语义,但这更多是一个实现细节。