Skip to content
标签
搜索
字数
7395 字
阅读时间
30 分钟

一、概述

Lucene是apache下的一个开放源代码的全文检索引擎工具包。提供了完整的查询引擎和索引引擎,部分文本分析引擎,可以使用使用Lucene实现全文检索。

常用于对于数据量大、数据结构不固定的数据可采用全文检索方式搜索,比如百度、Google等搜索引擎、论坛站内搜索、电商网站站内搜索等。

官网

1.1 实现全文检索流程

image-20201229201439828

  1. 创建索引

    对文档索引的过程,将用户将要进行搜索的文档内容进行索引,索引存储在索引库(index)中

    1. 获得搜索的对象(原始文档)

      原始文档是指要索引和搜索的内容。原始内容包括互联网上的网页、数据库中的数据、磁盘上的文件等。

    2. 创建文档对象

      获取原始内容的目的是为了索引,在索引前需要将原始内容创建成文档(Document),文档中包括一个一个的域(Field),域中存储内容

      每个Document可以有多个Field,不同的Document可以有不同的Field,同一个Document可以有相同的Field(域名和域值都相同)

      每个文档都有一个唯一的编号,就是文档id

    3. 分析文档

      将原始内容创建为包含域(Field)的文档(document),需要再对域中的内容进行分析,分析的过程是经过对原始文档提取单词、将字母转为小写、去除标点符号、去除停用词等过程生成最终的语汇单元,可以将语汇单元理解为一个一个的单词

      每个单词叫做一个Term,不同的域中拆分出来的相同的单词是不同的term。term中包含两部分一部分是文档的域名,另一部分是单词的内容。

    4. 创建索引

      对所有文档分析得出的语汇单元进行索引,索引的目的是为了搜索,最终要实现只搜索被索引的语汇单元从而找到Document(文档)

      注意:创建索引是对语汇单元索引,通过词语找文档,这种索引的结构叫倒排索引结构

      传统方法是根据文件找到该文件的内容,在文件内容中匹配搜索关键字,这种方法是顺序扫描方法,数据量大、搜索慢。

      倒排索引结构是根据内容(词语)找文档

      倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它的规模较小,而文档集合较大。

  2. 查询索引

    查询索引也是搜索的过程。搜索就是用户输入关键字,从索引(index)中进行搜索的过程。根据关键字搜索索引,根据索引找到对应的文档,从而找到要搜索的内容(这里指磁盘上的文件)

    全文检索系统提供用户搜索的界面供用户提交搜索的关键字,搜索完成展示搜索结果

    1. 创建查询

      用户输入查询关键字执行搜索之前需要先构建一个查询对象,查询对象中可以指定查询要搜索的Field文档域、查询关键字等,查询对象会生成具体的查询语法

      语法:fileName:表示要搜索fileName域的内容为“”的文档

    2. 执行查询

      搜索索引过程:根据查询语法在倒排索引词典表中分别找出对应搜索词的索引,从而找到索引所链接的文档链表。

    3. 渲染结果

      对匹配的信息操作(高亮显示)

1.2 底层储存结构

  • 索引(Index) :

一个目录一个索引,在Lucene中一个索引是放在一个文件夹中的。

  • 段(Segment) :

一个索引(逻辑索引)由多个段组成, 多个段可以合并, 以减少读取内容时候的磁盘IO.Lucene中的数据写入会先写内存的一个Buffer,当Buffer内数据到一定量后会被flush成一个Segment,每个Segment有自己独立的索引,可独立被查询,但数据永远不能被更改。这种模式避免了随机写,数据写入都是批量追加,能达到很高的吞吐量。Segment中写入的文档不可被修改,但可被删除,删除的方式也不是在文件内部原地更改,而是会由另外一个文件保存需要被删除的文档DocID,保证数据文件不可被修改。Index的查询需要对多个Segment进行查询并对结果进行合并,还需要处理被删除的文档,为了对查询进行优化,Lucene会有策略对多个Segment进行合并。

  • 文档(Document) :

文档是我们建索引的基本单位,不同的文档是保存在不同的段中的,一个段可以包含多篇文档。新添加的文档是单独保存在一个新生成的段中,随着段的合并,不同的文档合并到同一个段中。

  • 域(Field) :

一篇文档包含不同类型的信息,可以分开索引,比如标题,时间,正文,描述等,都可以保存在不同的域里。不同域的索引方式可以不同

  • 词(Term) :

