Usually using POST
for search-operations is not really recommended as you lose all advantages GET
has to offer - semantics, idempotency, saftyness (cacheability), ...
Many RESTful and REST-like systems use simple GET
queries with search parameters as either query
or path
parameters to allow client- and server-based caching of queries and results. Since HTTP 1.1. caching of GET requests which contain query-parameters isn't an issue unless caching headers are specified correclty.
But predefined queries have a smell of LIKE
queries which you try to avoid. Especially ElasticSearch allows to add new fields to types dynamically. This might introduce new overhead to keep up with adding new predefined filters to support queries for these fields. Therefore, adding queries dynamically as needed is probably a base requirement on the long run. This isn't all to hard to achive though.
A sample output for a GET /users/12345
query which contains dynamically added search filters might therefore look like this:
{
"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"
}
}
}
},
{
...
}
]
}
}
}
The example-code above contains a user representation with embedded address and search filters in an extended JSON HAL format. As RESTful resources should be as self-explanatory as posible, the sample contains links to their location and to their schema so that post
and put
operations also know what fields the server might need.
The search
resource acts as a controller for filters in that it only allows to add new filters or delete all of them at once, while iterating through a filter page is achieved by invoking GET
on /users/{userId}/search/filters?page=pageNo
.
An actual filter now contains the actual instruction to execute - in this case an ElasticSearch query for either the name of the user or for everything in 20km distance of the current address - as well as a link to the actual URI which executes the query. Note that the ElasticSearch code actually contains a link to the resource containing the data the actual query should use. Of course it would be possible to return a valid ElasticSearch query containing the actual user data or even a JSON Pointer instead of URIs to the data as well - this is again some implementation detail.
This approach allows to add new queries or update existing queries at runtime dynamically while also keep the GET
semantics at query-time intact. Furthermore, cacheing capabilities can also be utilized which may improve performance significantly - especially if user data does not change often.
Drawback to this approach however is, that you have to return more data on user lookups. You can also consider not to return embedded filters and have a client poll these explicitely. Furthermore, currently filters are added by a certain name which acts as key. In practice this may leed to naming-clashes. Eventually UUIDs are better therefore but also take away semantics if humans have to invoke those URIs as byName
has certainly more semantic to a human than de305d54-75b4-431b-adb2-eb6b9e546014
but this is more of an implementation detail.