企业级搜索引擎Solr使用入门指南

    由于搜索引擎功能在门户社区中对提高用户体验有着重在门户社区中涉及大量需要搜索引擎的功能需求,目前在实现搜索引擎的方案上有集中方案可供选择:

  • 基于Lucene自己进行封装实现站内搜索。

工作量及扩展性都较大,不采用。

  • 调用Google、Baidu的API实现站内搜索

同第三方搜索引擎绑定太死,无法满足后期业务扩展需要,暂时不采用。

  • 基于Compass+Lucene实现站内搜索

    适合于对数据库驱动的应用数据进行索引,尤其是替代传统的like ‘%expression%’来实现对varchar或clob等字段的索引,对于实现站内搜索是一种值得采纳的方案。但在分布式处理、接口封装上尚需要自己进行一定程度的封装,暂时不采用。

  • 基于Solr实现站内搜索

封装及扩展性较好,提供了较为完备的解决方案,因此在门户社区中采用此方案,后期加入Compass方案。

1、 Solr简介

    Solr是一个基于Lucene的Java搜索引擎服务器。Solr 提供了层面搜索、命中醒目显示并且支持多种输出格式(包括 XML/XSLT 和 JSON 格式)。它易于安装和配置,而且附带了一个基于 HTTP 的管理界面。Solr已经在众多大型的网站中使用,较为成熟和稳定。Solr 包装并扩展了 Lucene,所以Solr的基本上沿用了Lucene的相关术语。更重要的是,Solr 创建的索引与 Lucene 搜索引擎库完全兼容。通过对 Solr 进行适当的配置,某些情况下可能需要进行编码,Solr 可以阅读和使用构建到其他 Lucene 应用程序中的索引。此外,很多 Lucene 工具(如Nutch、 Luke)也可以使用 Solr 创建的索引。

2、 Tomcat下Solr安装配置

    由于Solr基于java开发,因此Solr在windows及Linux都能较好部署使用,但由于Solr提供了一些用于测试及管理、维护较为方便的shell脚本,因此在生产部署时候建议安装在Linux上,测试时候可以在windows使用。

下面以Linux下安装配置Solr进行说明,windows与此类似。

wget http://apache.mirror.phpchina.com/tomcat/tomcat-6/v6.0.16/bin/apache-tomcat-6.0.16.zip

unzip apache-tomcat-6.0.16.zip

mv apache-tomcat-6.0.16 /opt/tomcat