词是索引的最小单位,是经过词法分析和语言处理后的字符串。

1.3 索引库文件扩展名对照表

名称文件扩展名简短描述
Segments Filesegments_N保存了一个提交点(a commit point)的信息
Lock Filewrite.lock防止多个IndexWriter同时写到一份索引文件中
Segment Info.si保存了索引段的元数据信息
Compound File.cfs,.cfe一个可选的虚拟文件,把所有索引信息都存储到复合索引文件中
Fields.fnm保存fields的相关信息
Field Index.fdx保存指向field data的指针
Field Data.fdt文档存储的字段的值
Term Dictionary.timterm词典,存储term信息
Term Index.tip到Term Dictionary的索引
Frequencies.doc由包含每个term以及频率的docs列表组成
Positions.pos存储出现在索引中的term的位置信息
Payloads.pay存储额外的per-position元数据信息,例如字符偏移和用户payloads
Norms.nvd,.nvm.nvm文件保存索引字段加权因子的元数据,.nvd文件保存索引字段加权数据
Per-Document Values.dvd,.dvm.dvm文件保存索引文档评分因子的元数据,.dvd文件保存索引文档评分数据
Term Vector Index.tvx将偏移存储到文档数据文件中
Term Vector Documents.tvd包含有term vectors的每个文档信息
Term Vector Fields.tvf字段级别有关term vectors的信息
Live Documents.liv哪些是有效文件的信息
Point values.dii,.dim保留索引点,如果有的话

1.4 词典构建

为何Lucene大数据量搜索快, 要分两部分来看 :

一点是因为底层的倒排索引存储结构.另一点就是查询关键字的时候速度快, 因为词典的索引结构.

词典数据结构对比

倒排索引中的词典位于内存,其结构尤为重要,有很多种词典结构,各有各的优缺点,最简单如排序数组,通过二分查找来检索数据,更快的有哈希表,磁盘查找有B树、B+树,但一个能支持TB级数据的倒排索引结构需要在时间和空间上有个平衡,

数据结构优缺点
跳跃表占用内存小,且可调,但是对模糊查询支持不好
排序列表Array/List使用二分法查找,不平衡
字典树查询效率跟字符串长度有关,但只适合英文词典
哈希表性能高,内存消耗大,几乎是原始数据的三倍
双数组字典树适合做中文词典,内存占用小,很多分词工具均采用此种算法
Finite State Transducers (FST)一种有限状态转移机,Lucene 4有开源实现,并大量使用
B树磁盘索引,更新方便,但检索速度慢,多用于数据库

Lucene3.0之前使用的也是跳跃表结构,后换成了FST,但跳跃表在Lucene其他地方还有应用如倒排表合并和文档号索引。

跳跃表原理

Lucene3.0版本之前使用的跳跃表结构后换成了FST结构

优点 :结构简单、跳跃间隔、级数可控,Lucene3.0之前使用的也是跳跃表结构,,但跳跃表在Lucene其他地方还有应用如倒排表合并和文档号索引。 缺点 :模糊查询支持不好.

单链表 :

单链表中查询一个元素即使是有序的,我们也不能通过二分查找法的方式缩减查询时间。通俗的讲也就是按照链表顺序一个一个找.

1.5 数据分类

结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等。

非结构化数据:指不定长或无固定格式的数据,如邮件,word文档等磁盘上的文件

结构化数据由于数据的存储都是有规律的,有行有列而且数据格式、数据长度都是固定的,所以他的搜随很容易实现,如通过sql查询数据库

非结构化数据查询方法有:顺序扫描法、全文检索法

顺序扫描法:对一个个文件进行扫描查找,从一个文件的头到尾,如果包含即为符合条件的文档,然后接着往下查找,直至所有文件扫描完毕。效率慢

全文检索法:

将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这部分从非结构化数据中提取出的然后重新组织的信息,称之索引、如:字典

先建立索引,再对索引进行搜索的过程叫做全文检索。该方式创建索引的过程也非常耗时,但创建后可以多次使用,全文检索主要处理查询。

二、java操作示例

2.1 索引操作

创建索引

