Elasticsearch (ES) 搜索引擎: 文本搜索:分析器/分词器、同义词/停用词、拼音搜索、高亮显示、拼写纠错

原文链接:https://xiets.blog.csdn.net/article/details/132349032

版权声明:原创文章禁止转载

专栏目录:Elasticsearch 专栏(总目录)

文本搜索主要指的就是全文搜索,全文搜索是搜索引擎的核心功能,与精确匹配的结构化数据不同,文本(text)数据在构建索引和搜索时都需要进行额外的处理。

Elasticsearch 在存储和搜索文本数据时需要依赖分析器组件,Lucene 负责索引的物理构建和排序,而分析器将在建立索引前对文本数据进行分词和语法处理。搜索文本数据时,也需要先对搜索词进行分词和语法处理,然后使用分词后的子词执行多个子搜索。

全文搜索主要针对 text 类型的字段,使用 match 查询方式进行搜索,而 分析器 就是全文搜索的核心。

1. 分析器

1.1 过滤器与分词器

分析器用于在存储或更新文本数据时,对文本进行分词处理后再建立索引。查询文本类型的字段时,也需要先使用分析器对查询词进行分词处理。

ES 内置了多种分析器,分析器包含三个部分:

  • 字符过滤器(char_filter):可以有0个或多个,对原始文本数据进行粗粒度处理,如去除 HTML 标签,转义原始文本中的特殊字符等。
  • 分词器(tokenizer):有且只能有1个,按照指定规则对文本进行词语切分。
  • 词语过滤器(filter):可以有0个或多个,接收分词处理结果并对其进行规范和优化处理,如把字符统一转换为小写、增加同义词、删除停用词、增加拼音等。

分析器处理文本数据时,三个部分的文本数据流方向如下:

            文本
             |
            \|/
         字符过滤器1
         字符过滤器2
             |
            \|/
           分词器
             |
            \|/
         词语过滤器1
         词语过滤器2
             |
            \|/
    分词1, 分词2, 分词3, ...

1.2 内置的分析器

ES 附带了各种内置分析器,参考:Built-in analyzer reference

内置的分析器无需进一步配置即可在任何索引中直接使用,下面是内置的一些分析器:

扫描二维码关注公众号,回复: 16886770 查看本文章
  1. 标准分析器(standard):根据 Unicode 文本分段算法的定义,将文本划分为单词边界上的词语。它删除了大多数标点符号、小写词语,并支持删除停用词。
  2. 简单分析器(simple):每当遇到非字母的字符时(如:空格、标点符号),分析器就会将文本划分为词语,所有词语都小写。
  3. 空白分析器(whitespace):每当遇到任何空白字符时,分析器都会将文本分为词语,它不小写词语。
  4. 停止分析器(stop):与 simple 类似,但支持去除停用词。
  5. 关键词分析器(keyword):一个“noop”分析器,它接受给定的任何文本,并输出完全相同的文本作为单个词语。
  6. 正则分析器(pattern):使用正则表达式将文本拆分为词语,它支持小写和停止词。
  7. 语言分析器(english/french):提供了许多特定于语言的分析器。
  8. 指纹分析器(fingerprint):一种专业分析器,可创建可用于重复检测的指纹。

如果没有指定使用的分析器,默认情况下,text 类型的字段在建立索引和查询时使用 标准分析器(standard) 分析器。

标准分析器(standard)包含了:标准分词器(Standard Tokenizer)、小写转换过滤器(Lower Case Token Filter)、停用词过滤器(Stop Token Filter ),其中 停用词过滤器 默认情况下禁用。

1.3 测试分析器

分析器 API:Analyze APITest an analyzer

ES 提供了用于测试分析器分析结果的 API,分析器相关请求:

  • GET /_analyze
  • POST /_analyze
  • GET /<index>/_analyze
  • POST /<index>/_analyze

测试分析器请求格式:

POST /_analyze
{
    
    
    "analyzer": "standard",             // 指定分析器名称
    "text": "ES, Hello World!"          // 需要分析的文本
}

POST /_analyze
{
    
    
    "analyzer": "standard",
    "text": ["test es", "the test"]     // 分析文本也可是传递一个数组, 同时分析多个文本
}

POST /<index>/_analyze                  // 使用索引中自定义的分析器
{
    
    
    "analyzer": "my_analyzer",          // 创建索引时在 settings 中自定义的分析器
    "text": "ES, Hello World!"          // 需要分析的文本
}

POST /<index>/_analyze                  // 基于现有索引的 text 类型字段分析
{
    
    
    "field": "<field_name>",            // 使用给定字段使用的分析器, 如果字段不存在则使用默认分析器
    "text": "ES, Hello World!"          // 需要分析的文本
}

GET /_analyze                           // 也可以自定义分析器 (手动指定分词器和过滤器)
{
    
    
    "char_filter" : ["html_strip"],     // 字符过滤器
    "tokenizer" : "keyword",            // 分词器
    "filter" : ["lowercase"],           // 分词过滤器
    "text" : "this is a <b>test</b>"    // 需要分析的文本
}

测试分析器请求:

POST /_analyze
{
    
    
    "analyzer": "standard",
    "text": "ES, Hello World!"
}

// 返回
{
    
    
    "tokens": [                     // 每一个分词表示一个 token
        {
    
    
            "token": "es",          // 分词
            "start_offset": 0,      // 分词在原文本中的开始位置
            "end_offset": 2,        // 分词在原文本中的结束位置(不包括)
            "type": "<ALPHANUM>",   // 词类型
            "position": 0           // 该分词是所有分词中的第几个分词
        },
        {
    
    
            "token": "hello",
            "start_offset": 4,
            "end_offset": 9,
            "type": "<ALPHANUM>",
            "position": 1
        },
        {
    
    
            "token": "world",
            "start_offset": 10,
            "end_offset": 15,
            "type": "<ALPHANUM>",
            "position": 2
        }
    ]
}

使用默认分析器分析中文:

POST /_analyze
{
    
    
    "analyzer": "standard",
    "text": "我是中国人"
}

// 返回
{
    
    
    "tokens": [
        {
    
    
            "token": "我",
            "start_offset": 0,
            "end_offset": 1,
            "type": "<IDEOGRAPHIC>",
            "position": 0
        },
        {
    
    
            "token": "是",
            "start_offset": 1,
            "end_offset": 2,
            "type": "<IDEOGRAPHIC>",
            "position": 1
        },
        {
    
    
            "token": "中",
            "start_offset": 2,
            "end_offset": 3,
            "type": "<IDEOGRAPHIC>",
            "position": 2
        },
        {
    
    
            "token": "国",
            "start_offset": 3,
            "end_offset": 4,
            "type": "<IDEOGRAPHIC>",
            "position": 3
        },
        {
    
    
            "token": "人",
            "start_offset": 4,
            "end_offset": 5,
            "type": "<IDEOGRAPHIC>",
            "position": 4
        }
    ]
}

标准分析器(standard)处理中文词语时,直接把每一个中文字符都单独分为一个词,中文句子中一般包含了许多词语,因此并不适合分析中文。

1.4 创建索引时指定分析器

