solr全文搜索服务

站内搜索技术选型

在一些大型门户网站、电子商务网站等都需要站内搜索功能,
使用传统的数据库查询方式实现搜索无法满足一些高级的搜索需求,
比如:搜索速度要快、搜索结果按相关度排序、搜索内容格式不固定等,
这里就需要使用全文检索技术实现搜索功能。

单独使用Lucene实现

单独使用Lucene实现站内搜索需要开发的工作量较大,
主要表现在:索引维护、索引性能优化、搜索性能优化等,因此不建议采用。

使用Google或Baidu接口

通过第三方搜索引擎提供的接口实现站内搜索,这样和第三方引擎系统依赖紧密,不方便扩展,不建议采用。

使用Solr实现

基于Solr实现站内搜索扩展性较好并且可以减少程序员的工作量,
因为Solr提供了较为完备的搜索引擎解决方案,因此在门户、论坛等系统中常用此方案。

Solr介绍

什么是Solr

Solr 是Apache下的一个顶级开源项目,采用Java开发,它是基于Lucene的全文搜索服务器。
Solr提供了比Lucene更为丰富的查询语言,同时实现了可配置、可扩展,并对索引、搜索性能进行了优化。 

Solr可以独立运行,运行在Jetty、Tomcat等这些Servlet容器中,Solr 索引的实现方法很简单,
用POST方法向 Solr 服务器发送一个描述 Field 及其内容的 XML 文档,Solr根据xml文档添加、删除、更新索引。
Solr 搜索只需要发送 HTTP GET 请求,然后对 Solr 返回Xml、json等格式的查询结果进行解析,
组织页面布局。Solr不提供构建UI的功能,Solr提供了一个管理界面,通过管理界面可以查询Solr的配置和运行情况。

Solr与Lucene的区别

Lucene是一个开放源代码的全文检索引擎工具包,它不是一个完整的全文检索引擎,
Lucene提供了完整的查询引擎和索引引擎,目的是为软件开发人员提供一个简单易用的工具包,
以方便的在目标系统中实现全文检索的功能,或者以Lucene为基础构建全文检索引擎。

 Solr的目标是打造一款企业级的搜索引擎系统,它是一个搜索引擎服务,可以独立运行,
通过Solr可以非常快速的构建企业的搜索引擎,通过Solr也可以高效的完成站内搜索功能。

Solr安装配置

http://lucene.apache.org/solr/

下载lucene-4.10.3.zip 或 lucene-4.10.3.tgz 并解压:

bin:solr的运行脚本
contrib:solr的一些贡献软件/插件,用于增强solr的功能。
dist:该目录包含build过程中产生的war和jar文件,以及相关的依赖文件。
docs:solr的API文档
example:solr工程的例子目录:
    example/solr:
    该目录是一个包含了默认配置信息的Solr的Core目录。
    example/multicore:
    该目录包含了在Solr的multicore中设置的多个Core目录。 
    example/webapps:
    该目录中包括一个solr.war,该war可作为solr的运行实例工程。
licenses:solr相关的一些许可信息

运行环境

solr 需要运行在一个Servlet容器中,Solr4.10.3要求jdk使用1.7以上,
Solr默认提供Jetty(java写的Servlet容器)

环境如下:

Solr:Solr4.10.3
Jdk:jdk1.7.0_72
Tomcat:apache-tomcat-7.0.53

Solr与Tomcat整合配置

Solr Home与SolrCore

创建一个Solr home目录,SolrHome是Solr运行的主目录,目录中包括了运行Solr实例所有的配置文件和数据文件,
Solr实例就是SolrCore,一个SolrHome可以包括多个SolrCore(Solr实例),每个SolrCore提供单独的搜索和索引服务。

目录结构

example\solr是一个solr home目录结构,如下:

上图中“collection1”是一个SolrCore(Solr实例)目录 ,目录内容如下所示:

说明:

collection1:叫做一个Solr运行实例SolrCore,SolrCore名称不固定,
一个solr运行实例对外单独提供索引和搜索接口。

solrHome中可以创建多个solr运行实例SolrCore。

一个solr的运行实例对应一个索引目录。

conf是SolrCore的配置文件目录 。

配置

创建目录 F:\develop\solr 