java
public void createIndex() throws Exception {

    //指定索引库存放的路径
    //D:\temp\index
    Directory directory = FSDirectory.open(new File("D:\\temp\\index").toPath());
    //索引库还可以存放到内存中
    //Directory directory = new RAMDirectory();
    //创建indexwriterCofig对象
    IndexWriterConfig config = new IndexWriterConfig();
    //创建indexwriter对象
    IndexWriter indexWriter = new IndexWriter(directory, config);
    //原始文档的路径
    File dir = new File("D:\\temp\\searchsource");
    for (File f : dir.listFiles()) {
        //文件名
        String fileName = f.getName();
        //文件内容
        String fileContent = FileUtils.readFileToString(f);
        //文件路径
        String filePath = f.getPath();
        //文件的大小
        long fileSize  = FileUtils.sizeOf(f);
        //创建文件名域
        //第一个参数:域的名称
        //第二个参数:域的内容
        //第三个参数:是否存储
        Field fileNameField = new TextField("filename", fileName, Field.Store.YES);
        //文件内容域
        Field fileContentField = new TextField("content", fileContent, Field.Store.YES);
        //文件路径域(不分析、不索引、只存储)
        Field filePathField = new TextField("path", filePath, Field.Store.YES);
        //文件大小域
        Field fileSizeField = new TextField("size", fileSize + "", Field.Store.YES);

        //创建document对象
        Document document = new Document();
        document.add(fileNameField);
        document.add(fileContentField);
        document.add(filePathField);
        document.add(fileSizeField);
        //创建索引,并写入索引库
        indexWriter.addDocument(document);
    }
    //关闭indexwriter
    indexWriter.close();
}

查询索引

还可以通过Luke软件进行查询

java
public void searchIndex() throws Exception {
    //指定索引库存放的路径
    //D:\temp\index
    Directory directory = FSDirectory.open(new File("D:\\temp\\index").toPath());
    //创建indexReader对象
    IndexReader indexReader = DirectoryReader.open(directory);
    //创建indexsearcher对象
    IndexSearcher indexSearcher = new IndexSearcher(indexReader);
    //创建查询
    Query query = new TermQuery(new Term("filename", "apache"));
    //执行查询
    //第一个参数是查询对象,第二个参数是查询结果返回的最大值
    TopDocs topDocs = indexSearcher.search(query, 10);
    // 根据Query搜索,添加过滤策略,返回评分最高的n条记录  indexSearcher.search(query, filter, n)
    // 根据Query搜索,添加排序策略,返回评分最高的n条记录  indexSearcher.search(query, n, sort)
    // 根据Query搜索,添加过滤策略,添加排序策略,返回评分最高的n条记录  indexSearcher.search(booleanQuery,filter, n, sort)
    //查询结果的总条数
    System.out.println("查询结果的总条数:"+ topDocs.totalHits);
    //遍历查询结果
    //topDocs.scoreDocs存储了document对象的id
    for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
        //scoreDoc.doc属性就是document对象的id
        //根据document的id找到document对象
        Document document = indexSearcher.doc(scoreDoc.doc);
        System.out.println(document.get("filename"));
        //System.out.println(document.get("content"));
        System.out.println(document.get("path"));
        System.out.println(document.get("size"));
        System.out.println("-------------------------");
    }
    //关闭indexreader对象
    indexReader.close();
}

2.2 分析器

分词器说明

StandardAnalyzer:Lucene提供的标准分词器, 可以对用英文进行分词, 对中文是单字分词, 也就是一个字就认为是一个词.

WhitespaceAnalyzer:仅仅是去掉了空格,没有其他任何操作,不支持中文。

SimpleAnalyzer:将除了字母以外的符号全部去除,并且将所有字母变为小写,需要注意的是这个分词器同样把数字也去除了,同样不支持中文。

CJKAnalyzer:支持中日韩文字,前三个字母也就是这三个国家的缩写。对中文是二分法分词, 去掉空格, 去掉标点符号。

对中文支持也不是很好,扩展性差,扩展词库,禁用词库和同义词库等不好处理。