chmod 755 /opt/tomcat/bin/*

wget http://apache.mirror.phpchina.com/lucene/solr/1.2/apache-solr-1.2.0.tgz

tar zxvf apache-solr-1.2.0.tgz

Solr的安装配置最为麻烦的是对solr.solr.home的理解和配置,主要有三种

  • 基于当前路径的方式

cp apache-solr-1.2.0/dist/apache-solr-1.2.0.war /opt/tomcat/webapps/solr.war

mkdir /opt/solr-tomcat

cp -r apache-solr-1.2.0/example/solr/ /opt/solr-tomcat/

cd /opt/solr-tomcat

/opt/tomcat/bin/startup.sh

由于在此种情况下(没有设定solr.solr.home环境变量或JNDI的情况下),Solr查找./solr,因此在启动时候需要切换到/opt/solr-tomcat

  • 基于环境变量solr.solr.home

在当前用户的环境变量中(.bash_profile)或在/opt/tomcat/catalina.sh中添加如下环境变量

export JAVA_OPTS="$JAVA_OPTS -Dsolr.solr.home=/opt/solr-tomcat/solr"

  • 基于JNDI配置

mkdir –p /opt/tomcat/conf/Catalina/localhost

touch /opt/tomcat/conf/Catalina/localhost/solr.xml ,内容如下:

      <Context docBase="/opt/tomcat/webapps/solr.war" debug="0" crossContext="true" >
                 <Environment name="solr/home" type="java.lang.String" value="/opt/solr-tomcat/solr" override="true" />
    </Context>

访问solr管理界面

http://ip:port/solr

3、 Solr原理

clip_image002

    Solr对外提供标准的http接口来实现对数据的索引的增加、删除、修改、查询。在 Solr 中,用户通过向部署在servlet 容器中的 Solr Web 应用程序发送 HTTP 请求来启动索引和搜索。Solr 接受请求,确定要使用的适当SolrRequestHandler,然后处理请求。通过 HTTP 以同样的方式返回响应。默认配置返回 Solr 的标准 XML 响应,也可以配置 Solr 的备用响应格式。

可以向 Solr 索引 servlet 传递四个不同的索引请求:

  • add/update 允许向 Solr 添加文档或更新文档。直到提交后才能搜索到这些添加和更新。
  • commit 告诉 Solr,应该使上次提交以来所做的所有更改都可以搜索到。
  • optimize 重构 Lucene 的文件以改进搜索性能。索引完成后执行一下优化通常比较好。如果更新比较频繁,则应该在使用率较低的时候安排优化。一个索引无需优化也可以正常地运行。优化是一个耗时较多的过程。
  • delete 可以通过 id 或查询来指定。按 id 删除将删除具有指定 id 的文档;按查询删除将删除查询返回的所有文档。

一个典型的Add请求报文

<add>

<doc>

  <field name="id">TWINX2048-3200PRO</field>

  <field name="name">CORSAIR  XMS 2GB (2 x 1GB) 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) Dual Channel Kit System Memory – Retail</field>

  <field name="manu">Corsair Microsystems Inc.</field>

  <field name="cat">electronics</field>

  <field name="cat">memory</field>

  <field name="features">CAS latency 2, 2-3-3-6 timing, 2.75v, unbuffered, heat-spreader</field>

  <field name="price">185</field>

  <field name="popularity">5</field>

  <field name="inStock">true</field>

</doc>

<doc>

  <field name="id">VS1GB400C3</field>

  <field name="name">CORSAIR ValueSelect 1GB 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) System Memory – Retail</field>

  <field name="manu">Corsair Microsystems Inc.</field>

  <field name="cat">electronics</field>

  <field name="cat">memory</field>

  <field name="price">74.99</field>

  <field name="popularity">7</field>

  <field name="inStock">true</field>

</doc>

</add>

一个典型的搜索结果报文:

<response>

    <lst name="responseHeader">

        <int name="status">0</int>

        <int name="QTime">6</int>

        <lst name="params">

            <str name="rows">10</str>

            <str name="start">0</str>

            <str name="fl">*,score</str>

            <str name="hl">true</str>

            <str name="q">content:"faceted browsing"</str>

        </lst>

    </lst>

    <result name="response" numFound="1" start="0" maxScore="1.058217">

        <doc>

            <float name="score">1.058217</float>

            <arr name="all">

                <str>http://localhost/myBlog/solr-rocks-again.html</str>

                <str>Solr is Great</str>

                <str>solr,lucene,enterprise,search,greatness</str>

                <str>Solr has some really great features, like faceted browsing

                and replication</str>

            </arr>

            <arr name="content">

                <str>Solr has some really great features, like faceted browsing

                and replication</str>

            </arr>

            <date name="creationDate">2007-01-07T05:04:00.000Z</date>

            <arr name="keywords">

                <str>solr,lucene,enterprise,search,greatness</str>

            </arr>

            <int name="rating">8</int>

            <str name="title">Solr is Great</str>

            <str name="url">http://localhost/myBlog/solr-rocks-again.html</str>

        </doc>

    </result>

    <lst name="highlighting">

        <lst name="http://localhost/myBlog/solr-rocks-again.html">

            <arr name="content">

                <str>Solr has some really great features, like <em>faceted</em>

                <em>browsing</em> and replication</str>

            </arr>

        </lst>

    </lst>

</response>

关于solr的详细使用说明,请参考

http://wiki.apache.org/solr/FrontPage

4、 Solr测试使用

Solr的安装包包含了相关的测试样例,路径在apache-solr-1.2.0/example/exampledocs

  • 使用shell脚本(curl)测试Solr的操作:

cd apache-solr-1.2.0/example/exampledocs

vi post.sh,根据tomcat的ip、port修改URL变量的值URL=http://localhost:8080/solr/update

./post.sh *.xml                 # 
  • 使用Solr的java 包测试Solr的操作:

查看帮助:java -jar post.jar –help

提交测试数据:

java -Durl=http://localhost:8080/solr/update -Ddata=files -jar post.jar  *.xml      

 

下面以增加索引字段liangchuan、url为例,说明一下Solr中索引命令的使用

1) 修改solr的schema,配置需要索引字段的说明:

vi /opt/solr-tomcat/solr/conf/schema.xml ,在<fields>中增加如下内容

   <field name="liangchuan"  type="string" indexed="true" stored="true"/>

   <field name="url"  type="string" indexed="true" stored="true"/>

2) 创建增加索引请求的xml测试文件

touch /root/apache-solr-1.2.0/example/exampledocs/liangchuan.xml,内容如下:

<add>

<doc>

  <field name="id">liangchuan000</field>

  <field name="name">Solr, the Enterprise Search Server</field>

  <field name="manu">Apache Software Foundation</field>

  <field name="liangchuan">liangchuan’s solr "hello,world" test</field>

  <field name="url">http://www.google.com</field>

</doc>

</add>

3) 提交索引请求

cd apache-solr-1.2.0/example/exampledocs

    ./post.sh liangchuan.xml
    

4) 查询

通过solr的管理员界面http://localhost:8080/solr/admin查询

或通过curl 测试:

       export URL="http://localhost:8080/solr/select/"
       curl "$URL?indent=on&q=liangchuan&fl=*,score"
 

5、Solr查询条件参数说明

 
参数 描述 示例
q Solr 中用来搜索的查询。可以通过追加一个分号和已索引且未进行断词的字段的名称来包含排序信息。默认的排序是 score desc,指按记分降序排序。

q=myField:Java AND otherField:developerWorks; date asc

此查询搜索指定的两个字段并根据一个日期字段对结果进行排序。

start 将初始偏移量指定到结果集中。可用于对结果进行分页。默认值为 0。

start=15

返回从第 15 个结果开始的结果。

rows 返回文档的最大数目。默认值为 10。 rows=25
fq

提供一个可选的筛选器查询。查询结果被限制为仅搜索筛选器查询返回的结果。筛选过的查询由 Solr 进行缓存。它们对提高复杂查询的速度非常有用。

任何可以用 q 参数传递的有效查询,排序信息除外。
hl 当 hl=true 时,在查询响应中醒目显示片段。默认为 false。参看醒目显示参数上的 Solr Wiki 部分可以查看更多选项 hl=true
fl 作为逗号分隔的列表指定文档结果中应返回的 Field 集。默认为 “*”,指所有的字段。“score” 指还应返回记分。

*,score

   其中关于Solr查询相关的参数详细的信息请参看:
   http://wiki.apache.org/solr/CommonQueryParameters

    Solr的查询条件参数q的格式与Lucene相同,具体参看:

      http://lucene.apache.org/java/docs/queryparsersyntax.html

6、 在门户社区中solr使用模式

在门户社区中需要使用solr,可采用如下模式:

  • 对原有系统已有的数据或需要索引的数据量较大的情况

    直接采用通过http方式调用solr的接口方式,效率较差,采用solr本身对csv 的支持(http://wiki.apache.org/solr/UpdateCSV

),将数据导出为csv格式,然后调用solr的csv接口http://localhost:8080/solr/update/csv
  • 对系统新增的数据

    先将需要索引查询的数据组装成xml格式,然后使用httpclient 将数据提交到solr 的http接口,例如    

        http://localhost:8080/solr/update

   也可以参考post.jar中的SimplePostTool的实现。
http://svn.apache.org/viewvc/lucene/solr/trunk/src/java/org/apache/solr/util/SimplePostTool.java?view=co
  • 中文分词

    采用庖丁解牛作为solr(Lucene)缺省的中文分词方案

    项目库:http://code.google.com/p/paoding/

    Google groups:http://groups.google.com/group/paoding

    Javaeye的groups:http://analysis.group.javaeye.com/

  • 与nutch的集成使用

    http://blog.foofactory.fi/2007/02/online-indexing-integrating-nutch-with.html

  • 嵌入式Solr

    http://wiki.apache.org/solr/Solrj#EmbeddedSolrServer

  • 分布式索引

    http://wiki.apache.org/solr/CollectionDistribution

7、参考资料

http://wiki.apache.org/solr/

http://www.ibm.com/developerworks/cn/java/j-solr1/

http://www.ibm.com/developerworks/cn/java/j-solr2/

http://www.xml.com/pub/a/2006/08/09/solr-indexing-xml-with-lucene-andrest.html?page=1

http://lucene.apache.org/java/docs/queryparsersyntax.html

http://www.blogjava.net/RongHao/archive/2007/11/06/158621.html

 

Technorati Tags: ,,,

无线互联网垂直电子商务平台各系统内容运维策略思考

    在平台的eSales系统、BSS/OSS系统、支付平台、门户这几大平台中,门户及eSales系统中有大量的关于手机软件、手机素材、手机型号参数、手机相关专业知识等相关的资源,这些内容的完备与否是各平台能否成功的关键所在。但作为一家以“渠道通路”为核心竞争力的初创性的互联网公司,内容本身的运维、运营并不是我们自己最为擅长的地方,近期也不可能招聘一批专职的内容运营人员来做网站内容本身的运营。从技术角度谈一下门户及eSales系统的内容运维策略,以更好支撑运营部门的日常运营。

1. 平台(门户、eSales系统)内容运维基本原则

  • 门户定位:手机增值业务垂直性门户
  • 内容运维基本原则:采用搜索引擎全自动或搜索引擎+人工(人肉搜索)方式对内容进行爬取入库
  • 内容演进过程:垂直搜索引擎自动爬取的内容基础库(无原创内容,无筛选)->垂直搜索引擎爬取+内容运营人员编辑形成的内容库(较少原创内容,人工参与筛选)->垂直搜索引擎爬取+内容运营人员编辑+社区原创内容(具有部分原创内容)->以社区原创性内容为主的内容(UGC)。

第一阶段:垂直搜索引擎自动爬取的内容基础库

    由垂直搜索引擎的对互联网上海量的手机内容进行自动爬取入库,形成自身的基础内容库;在内容上无太多的原创内容,也无内容编辑人员对爬取的内容进行筛选和过滤。这属于内容运维的第一阶段,也是近期的技术开发重点。

第二阶段:垂直搜索引擎爬取+内容运营人员编辑形成的内容库

    在第一阶段的基础上,由内容运营人员对爬取内容进行审核、编辑,保证内容库的质量

第三阶段:垂直搜索引擎爬取+内容运营人员编辑+社区原创内容

    在第三阶段基础上,将内容平台逐步开放,并将社区人员来参与到内容平台的建设中,充分发挥群体智慧的力量。

第四阶段:以社区原创性内容为主的内容

    在社区逐步成熟后,在此阶段,平台的核心内容只要是用户参与产生的内容(UGC),这也是门户的核心价值。

2. 技术架构指导原则:

    技术架构的统一:各平台核心数据模型、业务模型、技术架构必须遵循平台统一的架构,保证平台各系统的内容资源是完全复用的。

    垂直搜索引擎的建设:尽管垂直门户的建设是门户的核心内容,但围绕“渠道通路”的建设才是的核心竞争力,这包括支付通路、内容分销通路、手机通路、互联网通路等,近期的建设重点并不是社区门户的建设,因此在开发上不能投入太多的开发资源到垂直搜索引擎的开发上,在满足未来扩展性的基础上,采用相对快捷的方式开发垂直搜索引擎。

3. 垂直搜索引擎技术实现

    近期垂直搜索引擎的建设重点是爬虫,与普通的垂直性搜索引擎不同,我们是对网站内容进行爬取,而不对内容进行索引。而在内容爬取上,重点是对指定网站页面内容(例如北斗手机网)所需要内容的定向解析。

    爬取整站内容或复杂爬取需求选用的爬虫框架:Heritrix、Nutch。但这两个框架都较重,初期我们并不需要处理诸如爬取层次、增量爬取等策略,因此这两个框架后期再采用。

    对Javascript的解析:采用Rhino(SpiderMonkey)

    爬取指定内容选用的爬虫框架(目前使用方式):httpclient+htmlparser(nekohtml)。采用httpclient完成对网站内容指定页面的爬取,采用nekohtml或htmlparser包来对页面内容进行定向解析并爬取。在实现上可以参考httpunit对Rhino+httpclient+nekohtml的封装和实现。

    对采用AJAX方式生成内容的爬取:采用Cobra(http://lobobrowser.org/cobra.jsp

    搜索引擎:采用Lucene+Compass

 

Compass 入门指南

    在新架构中打算选择Compass或Hibernate Search作为搜索引擎框架,比较后,感觉Hibernate Search上还是没有Compass成熟,另外考虑到后期对网页的爬取及搜索需求,决定还是基于Compass来作为架构缺省的搜索引擎。网上关于Compass的文档很多,但说得相对完整其详细的入门文档基本上没有,Compass的官方文档倒是说得很详细,但是例子一塌糊涂,存在很大问题。记录一下搭建的过程,作为入门的指南。

    Compass 通过OSEM(Object/Search Engine Mapping)允许把应用对象的领域模型映射到搜索引擎,最终通过访问common meta data来达到访问对象的目的。

1、几个核心概念

1.1、annotation vs. xml配置文件

   Compass的配置文件主要分成三类:

    第一类:*.cmd.xml文件*

      .cmd.xml文件是对common meta data进行定义,定义了最终搜索的结果中的最基本的元数据。

    第二类:*.cpm.xml文件

      *.cpm.xml是Object/Search Engine Mapping,提供了POJO到common meta data的映射。

    第三类:*.cfg.xml文件

      Compass的*.cfg.xml定义了Compass的Index存放路径、搜索引擎分词等相关信息。

    与采用xml配置文件相比较,采用Annonation方式还是相对简单,尤其是采用Spring时候,不用写*.cmd.xml文件、*.cpm.xml、*.cfg.xml,相对很方便,而且不像Hibernate的Annonation很多,Compass的Annonation的核心标注只有@Searchable、@SearchableId、@SearchableProperty、@SearchableComponent个,很容易记忆。因此推荐使用Annonation方式

1.2、Compass核心API

Compass的核心API借鉴了Hibernate的术语,因此在操作上基本上与Hibernate类似,以下为Compass的几个核心接口:

    CompassConfiguration(类似Hibernate Configuration):用来在一些设置参数、配置文件和映射定义上配置Compass。通常用来创建Compass接口。
    Compass(类似Hibernate SessionFactory):为单线程使用,创建线程安全的实例来打开Compass Seesion。同样还提供了一些搜索引擎索引级别的操作。
    CompassSesssion(类似Hibernate Session):用来执行像保存、删除、查找、装载这样的搜索操作。很轻量但是并不是线程安全的。
    CompassTransaction(类似Hibernate Transaction):管理Compass事务的接口。使用它并不需要事务管理环境(像Spring、JTA)。

1.3、Compass与Spring集成

Compass已经对对spring集成做了很好的封装,同时与Spring对Hibernate的支持类似,Compass也提供了CompassTemplate来简化诸如对Session、Transaction、Exception等操作,尽量充分使用此工具,可以有效提高效率。例如:

CompassTemplate ct = (CompassTemplate) context.getBean(“compassTemplate”);

Article article = new Article();
article.setTitle(“Compass Test”);
article.setPublishDate(new Date());
article.setAuthor(1);

ct.save(article); //存储对象需要索引的数据到Compass的索引中。

 

2、软件环境

Spring :2.5

Compas:1.2.1

Hibernate:3.2.5

Mysql :5.0.5

3、数据库脚本

CREATE TABLE `article` ( 

`Id` int(11) NOT NULL auto_increment, 

`title` varchar(40) NOT NULL default '', 

`author` int(11) default '0', 

`publish_date` date NOT NULL default '0000-00-00', 

PRIMARY KEY (`Id`) ) TYPE=MyISAM; 

CREATE TABLE `author` ( 

`Id` int(11) NOT NULL auto_increment, 

`username` varchar(20) NOT NULL default '', 

`password` varchar(20) NOT NULL default '', 

`age` smallint(6) default '0', 

PRIMARY KEY (`Id`) ) TYPE=MyISAM; 

4、测试用例

从测试用例讲起比较容易把关系理清楚,不然一堆术语和概念很让人晕乎。

import org.apache.log4j.Logger;
import java.util.Date;

import junit.framework.TestCase;

import org.compass.core.Compass;
import org.compass.core.CompassDetachedHits;
import org.compass.core.CompassHit;
import org.compass.core.CompassHits;
import org.compass.core.CompassSession;
import org.compass.core.CompassTemplate;
import org.compass.core.CompassTransaction;
import org.compass.core.support.search.CompassSearchCommand;
import org.compass.core.support.search.CompassSearchResults;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.mobilesoft.esales.dao.hibernate.ArticleDAO;
import com.mobilesoft.esales.dao.hibernate.AuthorDAO;
import com.mobilesoft.esales.model.Article;
import com.mobilesoft.esales.model.Author;
import com.mobilesoft.framework.search.service.CompassSearchService;

/**
 * Compass服务使用的测试用例
 * 
 * @author liangchuan@mobile-soft.cn
 * 
 */

