天天看點

compass指南針搜尋架構學習(二)

一、準備

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

compass指南針搜尋架構學習(二)

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

二、什麼是Compass

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

compass指南針搜尋架構學習(二)

圖解:

  • 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 plain copy

  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 plain copy

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

    說明:

    @Searchable(alias="article")表示這個是可以搜尋實體,别名為article. 

    @SearchableId  這個是實體搜尋的辨別ID,和hibernate裡的概念差不多,用來區分索引檔案裡的實體索引。 

    @SearchableProperty(index = Index.NOT_ANALYZED, store = Store.NO) 表示這個屬性存入索引檔案,不進行分詞,不存儲要索引中。

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

    3.建立Compass搜尋的類

[java]  view plain copy

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

索引的查詢主要是根據傳過來的參數,關鍵字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 plain copy

  1. import org.compass.gps.CompassGps;  
  2. import org.springframework.beans.factory.InitializingBean;  
  3. public class CompassIndexBuilder implements InitializingBean {  
  4.     private boolean buildIndex = false;  
  5.     private int lazyTime = 10;  
  6.     private CompassGps compassGps;  
  7.     private Thread indexThread = new Thread() {  
  8.         @Override  
  9.         public void run() {  
  10.             try {  
  11.                 System.out.println("lazyTime: " + lazyTime);  
  12.                 Thread.sleep(lazyTime * 1000);  
  13.                 System.out.println("begin compass index ...");  
  14.                 long beginTime = System.currentTimeMillis();  
  15.                 // 重建索引.  
  16.                 // 如果compass實體中定義的索引檔案已存在,索引過程中會建立臨時索引,  
  17.                 // 索引完成後再進行覆寫.  
  18.                 compassGps.index();  
  19.                 long costTime = System.currentTimeMillis() - beginTime;  
  20.                 System.out.println("compss index finished.");  
  21.                 System.out.println("costed " + costTime + " milliseconds");  
  22.             } catch (InterruptedException e) {  
  23.                 e.printStackTrace();  
  24.             }  
  25.         }  
  26.     };  
  27.     @Override  
  28.     public void afterPropertiesSet() throws Exception {  
  29.         if (buildIndex) {  
  30.             indexThread.setDaemon(true);  
  31.             indexThread.setName("Compass Indexer");  
  32.             indexThread.start();  
  33.         }  
  34.     }  
  35.     public boolean isBuildIndex() {  
  36.         return buildIndex;  
  37.     }  
  38.     public void setBuildIndex(boolean buildIndex) {  
  39.         this.buildIndex = buildIndex;  
  40.     }  
  41.     public int getLazyTime() {  
  42.         return lazyTime;  
  43.     }  
  44.     public void setLazyTime(int lazyTime) {  
  45.         this.lazyTime = lazyTime;  
  46.     }  
  47.     public CompassGps getCompassGps() {  
  48.         return compassGps;  
  49.     }  
  50.     public void setCompassGps(CompassGps compassGps) {  
  51.         this.compassGps = compassGps;  
  52.     }  
  53. }  

   5.轉換器

[java]  view plain copy

  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. import com.compass.example.utils.StringUtil;  
  8. public class HtmlPropertyConverter extends AbstractBasicConverter<String> {  
  9.     private static Logger logger = Logger.getLogger(HtmlPropertyConverter.class);  
  10.     public HtmlPropertyConverter() {  
  11.         super();  
  12.         // called by application server starting  
  13.         logger.info("----------HtmlPropertyConverter Initializing ...");  
  14.     }  
  15.     @Override  
  16.     protected String doFromString(String str,  
  17.             ResourcePropertyMapping resourcePropertyMapping,  
  18.             MarshallingContext context) throws ConversionException {  
  19.         logger.info("----------calling doFromString...");  
  20.         return str;  
  21.     }  
  22.     @Override  
  23.     protected Property createProperty(String value,  
  24.             ResourcePropertyMapping resourcePropertyMapping,  
  25.             MarshallingContext context) {  
  26.         logger.info("----------calling createProperty...");  
  27.                   //過濾html标簽  
  28.         value = StringUtil.removeHTML(value);  
  29.         return super.createProperty(value, resourcePropertyMapping, context);  
  30.     }  
  31. public class StringUtil {  
  32.     public static String removeHTML(String str, boolean addSpace) {  
  33.         //System.out.println(str);  
  34.         if(str == null) return "";  
  35.         StringBuffer ret = new StringBuffer(str.length());  
  36.         int start = 0;  
  37.         int beginTag = str.indexOf("<");  
  38.         int endTag = 0;  
  39.         if(beginTag == -1) return str;  
  40.         while(beginTag >= start) {  
  41.             if(beginTag > 0) {  
  42.                 ret.append(str.substring(start, beginTag));  
  43.                 // replace each tag with a space (looks better)  
  44.                 if(addSpace) ret.append(" ");  
  45.             }  
  46.             endTag = str.indexOf(">", beginTag);  
  47.             // if endTag found move "cursor" forward  
  48.             if(endTag > -1) {  
  49.                 start = endTag + 1;  
  50.                 beginTag = str.indexOf("<", start);  
  51.             }  
  52.             // if no endTag found, get rest of str and break  
  53.             else {  
  54.                 ret.append(str.substring(beginTag));  
  55.                 break;  
  56.             }  
  57.         }  
  58.         // append everything after the last endTag  
  59.         if(endTag >-1 && endTag + 1 < str.length()) {  
  60.             ret.append(str.substring(endTag + 1));  
  61.         }  
  62.         //System.out.println(ret.toString());  
  63.         return ret.toString().trim();  
  64.     }  
  65.     public static String removeHTML(String str) {  
  66.         return removeHTML(str, true);  
  67.     }  
  68. }  

   5.配置檔案

[xhtml]  view plain copy

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

    6.效果(英文)

compass指南針搜尋架構學習(二)

     中文

compass指南針搜尋架構學習(二)

四、問題總結

    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,此網站開放源碼,有助于大家學習

繼續閱讀