java
//标准分词器效果
public void testTokenStream() throws Exception {
    //创建一个标准分析器对象
    Analyzer analyzer = new StandardAnalyzer();
    //获得tokenStream对象
    //第一个参数:域名,可以随便给一个
    //第二个参数:要分析的文本内容
    TokenStream tokenStream = analyzer.tokenStream("test", "The Spring Framework provides a comprehensive programming and configuration model.");
    //添加一个引用,可以获得每个关键词
    CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
    //添加一个偏移量的引用,记录了关键词的开始位置以及结束位置
    OffsetAttribute offsetAttribute = tokenStream.addAttribute(OffsetAttribute.class);
    //将指针调整到列表的头部
    tokenStream.reset();
    //遍历关键词列表,通过incrementToken方法判断列表是否结束
    while(tokenStream.incrementToken()) {
        //关键词的起始位置
        System.out.println("start->" + offsetAttribute.startOffset());
        //取关键词
        System.out.println(charTermAttribute);
        //结束位置
        System.out.println("end->" + offsetAttribute.endOffset());
    }
    tokenStream.close();
}

中文分词器

  • Lucene自带中文分词器

    StandardAnalyzer:单字分词:就是按照中文一个字一个字地进行分词。如:“我爱中国”, 效果:“我”、“爱”、“中”、“国”。

    SmartChineseAnalyzer对中文支持较好,但扩展性差,扩展词库,禁用词库和同义词库等不好处理

  • IK中文分词器

    需要引入jar包,将配置文件和拓展词典、停用词典配置放入classpath下,注意:hotword.dic和ext_stopword.dic文件的格式为UTF-8,注意是无BOM 的UTF-8 编码。禁止使用windows记事本编辑扩展词典文件

xml
<dependency> 
    <groupId>org.wltea.ik-analyzer</groupId>
    <artifactId>ik-analyzer</artifactId> 
    <version>8.1.0</version>
</dependency>
java
//使用自定义的ik分词器
@Test
public void addDocument() throws Exception {
    //索引库存放路径
    Directory directory = FSDirectory.open(new File("D:\\temp\\index").toPath());
    IndexWriterConfig config = new IndexWriterConfig(new IKAnalyzer());
    //创建一个indexwriter对象
    IndexWriter indexWriter = new IndexWriter(directory, config);
//...
}

分词器配置

xml
ext.dic拓展词典  IKAnalyzer.cfg.xml IK配置文件  stopword.dic 停用词典
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> 
<properties>
    <comment>IK Analyzer 扩展配置</comment> 
    <!--用户可以在这里配置自己的扩展字典 -->
    <entry key="ext_dict">ext.dic;</entry>
    <!--用户可以在这里配置自己的扩展停止词字典--> 
    <entry key="ext_stopwords">stopword.dic;</entry>
</properties>

停用词典stopword.dic作用 :

停用词典中的词例如: a, an, the, 的, 地, 得等词汇, 凡是出现在停用词典中的字或者词, 在切分词的时候会被过滤掉.

扩展词典ext.dic作用 :

扩展词典中的词例如: 贵州茅台等专有名词, 在汉语中一些公司名称, 行业名称, 分 类, 品牌等不是汉语中的词汇, 是专有名词. 这些分词器默认不识别, 所以需要放入扩展词典中, 效果是被强制分成一个词.

2.3 索引库维护

添加

Field域的属性:

是否分析(tokenized):是否对域的内容进行分词处理。前提是我们要对域的内容进行查询。

是否索引(indexed):将Field分析后的词或整个Field值进行索引,只有索引方可搜索到。

比如:商品名称、商品简介分析后进行索引,订单号、身份证号不用分析但也要索引,这些将来都要作为查询条件。

是否存储(stored):将Field值存储在文档中,存储在文档中的Field才可以从Document中获取

比如:商品名称、订单号,凡是将来要从Document中获取的Field都要存储。

是否存储的标准:是否要将内容展示给用户

