compass指南針搜索框架學習(二)

一、準備

    個人在學習中採用Struts2 + Hibernate3.2 + Spring2.5 + Compass2.2.0, 一下圖片爲本次學習中用到的jar包:

 

圖中圈出的jar包爲本次學習的主要部分,另外用綠色框圈出的jar包爲分詞器,主要用來做實驗看分詞效果的,選用一個即可。

二、什麼是Compass

    Compass是一個Java搜索框架。它封裝了Lucene,增加了一些Lucene不支持的特性(例如實時更新索引),支持各種數據(Java對象、xml、json)到索引的映射,支持各種數據源(JDBC, Hibernate, iBatis)

 

 

圖解:

  • Compass - 一般在程序啓動時建立並被整個程序共享,主要用於建立CompassSession並通過其管理索引數據。與Hibernate的SessionFactory類似
  • CompassSession - 用於處理數據的session。與Hibernate的Session類似
  • CompassTransaction - 手動進行事務管理,如果不使用,Compass會自動管理事務。與Hibenate的事物管理類似
  • CompassTemplate - 將session和transaction透明化。類似Spring提供的HibernateTemplate
  • 數據到索引的各種映射 - OSEM, XSEM, JSEM, RSEM。支持通過程序、XML、JSON進行配置。
  • CompassGps - Gps的核心模塊,管理GpsDevice,有兩種實現:SingleCompassGps和DualCompassGps。
  • CompassGpsDevice - 處理各種數據源到索引的操作:JDBC, Hibernate, iBatis等。不能獨立使用而必須融合到CompassGps中。

三、與Spring、Hibernate整合

 

     這裏主要結合代碼進行。

    

     1.數據庫腳本(Oracle)

[java] view plaincopy
  1. --創建表Article  
  2. create table ARTICLE  
  3. (  
  4.   ID      NUMBER,--ID,主鍵  
  5.   TITLE   VARCHAR2(100),--標題  
  6.   CONTENT CLOB,--文章內容  
  7.   PUBDATE DATE--發佈日期  
  8. )  

   

    2.配置Compass的OSEM 以及Hibernate映射

