项目实训lucene学习及使用记录

写在前面
这篇博客主要是记录我为什么使用lucene和再项目中使用的一个过程,可能没有什么安装和使用教程,里面涉及的一些对lucene的理解可能有些偏颇,毕竟我也就是靠着自己半吊子的英语看着它的文档和结合一些博客来摸索的。
这里推荐一个我看到写的不错的老哥的博客,另外附上lucene官方文档


动机
项目实训里面的搜索功能,虽然队友说其实只需要提供职位名和公司名通过sql的like来做就好了,不过我想着自己之前从来没有做过全文搜索同时也觉得like来搜索的速度太慢了,所以我考虑研究下lucene来实现对我们系统中信息的全文搜索。
首先稍微介绍以下lucene,这是apache基金下的一个开源搜索引擎框架,solr和ES都是基于这个框架。我也是从solr那边了解到lucene的。如果使用solr的话就需要在服务器上另外启动一个solr的服务,然后传数据交给solr来建立索引,再请求solr返回搜索内容。考虑到我们服务器捉急的性能,和我们有限的搜索需求,我觉得还是就用lucene自己写就好了(也算是锻炼一下自己。

简单介绍
lucene实现全文搜索并把结果排序返回包括两部分,建立索引和搜索过程。
索引是lucene搜索的基础也是lucene搜索能够比使用sql的like进行搜索速度快的原因。lucene使用倒排索引来存储文件,而这种索引的组织方式不同于innoDB B+树存储方式,是使用字典树或者说前缀树的方式进行,天生是用来对查询文本的结构,而且在索引时根据情况会进行分词处理,让原本出现在文本中间的文字也能作为索引的对象。如果是在sql中使用like的搜索方式,只有形如stu%这种%在后面的搜索方式能够用到索引,其他都只能从头遍历。而lucen则会直接通过索引进行查找。
所以要使用lucene实现简单的全文搜索就要做好索引建立和搜索两个过程。这在lucene提供的demo代码中可以看到,这里贴出一点我加了注释的一些代码。

SmartChineseAnalyzer smartChineseAnalyzer = new SmartChineseAnalyzer();//提供中文分词的分析器
//ananlyzer主要的工作就是把一段文本分成一个个token,其实就是分词的过程。要注意stop word的概念
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(smartChineseAnalyzer);
indexWriterConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE);//这里的OpenMode的设置貌似不是给lucene里什么功能看的,从demo程序中可以看出,还是需要用户自己判断是需要以update的方式建立索引还是用add的方式建立索引

try {
    IndexWriter indexWriter = new IndexWriter(FSDirectory.open(
            Paths.get(dir)), indexWriterConfig);
    doc.add(new TextField("detail", jobPosition.detail, Field.Store.YES));//Store.YES表示传入的内容会另外进行保存,以便后续后续取用。
    //这里涉及到doc和field的概念,doc可以简单理解为sql数据库中的一条记录(一行),field就是一个属性(一列)。
    //Field的类型有很多可以查看文档,要注意的是StringField和TextField的区别,前者在做索引的时候不会进行分词,而后者会。在搜索的时候前者需要完全匹配,后者只需要匹配到一个词就行
    indexWriter.addDocument(doc);//把doc加入到索引文件中
    indexWriter.close();
} catch (IOException e) {
    e.printStackTrace();
}

另一方面是搜索

try {
    IndexSearcher indexSearcher = new IndexSearcher(
            DirectoryReader.open(
    FSDirectory.open(Paths.get(dir))));//这里看到searcher不像indexer,不需要传入configure,没有提供analyzer
	
    QueryParser parser = new QueryParser("detail", new SmartChineseAnalyzer());//这是一个比较基础的queryParser,会把查询串进行分词(如果可以)然后把分到的每个词作为关键词,把“detail”作为一个field创建TermQuery,然后通过BooleanQuery把这些query进行复合。
    //可以看出这个parser还比较简单,也有局限只能parse成指定的一个field。为了获得更多的特性可以查看文档
    Query query = parser.parse(keyWord);
//            Query query = new TermQuery(new Term("detail", keyWord));//查询并非一定要用parser来建立,具体每一种query的用法参见文档
    TopDocs topDocs = indexSearcher.search(query, 10);//搜索,打分并返回最高的10个docs
    //关于打分的依据主要是文档中包含关键词的数量和文档的长度。如果要按照其他标准进行排序,比如添加时间维度的考虑,可以通过修改weight等相关内容实现。
    for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
        System.out.print(scoreDoc.doc + "  ");
        System.out.println(scoreDoc.score);
        System.out.println("-------------------detail------------------");
        System.out.println(indexSearcher.doc(scoreDoc.doc).get("detail"));
    }
} catch (IOException e) {
    e.printStackTrace();
}catch (ParseException e) {
    e.printStackTrace();
}