public class TestCompass extends TestCase {

    private static final Logger logger = Logger.getLogger(TestCompass.class);

    private static ClassPathXmlApplicationContext context = null;
    private static CompassTemplate ct;

    static {
        context = new ClassPathXmlApplicationContext(new String[] {
                "applicationContext.xml", "applicationContext-resources.xml",
                "applicationContext-dao.xml", "applicationContext-service.xml",
                "applicationContext-compass.xml" });
        ct = (CompassTemplate) context.getBean("compassTemplate");
    }

    protected void setUp() throws Exception {

    }

    /**
     * 插入测试数据
     */
    public void testInsert() {

        ArticleDAO articleDao = (ArticleDAO) context.getBean("articleDAO");
        AuthorDAO authorDao = (AuthorDAO) context.getBean("authorDAO");
        Article article = new Article();
        Author author = new Author();
        author.setAge((short) 27);
        author.setUsername("liangchuan");
        author.setPassword("liangchuan");
        article.setTitle("Compass Test");
        article.setPublishDate(new Date());
        article.setAuthor(1);
        authorDao.save(author);
        articleDao.save(article);
        ct.save(article);
        ct.save(author);
    }

    /**
     * 用于测试使用CompassTransaction事务方式
     */
    public void testTransactionalFind() {

        Compass compass = ct.getCompass();
        CompassSession session = compass.openSession();
        CompassTransaction tx = null;
        try {
            tx = session.beginTransaction();
            CompassHits hits = session.find("Compass*");

            logger.error("testTransactionalFind() - CompassHits hits="
                    + hits.getLength());
            for (int i = 0; i < hits.getLength(); i++) {
                Object hit = hits.data(i);
                if (hit instanceof Article) {
                    Article item = (Article) hit;
                    logger.error("testTransactionalFind() - article     hits="
                            + item.getTitle());
                } else if (hit instanceof Author) {
                    Author item = (Author) hit;
                    logger.error("testTransactionalFind() - author hits="
                            + item.getUsername());
                } else {
                    logger.error("testTransactionalFind() - error hits=");
                }
            }
            tx.commit();
        } catch (Exception e) {
            if (tx != null) {
                tx.rollback();
            }
        } finally {
            session.close();
        }
    }

