You’d think that documentation on creating a search engine for a Spring 3 MVC website would be plentiful- search boxes are on most sites these days, and it should be trivial to implement some type of search functionality in Spring MVC.  Unfortunately, there is very little direct documentation on the subject,  so I thought I’d write up what I did so others can benefit (and hopefully I hit enough keyword combos to have it be found).

Prerequisites:

  • You want to implement a search box that scans a given model looking for a term.
  • You have a decent grasp on java, Spring 3 and are using Hiberate Search 3.1.
  • You have all your classpaths loaded, and a functional, if not skeletal MVC site.

Step 1: Grok the basics of Hibernate Search

Start off by reading a little bit of the documentation for Hibernate Search, mainly “Example 1.5. Example entities after adding Hibernate Search annotations.” Go ahead and annotate your model as well as you can using their example as a guide- it doesn’t have to be perfect, just make sure you include a field you’d like to search.

Step 2: Add Hibernate Search Configuration

Next you have to add some hibernate properties. You can do this in your persistence.xml, hibernate properties, or in your *-servlet.xml where you define your Hibernate Session Factory (which is what I did). Here’s an example of the props I used- note that my indexBase is in Tomcat’s /tmp/ directory- that may not be the best place for it, but it works for the time being:

<bean id="mySessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
...
    <property name="hibernateProperties">
      <props>
        ...
        <prop key="hibernate.transaction.factory_class">org.hibernate.transaction.JDBCTransactionFactory</prop>
        <prop key="hibernate.search.default.directory_provider">org.hibernate.search.store.FSDirectoryProvider</prop>
        <prop key="hibernate.search.default.indexBase">${catalina.base}/tmp/indexes</prop>
      </props>
    </property>
</bean>

Step 3: Pass SessionFactory to Controller

After you add these properties, you’ll need to autowire your SessionFactory bean into your controller:

<bean id="searchController">
    <property name="sessionFactory" ref="mySessionFactory" />
</bean>

You’ll also need to add the following to your SearchController to get the sessionFactory autowired:

@Autowired
public SessionFactory getSessionFactory() {
    return sessionFactory;
}
public void setSessionFactory(SessionFactory pSessionFactory) {
    this.sessionFactory = pSessionFactory;
}

Step 4: Implement Search

Now for the final step- to implement the actual search.  I’ll be searching the title and content of my Snippet Class. In my SearchController I’ll add the following:

@RequestMapping
public ModelAndView quickSearch(@RequestParam("q") final String searchQuery) {
...
    //FIXME need to sanitize user input
    //FIXME make sure to handle Exceptions
    ModelAndView mav = new ModelAndView();

    //Open a Hibernate Session since none exist. This was the cause of much grief.
    Session session = sessionFactory.openSession();
    //Then open a Hibernate Search session
    FullTextSession fullTextSession = Search.getFullTextSession(session);
    //Begin your search transaction
    Transaction tx = fullTextSession.beginTransaction();

    //Create a list of fields you want to search
    String[] fields = new String[]{"title", "content", "lastModifiedDate"};
    //Define what type of parser and Analyzer you want to use
    MultiFieldQueryParser parser = new MultiFieldQueryParser(fields, new StandardAnalyzer());
    // Create a low-level Lucene Query from our parser using our searchQuery passed in from the user
    org.apache.lucene.search.Query query = parser.parse(searchQuery);
    // Run a Hibernate Search query using the lucene query against our Snippet class
    org.hibernate.Query hibQuery = fullTextSession.createFullTextQuery(query, Snippet.class);

    // List the results
    List results = hibQuery.list();
    // Commit the transaction
    tx.commit();
    // Close your hibernate session
    session.close();
    // pass your result set to your ModelAndView to be used.
    mav.addObject("results", results);

And just like that, a simple search in your Spring 3 MVC application using Hibernate Search.  I’m still a little fuzzy about why you need a transaction for a read-only search, but hey, it’s working, I can fine tune later. It goes without saying that there is no exception handling here- it’s not critical to the example.

The only downside to this implementation is that it doesn’t index existing content, only new content. You CAN do a full index by running:

    FullTextSession fullTextSession = Search.getFullTextSession(session);
    Transaction tx = fullTextSession.beginTransaction(); 
    List snippets = session.createQuery("from Snippet as snippet").list();
      for (Snippet snippet : snippets) {    
        fullTextSession.index(snippet);
      }
    tx.commit(); //index is written at commit time

But I’d suggest making an administrative tool/button/one-time-event out of it rather than implementing it here, because I sure there’s a slight performance hit in indexing thousands of lines of text.

I hope this ends up being of value to someone- if it does, please leave a comment letting me know, and if I have any mistakes above, please let me know that as well 😀