最近对公司一个使用Lucene的搜索服务进行了维护,踩了一些坑,总结了一点经验,在此做个记录
1、分享一个十分有用的视频教程
由于Lucene没有中文官方文档,英文API文档对新手来说看得十分痛苦,加上中文互联网上Lucene相关的博客内容质量参差不齐,找好文章的概率无异于屎里淘金,所以我建议Lucene新手看一下黑马的这个视频,里面的内容相当详实,质量很高。黑马程序员Lucene全文检索技术,从底层到实战应用Lucene全套教程
2、分词器的使用和词典的维护很重要
在起点看过小说的人一定经历过在正常文章中被莫名其妙的星号统治的经历,比如“一个叫※※因斯坦的人”、“我们昨天去那个洞※※做了一次探险”,这就是起点客户端使用简单的黑名单字符匹配造成的恶果。为了提高搜索质量,一个良好的分词词典是很有必要的。
以常用的IKAnalyzer为例,它支持自定义词典和黑名单词典,只要在IKAnalyzer的配置文件中指定两种词典的位置即可。
以文本“去二仙桥走成华大道”为例,在使用IKAnalyzer默认配置分词时,结果如下:
在自定义词典中配置了“二仙桥”和“成华大道”后,分词结果如下:
我在网上找到了IKAnalyzer的发布包,如果想要在自己电脑上测试的可以自行下载。
链接: https://pan.baidu.com/s/1JH4ZnL8CTfj3fOV472GbvA?pwd=7ven 提取码: 7ven
PS:在创建索引和进行搜索时,务必使用同样的分词器,以保证搜索时的分词效果和创建索引时相同。
3、多条件搜索和分组(分类)搜索的实现
我所在的项目是某手机银行APP的后台,这个搜索服务的功能是为手机银行APP提供代销理财、基金、保险产品、客户端功能菜单、网点信息等数据的搜索服务。
本次修改的业务背景是这样的:APP方面新开发了一个老年版客户端,也要接入这个搜索服务。用户在设置中选择切换到老年版,APP的页面布局就会发生相应变化。由于面向的是老年人,所以搜索服务需要过滤掉基金、保险等产品,只保留基础的菜单、网点信息和代销理财等产品。
针对这一需求,有两种处理方案,第一种方案是搜索服务本身不作改动,只在前端做修改,在检测到是老年版搜索页面时,不展示基金、保险等产品;第二种方案是改动搜索服务,通过请求参数判断是来自老年版客户端的请求时,在搜索时便不搜索基金、保险等产品。出于效率以及节约网络资源方面的考虑,我们自然选择的是第二种方案。要实现搜索特定分类的产品,需要在创建索引对数据进行分类,在搜索时也要指定对应的分类。下面分别说明:
在创建索引时指定分类:
最简单的办法就是在创建Document时增加一个名为productType的StringField。之所以要用StringField,是因为Lucene不会对这个字段分词,这样在搜索时只要指定productType的值,便会且只会搜到对应类型的数据了。
下面是简单的实例代码,我在索引中分别添加了一个productType为branchInfo和finance的Document,分别对应网点数据和理财产品。以下代码均基于Lucene8,不同版本的Lucene API可能稍有不同,但大差不差。
private void createIndex() {
IndexWriterConfig config = new IndexWriterConfig(new IKAnalyzer());
try (Directory directory = FSDirectory.open(Paths.get("指定索引存储的文件夹"));
IndexWriter indexWriter = new IndexWriter(directory, config)) {
// 创建一个网点数据的Document
Document branchInfoDoc = new Document();
branchInfoDoc.add(new StringField("productType", "branchInfo", Field.Store.YES));
branchInfoDoc.add(new TextField("productName", "北京XX路XX支行", Field.Store.YES));
branchInfoDoc.add(new TextField("其它字段", "其它字段的信息", Field.Store.YES));
// 创建一个理财产品的Document
Document financeDoc = new Document();
financeDoc.add(new StringField("productType", "finance", Field.Store.YES));
financeDoc.add(new TextField("productName", "平安理财-周周成长固定收益类净值型理财产品", Field.Store.YES));
financeDoc.add(new TextField("其它字段", "其它字段的信息", Field.Store.YES));
// 使用indexWriter将document加入到索引中
indexWriter.addDocument(branchInfoDoc);
indexWriter.addDocument(financeDoc);
} catch (IOException e) {
e.printStackTrace();
}
}
在搜索时指定分类
假设我要在所有网点中搜索“ABC支行”的信息,那么我就需要指定两个搜索条件:productType=branchInfo,productName=ABC支行。代码示例如下:
private TopDocs search() {
try {
DirectoryReader reader = DirectoryReader.open(FSDirectory.open(Paths.get("索引存储的文件夹")));
IndexSearcher searcher = new IndexSearcher(reader);
BooleanQuery.Builder builder = new BooleanQuery.Builder();
// 指定产品类型查询
Query typeQuery = new TermQuery(new Term("productType", "branchInfo"));
// 指定关键词查询
Query keywordQuery = new QueryParser("productName", new IKAnalyzer()).parse("ABC支行");
// 组合查询条件
builder.add(typeQuery, BooleanClause.Occur.MUST);
builder.add(keywordQuery, BooleanClause.Occur.MUST);
BooleanQuery query = builder.build();
// 返回查询结果
return searcher.search(query, 100);
} catch (IOException | ParseException e) {
e.printStackTrace();
}
return null;
}
BooleanQuery就是组合查询,可以在BooleanQuery.Builder.add()方法中传入多个子查询条件。
BooleanClause.Occur是一个枚举类,一共有4种取值:MUST
,FILTER
,SHOULD
,MUST_NOT
。Occur可以看做是逻辑运算符,MUST相当于AND,SHOULD相当于OR,MUST_NOT相当于NOT。FILTER在功能上和MUST相同,都是相当于AND,但是FILTER修饰的查询条件不参与计算相关度得分,也是因此FILTER的查询效率比MUST要高。
举个栗子:假如我想搜索所有名字里含有“发”字,并且开放日期在今年3月1号之后的理财产品。那么我就得到了2个子查询条件
①productName里包含有发”
②startTime>3月1号
这两个条件由于都必须满足,所以Occur都应取MUST。但是在对所有查询结果进行相关度排序的时候,应当是名字里“发”字越多的产品越靠前,而条件②只需要满足即可,至于究竟开放日是4月1号还是5月1号,并不影响搜索结果的权重,此时,可以把条件②的Occur换成FILTER,以换取更高的查询效率。
4、根据需要选择正确的Field
在前面的例子中,我在创建索引时,产品类型productType使用的Field是StringField,而产品名称productName使用的Field是TextField,之所以有这样的差别,就在于StringField在创建索引时不分词,而TextField分词。如果要对使用一个字段对产品进行分类,那么注意一定要使用StringField而不要使用TextField。StringField和TextField是使用最多的文本类型的Field,但除此之外,Field抽象类之下还有很多的子类,需要根据具体需求选择对应的类型。