    /**
     * 用于演示CompassDetachedHits的使用。
     * 由于CompassTempalte得到的结果集必须在transactionalcontext中才能使用,
     * 因此必须使用CompassDetachedHits方式测试CompassDetachedHits方式
     */
    public void testDetachedFind() {

        // 由于CompassTempalte得到的结果集必须在transactional
        // context中才能使用,因此必须使用CompassDetachedHits方式
        // 测试CompassDetachedHits方式
        CompassDetachedHits hits = ct.findWithDetach("Compass*");

        logger.error("testDetachedFind() - CompassHits hits="
                + hits.getLength());
        for (int i = 0; i < hits.getLength(); i++) {
            Object hit = hits.data(i);
            if (hit instanceof Article) {
                Article item = (Article) hit;
                logger.error("testDetachedFind() - article     hits="
                        + item.getTitle());
            } else if (hit instanceof Author) {
                Author item = (Author) hit;
                logger.error("testDetachedFind() - author hits="
                        + item.getUsername());
            } else {
                logger.error("testDetachedFind() - error hits=");
            }
        }

    }

    /**
     * 用于演示com.mobilesoft.framework.search.service.CompassSearchService的使用
     * 
     */
    class CompassSearch extends CompassSearchService{
        CompassSearch(){
            Compass compass = ct.getCompass();
            CompassSession session = compass.openSession();
            CompassTransaction tx = null;

            try {
                tx = session.beginTransaction();
                CompassSearchCommand command = new CompassSearchCommand();
                command.setQuery("Compass");
                CompassSearchResults results= performSearch(command,session);
                logger.error("CompassSearch() - CompassHit TotalHits value=" +results.getTotalHits());

                for (int i = 0; i < results.getHits().length; i++) {
                    CompassHit hits=results.getHits()[i];
                    Object hit=hits.getData();
                    logger.error("CompassSearch() - CompassHit hit=" + hit); //$NON-NLS-1$

                    if (hit instanceof Article) {
                        Article item = (Article) hit;
                        logger.error("testCompassSearchService() - article     hits="
                                + item.getTitle());
                    } else if (hit instanceof Author) {
                        Author item = (Author) hit;
                        logger.error("testCompassSearchService() - author hits="
                                + item.getUsername());
                    } else {
                        logger.error("testCompassSearchService() - error hits=");
                    }

                    tx.commit();
                }
            } catch (Exception e) {
                if (tx != null) {
                    tx.rollback();
                }
            } finally {
                session.close();
            }        

        }

    }
    public void testCompassSearchService() {
        new CompassSearch();
        }