[java] view plaincopy
  1. import java.io.Serializable;  
  2. import java.util.Date;  
  3.   
  4. import javax.persistence.Column;  
  5. import javax.persistence.Entity;  
  6. import javax.persistence.GeneratedValue;  
  7. import javax.persistence.Id;  
  8. import javax.persistence.Lob;  
  9. import javax.persistence.Table;  
  10. import javax.persistence.Temporal;  
  11. import javax.persistence.TemporalType;  
  12.   
  13. import org.compass.annotations.Index;  
  14. import org.compass.annotations.Searchable;  
  15. import org.compass.annotations.SearchableId;  
  16. import org.compass.annotations.SearchableProperty;  
  17. import org.compass.annotations.Store;  
  18. import org.hibernate.annotations.GenericGenerator;  
  19.   
  20.   
  21. @Searchable(alias = "article")  
  22. @Entity  
  23. @Table(name = "ARTICLE", schema = "SCOTT")  
  24. public class Article implements Serializable {  
  25.   
  26.     private static final long serialVersionUID = 1L;  
  27.   
  28.     private Long id;  
  29.     private String title;  
  30.     private Date pubdate = new Date();  
  31.     private String content;  
  32.   
  33.     @SearchableId(  
  34.             name = "id",   
  35.             store = Store.NO,   
  36.             index = Index.NOT_ANALYZED)  
  37.     @Id  
  38.     @GeneratedValue(generator = "paymentableGenerator")  
  39.     @GenericGenerator(name = "paymentableGenerator", strategy = "increment")  
  40.     public Long getId() {  
  41.         return id;  
  42.     }  
  43.   
  44.     public void setId(Long id) {  
  45.         this.id = id;  
  46.     }  
  47.   
  48.     @SearchableProperty(  
  49.             name = "title",   
  50.             store = Store.YES,   
  51.             index = Index.ANALYZED)  
  52.     @Column(name = "TITLE")  
  53.     public String getTitle() {  
  54.         return title;  
  55.     }  
  56.   
  57.     public void setTitle(String title) {  
  58.         this.title = title;  
  59.     }  
  60.   
  61.     @SearchableProperty(  
  62.             name = "pubdate",   
  63.             store = Store.NO,   
  64.             index = Index.UN_TOKENIZED)  
  65.     @Temporal(TemporalType.TIMESTAMP)  
  66.     @Column(name = "PUBDATE")  
  67.     public Date getPubdate() {  
  68.         return pubdate;  
  69.     }  
  70.   
  71.     public void setPubdate(Date pubdate) {  
  72.         this.pubdate = pubdate;  
  73.     }  
  74.   
  75.     @SearchableProperty(  
  76.             name = "content",   
  77.             index = Index.TOKENIZED,   
  78.             store = Store.YES,   
  79.             converter = "htmlPropertyConverter")  
  80.     @Lob  
  81.     @Column(name = "CONTENT")  
  82.     public String getContent() {  
  83.         return content;  
  84.     }  
  85.   
  86.     public void setContent(String content) {  
  87.         this.content = content;  
  88.     }  
  89.   
  90. }  

    說明:

    @Searchable(alias="article")表示這個是可以搜索實體,別名爲article. 
    @SearchableId  這個是實體搜索的標識ID,和hibernate裏的概念差不多,用來區分索引文件裏的實體索引。 
    @SearchableProperty(index = Index.NOT_ANALYZED, store = Store.NO) 表示這個屬性存入索引文件,不進行分詞,不存儲要索引中。

    另外在getContent()方法上的@SearchableProperty中還加入了converter = "htmlPropertyConverter",主要是用來將文章中的HTML標籤進行過濾獲取純文本,在建立到索引中。在後面會具體介紹這個轉換器。

 

    3.建立Compass搜索的類

[java] view plaincopy
  1. import java.util.ArrayList;  
  2. import java.util.List;  
  3.   
  4. import javax.annotation.Resource;  
  5.   
  6. import org.compass.core.Compass;  
  7. import org.compass.core.CompassHighlighter;  
  8. import org.compass.core.CompassHits;  
  9. import org.compass.core.CompassQuery;  
  10. import org.compass.core.CompassQueryBuilder;  
  11. import org.compass.core.CompassSession;  
  12. import org.compass.core.CompassTemplate;  
  13. import org.compass.core.CompassHighlighter.TextTokenizer;  
  14. import org.compass.core.CompassQuery.SortPropertyType;  
  15. import org.springframework.stereotype.Component;  
  16.   
  17. import com.compass.example.dao.SearchArticleDao;  
  18. import com.compass.example.model.Article;  
  19.   
  20. @Component("SearchArticleDao")  
  21. public class SearchArticleDaoCompass implements SearchArticleDao {  
  22.       
  23.     @Resource  
  24.     private CompassTemplate compassTemplate;  
  25.       
  26.     @Override  
  27.     public List<Article> searchWithList(final String queryString) {  
  28.           
  29.         Compass compass = compassTemplate.getCompass();  
  30.         CompassSession session = compass.openSession();  
  31.         CompassQueryBuilder builder = session.queryBuilder();  
  32.         CompassQuery compassQuery = builder.queryString(queryString).toQuery().addSort("article.id",SortPropertyType.STRING);  
  33.         CompassHits compassHits = compassQuery.hits();  
  34.           
  35.           
  36.         List<Article> articles = new ArrayList<Article>();  
  37.         for(int i=0; i<compassHits.length(); i++) {  
  38.             Article article = (Article) compassHits.data(i);  
  39.             CompassHighlighter highlighter = compassHits.highlighter(i);  
  40.             String title = highlighter.fragment("title");  
  41.             if(title != null) {  
  42.                 article.setTitle(title);  
  43.             }  
  44.             String content = highlighter.setTextTokenizer(TextTokenizer.AUTO).fragment("content");  
  45.             if(content != null) {  
  46.                 article.setContent(content);  
  47.             }  
  48.             articles.add(article);  
  49.         }  
  50.         return articles;  
  51.     }  
  52.   
  53. }  