索引映射中 text 类型的字段需要使用分析器,如果没有指定分析器,默认使用标准分析器(standard),也可以在创建索引时指定分析器。

创建索引时指定默认的分析器,请求格式:

PUT /<index>                            // 创建索引
{
    
    
    "settings": {
    
                           // 索引设置
        "analysis": {
    
                       // 分析器的所有设置
            "analyzer": {
    
                   // 分析器设置
                "default": {
    
                // 当前索引默认的分析器, text字段如果没有指定分析器则用这个
                    "type": "simple"    // "simple" 表示分析器名称
                }
            }
        }
    },
    "mappings": {
    
                           // 映射
        "properties": {
    
                     // 映射的属性
            // ... 字段列表
        }
    }
}

创建索引时为某个具体的 text 类型字段指定分析器,请求格式:

PUT /<index>                                    // 创建索引
{
    
    
    "mappings": {
    
                                   // 映射
        "properties": {
    
                             // 映射的属性
            "<field>": {
    
                            // 字段
                "type": "text",                 // 字段类型
                "analyzer": "standard",         // 当前字段建立索引(写入文档)时使用的分析器
                "search_analyzer": "simple"     // 搜索当前字段时分析搜索词使用的分析器, 如果没有设置则默认与建立索引使用的分析器相同
            }
        }
    }
}

1.5 自定义分析器

分析器是字符过滤器、分词器、词语过滤器三部分的组合,ES 内置的分析器已经做好的相关的组合。这里说的自定义分析器指的是根据 ES 现有的过滤器和分词器组合出新的一个分析器。

创建索引时使用自定义分析器,请求格式:

PUT /<index>
{
    
    
    "settings": {
    
    
        "analysis": {
    
    
            "tokenizer": {
    
                          // 分词器
                "<tokenizer_name>": {
    
               // 自定义分词器, 节点名称就是自定义的分词器名称
                    "type": "pattern",          // 使用现有分词器, "pattern"表示正则分词器
                    "pattern": "<regex>"        // 用于切分词语的正则表达式, "pattern"分词器的参数
                }
            },
            "char_filter": {
    
                        // 字符过滤器
                "<char_filter_name>": {
    
             // 自定义字符过滤器, 节点名称就是自定义的字符过滤器名称
                    "type": "html_strip"        // 使用现有的字符过滤器, "html_strip"表示用于过滤HTML标签字符的过滤器
                }
            },
            "filter": {
    
                             // 词语过滤器
                "<filter_name>": {
    
                  // 自定义词语过滤器, 节点名称就是自定义的词语过滤器名称
                    "type": "stop",             // 使用现有的词语过滤器, "stop"表示停用词过滤器
                    "ignore_case": true,        // 忽略大小写, "stop"词语过滤器的参数
                    "stopwords": ["the", "is"]  // 停用词, "stop"词语过滤器的参数
                }
            },
            "analyzer": {
    
                                       // 分析器 (分词器 + 字符过滤器 + 词语过滤器)
                "<analyzer_name>": {
    
                            // 自定义分析器, 节点名称就是自定义的分析器名称
                    "tokenizer": "<tokenizer_name>",        // 指定使用的分词器, 必须是现有的或者前面自定义的
                    "char_filter": ["<char_filter_name>"],  // 指定使用的字符过滤器, 必须是现有的或者前面自定义的
                    "filter": ["<filter_name>"]             // 指定使用的词语过滤器, 必须是现有的或者前面自定义的
                }
            }
        }
    },
    "mappings": {
    
    
        "properties": {
    
    
            "<field>": {
    
    
                "type": "text",
                "analyzer": "<analyzer_name>"   // 指定字段使用的分析器, 必须是现有的或者前面自定义的
            }
        }
    }
}

自定义分析器示例:

PUT /demo-index
{
    
    
    "settings": {
    
    
        "analysis": {
    
    
            "tokenizer": {
    
    
                "my_tokenizer": {
    
    
                    "type": "pattern",      // 正则分词器
                    "pattern": ","          // 用于切分词语的正则表达式 (用","分隔)
                }
            },
            "char_filter": {
    
    
                "my_char_filter": {
    
    
                    "type": "html_strip"
                }
            },
            "filter": {
    
    
                "my_filter": {
    
    
                    "type": "stop",         // 停用词词语过滤器
                    "ignore_case": true,
                    "stopwords": ["the", "is"]
                }
            },
            "analyzer": {
    
    
                "my_analyzer": {
    
    
                    "tokenizer": "my_tokenizer",
                    "char_filter": ["my_char_filter"],
                    "filter": ["my_filter"]
                }
            }
        }
    },
    "mappings": {
    
    
        "properties": {
    
    
            "title": {
    
    
                "type": "text",
                "analyzer": "my_analyzer"   // 使用 settings 中自定义的分析器
            }
        }
    }
}

// 测试索引字段中使用的自定义分析器
POST /demo-index/_analyze
{
    
    
    "field": "title",
    "text": "The,Happy,中国人"
}
// 返回
{
    
    
    "tokens": [
        {
    
    
            "token": "Happy",
            "start_offset": 4,
            "end_offset": 9,
            "type": "word",
            "position": 1
        },
        {
    
    
            "token": "中国人",
            "start_offset": 10,
            "end_offset": 13,
            "type": "word",
            "position": 2
        }
    ]
}

2. 中文分析器

对应英文来说,分词很容易,只需要用空格和标点符号分词即可。但对于中文的分词,ES 内置的分析器就无法胜任,需要使用额外的分词器插件。ES 可以通过安装插件的方式支持第三方分析器,比较常用的第三方中文分析器有 IK 和 HanLP。

IK 分析器是基于词典的分词算法,该算法按照某种策略将提前准备好的词典对待分析文本进行分析,当文本匹配到词典中的某个词时,即有该分词。基于词典的分词算法是分词算法中最简单快速的算法,该算法又可以分为 3 种不同的匹配方式:正向最大化匹配、逆向最大化匹配、双向最大化匹配。

HanLP 分析器是基于统计的机器学习算法,该算法需要先构建一个已标记好分词形式的语料库。然后对分析文本时统计语料库中每个词出现的频率,基于统计结果给出某种语境下应该切分出的分词。

2.1 IK 分析器

IK 分析器是一个开源的,基于 Java 语言开发的轻量级中文分词工具包。IK 分析器提供了 ES 的插件(IK Analysis for Elasticsearch),并支持词典的冷更新和热更新。

IK 分析器 ES 插件相关链接:

2.1.1 安装 IK 插件

安装分析器插件,有两种安装方式。

安装方式一:手动下载安装

  • 从插件下载地址下载与 ES 版本对应的插件版本:elasticsearch-analysis-ik-X.X.X.zip
  • 在 ES 的插件目录plugins下创建一个analysis-ik文件夹:cd <es-root>/plugins/ && mkdir analysis-ik
  • 解压插件到plugins/analysis-ik文件夹:unzip elasticsearch-analysis-ik-X.X.X.zip -d <es-root>/plugins/analysis-ik