    protected void tearDown() throws Exception {
    }
}

5、配置文件

applicationContext-compass.xml

<?xml version="1.0"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN"
    "http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans default-lazy-init="true">
    <bean id="compassTemplate" class="org.compass.core.CompassTemplate">
        <property name="compass" ref="compass"/>
    </bean>
    <bean id="annotationConfiguration"
        class="org.compass.annotations.config.CompassAnnotationsConfiguration">
    </bean>
    <bean id="compass" class="org.compass.spring.LocalCompassBean">
        <property name="classMappings">
            <list>
                <value>com.mobilesoft.esales.model.Article</value>
                <value>com.mobilesoft.esales.model.Author</value>
            </list>
        </property>
        <property name="compassConfiguration" ref="annotationConfiguration"/>

        <property name="compassSettings">
            <props>
                <prop key="compass.engine.connection"> file://compass </prop>
                <prop key="compass.transaction.factory">
                    org.compass.spring.transaction.SpringSyncTransactionFactory
                    </prop>
                <prop
                    key="compass.engine.highlighter.default.formatter.simple.pre">
                    <![CDATA[<font color="red"><b>]]>
                </prop>
                <prop
                    key="compass.engine.highlighter.default.formatter.simple.post">
                    <![CDATA[</b></font>]]>
                </prop>
            </props>
        </property>

        <property name="transactionManager" ref="transactionManager"/>
    </bean>

    <bean id="hibernateGpsDevice"
        class="org.compass.gps.device.hibernate.HibernateGpsDevice">
        <property name="name">
            <value>hibernateDevice</value>
        </property>
        <property name="sessionFactory" ref="sessionFactory"/>
        <property name="mirrorDataChanges">
            <value>true</value>
        </property>
    </bean>
    <bean id="compassGps" class="org.compass.gps.impl.SingleCompassGps"
        init-method="start" destroy-method="stop">
        <property name="compass" ref="compass"/>
        <property name="gpsDevices">
            <list>
                <bean
                    class="org.compass.spring.device.SpringSyncTransactionGpsDeviceWrapper">
                    <property name="gpsDevice" ref="hibernateGpsDevice"/>
                </bean>
            </list>
        </property>
    </bean>
    <bean id="compassSearchService" class="com.mobilesoft.framework.search.service.CompassSearchService">
        <property name="compass" ref="compass"/>
        <property name="pageSize" value="15"/>
    </bean>

    <!-- 定时重建索引(利用quartz)或随Spring ApplicationContext启动而重建索引 -->
    <bean id="compassIndexBuilder" class="com.mobilesoft.framework.search.service.CompassIndexBuilder" lazy-init="false">
        <property name="compassGps" ref="compassGps"/>
        <property name="buildIndex" value="false"/>
        <property name="lazyTime" value="10"/>
    </bean>

