Wednesday, March 12, 2008

Search with Spring Hibernate Lucene and Aspect Oriented Programming in action

I've read a lot about AOP and what it's used for. It is useful for logging, tracing, declarative transactions management and even caching. Great ! I'm going to share with you yet another AOP use case. BTW, I found it really interesting. So, the story.

I have some entity with with field "content" which is HTML text. The number of entries is large and keep growing. It is natural that I want to find some entry which contain some text. So, I need search. Since text is in HTML format, database full text search is not the best solution possible. I've decided to try Lucene. It is a "high-performance, full-featured text search engine library". To use Lucene I need to crate an index, add my objects into it and then I will able to query index to get search results.

Since I'm using Spring, I was looking for Lucene-Spring integration. Bingo ! It is available as one of Spring modules project. This integration has two aspects - one for index management and another for performing actual search. Complete configuration :

<bean id="fsDirectory"
class="org.springmodules.lucene.index.support.FSDirectoryFactoryBean">
<property name="create" value="true"/>
<property name="location" value="file:///C:/temp/index" />
</bean>

<alias name="fsDirectory" alias="indexDirectory"/>

<bean
id="indexFactory"
class="org.springmodules.lucene.index.support.SimpleIndexFactoryBean">
<property name="create" value="true"/>
<property name="directory">
<ref bean="indexDirectory" />
</property>
<property name="analyzer">
<bean class="org.apache.lucene.analysis.standard.StandardAnalyzer"/>
</property>
</bean>

<bean id="indexAccessor"
class="com.mycoolcompany.app.search.LuceneIndexerService">
<property name="indexFactory">
<ref local="indexFactory" />
</property>
</bean>

<bean id="searcherFactory"
class="org.springmodules.lucene.search.factory.SimpleSearcherFactory">
<property name="directory">
<ref bean="indexDirectory" />
</property>
</bean>

<bean id="searchService"
class="com.mycoolcompany.app.search.LuceneSearchService">
<property name="searcherFactory">
<ref local="searcherFactory" />
</property>
<property name="analyzer">
<bean class="org.apache.lucene.analysis.standard.StandardAnalyzer" />
</property>
</bean>

I can pre-populate index by querying database and adding objects to index. But what to do when new entry has been added ? How to keep index up to date ? After some Googling I found solution with custom hibernate interceptor. It looks like this :

import java.io.Serializable;
import org.hibernate.EmptyInterceptor;
import org.hibernate.type.Type;

public class CustomHibernateInterceptor extends EmptyInterceptor
{
private static final long serialVersionUID = -1214372449390884955L;

//injected by spring
private LuceneIndexerService indexAccessor;
public LuceneIndexerService getIndexAccessor() {
return indexAccessor;
}
public void setIndexAccessor(LuceneIndexerService indexAccessor) {
this.indexAccessor = indexAccessor;
}

public boolean onSave(
Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types)
{
if (entity instanceof Entry)
{
//adding to index
indexAccessor.add((Entry)entity);
}
return super.onSave(entity, id, state, propertyNames, types);
}
}
Is it something wrong with this solution ? I guess no, it's working. But I have and idea to use AOP here. I guess, adding to index and persisting object to database at the same time is some king of cross-cutting concern. I'm using a Spring-Hibernate DAO for CRUD operations. My idea to resolve this issue is in using aspect. Aspect has a pointcut defined to intercept execution of my dao method addEntry(Entry entry) and "after" advice - this is the place where we can add object to index, after object has been persisted. Please notice that search service is injected into aspect :) All this stuff is very abstract but is pretty interesting to digg. Here is the source code of aspect :
@Aspect
public class IndexForSearchAspect
{
public IndexForSearchAspect()
{
super();
}
//will be injected
private LuceneIndexerService indexAccessor;
public void setIndexAccessor(LuceneIndexerService indexAccessor)
{
this.indexAccessor = indexAccessor;
}
//defining pointcut
@Pointcut("execution (* ....dao.BlogEntryDao.addEntry(....model.Entry))")
public void saveEntryToDB() {}

//add object to index after it has been persisted
@After("saveEntryToDB()")
public void addEntryToLuceneIndex(JoinPoint joinpoint)
{
//getting argument of dao.addEntry()
Entry entry = (Entry) joinpoint.getArgs()[0];
// adding to index
indexAccessor.add(entry);
}
}
Spring configuration for aspect :

<aop:aspectj-autoproxy/>
<bean id="searchindexAspect"
class="com.mycoolcompany.app.search.IndexForSearchAspect">
<!--indexer service will be injected -->
<property name="indexAccessor">
<ref local="indexAccessor"/>
</property>
</bean>

I know, I've missed a lot of details here, so this post is something like show-case, not cookbook :) Any opinions are welcome !

5 comments:

appu said...

Hi,

I have to devolop e web based search utility using spring hibernate and lucene.We have used hibernate for indexing and searching.But the index files are created on filesystem.How can other people access the index or query if the index is stored on FileSystem.I mean from clients machine.

Thanks and Regards

Anonymous said...

Can you please post the details of your Indexer and Search service classes?

Stas Ostapenko said...

Hi folks ! Give me some time and I'll create opensource project with all this suff on googlecode.

Emmanuel Bernard said...

You might want to look at Hibernate Search which fulfill the feature you are describing
Note that using a pointcut on your DAO is not good enough generally speaking. Objects can be updated by cascade.

Stas Ostapenko said...

Hi Emmanuel !

Thanks for comment ! I didn't think about cascading at all, since my case is very simple. But idea you mentioned is very interesting for me.