将example\solr目录 拷贝至 F:\develop\solr目录下并改名为solrhome

solrconfig.xml

solrconfig.xml,在SolrCore的conf目录下,它是SolrCore运行的配置文件。
加载jar包
将contrib和dist两个目录拷贝到F:\develop\solr下,此时,solrhome,dist,contrib三个目录在同一层级

修改solrconfig.xml文件:

<lib dir="${solr.install.dir:../..}/contrib/extraction/lib" regex=".*\.jar" />
<lib dir="${solr.install.dir:../..}/dist/" regex="solr-cell-\d.*\.jar" />

<lib dir="${solr.install.dir:../..}/contrib/clustering/lib/" regex=".*\.jar" />
<lib dir="${solr.install.dir:../..}/dist/" regex="solr-clustering-\d.*\.jar" />

<lib dir="${solr.install.dir:../..}/contrib/langid/lib/" regex=".*\.jar" />
<lib dir="${solr.install.dir:../..}/dist/" regex="solr-langid-\d.*\.jar" />

<lib dir="${solr.install.dir:../..}/contrib/velocity/lib" regex=".*\.jar" />
<lib dir="${solr.install.dir:../..}/dist/" regex="solr-velocity-\d.*\.jar" />
dataDir
置SolrCore的数据目录,数据目录下包括了index索引目录 和tlog日志文件目录,
数据目录默认在solrCore下的data目录 ,也可以更改目录地址 ,如下:

  <dataDir>${solr.data.dir:F:/develop/solr/collection1/data}</dataDir>
requestHandler
requestHandler请求处理器,定义了索引和搜索的访问方式。

通过/update维护索引,可以完成索引的添加、修改、删除操作。
通过/select搜索索引。
设置搜索参数完成搜索,搜索参数也可以设置一些默认值,如下:

<requestHandler name="/select" class="solr.SearchHandler">
<!-- 设置默认的参数值,可以在请求地址中修改这些参数-->
<lst name="defaults">
    <str name="echoParams">explicit</str>
    <int name="rows">10</int><!--显示数量-->
    <str name="wt">json</str><!--显示格式-->
    <str name="df">text</str><!--默认搜索字段-->
</lst>
</requestHandler>

Solr工程部署

1.    将dist\solr-4.10.3.war拷贝到Tomcat的webapp目录下改名为solr.war

2.    启动tomcat后,solr.war自动解压,将原来的solr.war删除。

3.    拷贝example\lib\ext 目录下所有jar包到Tomcat的webapp\solr\WEB-INF\lib目录下
4.    修改Tomcat目录 下webapp\solr\WEB-INF\web.xml文件,如下所示:
    设置Solr home

    <env-entry>
    <env-entry-name>solr/home</env-entry-name>
    <env-entry-value>f:/develop/solr/solrhome</env-entry-value>
    <env-entry-type>java.lang.String</env-entry-type>
    </env-entry>

5.    拷贝log4j.properties文件

    在 Tomcat下webapps\solr\WEB-INF目录中创建文件 classes文件夹,
    复制Solr目录下example\resources\log4j.properties至Tomcat下webapps\solr\WEB-INF\classes目录 

启动Tomcat

启动tomcat服务器,在浏览器输入: localhost/solr

Dashboard

仪表盘,显示了该Solr实例开始启动运行的时间、版本、系统资源、jvm等信息。

Logging

Solr运行日志信息

Cloud

Cloud即SolrCloud,即Solr云(集群),当使用Solr Cloud模式运行时会显示此菜单

Core Admin

Solr Core的管理界面。Solr Core 是Solr的一个独立运行实例单位,它可以对外提供索引和搜索服务,
一个Solr工程可以运行多个SolrCore(Solr实例),一个Core对应一个索引目录。

java properties

Solr在JVM 运行环境中的属性信息,包括类路径、文件编码、jvm内存设置等信息。

Tread Dump

显示Solr Server中当前活跃线程信息,同时也可以跟踪线程运行栈信息。

Analysis

通过此界面可以测试索引分析器和搜索分析器的执行情况。

dataimport

可以定义数据导入处理器,从关系数据库将数据导入 到Solr索引库中。

Document