</beans>

applicationContext-dao.xml、applicationContext-service.xml、applicationContext-resources.xml等略去。

 

6、Service层(参考了SpringSide实现)

AdvancedSearchCommand.java

package com.mobilesoft.framework.search.service;

import java.util.HashSet;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.compass.core.CompassQuery.SortDirection;
import org.compass.core.CompassQuery.SortPropertyType;
import org.compass.core.support.search.CompassSearchCommand;

import org.springframework.util.Assert;

public class AdvancedSearchCommand extends CompassSearchCommand {

    /**
     * 封装基于Compass 的排序参数.
     */
    class CompassSort {

        private String name;

        private SortPropertyType type;

        private SortDirection direction;

        public CompassSort() {
        }

        public CompassSort(String sortParamName, String paramType,
                           boolean isAscend) {
            Assert.isTrue(StringUtils.isNotBlank(sortParamName));
            setName(sortParamName);

            if ("int".equalsIgnoreCase(paramType)) {
                setType(SortPropertyType.INT);
            } else if ("float".equalsIgnoreCase(paramType)) {
                setType(SortPropertyType.FLOAT);
            } else if ("string".equalsIgnoreCase(paramType)) {
                setType(SortPropertyType.STRING);
            } else {
                setType(SortPropertyType.AUTO);
            }

            if (isAscend) {
                setDirection(SortDirection.AUTO);
            } else {
                setDirection(SortDirection.REVERSE);
            }
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public SortPropertyType getType() {
            return type;
        }

        public void setType(SortPropertyType type) {
            this.type = type;
        }

        public SortDirection getDirection() {
            return direction;
        }

        public void setDirection(SortDirection direction) {
            this.direction = direction;
        }
    }

    /**
     * 搜索结果排序表.
     */
    private Set<CompassSort> sortMap = new HashSet<CompassSort>();

    private String[] highlightFields;

    /**
     * @param paramType 现定义了三种类型: int string 以及 float。<br>
     *                  除去这三种外,其他会被自动定义为SortPropertyType.AUTO 具体的可见{@link org.compass.core.CompassQuery.SortPropertyType}
     * @param isAscend  顺序还是倒序排序
     * @see org.compass.core.CompassQuery.SortPropertyType#AUTO
     * @see org.compass.core.CompassQuery.SortPropertyType#INT
     * @see org.compass.core.CompassQuery.SortPropertyType#STRING
     * @see org.compass.core.CompassQuery.SortPropertyType#FLOAT
     * @see org.compass.core.CompassQuery.SortDirection#AUTO
     * @see org.compass.core.CompassQuery.SortDirection#REVERSE
     */
    public void addSort(String sortParamName, String paramType, boolean isAscend) {
        this.sortMap.add(new CompassSort(sortParamName, paramType, isAscend));
    }

    public Set<CompassSort> getSortMap() {
        return sortMap;
    }

    public void setSortMap(Set<CompassSort> sortMap) {
        this.sortMap = sortMap;
    }

    public String[] getHighlightFields() {
        return highlightFields;
    }

    public void setHighlightFields(String[] highlightFields) {
        this.highlightFields = highlightFields;
    }
}

CompassIndexBuilder.java

package com.mobilesoft.framework.search.service;

import org.apache.log4j.Logger;
import org.compass.gps.CompassGps;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;

/**
 * 通过quartz定时调度定时重建索引或自动随Spring ApplicationContext启动而重建索引的Builder.
 * 会启动后延时数秒新开线程调用compassGps.index()函数.
 * 默认会在Web应用每次启动时重建索引,可以设置buildIndex属性为false来禁止此功能.
 * 也可以不用本Builder, 编写手动调用compassGps.index()的代码.
 *
 */
public class CompassIndexBuilder implements InitializingBean {

    private static final Logger log = Logger.getLogger(CompassIndexBuilder.class);

    // 是否需要建立索引,可被设置为false使本Builder失效.
    private boolean buildIndex = false;

    // 索引操作线程延时启动的时间,单位为秒
    private int lazyTime = 10;

    // Compass封装
    private CompassGps compassGps;

