阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

Lucene API使用方法与和性能优化

128次阅读
没有评论

共计 20236 个字符,预计需要花费 51 分钟才能阅读完成。

1 lucene 简介
 1.1 什么是 lucene
 Lucene 是一个全文搜索框架,而不是应用产品。因此它并不像百度或者 google Desktop 那么拿来就能用,它只是提供了一种工具让你能实现这些产品。
 
1.2 lucene 能做什么
 要回答这个问题,先要了解 lucene 的本质。实际上 lucene 的功能很单一,说到底,就是你给它若干个字符串,然后它为你提供一个全文搜索服务,告诉你你要搜索的关键词出现在哪里。知道了这个本质,你就可以发挥想象做任何符合这个条件的事情了。你可以把站内新闻都索引了,做个资料库;你可以把一个数据库表的若干个字段索引起来,那就不用再担心因为“%like%”而锁表了;你也可以写个自己的搜索引擎……
 
1.3 你该不该选择 lucene
 下面给出一些测试数据,如果你觉得可以接受,那么可以选择。
 测试一:250 万记录,300M 左右文本,生成索引 380M 左右,800 线程下平均处理时间 300ms。
 测试二:37000 记录,索引数据库中的两个 varchar 字段,索引文件 2.6M,800 线程下平均处理时间 1.5ms。

基于 Lucene 多索引进行索引和搜索 http://www.linuxidc.com/Linux/2012-05/59757.htm

Lucene 实战(第 2 版) 中文版 配套源代码 http://www.linuxidc.com/Linux/2013-10/91055.htm

Lucene 实战(第 2 版) PDF 高清中文版 http://www.linuxidc.com/Linux/2013-10/91052.htm

使用 Lucene-Spatial 实现集成地理位置的全文检索 http://www.linuxidc.com/Linux/2012-02/53117.htm

Lucene + Hadoop 分布式搜索运行框架 Nut 1.0a9 http://www.linuxidc.com/Linux/2012-02/53113.htm

Lucene + Hadoop 分布式搜索运行框架 Nut 1.0a8 http://www.linuxidc.com/Linux/2012-02/53111.htm

Lucene + Hadoop 分布式搜索运行框架 Nut 1.0a7 http://www.linuxidc.com/Linux/2012-02/53110.htm

Project 2-1: 配置 Lucene, 建立 WEB 查询系统[Ubuntu 10.10] http://www.linuxidc.com/Linux/2010-11/30103.htm
 
2 lucene 的工作方式
 lucene 提供的服务实际包含两部分:一入一出。所谓入是写入,即将你提供的源(本质是字符串)写入索引或者将其从索引中删除;所谓出是读出,即向用户提供全文搜索服务,让用户可以通过关键词定位源。
 
2.1 写入流程
 源字符串首先经过 analyzer 处理,包括:分词,分成一个个单词;去除 stopword(可选)。
 将源中需要的信息加入 Document 的各个 Field 中,并把需要索引的 Field 索引起来,把需要存储的 Field 存储起来。
 将索引写入存储器,存储器可以是内存或磁盘。
 
2.2 读出流程
 用户提供搜索关键词,经过 analyzer 处理。
 对处理后的关键词搜索索引找出对应的 Document。
 用户根据需要从找到的 Document 中提取需要的 Field。
 
3 一些需要知道的概念
 lucene 用到一些概念,了解它们的含义,有利于下面的讲解。
 
3.1 analyzer
 Analyzer 是分析器,它的作用是把一个字符串按某种规则划分成一个个词语,并去除其中的无效词语,这里说的无效词语是指英文中的“of”、“the”,中文中的“的”、“地”等词语,这些词语在文章中大量出现,但是本身不包含什么关键信息,去掉有利于缩小索引文件、提高效率、提高命中率。
 分词的规则千变万化,但目的只有一个:按语义划分。这点在英文中比较容易实现,因为英文本身就是以单词为单位的,已经用空格分开;而中文则必须以某种方法将连成一片的句子划分成一个个词语。具体划分方法下面再详细介绍,这里只需了解分析器的概念即可。
 
3.2 document
 用户提供的源是一条条记录,它们可以是文本文件、字符串或者数据库表的一条记录等等。一条记录经过索引之后,就是以一个 Document 的形式存储在索引文件中的。用户进行搜索,也是以 Document 列表的形式返回。
 
3.3 field
 一个 Document 可以包含多个信息域,例如一篇文章可以包含“标题”、“正文”、“最后修改时间”等信息域,这些信息域就是通过 Field 在 Document 中存储的。
 Field 有两个属性可选:存储和索引。通过存储属性你可以控制是否对这个 Field 进行存储;通过索引属性你可以控制是否对该 Field 进行索引。这看起来似乎有些废话,事实上对这两个属性的正确组合很重要,下面举例说明:
 还是以刚才的文章为例子,我们需要对标题和正文进行全文搜索,所以我们要把索引属性设置为真,同时我们希望能直接从搜索结果中提取文章标题,所以我们把标题域的存储属性设置为真,但是由于正文域太大了,我们为了缩小索引文件大小,将正文域的存储属性设置为假,当需要时再直接读取文件;我们只是希望能从搜索解果中提取最后修改时间,不需要对它进行搜索,所以我们把最后修改时间域的存储属性设置为真,索引属性设置为假。上面的三个域涵盖了两个属性的三种组合,还有一种全为假的没有用到,事实上 Field 不允许你那么设置,因为既不存储又不索引的域是没有意义的。
 
