1、了解搜索技术
1.1 什么是搜索
简单的说,搜索就是搜寻、查找,在IT行业中就是指用户输入关键字,通过相应的算法,查询并返回用户所需要的信息。
1.2 普通的数据库搜索
类似:select * from 表名 where 字段名 like ‘%关键字%’
例如:select * from article where content like ’%here%’
结果: where here shere
1.3 新的业务需求
比如,用户在百度文本框中输入,“吃饭睡觉写程序”,会出现的以下结果:
从结果可以看出,百度搜索具备以下明显特点:
1、即使在相关结果数量接近500万时,也能快速得出结果。
2、搜索的结果不仅仅局限于完整的“吃饭睡觉写程序”这一短语,而是将此短语拆分成,“写程序”,“吃饭”,“睡觉”,“程序”等关键字。
3、对拆分后的搜索关键字进行标红显示。
4、…
问题:上述功能,使用大家以前学过的数据库搜索能够方便实现吗?
1.4 普通的数据库搜索的缺陷
类似:select * from 表名 where 字段名 like ‘%关键字%’
例如:select * from article where content like ’%here%’
结果: where here shere
1、因为没有通过高效的索引方式,所以查询的速度在大量数据的情况下是很慢。
2、搜索效果比较差,只能对用户输入的完整关键字首尾位进行模糊匹配。用户搜索的结果误多输入一个字符,可能就导致查询出的结果远离用户的预期。
2、 搜索技术
2.1 搜索引擎的种类
搜索引擎按照功能通常分为垂直搜索和综合搜索。
1、垂直搜索是指专门针对某一类信息进行搜索。例如:会搜网主要做商务搜索的,并且提供商务信息。除此之外还有爱看图标网、职友集等。2、综合搜索是指对众多信息进行综合性的搜索。例如:百度、谷歌、搜狗、360搜索等。
2.2 倒排索引
倒排索引又叫反向索引(右下图)以字或词为文档中出现的位置情况。
在实际的运用中,我们可以对数据库中原始的数据结构(左图),在业务空闲时事先根据左图内容,创建新的倒排索引结构的数据区域(右图)。
用户有查询需求时,先访问倒排索引数据区域(右图),得出文档id后,通过文档id即可快速,准确的通过左图找到具体的文档内容。
这一过程,可以通过我们自己写程序来实现,也可以借用已经抽象出来的通用开源技术来实现。
3 Lucene概述
3.1 什么是Lucene
LOGO:
- Lucene是一套用于全文检索和搜寻的开源程序库,由Apache软件基金会支持和提供
- Lucene提供了一个简单却强大的应用程序接口(API),能够做全文索引和搜寻,在Java开发环境里Lucene是一个成熟的免费开放源代码工具
- Lucene并不是现成的搜索引擎产品,但可以用来制作搜索引擎产品
- Lucene是apache软件基金会发布的一个开放源代码的全文检索引擎工具包,由资深全文检索专家Doug Cutting所撰写,它是一个全文检索引擎的架构,提供了完整的创建索引和查询索引,以及部分文本分析的引擎,Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎,Lucene在全文检索领域是一个经典的祖先,现在很多检索引擎都是在其基础上创建的,思想是相通的。
3.2 什么是全文检索?
3.3 Lucene下载及版本问题
官网:
- 目前最新的版本是7.x系列,但是在企业中还是用4.x比较多,所以我们学习4.x的版本
老版本下载地址:
http://archive.apache.org/dist/lucene/java/
Lucene是根据关健字来搜索的文本搜索工具,只能在某个网站内部搜索文本内容,不能跨网站搜索
既然谈到了网站内部的搜索,那么我们就谈谈我们熟悉的百度、google那些搜索引擎又是基于什么搜索的呢....
从图上已经看得很清楚,baidu、google等搜索引擎其实是通过网络爬虫的程序来进行搜索的...
3.4 Lucene、Solr、Elasticsearch关系
Lucene:底层的API,工具包
Solr:基于Lucene开发的企业级的搜索引擎产品
Elasticsearch:基于Lucene开发的企业级的搜索引擎产品
4 Lucene的基本使用
使用Lucene的API来实现对索引的增(创建索引)、删(删除索引)、改(修改索引)、查(搜索数据)。
4.1 创建索引
4.1.1 创建索引的流程
文档Document:数据库中一条具体的记录
字段Field:数据库中的每个字段
目录对象Directory:物理存储位置
写出器的配置对象:需要分词器和lucene的版本
4.1.2 添加依赖
<properties><lunece.version>4.10.2</lunece.version></properties><dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version></dependency><!--lucene核心库--><dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-core</artifactId><version>${lunece.version}</version></dependency><!--Lucene的查询解析器--><dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-queryparser</artifactId><version>${lunece.version}</version></dependency><!--lucene的默认分词器库--><dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-analyzers-common</artifactId><version>${lunece.version}</version></dependency><!--lucene的高亮显示--><dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-highlighter</artifactId><version>${lunece.version}</version></dependency></dependencies>
4.1.3 代码实现
步骤:
//1 创建文档对象//2 创建存储目录//3 创建分词器//4 创建索引写入器的配置对象//5 创建索引写入器对象//6 将文档交给索引写入器//7 提交//8 关闭
// 创建索引publicvoidtestCreate() throwsException{ //1 创建文档对象Documentdocument=newDocument(); // 创建并添加字段信息。参数:字段的名称、字段的值、是否存储,这里选Store.YES代表存储到文档列表。Store.NO代表不存储document.add(newStringField("id", "1", Field.Store.YES)); // 这里我们title字段需要用TextField,即创建索引又会被分词。StringField会创建索引,但是不会被分词document.add(newTextField("title", "谷歌地图之父跳槽facebook", Field.Store.YES)); //2 索引目录类,指定索引在硬盘中的位置Directorydirectory=FSDirectory.open(newFile("d:\\indexDir")); //3 创建分词器对象Analyzeranalyzer=newStandardAnalyzer(); //4 索引写出工具的配置对象IndexWriterConfigconf=newIndexWriterConfig(Version.LATEST, analyzer); //5 创建索引的写出工具类。参数:索引的目录和配置信息IndexWriterindexWriter=newIndexWriter(directory, conf); //6 把文档交给IndexWriterindexWriter.addDocument(document); //7 提交indexWriter.commit(); //8 关闭indexWriter.close(); }
4.1.4 使用工具查看索引
4.1.5 创建索引的API详解
4.1.5.1 Document(文档类)
Document:文档对象,是一条原始的数据
4.1.5.2 Field(字段类)
一个Document中可以有很多个不同的字段,每一个字段都是一个Field类的对象。
一个Document中的字段其类型是不确定的,因此Field类就提供了各种不同的子类,来对应这些不同类型的字段。
这些子类有一些不同的特性:
1)DoubleField、FloatField、IntField、LongField、StringField、TextField这些子类一定会被创建索引,但是不会被分词,而且不一定会被存储到文档列表。要通过构造函数中的参数Store来指定:如果Store.YES代表存储,Store.NO代表不存储
2)TextField即创建索引,又会被分词。StringField会创建索引,但是不会被分词。
如果不分词,会造成整个字段作为一个词条,除非用户完全匹配,否则搜索不到:
我们一般,需要搜索的字段,都会做分词:
3)StoreField一定会被存储,但是一定不创建索引
StoredField可以创建各种数据类型的字段:
问题1:如何确定一个字段是否需要存储?
如果一个字段要显示到最终的结果中,那么一定要存储,否则就不存储
问题2:如何确定一个字段是否需要创建索引?
如果要根据这个字段进行搜索,那么这个字段就必须创建索引。
问题3:如何确定一个字段是否需要分词?
前提是这个字段首先要创建索引。然后如果这个字段的值是不可分割的,那么就不需要分词。例如:ID
4.1.5.3 Directory(目录类)
指定索引要存储的位置
FSDirectory:文件系统目录,会把索引库指向本地磁盘。
特点:速度略慢,但是比较安全
RAMDirectory:内存目录,会把索引库保存在内存。
特点:速度快,但是不安全
4.1.5.4 Analyzer(分词器类)
• 提供分词算法,可以把文档中的数据按照算法分词
这些分词器,并没有合适的中文分词器,因此一般我们会用第三方提供的分词器:
一般我们用IK分词器。
4.1.5.5 IK分词器(重要)
- 概述
IK分词器官方版本是不支持Lucene4.X的,有人基于IK的源码做了改造,支持了Lucene4.X:
- 基本使用
引入IK分词器:
<dependency><groupId>com.janeluo</groupId><artifactId>ikanalyzer</artifactId><version>2012_u6</version></dependency>
中文分词更专业:
- 扩展词典和停用词典
IK分词器的词库有限,新增加的词条可以通过配置文件添加到IK的词库中,也可以把一些不用的词条去除:
扩展词典:用来引入一些自定义的新词
停止词典:用来停用一些不必要的词条
字典与配置文件要放在项目正确的位置这样才可生效
结果:分词中,加入了我们新的词,被停用的词语没有被分词:
测试分词方法:
/*** 测试分词* @param line* @return*/publicstaticStringstartIKAnalyzer(Stringline){ IKAnalyzeranalyzer=newIKAnalyzer(); // 使用智能分词analyzer.setUseSmart(true); // 打印分词结果try { returnprintAnalysisResult(analyzer, line); } catch (Exceptione) { e.printStackTrace(); }finally{ if(analyzer!=null){ analyzer.close(); } } returnnull; } /*** 打印分词* @param analyzer* @param keyWord* @return* @throws Exception*/privatestaticStringprintAnalysisResult(Analyzeranalyzer, StringkeyWord) throwsException { Stringresultdata=""; StringinfoData=""; if(keyWord!=""&&keyWord!=null){ TokenStreamtokenStream=analyzer.tokenStream("content", newStringReader(keyWord)); tokenStream.addAttribute(CharTermAttribute.class); tokenStream.reset(); while (tokenStream.incrementToken()) { CharTermAttributecharTermAttribute=tokenStream .getAttribute(CharTermAttribute.class); Stringdest=charTermAttribute.toString().replace("-",""); infoData=infoData+" "+charTermAttribute.toString(); } if(infoData!=""&&infoData!=null){ resultdata=resultdata+infoData.trim()+"\r\n"; }else{ resultdata=""; } } returnresultdata; } publicstaticvoidmain(String[] args) throwsException { Stringword="8月17日,“雄鹰突击-2018”中国和白俄罗斯特种部队联合训练,在北部战区陆军某综合训练基地完成综合演练并举行结训仪式。北部战区副司令员兼北部战区陆军司令员王印芳、白俄罗斯武装力量总参谋长兼国防部第一副部长别洛科涅夫出席结训仪式。"+"记者在综合演练现场看到,联合突击营救行动展开后,双方特战队员混编成多个战斗小组,各小组指挥员通过眼神和手语快速交流现场侦察情况,分配任务、搜索前行、交替掩护等一系列动作顺畅默契,判定目标后迅即展开行动。演练中,中白特战队员密切协同,出色完成立体封控、分区搜歼等一系列行动,赢得两军观摩团阵阵掌声。"; Stringword2="第1条:谷歌地图之父跳槽facebook\n"+"第2条:谷歌地图之父加盟FaceBook\n"+"第3条:谷歌地图创始人拉斯离开谷歌加盟Facebook\n"+"第4条:谷歌地图之父拉斯加盟社交网站Facebook\n"+"第5条:谷歌地图之父跳槽Facebook与Wave项目取消有关"+"微信:weiai923713392"; Stringresult=null; result=startIKAnalyzer(word2); System.out.println("result:"+result); }
补充一下ik分词器 的下载地址:点我下载
4.1.5.6 IndexWriterConfig(索引写出器配置类)
1) 设置配置信息:Lucene的版本和分词器类型
2)设置是否清空索引库中的数据
4.1.5.7 IndexWriter(索引写出器类)
- 索引写出工具,作用就是 实现对索引的增(创建索引)、删(删除索引)、改(修改索引)
- 可以一次创建一个,也可以批量创建索引
// 批量创建索引publicvoidtestCreate2() throwsException{ // 创建文档的集合Collection<Document>docs=newArrayList<>(); // 创建文档对象Documentdocument1=newDocument(); document1.add(newStringField("id", "1", Field.Store.YES)); document1.add(newTextField("title", "谷歌地图之父跳槽facebook", Field.Store.YES)); docs.add(document1); // 创建文档对象Documentdocument2=newDocument(); document2.add(newStringField("id", "2", Field.Store.YES)); document2.add(newTextField("title", "谷歌地图之父加盟FaceBook", Field.Store.YES)); docs.add(document2); // 创建文档对象Documentdocument3=newDocument(); document3.add(newStringField("id", "3", Field.Store.YES)); document3.add(newTextField("title", "谷歌地图创始人拉斯离开谷歌加盟Facebook", Field.Store.YES)); docs.add(document3); // 创建文档对象Documentdocument4=newDocument(); document4.add(newStringField("id", "4", Field.Store.YES)); document4.add(newTextField("title", "谷歌地图之父跳槽Facebook与Wave项目取消有关", Field.Store.YES)); docs.add(document4); // 创建文档对象Documentdocument5=newDocument(); document5.add(newStringField("id", "5", Field.Store.YES)); document5.add(newTextField("title", "谷歌地图之父拉斯加盟社交网站Facebook", Field.Store.YES)); docs.add(document5); // 索引目录类,指定索引在硬盘中的位置Directorydirectory=FSDirectory.open(newFile("d:\\indexDir")); // 引入IK分词器Analyzeranalyzer=newIKAnalyzer(); // 索引写出工具的配置对象IndexWriterConfigconf=newIndexWriterConfig(Version.LATEST, analyzer); // 设置打开方式:OpenMode.APPEND 会在索引库的基础上追加新索引。OpenMode.CREATE会先清空原来数据,再提交新的索引conf.setOpenMode(IndexWriterConfig.OpenMode.CREATE); // 创建索引的写出工具类。参数:索引的目录和配置信息IndexWriterindexWriter=newIndexWriter(directory, conf); // 把文档集合交给IndexWriterindexWriter.addDocuments(docs); // 提交indexWriter.commit(); // 关闭indexWriter.close(); }
4.2 查询索引数据
4.2.1 代码实现
实现步骤:
//1 创建读取目录对象//2 创建索引读取工具//3 创建索引搜索工具//4 创建查询解析器//5 创建查询对象//6 搜索数据//7 各种操作
publicvoidtestSearch() throwsException { // 索引目录对象Directorydirectory=FSDirectory.open(newFile("d:\\indexDir")); // 索引读取工具IndexReaderreader=DirectoryReader.open(directory); // 索引搜索工具IndexSearchersearcher=newIndexSearcher(reader); // 创建查询解析器,两个参数:默认要查询的字段的名称,分词器QueryParserparser=newQueryParser("title", newIKAnalyzer()); // 创建查询对象Queryquery=parser.parse("谷歌"); // 搜索数据,两个参数:查询条件对象要查询的最大结果条数// 返回的结果是 按照匹配度排名得分前N名的文档信息(包含查询到的总条数信息、所有符合条件的文档的编号信息)。TopDocstopDocs=searcher.search(query, 10); // 获取总条数System.out.println("本次搜索共找到"+topDocs.totalHits+"条数据"); // 获取得分文档对象(ScoreDoc)数组.SocreDoc中包含:文档的编号、文档的得分ScoreDoc[] scoreDocs=topDocs.scoreDocs; for (ScoreDocscoreDoc : scoreDocs) { // 取出文档编号intdocID=scoreDoc.doc; // 根据编号去找文档Documentdoc=reader.document(docID); System.out.println("id: "+doc.get("id")); System.out.println("title: "+doc.get("title")); // 取出文档得分System.out.println("得分: "+scoreDoc.score); } }
4.2.2 核心API
4.2.2.1 QueryParser(查询解析器)
1)QueryParser(单一字段的查询解析器)
2)MultiFieldQueryParser(多字段的查询解析器)
4.2.2.2 Query(查询对象,包含要查询的关键词信息)
- 1)通过QueryParser解析关键字,得到查询对象
- 2)自定义查询对象(高级查询)
我们可以通过Query的子类,直接创建查询对象,实现高级查询(后面详细讲)
4.2.2.3 IndexSearch(索引搜索对象,执行搜索功能)
IndexSearch可以帮助我们实现:快速搜索、排序、打分等功能。
IndexSearch需要依赖IndexReader类
查询后得到的结果,就是打分排序后的前N名结果。N可以通过第2个参数来指定:
4.2.2.4 TopDocs(查询结果对象)
通过IndexSearcher对象,我们可以搜索,获取结果:TopDocs对象
在TopDocs中,包含两部分信息:
inttotalHits:查询到的总条数ScoreDoc[] scoreDocs:得分文档对象的数组
4.2.2.5 ScoreDoc(得分文档对象)
ScoreDoc是得分文档对象,包含两部分数据:
intdoc:文档的编号----lucene给文档的一个唯一编号floatscore:文档的得分信息拿到编号后,我们还需要根据编号来获取真正的文档信息
4.3 特殊查询
抽取公用的搜索方法:
publicvoidsearch(Queryquery) throwsException { // 索引目录对象Directorydirectory=FSDirectory.open(newFile("indexDir")); // 索引读取工具IndexReaderreader=DirectoryReader.open(directory); // 索引搜索工具IndexSearchersearcher=newIndexSearcher(reader); // 搜索数据,两个参数:查询条件对象要查询的最大结果条数// 返回的结果是 按照匹配度排名得分前N名的文档信息(包含查询到的总条数信息、所有符合条件的文档的编号信息)。TopDocstopDocs=searcher.search(query, 10); // 获取总条数System.out.println("本次搜索共找到"+topDocs.totalHits+"条数据"); // 获取得分文档对象(ScoreDoc)数组.SocreDoc中包含:文档的编号、文档的得分ScoreDoc[] scoreDocs=topDocs.scoreDocs; for (ScoreDocscoreDoc : scoreDocs) { // 取出文档编号intdocID=scoreDoc.doc; // 根据编号去找文档Documentdoc=reader.document(docID); System.out.println("id: "+doc.get("id")); System.out.println("title: "+doc.get("title")); // 取出文档得分System.out.println("得分: "+scoreDoc.score); } }
4.3.1 TermQuery(词条查询)
/** 测试普通词条查询* 注意:Term(词条)是搜索的最小单位,不可再分词。值必须是字符串!*/publicvoidtestTermQuery() throwsException { // 创建词条查询对象Queryquery=newTermQuery(newTerm("title", "谷歌地图")); search(query); }
4.3.2 WildcardQuery(通配符查询)
/** 测试通配符查询* ? 可以代表任意一个字符* * 可以任意多个任意字符*/publicvoidtestWildCardQuery() throwsException { // 创建查询对象Queryquery=newWildcardQuery(newTerm("title", "*歌*")); search(query); }
4.3.3 FuzzyQuery(模糊查询)
/** 测试模糊查询*/publicvoidtestFuzzyQuery() throwsException { // 创建模糊查询对象:允许用户输错。但是要求错误的最大编辑距离不能超过2// 编辑距离:一个单词到另一个单词最少要修改的次数 facebool --> facebook 需要编辑1次,编辑距离就是1// Query query = new FuzzyQuery(new Term("title","fscevool"));// 可以手动指定编辑距离,但是参数必须在0~2之间Queryquery=newFuzzyQuery(newTerm("title","facevool"),1); search(query); }
4.3.4 NumericRangeQuery(数值范围查询)
/** 测试:数值范围查询* 注意:数值范围查询,可以用来对非String类型的ID进行精确的查找*/publicvoidtestNumericRangeQuery() throwsException{ // 数值范围查询对象,参数:字段名称,最小值、最大值、是否包含最小值、是否包含最大值Queryquery=NumericRangeQuery.newLongRange("id", 2L, 2L, true, true); search(query); }
4.3.5 BooleanQuery(组合查询)
/** 布尔查询:* 布尔查询本身没有查询条件,可以把其它查询通过逻辑运算进行组合!* 交集:Occur.MUST + Occur.MUST* 并集:Occur.SHOULD + Occur.SHOULD* 非:Occur.MUST_NOT*/publicvoidtestBooleanQuery() throwsException{ Queryquery1=NumericRangeQuery.newLongRange("id", 1L, 3L, true, true); Queryquery2=NumericRangeQuery.newLongRange("id", 2L, 4L, true, true); // 创建布尔查询的对象BooleanQueryquery=newBooleanQuery(); // 组合其它查询query.add(query1, BooleanClause.Occur.MUST_NOT); query.add(query2, BooleanClause.Occur.SHOULD); search(query); }
4.4 修改索引
步骤:
//1 创建文档存储目录//2 创建索引写入器配置对象//3 创建索引写入器//4 创建文档数据//5 修改//6 提交//7 关闭
/* 测试:修改索引* 注意:* A:Lucene修改功能底层会先删除,再把新的文档添加。* B:修改功能会根据Term进行匹配,所有匹配到的都会被删除。这样不好* C:因此,一般我们修改时,都会根据一个唯一不重复字段进行匹配修改。例如ID* D:但是词条搜索,要求ID必须是字符串。如果不是,这个方法就不能用。* 如果ID是数值类型,我们不能直接去修改。可以先手动删除deleteDocuments(数值范围查询锁定ID),再添加。*/publicvoidtestUpdate() throwsException{ // 创建目录对象Directorydirectory=FSDirectory.open(newFile("indexDir")); // 创建配置对象IndexWriterConfigconf=newIndexWriterConfig(Version.LATEST, newIKAnalyzer()); // 创建索引写出工具IndexWriterwriter=newIndexWriter(directory, conf); // 创建新的文档数据Documentdoc=newDocument(); doc.add(newStringField("id","1",Store.YES)); doc.add(newTextField("title","谷歌地图之父跳槽facebook ",Store.YES)); /* 修改索引。参数:* 词条:根据这个词条匹配到的所有文档都会被修改* 文档信息:要修改的新的文档数据*/writer.updateDocument(newTerm("id","1"), doc); // 提交writer.commit(); // 关闭writer.close(); }
4.5 删除索引
步骤:
//1 创建文档对象目录//2 创建索引写入器配置对象//3 创建索引写入器//4 删除//5 提交//6 关闭
/** 演示:删除索引* 注意:* 一般,为了进行精确删除,我们会根据唯一字段来删除。比如ID* 如果是用Term删除,要求ID也必须是字符串类型!*/publicvoidtestDelete() throwsException { // 创建目录对象Directorydirectory=FSDirectory.open(newFile("indexDir")); // 创建配置对象IndexWriterConfigconf=newIndexWriterConfig(Version.LATEST, newIKAnalyzer()); // 创建索引写出工具IndexWriterwriter=newIndexWriter(directory, conf); // 根据词条进行删除// writer.deleteDocuments(new Term("id", "1"));// 根据query对象删除,如果ID是数值类型,那么我们可以用数值范围查询锁定一个具体的ID// Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);// writer.deleteDocuments(query);// 删除所有writer.deleteAll(); // 提交writer.commit(); // 关闭writer.close(); }
5 Lucene的高级使用
5.1 高亮显示
原理:
1)给所有关键字加上一个HTML标签
2)给这个特殊的标签设置CSS样式
实现步骤:
//1 创建目录 对象//2 创建索引读取工具//3 创建索引搜索工具//4 创建查询解析器//5 创建查询对象//6 创建格式化器//7 创建查询分数工具//8 准备高亮工具//9 搜索//10 获取结果//11 用高亮工具处理普通的查询结果
// 高亮显示publicvoidtestHighlighter() throwsException { // 目录对象Directorydirectory=FSDirectory.open(newFile("indexDir")); // 创建读取工具IndexReaderreader=DirectoryReader.open(directory); // 创建搜索工具IndexSearchersearcher=newIndexSearcher(reader); QueryParserparser=newQueryParser("title", newIKAnalyzer()); Queryquery=parser.parse("谷歌地图"); // 格式化器Formatterformatter=newSimpleHTMLFormatter("<em>", "</em>"); QueryScorerscorer=newQueryScorer(query); // 准备高亮工具Highlighterhighlighter=newHighlighter(formatter, scorer); // 搜索TopDocstopDocs=searcher.search(query, 10); System.out.println("本次搜索共"+topDocs.totalHits+"条数据"); ScoreDoc[] scoreDocs=topDocs.scoreDocs; for (ScoreDocscoreDoc : scoreDocs) { // 获取文档编号intdocID=scoreDoc.doc; Documentdoc=reader.document(docID); System.out.println("id: "+doc.get("id")); Stringtitle=doc.get("title"); // 用高亮工具处理普通的查询结果,参数:分词器,要高亮的字段的名称,高亮字段的原始值StringhTitle=highlighter.getBestFragment(newIKAnalyzer(), "title", title); System.out.println("title: "+hTitle); // 获取文档的得分System.out.println("得分:"+scoreDoc.score); } }
5.2 排序
// 排序publicvoidtestSortQuery() throwsException { // 目录对象Directorydirectory=FSDirectory.open(newFile("indexDir")); // 创建读取工具IndexReaderreader=DirectoryReader.open(directory); // 创建搜索工具IndexSearchersearcher=newIndexSearcher(reader); QueryParserparser=newQueryParser("title", newIKAnalyzer()); Queryquery=parser.parse("谷歌地图"); // 创建排序对象,需要排序字段SortField,参数:字段的名称、字段的类型、是否反转如果是false,升序。true降序Sortsort=newSort(newSortField("id", SortField.Type.LONG, true)); // 搜索TopDocstopDocs=searcher.search(query, 10,sort); System.out.println("本次搜索共"+topDocs.totalHits+"条数据"); ScoreDoc[] scoreDocs=topDocs.scoreDocs; for (ScoreDocscoreDoc : scoreDocs) { // 获取文档编号intdocID=scoreDoc.doc; Documentdoc=reader.document(docID); System.out.println("id: "+doc.get("id")); System.out.println("title: "+doc.get("title")); } }
5.3 分页
// 分页publicvoidtestPageQuery() throwsException { // 实际上Lucene本身不支持分页。因此我们需要自己进行逻辑分页。我们要准备分页参数:intpageSize=2;// 每页条数intpageNum=3;// 当前页码intstart= (pageNum-1) *pageSize;// 当前页的起始条数intend=start+pageSize;// 当前页的结束条数(不能包含)// 目录对象Directorydirectory=FSDirectory.open(newFile("indexDir")); // 创建读取工具IndexReaderreader=DirectoryReader.open(directory); // 创建搜索工具IndexSearchersearcher=newIndexSearcher(reader); QueryParserparser=newQueryParser("title", newIKAnalyzer()); Queryquery=parser.parse("谷歌地图"); // 创建排序对象,需要排序字段SortField,参数:字段的名称、字段的类型、是否反转如果是false,升序。true降序Sortsort=newSort(newSortField("id", Type.LONG, false)); // 搜索数据,查询0~end条TopDocstopDocs=searcher.search(query, end,sort); System.out.println("本次搜索共"+topDocs.totalHits+"条数据"); ScoreDoc[] scoreDocs=topDocs.scoreDocs; for (inti=start; i<end; i++) { ScoreDocscoreDoc=scoreDocs[i]; // 获取文档编号intdocID=scoreDoc.doc; Documentdoc=reader.document(docID); System.out.println("id: "+doc.get("id")); System.out.println("title: "+doc.get("title")); } }
6.4 得分算法
l Lucene会对搜索结果打分,用来表示文档数据与词条关联性的强弱,得分越高,表示查询的匹配度就越高,排名就越靠前!其算法公式是:
5.5、总结
- Lucene是全文索引引擎的祖先,后面的Solr、Elasticsearch都是基于Lucene的(后面会有一篇讲Elasticsearch的,敬请期待~)
- Lucene中存的就是一系列的二进制压缩文件和一些控制文件,这些内容统称为索引库,索引库又分了两个部分:
- 原始记录
- 词汇表
- 了解索引库的优化方式:1、合并文件 2、设置内存索引库
- Lucene的分词器有非常多种,选择自己适合的一种进行分词
- 查询出来的结果可对其设置高亮、摘要、排序
这篇这是Lucene的冰山一角,一般现在用的可能都是Solr、Elasticsearch的了,但想要更加深入了解Lucene可翻阅其他资料哦~
此文章转载自美女博主maojunfang,在下稍作补充,如有侵权请联系微信:weiai923713392