插件的正式名称在插件描述文件(plugins/analysis-ik/plugin-descriptor.properties)的name=analysis-ik字段中定义,但 plugins 目录下的插件文件夹名称可以自定义。

安装方式二:使用 ES 插件命令安装

./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/vX.X.X/elasticsearch-analysis-ik-X.X.X.zip

插件命令在线安装支持 file:// 格式的 URL,即可以把插件的 zip 安装包下载到本地再安装。

使用插件命令安装插件,将在 ES 配置文件目录(<es-root>/config/)下创建与插件名同名的文件夹(analysis-ik),然后把插件的配置文件安装到此目录下(<es-root>/config/analysis-ik)。

查看已安装的第三方插件:发送请求 GET /_cat/plugins?v,或者运行命令 ./bin/elasticsearch-plugin list

安装插件后需重启 ES,如果控制台打印出了下面一行日志,表示 IK 分析器插件安装成功:

[2023-08-09T20:00:00,000][INFO] ... loaded plugin [analysis-ik]

加载 IK 插件的配置文件:

ES 启动时将加载 IK 插件,并加载 IK 字典配置,字典配置文件名为 IKAnalyzer.cfg.xml,优先从 <es-root>/config/analysis-ik/IKAnalyzer.cfg.xml 路径加载,如果找不到文件,再从插件目录下的 <es-root>/plugins/analysis-ik/config/IKAnalyzer.cfg.xml 路径加载。

2.1.2 使用 IK 分析器

IK 插件中包含了的 ik_smartik_max_word 两个分析器(analyzer),以及名称相同的两个分词器(tokenizer)。

ik_smart 与 ik_max_word 的区别:

  • ik_smart 将文本做最细粒度的拆分,会穷尽各种可能的组合。
  • ik_max_word 将文本做最粗粒度的拆分,适合用于 phrase 查询。

使用 IK 分析器测试分析文本:

GET /_analyze
{
    
    
    "analyzer": "ik_smart",         // 最粗粒度拆分
    "text": "我是中国人"
}
// 返回
{
    
    
    "tokens": [
        {
    
    
            "token": "我",
            "start_offset": 0,
            "end_offset": 1,
            "type": "CN_CHAR",
            "position": 0
        },
        {
    
    
            "token": "是",
            "start_offset": 1,
            "end_offset": 2,
            "type": "CN_CHAR",
            "position": 1
        },
        {
    
    
            "token": "中国人",
            "start_offset": 2,
            "end_offset": 5,
            "type": "CN_WORD",
            "position": 2
        }
    ]
}

GET /_analyze
{
    
    
    "analyzer": "ik_max_word",      // 最细粒度拆分
    "text": "我是中国人"
}
// 返回
{
    
    
    "tokens": [
        {
    
    
            "token": "我",
            "start_offset": 0,
            "end_offset": 1,
            "type": "CN_CHAR",
            "position": 0
        },
        {
    
    
            "token": "是",
            "start_offset": 1,
            "end_offset": 2,
            "type": "CN_CHAR",
            "position": 1
        },
        {
    
    
            "token": "中国人",
            "start_offset": 2,
            "end_offset": 5,
            "type": "CN_WORD",
            "position": 2
        },
        {
    
    
            "token": "中国",
            "start_offset": 2,
            "end_offset": 4,
            "type": "CN_WORD",
            "position": 3
        },
        {
    
    
            "token": "国人",
            "start_offset": 3,
            "end_offset": 5,
            "type": "CN_WORD",
            "position": 4
        }
    ]
}

2.1.3 IK 扩展词典

IK 分词器是基于词典的分词,如果词典中没有的词则不能被拆分出来:

GET /_analyze
{
    
    
    "analyzer": "ik_max_word",
    "text": "雄安新区"
}
// 返回
{
    
    
    "tokens": [
        {
    
    
            "token": "雄",
            "start_offset": 0,
            "end_offset": 1,
            "type": "CN_CHAR",
            "position": 0
        },
        {
    
    
            "token": "安新",
            "start_offset": 1,
            "end_offset": 3,
            "type": "CN_WORD",
            "position": 1
        },
        {
    
    
            "token": "新区",
            "start_offset": 2,
            "end_offset": 4,
            "type": "CN_WORD",
            "position": 2
        }
    ]
}

拆分「雄安新区」并没有拆分出「雄安」这个分词,因为这个词在默认的词典中并没有,可以手动扩展词典。

IK 词典配置文件在 <es-root>/config/analysis-ik/IKAnalyzer.cfg.xml<es-root>/plugins/analysis-ik/config/IKAnalyzer.cfg.xml 位置。

IKAnalyzer.cfg.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"></entry>
     <!-- 用户可以在这里配置自己的扩展停止词字典 -->
    <entry key="ext_stopwords"></entry>
    <!-- 用户可以在这里配置远程扩展字典 -->
    <!-- <entry key="remote_ext_dict">words_location</entry> -->
    <!-- 用户可以在这里配置远程扩展停止词字典 -->
    <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

词典是一个纯文本文件,每一行为一个词语,在 IKAnalyzer.cfg.xml 配置文件所在目录本地创建一个名为 my.dic 的文件,写入以下内容:

雄安
雄安新区

修改 IKAnalyzer.cfg.xml 配置:

<!-- 用户可以在这里配置自己的扩展字典 -->
<!-- 配置本地字典文件的路径, 路径必须是相对于配置文件所在目录的相对路径, 多个路径使用 ; 分隔, 文件必须是 UTF-8 编码 -->
<entry key="ext_dict">my.dic</entry>

保存配置,重启 ES,控制台打印出如下日志则表示扩展字典加载成功:

[2023-08-09T20:30:00,000][INFO] ... [Dict Loading] .../plugins/analysis-ik/config/my.dic

再测试分词:

GET /_analyze
{
    
    
    "analyzer": "ik_max_word",
    "text": "雄安新区"
}

// 返回
{
    
    
    "tokens": [
        {
    
    
            "token": "雄安新区",
            "start_offset": 0,
            "end_offset": 4,
            "type": "CN_WORD",
            "position": 0
        },
        {
    
    
            "token": "雄安",
            "start_offset": 0,
            "end_offset": 2,
            "type": "CN_WORD",
            "position": 1
        },
        {
    
    
            "token": "安新",
            "start_offset": 1,
            "end_offset": 3,
            "type": "CN_WORD",
            "position": 2
        },
        {
    
    
            "token": "新区",
            "start_offset": 2,
            "end_offset": 4,
            "type": "CN_WORD",
            "position": 3
        }
    ]
}

2.1.4 IK 热更新扩展词典

IK 插件支持热更新词典(无需重启ES),IKAnalyzer.cfg.xml 配置文件中的热更新配置:

<!-- 用户可以在这里配置远程扩展字典 -->
<entry key="remote_ext_dict">words_location</entry>
<!-- 用户可以在这里配置远程扩展停止词字典 -->
<entry key="remote_ext_stopwords">words_location</entry>

其中 words_location 是一个或多个 URL,比如 http://mysite.com/my.dic,可以配置多个 URL,多个 URL 之间使用 ; 分隔。该词典链接请求需要满足以下两点:

  1. 该 HTTP URL 请求返回的是一行一个分词的文本内容。
  2. 该 HTTP 服务器需支持缓存请求(基本上所有的 HTTP Server 都会支持),即响应头需要返回 Last-ModifiedETag