    // 索引线程
    private Thread indexThread = new Thread() {

        @Override
        public void run() {
            try {
                Thread.sleep(lazyTime * 1000);

                log.info("begin compass index...");
                long beginTime = System.currentTimeMillis();
                // 重建索引.
                // 如果compass实体中定义的索引文件已存在,索引过程中会建立临时索引,
                // 索引完成后再进行覆盖.
                compassGps.index();
                long costTime = System.currentTimeMillis() - beginTime;
                log.info("compss index finished.");
                log.info("costed " + costTime + " milliseconds");
            } catch (InterruptedException e) {
                // simply proceed
            }
        }
    };

    /**
     * 实现<code>InitializingBean</code>接口,在完成注入后调用启动索引线程.
     *
     * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
     */
    public void afterPropertiesSet() throws Exception {
        if (buildIndex) {
            Assert.notNull(compassGps, "CompassIndexBuilder not set CompassGps yet.");
            indexThread.setDaemon(true);
            indexThread.setName("Compass Indexer");
            indexThread.start();
        }
    }

    public void setBuildIndex(boolean buildIndex) {
        this.buildIndex = buildIndex;
    }

    public void setLazyTime(int lazyTime) {
        this.lazyTime = lazyTime;
    }

    public void setCompassGps(CompassGps compassGps) {
        this.compassGps = compassGps;
    }
}

CompassSearchService.java

package com.mobilesoft.framework.search.service;

import org.compass.core.Compass;
import org.compass.core.CompassCallback;
import org.compass.core.CompassDetachedHits;
import org.compass.core.CompassHits;
import org.compass.core.CompassQuery;
import org.compass.core.CompassSession;
import org.compass.core.CompassTemplate;
import org.compass.core.CompassTransaction;
import org.compass.core.support.search.CompassSearchCommand;
import org.compass.core.support.search.CompassSearchResults;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;

import com.mobilesoft.framework.search.service.AdvancedSearchCommand.CompassSort;
/**
 * 仿照 {@link org.compass.spring.web.mvc.CompassSearchController}
 * 中的代码,构建了一个Service,方便不使用Spring MVC 
 *
 * @see org.compass.spring.web.mvc.CompassSearchController
 * @see org.compass.spring.web.mvc.AbstractCompassCommandController
 */
public class CompassSearchService implements InitializingBean {

    //每页显示的条目数量
    private Integer pageSize = 15;

    private Compass compass;

    private CompassTemplate compassTemplate;

    /**
     * 公开的搜索接口,返回匹配的搜索结果,与
     * {@link org.compass.spring.web.mvc.CompassSearchController#handle(javax.servlet.http.HttpServletRequest,
     *javax.servlet.http.HttpServletResponse,Object,org.springframework.validation.BindException) 处理相似
     *
     * @see org.compass.spring.web.mvc.CompassSearchController#handle(javax.servlet.http.HttpServletRequest,
     *javax.servlet.http.HttpServletResponse,java.lang.Object,org.springframework.validation.BindException)
     */
    public CompassSearchResults search(final CompassSearchCommand command) {
        return (CompassSearchResults) getCompassTemplate().execute(
                CompassTransaction.TransactionIsolation.READ_ONLY_READ_COMMITTED, new CompassCallback() {
            public Object doInCompass(CompassSession session) {
                return performSearch(command, session);
            }
        });
    }

    /**
     * 通过此方法调用搜索引擎,进行结果匹配搜索.
     *
     * @see org.compass.spring.web.mvc.CompassSearchController#performSearch(
     *org.compass.spring.web.mvc.CompassSearchCommand,org.compass.core.CompassSession)
     */
    protected CompassSearchResults performSearch(CompassSearchCommand searchCommand, CompassSession session) {
        long time = System.currentTimeMillis();
        CompassQuery query = buildQuery(searchCommand, session);
        CompassHits hits = query.hits();
        CompassDetachedHits detachedHits;
        CompassSearchResults.Page[] pages = null;
        if (pageSize == null) {
            doProcessBeforeDetach(searchCommand, session, hits, -1, -1);
            detachedHits = hits.detach();
        } else {
            int iPageSize = pageSize;
            int page = 0;
            int hitsLength = hits.getLength();
            if (searchCommand.getPage() != null) {
                page = searchCommand.getPage();
            }
            int from = page * iPageSize;

            if (from > hits.getLength()) {

                // 如果起始的条目大于搜索到的条目
                from = hits.getLength() - iPageSize;
                doProcessBeforeDetach(searchCommand, session, hits, from, hitsLength);
                detachedHits = hits.detach(from, hitsLength);
            } else if ((from + iPageSize) > hitsLength) {

                // 结束的条目大于搜索到的结果
                doProcessBeforeDetach(searchCommand, session, hits, from, hitsLength);
                detachedHits = hits.detach(from, hitsLength);
            } else {

                // 中间的页码,直接取出相应的条目
                doProcessBeforeDetach(searchCommand, session, hits, from, iPageSize);
                detachedHits = hits.detach(from, iPageSize);
            }
            doProcessAfterDetach(searchCommand, session, detachedHits);
            int numberOfPages = (int) Math.ceil((float) hitsLength / iPageSize);
            pages = new CompassSearchResults.Page[numberOfPages];
            for (int i = 0; i < pages.length; i++) {
                pages[i] = new CompassSearchResults.Page();
                pages[i].setFrom(i * iPageSize + 1);
                pages[i].setSize(iPageSize);
                pages[i].setTo((i + 1) * iPageSize);
                if (from >= (pages[i].getFrom() - 1) && from < pages[i].getTo()) {
                    pages[i].setSelected(true);
                } else {
                    pages[i].setSelected(false);
                }
            }
            if (numberOfPages > 0) {
                CompassSearchResults.Page lastPage = pages[numberOfPages - 1];
                if (lastPage.getTo() > hitsLength) {
                    lastPage.setSize(hitsLength - lastPage.getFrom());
                    lastPage.setTo(hitsLength);
                }
            }
        }
        time = System.currentTimeMillis() - time;
        CompassSearchResults searchResults = new CompassSearchResults(detachedHits.getHits(), time, pageSize);
        searchResults.setPages(pages);
        return searchResults;
    }

