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

时间:2015-07-02 09:32:49

标签: rest search design-patterns model-view-controller

所以,我确定之前一定要问过,但我似乎找不到任何东西。问题在于,当我为网络应用程序编写搜索功能时,对我来说感觉不太对。

我使用Ruby on Rails,但我想这是一个适用于您使用RESTful MVC模式的任何情况的问题。

假设您要搜索资源(例如,用户,ToDos等)。一旦应用程序增长,再用简单的LIKE查询就不可行了,你开始使用索引(例如Solr,ElasticSearch,Lucene,......)。索引资源也往往是来自资源及其相关对象(用户位置,ToDos创建者......)的复合数据。

我们如何最好地代表这一点?

  • 是/资源(资源 #index)的GET?它是主要资源的选择性列表,但实际上它实际上是复合的东西,如果搜索功能很广泛,它真的会使模型的代码膨胀。
  • 是/ POST搜索(搜索 #create)?我们正在创建搜索但不保存它。相反,它会被转换为一组SearchResults。
  • 那么,它是对SearchResult的一个GET( SearchResult #show)吗?但它没有身份证。我猜SearchIndex是该模型的数据库,但你不会真正创建一个SearchResult,对吧?它更像是一个以SearchResult#show结尾的Search#create,但对我来说也感觉不太好。

1 个答案:

答案 0 :(得分:1)

通常不建议使用POST进行搜索操作,因为您失去了GET必须提供的所有优势 - 语义,幂等性,安全性(可缓存性),......

许多RESTful和类似REST的系统使用带有搜索参数的简单GET查询作为querypath参数,以允许基于客户端和服务器的查询和结果缓存。从HTTP 1.1开始。除非指定了高速缓存标头correclty,否则缓存包含查询参数的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"
                            }
                        }
                    }
                },
                {
                    ...
                }
            ]
        }
    }
}

上面的示例代码包含一个用户表示,其中包含嵌入式地址和扩展JSON HAL格式的搜索过滤器。由于RESTful资源应该像posible一样不言自明,因此示例包含指向其位置和架构的链接,以便postput操作也知道服务器可能需要哪些字段。

search资源充当过滤器的控制器,因为它只允许添加新过滤器或一次删除所有过滤器,而通过在{上调用GET来实现迭代过滤页面{1}}。

现在,实际的过滤器包含要执行的实际指令 - 在这种情况下,ElasticSearch查询用户名或当前地址20km距离内的所有内容 - 以及指向执行该实际URI的实际URI的链接查询。请注意,ElasticSearch代码实际上包含指向包含实际查询应使用的数据的资源的链接。当然,有可能返回一个包含实际用户数据的有效ElasticSearch查询,甚至可以返回JSON Pointer而不是URI到数据 - 这也是一些实现细节。

此方法允许在运行时动态添加新查询或更新现有查询,同时在查询时保持/users/{userId}/search/filters?page=pageNo语义不变。此外,还可以利用缓存功能,这可以显着提高性能 - 尤其是在用户数据不经常更改的情况下。

然而,这种方法的缺点是,您必须返回有关用户查找的更多数据。您还可以考虑不返回嵌入式过滤器,并让客户端明确地轮询这些过滤器。此外,当前过滤器由作为密钥的特定名称添加。在实践中,这可能会导致命名冲突。因此,最终UUID更好,但如果人类必须调用这些URI,也会带走语义,因为GET对人类的语义肯定比byName更多,但这更像是一个实现细节。