1.中文分词背景

中文分词的难点是,不能简单的按照一个个的字分隔,需要根据不一样的上下文,切分,不像英文有空格做分隔。

例如:中华人民共和国国歌

先只有默认的分词器看下效果

GET _analyze
{
  "analyzer": "standard",
  "text": "中华人民共和国国歌"
}

从下图中可以看出,完全是按照一个汉字,一个个的分词的,效果差强人意

es安装后如何安装ik分词器 es中文分词器ik_elasticsearch

2.安装IK中文分词器

ik分词是一款流行的elasticsearch 中文分词器,安装ik分词器版本一定要与所安装es版本一致。

官方Github地址:传送门

如果自己下载源码的话,还需要编译,可以直接下载Release版本。
分词器插件下载地址

**

**注意:这里下载的时候要对照好ES的版本。

es安装后如何安装ik分词器 es中文分词器ik_分词器_02

下载分词器

#在ES插件存储目录下创建 analysis-ik文件夹
mkdir -p /usr/local/elasticsearch-7.4.0/plugins/analysis-ik

#切换到插件目录下
cd /usr/local/elasticsearch-7.4.0/plugins/analysis-ik

#下载分词器
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.0/elasticsearch-analysis-ik-7.4.0.zip

unzip elasticsearch-analysis-ik-7.4.0.zip

#删除压缩包
rm -rf elasticsearch-analysis-ik-7.4.0.zip

然后重启elasticsearch

执行如下命令可以查看按照好的

GET _cat/plugins

es安装后如何安装ik分词器 es中文分词器ik_es安装后如何安装ik分词器_03

安装步骤总结:

  • 解压ik分词器安装包到plugin目录下,并删除压缩包。
  • 重启elasticsearch进程即可。

注意:es集群的话,每台es都需要安装ik分词器。

3.3.使用IK中文分词器

IK中文分词器,有ik_smart 和 ik_max_word

ik_max_word 和 ik_smart 什么区别?
ik_max_word: 将文本做最细粒度的拆分,会穷尽各种可能的组合,适合 Term Query;
ik_smart: 会做最粗粒度的拆分,适合 Phrase 查询。

3.3.1ik_max_word
GET _analyze
{
  "analyzer": "ik_max_word",
  "text": "中华人民共和国国歌"
}

分词结果

[ 中华人民共和国,中华人民,中华,华人,人民共和国,人民,共和国,共和,国,国歌 ]

es安装后如何安装ik分词器 es中文分词器ik_es安装后如何安装ik分词器_04

3.3.2ik_smart
GET _analyze
{
  "analyzer": "ik_smart",
  "text": "中华人民共和国国歌"
}

分词结果

[ 中华人民共和国,国歌 ]

es安装后如何安装ik分词器 es中文分词器ik_中文分词器_05

3.4.自定义字典

(1)切换到分词器插件目录下

cd /usr/local/elasticsearch-7.2.0/plugins/analysis-ik/config

(2)创建自定义文件存储目录

mkdir custom

(3)创建自定义字典文件

vi custom/custom_word.dic

比如填入自定义的字典词语

我是张三
我是李四

注:请确保扩展词典的文本格式为 UTF8 编码

(3)配置自定义字典读取路径
打开配置

vi IKAnalyzer.cfg.xml

es安装后如何安装ik分词器 es中文分词器ik_ik_06

注:多个配置,可以使用";"分号间隔

es安装后如何安装ik分词器 es中文分词器ik_分词器_07

(4)重启ElasticSearch

(5)测试效果

示例内容:我是张三的老师

es安装后如何安装ik分词器 es中文分词器ik_ik_08

3.5.热更新自定义字典

支持热更新 IK 分词。需要在 IKAnalyzer.cfg.xml配置中添加远程字典访问地址。

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

可以将需自动更新的热词放在一个UTF-8 编码的 .txt 文件里.

  • 放在 nginx,或其他简易 http server

通过http server的地址替换上述配置中的words_location 参数,

words_location 是一个URL地址。配置如下图。多个地址用分号分隔。

注: 单个文件不要过大,建议不超2M ,可以配置多个。

es安装后如何安装ik分词器 es中文分词器ik_中文分词器_09

可以另外做一个工具来从业务系统提取相关词汇,并更新这个 .txt 文件。

该请求需要满足的条件

  • 该 http 请求需要返回两个头部(header),一个是 Last-Modified,一个是 ETag,这两者都是字符串类型,只要有一个发生变化,该插件就会去抓取新的分词进而更新词库。
  • 该 http 请求返回的内容格式是一行一个分词,换行符用 \n 即可

综上,满足上面两点要求就可以实现热更新分词了,不需要重启 ES 实例。

热key服务简单实现

/**
 * IK自定义分词器,热点字典-存储目录
 * /Users/mengqiang/fsdownload
 * /home/devjava/ik_custom_dir
 */
private static final String EXT_DICT_PATH_PREFIX = "/home/devjava/ik_custom_dir";

/**
 * IK自定义分词器,热点字典读取
 *
 * @param response
 */
@RequestMapping(value = "/getCustomDict/{fileName}")
public void getCustomDict(@PathVariable("fileName") String fileName, HttpServletResponse response) {
    FileInputStream inputStream = null;
    OutputStream outputStream = null;
    BufferedReader bufferedReader = null;
    try {
        //文件全路径
        String fileFullPath = this.getFileFullPath(fileName);
        //读取字典文件
        File file = new File(fileFullPath);
        StringBuilder content = new StringBuilder();
        int time = 0;
        if (file.exists()) {
            inputStream = new FileInputStream(file);
            bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
            String line = null;
            while ((line = bufferedReader.readLine()) != null) {
                //需要加换行符
                content.append(line).append("\n");
            }
            bufferedReader.close();
        }
        //内容字节长度
        int contentLength = content.toString().getBytes().length;
        // 返回数据
        outputStream = response.getOutputStream();
        response.setContentLength(contentLength);
        response.setHeader("Last-Modified", String.valueOf(contentLength));
        response.setHeader("ETag", String.valueOf(contentLength));
        response.setContentType("text/plain; charset=utf-8");
        outputStream.write(content.toString().getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        String requestIp = WebToolUtils.getIpAddress(request);
        log.info("[{}] >> [ getCustomDict ] fileFullPath:{}, time:{}, length:{}", requestIp, fileFullPath, time, contentLength);
    } catch (Exception e) {
        log.error("[ getCustomDict ]  exception >> ", e);
    } finally {
        //关闭资源
        this.closeStream(inputStream, outputStream, bufferedReader);
    }
}

/**
 * 文件全路径
 *
 * @param fileName
 */
private String getFileFullPath(String fileName) {
    if (StringUtils.isEmpty(fileName)) {
        return "";
    }
    if (fileName.contains(".")) {
        return EXT_DICT_PATH_PREFIX + "/" + fileName;
    }
    //若非.txt结尾
    if (!fileName.endsWith(".txt")) {
        return EXT_DICT_PATH_PREFIX + "/" + fileName.concat(".txt");
    }
    return EXT_DICT_PATH_PREFIX + "/" + fileName;
}

/**
 * 防止异常后未关闭流
 *
 * @param inputStream
 * @param outputStream
 * @param bufferedReader
 */
private void closeStream(FileInputStream inputStream, OutputStream outputStream, BufferedReader bufferedReader) {
    try {
        if (null != inputStream) {
            inputStream.close();
        }
        if (null != outputStream) {
            outputStream.close();
        }
        if (null != bufferedReader) {
            bufferedReader.close();
        }
    } catch (Exception e) {
        log.error("[ getCustomDict ]  close stream exception >> ", e);
    }
}