如果配置了远程字典,IK 插件首次加载时会直接发送 GET 请求加载远程字典。之后每隔一分钟发送一次 HEAD 请求,并携带上 If-Modified-SinceIf-None-Match 请求头,如果 HTTP 服务器返回 304 Not Modified 的状态,则不做任何处理继续等下一次请求,如果返回的是 200 OK 状态,则立即再发送 GET 请求获取词典内容进行更新。

加载远程词典时控制台会打印如下日志:

[2023-08-09T20:35:00.000][INFO] ... [Dict Loading] http://mysite.com/my.dic

注意:热更新远程词典,只是更新分析器的词典,对于之前已经建立好索引的文档字段不会重建索引,但新写入的文档和查询文档时的分词会使用更新后的词典,如果旧文档需要使用更新后的词典进行分词则需要重新更新文档重建索引。

2.2 HanLP 分析器

HanLP 是面向生产环境的多语种自然语言处理工具包,HanLP 支持中文分词、词性标注、句法分析等多种语言处理功能。HanLP 也有 Elasticsearch 分析器插件。

HanLP 相关链接:

2.2.1 安装 HanLP 插件

安装 HanLP ES 插件:

# 需使用兼容 ES 版本的版本
./bin/elasticsearch-plugin install https://github.com/KennFalcon/elasticsearch-analysis-hanlp/releases/download/vX.X.X/elasticsearch-analysis-hanlp-X.X.X.zip

安装成功后重启 ES,控制台会输出下面日志:

[2023-08-10T20:30:00,000][INFO] ... loaded plugin [analysis-hanlp]

2.2.2 使用 HanLP 分析器

HanLP ES 插件提供的分析器/分词器:

  • hanlp:HanLP默认分词
  • hanlp_standard:标准分词
  • hanlp_index:索引分词
  • hanlp_nlp:NLP分词
  • hanlp_crf:CRF分词
  • hanlp_n_short:N-最短路分词
  • hanlp_dijkstra:最短路分词
  • hanlp_speed:极速词典分词

使用 HanLP 分析器分析文本:

POST /_analyze
{
    
    
    "analyzer": "hanlp_standard",
    "text": "雄安新区"
}

// 返回
{
    
    
    "tokens": [
        {
    
    
            "token": "雄",
            "start_offset": 0,
            "end_offset": 1,
            "type": "ag",
            "position": 0
        },
        {
    
    
            "token": "安",
            "start_offset": 1,
            "end_offset": 2,
            "type": "ag",
            "position": 1
        },
        {
    
    
            "token": "新区",
            "start_offset": 2,
            "end_offset": 4,
            "type": "n",
            "position": 2
        }
    ]
}

分词结果并没有切分出“雄安”,需要添加到扩展词典。

2.2.3 HanLP 扩展词典

<es_root>/plugins/analysis-hanlp/data/dictionary/custom/ 目录下创建一个 mydict.txt 文件,输入下面内容:

雄安

词典文本中每一行为一个分词。

HanLP 插件的配置文件位置:<es_root>/config/analysis-hanlp/hanlp.properties<es_root>/plugins/analysis-hanlp/config/hanlp.properties

修改 hanlp.properties 配置文件中的 CustomDictionaryPath 字段的值,把扩展词典文件路径添加到末尾:

# 路径默认相对于 analysis-hanlp 插件根目录, 多个路径使用 ; 分隔
CustomDictionaryPath=...;data/dictionary/custom/mydict.txt;

重启 ES,再测试分析器:

POST /_analyze
{
    
    
    "analyzer": "hanlp_standard",
    "text": "雄安新区"
}

// 返回
{
    
    
    "tokens": [
        {
    
    
            "token": "雄安",
            "start_offset": 0,
            "end_offset": 2,
            "type": "n",
            "position": 0
        },
        {
    
    
            "token": "新区",
            "start_offset": 2,
            "end_offset": 4,
            "type": "n",
            "position": 1
        }
    ]
}

2.2.4 HanLP 热更新扩展词典

HanLP 插件的配置热更新词典的配置文件位置:<es_root>/config/analysis-hanlp/hanlp-remote.xml<es_root>/plugins/analysis-hanlp/config/hanlp-remote.xml

hanlp-remote.xml 配置文件的内容:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
    <comment>HanLP Analyzer 扩展配置</comment>
    <!-- 用户可以在这里配置远程扩展字典 -->
    <!-- <entry key="remote_ext_dict">words_location</entry>-->
    <!-- 用户可以在这里配置远程扩展停止词字典 -->
    <!-- <entry key="remote_ext_stopwords">stop_words_location</entry> -->
</properties>

其中 words_location 是一个 URL 或者 URL 词性,如:

  • http://mysite.com/mydic.txt
  • http://mysite.com/mydic.txt nt

具体可参考:HanLP 远程词典配置

NanLP 远程扩展词典的加载规则和 IK 远程扩展词典的加载规则一样,都是每隔一分钟请求 URL 检测是否有更新,有更新则重新加载远程词典。

3. 使用同义词

在搜索引擎中,同义词用于处理不同的查询词搜索相同目标。例如,当用户搜索“Android手机”和“安卓手机”时,其实想搜的是同一个目标。再比如电商搜索中,搜索“凤梨”和“菠萝”、“番茄”和“西红柿”、“电饭锅”和“电饭煲”、“李宁”和“LI-NING”,用户想要搜索的都是相同的商品。

ES 通过分析器中的分词过滤器来实现同义词的搜索,要使用同义词,可以在建立索引时同步建立同义词的索引,或者在搜索时经过同义词分析器同时搜索同义词。

ES 内置的分词过滤器 synonymsynonym_graph 支持同义词过滤。

3.1 创建索引时使用同义词

在建立索引时使用同义词,ES 内置了一个 synonym 同义词分词过滤器。下面使用 IK分析器 和 synonyms分词过滤器 一起创建索引:

PUT /menu
{
    
    
    "settings": {
    
                                   // 索引设置
        "analysis": {
    
    
            "filter": {
    
                             // 过滤器定义
                "my_filter": {
    
                      // 自定义分词过滤器, 名称为 "my_filter"
                    "type": "synonym",          // 使用内置的 同义词过滤器
                    "synonyms": [               // 同义词列表, 同义词之间用 , 分隔
                        "凤梨,菠萝",
                        "番茄,西红柿"
                    ]
                }
            },
            "analyzer": {
    
                           // 分析器定义
                "my_analyzer": {
    
                    // 自定义分析器, 名称为 "my_analyzer"
                    "tokenizer": "ik_max_word", // 分词器, 使用安装的第三方分词器 "ik_max_word"
                    "filter": [                 // 分词过滤器
                        "lowercase",            // 自带的分词过滤器
                        "my_filter"             // 前面自定义的分词过滤器
                    ]
                }
            }
        }
    },
    "mappings": {
    
                                   // 映射
        "properties": {
    
    
            "name": {
    
                               // text类型的字段
                "type": "text",
                "analyzer": "my_analyzer"       // 使用上面自定义的分析器
            }
        }
    }
}

