Monday, 15 December 2008

grails: Implementing Search [2]

In a previous post, I described a quick start approach to using the Searchable Plugin. This approach relied on the controller and view supplied with the plugin and is useful for prototyping and testing. However it's not the ideal way to implement search using the plugin.

In this post, I demonstrate how you can customise the Searchable plugin by developing your own controller and views.

The Searchable plugin provides us with 2 distinct ways to search the properties of domain classes. The first of these is the dynamic 'search' method

DomainClass.search(String query)



which is attached to Domain classes that have the static property 'Searchable' defined within them. This restricts the search to only the properties of the Domain Class.

Secondly the plugin provides the SearchableService class,

searchableService.search(String query)



which enables us to search properties across a set of Domain classes. The documentation here provides further details for both approaches.

Step 1-3: Setup

Repeat Steps 1,2 & 3 from the previous post.

I'm using a different example for this post to better demonstrate the similarities and differences between the 2 search mechanisms. This example consists of 2 domain classes listed below and whose purpose is self-explanatory,


class Film{
    String actor

String title

String releaseDate

static Searchable = true

static constraints = {
actor()
title()
releaseDate()
}

}




and,


class Music{
    String artist

String title

String releaseDate


static Searchable = true

static constraints = {
artist()
title()
releaseDate()
}


}



I've implemented step 3 slightly differently. Instead of 1 text-field, we now have the following,

<g:form url='[controller: "customSearch", action: "searchMusic"]' id="searchMusic" name="searchMusic" method="get">
<g:textField name="query" value="${params.query}" size="10"/> <input type="submit" value="Search Music" />
</g:form>

<g:form url='[controller: "customSearch", action: "searchFilm"]' id="searchFilm" name="searchFilm" method="get">
<g:textField name="query" value="${params.query}" size="10"/> <input type="submit" value="Search Film" />
</g:form>


<g:form url='[controller: "customSearch", action: "searchAll"]' id="searchAll" name="searchAll" method="get">
<g:textField name="query" value="${params.query}" size="10"/> <input type="submit" value="Search All" />
</g:form>


Which look like,



These text-fields, allow the User to search individually for Music or Film or indeed collectively across both categories.

Step 4: Implement the Controller

Next, we build our custom search controller, containing actions that will service the text-fields defined in the forms above.


import org.compass.core.engine.SearchEngineQueryParseException

class CustomSearchController{

def searchableService

def searchMusic = {

if(params.query?.trim())
{

def searchResults = Music.search(params.query)

def total = searchResults.total

def list = searchResults.results

return [totalResults: total, list:list, type:"music"]
}
else
{
return [:]
}

}


def searchFilm = {

if(params.query?.trim())
{

def searchResults = Film.search(params.query)

def total = searchResults.total

def list = searchResults.results

return [totalResults:total, list:list, type:"film"]
}
else
{
return [:]
}

}


def searchAll = {

if(!params.max) params.max = 5
if(!params.offset) params.offset = 0

if(params.query?.trim())
{
try
{

def searchResult = searchableService.search(params.query, params)

def total = searchResult.total

def list = searchResult.results

return [totalResults:total, list:list, type:"all"]
}
catch (SearchEngineQueryParseException ex)
{
return [parseException: true]
}
}
else
{
return [:]
}

}

}



At the top of the code, we inject in the plugins' SearchService class. Next we declare the actions required to service the search textfields defined in step 3. The first 2 actions implement search using the dynamic method, whereas the 'searchAll' action uses the SearchableService class mechanism. Both approaches return a 'searchResult' object which contains a number of results data documented here. For our purposes, we're only interested in the search results and their count.

I'm not going to list the view pages here as they're typical gsp pages and don't add any value to the current discussion. However if you're interested, the full code is available [compass2.zip] for download here.

The following images show the results from performing individual and collective searches.

This one shows a 'Music' only search, when I enter a value of 'Will Smith' into the 'search Music' textfield.




This one shows a 'Film' only search, when I enter a value of 'Will Smith' into the 'search Film' textfield.



And finally a collective search, where I enter the value 'Will Smith' into the 'searchAll' text-field.



As expected the plugin returns Music and Films associated with the artist/actor, Will Smith.

The complete code for this example [compass2.zip] is available here.

ps - I noticed something peculiar with Searchable plugin, when using the test data that I generate in the bootstrap class. If I perform a query on 'Will', it returns nothing. Yet if I enter a value of 'Will Smith', then the search returns the correct results. However if I repeat this for 'Mark' and 'Mark Wahlberg', I get identical results, i.e. I get back, Music/Film or both depending on the search type.

6 comments:

kimchy said...

Great tutorial. The "Will" search does not work because "will" is a stop word by default when using Compass Lucene standard analyzer.

Cheers,
Shay

mo said...

@kimchy,
Thanks for your kind comment and explanation. That makes a lot of sense. BTW all kudos for developing a fantastic tool.

Cuneyt Uysal said...

Again, good explanation. Where does one learn more about the searchResult object and the availible methods/properties? I'm trying to display the title of the of the individual result in my View, but can't simply reference it as ${title}. Cheers.

Francesco said...

Great tutorial.
The pagination of the search result doesn't work.
Any issue?

Surya said...

Thanks for writing two great pieces on using the searchable plugin. Have you had any issues with deploying this plugin with Tomcat. For me Tomcat wont start. I see the following errors in the log file. Any help would be greatly appreciated. I would really like to use this plugin, but I can not until I resolve this tomcat error.

SEVERE: Exception sending context initialized event to listener instance of class org.codehaus.groovy.grails.web.context.GrailsContextLoaderListener

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'compassGps': Cannot resolve reference to bean 'compass' while setting bean property 'compass'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'compass': FactoryBean threw exception on object creation; nested exception is java.lang.NoClassDefFoundError: org/codehaus/groovy/grails/plugins/searchable/compass/mapping/DefaultSearchableCompassClassMappingXmlBuilder$_buildClassMappingXml_closure1_closure4_closure12_closure13

suurya said...

Great tutorial. For all search text fields, I need to have only one search button, How can I achieve that. Please let me know. Thanks
Surrya