Elasticsearch中文分词器

Elasticsearch中文分词器的介绍、对比和用法。

一、背景

1. 必须要理解清楚的术语

  lucene的分析器(analyzer)以及它的三个组成成分
  字符过滤器(character filters)分词器(tokenizers)过滤器(token filters)
  ① analyzer:由字符过滤器、分词器跟过滤器组成,他的功能就是:将分词器跟分析器进行合理的组合,使之产生对文本分词和过滤效果。因此,分析器使用分词和过滤器构成一个管道,文本在“滤过”这个管道之后,就成为可以进入索引的最小单位。
  ② character filters:预处理,共有三种。
    mapping char filter 通过给定的mappings(mappings数组或者读取外部文件)进行数据的替换;
    html strip char filter 把数据中的html标签元素剥离出来,例如 <a> 变成 a
    pattern replace char filter 用正则表达式的方式来替换数据。
  ③ tokenizers:主要用于对文本资源进行切分,将文本规则切分为一个个可以进入索引的最小单元
  ④ token filters:主要对分词器切分的最小单位进入索引进行预处理,如:大写转小写,复数转单数,也可以复杂(根据语义改写拼写错误的单词)

2. analyzer的内部机制

  前面有篇文章整理过了,点击查看

3. 各种分词器的对比与使用方法

  网上有篇文章整理的很好,这里就不重复整理了,点击查看

二、安装

1. 环境

  ● Ubuntu 14.04/16、04
  ● JDK1.8
  ● Elasticsearch 5.3
  ● Kibana 5.3.2

2. 步骤

  我这里使用 ik-analyzer + pinyin 分词器。
  ik-analyzerhttps://github.com/medcl/elasticsearch-analysis-ik
  pinyin分词器https://github.com/medcl/elasticsearch-analysis-pinyin
  繁简切换分词器https://github.com/medcl/elasticsearch-analysis-stconvert
  安装方式都是选择相应版本的插件后,下载到es安装目录的 plugins/ 下,解压缩。
  解压缩前最好在 plugins/ 目录下新建两个目录,分别为 ikpinyin,因为解压完的文件比较多,这样便于区分。
  最后,重启es。

三、简单测试

1. 测试ik分词器

  使用ik_smart分词器,会做最粗粒度的拆分;已被分出的词语将不会再次被其它词语占有。

GET _analyze
{
  "analyzer":"ik_smart",
  "text":"艾泽拉斯国家地理"
}

  使用ik_max_word分词器,会将文本做最细粒度的拆分;尽可能多的拆分出词语。

GET _analyze
{
  "analyzer":"ik_max_word",
  "text":"艾泽拉斯国家地理"
}

2. 测试拼音分词器

  就是普通的把汉字转换成拼音,提取汉字的拼音首字母。

GET _analyze
{
  "analyzer":"pinyin",
  "text":"艾泽拉斯国家地理"
}

3. 测试简/繁体分词器

  默认是简体转换成繁体

GET _analyze
{
  "analyzer":"stconvert",
  "text":"艾泽拉斯国家地理"
}

四、实战

  我在实际业务中,用过下列几种方案。

1. 方案1:自定义分析器

  需求
  ① 设定一个分词器,集成中文、英文、拼音
  ② 原理:先进行中文分词、再进行拼音分词
  ③ 弊端:同音词问题
  索引设计
  创建一个索引,并设置index分析器相关属性:
  分析器名称为 nb_analyzer