测试索引中自定义的分析器:

GET /menu/_analyze
{
    
    
    "field": "name",
    "text": "西红柿炒蛋"
}

// 返回, 西红柿除了拆分出“西红柿”外, 还拆分出了它的同义词“番茄”
{
    
    
    "tokens": [
        {
    
    
            "token": "西红柿",
            "start_offset": 0,
            "end_offset": 3,
            "type": "CN_WORD",
            "position": 0
        },
        {
    
    
            "token": "番茄",
            "start_offset": 0,
            "end_offset": 3,
            "type": "SYNONYM",
            "position": 0
        },
        {
    
    
            "token": "炒蛋",
            "start_offset": 3,
            "end_offset": 5,
            "type": "CN_WORD",
            "position": 1
        }
    ]
}

写入一些文档:

PUT /menu/_doc/001
{
    
    "name": "番茄炒蛋"}

PUT /menu/_doc/002
{
    
    "name": "凤梨炒西红柿"}

PUT /menu/_doc/003
{
    
    "name": "菠萝猪扒包"}

搜索文档:

GET /menu/_search
{
    
    
    "query": {
    
    
        "match": {
    
    
          "name": "番茄"
        }
    }
}

// 返回
{
    
    
    "took": 436,
    "timed_out": false,
    "_shards": {
    
    
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
    
    
        "total": {
    
    
            "value": 2,
            "relation": "eq"
        },
        "max_score": 0.7520058,
        "hits": [
            {
    
    
                "_index": "menu",
                "_id": "001",
                "_score": 0.7520058,
                "_source": {
    
    
                    "name": "番茄炒蛋"
                }
            },
            {
    
    
                "_index": "menu",
                "_id": "002",
                "_score": 0.6951314,
                "_source": {
    
    
                    "name": "凤梨炒西红柿"
                }
            }
        ]
    }
}

从返回结果可以看出,搜索“番茄”,除了匹配了“番茄炒蛋”,还匹配了“凤梨炒西红柿”。

3.2 搜索时使用同义词

文档建立索引时,或者搜索时使用同义词,都能实现搜索同义词的目的,因此不需要两个地方都使用同义词。文档建立索引时不使用同义词,搜索时再使用同义词搜索,可以减少文档索引的数量,而且之后更新同义词,也不需要重新对文档建立索引。

搜索时再使用同义词过滤器的索引创建:

PUT /menu
{
    
    
    "settings": {
    
                                   // 索引设置
        "analysis": {
    
    
            "filter": {
    
                             // 过滤器定义
                "my_filter": {
    
                      // 自定义分词过滤器, 名称为 "my_filter"
                    "type": "synonym",          // 使用内置的 同义词过滤器
                    "synonyms": [               // 同义词列表, 同义词之间用 , 分隔
                        "凤梨,菠萝",
                        "番茄,西红柿"
                    ]
                }
            },
            "analyzer": {
    
                           // 分析器定义
                "my_analyzer": {
    
                    // 自定义分析器, 名称为 "my_analyzer"
                    "tokenizer": "ik_max_word", // 分词器, 使用安装的第三方分词器 "ik_max_word"
                    "filter": [                 // 分词过滤器
                        "lowercase",            // 自带的分词过滤器
                        "my_filter"             // 前面自定义的分词过滤器
                    ]
                }
            }
        }
    },
    "mappings": {
    
                                   // 映射
        "properties": {
    
    
            "name": {
    
                               // text类型的字段
                "type": "text",
                "analyzer": "ik_max_word",          // 文档建立索引时使用 "ik_max_word" 分析器
                "search_analyzer": "my_analyzer"    // 搜索时使用上面自定义的分析器
            }
        }
    }
}

3.3 使用同义词字典

如果同义词比较多,在 settings 中配置就变的非常繁琐,也不易于更新同义词词典。ES 内置的同义词过滤器,支持把同义词放到本地文本文件中,文件位置必须在 <es_root>/config/ 目录下,并且在每一个集群节点中都需要放置。

<es_root>/config/ 目录下创建一个 mydict 文件夹,在该文件夹下载创建一个 synonyms.txt 文本文件用于存放同义词,每行一组同义词,同义词之间使用,分隔,如下所示:

<es_root>$ cat config/mydict/synonyms.txt
凤梨,菠萝
番茄,西红柿

创建索引时使用 ES 内置的 synonym_graph 同义词过滤器自定义分析器,同义词使用本地词典:

PUT /menu
{
    
    
    "settings": {
    
    
        "analysis": {
    
    
            "filter": {
    
    
                "my_filter": {
    
                                  // 自定义分词过滤器
                    "type": "synonym_graph",                // 使用内置的 同义词过滤器
                    "synonyms_path": "mydict/synonyms.txt"  // 指定同义词词典, 文件路径相对于 `<es_root>/config/` 目录
                }
            },
            "analyzer": {
    
    
                "my_analyzer": {
    
                    // 自定义分析器
                    "tokenizer": "ik_max_word",
                    "filter": [                 // 分词过滤器
                        "lowercase",
                        "my_filter"             // 前面自定义的分词过滤器
                    ]
                }
            }
        }
    },
    "mappings": {
    
                                       // 映射
        "properties": {
    
    
            "name": {
    
                                   // text类型的字段
                "type": "text",
                "analyzer": "ik_max_word",
                "search_analyzer": "my_analyzer"    // 使用上面自定义的分析器
            }
        }
    }
}

4. 使用停用词

停用词(Stop Words)是指在文本分词后的词语中一些没有搜索意义的词语。例如文本为“我喜欢这里的风景”,分词后词语中“我”、
“这里”、“的”这些词语使用频率非常高,并且对于检索文本信息没有独特的意义,因此在构建索引和搜索时可以忽略掉这些停用词,从而提高索引和搜索效率,并且能节省存储空间。

下面网站有英文和中文常用的停用词表:

HTML 标签等特殊字符除了可以使用字符过滤器过滤外,也可以使用停用词过滤器来过滤。

4.1 停用词过滤器

ES 内置的 stop 分词过滤器可用于过滤停用词。

创建索引,自定义分词过滤器,使用停用词:

PUT /demo
{
    
    
    "settings": {
    
    
        "analysis": {
    
    
            "filter": {
    
    
                "my_stop_filter": {
    
             // 自定义分词过滤器
                    "type": "stop",                     // 使用内置的停用词过滤器
                    "stopwords": ["我", "这里", "的"]    // 停用词列表
                }
            },
            "analyzer": {
    
    
                "my_analyzer": {
    
                // 自定义分析器
                    "tokenizer": "ik_max_word",
                    "filter": ["my_stop_filter"]
                }
            }
        }
    },
    "mappings": {
    
    
        "properties": {
    
    
            "title": {
    
    
                "type": "text",
                "analyzer": "my_analyzer"   // 使用自定义的分析器
            }
        }
    }
}

测试索引映射字段的分析器:

POST /demo/_analyze
{
    
    
    "field": "title",
    "text": "我喜欢这里的风景"
}

