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:
Great tutorial. The "Will" search does not work because "will" is a stop word by default when using Compass Lucene standard analyzer.
Cheers,
Shay
@kimchy,
Thanks for your kind comment and explanation. That makes a lot of sense. BTW all kudos for developing a fantastic tool.
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.
Great tutorial.
The pagination of the search result doesn't work.
Any issue?
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
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
Post a Comment