[jboss-cvs] jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/search ...
Christian Bauer
christian at hibernate.org
Tue Jun 12 08:29:59 EDT 2007
User: cbauer
Date: 07/06/12 08:29:59
Added: examples/wiki/src/main/org/jboss/seam/wiki/core/search
WikiSearchSupport.java WikiSearch.java
PaddedIntegerBridge.java SearchHit.java
PropertySearch.java IndexManager.java
Log:
Completed first iteration of search engine
Revision Changes Path
1.1 date: 2007/06/12 12:29:59; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/search/WikiSearchSupport.java
Index: WikiSearchSupport.java
===================================================================
package org.jboss.seam.wiki.core.search;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.wiki.core.model.Document;
import org.jboss.seam.wiki.core.model.Comment;
import org.jboss.seam.wiki.core.search.metamodel.SearchSupport;
import org.jboss.seam.wiki.core.search.metamodel.SearchableEntityHandler;
import org.jboss.seam.wiki.util.WikiUtil;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.highlight.*;
import java.util.Set;
import java.util.HashSet;
/**
* Handlers for searchable entities of the core domain model.
*
* @author Christian Bauer
*/
@Name("wikiSearchSupport")
public class WikiSearchSupport extends SearchSupport {
public Set<SearchableEntityHandler> getSearchableEntityHandlers() {
return new HashSet<SearchableEntityHandler>() {{
add(
new SearchableEntityHandler<Document>() {
public boolean isReadAccessChecked() {
return true;
}
public SearchHit extractHit(Query query, Document doc) throws Exception {
return new SearchHit(
Document.class.getSimpleName(),
"icon.doc.gif",
escapeBestFragments(query, new NullFragmenter(), doc.getName(), 0, 0),
WikiUtil.renderURL(doc),
escapeBestFragments(query, new SimpleFragmenter(100), doc.getContent(), 5, 350)
);
}
}
);
add(
new SearchableEntityHandler<Comment>() {
public SearchHit extractHit(Query query, Comment comment) throws Exception {
return new SearchHit(
Comment.class.getSimpleName(),
"icon.user.gif",
"(" + comment.getFromUserName() + ") "
+ escapeBestFragments(query, new NullFragmenter(), comment.getSubject(), 0, 0),
WikiUtil.renderURL(comment.getDocument())+ "#commentsDisplay",
escapeBestFragments(query, new SimpleFragmenter(100), comment.getText(), 5, 350)
);
}
}
);
}};
}
}
1.1 date: 2007/06/12 12:29:59; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/search/WikiSearch.java
Index: WikiSearch.java
===================================================================
package org.jboss.seam.wiki.core.search;
import org.apache.lucene.search.*;
import org.apache.lucene.search.Filter;
import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.hibernate.search.FullTextSession;
import org.hibernate.search.FullTextQuery;
import org.hibernate.search.bridge.StringBridge;
import org.jboss.seam.Component;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.*;
import org.jboss.seam.annotations.datamodel.DataModel;
import org.jboss.seam.log.Log;
import org.jboss.seam.wiki.core.search.annotations.SearchableType;
import org.jboss.seam.wiki.core.search.metamodel.SearchRegistry;
import org.jboss.seam.wiki.core.search.metamodel.SearchableEntity;
import org.jboss.seam.wiki.core.search.metamodel.SearchableProperty;
import javax.persistence.EntityManager;
import java.io.Serializable;
import java.util.*;
/**
* Core search engine, coordinates the search UI, query building, and hit extraction.
* <p>
* This controller is the backend for two different UIs: A simple query input field that
* is available on all pages, and the complete and complex search mask on the search page.
*
* @author Christian Bauer
*/
@Name("wikiSearch")
@Scope(ScopeType.CONVERSATION)
public class WikiSearch implements Serializable {
public static final String FIELD_READACCESSLVL = "readAccessLevel";
@Logger
static Log log;
@In
protected EntityManager restrictedEntityManager;
@In
private SearchRegistry searchRegistry;
// For UI binding to the global search field (and simplified search mask)
private String simpleQuery = "Search...";
private Boolean simpleQueryMatchExactPhrase;
public String getSimpleQuery() { return simpleQuery; }
public void setSimpleQuery(String simpleQuery) { this.simpleQuery = simpleQuery; }
public Boolean getSimpleQueryMatchExactPhrase() { return simpleQueryMatchExactPhrase; }
public void setSimpleQueryMatchExactPhrase(Boolean simpleQueryMatchExactPhrase) { this.simpleQueryMatchExactPhrase = simpleQueryMatchExactPhrase; }
/// For UI binding of the complex search mask (with expanded options)
private SearchableEntity selectedSearchableEntity;
public SearchableEntity getSelectedSearchableEntity() { return selectedSearchableEntity; }
public void setSelectedSearchableEntity(SearchableEntity selectedSearchableEntity) { this.selectedSearchableEntity = selectedSearchableEntity; }
private Map<SearchableEntity, List<PropertySearch>> searches = new HashMap<SearchableEntity, List<PropertySearch>>();
public Map<SearchableEntity, List<PropertySearch>> getSearches() { return searches; }
public void setSearches(Map<SearchableEntity, List<PropertySearch>> searches) { this.searches = searches; }
Set<SearchableEntity> searchEntities;
private int totalCount;
private int maxPageSize;
private int pageSize;
private int page;
@Create
public void create() {
// Initialize the value holders used for UI binding
for (SearchableEntity searchableEntity : searchRegistry.getSearchableEntities()) {
log.debug("preparing search value holder for entity: " + searchableEntity.getDescription());
List<PropertySearch> searchesForEntity = new ArrayList<PropertySearch>();
for (SearchableProperty prop : searchableEntity.getProperties()) {
log.debug("preparing search value holder for property: " + prop.getDescription());
searchesForEntity.add(new PropertySearch(prop));
}
searches.put(searchableEntity, searchesForEntity);
}
pageSize = 15;
maxPageSize = 100;
}
@DataModel
List<SearchHit> searchResult;
@Transactional
@Factory("searchResult")
@Begin(join = true)
public void search() {
page = 0;
searchEntities = new TreeSet<SearchableEntity>();
if (selectedSearchableEntity == null) {
// Nothing selected, do a global search on all entities that support phrases and
// use the simpleQuery as "include" search term for these phrases
log.debug("global search on all entities with phrase-type properties");
for (Map.Entry<SearchableEntity, List<PropertySearch>> entry : searches.entrySet()) {
for (PropertySearch propertySearch : entry.getValue()) {
if (SearchableType.PHRASE.equals(propertySearch.getProperty().getType())) {
propertySearch.getTerms().put(SearchableProperty.TERM_INCLUDE, getSimpleQuery());
propertySearch.getTerms().put(SearchableProperty.TERM_EXCLUDE, "");
propertySearch.getTerms().put(SearchableProperty.TERM_MATCHEXACTPHRASE, getSimpleQueryMatchExactPhrase());
searchEntities.add(entry.getKey());
}
}
}
} else {
// Form with search details selected and filled out
log.debug("searching only indexed entity: " + selectedSearchableEntity);
searchEntities.add(selectedSearchableEntity);
}
executeSearch(searchEntities);
}
private void executeSearch(Set<SearchableEntity> searchableEntities) {
log.debug("searching entities: " + searchableEntities.size());
BooleanQuery mainQuery = new BooleanQuery();
// Get value holders filled out by UI forms and generate a Lucene query
Class[] indexedEntities = new Class[searchableEntities.size()];
int i = 0;
for (SearchableEntity searchableEntity : searchableEntities) {
log.debug("building query for entity: " + searchableEntity.getClazz());
BooleanQuery entityQuery = new BooleanQuery();
// Add sub-queries for all entity properties
for (PropertySearch search : searches.get(searchableEntity)) {
log.debug("building query for property: " + search.getProperty());
Query query = search.getProperty().getQuery(search);
if (query != null) {
log.debug("adding query for property to owning entity: " + query.toString());
// If there is more than one searchable entity, use OR, otherwise combine properties with AND
entityQuery.add(
query,
searchableEntities.size() > 1 ? BooleanClause.Occur.SHOULD : BooleanClause.Occur.MUST
);
}
}
// Add to main query with or without access control filter wrapping
if (entityQuery.getClauses().length > 0 && searchableEntity.getHandler().isReadAccessChecked()) {
Integer currentAccessLevel = (Integer)Component.getInstance("currentAccessLevel");
StringBridge paddingBridge = new PaddedIntegerBridge();
Query accessLimitQuery =
new ConstantScoreRangeQuery(FIELD_READACCESSLVL, null, paddingBridge.objectToString(currentAccessLevel), true, true);
Filter accessFilter = new QueryFilter(accessLimitQuery);
FilteredQuery accessFilterQuery = new FilteredQuery(entityQuery, accessFilter);
mainQuery.add(accessFilterQuery, BooleanClause.Occur.SHOULD);
} else if (entityQuery.getClauses().length > 0) {
mainQuery.add(entityQuery, BooleanClause.Occur.SHOULD);
}
indexedEntities[i++] = searchableEntity.getClazz();
}
log.debug("search query: " + mainQuery.toString());
try {
FullTextSession ftSession = org.hibernate.search.Search.createFullTextSession(getSession());
FullTextQuery ftQuery = ftSession.createFullTextQuery(mainQuery, indexedEntities);
ftQuery.setFirstResult(page * pageSize).setMaxResults(pageSize);
totalCount = ftQuery.getResultSize();
log.debug("total search hits (might be paginated next): " + totalCount);
List result = ftQuery.list();
// Extract hits
log.debug("search hits passed to handlers: " + result.size());
searchResult = new ArrayList<SearchHit>();
for (Object o : result) {
SearchableEntity se = searchRegistry.getSearchableEntitiesByName().get(Hibernate.getClass(o).getName());
if (se != null) {
log.debug("extracting hit for indexed class: " + Hibernate.getClass(o).getName());
//noinspection unchecked
searchResult.add( se.getHandler().extractHit(mainQuery, o) );
}
}
log.debug("extracted search hits and final result: " + searchResult.size());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private Session getSession() {
restrictedEntityManager.joinTransaction();
return ((Session) ((org.jboss.seam.persistence.EntityManagerProxy) restrictedEntityManager).getDelegate());
}
public void nextPage() {
page++;
executeSearch(searchEntities);
}
public void previousPage() {
page--;
executeSearch(searchEntities);
}
public void firstPage() {
page = 0;
executeSearch(searchEntities);
}
public void lastPage() {
page = (totalCount / pageSize);
if (totalCount % pageSize == 0) page--;
executeSearch(searchEntities);
}
public boolean isNextPageAvailable() {
return totalCount > ((page * pageSize) + pageSize);
}
public boolean isPreviousPageAvailable() {
return page > 0;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize > maxPageSize ? maxPageSize : pageSize; // Prevent tampering
}
public long getFirstRow() {
return page * pageSize + 1;
}
public long getLastRow() {
return (page * pageSize + pageSize) > totalCount
? totalCount
: page * pageSize + pageSize;
}
public int getTotalCount() {
return totalCount;
}
}
1.1 date: 2007/06/12 12:29:59; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/search/PaddedIntegerBridge.java
Index: PaddedIntegerBridge.java
===================================================================
package org.jboss.seam.wiki.core.search;
import org.hibernate.search.bridge.StringBridge;
/**
* Padding Integer bridge.
* All numbers will be padded with 0 to match 5 digits
*
* @author Emmanuel Bernard
*/
public class PaddedIntegerBridge implements StringBridge {
private int PADDING = 5;
public String objectToString(Object object) {
String rawInteger = ( (Integer) object ).toString();
if (rawInteger.length() > PADDING)
throw new IllegalArgumentException( "Try to pad on a number too big" );
StringBuilder paddedInteger = new StringBuilder( );
for ( int padIndex = rawInteger.length() ; padIndex < PADDING ; padIndex++ ) {
paddedInteger.append('0');
}
return paddedInteger.append( rawInteger ).toString();
}
}
1.1 date: 2007/06/12 12:29:59; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/search/SearchHit.java
Index: SearchHit.java
===================================================================
package org.jboss.seam.wiki.core.search;
/**
* Represents a single search result, used for rendering a hit in the UI.
* <p>
* This is a value holder that is build by the search engine and rendered by the
* search user interface. <b>Important:</b> The title and fragment is rendered
* <i>as is</i>, with no escaping of dangerous HTML! This is required because the
* fragments might contain HTML markup that represents the hit highlights.
* You need to absolutely make sure that these values do not contain any Javascript
* or your site will be open for XSS attacks. Use <tt>WikiUtil.escapeHtml(s)</tt>
* as a helper method.
*
* @author Christian Bauer
*/
public class SearchHit {
public String type;
public String icon;
public String title;
public String link;
public String fragment;
public SearchHit() {}
public SearchHit(String type, String icon, String title, String link, String fragment) {
this.type = type;
this.icon = icon;
this.title = title;
this.link = link;
this.fragment = fragment;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getIcon() {
return icon;
}
public void setIcon(String icon) {
this.icon = icon;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getLink() {
return link;
}
public void setLink(String link) {
this.link = link;
}
public String getFragment() {
return fragment;
}
public void setFragment(String fragment) {
this.fragment = fragment;
}
}
1.1 date: 2007/06/12 12:29:59; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/search/PropertySearch.java
Index: PropertySearch.java
===================================================================
package org.jboss.seam.wiki.core.search;
import org.jboss.seam.wiki.core.search.metamodel.SearchableProperty;
import java.util.Map;
import java.util.HashMap;
/**
* A value holder for UI binding.
* <p>
* Bound to the dynamic search mask user interface and used to transport user input values
* into the search engine backend.
*
* @author Christian Bauer
*/
public class PropertySearch {
private Map<String, Object> terms = new HashMap<String, Object>();
private SearchableProperty property;
public PropertySearch(SearchableProperty property) {
this.property = property;
}
public Map<String, Object> getTerms() {
return terms;
}
public void setTerms(Map<String, Object> terms) {
this.terms = terms;
}
public SearchableProperty getProperty() {
return property;
}
}
1.1 date: 2007/06/12 12:29:59; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/search/IndexManager.java
Index: IndexManager.java
===================================================================
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.Session;
import org.hibernate.search.FullTextSession;
import org.hibernate.search.Search;
import org.hibernate.search.store.DirectoryProvider;
import org.hibernate.search.util.ContextHelper;
import org.jboss.seam.Component;
import org.jboss.seam.annotations.Asynchronous;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.log.Log;
import org.jboss.seam.wiki.util.Progress;
import javax.persistence.EntityManager;
import javax.transaction.UserTransaction;
/**
* 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 = 50;
/**
* 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");
userTx = (UserTransaction)org.jboss.seam.Component.getInstance("org.jboss.seam.transaction.transaction");
userTx.begin();
EntityManager em = (EntityManager) Component.getInstance("entityManager");
Session session = (Session) em.getDelegate();
// Delete all documents with "_hibernate_class" term of the selected entity
DirectoryProvider dirProvider = ContextHelper.getSearchFactory(session).getDirectoryProvider(entityClass);
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)");
Search.createFullTextSession(session).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");
session = (Session) em.getDelegate();
FullTextSession ftSession = org.hibernate.search.Search.createFullTextSession(session);
userTx.begin();
// Use HQL instead of Criteria to eager fetch lazy properties
ScrollableResults cursor = session.createQuery("select o from " + entityClass.getName() + " o fetch all properties").scroll();
cursor.last();
int count = cursor.getRowNumber() + 1;
log.debug("total documents in database: " + count);
cursor.first(); // Reset to first result row
int i = 0;
while (true) {
i++;
Object o = cursor.get(0);
log.debug("indexing: " + o);
ftSession.index(o);
if (i % batchSize == 0) session.clear(); // Clear persistence context for each batch
progress.setPercentComplete( (100/count) * i);
log.debug("percent of index update complete: " + 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);
}
}
}
More information about the jboss-cvs-commits
mailing list