// 返回, 从返回中可以看出, “我”、“这里”、“的” 被过滤掉了
{
    
    
    "tokens": [
        {
    
    
            "token": "喜欢",
            "start_offset": 1,
            "end_offset": 3,
            "type": "CN_WORD",
            "position": 1
        },
        {
    
    
            "token": "风景",
            "start_offset": 6,
            "end_offset": 8,
            "type": "CN_WORD",
            "position": 4
        }
    ]
}

测试不带 stop 过滤器的分析器:

POST /_analyze
{
    
    
    "analyzer": "ik_max_word",
    "text": "我喜欢这里的风景"
}

// 返回, 没有使用停止词过滤器 “我”、“这里”、“的” 没有被过滤掉
{
    
    
    "tokens": [
        {
    
    
            "token": "我",
            "start_offset": 0,
            "end_offset": 1,
            "type": "CN_CHAR",
            "position": 0
        },
        {
    
    
            "token": "喜欢",
            "start_offset": 1,
            "end_offset": 3,
            "type": "CN_WORD",
            "position": 1
        },
        {
    
    
            "token": "这里",
            "start_offset": 3,
            "end_offset": 5,
            "type": "CN_WORD",
            "position": 2
        },
        {
    
    
            "token": "的",
            "start_offset": 5,
            "end_offset": 6,
            "type": "CN_CHAR",
            "position": 3
        },
        {
    
    
            "token": "风景",
            "start_offset": 6,
            "end_offset": 8,
            "type": "CN_WORD",
            "position": 4
        }
    ]
}

4.2 内置分析器使用停用词

很多内置的分析器都自带了停用词过滤器,只要设置相关参数即可使用。

下面使用内置的 standard 分析器通过设置其 stopwords 停用词参数自定义分析器,然后创建索引:

PUT /demo
{
    
    
    "settings": {
    
    
        "analysis": {
    
    
            "analyzer": {
    
    
                "my_standard": {
    
    
                    "type": "standard",
                    "stopwords": ["the", "a"]
                }
            }
        }
    },
    "mappings": {
    
    
        "properties": {
    
    
            "title": {
    
    
                "type": "text",
                "analyzer": "my_standard"
            }
        }
    }
}

测试索引字段的分析器:

POST /demo/_analyze
{
    
    
    "field": "title",
    "text": "the a beautiful view."
}

// 返回
{
    
    
    "tokens": [
        {
    
    
            "token": "beautiful",
            "start_offset": 6,
            "end_offset": 15,
            "type": "<ALPHANUM>",
            "position": 2
        },
        {
    
    
            "token": "view",
            "start_offset": 16,
            "end_offset": 20,
            "type": "<ALPHANUM>",
            "position": 3
        }
    ]
}

4.3 IK 分析器使用停用词

IK 分析器默认只使用了英文停用词,没有使用中文停用词。在 IK 插件的 IKAnalyzer.cfg.xml 词典配置文件中可以配置扩展停用词词典和远程扩展停用词词典。

IKAnalyzer.cfg.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"></entry>
     <!-- 用户可以在这里配置自己的扩展停止词字典 -->
    <entry key="ext_stopwords"></entry>
    <!-- 用户可以在这里配置远程扩展字典 -->
    <!-- <entry key="remote_ext_dict">words_location</entry> -->
    <!-- 用户可以在这里配置远程扩展停止词字典 -->
    <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

配置停用词词典和配置扩展词典的方式相同,在 IK 词典配置文件IKAnalyzer.cfg.xml所在目录创建一个 my_stopwords.dic 文件,写入一些内容:

$ cat my_stopwords.dic
我
这里
的

修改 IK 词典配置文件,配置本地扩展停用词:

<entry key="remote_ext_dict">my_stopwords.dic</entry>

保存配置,重启 ES,控制台打印出了下面日志表示停用词词典加载成功:

[2023-08-10T20:30:00,000][INFO] ... [Dict Loading] .../plugins/analysis-ik/config/my_stopwords.dic

使用配置了停用词的 IK 分析器分析文本:

POST /_analyze
{
    
    
    "analyzer": "ik_max_word",
    "text": "我喜欢这里的风景"
}

// 返回
{
    
    
    "tokens": [
        {
    
    
            "token": "喜欢",
            "start_offset": 1,
            "end_offset": 3,
            "type": "CN_WORD",
            "position": 0
        },
        {
    
    
            "token": "风景",
            "start_offset": 6,
            "end_offset": 8,
            "type": "CN_WORD",
            "position": 1
        }
    ]
}

IK 分析器支持配置远程扩展停用词词典(停用词热更新),配置方式和配置远程扩展词典一样。

4.4 HanLP 分析器使用停用词

HanLP 插件的分析器默认没有启用停用词:

POST /_analyze
{
    
    
    "analyzer": "hanlp_standard",
    "text": "我喜欢这里的风景"
}

// 返回
{
    
    
    "tokens": [
        {
    
    
            "token": "我",
            "start_offset": 0,
            "end_offset": 1,
            "type": "rr",
            "position": 0
        },
        {
    
    
            "token": "喜欢",
            "start_offset": 1,
            "end_offset": 3,
            "type": "vi",
            "position": 1
        },
        {
    
    
            "token": "这里",
            "start_offset": 3,
            "end_offset": 5,
            "type": "rzs",
            "position": 2
        },
        {
    
    
            "token": "的",
            "start_offset": 5,
            "end_offset": 6,
            "type": "ude1",
            "position": 3
        },
        {
    
    
            "token": "风景",
            "start_offset": 6,
            "end_offset": 8,
            "type": "n",
            "position": 4
        }
    ]
}

启用 HanLP 分析器的停用词,可以在自定义分析器中启用:

PUT /demo
{
    
    
    "settings": {
    
    
        "analysis": {
    
    
            "analyzer": {
    
    
                "my_hanlp": {
    
                               // 自定义分析器
                    "type": "hanlp_standard",           // 使用 HanLP 插件中的分析器
                    "enable_stop_dictionary": true      // 启用停用词词典
                }
            }
        }
    },
    "mappings": {
    
    
        "properties": {
    
    
            "title": {
    
    
                "type": "text",
                "analyzer": "my_hanlp"      // 使用自定义分析器
            }
        }
    }
}

测试索引字段的分析器:

POST /demo/_analyze
{
    
    
    "field": "title",
    "text": "我喜欢这里的风景"
}

// 返回
{
    
    
    "tokens": [
        {
    
    
            "token": "喜欢",
            "start_offset": 1,
            "end_offset": 3,
            "type": "vi",
            "position": 0
        },
        {
    
    
            "token": "风景",
            "start_offset": 6,
            "end_offset": 8,
            "type": "n",
            "position": 1
        }
    ]
}

HanLP 分析器的停用词词典,支持本地扩展和远程扩展,本地扩展在插件配置文件 <es_root>/config/analysis-hanlp/hanlp.properties 中的 CoreStopWordDictionaryPath 字段值中配置,字段默认值为 <es_root>/plugins/analysis-hanlp/data/dictionary/stopwords.txt,也可以直接把扩展停用词添加到该文件的末尾。

