Elasticsearch文本分析器

1. 前言

Elasticsearch数据搜索和关系型数据库的SQL查询最显著的区别就是:除了精准匹配和模糊查询,Elasticsearch还具备全文检索的能力,而全文检索的核心是文本分析。

文本分析会将长文本内容进行字符过滤和细粒度的分词,先将长文本里面一些无关紧要的字符给过滤掉,再按照语义将长文本切分为一个个单独的词,然后可能还会根据切分后的词再添加一些同义词,最终构建倒排索引用于全文检索 。

文本分析使得Elasticsearch能够执行全文检索召回相关的文档,而不仅仅是是精准匹配。如下示例,当用户搜索"苹果"二字,召回下面三个文档在ES看来都是合理的:

json 复制代码
{
  "_id":"1",
  "text":"什么品种的苹果更好吃呢?"
}
{
  "_id":"2",
  "text":"Apple Store上海静安店今日开业!"
}
{
  "_id":"3",
  "text":"番茄是水果还是蔬菜?"
}

1号文档召回是因为直接有"苹果"二字、2号文档召回可以理解为"Apple"和"苹果"是同义词、3号文档召回有点意外,如果分词的粒度很细就会导致"果"字被切分为一个单独的词,从而和搜索词匹配成功。

全文检索时,如果Elasticsearch没有召回预期的文档,就应该检查一下文本分析器的配置是否合理。

2. 分析器的构成

Elasticsearch规定,文本分析器由三个基本组件构成,分别是:

  • 0个或多个字符过滤器 character filters
  • 一个分词器 tokenizer
  • 0个或多个分词过滤器 token filters

2.1 字符过滤器

字符过滤器接收作为字符流的原始文本,并可以通过添加、删除或更改字符来转换该流。简单来说,就是字符过滤器可以在原始文本的基础上,做一些字符过滤、删除、替换等操作,在分词前做一些预处理的工作。

例如,字符过滤器可以把"&"符号替换成"and"、也可以去除HTML中的标签,保留文本内容。

Elasticsearch内置了三种字符过滤器,下面分别介绍。

html_strip 用于过滤HTML元素标签,如下示例,结果自动过滤掉了

标签

json 复制代码
POST _analyze
{
  "char_filter": [
    {
      "type": "html_strip"
    }
  ],
  "text": "<p><b>听我说</b>谢谢你,因为有你</p>"
}

{
  "tokens": [
    {
      "token": """
听我说谢谢你,因为有你
""",
      "start_offset": 0,
      "end_offset": 25,
      "type": "word",
      "position": 0
    }
  ]
}

mapping 用于替换字符,给定一个mappings数组,Elasticsearch扫描原始文本时每当遇到相同的Key就会替换成指定的Value。如下示例:

json 复制代码
POST _analyze
{
  "char_filter": [
    {
      "type": "mapping",
      "mappings": [
        "& => 和",
        ":) => 开心",
        ":( => 悲伤"
      ]
    }
  ],
  "text": "我&你独自:),独自:("
}

{
  "tokens": [
    {
      "token": "我和你独自开心,独自悲伤",
      "start_offset": 0,
      "end_offset": 12,
      "type": "word",
      "position": 0
    }
  ]
}

pattern_replace 使用正则表达式来匹配和替换字符,比如可以给重要信息如身份证号脱敏:

json 复制代码
POST _analyze
{
  "char_filter": [
    {
      "type": "pattern_replace",
      "pattern":"(\\d{6})\\d{8}(\\d{4})",
      "replacement":"$1******$2"
    }
  ],
  "text": "The ID number is:362330199001012345"
}

{
  "tokens": [
    {
      "token": "The ID number is:362330******2345",
      "start_offset": 0,
      "end_offset": 35,
      "type": "word",
      "position": 0
    }
  ]
}

2.2 分词器

分词器接收一个被字符过滤器预处理后的字符流作为输入,它是文本分析最重要的一个环节,直接决定了一段长文本会按照怎样的算法来切分成一个个分词。

除了切分文本,分词器还会保留以下信息:

  • 每个分词的相对位置,用于短语搜索和单词临近搜索
  • 字符偏移量,记录分词在原始文本中的出现的位置
  • 分词类型,记录分词的种类,是单词还是数字等

Elasticsearch 内置了大量的面向单词的分词器

以 standard 为例,它也是默认的分词器。它会按照Unicode文本分割算法定义的词边界将文本划分为术语,并且删除了大多数标点符号。对于英文,它会按照空格来分词;对于中文,它会按照一个个汉字来分词。

如下示例,对于英文分词效果还行,针对中文效果就不好了,切分成一个个汉字丢失了词意。

json 复制代码
POST _analyze
{
  "tokenizer":"standard",
  "text": "I am from China.我爱中国"
}

分词结果
["I","am","from","China","我","爱","中","国"]