3.4 term
 term 是搜索的最小单位,它表示文档的一个词语,term 由两部分组成:它表示的词语和这个词语所出现的 field。
 
3.5 tocken
 tocken 是 term 的一次出现,它包含 trem 文本和相应的起止偏移,以及一个类型字符串。一句话中可以出现多次相同的词语,它们都用同一个 term 表示,但是用不同的 tocken,每个 tocken 标记该词语出现的地方。
 
3.6 segment
 添加索引时并不是每个 document 都马上添加到同一个索引文件,它们首先被写入到不同的小文件,然后再合并成一个大索引文件,这里每个小文件都是一个 segment。
 
4 lucene 的结构
 lucene 包括 core 和 sandbox 两部分,其中 core 是 lucene 稳定的核心部分,sandbox 包含了一些附加功能,例如 highlighter、各种分析器。
 Lucene core 有七个包:analysis,document,index,queryParser,search,store,util。
 4.1 analysis
 Analysis 包含一些内建的分析器,例如按空白字符分词的 WhitespaceAnalyzer,添加了 stopwrod 过滤的 StopAnalyzer,最常用的 StandardAnalyzer。
 4.2 document
 Document 包含文档的数据结构,例如 Document 类定义了存储文档的数据结构,Field 类定义了 Document 的一个域。
 4.3 index
 Index 包含了索引的读写类,例如对索引文件的 segment 进行写、合并、优化的 IndexWriter 类和对索引进行读取和删除操作的 IndexReader 类,这里要注意的是不要被 IndexReader 这个名字误导,以为它是索引文件的读取类,实际上删除索引也是由它完成,IndexWriter 只关心如何将索引写入一个个 segment,并将它们合并优化;IndexReader 则关注索引文件中各个文档的组织形式。
 4.4 queryParser
 QueryParser 包含了解析查询语句的类,lucene 的查询语句和 sql 语句有点类似,有各种保留字,按照一定的语法可以组成各种查询。Lucene 有很多种 Query 类,它们都继承自 Query,执行各种特殊的查询,QueryParser 的作用就是解析查询语句,按顺序调用各种 Query 类查找出结果。
 4.5 search
 Search 包含了从索引中搜索结果的各种类,例如刚才说的各种 Query 类,包括 TermQuery、BooleanQuery 等就在这个包里。
 4.6 store
 Store 包含了索引的存储类,例如 Directory 定义了索引文件的存储结构,FSDirectory 为存储在文件中的索引,RAMDirectory 为存储在内存中的索引,MmapDirectory 为使用内存映射的索引。
 4.7 util
 Util 包含一些公共工具类,例如时间和字符串之间的转换工具。
 5 如何建索引
 5.1 最简单的能完成索引的代码片断

IndexWriter writer = new IndexWriter(“/data/index/”, new StandardAnalyzer(), true);
 Document doc = new Document();
 doc.add(new Field(“title”,”lucene introduction”, Field.Store.YES, Field.Index.TOKENIZED));
 doc.add(new Field(“content”,”lucene works well”, Field.Store.YES, Field.Index.TOKENIZED));
 writer.addDocument(doc);
 writer.optimize();
 writer.close();
 
下面我们分析一下这段代码。
 首先我们创建了一个 writer,并指定存放索引的目录为“/data/index”,使用的分析器为 StandardAnalyzer,第三个参数说明如果已经有索引文件在索引目录下,我们将覆盖它们。
 然后我们新建一个 document。
 我们向 document 添加一个 field,名字是“title”,内容是“lucene introduction”,对它进行存储并索引。
 再添加一个名字是“content”的 field,内容是“lucene works well”,也是存储并索引。
 然后我们将这个文档添加到索引中,如果有多个文档,可以重复上面的操作,创建 document 并添加。
 添加完所有 document,我们对索引进行优化,优化主要是将多个 segment 合并到一个,有利于提高索引速度。
 随后将 writer 关闭,这点很重要。
 
对,创建索引就这么简单!
 当然你可能修改上面的代码获得更具个性化的服务。
 
5.2 将索引直接写在内存
 你需要首先创建一个 RAMDirectory,并将其传给 writer,代码如下:
 
Directory dir = new RAMDirectory();
 IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(), true);
 Document doc = new Document();
 doc.add(new Field(“title”,”lucene introduction”, Field.Store.YES, Field.Index.TOKENIZED));
 doc.add(new Field(“content”,”lucene works well”, Field.Store.YES, Field.Index.TOKENIZED));
 writer.addDocument(doc);
 writer.optimize();
 writer.close();
 
5.3 索引文本文件
 如果你想把纯文本文件索引起来,而不想自己将它们读入字符串创建 field,你可以���下面的代码创建 field:
 
Field field = new Field(“content”, new FileReader(file));
 
这里的 file 就是该文本文件。该构造函数实际上是读去文件内容,并对其进行索引,但不存储。

更多详情见请继续阅读下一页的精彩内容:http://www.linuxidc.com/Linux/2014-06/102857p2.htm