HanLP 分析器的远程停用词词典的配置和配置扩展远程词典的方式一样,都是在 <es_root>/config/analysis-hanlp/hanlp-remote.xml 配置文件中配置。

5. 拼音搜索

拼音搜索在中文搜索中很实用,中文一般使用拼音输入法,如果支持拼音搜索,用户只需要输入关键词的拼音或首字母即可搜索出相关结果。

要支持拼音搜索,需要安装 ES 拼音分析器插件:https://github.com/medcl/elasticsearch-analysis-pinyin

5.1 安装拼音插件

analysis-pinyin 插件没有编译打包,需要自己构建项目。编译 ES 拼音插件项目,需要在本地确保已安装 Git 和 Maven 工具。

先克隆 Git 仓库到本地:

git clone https://github.com/medcl/elasticsearch-analysis-pinyin.git

修改项目中 pom.xml 文件中的 elasticsearch.version 字段的值,改为与 ES 相同的版本号:

<project>
    <properties>
        <elasticsearch.version>8.8.2</elasticsearch.version>
    </properties>
</project>

使用 Maven 构建项目,在项目根目录(pom.xml文件所在目录)下执行命令:

mvn install

编译过程中,会先下载依赖包和插件,耐心等待,构建成功后,在 <project_root>/target/releases 目录下将生成一个 elasticsearch-analysis-pinyin-X.X.X.zip 压缩包,这个就是编译打包好的 ES 插件。

安装 ES 插件,直接把插件压缩包解压到 <es_root>/plugins/ 目录下,或者使用插件命令安装:

$ ./bin/elasticsearch-plugin install file:///.../target/releaseselasticsearch-analysis-pinyin-X.X.X.zip

查看已安装的插件:

$ ./bin/elasticsearch-plugin list
analysis-hanlp
analysis-ik
analysis-pinyin

重启 ES,控制台打印出下面日志表示插件加载成功:

[2023-08-10T20:30:00,000][INFO] ... loaded plugin [analysis-hanlp]
[2023-08-10T20:30:00,000][INFO] ... loaded plugin [analysis-ik]
[2023-08-10T20:30:00,000][INFO] ... loaded plugin [analysis-pinyin]

5.2 使用拼音插件

analysis-pinyin 插件包含了:

  • 分析器:pinyin
  • 分词器:pinyin
  • 分词过滤器:pinyin

测试拼音分析器:

POST /_analyze
{
    
    
    "analyzer": "pinyin",
    "text": "美丽的风景"
}

// 返回
{
    
    
    "tokens": [
        {
    
    
            "token": "mei",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 0
        },
        {
    
    
            "token": "mldfj",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 0
        },
        {
    
    
            "token": "li",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 1
        },
        {
    
    
            "token": "de",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 2
        },
        {
    
    
            "token": "feng",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 3
        },
        {
    
    
            "token": "jing",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 4
        }
    ]
}

拼音分析器的分词比较简单,默认只分出关键词中每一个汉字的拼音分词和首字母组成的分词,还有很多可选参数,参考 GitHub 仓库中的介绍

下面使用 IK分词器 和 拼音分词过滤器 一起组成自定义分析器:

PUT /demo
{
    
    
    "settings": {
    
    
        "analysis": {
    
    
            "filter": {
    
    
                "my_pinyin": {
    
                          // 自定义分词过滤器
                    "type": "pinyin",               // 使用 "pinyin" 分词过滤器
                    "keep_first_letter": true,      // 保留首字母
                    "keep_full_pinyin": true,       // 保留全拼
                    "keep_original": true,          // 保留原始输入
                    "lowercase": true               // 小写
                }
            },
            "analyzer": {
    
    
                "my_ik_pinyin": {
    
                       // 自定义分析器
                    "tokenizer": "ik_max_word",     // 使用 "ik_max_word" 分词器
                    "filter": ["my_pinyin"]         // 使用自定义的分析器
                }
            }
        }
    },
    "mappings": {
    
    
        "properties": {
    
    
            "title": {
    
                                  // text 类型的索引字段
                "type": "text",
                "analyzer": "my_ik_pinyin"          // 使用自定义的分析器
            }
        }
    }
}

测试索引中自定义的分析器:

POST /demo/_analyze
{
    
    
    "analyzer": "my_ik_pinyin",
    "text": "美丽的风景"
}

// 返回
{
    
    
    "tokens": [
        {
    
    
            "token": "mei",
            "start_offset": 0,
            "end_offset": 2,
            "type": "CN_WORD",
            "position": 0
        },
        {
    
    
            "token": "li",
            "start_offset": 0,
            "end_offset": 2,
            "type": "CN_WORD",
            "position": 1
        },
        {
    
    
            "token": "美丽",
            "start_offset": 0,
            "end_offset": 2,
            "type": "CN_WORD",
            "position": 1
        },
        {
    
    
            "token": "ml",
            "start_offset": 0,
            "end_offset": 2,
            "type": "CN_WORD",
            "position": 1
        },
        {
    
    
            "token": "feng",
            "start_offset": 3,
            "end_offset": 5,
            "type": "CN_WORD",
            "position": 2
        },
        {
    
    
            "token": "jing",
            "start_offset": 3,
            "end_offset": 5,
            "type": "CN_WORD",
            "position": 3
        },
        {
    
    
            "token": "风景",
            "start_offset": 3,
            "end_offset": 5,
            "type": "CN_WORD",
            "position": 3
        },
        {
    
    
            "token": "fj",
            "start_offset": 3,
            "end_offset": 5,
            "type": "CN_WORD",
            "position": 3
        }
    ]
}

写入一些文档:

PUT /demo/_doc/001
{
    
    "title": "美丽的风景"}

PUT /demo/_doc/002
{
    
    "title": "附近的景区"}

拼音搜索:

POST /demo/_search
{
    
    
    "query": {
    
    
        "match": {
    
    
            "title": "fujin"
        }
    }
}

// 返回
{
    
    
    "took": 1,
    "timed_out": false,
    "_shards": {
    
    
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
    
    
        "total": {
    
    
            "value": 1,
            "relation": "eq"
        },
        "max_score": 1.7427702,
        "hits": [
            {
    
    
                "_index": "demo",
                "_id": "002",
                "_score": 1.7427702,
                "_source": {
    
    
                    "title": "附近的景区"
                }
            }
        ]
    }
}

拼音首字母搜索:

POST /demo/_search
{
    
    
    "query": {
    
    
        "match": {
    
    
            "title": "fj"
        }
    }
}

// 返回
{
    
    
    "took": 1,
    "timed_out": false,
    "_shards": {
    
    
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
    
    
        "total": {
    
    
            "value": 2,
            "relation": "eq"
        },
        "max_score": 0.22920428,
        "hits": [
            {
    
    
                "_index": "demo",
                "_id": "001",
                "_score": 0.22920428,
                "_source": {
    
    
                    "title": "美丽的风景"
                }
            },
            {
    
    
                "_index": "demo",
                "_id": "002",
                "_score": 0.22920428,
                "_source": {
    
    
                    "title": "附近的景区"
                }
            }
        ]
    }
}

6. 高亮显示