Elasticsearch 还内置了一些比较特殊的分词器:N-Gram Tokenizer,也叫 N元语法分词器。它会将单词分解成更小的片段,用于部分单词匹配,缺点是分词数量太多,占用更多的存储空间。

json 复制代码
POST _analyze
{
  "tokenizer":{
    "type":"ngram",
    "min_gram":2, // 分词最小长度
    "max_gram":3, // 分词最大长度
    "token_chars":["letter"] // 只切分字母
  },
  "text": "Elasticsearch 666"
}

分词结果
["se","sea","ea","ear","ar","arc","rc","rch","ch"]

此外,Elasticsearch还内置了一些用于结构化文本的分词器,比如:keyword 是一个什么也不做的分词器,它的分词结果就是原始文本。

char_group 分词器会根据给定的字符集对原始文本做切分,它比正则表达式的效率要高,适用于分词规则简单的场景。

json 复制代码
POST _analyze
{
  "tokenizer":{
    "type":"char_group",
    "tokenize_on_chars":["_"]
  },
  "text": "I_am_from_China"
}

分词结果
["I","am","from","China"]

再比如,path_hierarchy 会按照路径分隔符做分词,并输出每个子路径的分词结果。如下示例:

json 复制代码
POST _analyze
{
  "tokenizer":{
    "type":"path_hierarchy"
  },
  "text": "/users/admin/a.txt"
}

分词结果
["/users","/users/admin","/users/admin/a.txt"]

2.3 分词过滤器

分词过滤器接收来自分词器切分后的文本作为输入,并做最后处理。包括:改写词(转换大小写)、删除停用词、添加同义词等操作。

截止Elasticsearch8.13版本,官方内置了几十种分词过滤器,这里只介绍几个,其它参考官方文档。

apostrophe 分词过滤器会删除撇号后面的所有字符,包括撇号本身,适用于土耳其语。如下示例:

json 复制代码
POST _analyze
{
  "tokenizer":"standard",
  "filter":["apostrophe"],
  "text": "I'm 18 years old now"
}

分词结果
["I","18","years","old","now"]

reverse 分词过滤器会反转切分后的每个词,这对于后缀检索非常有用。如下示例:

json 复制代码
POST _analyze
{
  "tokenizer":"standard",
  "filter":["reverse"],
  "text": "hello world"
}

分词结果
["olleh","dlrow"]

stemmer 分词过滤器会根据分词的单词去掉复数、时态等,统一转换成对应的原型,这在英文搜索时非常有用。如下示例:

json 复制代码
POST _analyze
{
  "tokenizer":"standard",
  "filter":["stemmer"],
  "text": "they flying peoples"
}

分词结果
["thei","fly","peopl"]

stop 分词过滤器器会删除分词中无意义的停用词,比如冠词、介词等,还可以自定义停用词黑名单。

如下示例,过滤后只保留了"apple"

json 复制代码
POST _analyze
{
  "tokenizer":"standard",
  "filter":["stop"],
  "text": "this is an apple"
}

分词结果
["apple"]

3. 自定义分析器

Elasticsearch规定,任何分析器都由0个或多个字符过滤器、1个分词器、0个或多个分词过滤器组成,官方内置了大量的基础组件,同时又基于这些基础组件定义了一堆内置的分析器。如果这些内置分析器不能够满足我们的需求,我们也可以任意搭配组合定制化一个分析器。

假设,我们现在创建一个questions索引,用来索引问题,然后可以根据关键词搜索问题。question字段使用text类型,同时使用我们自定义的文本分析器my_analyzer。

假设自定义的文本分析器需要满足下列需求:

  • 自动过滤掉文本中的标点符号
  • 简单点,针对"/"符号来做分词吧
  • 自动过滤掉一些无意义的停用词,例如:"的"、"啊"
  • 使用同义词也能搜索到文档,例如搜索"Android"可以召回含"安卓"的文档

基于以上需求,我们可以先从字符过滤器、分词器、分词过滤器的顺序来慢慢调试我们的分析器。这里可以用到Elasticsearch提供的「_analyze」端点,它可以很方便的用来测试分析器的效果。

过滤标点符号可以用"mapping"实现,分词就用"char_group",删除停用词用"stop",最后同义词可以用"synonym"实现,最终自定义分析器的配置如下:

json 复制代码
POST _analyze
{
  "char_filter": [
    {
      "type": "mapping",
      "mappings": [
        ", => ",
        "。 => ",
        "? => "
      ]
    }
  ],
  "tokenizer": {
    "type": "char_group",
    "tokenize_on_chars": ["/"]
  },
  "filter":[
    {
      "type":"stop",
      "stopwords":["的","呢","和"]
    },
    {
      "type":"synonym",
      "synonyms":[
        "安卓 => 安卓,Android",
        "鸿蒙 => 鸿蒙,HarmonyOS,OpenHarmony"
      ]
    }
  ],
  "text": "鸿蒙/系统/和/安卓/系统/有/什么/区别/呢/?"
}