通过此菜单可以创建索引、更新索引、删除索引等操作
/update表示更新索引,solr默认根据id(唯一约束)域来更新Document的内容,
如果根据id值搜索不到id域则会执行添加操作,如果找到则更新。

query

通过/select执行搜索索引,必须指定“q”查询条件方可搜索。

多core配置

复制原来的core目录为collection2
修改collection2下的core.properties:
name=collection2

Solr索引

scheam.xml

schema.xml,在SolrCore的conf目录下,它是Solr数据表配置文件,它定义了加入索引的数据的数据类型的。
主要包括FieldTypes、Fields和其他的一些缺省设置。

FieldType域类型定义

下边“text_general”是Solr默认提供的FieldType,通过它说明FieldType定义的内容:

 <fieldType name="text_general" class="solr.TextField" positionIncrementGap="100">
  <analyzer type="index">
    <tokenizer class="solr.StandardTokenizerFactory"/>
    <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
    <!-- in this example, we will only use synonyms at query time
    <filter class="solr.SynonymFilterFactory" synonyms="index_synonyms.txt" ignoreCase="true" expand="false"/>
    -->
    <filter class="solr.LowerCaseFilterFactory"/>
  </analyzer>
  <analyzer type="query">
    <tokenizer class="solr.StandardTokenizerFactory"/>
    <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
    <filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
    <filter class="solr.LowerCaseFilterFactory"/>
  </analyzer>
</fieldType>

FieldType子结点包括:name,class,positionIncrementGap等一些参数:

name:是这个FieldType的名称

class:是Solr提供的包solr.TextField,solr.TextField 允许用户通过分析器来定制索引和查询,
分析器包括一个分词器(tokenizer)和多个过滤器(filter)

positionIncrementGap:可选属性,定义在同一个文档中此类型数据的空白间隔,
避免短语匹配错误,此值相当于Lucene的短语查询设置slop值,根据经验设置为100。

在FieldType定义的时候最重要的就是,
定义这个类型的数据在建立索引和进行查询的时候要使用的分析器analyzer,包括分词和过滤

索引分析器中:使用solr.StandardTokenizerFactory标准分词器,
solr.StopFilterFactory停用词过滤器,
solr.LowerCaseFilterFactory小写过滤器。

搜索分析器中:使用solr.StandardTokenizerFactory标准分词器,
solr.StopFilterFactory停用词过滤器,这里还用到了solr.SynonymFilterFactory同义词过滤器。

Field定义

在solr中,Field要先定义后使用。
在fields结点内定义具体的Field,filed定义包括name,
type(为之前定义过的各种FieldType),indexed(是否被索引),stored(是否被储存),multiValued(是否存储多个值,比如多个商品图片)等属性。

如下:

<field name="name" type="text_general" indexed="true" stored="true"/>
<field name="features" type="text_general" indexed="true" stored="true" multiValued="true"/>

multiValued:该Field如果要存储多个值时设置为true,solr允许一个Field存储多个值,比如存储一个用户的好友id(多个),
商品的图片(多个,大图和小图),通过使用solr查询要看出返回给客户端是数组。

uniqueKey

Solr中默认定义唯一主键key为id域,如下:

<uniqueKey>id</uniqueKey>

Solr在删除、更新索引时使用id域进行判断,也可以自定义唯一主键。

copyField复制域

copyField复制域,可以将多个Field复制到一个Field中,以便进行统一的检索:
比如,输入关键字搜索title标题内容content,

定义title、content、text的域:

<field name="title" type="text_general" indexed="true" stored="true" multiValued="true"/>
<field name="subject" type="text_general" indexed="true" stored="true"/>
<field name="description" type="text_general" indexed="true" stored="true"/>

根据关键字只搜索text域的内容就相当于搜索title和content,将title和content复制到text中,如下:

<copyField source="cat" dest="text"/>
<copyField source="name" dest="text"/>
<copyField source="manu" dest="text"/>
<copyField source="features" dest="text"/>
<copyField source="includes" dest="text"/>
<copyField source="manu" dest="manu_exact"/>

dynamicField(动态字段)

动态字段就是不用指定具体的名称,只要定义字段名称的规则,
例如定义一个 dynamicField,name 为*_i,定义它的type为text,
那么在使用这个字段的时候,任何以_i结尾的字段都被认为是符合这个定义的,
例如:name_i,gender_i,school_i等。