Field类数据类型Analyzed 是否分析Indexed 是否索引Stored 是否存储说明
StringField(FieldName, FieldValue,Store.YES))字符串NYY或N这个Field用来构建一个字符串Field,但是不会进行分析,会将整个串存储在索引中,比如(订单号,姓名等) 是否存储在文档中用Store.YES或Store.NO决定
FloatPoint(FieldName, FieldValue)Float型YYN这个Field用来构 建一个Float数字 型Field,进行分 词和索引,不存 储, 比如(价格) 存 储在文档中
DoublePoint(FieldName,FieldValue)Double型YYN这个Field用来构 建一个Double数 字型Field,进行 分词和索引,不 存储
LongPoint(String name, long... point)Long型YYN可以使用LongPoint、IntPoint等类型存储数值类型的数据。让数值类型可以进行索引。但是不能存储数据,如果想存储数据还需要使用StoredField。
IntPoint(FieldName, FieldValue)Integer 型YYN这个Field用来构 建一个Integer数 字型Field,进行 分词和索引,不 存储
StoredField(FieldName, FieldValue)重载方法,支持多种类型NNY这个Field用来构建不同类型Field 不分析,不索引,但要Field存储在文档中
TextField(FieldName, FieldValue, Store.NO) 或 TextField(FieldName, reader)字符串 或 流YYY或N如果是一个Reader, lucene猜测内容比较多,会采用Unstored的策略.
NumericDocValuesField(FieldName, FieldValue)数值---配合其他域排序 使用
java
//添加索引
@Test
public void addDocument() throws Exception {
    //索引库存放路径
    Directory directory = FSDirectory.open(new File("D:\\temp\\index").toPath());
    IndexWriterConfig config = new IndexWriterConfig(new IKAnalyzer());
    //创建一个indexwriter对象
    IndexWriter indexWriter = new IndexWriter(directory, config);
    //创建一个Document对象
    Document document = new Document();
    //向document对象中添加域。
    //不同的document可以有不同的域,同一个document可以有相同的域。
    document.add(new TextField("filename", "新添加的文档", Field.Store.YES));
    document.add(new TextField("content", "新添加的文档的内容", Field.Store.NO));
    //LongPoint创建索引
    document.add(new LongPoint("size", 1000l));
    //StoreField存储数据
    document.add(new StoredField("size", 1000l));
    //不需要创建索引的就使用StoreField存储
    document.add(new StoredField("path", "d:/temp/1.txt"));
    //添加文档到索引库
    indexWriter.addDocument(document);
    //关闭indexwriter
    indexWriter.close();

}

删除

java
//删除全部索引
// 将索引目录的索引信息全部删除,直接彻底删除,无法恢复
	@Test
	public void deleteAllIndex() throws Exception {
		IndexWriter indexWriter = getIndexWriter();
		//删除全部索引
		indexWriter.deleteAll();
		//关闭indexwriter
		indexWriter.close();
}

//根据查询条件删除索引
@Test
public void deleteIndexByQuery() throws Exception {
    IndexWriter indexWriter = getIndexWriter();
    //创建一个查询条件
    Query query = new TermQuery(new Term("filename", "apache"));
    //根据查询条件删除
    indexWriter.deleteDocuments(query);
    //关闭indexwriter
    indexWriter.close();
}

修改

java
//原理:先删除后添加
public void updateIndex() throws Exception {
    IndexWriter indexWriter = getIndexWriter();
    //创建一个Document对象
    Document document = new Document();
    //向document对象中添加域。
    //不同的document可以有不同的域,同一个document可以有相同的域。
    document.add(new TextField("filename", "要更新的文档", Field.Store.YES));
    document.add(new TextField("content", " Lucene 简介 Lucene 是一个基于 Java 的全文信息检索工具包," +
                                                       "它不是一个完整的搜索应用程序,而是为你的应用程序提供索引和搜索功能。",
                Field.Store.YES));
    indexWriter.updateDocument(new Term("content", "java"), document);
    //关闭indexWriter
    indexWriter.close();
}

2.4 索引库查询

对要搜索的信息创建Query查询对象,Lucene会根据Query查询对象生成最终的查询语法,类似关系数据库Sql语法一样Lucene也有自己的查询语法,比如:“name:lucene”表示查询Field的name为“lucene”的文档信息。

​ 可通过两种方法创建查询对象:

​ 1)使用Lucene提供Query子类

​ 2)使用QueryParse解析查询表达式

TermQuery

通过项查询,TermQuery不使用分析器所以建议匹配不分词的Field域查询

java
public void testTermQuery() throws Exception {
    Directory directory = FSDirectory.open(new File("D:\\temp\\index").toPath());
    IndexReader indexReader = DirectoryReader.open(directory);
    IndexSearcher indexSearcher = new IndexSearcher(indexReader);
    
    //创建查询对象
    Query query = new TermQuery(new Term("content", "lucene"));
    //执行查询
    TopDocs topDocs = indexSearcher.search(query, 10);
    //共查询到的document个数
    System.out.println("查询结果总数量:" + topDocs.totalHits);
    //遍历查询结果
    for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
        Document document = indexSearcher.doc(scoreDoc.doc);
        System.out.println(document.get("filename"));
        //System.out.println(document.get("content"));
        System.out.println(document.get("path"));
        System.out.println(document.get("size"));
    }
    //关闭indexreader
    indexSearcher.getIndexReader().close();
}

