用于搜索和搜索结果的RESTful MVC模式



所以,我确信以前一定有人问过这个问题,但我似乎什么都找不到。问题是,当我为网络应用程序编程搜索功能时,我感觉从来都不太对劲

我使用的是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查询,其中搜索参数作为querypath参数,以允许基于客户端和服务器的查询和结果缓存。自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资源应该尽可能不言自明,因此示例包含指向其位置和模式的链接,以便postput操作也知道服务器可能需要哪些字段。

search资源充当过滤器的控制器,因为它只允许一次添加新过滤器或删除所有过滤器,而通过调用/users/{userId}/search/filters?page=pageNo上的GET来迭代过滤器页面。

现在,一个实际的过滤器包含要执行的实际指令——在本例中,是一个ElasticSearch查询,用于查找用户名称或当前地址20公里范围内的所有内容——以及执行查询的实际URI的链接。请注意,ElasticSearch代码实际上包含一个指向资源的链接,该资源包含实际查询应该使用的数据。当然,可以返回一个有效的ElasticSearch查询,该查询包含实际的用户数据,甚至还可以返回JSON指针,而不是指向数据的URI——这也是一些实现细节。

这种方法允许在运行时动态添加新查询或更新现有查询,同时在查询时保持GET语义不变。此外,还可以利用缓存功能,这可以显著提高性能,尤其是在用户数据不经常更改的情况下。

然而,这种方法的缺点是,您必须返回更多关于用户查找的数据。您也可以考虑不返回嵌入的过滤器,并让客户端明确地轮询这些过滤器。此外,当前的过滤器是由一个特定的名称添加的,该名称充当密钥。在实践中,这可能会导致命名冲突。因此,最终UUID更好,但如果人类必须调用这些URI,也会去掉语义,因为byName对人类来说肯定比de305d54-75b4-431b-adb2-eb6b9e546014更具语义,但这更多的是一个实现细节。

最新更新