所以,我确信以前一定有人问过这个问题,但我似乎什么都找不到。问题是,当我为网络应用程序编程搜索功能时,我感觉从来都不太对劲
我使用的是RubyonRails,但我想这个问题适用于任何使用RESTful MVC模式的情况。
假设您有一个资源(例如,Users、ToDos…)要搜索。一旦应用程序增长,这将不再适用于简单的LIKE查询,并且您开始使用索引(例如Solr、ElasticSearch、Lucene…)。索引资源也往往是来自资源及其关联对象(用户的位置、ToDos创建者等)的复合数据。
我们如何最好地代表这一点?
- 它是GET到/resources(Resource#索引)吗?这是一个主要资源的选择性列表,但话说回来,它实际上是一个复合的东西,如果搜索功能很广泛,它确实会使模型的代码膨胀
- 是POST到/searchs(Search#create)吗?我们正在创建一个搜索,但没有保存它。相反,它被转换为一组SearchResults
- 那么,它是获取SearchResult(SearchResult#show)吗?但它并没有ID。我想SearchIndex就是那个模型的数据库,但你们不会真正创建SearchResult,对吧?这更像是一个以SearchResult#节目结束的Search#创建,但对我来说也很不稳定
通常不建议使用POST
进行搜索操作,因为您失去了GET
所提供的所有优势-语义、幂等性、安全性(可缓存性)。。。
许多RESTful和类似REST的系统使用简单的GET
查询,其中搜索参数作为query
或path
参数,以允许基于客户端和服务器的查询和结果缓存。自HTTP 1.1。缓存包含查询参数的GET请求不是问题,除非正确指定了缓存头。
但是预定义的查询有一种LIKE
查询的味道,您尽量避免这种味道。特别是ElasticSearch允许动态地向类型添加新字段。这可能会引入新的开销,以便添加新的预定义过滤器来支持对这些字段的查询。因此,从长远来看,根据需要动态添加查询可能是一项基本要求。不过,这并不是很难实现的。
因此,包含动态添加的搜索过滤器的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"
}
}
}
},
{
...
}
]
}
}
}
上面的示例代码包含一个用户表示,其中嵌入了扩展JSONHAL格式的地址和搜索过滤器。由于RESTful资源应该尽可能不言自明,因此示例包含指向其位置和模式的链接,以便post
和put
操作也知道服务器可能需要哪些字段。
search
资源充当过滤器的控制器,因为它只允许一次添加新过滤器或删除所有过滤器,而通过调用/users/{userId}/search/filters?page=pageNo
上的GET
来迭代过滤器页面。
现在,一个实际的过滤器包含要执行的实际指令——在本例中,是一个ElasticSearch查询,用于查找用户名称或当前地址20公里范围内的所有内容——以及执行查询的实际URI的链接。请注意,ElasticSearch代码实际上包含一个指向资源的链接,该资源包含实际查询应该使用的数据。当然,可以返回一个有效的ElasticSearch查询,该查询包含实际的用户数据,甚至还可以返回JSON指针,而不是指向数据的URI——这也是一些实现细节。
这种方法允许在运行时动态添加新查询或更新现有查询,同时在查询时保持GET
语义不变。此外,还可以利用缓存功能,这可以显著提高性能,尤其是在用户数据不经常更改的情况下。
然而,这种方法的缺点是,您必须返回更多关于用户查找的数据。您也可以考虑不返回嵌入的过滤器,并让客户端明确地轮询这些过滤器。此外,当前的过滤器是由一个特定的名称添加的,该名称充当密钥。在实践中,这可能会导致命名冲突。因此,最终UUID更好,但如果人类必须调用这些URI,也会去掉语义,因为byName
对人类来说肯定比de305d54-75b4-431b-adb2-eb6b9e546014
更具语义,但这更多的是一个实现细节。