6 如何维护索引
 索引的维护操作都是由 IndexReader 类提供。
 
6.1 如何删除索引
 lucene 提供了两种从索引中删除 document 的方法,一种是
 
void deleteDocument(int docNum)
 
这种方法是根据 document 在索引中的编号来删除,每个 document 加进索引后都会有个唯一编号,所以根据编号删除是一种精确删除,但是这个编号是索引的内部结构,一般我们不会知道某个文件的编号到底是几,所以用处不大。另一种是
 
void deleteDocuments(Term term)
 
这种方法实际上是首先根据参数 term 执行一个搜索操作,然后把搜索到的结果批量删除了。我们可以通过这个方法提供一个严格的查询条件,达到删除指定 document 的目的。
 下面给出一个例子:
 
Directory dir = FSDirectory.getDirectory(PATH, false);
 IndexReader reader = IndexReader.open(dir);
 Term term = new Term(field, key);
 reader.deleteDocuments(term);
 reader.close();
 
6.2 如何更新索引
 lucene 并没有提供专门的索引更新方法,我们需要先将相应的 document 删除,然后再将新的 document 加入索引。例如:
 
Directory dir = FSDirectory.getDirectory(PATH, false);
 IndexReader reader = IndexReader.open(dir);
 Term term = new Term(“title”,“lucene introduction”);
 reader.deleteDocuments(term);
 reader.close();
 
IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(), true);
 Document doc = new Document();
 doc.add(new Field(“title”,”lucene introduction”, Field.Store.YES, Field.Index.TOKENIZED));
 doc.add(new Field(“content”,”lucene is funny”, Field.Store.YES, Field.Index.TOKENIZED));
 writer.addDocument(doc);
 writer.optimize();
 writer.close();
 7 如何搜索
 lucene 的搜索相当强大,它提供了很多辅助查询类,每个类都继承自 Query 类,各自完成一种特殊的查询,你可以像搭积木一样将它们任意组合使用,完成一些复杂操作;另外 lucene 还提供了 Sort 类对结果进行排序,提供了 Filter 类对查询条件进行限制。你或许会不自觉地拿它跟 SQL 语句进行比较:“lucene 能执行 and、or、order by、where、like‘%xx%’操作吗?”回答是:“当然没问题!”
 
7.1 各种各样的 Query
 下面我们看看 lucene 到底允许我们进行哪些查询操作:
 
7.1.1 TermQuery
 首先介绍最基本的查询,如果你想执行一个这样的查询:“在 content 域中包含‘lucene’的 document”,那么你可以用 TermQuery:
 