    /**
     * 构建Lucene搜索器.
     */
    protected CompassQuery buildQuery(CompassSearchCommand searchCommand, CompassSession session) {
        CompassQuery query = session.queryBuilder().queryString(searchCommand.getQuery().trim()).toQuery();

        if (AdvancedSearchCommand.class.isAssignableFrom(searchCommand.getClass())) {
            AdvancedSearchCommand advancedSearchCommand = (AdvancedSearchCommand) searchCommand;

            for (CompassSort sort : advancedSearchCommand.getSortMap()) {
                query.addSort(sort.getName(), sort.getType(), sort.getDirection());
            }
        }
        return query;
    }

    /**
     * 在detach 之前,可以做一些操作。比如highlighting...
     *
     * @param from 需要注意的是,如果pageSize 没有指定,那么这里传入的参数为-1
     */
    protected void doProcessBeforeDetach(CompassSearchCommand searchCommand, CompassSession session, CompassHits hits,
                                         int from, int size) {
        if (AdvancedSearchCommand.class.isAssignableFrom(searchCommand.getClass())) {
            if (from < 0) {
                from = 0;
                size = hits.getLength();
            }
            String[] highlightFields = ((AdvancedSearchCommand) searchCommand).getHighlightFields();

            if (highlightFields == null) {
                return;
            }

            // highlight fields
            for (int i = from; i < size; i++) {
                for (String highlightField : highlightFields) {
                    hits.highlighter(i).fragment(highlightField);
                }
            }
        }
    }

    /**
     * An option to perform any type of processing before the hits are detached.
     */
    protected void doProcessAfterDetach(CompassSearchCommand searchCommand, CompassSession session,
                                        CompassDetachedHits hits) {

    }

    public void afterPropertiesSet() throws Exception {
        Assert.notNull(compass, "Must set compass property");
        this.compassTemplate = new CompassTemplate(compass);
    }

    public Integer getPageSize() {
        return pageSize;
    }

    public void setPageSize(Integer pageSize) {
        this.pageSize = pageSize;
    }

    public void setCompass(Compass compass) {
        this.compass = compass;
    }

    protected CompassTemplate getCompassTemplate() {
        return this.compassTemplate;
    }

}

8、Model层

@SearchableId 声明Document的id列;

@SearchableProperty 声明要索引的field;

@SearchableComponent 声明要索引的其他关联对象。

Article.java

package com.mobilesoft.esales.model;

import java.util.Date;

import org.compass.annotations.Searchable;
import org.compass.annotations.SearchableId;
import org.compass.annotations.SearchableProperty;
import org.compass.core.CompassTemplate;

@Searchable
public class Article  implements java.io.Serializable {

    @SearchableId
    private Integer id;
    @SearchableProperty(name="title")
    private String title;
    @SearchableProperty(name="author")
    private Integer author;
    @SearchableProperty(name="publishDate")
    private Date publishDate;

    /** default constructor */
    public Article() {
    }

    /** minimal constructor */
    public Article(String title, Date publishDate) {
        this.title = title;
        this.publishDate = publishDate;
    }

    /** full constructor */
    public Article(String title, Integer author, Date publishDate) {
        this.title = title;
        this.author = author;
        this.publishDate = publishDate;
    }

    public Integer getId() {
        return this.id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getTitle() {
        return this.title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Integer getAuthor() {
        return this.author;
    }

    public void setAuthor(Integer author) {
        this.author = author;
    }

    public Date getPublishDate() {
        return this.publishDate;
    }

    public void setPublishDate(Date publishDate) {
        this.publishDate = publishDate;
    }

}

Author.java

package com.mobilesoft.esales.model;

import org.compass.annotations.Searchable;
import org.compass.annotations.SearchableId;
import org.compass.annotations.SearchableProperty;
import org.compass.core.CompassTemplate;

@Searchable
public class Author  implements java.io.Serializable {

    @SearchableId
    private Integer id;
    @SearchableProperty(name="username")
    private String username;
    private String password;
    @SearchableProperty(name="age")
    private Short age;

    public Author() {
    }

    /** minimal constructor */
    public Author(String username, String password) {
        this.username = username;
        this.password = password;
    }

    /** full constructor */
    public Author(String username, String password, Short age) {
        this.username = username;
        this.password = password;
        this.age = age;
    }

    // Property accessors

    public Integer getId() {
        return this.id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return this.username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return this.password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Short getAge() {
        return this.age;
    }

    public void setAge(Short age) {
        this.age = age;
    }

}

9、DAO层

ArticleDAO.java和AuthorDAO.java省略

直接用MyEclipse生成的,没有什么特别的。

10、参考文档

http://www.compass-project.org/docs/1.2.1/reference/html/

The Compass Framework Search made easy.pdf

Compass TSSJS Europe 06.pdf

Hello World Tutorial

InfoQ:  Compass: Integrate Search into your apps

InfoQ: Compass: Simplifying and Extending Lucene to Provide Google-like Search

InfoQ: Compass: 在你的应用中集成搜索功能

Compass 指南

http://www.kimchy.org/