索引的查詢主要是根據傳過來的參數,關鍵字keyword,是搜索的關鍵字

String title = hits.highlighter(i).fragment("title");這段是檢索titile這個屬性有沒有出現搜索的關鍵字,有就將它高亮(其實就是在關鍵字前後加個html標記設置顏色,等下可以看到在配置文件裏可以自由設置高亮的顏色). 
String content = hits.highlighter(i).setTextTokenizer( 
CompassHighlighter.TextTokenizer.AUTO) 
.fragment("content"); 
這段代碼和上面的title具有一樣的一樣的功能,另外還多了個很重要的功能,自動選擇正文中最匹配關鍵字的內容中的一部分輸出。因爲很多時候一篇文章幾千字,我們只想顯示有關鍵字的那部分的摘要,這時候這個功能就很方便.

 

    4.建立索引,將在服務器啓動時或定時重建索引

[java] view plaincopy
  1. import org.compass.gps.CompassGps;  
  2. import org.springframework.beans.factory.InitializingBean;  
  3.   
  4. /** 
  5.  * 通過quertz 定時調度定時重建索引或自動隨Spring ApplicationContext 啓動而重建 
  6.  * 索引的Builder。會在啓動後延時數秒新開線程調用compassGps.index()函數。 
  7.  * 默認會在Web應用每次啓動時重建索引,可以設置BuildIndex屬性爲false來禁止此功能。 
  8.  * 也可以不用本builder,編寫手動調用compasssGps.index()的代碼。 
  9.  * @author YinGuojun 
  10.  * 
  11.  */  
  12. public class CompassIndexBuilder implements InitializingBean {  
  13.   
  14.     /*是否需要建立索引,可以被設置爲false使本Builder失效*/  
  15.     private boolean buildIndex = false;  
  16.     /*索引操作線程延時啓動的時間,單位爲秒*/  
  17.     private int lazyTime = 10;  
  18.     /*Compass封裝*/  
  19.     private CompassGps compassGps;  
  20.       
  21.     private Thread indexThread = new Thread() {  
  22.   
  23.         @Override  
  24.         public void run() {  
  25.             try {  
  26.                 System.out.println("lazyTime: " + lazyTime);  
  27.                 Thread.sleep(lazyTime * 1000);  
  28.                 System.out.println("begin compass index ...");  
  29.                 long beginTime = System.currentTimeMillis();  
  30.                   
  31.                 // 重建索引.  
  32.                 // 如果compass實體中定義的索引文件已存在,索引過程中會建立臨時索引,  
  33.                 // 索引完成後再進行覆蓋.  
  34.                 compassGps.index();  
  35.                   
  36.                 long costTime = System.currentTimeMillis() - beginTime;  
  37.                 System.out.println("compss index finished.");  
  38.                 System.out.println("costed " + costTime + " milliseconds");  
  39.                   
  40.             } catch (InterruptedException e) {  
  41.                 e.printStackTrace();  
  42.             }  
  43.               
  44.         }  
  45.           
  46.     };  
  47.       
  48.     /** 
  49.      * 實現<code>InitializingBean</code>接口,在完成注入後調用啓動索引線程. 
  50.      * 
  51.      * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 
  52.      */  
  53.     @Override  
  54.     public void afterPropertiesSet() throws Exception {  
  55.         if (buildIndex) {  
  56.             indexThread.setDaemon(true);  
  57.             indexThread.setName("Compass Indexer");  
  58.             indexThread.start();  
  59.         }  
  60.     }  
  61.   
  62.     public boolean isBuildIndex() {  
  63.         return buildIndex;  
  64.     }  
  65.   
  66.     public void setBuildIndex(boolean buildIndex) {  
  67.         this.buildIndex = buildIndex;  
  68.     }  
  69.   
  70.     public int getLazyTime() {  
  71.         return lazyTime;  
  72.     }  
  73.   
  74.     public void setLazyTime(int lazyTime) {  
  75.         this.lazyTime = lazyTime;  
  76.     }  
  77.   
  78.     public CompassGps getCompassGps() {  
  79.         return compassGps;  
  80.     }  
  81.   
  82.     public void setCompassGps(CompassGps compassGps) {  
  83.         this.compassGps = compassGps;  
  84.     }  
  85.   
  86. }  

  

   5.轉換器

  