自定义Field名为:product_title_t,“product_title_t”和scheam.xml中的dynamicField规则匹配成功,如下:

<dynamicField name="*_i"  type="int"  indexed="true" stored="true"/>

“product_title_t”是以“_t”结尾。

Analyzer

安装中文分词器

IKAnalyzer部署

拷贝IKAnalyzer的文件到Tomcat下Solr目录 中

将IKAnalyzer2012FF_u1.jar拷贝到 Tomcat的webapps/solr/WEB-INF/lib 下。
在Tomcat的webapps/solr/WEB-INF/下创建classes目录
将IKAnalyzer.cfg.xml、ext_stopword.dic  mydict.dic  copy到 Tomcat的
webapps/solr/WEB-INF/classes

注意:ext_stopword.dic 和mydict.dic必须保存成无BOM的utf-8类型。

修改schema.xml文件

1.    FieldType

首先需要在types结点内定义一个FieldType子结点,包括name,class,等参数,
name就是这个FieldType的名称,
class指向org.apache.solr.analysis包里面对应的class名称,用来定义这个类型的行为。
在FieldType定义的时候最重要的就是定义这个类型的数据在建立索引和进行查询的时候要使用的分析器analyzer,包括分词和过滤

修改Solr的schema.xml文件,添加FieldType:

<!-- IKAnalyzer-->
     <fieldType name="text_ik" class="solr.TextField">
        <analyzer type="index" isMaxWordLength="false" class="org.wltea.analyzer.lucene.IKAnalyzer"/>
        <analyzer type="query" isMaxWordLength="true" class="org.wltea.analyzer.lucene.IKAnalyzer"/>
     </fieldType>

其中查询采用IK自己的最大分词法,索引则采用它的细粒度分词法.所以各自配置了isMaxWordLength属性.

2.    Field:

FieldType定义好后就可以在fields结点内定义具体的field,filed定义包括:
name,type(即FieldType),indexed(是否被索引),stored(是否被储存),multiValued(是否有多个值)等

<!--IKAnalyzer Field-->
   <field name="title_ik" type="text_ik" indexed="true" stored="true" />
   <field name="content_ik" type="text_ik" indexed="true" stored="false" multiValued="true"/>

设置业务系统Field

如果不使用Solr提供的Field可以针对具体的业务需要自定义一套Field,如下是商品信息Field:

 <!--product-->
<field name="product_name" type="text_ik" indexed="true" stored="true"/>
<field name="product_price"  type="float" indexed="true" stored="true"/>
<field name="product_description" type="text_ik" indexed="true" stored="false" />
<field name="product_picture" type="string" indexed="false" stored="true" />
<field name="product_catalog_name" type="string" indexed="true" stored="true" />

<field name="product_keywords" type="text_ik" indexed="true" stored="false" multiValued="true"/>
<copyField source="product_name" dest="product_keywords"/>
<copyField source="product_description" dest="product_keywords"/>

dataimport-handler

安装dataimport-Handler从关系数据库将数据导入到索引库。

第一步:向SolrCore中加入jar包
在SolrCore目录中创建lib目录,将dataimportHandler和mysql数据库驱动的jar拷贝至lib下:
dataimportHandler在solr安装目录的dist下。
复制到D:\apache-solr-home\contrib\dataimporthandler\lib
mysql驱动复制到D:\apache-solr-home\contrib\db\lib

第二步 : 修改solrconfig.xml文件,添加requestHandler

     <requestHandler name="/dataimport" class="org.apache.solr.handler.dataimport.DataImportHandler">
     <lst name="defaults">
       <str name="config">data-config.xml</str>
     </lst>
      </requestHandler>

第三步:编辑data-config.xml文件,存放在SolrCore的conf目录 

    <?xml version="1.0" encoding="UTF-8" ?>  
    <dataConfig>   
    <dataSource type="JdbcDataSource"   
              driver="com.mysql.jdbc.Driver"   
              url="jdbc:mysql://localhost:3306/lucene"   
              user="w6233834"   
              password="6233834"/>   
        <document>   
            <entity name="product" query="SELECT id,name,catalog_name,price,description,pic FROM products ">
                 <field column="id" name="id"/> 
                 <field column="name" name="product_name"/> 
                 <field column="catalog_name" name="product_catalog_name"/> 
                 <field column="price" name="product_price"/> 
                 <field column="description" name="product_description"/> 
                 <field column="pic" name="product_picture"/> 
            </entity>   
        </document>   
    </dataConfig>

    <field column="id" name="id"/>必须有一个id域,这里使用Solr默认的id域,域值是从关系数据库查询的id列值。
    下边以“product_”开头的Field都是在schema.xml中自定义的商品信息Field。

