package org.jboss.seam.wiki.core.search; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.Term; import org.hibernate.ScrollableResults; import org.hibernate.search.FullTextSession; import org.hibernate.search.store.DirectoryProvider; import org.jboss.seam.Component; import org.jboss.seam.annotations.Logger; import org.jboss.seam.annotations.Name; import org.jboss.seam.annotations.async.Asynchronous; import org.jboss.seam.log.Log; import org.jboss.seam.wiki.util.Progress; import org.jboss.seam.wiki.core.model.WikiNode; import javax.persistence.EntityManager; import javax.transaction.UserTransaction; import java.math.BigDecimal; /** * Management the Lucene index. * * @author Christian Bauer */ @Name("indexManager") public class IndexManager { @Logger static Log log; // TODO: Read the Hibernate Seach configuration option instead, when it becomes available as an API public int batchSize = 500; /** * Runs asynchronously and re-indexes the given entity class after purging the index. * * @param entityClass the class to purge and re-index * @param progress a value holder that is continously updated while the asynchronous procedure runs */ @Asynchronous public void rebuildIndex(Class entityClass, Progress progress) { log.info("asynchronously rebuilding Lucene index for entity: " + entityClass); UserTransaction userTx = null; try { progress.setStatus("Purging index"); log.debug("deleting indexed documents of entity: " + entityClass.getName()); userTx = (UserTransaction)org.jboss.seam.Component.getInstance("org.jboss.seam.transaction.transaction"); userTx.begin(); EntityManager em = (EntityManager) Component.getInstance("entityManager"); FullTextSession ftSession = (FullTextSession)em.getDelegate(); // Delete all documents with "_hibernate_class" term of the selected entity DirectoryProvider dirProvider = ftSession.getSearchFactory().getDirectoryProviders(entityClass)[0]; IndexReader reader = IndexReader.open(dirProvider.getDirectory()); // TODO: This is using an internal term of HSearch reader.deleteDocuments(new Term("_hibernate_class", entityClass.getName())); reader.close(); // Optimize index progress.setStatus("Optimizing index"); log.debug("optimizing index (merging segments)"); ftSession.getSearchFactory().optimize(entityClass); userTx.commit(); progress.setStatus("Building index"); log.debug("indexing documents in batches of: " + batchSize); // Now re-index with HSearch em = (EntityManager) Component.getInstance("entityManager"); ftSession = (FullTextSession)em.getDelegate(); // TODO: Let's run this in auto-commit mode, assuming we have READ COMMITTED isolation anyway and non-repeatable reads //userTx.setTransactionTimeout(3600); //userTx.begin(); // Use HQL instead of Criteria to eager fetch lazy properties String query = "select o from " + entityClass.getName() + " o fetch all properties"; if (WikiNode.class.isAssignableFrom(entityClass)) { // If it's a WikiNode, fetch the associated User instances, avoiding N+1 selects query = "select o from " + entityClass.getName() + " o inner join fetch o.createdBy left join fetch o.lastModifiedBy fetch all properties"; } ScrollableResults cursor = ftSession.createQuery(query).scroll(); cursor.last(); int count = cursor.getRowNumber() + 1; log.debug("total documents in database: " + count); if (count > 0) { cursor.first(); // Reset to first result row int i = 0; while (true) { i++; Object o = cursor.get(0); log.debug("indexing instance " + i + ": " + o); ftSession.index(o); if (i % batchSize == 0) { log.debug("ending batch, beginning new batch"); ftSession.clear(); // Clear persistence context for each batch } progress.setPercentComplete( new Double(100d/count*i).intValue() ); log.debug(progress); if (cursor.isLast()) break; else cursor.next(); } } cursor.close(); //userTx.commit(); progress.setStatus(Progress.COMPLETE); log.debug("indexing complete of entity class: " + entityClass); } catch (Exception ex) { /* try { if (userTx != null) userTx.rollback(); } catch (Exception rbEx) { rbEx.printStackTrace(); } */ throw new RuntimeException(ex); } } }