[java] view plaincopy
  1. import org.apache.log4j.Logger;  
  2. import org.compass.core.Property;  
  3. import org.compass.core.converter.ConversionException;  
  4. import org.compass.core.converter.basic.AbstractBasicConverter;  
  5. import org.compass.core.mapping.ResourcePropertyMapping;  
  6. import org.compass.core.marshall.MarshallingContext;  
  7.   
  8. import com.compass.example.utils.StringUtil;  
  9.   
  10. public class HtmlPropertyConverter extends AbstractBasicConverter<String> {  
  11.   
  12.     private static Logger logger = Logger.getLogger(HtmlPropertyConverter.class);  
  13.       
  14.     public HtmlPropertyConverter() {  
  15.         super();  
  16.         // called by application server starting  
  17.         logger.info("----------HtmlPropertyConverter Initializing ...");  
  18.     }  
  19.       
  20.     /** 
  21.      *  搜索的時候被調用 
  22.      */  
  23.     @Override  
  24.     protected String doFromString(String str,  
  25.             ResourcePropertyMapping resourcePropertyMapping,  
  26.             MarshallingContext context) throws ConversionException {  
  27.         logger.info("----------calling doFromString...");  
  28.         return str;  
  29.     }  
  30.   
  31.       
  32.     /** 
  33.      * 創建索引的時候被調用,此時將文本中的HTML標籤過濾 
  34.      */  
  35.     @Override  
  36.     protected Property createProperty(String value,  
  37.             ResourcePropertyMapping resourcePropertyMapping,  
  38.             MarshallingContext context) {  
  39.         logger.info("----------calling createProperty...");  
  40.                   //過濾html標籤  
  41.         value = StringUtil.removeHTML(value);  
  42.           
  43.         return super.createProperty(value, resourcePropertyMapping, context);  
  44.     }  
  45.       
  46.   
  47. public class StringUtil {  
  48. /** 
  49.      * Remove occurences of html, defined as any text 
  50.      * between the characters "<" and ">". Optionally  
  51.      * replace HTML tags with a space. 
  52.      * @param str 
  53.      * @param addSpace 
  54.      * @return 
  55.      */  
  56.     public static String removeHTML(String str, boolean addSpace) {  
  57.           
  58.         //System.out.println(str);  
  59.           
  60.         if(str == nullreturn "";  
  61.         StringBuffer ret = new StringBuffer(str.length());  
  62.         int start = 0;  
  63.         int beginTag = str.indexOf("<");  
  64.         int endTag = 0;  
  65.         if(beginTag == -1return str;  
  66.           
  67.         while(beginTag >= start) {  
  68.             if(beginTag > 0) {  
  69.                 ret.append(str.substring(start, beginTag));  
  70.                   
  71.                 // replace each tag with a space (looks better)  
  72.                 if(addSpace) ret.append(" ");  
  73.             }  
  74.             endTag = str.indexOf(">", beginTag);  
  75.               
  76.             // if endTag found move "cursor" forward  
  77.             if(endTag > -1) {  
  78.                 start = endTag + 1;  
  79.                 beginTag = str.indexOf("<", start);  
  80.             }  
  81.               
  82.             // if no endTag found, get rest of str and break  
  83.             else {  
  84.                 ret.append(str.substring(beginTag));  
  85.                 break;  
  86.             }  
  87.         }  
  88.         // append everything after the last endTag  
  89.         if(endTag >-1 && endTag + 1 < str.length()) {  
  90.             ret.append(str.substring(endTag + 1));  
  91.         }  
  92.           
  93.         //System.out.println(ret.toString());  
  94.           
  95.         return ret.toString().trim();  
  96.     }  
  97.       
  98.     /** 
  99.      * Remove occurences of html, defined as any text 
  100.      * between the characters "<" and ">". 
  101.      * Replace any HTML tags with a space.  
  102.      * @param str 
  103.      * @return 
  104.      */  
  105.     public static String removeHTML(String str) {  
  106.         return removeHTML(str, true);  
  107.     }  
  108. }  
  109.       

 

   5.配置文件

 

[xhtml] view plaincopy
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <beans  
  3.     xmlns="http://www.springframework.org/schema/beans"  
  4.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  5.     xmlns:p="http://www.springframework.org/schema/p"  
  6.     xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">  
  7.   
  8.     <bean id="annotationConfiguration" class="org.compass.annotations.config.CompassAnnotationsConfiguration"/>  
  9.       
  10.     <bean id="compass" class="org.compass.spring.LocalCompassBean">  
  11.         <property name="resourceDirectoryLocations">  
  12.             <list>  
  13.                 <value>classpath:com/compass</value>  
  14.             </list>  
  15.         </property>  
  16.         <!--  如果compass有單獨的配置文件,可以從這裏引入  
  17.         <property name="configLocation" value="classpath:compass.cfg.xml"/>  
  18.          -->  
  19.         <!-- 數據索引存儲位置 -->  
  20.         <property name="connection" value="/lucene/indexes"/>  
  21.            
  22.         <property name="classMappings">  
  23.             <list>  
  24.                 <value>com.compass.example.model.Product</value>  
  25.                 <value>com.compass.example.model.Article</value>  
  26.             </list>  
  27.         </property>  
  28.         <property name="compassConfiguration" ref="annotationConfiguration"/>  
  29.         <property name="compassSettings">  
  30.             <props>  
  31.                 <!-- 建立索引位置的另一種方式  
  32.                 <prop key="compass.engine.connection">  
  33.                     file://${user.home}/lucene/indexes  
  34.                 </prop>  
  35.                  -->  
  36.                 <prop key="compass.transaction.factory">  
  37.                     org.compass.spring.transaction.SpringSyncTransactionFactory  
  38.                 </prop>  
  39.                   
  40.                 <!-- 指定摘要文本的長度 -->  
  41.                 <prop key="compass.engine.highlighter.default.fragmenter.simple.size">  
  42.                     200  
  43.                 </prop>  
  44.                   
  45.                 <!-- 搜索內容高亮顯示 -->  
  46.                 <prop  key="compass.engine.highlighter.default.formatter.simple.pre">     
  47.                     <![CDATA[<span style='background-color:yellow;color:red;'>]]>     
  48.                 </prop>     
  49.                 <prop  key="compass.engine.highlighter.default.formatter.simple.post">     
  50.                   <![CDATA[</span>]]>     
  51.                 </prop>   
  52.                 <!--定義分詞器-->         
  53.                 <!--          
  54.                 <prop     
  55.                     key="compass.engine.analyzer.default.type">     
  56.                    org.wltea.analyzer.lucene.IKAnalyzer  
  57.                 </prop>    
  58.                 -->  
  59.                 <!--   
  60.                 <prop key="compass.engine.analyzer.MMAnalyzer.CustomAnalyzer">  
  61.                     org.wltea.analyzer.lucene.IKAnalyzer  
  62.                     jeasy.analysis.MMAnalyzer  
  63.                     net.paoding.analysis.analyzer.PaodingAnalyzer  
  64.                 </prop>  
  65.                 -->  
  66.                   
  67.                 <prop key="compass.engine.analyzer.default.type">  
  68.                     org.wltea.analyzer.lucene.IKAnalyzer  
  69.                 </prop>  
  70.                    
  71.             </props>  
  72.         </property>  
  73.         <property name="transactionManager" ref="transactionManager"/>  
  74.         <property name="convertersByName">  
  75.             <map>  
  76.                 <entry key="htmlPropertyConverter">  
  77.                     <bean class="com.compass.converter.HtmlPropertyConverter"/>  
  78.                 </entry>  
  79.             </map>  
  80.         </property>  
  81.     </bean>  
  82.   
  83.     <bean id="hibernateGpsDevice" class="org.compass.gps.device.hibernate.HibernateGpsDevice">  
  84.         <property name="name" value="hibernateDevice"/>  
  85.         <property name="sessionFactory" ref="sessionFactory"/>  
  86.         <property name="mirrorDataChanges" value="true"/>  
  87.     </bean>  
  88.   
  89.     <!-- 數據庫中的記錄變化後同步更新索引 -->  
  90.     <bean id="compassGps" class="org.compass.gps.impl.SingleCompassGps" init-method="start" destroy-method="stop">  
  91.         <property name="compass" ref="compass"/>  
  92.         <property name="gpsDevices">  
  93.             <list>  
  94.                 <!-- compass2.1  
  95.                 <bean class="org.compass.spring.device.SpringSyncTransactionGpsDeviceWrapper">  
  96.                     <property name="gpsDevice" ref="hibernateGpsDevice"/>  
  97.                 </bean>  
  98.                  -->  
  99.                  <!-- compass2.2 -->  
  100.                  <ref local="hibernateGpsDevice"/>  
  101.             </list>  
  102.         </property>  
  103.     </bean>  
  104.   
  105.     <!-- compass模板 -->  
  106.     <bean id="compassTemplate" class="org.compass.core.CompassTemplate">  
  107.         <property name="compass" ref="compass"/>  
  108.     </bean>  
  109.       
  110.     <!-- 定時重建索引(利用quartz)或隨Spring ApplicationContext啓動而重建索引 -->  
  111.     <bean id="compassIndexBuilder" lazy-init="false" class="com.compass.example.utils.CompassIndexBuilder">  
  112.         <property name="compassGps" ref="compassGps"/>  
  113.         <property name="buildIndex" value="true"/>  
  114.         <property name="lazyTime" value="10"/>  
  115.     </bean>  
  116. </beans>  

 

    6.效果(英文)

 

     中文

 

 

四、問題總結

 

    1.異常there are more terms than documents in field "XXX", but it's impossible to sort on tokenized fields.

    在Luncene的API中對Sort的說明中有以下描述:

    The fields used to determine sort order must be carefully chosen. Documents must contain a single term in such a field, and the value of the term should indicate the document's relative position in a given sort order.The field must be indexed, but should not be tokenized, and does not need to be stored

(unless you happen to want it back with the rest of your document data). In other words:

document.add (new Field ("byNumber", Integer.toString(x), Field.Store.NO, Field.Index.NOT_ANALYZED));

    描述中紅色部分需特別注意,排序的字段必須被索引,並且不應該被tokennized,也就是在註解@SearchableProperty中的index=Index.NOT_ANALYZED, store=Store.NO,括號中的說明不是很明白,希望知道的可以給我點提示,再此謝謝了。

   

    2.異常java.lang.RuntimeException: field "XXX" does not appear to be indexed

    對多個表建索引後進行搜索,在添加排序條件時,如果不指定SortPropertyType,那麼在沒有指定converter的字段上排序時會拋以上異常, 但如果只對單個表建索引,不會有這個問題。

 

五、本次學習在網上查找各種資料的彙總,對引用到別處博客內容的博主表示感謝!文章尚有很多不完善之處,望指正,本人不勝感激!

六、其他資料

     Compass入門

     http://www.yeeach.com/2008/03/23/compass-%E5%85%A5%E9%97%A8%E6%8C%87%E5%8D%97/

  

     關於高亮顯示的解決方案

     http://jdkcn.com/entry/the-better-revolution-about-the-compass-lucene-highlight.html,此網站開放源碼,有助於大家學習

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章