Monday, April 25, 2011

A quick journey through Grails searchable plugin

Grails searchable plugin is a great Compass based tool to implement search in Grails. As a part of my server inventory app, I used it to implement Ajax based search where search results update as you type. The relevant part of my domain model looks like this:

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)
}

2 comments:

  1. Great post! I have been an avid user of Searchable since Grails was at v0.8. But I have my reservations in using it these days. Your post motivated me to write my thoughts here - http://robjam.es/2011/04/whats-wrong-with-grails-searchable/

    ReplyDelete
  2. Hi Boris.

    It's a great post indeed. But I still have some problem with the sorting part. Mostly when I try to sort by a component field like "site". I created the exact same structure, but when I try to sort by the component I get the following error:
    ...
    field "$/Host/site/name" does not appear to be indexed
    ...

    Am I missing something here?

    I'm using the version 0.6.2 of searchable plug-in for grails.

    If it's not the right forum for this, please tell me where to put it.

    Thanks in advance for any help.

    Mateus.

    ReplyDelete