搜索文本时对于匹配关键词的部分有时需要高亮显示(highlight),即对匹配的关键词进行颜色或字体等处理。

高亮显示官网介绍:Highlighting

先创建一个索引,并写入一些文本:

PUT /demo
{
    
    
    "mappings": {
    
    
        "properties": {
    
    
            "title": {
    
    
                "type": "text",
                "analyzer": "ik_max_word"
            }
        }
    }
}

PUT /demo/_doc/001
{
    
    "title": "美丽的风景"}

PUT /demo/_doc/002
{
    
    "title": "附近的风景"}

6.1 高亮显示搜索结果

高亮显示通过返回结果中匹配关键词部分加上标签来实现,搜索返回高亮结果示例:

POST /demo/_search          // 搜索
{
    
    
    "query": {
    
    
        "match": {
    
              // 分词查询
          "title": "风景"
        }
    },
    "highlight": {
    
              // 高亮设置
        "fields": {
    
             // 需要高亮的字段
            "title": {
    
          // 高亮 "title" 字段匹配部分
                "pre_tags": "<font color='red'>",       // 高亮词的前标签, 默认为 "<em>"
                "post_tags": "</font>"                  // 高亮词的后标签, 默认为 "</em>"
            }
        }
    }
}

// 返回
{
    
    
    "took": 2,
    "timed_out": false,
    "_shards": {
    
    
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
    
    
        "total": {
    
    
            "value": 2,
            "relation": "eq"
        },
        "max_score": 0.18232156,
        "hits": [
            {
    
    
                "_index": "demo",
                "_id": "001",
                "_score": 0.18232156,
                "_source": {
    
    
                    "title": "美丽的风景"
                },
                "highlight": {
    
    
                    "title": [
                        "美丽的<font color='red'>风景</font>"
                    ]
                }
            },
            {
    
    
                "_index": "demo",
                "_id": "002",
                "_score": 0.18232156,
                "_source": {
    
    
                    "title": "附近的风景"
                },
                "highlight": {
    
    
                    "title": [
                        "附近的<font color='red'>风景</font>"
                    ]
                }
            }
        ]
    }
}

6.2 高亮显示策略

ES 支持三种高亮显示策略,分别为:unifiedplainfvh,搜索时通过指定 highlight.fields.<field>.type 参数指定。

unified 是 ES 默认的高亮显示策略,该策略使用 Lucene Unified Highlighter 实现,并使用 BM25 算法对各个句子进行评分,就像它们是语料库中的文档一样。

plain 策略使用标准的 Lucene highlighter,该策略精准度比较高,它需要把文档全部加载到内存中,并重新执行查询和分析。该策略在处理大量文档的文本高亮显示时需要耗费比较多的资源,因此比较适合只对单字段文档的搜索高亮显示。

fvh (fast vector highlighter) 策略使用 Lucene Fast Vector highlighter,该策略适合文档中包含大文本字段时使用。

搜索时指定高亮策略:

POST /demo/_search
{
    
    
    "query": {
    
    
        "match": {
    
    
          "title": "风景"
        }
    },
    "highlight": {
    
    
        "fields": {
    
    
            "title": {
    
    
                "pre_tags": "<font color='red'>",
                "post_tags": "</font>",
                "type": "plain"         // 指定高亮策略
            }
        }
    }
}

7. 拼写纠错

用户在输入搜索关键词时,可以会输入读音或笔画相似的错别字,搜索包含错别字的关键词时,许多搜索引擎都会自动识别并自动搜索矫正后的正确关键词。

7.1 拼写纠错实现原理

ES 在执行全文搜索(match查询)时,可以指定一个编辑距离参数(fuzziness)。编辑距离是指对一个词语经过多少次编辑后可以得到另一个词语,一次编辑操作包括 替换一个字符、删除一个字符、插入一个字符、交互两个字符位置,每一次操作都是一次编辑距离。例如从「男大道」到「深南大道」,可以把“男”替换为“南”,然后在头部插入一个“深”,就得到了“深南大道”,就表示从「男大道」到「深南大道」的编辑距离是2。

为了实现拼写纠错,可以先创建一个专门用来保存热门搜索词的索引,当使用用户提供的关键词搜索不到内容时,用该关键词去搜索热搜词索引,指定编辑距离(纠正搜索词),然后把搜到的正确的热搜词再去搜索内容。

7.2 使用热搜词纠正搜索词

创建热搜词索引,写入一些文档:

PUT /hotword
{
    
    
    "mappings": {
    
    
        "properties": {
    
    
            "hotword": {
    
                        // text类型的字段
                "type": "text",
                "analyzer": "ik_max_word"
            }
        }
    }
}

PUT /hotword/_doc/001
{
    
    "hotword": "环城大道"}

PUT /hotword/_doc/002
{
    
    "hotword": "林荫小道"}

PUT /hotword/_doc/003
{
    
    "hotword": "滨海立交"}

指定编辑距离为 1,搜索关键词:

POST /hotword/_search
{
    
    
    "query": {
    
    
        "match": {
    
    
            "hotword": {
    
    
                "query": "幻城大道",      // 搜索词, 将分词为 "幻城" 和 "大道"
                "fuzziness": 1          // 最大编辑距离为 1
            }
        }
    }
}

// 返回
{
    
    
    "took": 2,
    "timed_out": false,
    "_shards": {
    
    
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
    
    
        "total": {
    
    
            "value": 2,
            "relation": "eq"
        },
        "max_score": 0.9808291,
        "hits": [
            {
    
    
                "_index": "hotword",
                "_id": "001",
                "_score": 0.9808291,
                "_source": {
    
    
                    "hotword": "环城大道"       // "大道" 匹配 "环城大道"
                }
            },
            {
    
    
                "_index": "hotword",
                "_id": "002",
                "_score": 0.49041456,
                "_source": {
    
    
                    "hotword": "林荫小道"       // "大道" 经过1个编辑距离后得到 "小道", 匹配 "林荫小道"
                }
            }
        ]
    }
}

分词搜索“幻城大道”,将切分出“幻城”和“大道”两个分词,其中“大道”匹配了“环城大道”,“大道”经过 1 个编辑距离得到“小道”,然后“小道”又匹配了“林荫小道”,因此搜出结果包括了“环城大道”和“林荫小道”。经过编辑后的分词匹配分数比原分词匹配分数低,因此“林荫小道”的匹配度分数比“环城大道”要低很多。

当用户使用“幻城大道”搜索文档时,如果返回结果为空或数据量极少,可以尝试用“幻城大道”从热搜词索引中搜索,结果得到分数最高的“环城大道”关键词,然后用“环城大道”关键词再去搜索原文档,这样就达到了一定程度的拼写纠错目的。

拼写纠错的另一种思路:绝大多数用户使用的输入法基本都是拼音输入,因此用户输入的错误关键词很可能拼音是正确的。热搜词索引的关键词字段可以使用拼音分析器分词,然后用错误关键词搜索热搜词索引时会转换为拼音再搜索,用拼音从热搜词索引中搜索出正确的关键词。

猜你喜欢

转载自blog.csdn.net/xietansheng/article/details/132349032