数值范围查询

java
public void testRangeQuery() throws Exception {
    IndexSearcher indexSearcher = getIndexSearcher();
    Query query = LongPoint.newRangeQuery("size", 0l, 10000l);
    printResult(query, indexSearcher);
}

使用queryparser查询

通过QueryParser也可以创建Query,QueryParser提供一个Parse方法,此方法可以直接根据查询语法来查询。Query对象执行的查询语法可通过System.out.println(query);查询。

需要使用到分析器。建议创建索引时使用的分析器和查询索引时使用的分析器要一致。

需要加入queryParser依赖的jar包。

java
public void testQueryParser() throws Exception {
    IndexSearcher indexSearcher = getIndexSearcher();
    //创建queryparser对象
    //第一个参数默认搜索的域
    //第二个参数就是分析器对象
    QueryParser queryParser = new QueryParser("content", new IKAnalyzer());
    Query query = queryParser.parse("Lucene是java开发的");
    // 查询所有
   //  query1 = queryParser.parse("*:*");
    //执行查询
    printResult(query, indexSearcher);
}

private void printResult(Query query, IndexSearcher indexSearcher) throws Exception {
    //执行查询
    TopDocs topDocs = indexSearcher.search(query, 10);
    //共查询到的document个数
    System.out.println("查询结果总数量:" + topDocs.totalHits);
    //遍历查询结果
    for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
        Document document = indexSearcher.doc(scoreDoc.doc);
        System.out.println(document.get("filename"));
        //System.out.println(document.get("content"));
        System.out.println(document.get("path"));
        System.out.println(document.get("size"));
    }
    //关闭indexreader
    indexSearcher.getIndexReader().close();
}

组合查询

java
@Test public void testBooleanSearch() throws Exception {
    // 创建分词器 
    Analyzer analyzer = new IKAnalyzer(); 
    // 创建数值范围搜索对象
    Query query1 = FloatPoint.newRangeQuery("price", 100, 1000);
    // 创建文本搜索对象 
    QueryParser queryParser = new QueryParser("name", analyzer); 
    // 创建搜索对象 
    Query query2 = queryParser.parse("华为手机"); 
    //创建组合搜索对象
    BooleanQuery.Builder builder = new BooleanQuery.Builder();
    builder.add(new BooleanClause(query1, BooleanClause.Occur.MUST));
    builder.add(new BooleanClause(query2, BooleanClause.Occur.MUST_NOT));
    // 2. 创建Directory流对象,声明索引库位置
    Directory directory = FSDirectory.open(Paths.get("E:\\dir")); 
    // 3. 创建索引读取对象IndexReader 
    IndexReader reader = DirectoryReader.open(directory);
    // 4. 创建索引搜索对象 
    IndexSearcher searcher = new IndexSearcher(reader);
    // 5. 使用索引搜索对象,执行搜索,返回结果集TopDocs 
    // 第一个参数:搜索对象,第二个参数:返回的数据条数,指定查询结果最顶部的n条数据返回 
    TopDocs topDocs = searcher.search(builder.build(), 10); 
    System.out.println("查询到的数据总条数是:" + topDocs.totalHits);
    // 获取查询结果集 
    ScoreDoc[] docs = topDocs.scoreDocs;
    // 6. 解析结果集 
    for (ScoreDoc scoreDoc : docs) { 
        // 获取文档
        int docID = scoreDoc.doc;
        Document doc = searcher.doc(docID);
        System.out.println("=============================");
        System.out.println("docID:" + docID); 
        System.out.println("id:" + doc.get("id"));
        System.out.println("name:" + doc.get("name"));
        System.out.println("price:" + doc.get("price"));
        System.out.println("brandName:" + doc.get("brandName")); 
        System.out.println("image:" + doc.get("image"));
    }
    // 7. 释放资源 
    reader.close(); 
}

三、Lucene拓展

3.1 Lucene优化

解决大量磁盘IO

config.setMaxBufferedDocs(100000); 控制写入一个新的segment前内存中保存的document的数目,设置较大的数目可以加快建索引速度。