第四步:重启Tomcat,进入管理界面 -> SolrCore -> dataimport下执行导入

SolrJ完成索引维护

什么是SolrJ

solrj是访问Solr服务的java客户端,提供索引和搜索的请求方法,
SolrJ通常在嵌入在业务系统中,通过SolrJ的API接口操作Solr服务。

如下图:

创建索引

使用SolrJ创建索引,通过调用SolrJ提供的API请求Solr服务,Document通过SolrInputDocument进行构建。

@Test
public void createIndex() throws SolrServerException, IOException{

    //创建HttpSolrServer
    //参数:表示solr服务的访问基础url
    HttpSolrServer server = new HttpSolrServer("http://localhost/solr");
    //通过server添加SolrInputDocument
    SolrInputDocument doc = new SolrInputDocument();
    doc.addField("id", "X001");
    doc.addField("content_ik", "我爱中国");

    server.add(doc);
    //提交操作
    server.commit();
}

说明:根据id(唯一约束)域来更新Document的内容,如果根据id值搜索不到id域则会执行添加操作,如果找到则更新。

删除索引

@Test
public void deleteIndex() throws SolrServerException, IOException{
    //创建HttpSolrServer
    //参数:表示solr服务的访问基础url
    HttpSolrServer server = new HttpSolrServer("http://localhost/solr");

    server.deleteById("CX001");

    server.commit();
}    


@Test
public void deleteIndexByQuery() throws SolrServerException, IOException{
    //创建HttpSolrServer
    //参数:表示solr服务的访问基础url
    HttpSolrServer server = new HttpSolrServer("http://localhost/solr");

    //根据条件删除
    server.deleteByQuery("id:cx001");

    //批量删除
    server.deleteByQuery("content_ik:我爱中国");

    server.commit();
}

复杂查询

@Test
public void searchIndex02() throws SolrServerException{
    //创建HttpSolrServer
    //参数:表示solr服务的访问基础url
    HttpSolrServer server = new HttpSolrServer("http://localhost/solr");

    //创建查询对象
    SolrQuery query = new SolrQuery();

    //设置查询条件
    query.setQuery("小黄人");

    //设置过滤条件
    query.addFilterQuery("product_catalog_name:幽默杂货");
    query.addFilterQuery("product_price:[5 TO 20]");

    //设置排序
    query.setSort("product_price", ORDER.desc);

    //设置分页信息
    query.setStart(0);
    query.setRows(10);

    //设置需要显示的域列表
    query.setFields("id,product_name,product_price,product_catalog_name,product_picture");

    //设置默认搜索域
    query.set("df", "product_keywords");

    //设置高亮
    query.setHighlight(true);
    query.addHighlightField("product_name");
    query.setHighlightSimplePre("<font style=\"color:red\">");
    query.setHighlightSimplePost("</font>");

    QueryResponse response = server.query(query);

    //获取查询结果
    SolrDocumentList results = response.getResults();

    //匹配出的所有商品记录条数
    long numFound = results.getNumFound();

    System.out.println("匹配出的所有商品记录条数: "+numFound);

    //获取高亮信息
    Map<String, Map<String, List<String>>> highlighting = response.getHighlighting();

    for (SolrDocument solrDocument : results) {

        System.out.println("商品id: "+solrDocument.get("id"));
        System.out.println("商品name: "+solrDocument.get("product_name"));
        System.out.println("商品price: "+solrDocument.get("product_price"));
        System.out.println("商品分类名称: "+solrDocument.get("product_catalog_name"));
        System.out.println("商品图片: "+solrDocument.get("product_picture"));

        List<String> list = highlighting.get(solrDocument.get("id")).get("product_name");

        if(list!= null) System.out.println("显示高亮信息: "+list.get(0));

        System.out.println("====================================================");

    }

}