测试一下,分词结果如下所示,符合我们的需求:

json 复制代码
["鸿蒙","HarmonyOS","OpenHarmony","系统","安卓","Android","系统","有","什么","区别"]

接下来就是创建索引时,指定我们自定义的分析器即可:

json 复制代码
PUT questions
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_analyzer": {
          "char_filter": [
            "my_char_filter"
          ],
          "tokenizer": "my_char_group",
          "filter": [
            "stop_filter",
            "synonym_filter"
          ]
        }
      },
      "char_filter": {
        "my_char_filter": {
          "type": "mapping",
          "mappings": [
            ", => ",
            "。 => ",
            "? => "
          ]
        }
      },
      "tokenizer": {
        "my_char_group": {
          "type": "char_group",
          "tokenize_on_chars": [
            "/"
          ]
        }
      },
      "filter": {
        "stop_filter": {
          "type": "stop",
          "stopwords": [
            "的",
            "呢",
            "和"
          ]
        },
        "synonym_filter": {
          "type": "synonym",
          "synonyms": [
            "安卓 => 安卓,Android",
            "鸿蒙 => 鸿蒙,HarmonyOS,OpenHarmony"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "question": {
        "type": "text",
        "analyzer": "my_analyzer",
        "search_analyzer": "my_analyzer"
      }
    }
  }
}

索引一篇文档

json 复制代码
POST questions/_doc
{
  "question":"鸿蒙/系统/和/安卓/系统/有/什么/区别/呢/?"
}

搜索"Android",发现成功召回了预期的文档

json 复制代码
GET questions/_search
{
  "query": {
    "match": {
      "question": "Android"
    }
  }
}

{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0.32792777,
    "hits": [
      {
        "_index": "questions",
        "_id": "y-SQx44BPIYet_3fDf9T",
        "_score": 0.32792777,
        "_source": {
          "question": "鸿蒙/系统/和/安卓/系统/有/什么/区别/呢/?"
        }
      }
    ]
  }
}

4. IK中文分析器

无论是Elasticsearch官方提供的分析器,还是我们自定义的分析器,都很难对中文内容有很好的分词效果,把一段中文切分成孤立的汉字会丢失语义。所以,我们需要一款针对中文的文本分析器,这里推荐 IK Analysis。

IK分析器需要单独安装,进入到Github页面:https://github.com/infinilabs/analysis-ik/releases,找到对应版本下载安装包放到Elasticsearch安装目录下的plugins目录,重启Elasticsearch即可使用。

IK Analysis 提供了两种中文分析器供我们使用,分别是:ik_max_word和ik_smart。

ik_max_word 切分的粒度更细,ik_smart 切分的粒度会粗一下,占用的存储空间也会相对少一些。一般推荐索引时使用ik_smart节省存储空间,搜索时使用 ik_max_word 切分出更多的词以便能搜索到结果。

如下示例,是ik_max_word的分词效果,分出10个词

json 复制代码
POST _analyze
{
  "analyzer":"ik_max_word",
  "text": "ik_max_word分词器的测试效果"
}

分词结果
["ik_max_word","ik","max","word","分词器","分词","器","的","测试","效果"]

下面是ik_smart的分词效果,只切分出5个词

json 复制代码
POST _analyze
{
  "analyzer":"ik_smart",
  "text": "ik_smart分词器的测试效果"
}

分词结果
["ik_smart","分词器","的","测试","效果"]
相关推荐
梦幻通灵19 分钟前
ES分词环境实战
大数据·elasticsearch·搜索引擎
Elastic 中国社区官方博客24 分钟前
Elasticsearch 中的热点以及如何使用 AutoOps 解决它们
大数据·运维·elasticsearch·搜索引擎·全文检索
小黑屋说YYDS6 小时前
ElasticSearch7.x入门教程之索引概念和基础操作(三)
elasticsearch
Java 第一深情8 小时前
Linux上安装单机版ElasticSearch6.8.1
linux·elasticsearch·全文检索
KevinAha1 天前
Elasticsearch 6.8 分析器
elasticsearch
wuxingge1 天前
elasticsearch7.10.2集群部署带认证
运维·elasticsearch
Elastic 中国社区官方博客1 天前
Elasticsearch:如何部署文本嵌入模型并将其用于语义搜索
大数据·人工智能·elasticsearch·搜索引擎·ai·全文检索
Dreams°1231 天前
【大数据测试 Elasticsearch 的标准--超详细篇】
大数据·elasticsearch·jenkins
鸠摩智首席音效师1 天前
如何在 Elasticsearch 中配置 SSL / TLS ?
elasticsearch·ssl
fishjam2 天前
[开源重构]Search(Elasticsearch/OpenSearch) Sync Tool
elasticsearch·重构·开源