最近刚忙完一个电影网站,其中的全文搜索的功能我就是用Solr完成的,在此将我在开发中遇到的问题以及怎样解决问题的经验拿出来与大家分享。
我们这个网站有一个站内搜索的功能,例如站内新闻,输入关键字去搜索。数据库里有上万条数据,如果去挨个like,效率会很低,经领导指点,说可以试一试 HibernateSearch和Apache solr结合mmseg4j分词进行全文检索,于是我就开始我的Solr之旅。
一开始在网上搜了很多例子拿来入门,首先是分词,mmseg4j是用来分词的,常用的分词分析器有三种:MaxWordAnalyzer(最大分 词),SimpleAnalyzer(简单的),ComplexAnalyzer(复杂的),最开始我用的是ComplexAnalyzer,看上去很不 错,后来遇到了个小问题,例如“吴宇森吃羊肉串”,经过ComplexAnalyzer分分词后,用Solr去搜“吴宇森”会返回想要的结果,但是“吴 宇”去搜什么也没返回。这是一个让人很头疼的问题,于是我试验了MaxWordAnalyzer,发现“吴宇”,“吴宇森”都能返回正确的结果,这才是我 们想要的。
一段测试例子,大家可以拿去试一下MaxWordAnalyzer,SimpleAnalyzer,ComplexAnalyzer之间的区别。
import java.io.IOException; import junit.framework.TestCase; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.queryParser.ParseException; import org.apache.lucene.queryParser.QueryParser; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TopDocs; import org.apache.lucene.store.Directory; import org.apache.lucene.store.RAMDirectory; import com.chenlb.mmseg4j.analysis.MaxWordAnalyzer; public class LuceneUseSimpleAnalyzerTest extends TestCase { Directory dir; Analyzer analyzer; @Override protected void setUp() throws Exception { String txt = "吴宇森吃羊肉串"; //analyzer = new SimpleAnalyzer(); //analyzer = new ComplexAnalyzer(); //分词分析器 analyzer = new MaxWordAnalyzer(); //内存索引对象 dir = new RAMDirectory(); IndexWriter iw = new IndexWriter(dir, analyzer); Document doc = new Document(); //Field.Store.YES表示在索引里将整条数据存储 doc.add(new Field("txt", txt, Field.Store.YES, Field.Index.ANALYZED)); iw.addDocument(doc); iw.commit(); iw.optimize(); iw.close(); } public void testSearch() { try { //实例化搜索器 IndexSearcher searcher = new IndexSearcher(dir); //构造Query对象 QueryParser qp = new QueryParser("txt", analyzer); Query q = qp.parse("吴宇森"); System.out.println(q); //搜索相似度最高的10条 TopDocs tds = searcher.search(q, 10); //命中的个数 System.out.println("======size:" + tds.totalHits + "========"); //输出返回结果 for (ScoreDoc sd : tds.scoreDocs) { System.out.println(sd.score); System.out.println(searcher.doc(sd.doc).get("txt")); } } catch (CorruptIndexException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ParseException e) { e.printStackTrace(); } } }
可见mmseg4j是以非常灵活的分词工具而且分词速度也很快,2.5M的长篇小说用Complex模式是5.3秒左右,用Simple模式是2.9秒 左右,好的,分词搞定了。再提一点,如何将mmseg4j应用到我们的Entity上的,我用的是元数据声明的方式:
@Field(name = "newsTitle", store = Store.YES, index = Index.TOKENIZED, analyzer = @Analyzer(impl = MaxWordAnalyzer.class)) private String title; @Field(name = "newsSummary", store = Store.YES, index = Index.TOKENIZED, analyzer = @Analyzer(impl = MaxWordAnalyzer.class)) private String summary; @Field(name = "newsPublishedTime", store = Store.YES, index = Index.TOKENIZED, analyzer = @Analyzer(impl = MaxWordAnalyzer.class)) private Date publishedTime;
这里面的name = "newTitle",name = "newsSummry",是生成索引的名称,同时还要在Solr的schema.xml里定义与之相对应的field:
<field name="newsTitle" type="textMax" indexed="true" stored="true"/> <field name="newsSummary" type="textMax" indexed="true" stored="true"/> <field name="newsPublishedTime" type="date" indexed="true" stored="true" default="NOW" multiValued="false"/>
说到这,再提一下如何将集合建立索引以及配置,例如OneToMany,首先在一的一端进行声明:
@IndexedEmbedded private Set<PlotKeyword> plotKeywords = new HashSet<PlotKeyword>(); //关键词联想
然后在多的一端指定到底是哪一个字段建立索引:
@Field(store = Store.YES, index = Index.TOKENIZED, analyzer = @Analyzer(impl = MaxWordAnalyzer.class)) private String summary;
在这里你可以不给你的索引字段加name,默认会在索引库里有name="plotKeywords.summary"这样一个索引名称,同时也不要忘记在Solr的schema.xml里定义与之相对应的field:
<field name="plotKeywords.summary" type="textMax" indexed="true" stored="true"/>
type="textMax" ,对应schema.xml里的:
<fieldType name="textMax" class="solr.TextField" positionIncrementGap="100" > <analyzer> <tokenizer class="com.chenlb.mmseg4j.solr.MMSegTokenizerFactory" mode="max-word" dicPath="D:/data/dict"/> <filter class="solr.LowerCaseFilterFactory"/> </analyzer> </fieldType>
dicPath="D:/data/dict"是词库的路径,如果我们用的是MaxWordAnalyzer进行的分词,那么为了保证Solr能搜到我们 想要的结果,必须在schema.xml里配置上面一段fieldType,指定mode="max-word",这样Solr就会按照最大分词去给我们 返回与之相对应的结果,“吴宇”,“吴宇森”,都会返回结果。
如何将Solr集成到我们的项目中呢?很简单,就拿Tomcat举例,解压Solr,将..\apache-solr-1.3.0\example \solr 文件夹拷贝到Tmocat的bin文件夹下,配置schema.xml上面提到了,还有索引库路径,在solrconfig.xml里配置:
<dataDir>${solr.data.dir:D:/data/index}</dataDir>
然后在web.xml里面配置Solr的SolrDispatchFilter:
<filter> <filter-name>SolrRequestFilter</filter-name> <filter-class>org.apache.solr.servlet.SolrDispatchFilter</filter-class> </filter> <filter-mapping> <filter-name>SolrRequestFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
在这里就不提引入的第三方jar文件,可以参考Solr文档。要注意的是HibernateSearch结合mmseg4j分词时候,我们用到了自己的词 库,需要指定一个虚拟机参数:-Dmmseg.dic.path=d:/data/dict,在这里我们将分词用到的词库放到了d:/data /dict,有可能词库过大造成虚拟机内存溢出,还要加参数: -Xmx256m -Xmx512m。就这样Solr就集成到我们的项目中了,而且HibernateSearch帮我们管理了索引库,增加,删除,还有修改没有个被索引的 字段,HibernateSearch都会帮我们同步索引库。
Solr的查询语法很灵活,大家可以参考官方文档,在这里我要提一下查询or和and,Solr默认的配置是or,在schema.xml里:
<!-- SolrQueryParser configuration: defaultOperator="AND|OR" --> <solrQueryParser defaultOperator="OR"/>
如果想用到and,还有一种方式在Url里指定参数fq,过滤条件,例如:q=kaka&fq=age:[15 to 20],搜索条件是kaka,并且age是15到20岁之间。
Solr支持很多种查询输出格式,用参数wt指定,有xml, json, php, phps,我这里用到了json。一段JavaScript供大家参考:
$.ajax({ type: "post",//使用post方法访问后台 dataType: "json",//返回json格式的数据 url: "/select/",//要访问的后台地址 data: "q=" + keywords + "&start=" + 0 + "&rows=" + 5 + "&indent=" + "on" + "&wt=" + "json" + "&sort=" + "newsPublishedTime asc",//要发送的数据 success: function(j) {//msg为返回的数据,在这里做数据绑定 var docs = j.response.docs; for (var i=0; i<docs.length; i++) { var date = docs[i].newsPublishedTime; var title = docs[i].newsTitle; var summary = docs[i].newsSummary; } } });
一切都顺利进行,但是马上就遇到了一个新问题,重建索引后Solr也必须重新拿启动,要么Solr不会用最新的索引库,Solr有自己的缓存,这个很让让 人头疼,如果删除一条数据,HibernateSearch会帮我们同步索引库,但是Solr的缓存还是缓存之前没有删除的数据。我试图通过配置文件不用 Solr的缓存,但是没有效果,根据官方文档上说的意思是:Solr的缓存是干不掉的。这绝对不是我们想要的结果,但是最后还是解决了,在 sorlconfig.xml里配置:
<requestHandler name="/update" class="solr.XmlUpdateRequestHandler" />
然后访问/update?commit=true,Solr就会放弃之前的缓存,去重新检索我们的索引库建立新的缓存,这正是我们想要的结果。