在我们的项目中
我们项目中希望先实现一个简单的按照职位信息中和关键字的匹配程度来搜索和排序的功能。这里面主要的难点其实涉及到一个join的问题。我们的岗位信息其实包含了两部分,job和position,position是属于job的,job中包含了于其关联的position共同的信息,包括公司名称、公司介绍、福利等。而我们显示的时候是显示position,所以我们需要实现job匹配的分数累加到position匹配的分数上,最终对position的分数进行排序并返回查询结果。
这种需求类似于sql中的join,在lucene中这方面的支持也叫做join,具体使用的方法可以下src,找到join模块下的test,里面有基本使用的代码。
join有index时的join,有search时的join。性能上的差别是使用index-time join在查询时只需要查一遍而search-time join需要查询两次。所以我使用index-time join。
虽然叫做index-time join,但是在index时需要我们做的工作其实不多,就是记住parent doc和child doc之间相对的位置关系即可。在很多child doc之后紧接着加入一个parent doc。(其实一般情况下一个index文件中原则上是可以放入任意格式的doc,但是为了实现join必须遵守这种规则。
另外注意两种doc尽量不要有相同名称的field,这在搜索的时候可能造成错误
在这里插入图片描述
除了parent filter,parent query 也不能匹配任何文档。
据我推测,是parent query和parent filter不能匹配到child doc中的内容。

建完索引就要进行查询,查询中为了把parent的分数给child或是把child的分数给parent需要使用ToChildBlockJoinQueryToParentBlockJoinQueryParentChildrenBlockJoinQuery
下面我用lucene的测试代码进行简单的讲解

DirectoryReader r = DirectoryReader.open(dir);
IndexSearcher s = new IndexSearcher(r);

// Create a filter that defines "parent" documents in the index - in this case resumes
BitSetProducer parentsFilter = new QueryBitSetProducer(new TermQuery(new Term("docType", "job")));//filter其实也是一个query,它是用来区分parent和child的,docType是parent中的一个field,值为job。只要找到docType值为job的就知道这是一个parent doc,进而也能找到它的child
CheckJoinIndex.check(r, parentsFilter);//测试当前index文件是否符合join的条件

//下面的代码建议从最后往上看
// Define child document criteria (finds an example of relevant work experience)
BooleanQuery.Builder childQuery = new BooleanQuery.Builder();//规定了child要符合的内容
childQuery.add(new BooleanClause(new TermQuery(new Term("skill", "java")), BooleanClause.Occur.MUST));
childQuery.add(new BooleanClause(IntPoint.newRangeQuery("year", 2006, 2011), BooleanClause.Occur.MUST));

// Define parent document criteria (find a resident in the UK)
Query parentQuery = new TermQuery(new Term("country", "United Kingdom"));//规定要找符合这种条件的parent

// Now join "up" (map parent hits to child docs) instead...:
ToChildBlockJoinQuery parentJoinQuery = new ToChildBlockJoinQuery(parentQuery, parentsFilter);
//(纯猜测)从符合parentsFilter的doc中找符合parentQuery的,再找到parent对应的children
BooleanQuery.Builder fullChildQuery = new BooleanQuery.Builder();
fullChildQuery.add(new BooleanClause(parentJoinQuery, BooleanClause.Occur.SHOULD));
fullChildQuery.add(new BooleanClause(childQuery.build(), BooleanClause.Occur.SHOULD));

TopDocs hits = s.search(fullChildQuery.build(), 10);//fullQuery就是把parentJoinQuery的结果和childQuery的结果进行合并
Document childDoc = s.doc(hits.scoreDocs[0].doc);
System.out.println("CHILD = " + childDoc + " docID=" + hits.scoreDocs[0].doc);

r.close();
dir.close();

按照上面的基本思路就能完成我们的想要达到的效果
另外我关注的问题是对搜索的翻页的支持,就是要查询排在0-10个的doc,查询10-20的doc依此类推。但是lucene不支持查询指定区间的某些结果,只提供了一个searchAfter,这也许于lucene中使用的取topk的数据结构有个,一般的数据结构是使用最小堆来实现的,这样也不难理解有这种限制了。不过要进行一个范围的搜索就只能从头查或是保存一个查询范围之前的一个scoreDoc了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值