Term t = new Term(“content”,”lucene”;
 Query query = new TermQuery(t);
 
7.1.2 BooleanQuery
 如果你想这么查询:“在 content 域中包含 java 或 perl 的 document”,那么你可以建立两个 TermQuery 并把它们用 BooleanQuery 连接起来:
 
TermQuery termQuery1 = new TermQuery(new Term(“content”,”java”);
 TermQuery termQuery 2 = new TermQuery(new Term(“content”,”perl”);
 BooleanQuery booleanQuery = new BooleanQuery();
 booleanQuery.add(termQuery 1, BooleanClause.Occur.SHOULD);
 booleanQuery.add(termQuery 2, BooleanClause.Occur.SHOULD);
 
7.1.3 WildcardQuery
 如果你想对某单词进行通配符查询,你可以用 WildcardQuery,通配符包括’?’匹配一个任意字符和’*’匹配零个或多个任意字符,例如你搜索’use*’,你可能找到’useful’或者’useless’:
 
Query query = new WildcardQuery(new Term(“content”,”use*”);
 
7.1.4 PhraseQuery
 你可能对中日关系比较感兴趣,想查找‘中’和‘日’挨得比较近(5 个字的距离内)的文章,超过这个距离的不予考虑,你可以:
 
PhraseQuery query = new PhraseQuery();
 query.setSlop(5);
 query.add(new Term(“content”,“中”));
 query.add(new Term(“content”,“日”));
 
那么它可能搜到“中日合作……”、“中方和日方……”,但是搜不到“中国某高层领导说日本欠扁”。
 
7.1.5 PrefixQuery
 如果你想搜以‘中’开头的词语,你可以用 PrefixQuery:
 
PrefixQuery query = new PrefixQuery(new Term(“content”,”中”);
 
7.1.6 FuzzyQuery
 FuzzyQuery 用来搜索相似的 term,使用 Levenshtein 算法。假设你想搜索跟‘wuzza’相似的词语,你可以:
 
Query query = new FuzzyQuery(new Term(“content”,”wuzza”);
 
你可能得到‘fuzzy’和‘wuzzy’。
 
7.1.7 RangeQuery
 另一个常用的 Query 是 RangeQuery,你也许想搜索时间域从 20060101 到 20060130 之间的 document,你可以用 RangeQuery:
 
RangeQuery query = new RangeQuery(new Term(“time”,“20060101”), new Term(“time”,“20060130”), true);
 
最后的 true 表示用闭合区间。
 
7.2 QueryParser
 看了这么多 Query,你可能会问:“不会让我自己组合各种 Query 吧,太麻烦了!”当然不会,lucene 提供了一种类似于 SQL 语句的查询语句,我们姑且叫它 lucene 语句,通过它,你可以把各种查询一句话搞定,lucene 会自动把它们查分成小块交给相应 Query 执行。下面我们对应每种 Query 演示一下:
 TermQuery 可以用“field:key”方式,例如“content:lucene”。
 BooleanQuery 中‘与’用‘+’,‘或’用‘’,例如“content:java contenterl”。
 WildcardQuery 仍然用‘?’和‘*’,例如“content:use*”。
 PhraseQuery 用‘~’,例如“content:”中日”~5”。
 PrefixQuery 用‘*’,例如“中 *”。
 FuzzyQuery 用‘~’,例如“content: wuzza ~”。
 RangeQuery 用‘[]’或‘{}’,前者表示闭区间,后者表示开区间,例如“time:[20060101 TO 20060130]”,注意 TO 区分大小写。
 你可以任意组合 query string,完成复杂操作,例如“标题或正文包括 lucene,并且时间在 20060101 到 20060130 之间的文章”可以表示为:“+ (title:lucene content:lucene) +time:[20060101 TO 20060130]”。代码如下:
 
Directory dir = FSDirectory.getDirectory(PATH, false);
 IndexSearcher is = new IndexSearcher(dir);
 QueryParser parser = new QueryParser(“content”, new StandardAnalyzer());
 Query query = parser.parse(“+(title:lucene content:lucene) +time:[20060101 TO 20060130]“;
 Hits hits = is.search(query);
 for (int i = 0; i < hits.length(); i++)
 {
 Document doc = hits.doc(i);
 System.out.println(doc.get(“title”);
 }
 is.close();
 
首先我们创建一个在指定文件目录上的 IndexSearcher。
 然后创建一个使用 StandardAnalyzer 作为分析器的 QueryParser,它默认搜索的域是 content。
 接着我们用 QueryParser 来 parse 查询字串,生成一个 Query。
 然后利用这个 Query 去查找结果,结果以 Hits 的形式返回。
 这个 Hits 对象包含一个列表,我们挨个把它的内容显示出来。
 
7.3 Filter
 filter 的作用就是限制只查询索引的某个子集,它的作用有点像 SQL 语句里的 where,但又有区别,它不是正规查询的一部分,只是对数据源进行预处理,然后交给查询语句。注意它执行的是预处理,而不是对查询结果进行过滤,所以使用 filter 的代价是很大的,它可能会使一次查询耗时提高一百倍。
 最常用的 filter 是 RangeFilter 和 QueryFilter。RangeFilter 是设定只搜索指定范围内的索引;QueryFilter 是在上次查询的结果中搜索。
 Filter 的使用非常简单,你只需创建一个 filter 实例,然后把它传给 searcher。继续上面的例子,查询“时间在 20060101 到 20060130 之间的文章”除了将限制写在 query string 中,你还可以写在 RangeFilter 中:
 
Directory dir = FSDirectory.getDirectory(PATH, false);
 IndexSearcher is = new IndexSearcher(dir);
 QueryParser parser = new QueryParser(“content”, new StandardAnalyzer());
 Query query = parser.parse(“title:lucene content:lucene”;
 RangeFilter filter = new RangeFilter(“time”,”20060101″,”20060230″, true, true);
 Hits hits = is.search(query, filter);
 for (int i i < hits.length(); i++)
 {
 Document doc = hits.doc(i);
 System.out.println(doc.get(“title”);
 }
 is.close();
 
7.4 Sort
 有时你想要一个排好序的结果集,就像 SQL 语句的“order by”,lucene 能做到:通过 Sort。
 Sort sort Sort(“time”); // 相当于 SQL 的“order by time”
 Sort sort = new Sort(“time”, true); // 相当于 SQL 的“order by time desc”
 下面是一个完整的例子:
 
Directory dir = FSDirectory.getDirectory(PATH, false);
 IndexSearcher is = new IndexSearcher(dir);
 QueryParser parser = new QueryParser(“content”, new StandardAnalyzer());
 Query query = parser.parse(“title:lucene content:lucene”;
 RangeFilter filter = new RangeFilter(“time”,”20060101″,”20060230″, true, true);
 Sort sort = new Sort(“time”);
 Hits hits = is.search(query, filter, sort);
 for (int i = 0; i < hits.length(); i++)
 {
 Document doc = hits.doc(i);
 System.out.println(doc.get(“title”);
 }
 is.close();

1 lucene 简介
 1.1 什么是 lucene
 Lucene 是一个全文搜索框架,而不是应用产品。因此它并不像百度或者 google Desktop 那么拿来就能用,它只是提供了一种工具让你能实现这些产品。
 
1.2 lucene 能做什么
 要回答这个问题,先要了解 lucene 的本质。实际上 lucene 的功能很单一,说到底,就是你给它若干个字符串,然后它为你提供一个全文搜索服务,告诉你你要搜索的关键词出现在哪里。知道了这个本质,你就可以发挥想象做任何符合这个条件的事情了。你可以把站内新闻都索引了,做个资料库;你可以把一个数据库表的若干个字段索引起来,那就不用再担心因为“%like%”而锁表了;你也可以写个自己的搜索引擎……
 
1.3 你该不该选择 lucene
 下面给出一些测试数据,如果你觉得可以接受,那么可以选择。
 测试一:250 万记录,300M 左右文本,生成索引 380M 左右,800 线程下平均处理时间 300ms。
 测试二:37000 记录,索引数据库中的两个 varchar 字段,索引文件 2.6M,800 线程下平均处理时间 1.5ms。

基于 Lucene 多索引进行索引和搜索 http://www.linuxidc.com/Linux/2012-05/59757.htm

Lucene 实战(第 2 版) 中文版 配套源代码 http://www.linuxidc.com/Linux/2013-10/91055.htm

Lucene 实战(第 2 版) PDF 高清中文版 http://www.linuxidc.com/Linux/2013-10/91052.htm

使用 Lucene-Spatial 实现集成地理位置的全文检索 http://www.linuxidc.com/Linux/2012-02/53117.htm

Lucene + Hadoop 分布式搜索运行框架 Nut 1.0a9 http://www.linuxidc.com/Linux/2012-02/53113.htm

Lucene + Hadoop 分布式搜索运行框架 Nut 1.0a8 http://www.linuxidc.com/Linux/2012-02/53111.htm

Lucene + Hadoop 分布式搜索运行框架 Nut 1.0a7 http://www.linuxidc.com/Linux/2012-02/53110.htm

Project 2-1: 配置 Lucene, 建立 WEB 查询系统[Ubuntu 10.10] http://www.linuxidc.com/Linux/2010-11/30103.htm
 
2 lucene 的工作方式
 lucene 提供的服务实际包含两部分:一入一出。所谓入是写入,即将你提供的源(本质是字符串)写入索引或者将其从索引中删除;所谓出是读出,即向用户提供全文搜索服务,让用户可以通过关键词定位源。
 
2.1 写入流程
 源字符串首先经过 analyzer 处理,包括:分词,分成一个个单词;去除 stopword(可选)。
 将源中需要的信息加入 Document 的各个 Field 中,并把需要索引的 Field 索引起来,把需要存储的 Field 存储起来。
 将索引写入存储器,存储器可以是内存或磁盘。
 
2.2 读出流程
 用户提供搜索关键词,经过 analyzer 处理。
 对处理后的关键词搜索索引找出对应的 Document。
 用户根据需要从找到的 Document 中提取需要的 Field。
 
3 一些需要知道的概念
 lucene 用到一些概念,了解它们的含义,有利于下面的讲解。
 
3.1 analyzer
 Analyzer 是分析器,它的作用是把一个字符串按某种规则划分成一个个词语,并去除其中的无效词语,这里说的无效词语是指英文中的“of”、“the”,中文中的“的”、“地”等词语,这些词语在文章中大量出现,但是本身不包含什么关键信息,去掉有利于缩小索引文件、提高效率、提高命中率。
 分词的规则千变万化,但目的只有一个:按语义划分。这点在英文中比较容易实现,因为英文本身就是以单词为单位的,已经用空格分开;而中文则必须以某种方法将连成一片的句子划分成一个个词语。具体划分方法下面再详细介绍,这里只需了解分析器的概念即可。
 
3.2 document
 用户提供的源是一条条记录,它们可以是文本文件、字符串或者数据库表的一条记录等等。一条记录经过索引之后,就是以一个 Document 的形式存储在索引文件中的。用户进行搜索,也是以 Document 列表的形式返回。
 
3.3 field
 一个 Document 可以包含多个信息域,例如一篇文章可以包含“标题”、“正文”、“最后修改时间”等信息域,这些信息域就是通过 Field 在 Document 中存储的。
 Field 有两个属性可选:存储和索引。通过存储属性你可以控制是否对这个 Field 进行存储;通过索引属性你可以控制是否对该 Field 进行索引。这看起来似乎有些废话,事实上对这两个属性的正确组合很重要,下面举例说明:
 还是以刚才的文章为例子,我们需要对标题和正文进行全文搜索,所以我们要把索引属性设置为真,同时我们希望能直接从搜索结果中提取文章标题,所以我们把标题域的存储属性设置为真,但是由于正文域太大了,我们为了缩小索引文件大小,将正文域的存储属性设置为假,当需要时再直接读取文件;我们只是希望能从搜索解果中提取最后修改时间,不需要对它进行搜索,所以我们把最后修改时间域的存储属性设置为真,索引属性设置为假。上面的三个域涵盖了两个属性的三种组合,还有一种全为假的没有用到,事实上 Field 不允许你那么设置,因为既不存储又不索引的域是没有意义的。
 
3.4 term
 term 是搜索的最小单位,它表示文档的一个词语,term 由两部分组成:它表示的词语和这个词语所出现的 field。
 
3.5 tocken
 tocken 是 term 的一次出现,它包含 trem 文本和相应的起止偏移,以及一个类型字符串。一句话中可以出现多次相同的词语,它们都用同一个 term 表示,但是用不同的 tocken,每个 tocken 标记该词语出现的地方。
 
3.6 segment
 添加索引时并不是每个 document 都马上添加到同一个索引文件,它们首先被写入到不同的小文件,然后再合并成一个大索引文件,这里每个小文件都是一个 segment。
 
4 lucene 的结构
 lucene 包括 core 和 sandbox 两部分,其中 core 是 lucene 稳定的核心部分,sandbox 包含了一些附加功能,例如 highlighter、各种分析器。
 Lucene core 有七个包:analysis,document,index,queryParser,search,store,util。
 4.1 analysis
 Analysis 包含一些内建的分析器,例如按空白字符分词的 WhitespaceAnalyzer,添加了 stopwrod 过滤的 StopAnalyzer,最常用的 StandardAnalyzer。
 4.2 document
 Document 包含文档的数据结构,例如 Document 类定义了存储文档的数据结构,Field 类定义了 Document 的一个域。
 4.3 index
 Index 包含了索引的读写类,例如对索引文件的 segment 进行写、合并、优化的 IndexWriter 类和对索引进行读取和删除操作的 IndexReader 类,这里要注意的是不要被 IndexReader 这个名字误导,以为它是索引文件的读取类,实际上删除索引也是由它完成,IndexWriter 只关心如何将索引写入一个个 segment,并将它们合并优化;IndexReader 则关注索引文件中各个文档的组织形式。
 4.4 queryParser
 QueryParser 包含了解析查询语句的类,lucene 的查询语句和 sql 语句有点类似,有各种保留字,按照一定的语法可以组成各种查询。Lucene 有很多种 Query 类,它们都继承自 Query,执行各种特殊的查询,QueryParser 的作用就是解析查询语句,按顺序调用各种 Query 类查找出结果。
 4.5 search
 Search 包含了从索引中搜索结果的各种类,例如刚才说的各种 Query 类,包括 TermQuery、BooleanQuery 等就在这个包里。
 4.6 store
 Store 包含了索引的存储类,例如 Directory 定义了索引文件的存储结构,FSDirectory 为存储在文件中的索引,RAMDirectory 为存储在内存中的索引,MmapDirectory 为使用内存映射的索引。
 4.7 util
 Util 包含一些公共工具类,例如时间和字符串之间的转换工具。
 5 如何建索引
 5.1 最简单的能完成索引的代码片断

IndexWriter writer = new IndexWriter(“/data/index/”, new StandardAnalyzer(), true);
 Document doc = new Document();
 doc.add(new Field(“title”,”lucene introduction”, Field.Store.YES, Field.Index.TOKENIZED));
 doc.add(new Field(“content”,”lucene works well”, Field.Store.YES, Field.Index.TOKENIZED));
 writer.addDocument(doc);
 writer.optimize();
 writer.close();
 
下面我们分析一下这段代码。
 首先我们创建了一个 writer,并指定存放索引的目录为“/data/index”,使用的分析器为 StandardAnalyzer,第三个参数说明如果已经有索引文件在索引目录下,我们将覆盖它们。
 然后我们新建一个 document。
 我们向 document 添加一个 field,名字是“title”,内容是“lucene introduction”,对它进行存储并索引。
 再添加一个名字是“content”的 field,内容是“lucene works well”,也是存储并索引。
 然后我们将这个文档添加到索引中,如果有多个文档,可以重复上面的操作,创建 document 并添加。
 添加完所有 document,我们对索引进行优化,优化主要是将多个 segment 合并到一个,有利于提高索引速度。
 随后将 writer 关闭,这点很重要。
 
对,创建索引就这么简单!
 当然你可能修改上面的代码获得更具个性化的服务。
 
5.2 将索引直接写在内存
 你需要首先创建一个 RAMDirectory,并将其传给 writer,代码如下:
 
Directory dir = new RAMDirectory();
 IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(), true);
 Document doc = new Document();
 doc.add(new Field(“title”,”lucene introduction”, Field.Store.YES, Field.Index.TOKENIZED));
 doc.add(new Field(“content”,”lucene works well”, Field.Store.YES, Field.Index.TOKENIZED));
 writer.addDocument(doc);
 writer.optimize();
 writer.close();
 
5.3 索引文本文件
 如果你想把纯文本文件索引起来,而不想自己将它们读入字符串创建 field,你可以���下面的代码创建 field:
 
Field field = new Field(“content”, new FileReader(file));
 
这里的 file 就是该文本文件。该构造函数实际上是读去文件内容,并对其进行索引,但不存储。

更多详情见请继续阅读下一页的精彩内容:http://www.linuxidc.com/Linux/2014-06/102857p2.htm

8 分析器
 在前面的概念介绍中我们已经知道了分析器的作用,就是把句子按照语义切分成一个个词语。英文切分已经有了很成熟的分析器:StandardAnalyzer,很多情况下 StandardAnalyzer 是个不错的选择。甚至你会发现 StandardAnalyzer 也能对中文进行分词。
 但是我们的焦点是中文分词,StandardAnalyzer 能支持中文分词吗?实践证明是可以的,但是效果并不好,搜索“如果”会把“牛奶不如果汁好喝”也搜索出来,而且索引文件很大。那么我们手头上还有什么分析器可以使用呢?core 里面没有,我们可以在 sandbox 里面找到两个:ChineseAnalyzer 和 CJKAnalyzer。但是它们同样都有分词不准的问题。相比之下用 StandardAnalyzer 和 ChineseAnalyzer 建立索引时间差不多,索引文件大小也差不多,CJKAnalyzer 表现会差些,索引文件大且耗时比较长。
 要解决问题,首先分析一下这三个分析器的分词方式。StandardAnalyzer 和 ChineseAnalyzer 都是把句子按单个字切分,也就是说“牛奶不如果汁好喝”会被它们切分成“牛 奶 不 如 果 汁 好 喝”;而 CJKAnalyzer 则会切分成“牛奶 奶不 不如 如果 果汁 汁好好喝”。这也就解释了为什么搜索“果汁”都能匹配这个句子。
 以上分词的缺点至少有两个:匹配不准确和索引文件大。我们的目标是将上面的句子分解成“牛奶 不如 果汁好喝”。这里的关键就是语义识别,我们如何识别“牛奶”是一个词而“奶不”不是词语?我们很自然会想到基于词库的分词法,也就是我们先得到一个词库,里面列举了大部分词语,我们把句子按某种方式切分,当得到的词语与词库中的项匹配时,我们就认为这种切分是正确的。这样切词的过程就转变成匹配的过程,而匹配的方式最简单的有正向最大匹配和逆向最大匹配两种,说白了就是一个从句子开头向后进行匹配,一个从句子末尾向前进行匹配。基于词库的分词词库非常重要,词库的容量直接影响搜索结果,在相同词库的前提下,据说逆向最大匹配优于正向最大匹配。
 当然还有别的分词方法,这本身就是一个学科,我这里也没有深入研究。回到具体应用,我们的目标是能找到成熟的、现成的分词工具,避免重新发明车轮。经过网上搜索,用的比较多的是中科院的 ICTCLAS 和一个不开放源码但是免费的 JE-Analysis。ICTCLAS 有个问题是它是一个动态链接库,java 调用需要本地方法调用,不方便也有安全隐患,而且口碑也确实不大好。JE-Analysis 效果还不错,当然也会有分词不准的地方,相比比较方便放心。= new = 0;
 

9 性能优化
 一直到这里,我们还是在讨论怎么样使 lucene 跑起来,完成指定任务。利用前面说的也确实能完成大部分功能。但是测试表明 lucene 的性能并不是很好,在大数据量大并发的条件下甚至会有半分钟返回的情况。另外大数据量的数据初始化建立索引也是一个十分耗时的过程。那么如何提高 lucene 的性能呢?下面从优化创建索引性能和优化搜索性能两方面介绍。
 

9.1 优化创建索引性能
 这方面的优化途径比较有限,IndexWriter 提供了一些接口可以控制建立索引的操作,另外我们可以先将索引写入 RAMDirectory,再批量写入 FSDirectory,不管怎样,目的都是尽量少的文件 IO,因为创建索引的最大瓶颈在于磁盘 IO。另外选择一个较好的分析器也能提高一些性能。
 
9.1.1 通过设置 IndexWriter 的参数优化索引建立
 setMaxBufferedDocs(int maxBufferedDocs)
 控制写入一个新的 segment 前内存中保存的 document 的数目,设置较大的数目可以加快建索引速度,默认为 10。
 setMaxMergeDocs(int maxMergeDocs)
 控制一个 segment 中可以保存的最大 document 数目,值较小有利于追加索引的速度,默认 Integer.MAX_VALUE,无需修改。
 setMergeFactor(int mergeFactor)
 控制多个 segment 合并的频率,值较大时建立索引速度较快,默认是 10,可以在建立索引时设置为 100。
 
9.1.2 通过 RAMDirectory 缓写提高性能
 我们可以先把索引写入 RAMDirectory,达到一定数量时再批量写进 FSDirectory,减少磁盘 IO 次数。
 
FSDirectory fsDir = FSDirectory.getDirectory(“/data/index”, true);
 RAMDirectory ramDir = new RAMDirectory();
 IndexWriter fsWriter = new IndexWriter(fsDir, new StandardAnalyzer(), true);
 IndexWriter ramWriter = new IndexWriter(ramDir, new StandardAnalyzer(), true);
 while (there are documents to index)
 {
 … create Document …
 ramWriter.addDocument(doc);
 if (condition for flushing memory to disk has been met)
 {
 fsWriter.addIndexes(new Directory[] {ramDir});
 ramWriter.close();
 ramWriter = new IndexWriter(ramDir, new StandardAnalyzer(), true);
 }
 }
 
9.1.3 选择较好的分析器
 这个优化主要是对磁盘空间的优化,可以将索引文件减小将近一半,相同测试数据下由 600M 减少到 380M。但是对时间并没有什么帮助,甚至会需要更长时间,因为较好的分析器需要匹配词库,会消耗更多 cpu,测试数据用 StandardAnalyzer 耗时 133 分钟;用 MMAnalyzer 耗时 150 分钟。
 
9.2 优化搜索性能
 虽然建立索引的操作非常耗时,但是那毕竟只在最初创建时才需要,平时只是少量的维护操作,更何况这些可以放到一个后台进程处理,并不影响用户搜索。我们创建索引的目的就是给用户搜索,所以搜索的性能才是我们最关心的。下面就来探讨一下如何提高搜索性能。
 
9.2.1 将索引放入内存
 这是一个最直观的想法,因为内存比磁盘快很多。Lucene 提供了 RAMDirectory 可以在内存中容纳索引:
 
Directory fsDir = FSDirectory.getDirectory(“/data/index/”, false);
 Directory ramDir = new RAMDirectory(fsDir);
 Searcher searcher = new IndexSearcher(ramDir);
 
但是实践证明 RAMDirectory 和 FSDirectory 速度差不多,当数据量很小时两者都非常快,当数据量较大时(索引文件 400M)RAMDirectory 甚至比 FSDirectory 还要慢一点,这确实让人出乎意料。
 而且 lucene 的搜索非常耗内存,即使将 400M 的索引文件载入内存,在运行一段时间后都会 out of memory,所以个人认为载入内存的作用并不大。
 
9.2.2 优化时间范围限制
 既然载入内存并不能提高效率,一定有其它瓶颈,经过测试发现最大的瓶颈居然是时间范围限制,那么我们可以怎样使时间范围限制的代价最小呢?
 当需要搜索指定时间范围内的结果时,可以:
 1、用 RangeQuery,设置范围,但是 RangeQuery 的实现实际上是将时间范围内的时间点展开,组成一个个 BooleanClause 加入到 BooleanQuery 中查询,因此时间范围不可能设置太大,经测试,范围超过一个月就会抛 BooleanQuery.TooManyClauses,可以通过设置 BooleanQuery.setMaxClauseCount(int maxClauseCount) 扩大,但是扩大也是有限的,并且随着 maxClauseCount 扩大,占用内存也扩大
 2、用 RangeFilter 代替 RangeQuery,经测试速度不会比 RangeQuery 慢,但是仍然有性能瓶颈,查询的 90% 以上时间耗费在 RangeFilter,研究其源码发现 RangeFilter 实际上是首先遍历所有索引,生成一个 BitSet,标记每个 document,在时间范围内的标记为 true,不在的标记为 false,然后将结果传递给 Searcher 查找,这是十分耗时的。
 3、进一步提高性能,这个又有两个思路:
 a、缓存 Filter 结果。既然 RangeFilter 的执行是在搜索之前,那么它的输入都是一定的,就是 IndexReader,而 IndexReader 是由 Directory 决定的,所以可以认为 RangeFilter 的结果是由范围的上下限决定的,也就是由具体的 RangeFilter 对象决定,所以我们只要以 RangeFilter 对象为键,将 filter 结果 BitSet 缓存起来即可。lucene API 已经提供了一个 CachingWrapperFilter 类封装了 Filter 及其结果,所以具体实施起来我们可以 cache CachingWrapperFilter 对象,需要注意的是,不要被 CachingWrapperFilter 的名字及其说明误导,CachingWrapperFilter 看起来是有缓存功能,但的缓存是针对同一个 filter 的,也就是在你用同一个 filter 过滤不同 IndexReader 时,它可以帮你缓存不同 IndexReader 的结果,而我们的需求恰恰相反,我们是用不同 filter 过滤同一个 IndexReader,所以只能把它作为一个封装类。
 b、降低时间精度。研究 Filter 的工作原理可以看出,它每次工作都是遍历整个索引的,所以时间粒度越大,对比越快,搜索时间越短,在不影响功能的情况下,时间精度越低越好,有时甚至牺牲一点精度也值得,当然最好的情况是根本不作时间限制。
 下面针对上面的两个思路演示一下优化结果(都采用 800 线程随机关键词随即时间范围):
 第一组,时间精度为秒:
 方式 直接用 RangeFilter 使用 cache 不用 filter
 平均每个线程耗时 10s 1s 300ms
 
第二组,时间精度为天
 方式 直接用 RangeFilter 使用 cache 不用 filter
 平均每个线程耗时 900ms 360ms 300ms
 
由以上数据可以得出结论:
 1、尽量降低时间精度,将精度由秒换成天带来的性能提高甚至比使用 cache 还好,最好不使用 filter。
 2、在不能降低时间精度的情况下,使用 cache 能带了 10 倍左右的性能提高。
 
9.2.3 使用更好的分析器
 这个跟创建索引优化道理差不多,索引文件小了搜索自然会加快。当然这个提高也是有限的。较好的分析器相对于最差的分析器对性能的提升在 20% 以下。
 
10 一些经验
 
10.1 关键词区分大小写
 or AND TO 等关键词是区分大小写的,lucene 只认大写的,小写的当做普通单词。
 
10.2 读写互斥性
 同一时刻只能有一个对索引的写操作,在写的同时可以进行搜索
 
10.3 文件锁
 在写索引的过程中强行退出将在 tmp 目录留下一个 lock 文件,使以后的写操作无法进行,可以将其手工删除
 
10.4 时间格式
 lucene 只支持一种时间格式 yyMMddHHmmss,所以你传一个 yy-MM-dd HH:mm:ss 的时间给 lucene 它是不会当作时间来处理的
 
10.5 设置 boost
 有些时候在搜索时某个字段的权重需要大一些,例如你可能认为标题中出现关键词的文章比正文中出现关键词的文章更有价值,你可以把标题的 boost 设置的更大,那么搜索结果会优先显示标题中出现关键词的文章(没有使用排序的前题下)。使用方法:
 Field. setBoost(float boost); 默认值是 1.0,也就是说要增加权重的需要设置得比 1 大。

Lucene 的详细介绍:请点这里
Lucene 的下载地址:请点这里

正文完
星哥说事-微信公众号
post-qrcode
 
星锅
版权声明:本站原创文章,由 星锅 2022-01-20发表,共计20236字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中