Lucene就是这么容易
公众号阅读
https://mp.weixin.qq.com/s/M3...
Lucene
[TOC]
什么是Lucene ???
The Apache LuceneTM project develops open-source search software, including:
Lucene Core, our flagship sub-project, provides Java-based indexing and search technology, as well as spellchecking, hit highlighting and advanced analysis/tokenization capabilities.
lucene官网(http://lucene.apache.org/)
Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。Lucene是一套用于全文检索和搜寻的开源程式库,由Apache软件基金会支持和提供。Lucene提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。在Java开发环境里Lucene是一个成熟的免费开源工具。就其本身而言,Lucene是当前以及最近几年最受欢迎的免费Java信息检索程序库。人们经常提到信息检索程序库,虽然与搜索引擎有关,但不应该将信息检索程序库与搜索引擎相混淆。
为什么使用Lucene
现在Lucene在互联网行业的用的非常广泛,尤其是大数据时代的今天,那么根据自己的理解给大家简单的介绍一下为什么要学习Lucene。
传统的sql查询方式,数据量过多时,数据库的压力就会变得很大,查询速度会变得非常慢。我们需要使用更好的解决方案来分担数据库的压力。为了解决数据库压力和速度的问题,我们的数据库就变成了索引库,我们使用Lucene的API的来操作服务器上的索引库。这样完全和数据库进行了隔离。
一、快速入门
现在我们已经了解了Lucene。
- Lucene是一套用于全文检索和搜寻的开源程序库,由Apache软件基金会支持和提供
- Lucene提供了一个简单却强大的应用程序接口(API),能够做全文索引和搜寻,在Java开发环境里Lucene是一个成熟的免费开放源代码工具
- Lucene并不是现成的搜索引擎产品,但可以用来制作搜索引擎产品
总结:Lucene全文检索就是对文档中全部内容进行分词,然后对所有单词建立倒排索引的过程。
- 目前最新的版本是7.x系列,但是在企业中还是用4.x比较多,所以我们学习4.x的版本。
检索数据需要我们先分词
,存入索引库
。
- 文档Document:数据库中一条具体的记录
- 字段Field:数据库中的每个字段
- 目录对象Directory:物理存储位置
- 写出器的配置对象:需要分词器和lucene的版本
开发需要的jar包
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.7</maven.compiler.source> <maven.compiler.target>1.7</maven.compiler.target> <lunece.version>4.10.2</lunece.version> </properties> <dependencies> <!-- 分词器 --> <dependency> <groupId>com.janeluo</groupId> <artifactId>ikanalyzer</artifactId> <version>2012_u6</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> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> </dependencies>
- 创建文档对象
- 创建存储目录
- 创建分词器
- 创建索引写入器的配置对象
- 创建索引写入器对象
- 将文档交给索引写入器
- 提交
- 关闭
// 创建索引 @Test public void testCreate() throws Exception { // 1 创建文档对象 Document document = new Document(); // 创建并添加字段信息。参数:字段的名称、字段的值、是否存储,这里选Store.YES代表存储到文档列表。Store.NO代表不存储 document.add(new StringField("id", "1", Store.YES)); // 这里我们title字段需要用TextField,即创建索引又会被分词。StringField会创建索引,但是不会被分词 document.add(new TextField("title", "谷歌地图之父跳槽facebook", Store.YES)); // 2 索引目录类,指定索引在硬盘中的位置 Directory directory = FSDirectory.open(new File("E:\\luceneTest")); // 3 创建分词器对象 // Analyzer analyzer = new StandardAnalyzer(); Analyzer analyzer = new IKAnalyzer(); // 4 索引写出工具的配置对象 IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer); // 是否清空索引库;设置打开方式:OpenMode.APPEND // 会在索引库的基础上追加新索引。OpenMode.CREATE会先清空原来数据,再提交新的索引 conf.setOpenMode(OpenMode.CREATE); // 5 创建索引的写出工具类。参数:索引的目录和配置信息 IndexWriter indexWriter = new IndexWriter(directory, conf); // 6 把文档交给IndexWriter indexWriter.addDocument(document); // 7 提交 indexWriter.commit(); // 8 关闭 indexWriter.close(); }
索引查看工具
启动run.bat
*** @Test public void testSearch() throws Exception { // 索引目录对象 Directory directory = FSDirectory.open(new File("E:\\luceneTest")); // 索引读取工具 IndexReader reader = DirectoryReader.open(directory); // 索引搜索工具 IndexSearcher searcher = new IndexSearcher(reader); // 创建查询解析器,两个参数:默认要查询的字段的名称,分词器 QueryParser parser = new QueryParser("title", new IKAnalyzer()); // 创建查询解析器,俩个参数:默认要查询的字段名称,分词器 // MultiFieldQueryParser parser2 = new MultiFieldQueryParser(new // String[] { // "id", "title" }, new IKAnalyzer()); // Query query2 = parser2.parse("1"); // 创建查询对象 Query query = parser.parse("谷歌之父"); // 搜索数据,两个参数:查询条件对象要查询的最大结果条数 // 返回的结果是 按照匹配度排名得分前N名的文档信息(包含查询到的总条数信息、所有符合条件的文档的编号信息)。 TopDocs topDocs = searcher.search(query, 10); // 获取总条数 System.out.println("本次搜索共找到" + topDocs.totalHits + "条数据"); // 获取得分文档对象(ScoreDoc)数组.SocreDoc中包含:文档的编号、文档的得分 ScoreDoc[] scoreDocs = topDocs.scoreDocs; for (ScoreDoc scoreDoc : scoreDocs) { // 取出文档编号 int docID = scoreDoc.doc; // 根据编号去找文档 Document doc = reader.document(docID); System.out.println("id: " + doc.get("id")); System.out.println("title: " + doc.get("title")); // 取出文档得分 System.out.println("得分: " + scoreDoc.score); } }
二、工具类
查询
注:代码中加了必要注释
public void search(Query query) throws Exception { // 索引目录对象 Directory directory = FSDirectory.open(new File("E:\\luceneTest")); // 索引读取工具 IndexReader reader = DirectoryReader.open(directory); // 索引搜索工具 IndexSearcher searcher = new IndexSearcher(reader); // 搜索数据,两个参数:查询条件对象要查询的最大结果条数 // 返回的结果是 按照匹配度排名得分前N名的文档信息(包含查询到的总条数信息、所有符合条件的文档的编号信息)。 TopDocs topDocs = searcher.search(query, 10); // 获取总条数 System.out.println("本次搜索共找到" + topDocs.totalHits + "条数据"); // 获取得分文档对象(ScoreDoc)数组.SocreDoc中包含:文档的编号、文档的得分 ScoreDoc[] scoreDocs = topDocs.scoreDocs; for (ScoreDoc scoreDoc : scoreDocs) { // 取出文档编号 int docID = scoreDoc.doc; // 根据编号去找文档 Document doc = reader.document(docID); System.out.println("id: " + doc.get("id")); System.out.println("title: " + doc.get("title")); // 取出文档得分 System.out.println("得分: " + scoreDoc.score); } }
注:普通词条查询
/* * 测试普通词条查询 注意:Term(词条)是搜索的最小单位,不可再分词。值必须是字符串! */ @Test public void testTermQuery() throws Exception { // 创建词条查询对象 Query query = new TermQuery(new Term("title", "谷歌地图")); search(query); }
注:通配符查询
/* * 测试通配符查询 ? 可以代表任意一个字符 * 可以任意多个任意字符 */ @Test public void testWildCardQuery() throws Exception { // 创建查询对象 Query query = new WildcardQuery(new Term("title", "*歌*")); search(query); }
注:模糊查询;数组范围查询;布尔查询
/* * 测试模糊查询 */ @Test public void testFuzzyQuery() throws Exception { // 创建模糊查询对象:允许用户输错。但是要求错误的最大编辑距离不能超过2 // 编辑距离:一个单词到另一个单词最少要修改的次数 facebool --> facebook 需要编辑1次,编辑距离就是1 // Query query = new FuzzyQuery(new Term("title","fscevool")); // 可以手动指定编辑距离,但是参数必须在0~2之间 Query query = new FuzzyQuery(new Term("title", "facevool"), 2); search(query); } /*************************************************************** * 测试:数值范围查询 注意:数值范围查询,可以用来对非String类型的ID进行精确的查找 */ @Test public void testNumericRangeQuery() throws Exception { // 数值范围查询对象,参数:字段名称,最小值、最大值、是否包含最小值、是否包含最大值 Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true); search(query); } /***************************************************************** * 布尔查询: 布尔查询本身没有查询条件,可以把其它查询通过逻辑运算进行组合! 交集:Occur.MUST + Occur.MUST * 并集:Occur.SHOULD + Occur.SHOULD 非:Occur.MUST_NOT */ // @Test // public void testBooleanQuery() throws Exception { // // Query query1 = NumericRangeQuery.newLongRange("id", 1L, 3L, true, true); // Query query2 = NumericRangeQuery.newLongRange("id", 2L, 4L, true, true); // // 创建布尔查询的对象 // BooleanQuery query = new BooleanQuery(); // // 组合其它查询 // query.add(query1, BooleanClause.Occur.MUST_NOT); // query.add(query2, BooleanClause.Occur.SHOULD); // // search(query); // }
修改-删除
注:修改和删除操作
/* * 测试:修改索引 注意: A:Lucene修改功能底层会先删除,再把新的文档添加。 * B:修改功能会根据Term进行匹配,所有匹配到的都会被删除。这样不好 C:因此,一般我们修改时,都会根据一个唯一不重复字段进行匹配修改。例如ID * D:但是词条搜索,要求ID必须是字符串。如果不是,这个方法就不能用。 * 如果ID是数值类型,我们不能直接去修改。可以先手动删除deleteDocuments(数值范围查询锁定ID),再添加。 */ @Test public void testUpdate() throws Exception { // 创建目录对象 Directory directory = FSDirectory.open(new File("E:\\luceneTest")); // 创建配置对象 IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer()); // 创建索引写出工具 IndexWriter writer = new IndexWriter(directory, conf); // 创建新的文档数据 Document doc = new Document(); doc.add(new StringField("id", "1", Store.YES)); doc.add(new TextField("title", "谷歌地图之父跳槽facebook ", Store.YES)); /* * 修改索引。参数: 词条:根据这个词条匹配到的所有文档都会被修改 文档信息:要修改的新的文档数据 */ writer.updateDocument(new Term("id", "1"), doc); // 提交 writer.commit(); // 关闭 writer.close(); } /* * 演示:删除索引 注意: 一般,为了进行精确删除,我们会根据唯一字段来删除。比如ID 如果是用Term删除,要求ID也必须是字符串类型! */ @Test public void testDelete() throws Exception { // 创建目录对象 Directory directory = FSDirectory.open(new File("E:\\luceneTest")); // 创建配置对象 IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer()); // 创建索引写出工具 IndexWriter writer = new IndexWriter(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(); }
高亮显示
- 创建目录 对象
- 创建索引读取工具
- 创建索引搜索工具
- 创建查询解析器
- 创建查询对象
- 创建格式化器
- 创建查询分数工具
- 准备高亮工具
- 搜索
- 获取结果
- 用高亮工具处理普通的查询结果
- 关键字增加css样式
// 高亮显示 @Test public void testHighlighter() throws Exception { // 目录对象 Directory directory = FSDirectory.open(new File("indexDir")); // 创建读取工具 IndexReader reader = DirectoryReader.open(directory); // 创建搜索工具 IndexSearcher searcher = new IndexSearcher(reader); QueryParser parser = new QueryParser("title", new IKAnalyzer()); Query query = parser.parse("谷歌地图"); // 格式化器 Formatter formatter = new SimpleHTMLFormatter("<em>", "</em>"); QueryScorer scorer = new QueryScorer(query); // 准备高亮工具 Highlighter highlighter = new Highlighter(formatter, scorer); // 搜索 TopDocs topDocs = searcher.search(query, 10); System.out.println("本次搜索共" + topDocs.totalHits + "条数据"); ScoreDoc[] scoreDocs = topDocs.scoreDocs; for (ScoreDoc scoreDoc : scoreDocs) { // 获取文档编号 int docID = scoreDoc.doc; Document doc = reader.document(docID); System.out.println("id: " + doc.get("id")); String title = doc.get("title"); // 用高亮工具处理普通的查询结果,参数:分词器,要高亮的字段的名称,高亮字段的原始值 String hTitle = highlighter.getBestFragment(new IKAnalyzer(), "title", title); System.out.println("title: " + hTitle); // 获取文档的得分 System.out.println("得分:" + scoreDoc.score); } }
排序
// 排序 @Test public void testSortQuery() throws Exception { // 目录对象 Directory directory = FSDirectory.open(new File("indexDir")); // 创建读取工具 IndexReader reader = DirectoryReader.open(directory); // 创建搜索工具 IndexSearcher searcher = new IndexSearcher(reader); QueryParser parser = new QueryParser("title", new IKAnalyzer()); Query query = parser.parse("谷歌地图"); // 创建排序对象,需要排序字段SortField,参数:字段的名称、字段的类型、是否反转如果是false,升序。true降序 Sort sort = new Sort(new SortField("id", SortField.Type.LONG, true)); // 搜索 TopDocs topDocs = searcher.search(query, 10,sort); System.out.println("本次搜索共" + topDocs.totalHits + "条数据"); ScoreDoc[] scoreDocs = topDocs.scoreDocs; for (ScoreDoc scoreDoc : scoreDocs) { // 获取文档编号 int docID = scoreDoc.doc; Document doc = reader.document(docID); System.out.println("id: " + doc.get("id")); System.out.println("title: " + doc.get("title")); } }
分页
// 分页 @Test public void testPageQuery() throws Exception { // 实际上Lucene本身不支持分页。因此我们需要自己进行逻辑分页。我们要准备分页参数: int pageSize = 2;// 每页条数 int pageNum = 3;// 当前页码 int start = (pageNum - 1) * pageSize;// 当前页的起始条数 int end = start + pageSize;// 当前页的结束条数(不能包含) // 目录对象 Directory directory = FSDirectory.open(new File("indexDir")); // 创建读取工具 IndexReader reader = DirectoryReader.open(directory); // 创建搜索工具 IndexSearcher searcher = new IndexSearcher(reader); QueryParser parser = new QueryParser("title", new IKAnalyzer()); Query query = parser.parse("谷歌地图"); // 创建排序对象,需要排序字段SortField,参数:字段的名称、字段的类型、是否反转如果是false,升序。true降序 Sort sort = new Sort(new SortField("id", Type.LONG, false)); // 搜索数据,查询0~end条 TopDocs topDocs = searcher.search(query, end,sort); System.out.println("本次搜索共" + topDocs.totalHits + "条数据"); ScoreDoc[] scoreDocs = topDocs.scoreDocs; for (int i = start; i < end; i++) { ScoreDoc scoreDoc = scoreDocs[i]; // 获取文档编号 int docID = scoreDoc.doc; Document doc = reader.document(docID); System.out.println("id: " + doc.get("id")); System.out.println("title: " + doc.get("title")); } }
三、优化
Lucene打分算法
- 当谈论到查询的相关性,很重要的一件事就是对于给定的查询语句,如何计算文档得分。文档得分是一个用来描述查询语句和文档之间匹配程度的变量。如果你希望通过干预Lucene查询来改变查询结果的排序,你就需要对Lucene的得分计算有所理解。
- 当一个文档出现在了搜索结果中,这就意味着该文档与用户给定的查询语句是相匹配的。Lucene会对匹配成功的文档给定一个分数。至少从Lucene这个层面,从打分公式的结果来看,分数值越高,代表文档相关性越高。 自然而然,我们可以得出:两个不同的查询语句对同一个文档的打分将会有所不同,但是比较这两个得分是没有意义的。用户需要记住的是:我们不仅要避免去比较不同查询语句对同一个文档的打分结果,还要避免比较不同查询语句对文档打分结果的最大值。这是因为文档的得分是多个因素共同影响的结果,不仅有权重(boosts)和查询语句的结构起作用,还有匹配关键词的个数,关键词所在的域,查询归一化因子中用到的匹配类型……。在极端情况下,只是因为我们用了自定义打分的查询对象或者由于倒排索引中词的动态变化,相似的查询表达式对于同一个文档都会产生截然不同的打分。
计算文档得分,考虑因素如下:
- 文档权重(Document boost):在索引时给某个文档设置的权重值。
- 域权重(Field boost):在查询的时候给某个域设置的权重值。
- 调整因子(Coord):基于文档中包含查询关键词个数计算出来的调整因子。一般而言,如果一个文档中相比其它的文档出现了更多的查询关键词,那么其值越大。
- 逆文档频率(Inerse document frequency):基于Term的一个因子,存在的意义是告诉打分公式一个词的稀有程度。其值越低,词越稀有(这里的值是指单纯的频率,即多少个文档中出现了该词;而非指Lucene中idf的计算公式)。打分公式利用这个因子提升包含稀有词文档的权重。
- 长度归一化(Length norm):基于域的一个归一化因子。其值由给定域中Term的个数决定(在索引文档的时候已经计算出来了,并且存储到了索引中)。域越的文本越长,因子的权重越低。这表明Lucene打分公式偏向于域包含Term少的文档。
- 词频(Term frequency):基于Term的一个因子。用来描述给定Term在一个文档中出现的次数,词频越大,文档的得分越大。
- 查询归一化因子(Query norm):基于查询语句的归一化因子。其值为查询语句中每一个查询词权重的平方和。查询归一化因子使得比较不同查询语句的得分变得可行,当然比较不同查询语句得分并不总是那么易于实现和可行的。
Lucene打分公式
Lucene概念上的打分公式是这样的:(TF/IDF公式的概念版)
上面的公式展示了布尔信息检索模型和向量空间信息检索模型的组合。我们暂时不去讨论它,直接见识下Lucene实际应用的打分公式:
可以看到,文档的分数实际上是由查询语句q和文档d作为变量的一个函数值。打分公式中有两部分不直接依赖于查询词,它们是coord和queryNorm。公式的值是这样计算的,coord和queryNorm两大部分直接乘以查询语句中每个查询词计算值的总和。另一方面,这个总和也是由每个查询词的词频(tf),逆文档频率(idf),查询词的权重,还有norm,也就是前面说的length norm相乘而得的结果。听上去有些复杂吧?不用担心,这些东西不需要全部记住。你只需要知道在进行文档打分的时候,哪些因素是起决定作用的就可以了。基本上,从前面的公式中可以提炼出以下的几个规则:
- 匹配到的关键词越稀有,文档的得分就越高。
- 文档的域越小(包含比较少的Term),文档的得分就越高。
- 设置的权重(索引和搜索时设置的都可以)越大,文档得分越高。
正如我们所看到的那样,Lucene会给具有这些特征的文档打最高分:文档内容能够匹配到较多的稀有的搜索关键词,文档的域包含较少的Term,并且域中的Term多是稀有的。简而言之
停用词和扩展词加载
将IKAnalyzer.cfg.xml和stopword.dic和xxx.dic文件复制到MyEclipse的src目录下,再进行配置
IK Analyzer默认的停用词词典为IKAnalyzer2012_u6/stopword.dic,这个停用词词典并不完整,只有30多个英文停用词。可以扩展停用词字典,新增ext_stopword.dic,文件和IKAnalyzer.cfg.xml在同一目录,编辑IKAnalyzer.cfg.xml把新增的停用词字典写入配置文件,多个停用词字典用逗号隔开,如下所示。
<entry key="ext_stopwords">stopword.dic;ext_stopword.dic</entry>
接下来就可以构建自己的搜索引擎了。
上面展示了lucene一些基本操作,更详细的的工具类可以访问https://github.com/wangshiyu777/usefulDemo,分享了详细Demo。