# Lucene应用实战


<!--more-->


## 1. 索引创建和搜索流程

![示意图](https://img.zhaojq.top/20260727091720859.png "示意图")

### 1.1 索引创建流程

1. 一些要索引的原文档(Document)数据

   ```
   采集数据分类： 
   1、对于互联网上网页，可以使用工具将网页抓取到本地生成html文件。
   2、数据库中的数据，可以直接连接数据库读取表中的数据。 
   3、文件系统中的某个文件，可以通过I/O操作读取文件的内容。
   ```

   如我们要分析的数据内容是

   ```
   Lucene Core is a Java library providing powerful indexing and search features, as well as spellchecking, hit highlighting and advanced analysis/tokenization capabilities. The PyLucene sub project provides Python bindings for Lucene Core.
   ```

   ```
   Solr is highly scalable, providing fully fault tolerant distributed indexing, search and analytics. It exposes Lucene's features through easy to use JSON/HTTP interfaces or native clients for Java and other languages.
   ```

   ```
   The Apache Hadoop software library is a framework that allows for the distributed processing of large data sets across clusters of computers using simple programming models.
   ```

2. 创建文档对象, 进行词法分析,语言处理, 将原文档传给分词器(Tokenizer)形成一系列词(Term)

3. 索引创建 将得到的词(Term)传给索引组件(Indexer)形成倒排索引结构![示意图](https://img.zhaojq.top/20260727091721111.png "示意图")

4. 通过索引存储器, 将索引写入到磁盘

### 1.2 搜索过程

```
a) 用户输入查询语句
b) 对查询语句经过词法分析和语言分析得到一系列词(Term)
c) 通过语法分析得到一个查询树
d) 通过索引存储将索引读到内存
e) 利用查询树搜索索引，从而得到每个词(Term)的文档列表，对文档列表进行交、差、并得到结果文
档
f) 将搜索到的结果文档按照对查询语句的相关性进行排序
g) 返回查询结果给用户
```

## 2. Lucene索引创建和搜索实现

示例代码：[lucene-index](https://gitee.com/ixinglan/lucene-demo.git) **LuceneIndexTest#testCreateIndex/testSearchIndex**

Luke作为Lucene工具包中的一个工具（https://github.com/DmitryKey/luke/releases），可以通过界面来进行索引文件的查询、修改

Lucene可以通过query对象输入查询语句。同数据库的sql一样，lucene也有固定的查询语法：最基本的有比如：AND, OR, NOT 等（必须大写）

> 如：
>
> 用户想找一个desc中包括java关键字和lucene关键字的文档。
>
> 它对应的查询语句：desc:java AND desc:lucene

## 3. Field域的使用

### 3.1 Field属性

Lucene存储对象是以Document为存储单元，对象中相关的属性值则存放到Field中

Field是文档中的域，包括Field名和Field值两部分，一个文档可以包括多个Field，**Document只是Field的一个承载体**，Field值即为要索引的内容，也是要搜索的内容

Field三大属性：

- 是否分词（tokenized)

  - 是： 作分词处理，即将Field值进行分词，分词的目的是为了索引

    > 比如：商品名称、商品简介等，这些内容用户要输入关键字搜索，由于搜索的内容格式不固定、内容多需要 分词后将语汇单元索引。

  - 否: 不作分词处理

    如: 订单号,身份证等

- 是否索引

  - 是: 进行索引.将Field分词后的词或整个Field值进行索引，索引的目的是为了搜索

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

  - 否：不索引.该域的内容无法搜索到

    如:文件路径，图片路径等，不用作为查询条件的不用索引

- 是否存储

  - 是: 将field值存储在文档中,存储在文档中的field才可以从document中获取

    如: 商品名称,订单号,凡是将来要从document中获取的都要存储

  - 否: 不存储field值,不存储的field无法通过document获取

    如: 商品简介,内容较大不用存储.

### 3.2 Field常用类型

Field对应的类是 `org.apache.lucene.document.Field `，该类实现了`org.apache.lucene.document.IndexableField `接口, 代表用于indexing的一个字段。Field类比较底层一些，所以Lucene实现了许多Field子类，用于不同的场景

| Field类型                                                    | 数据类型     | 是否分词 | 是否索引 | 是否存储 | 说明                                                         |
| ------------------------------------------------------------ | ------------ | -------- | -------- | -------- | ------------------------------------------------------------ |
| StringField(<br>FieldName, <br/>FieldValue, <br/>Store.YES)  | 字符串       | N        | Y        | Y/N      | 字符串类型Field,不分词,<br/>作为一个整体进行索引(如:身份证号,订单号) |
| TextField(<br/>FieldName,<br/>FieldValue,<br/>Store.NO)      | 文本类型     | Y        | Y        | Y/N      | 文本类型Field,分词并且索引                                   |
| LongField(<br/>FieldName,<br/>FieldValue,<br/>Store.YES)<br/>或<br/>LongPoint(<br/>String name<br/>, int... point<br/>)等 | 数值类型     | Y        | Y        | Y/N      | 在Lucene6.0中, LongField替换为LongPoint,<br/>IntField替换为IntPoint,FloatField替换为<br/>FloatPoint，DoubleField替换 为DoublePoint。<br/>对数值型字段索引，索引不存储。要存储结 合StoredField即可 |
| StoredField(<br/>FieldName, <br/>FieldValue)                 | 支持多种类型 | N        | N        | Y        | 构建不同类型的Field,不分词,不索引,<br/>要存储(如:商品图片路径) |

### 3.3 Field示例代码

示例代码：[lucene-index](https://gitee.com/ixinglan/lucene-demo.git) **LuceneIndexTest#createIndexForField**

## 4. 索引的维护

- 添加索引

  `indexWriter.addDocument(doc);`

- 索引删除

  根据Term项删除索引，满足条件的将全部删除。

  `indexWriter.deleteDocuments(new Term("name", "game"));`

- 全部删除

  `indexWriter.deleteAll();`

- 更新索引

  更新索引是先删除再添加，建议对更新需求采用此方法并且要保证对已存在的索引执行更新，可以先查询出来，确定更新记录存在执行更新操作

  如果更新索引的目标文档对象不存在，则执行添加。

示例代码: [lucene-index](https://gitee.com/ixinglan/lucene-demo.git) **LuceneIndexTest**

## 5. 分词器

### 5.1 分词器概念

采集到的数据会存储到Document对象的Field域中，分词器就是将Document中Field的value值切分成一个一个的词。

**停用词**: 停用词是为节省存储空间和提高搜索效率，搜索程序在索引页面或处理搜索请求时会自动忽略某些字或词，这些字或词即被称为Stop Words(停用词)。比如语气助词、副词、介词、连接词等，通常自身并无明确的意义，只有将其放入一个完整的句子中才有一定作用，如常见的“的”、“在”、“是”、“啊” 、 a、an、the 等

**扩展词**: 扩展词 就是分词器默认不会切出的词 但我们希望分词器切出这样的词 。

**过滤**：包括去除标点符号过滤、去除停用词过滤（的、是、a、an、the等）、大写转小写、词的形还原（复数形式转成单数形参、过去式转成现在式。。。）等。

### 5.2 分词器案例

对于分词来说，不同的语言，分词规则不同。Lucene作为一个工具包提供不同国家的分词器，本例子使用**StandardAnalyzer**，它可以对用英文进行分词。

示例代码: [lucene-index](https://gitee.com/ixinglan/lucene-demo.git) **AnalyzerTest**

如下是org.apache.lucene.analysis.standard.StandardAnalyzer的部分源码：

```java
protected TokenStreamComponents createComponents(String fieldName) {
        final StandardTokenizer src = new StandardTokenizer();
        src.setMaxTokenLength(this.maxTokenLength);
        TokenStream tok = new LowerCaseFilter(src);
        TokenStream tok = new StopFilter(tok, this.stopwords);
        return new TokenStreamComponents(src, tok) {
            protected void setReader(Reader reader) {
                src.setMaxTokenLength(StandardAnalyzer.this.maxTokenLength);
                super.setReader(reader);
            }
        };
    }
```

**Tokenizer**就是分词器，负责将reader转换为语汇单元即进行分词处理，Lucene提供了很多的分词器，也可以使用第三方的分词，比如**IKAnalyzer**一个中文分词器

**TokenFilter**是分词过滤器，负责对语汇单元进行过滤，TokenFilter可以是一个过滤器链儿，Lucene提供了很多的分词器过滤器，比如大小写转换、去除停用词等。

如下是语汇单元的生成过程:创建一个Tokenizer分词器，经过三个TokenFilter生成语汇单元Token

Tokenizer --->TokenFilter(标准过滤)--->TokenFilter(大小写过滤)--->TokenFilter(停用词过滤)---->Tokens

### 5.3 中文分词器

英文是以单词为单位的，单词与单词之间以空格或者逗号句号隔开。所以对于英文，我们可以简单以空格判断某个字符串是否为一个单词，比如I love China，love 和 China很容易被程序区分开来

而中文则以字为单位，字又组成词，字和词再组成句子。中文“我是中国人”就不一样了，电脑不知道“中 国”是一个词语还是“是 中”是一个词语

把中文的句子切分成有意义的词，就是中文分词，也称切词.

如: 我是中国人: 我, 是, 我是, 中国, 人

- Lucene自带中文分词器

  **StandardAnalyzer**: 单字分词, 就是按照中文一个字一个字地进行分词

  **CJKAnalyzer**: 按两个字进行切分, 如"我是中国人": 我是,是中,中国, 国人

  两个都无法满足需求

- 中文分词器**IKAnalyzer**

  IKAnalyzer继承Lucene的Analyzer抽象类，使用IKAnalyzer和Lucene自带的分析器方法一样，将Analyzer测试代码改为IKAnalyzer测试中文分词效果。

  如果使用中文分词器ik-analyzer，就需要在索引和搜索程序中使用一致的分词器：IK-analyzer。

示例代码同上: [lucene-index](https://gitee.com/ixinglan/lucene-demo.git) **AnalyzerTest**

### 5.4 扩展中文词库

如果想配置扩展词和停用词，就创建扩展词的文件和停用词的文件。

注意：不要用window自带的记事本保存扩展词文件和停用词文件，那样的话，格式中是含有bom的。

从ikanalyzer包中拷贝配置文件 拷贝到资源文件夹中

**IKAnalyzer.cfg.xml**配置文件

```xml
<?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>
```

中文词库，添加新词的地方 ext.dic

stopword.dic是存放停用词的地方

最终分词效果 和之前的分词器对比

## 6. 搜索实战

创建查询的两种方式:

- 使用lucene提供Query子类

  Query是一个抽象类，lucene提供了很多查询对象，比如TermQuery项精确查询，WildcardQuery统配查询 各种Point的数字范围查询等

  ```
  1.TermQuery 词项查询
  2.BooleanQuery 布尔查询
  3.PhraseQuery 短语查询
  4.MultiPhraseQuery多重短语查询
  5.SpanNearQuery 临近查询(跨度查询)
  6.TermRangeQuery 词项范围查询
  7.PrefixQuery,WildcardQuery,RegexpQuery
  8.FuzzyQuery 模糊查询
  9.数值查询
  ```

- 使用QueryParser解析查询表达式

  - 传统的解析器：QueryParser和MultiFieldQueryParser
  - 基于新的 flflexible 框架的解析器：StandardQueryParser

**TopDocs:**

Lucene搜索结果可通过TopDocs遍历, 其提供了少量的属性,如下:

- totalHits: 匹配搜索条件的总记录数
- scoreDocs: 顶部匹配记录

注:

- Search方法需要指定匹配记录数量n：search(query, n)

- TopDocs.totalHits：是匹配索引库中所有记录的数量

- TopDocs.scoreDocs：匹配相关度高的前边记录数组，scoreDocs的长度小于等于search方法指定的参数n

以下搜索示例代码: [lucene-index](https://gitee.com/ixinglan/lucene-demo.git) **QueryTest**

### 6.1 Query子类搜索

- TermQuery

  不使用分词器, 精确匹配查找

- BooleanQuery

  布尔查询,实现组合条件查询

  ```
  组合关系代表的意思如下:
   1、MUST和MUST表示“与”的关系，即“交集”。
   2、MUST和MUST_NOT前者包含后者不包含。
   3、MUST_NOT和MUST_NOT没意义
   4、SHOULD与MUST表示MUST，SHOULD失去意义；
   5、SHOULD与MUST_NOT相当于MUST与MUST_NOT。
   6、SHOULD与SHOULD表示“或”的关系，即“并集”。
  ```

- 短语查询 PhraseQuery

- 跨度查询 SpanTermQuery

- 模糊查询 WildcardQuery

  ：通配符查询， *表示0个或多个字符，

  ?表示1个字符，

  \是转义符

  > 通配符查询可能会比较慢，不可以通配符开头（那样就是所有词项了）

- 数值查询

### 6.2 QueryParser搜索

**查询语法**

- 基础查询,关键词查询

  域名+":"+搜索的关键字

  如:name:java

- 范围查询

  域名+":"+[最小值 TO 最大值]

  如:size:[1 TO 1000]

  注意：QueryParser不支持对数字范围的搜索，它支持字符串范围。数字范围搜索建议使用对应Point。

- 组合条件查询

  ```
  第一种写法: 
  1）+条件1 + 条件2：两个条件之间是并且的关系and
  例如：+filename:lucene + content:lucene
  2）+条件1 条件2：必须满足第一个条件，应该满足第二个条件
  例如：+filename:lucene content:lucene
  3）条件1 条件2：两个条件满足其一即可。
  例如：filename:lucene content:lucene
  4）-条件1条件2：必须不满足条件1，要满足条件2
  例如：-filename:lucene content:lucene
  
  第二种写法：
  条件1 AND 条件2
  条件1 OR 条件2
  条件1 NOT 条件2
  ```

  