PUT /single-analyzer-demo
{
  "settings": {
    "refresh_interval": "5s",
    "number_of_shards": 1,
    "number_of_replicas": 1,
    "analysis": {
      "analyzer": {
        "nb_analyzer": {
          "type": "custom",
          "tokenizer": "ik_smart",
          "filter": [
            "lowercase",
            "stemmer",
            "pinyin_filter"
          ],
          "char_filter": [
            "html_strip"
          ]
        }
      },
      "filter": {
        "pinyin_filter": {
          "type": "pinyin",
          "keep_first_letter": false,
          "keep_separate_first_letter": false,
          "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_none_chinese": true,
          "keep_none_chinese_together": true,
          "keep_none_chinese_in_first_letter": true,
          "keep_none_chinese_in_joined_full_pinyin": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  },
  "mappings": {
    "demo_type": {
      "properties": {
        "name": {
          "type": "text",
          "analyzer": "nb_analyzer"
        },
        "introduction": {
          "type": "text",
          "analyzer": "nb_analyzer"
        },
        "details": {
          "type": "text",
          "analyzer": "nb_analyzer"
        }
      }
    }
  }
}

  测试分词效果
  这一步比较重要,可以先看看分词效果是不是自己想要的。

GET /single-analyzer-demo/_analyze
{
  "text": ["安全气囊产气药you are a dogs"],
  "analyzer": "nb_analyzer"
}

  插入两条数据
  插入两条,稍后查询时候通过加权来控制评分

POST single-analyzer-demo/demo_type
{
  "name": "dog",
  "introduction": "hello",
  "details": "name is dog"
}
POST single-analyzer-demo/demo_type
{
  "name": "hello",
  "introduction": "dog",
  "details": "introdiction is dog"
}

  查询语法
  通过改变权重,查看查询到的评分区别。

GET single-analyzer-demo/_search
{
  "highlight": {
    "fields": {
      "name": {
        "fragment_size": 50
      },
      "introduction": {
        "fragment_size": 50
      },
      "details": {
        "fragment_size": 150
      }
    },
    "post_tags": [
      "</em>"
    ],
    "order": "score",
    "pre_tags": [
      "<em>"
    ]
  },
  "query": {
    "query_string": {
      "query": "dog",
      "fields": [
        "name^2",
        "introduction^3",
        "details"
      ],
      "minimum_should_match": "75%"
    }
  },
  "_source": [
    "name",
    "introduction",
    "details"
  ]
}

2. 方案2:切分到不同的域

  这种方案适用于一份文本有各种国家的翻译,然后把每份翻译存储在不同的域中,根据域的语言决定使用相应的分析器。
  这里不多介绍,需要了解的话见官网

3. 方案3:使用multi_field为搜索字段建立不同类型的索引

  参考
  参考了一下这篇文章
  需求
  ① 中文搜索、英文搜索、中英混搜 如:“南京东路”,“cafe 南京东路店”
  ② 全拼搜索、首字母搜索、中文+全拼、中文+首字母混搜 如:“nanjingdonglu”,“njdl”,“南京donglu”,“南京dl”,“nang南东路”,“njd路”等等组合
  ③ 简繁搜索、特殊符号过滤搜索 如:“龍馬”可通过“龙马”搜索,再比如 L.G.F可以通过lgf搜索,café可能通过cafe搜索
  ④ 排序优先级为: 以关键字开头>包含关键字(没做
  索引设计
  通过在主域下使用多个子域,每个子域使用各自的分析器。(待处理的语言有限的情况用)
  使用 most_fields query type(多字段搜索语法) 来让我们可以用多个字段来匹配同一段文本。
  使用 multi_field 为搜索字段建立不同类型的索引,有全拼索引、首字母简写索引、Ngram索引以及IK索引,从各个角度分别击破,然后通过 char-filter 进行特殊符号与简繁转换。
  注意:
  char_filter 部分:通过给定的mappings数据来替换。直接给mappings数据 或者 将mappings数据写到配置文件,给出配置文件的路径。默认在config/mappings.txt测试时,删掉下面mappings_path那一行。

PUT /multi-analyzer-demo
{
  "settings": {
    "refresh_interval": "5s",
    "number_of_shards": 1,
    "number_of_replicas": 1,
    "analysis": {
      "char_filter": {
        "special_char_convert": {
          "type": "mapping",
          "mappings_path": "char_filter_mappings.txt",
          "mappings": [
            "à=>a",
            "á=>a"
          ]
        },
        "t2s_char_convert": {
          "type": "stconvert",
          "convert_type": "t2s"
        }
      },
      "filter": {
        "pinyin_filter": {
          "type": "pinyin",
          "keep_first_letter": false,
          "keep_separate_first_letter": false,
          "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_none_chinese": true,
          "keep_none_chinese_together": true,
          "keep_none_chinese_in_first_letter": true,
          "keep_none_chinese_in_joined_full_pinyin": true,
          "none_chinese_pinyin_tokenize": false,
          "lowercase": true
        }
      },
      "analyzer": {
        "elastic_ik_analyzer": {
          "type": "custom",
          "char_filter": [
            "html_strip",
            "t2s_char_convert",
            "special_char_convert"
          ],
          "tokenizer": "ik_smart",
          "filter": [
            "lowercase",
            "stemmer"
          ]
        },
        "elastic_pinyin_analyzer": {
          "type": "custom",
          "char_filter": [
            "html_strip",
            "t2s_char_convert",
            "special_char_convert"
          ],
          "tokenizer": "ik_smart",
          "filter": [
            "lowercase",
            "pinyin_filter"
          ]
        }
      }
    }
  },
  "mappings": {
    "demo_type": {
      "properties": {
        "name": {
          "type": "text",
          "fields": {
            "ik": {
              "type": "text",
              "analyzer": "elastic_ik_analyzer"
            },
            "pinyin": {
              "type": "text",
              "analyzer": "elastic_pinyin_analyzer"
            }
          }
        },
        "introduction": {
          "type": "text",
          "fields": {
            "ik": {
              "type": "text",
              "analyzer": "elastic_ik_analyzer"
            },
            "pinyin": {
              "type": "text",
              "analyzer": "elastic_pinyin_analyzer"
            }
          }
        },
        "details": {
          "type": "text",
          "fields": {
            "ik": {
              "type": "text",
              "analyzer": "elastic_ik_analyzer"
            },
            "pinyin": {
              "type": "text",
              "analyzer": "elastic_pinyin_analyzer"
            }
          }
        }
      }
    }
  }
}

  检查一下分词效果
  中文、繁体和英文

GET /multi-analyzer-demo/_analyze
{
  "text": ["我爱祖国"],
  "analyzer": "elastic_ik_analyzer"
}

  拼音

GET /multi-analyzer-demo/_analyze
{
  "text": ["我爱祖国"],
  "analyzer": "elastic_pinyin_analyzer"
}

  插入数据
  插入三条,稍后查询时候通过加权来控制评分,注意同音词问题。

POST multi-analyzer-demo/demo_type
{
  "name": "张三",
  "introduction": "一个前端工程师",
  "details": "名字是张三"
}
POST multi-analyzer-demo/demo_type
{
  "name": "章三",
  "introduction": "一个后端工程师",
  "details": "名字是章三"
}
POST multi-analyzer-demo/demo_type
{
  "name": "神秘人",
  "introduction": "一个叫张三的全栈工程师",
  "details": "名字在introduction里面"
}

  查询语法
  可以给个别字段加权(查询时每个字段默认的权重是1)
  设置最少应当匹配数来减少低质量的匹配。

GET multi-analyzer-demo/_search
{
  "highlight": {
    "fields": {
      "name.ik": {
        "fragment_size": 50
      },
      "name.pinyin": {
        "fragment_size": 50
      },
      "introduction.ik": {
        "fragment_size": 50
      },
      "introduction.pinyin": {
        "fragment_size": 50
      },
      "details.ik": {
        "fragment_size": 150
      },
      "details.pinyin": {
        "fragment_size": 150
      }
    },
    "post_tags": [
      "</em>"
    ],
    "order": "score",
    "pre_tags": [
      "<em>"
    ]
  },
  "query": {
    "query_string": {
      "query": "zhang",
      "fields": [
        "name.ik",
        "name.pinyin",
        "introduction.ik",
        "introduction.pinyin",
        "details.ik",
        "details.pinyin"
      ],
      "minimum_should_match": "75%"
    }
  },
  "_source": [
    "name",
    "introduction",
    "details"
  ],
  "from": 0,
  "size": 20
}

  考虑优化
  query fields里面是否要进一步加权控制,可以自行尝试下。

五、补充:ik添加自定义词库

1. 创建自己的词库

  首先在ik插件的 config/custom 目录下创建一个文件 my.dic (名字任意,以.dic结尾)。
  在文件中添加词语即可,每一个词语一行。
  注意: 这个文件可以在 linux 中直接 vim 生成, 或者在 windows 中创建之后上传到这里。
  如果是在 linux 中直接 vim 生成的, 可以直接使用。
  如果是在 windows中创建的,需要注意文件的编码必须是 UTF-8 without BOM 格式

2. 修改ik的配置文件

  默认情况下 ik 的配置文件就在 ik 插件的 config 目录下面,名字为 IKAnalyzer.cfg.xml
  把刚才创建的文件所在位置添加到 ik 的配置文件中即可。
  vim config/IKAnalyzer.cfg.xml
  即需要把 my.dic 文件的位置添加到 key=ext_dict 这个 entry 中。
  注意:下面第6行的 ;custom/my.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">custom/mydict.dic;custom/single_word_low_freq.dic;custom/my.dic</entry>
         <!--用户可以在这里配置自己的扩展停止词字典-->
        <entry key="ext_stopwords">custom/ext_stopword.dic</entry>
        <!--用户可以在这里配置远程扩展字典 -->
        <!-- <entry key="remote_ext_dict">words_location</entry> -->
        <!--用户可以在这里配置远程扩展停止词字典-->
        <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

六、补充:ik配置远程扩展词库

1. 好处

  可以使用其他程序调用更新,且不用重启 ES,很方便。

2. 配置

  即需要把 请求地址 添加到 key=remote_ext_dict 这个 entry 中。
  注意:下面第10行的 http://192.168.1.136/hotWords.php 是我新增的:

<?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">custom/mydict.dic;custom/single_word_low_freq.dic</entry>
         <!--用户可以在这里配置自己的扩展停止词字典-->
        <entry key="ext_stopwords">custom/ext_stopword.dic</entry>
        <!--用户可以在这里配置远程扩展字典 -->
               <!-- <entry key="remote_ext_dict">http://192.168.1.136/hotWords.php</entry> -->
        <!--用户可以在这里配置远程扩展停止词字典-->
        <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

3. 远程扩展字典

  远程词典,那么就要是一个可访问的链接,可以是一个页面,也可以是一个txt的文档,但要保证输出的内容是 utf-8 的格式。
  ik 接收两个返回的头部属性 Last-ModifiedETag,只要其中一个有变化,就会触发更新,ik 会每分钟获取一次。
  hotWords.php 的内容:

$s = <<<'EOF'
陈港生
元楼
蓝瘦
EOF;
header('Last-Modified: '.gmdate('D, d M Y H:i:s', time()).' GMT', true, 200);
header('ETag: "5816f349-19"');
echo $s;

七、总结

  以上就是我使用ES中文分词器的笔记,如有问题或建议,可以留言。


  目录