数值越大索引速度越快, 但是会消耗更多的内存

indexWriter.forceMerge(文档数量); 设置N个文档合并为一个段,数值越大索引速度越快, 搜索速度越慢; 值越小索引速度越慢, 搜索速度越快

更高的值意味着索引期间更低的段合并开销,但同时也意味着更慢的搜索速度,因为此时的索引通常会包含更多的段。如果该值设置的过高,能获得更高的索引性能。但若在最后进行索引优化,那么较低的值会带来更快的搜索速度,因为在索引操作期间程序会利用并发机制完成段合并操作。故建议对程序分别进行高低多种值的测试,利用计算机的实际性能来告诉你最优值。

选择合适的分词器

不同的分词器分词效果不同, 所用时间也不同

虽然StandardAnalyzer切分词速度快过IKAnalyzer, 但是由于StandardAnalyzer对中文支持不好, 所以为了追求好的分词效果, 为了追求查询时的准确率, 也只能用IKAnalyzer分词器, IKAnalyzer支持停用词典和扩展词典, 可以通过调整两个词典中的内容, 来提升查询匹配的精度

写操作读操作特点
SimpleFSDirectoryjava.io.RandomAccessFilejava.io.RandomAccessFile简单实现,并发 能力差
NIOFSDirectoryjava.nio.FileChannelFSDirectory.FSIndexOutput并发能力强, windows平台下 有重大bug
MMapDirectory内存映射FSDirectory.FSIndexOutput读取操作基于内 存

搜索api的选择

  1. 尽量使用TermQuery代替QueryParser
  2. 尽量避免大范围的日期查询

其他注意事项

关键词区分大小写 OR AND TO等关键词是区分大小写的,lucene只认大写的,小写的当做普通单词。

读写互斥性 同一时刻只能有一个对索引的写操作,在写的同时可以进行搜索文件锁 在写索引的过程中强行退出将在tmp目录留下一个lock文件,使以后的写操作无法进行,可以将其手工删除

时间格式 lucene只支持一种时间格式yyMMddHHmmss,所以你传一个yy-MM-dd HH:mm:ss的时间给lucene它是不会当作时间来处理的

设置boost 有些时候在搜索时某个字段的权重需要大一些,例如你可能认为标题中出现关键词的文章比正文中出现关键词的文章更有价值,你可以把标题的boost设置的更大,那么搜索结果会优先显示标题中出现关键词的文章.

3.2 相关度排序

Lucene对查询关键字和索引文档的相关度进行打分,得分高的就排在前边。

如何打分

Lucene是在用户进行检索时实时根据搜索的关键字计算出来的,分两步:

  1. 计算出词(Term)的权重
  2. 根据词的权重值,计算文档相关度得分。

明确索引的最小单位是一个Term(索引词典中的一个词),搜索也是要从Term中搜索,再根据Term找到文档,Term对文档的重要性称为权重,影响Term权重有两个因素:

Term Frequency (tf): 指此Term在此文档中出现了多少次。tf 越大说明越重要。 词(Term)在文档中出现的次数越多,说明此词(Term)对该文档越重要,如“Lucene”这个词,在文档中出现的次数很多,说明该文档主要就是讲Lucene技术的。

Document Frequency (df): 指有多少文档包含次Term。df 越大说明越不重要。 比如,在一篇英语文档中,this出现的次数更多,就说明越重要吗?不是的,有越多的文档包含此词(Term), 说明此词(Term)太普通,不足以区分这些文档,因而重要性越低

boost是一个加权值(默认加权值为1.0f),它可以影响权重的计算。

在索引时对某个文档中的field设置加权值高,在搜索时匹配到这个文档就可能排在前边。在搜索时对某个域进行加权,在进行组合域查询时,匹配到加权值高的域最后计算的相关度得分就高。设置boost是给域(field)或者Document设置的。

人为设置

java
// 1. 创建Query搜索对象
// 创建分词器
Analyzer analyzer = new IKAnalyzer(); 
//查询的域名 
String[] fields = {"name","brandName","categoryName"};
//设置权重
Map<String, Float> boots = new HashMap<>();
boots.put("categoryName", 10000000f); 
// 根据多个域进行搜索 
MultiFieldQueryParser queryParser = new MultiFieldQueryParser(fields, analyzer, boots);
// 创建搜索对象
Query query = queryParser.parse("手机");