class BaseDomain { Date dateCreated Date lastUpdated Boolean active = true } class Host extends BaseDomain { String ipAddress String hostName Site site } class Site extends BaseDomain { String name String description }
Objective: user searches for matching hosts by either IP address, host name, site name, as well as filter by active/inactive field.
Defining searchable fields.
class Host extends BaseDomain { static searchable = { only: ["ipAddress", "hostName", "site", "active"] ipAddress boost: 2.0 hostName boost: 2.0 site component: true } String ipAddress String hostName Site site } class Site extends BaseDomain { static searchable = { only: ["name", "active"] } String name String description }
"ipAddress" and "hostName" get a boost over "active" and Site.name. Site is defined as searchable component, meaning if a match is found on a site name, the Host that has a Site whose name matched the query is returned in the search result.
Working the query
User submitted query string should be a wildcard match on Host.ipAddress and Host.hostName fields, so if we have following data:
ip address | 127.0.0.1 |
host name | localhost |
active | true |
site name | yahoo |
queries like '127.0' or '0.0.1', 'local', 'calho' , 'yahoo', should all produce the above Host object as a match. Since there is only one search field, there is no way to know if what a user submitted is an ip address, a host name, or a site, therefore, all fields need to be checked. So, if a user submits "127.0", the query string for search should look like this:
ipAddress:*127.0* OR hostName:*127.0*
Since Site is a component, its matches are to be handled separately inside the search closure:
def wildcardQuery = "*" + params.query + "*" def searchQuery = "ipAddress:" + wildcardQuery + " OR hostName:" + wildcardQuery return Host.search({ must { queryString(searchQuery) wildcard('$/Host/site/name', wildcardQuery) } }, params)
Then we add search for only active Hosts unless 'inactive' flag is set:
return Host.search({ must { queryString(searchQuery) wildcard('$/Host/site/name', wildcardQuery) } if (!params.inactive) { must(term('$/Host/active', "true")) } }, params)
Now, let's add sort to the query. Technically, nothing needs to be added to the above snippet as long as there is a params.sort in the request. It works fine for fields like "ipAddress" and "hostName". However, if sorting by Site.name or 'active', actual "sort" request parameter must contain '$/Host/site/name' and '$/Host/active', which is not very convenient. It is more practical to have "site" and "active" as the value for params.sort, clone params into a separate map, and then overwrite sort param with the right value. This is how entire search closure looks:
def search = { def searchParams = [:] params.each { searchParams."$it.key" = it.value } switch (params.sort) { case "site" : searchParams.sort = '$/Host/site/name' break case "active" : searchParams.sort = '$/Host/active' } def wildcardQuery = "*" + searchParams.query + "*" def searchQuery = "ipAddress:" + wildcardQuery + " OR hostName:" + wildcardQuery return Host.search({ must { queryString(searchQuery) wildcard('$/Host/site/name', wildcardQuery) } if (!params.inactive) { must(term('$/Host/active', "true")) } }, searchParams) }