[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