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","分词器","的","测试","效果"]
相关推荐
运维&陈同学10 小时前
【Kibana01】企业级日志分析系统ELK之Kibana的安装与介绍
运维·后端·elk·elasticsearch·云原生·自动化·kibana·日志收集
ly21st13 小时前
elasticsearch安全认证
安全·elasticsearch
Mitch31113 小时前
【漏洞复现】CVE-2014-3120 & CVE-2015-1427 Expression Injection
运维·web安全·elasticsearch·docker·apache
开心最重要(*^▽^*)15 小时前
Metricbeat安装教程——Linux——Metricbeat监控ES集群
linux·elasticsearch
叫我DPT15 小时前
Elasticsearch 数据存储底层机制详解
elasticsearch·搜索引擎·全文检索
努力的布布15 小时前
Elasticsearch-索引的批量操作
大数据·elasticsearch·搜索引擎·全文检索
斑驳竹影16 小时前
ElasticSearch存储引擎
大数据·elasticsearch·搜索引擎
努力的布布20 小时前
Elasticsearch-模糊查询
大数据·elasticsearch·搜索引擎
m0_748237051 天前
Monorepo pnpm 模式管理多个 web 项目
大数据·前端·elasticsearch
java1234_小锋1 天前
ElasticSearch如何做性能优化?
大数据·elasticsearch·性能优化