文本分析是将非结构化文本 (如电子邮件的正文或产品描述) 转换为结构化格式,以便于搜索的过程。
如果索引中包含文本字段,或者文本搜索没有返回预期的结果,就可以通过配置文本 Analysis 来微调搜索。

1. Text Analysis 概述

文本分析让 Elasticsearch 能够执行全文搜索,返回所有相关结果,而不仅仅是精确匹配。

  • 精确匹配
    • 2017-01-01,exact value,搜索的时候,必须输入 2017-01-01,才能搜索出来。如果你输入一个 01,是搜索不出来的;
  • 全文搜索
    • 就不是说单纯的只是匹配完整的一个值,而是可以对值进行拆分词语后(分词)进行匹配,也可以通过缩写、时态、大小写、同义词等进行匹配;
    • 你在文本里搜索 Quick fox jumps,可能不仅希望返回包含 quick foxs 的文档,也希望返回 fox leap 的文档,因为 jump 和 leap 是同义词,相同意思的也满足需求可以作为结果返回;

1.1 Tokenization

  • 文本分析通过 tokenization 分词操作,让全文搜索变成了可能。
  • Tokenization 分词操作,将文本分解成更小的块,称为 token,大多情况下,这些 token 是单独的单词。
  • 将一段句子拆分成一个一个的单个的单词,并分别将每个单词编入索引,只要匹配到一个搜索关键词就可以匹配到结果。

1.2 Normalization

  • 切分词语后,文本分析就可以对单个 token 进行匹配,但是每个 token 的匹配也只是进行字面意义上的直接比较;
  • 我们希望可以在匹配关键词时,可以忽略大小写,时态,同时只要与搜索词相近意思的也符合条件,将结果返回;
  • 为了解决这个问题,文本分析将切分词语后的 token 转化为一个标准化的格式,这允许匹配到与搜索词不完全相同但足够相似的相关 token,例如:
    • Quick 小写化:quick;
    • foxes 简化它的词根:fox;
    • jump、leap 这样的同义词,可以作为一个单词进行索引:jump;
  • 为了确保搜索词与这些 token 匹配,可以对搜索短语使用相同的分词和规范化规则,例如,搜索短语 Foxes leap 会被规范化为搜索关键词 fox jump;

1.3 自定义文本分析

  • 文本分析是由分词器执行的,分词器包含一组规则来控制文本分析的过程;
  • ES 默认分词器是 standard analyzer;
  • 如果想自定义文本分析,可以使用其他的内置分词器,或者使用一个自定义的分词器;
  • 一个自定义的分词器可以控制文本分析的每一步,包括:
    • 在 tokenization 之前对文本进行修改;
    • 控制文本的切词规则;
    • 在索引 token 前,对 token 进行规范化;

2. Text Analysis 基本概念

2.1 Analyzer 组成

无论是内置的,还是自定义的 Analyzer 都是由底层的三个模块组成:character filters, tokenizers, and token filters;
我们可以自己组合这些模块来定义新的自定义 Analyzer;

1)Character filters

  • 针对原始文本进行处理,例如去除 HTML 标记;
  • 一个 Analyzer 可以由 0 个或多个 character filter,按顺序使用;
  • 《内置的 Character filter 详解》

    2)Tokenizer

  • 分词器,将接收的文本按照一定的规则进行切分;

  • Tokenizer 还负责记录每个 token 在文本中的顺序和位置,以及 token 所代表的原始单词的开始字符与结束字符的偏移量;
  • 一个 Analyzer 文本分析器只能有一个 Tokenizer 分词器;
  • 《内置的 Tokenizer 详解》

    3)Token filters

  • 针对切分的单词进行加工,比如转为小写、删除 stopwords、增加同义词;

  • 规范化操作 Normalization;
  • Token filters 不允许改变每个 token 记录在 Tokenizer 中的位置或字符偏移量;
  • 一个 Analyzer 可以有 0 个或多个 Token filters,它们按顺序使用;
  • 《内置的 Token filter 详解》更新ing

2.2 Stemming 词干分析

  • Stemming,就是将一个单词还原成它的词根形式的过程,这是 Normalization 规范化中的一种方式。
    • 例如:walking 和 walked 有相同的词根 walk,一旦进行了词根分析,任何一个词的出现都将在搜索中与另一个词匹配;
  • Stemming 词干分析虽然依赖于语言,但是通常需要移除单词的前缀和后缀;
  • 某些情况下,一些词的被词干分析后得到的词根不是一个正确的单词,但它在搜索中并不重要,如果一个单词的所有变体都简化为相同的词根形式,那么它们也将正确匹配;
    • 例如:jumping 和 jumpiness 经过词干分析后都是 jumpi,但 jumpi 不是一个正确的单词,但是不影响搜索;
  • Stemmer token filters,token 词干过滤器,es 中的词干分析过程是由它来处理的,有以下几类:

    • Algorithmic stemmers 算法词干分析器,根据一组规则进行词干分析;
    • Dictionary stemmers 字典词干分析器,通过查字典来对单词进行词干还原;

      1)Algorithmic stemmers

  • 算法词干分析器对每个单词应用一系列规则,将其简化为根形式。

    • 例如,用于英语的 Algorithmic stemmers 可能会从复数单词的末尾去掉 -s 和 -es 后缀;
  • 优势:
    • 很少的配置,而且可以在大多数情况下满足需求;
    • 占用的内存很少;
    • 通常比 Dictionary stemmers 要快;
  • 缺点:
    • 大多数的 Algorithmic stemmers 只能对包含词根的单词进行简化,如果是不包含词根的不规则单词,那么就不能进行转化,例如:
      • be, are, and am;
      • mouse and mice;
      • foot and feet;
  • 以下 token filters 使用了 Algorithmic stemmers:

    • stemmer,它为几种语言提供算法词干分析;
    • kstem,一种用于英语的词干分析器,它将算法词干分析与内置字典结合在一起;
    • porter_stem,官方推荐英语词干分析器;
    • snowball,它为几种语言使用基于滚雪球的词干分析规则;

      2)Dictionary stemmers

  • 字典词干分析器在提供的字典中查找单词,用字典中的词根单词替换未词根化的单词变体;

  • 理论上,Dictionary stemmers 很适合以下情况;
    • 对不规则的单词进行词干分析;
    • 区分拼写很像,但是概念上不相关的词,例如:
      • organ、organization;
      • broker、broken;
  • 实际上,Algorithmic stemmers 的表现通常优于 Dictionary stemmers。这是因为 Dictionary stemmers 有以下的缺点:
    • 字典的质量
      • Dictionary stemmers 好坏取决于它的字典。为了发挥作用,这些词典必须包含大量的单词,定期更新,并随语言趋势而变化。通常情况下,当一本词典问世时,它已经是不完整的,它的一些条目已经过时了。
    • 大小和性能
      • Dictionary stemmers 必须将字典中的所有单词、前缀和后缀加载到内存中。这可能会使用大量的RAM。低质量的字典可能在移除前缀和后缀的时候效率也很低,这可能会大大降低词干分析的速度。
  • hunspell,就是使用的字典词干分析器;
    • 官方建议:在使用 hunspell 前,先试试 Algorithmic stemmers;

:::tips 有时,词干分析会产生拼写类似但在概念上不相关的共享根词。例如,stemmer 会将 skies 和 skiing 简化为同一个词根:ski。
为了防止这种情况并更好地控制词根分析,可以使用以下的 token filters:

  • stemmer_override,它允许您定义针对特定 token 进行词干分析的规则;
  • keyword_marker,它将特定的 tokens 定义为 keyword,keyword 类型的 tokens 将不会被后续的 stemmer token filters 处理;
  • conditional,与 keyword_marker 类似,将 tokens 定义为 keyword 类型;

  • 对于内置的词干分析器,可以使用 stem_exclusion 参数来指定一个不会被词根化的单词列表; :::

2.3 Token graphs 分词图

  • 当 tokenizer 分词器将文本切分成 token 流时,它还会记录以下信息:
    • position:每个 token 在 token 流中的位置;
    • positionLength,token 在 token 流占的位置个数;
  • 使用 position 和 positionLength,就可以为一个 token 流生成一个 token graph,每一个 position 代表一个节点,每个 token 代表一个指向下一个 position 的边;

文本 Analysis - 图1

1)Synonyms 同义词

  • 一些 token filter 会向 token stream 中添加一些新的 token(例如,同义词)。这些新增的 token 的 position 通常与它们的同义词分布在相同的位置。

文本 Analysis - 图2

2)Multi-position tokens 横跨多个position的token

  • 一些 token filter 会向 token stream 中添加一些横跨多个 position 的 token。这类 token 通常时一些单词组成的词组的缩写,例如:atm 是 automatic teller machine 的同义词。
  • 但是,只有某些 token filter 才能被称为 graph token filter,会准确的记录这类 multi_position token 的 positionLength:
  • 举例,domain name system 和它的同义词 dns,它们的 position 都是0,然而,dns 的 positionLength 是3,其他的 token 都是默认 positionLength 为1。

文本 Analysis - 图3

3)搜索中使用 token graph

  • 索引时,是会忽略 positionLength 信息,而且不支持包含横跨多个 position 的 token 的 token graph。
  • 但是,查询时,例如 match、match_phrase,会从想要查询的字符串中生成多个 token graph (包括 multi_position tokens 的 graph),借此创建多个子查询。
  • 举例,使用 match_phrase 查询:domain name system is fragile;
    • 在对搜索字符串进行文本分析时,domain name system 的同义词 dns 会被添加到查询文本的 token stream 中,dns 的 positionLength 是3;

文本 Analysis - 图4

  • match_phrase 查询会使用这个 graph 生成多个子查询,这意味着匹配到的文档更加的全面。
    1. dns is fragile
    2. domain name system is fragile

4)Invalid token graphs

  • 下面的 token filter 可以添加跨越多个位置的 token (如 dns),但只记录默认的 positionLength 为1:
  • 这意味着如果 token stream 包含 domain name system 这样的词组时,也会向 token stream 中添加同义词 token(dns),但是 dns 的 positionLength 是默认值 1,导致 token graph 是错误的;

文本 Analysis - 图5

  • 无效的 token graph 可能导致意外的搜索结果,要避免使用。

3. 配置 Text Analysis

  • 一般情况下,ES 提供的默认分析器 standard analyzer 可以满足大部分的使用需求;
  • 如果一些内置的分析器也无法满足需求,可以使用 ES 提供的一些 option 选项功能来对 analyzer 进行微调
    • 例如,给 standard analyzer 配置 stop words 的自定义移除列表;
  • 最后就是,可以通过组合 analyzer 的底层模块来定制自己的分析器;

3.1 使用 Analyzer API 测试

1)测试内置的 Analyzer:

  1. POST _analyze
  2. {
  3. "analyzer": "whitespace",
  4. "text": "The quick brown fox."
  5. }

2)测试自定义组合模块

  • 也可以组合 tokenizer、character filter、token filter 来进行测试:

    • 一个 tokenizer 分词器;
    • 0 或多个 token filter;
    • 0 或多个 character filter;
      1. POST _analyze
      2. {
      3. "tokenizer": "standard",
      4. "filter": [ "lowercase", "asciifolding" ],
      5. "text": "Is this déja vu?"
      6. }
      7. ######返回结果
      8. {
      9. "tokens": [
      10. {
      11. "token": "is",
      12. "start_offset": 0,
      13. "end_offset": 2,
      14. "type": "<ALPHANUM>",
      15. "position": 0
      16. },
      17. {
      18. "token": "this",
      19. "start_offset": 3,
      20. "end_offset": 7,
      21. "type": "<ALPHANUM>",
      22. "position": 1
      23. },
      24. {
      25. "token": "deja",
      26. "start_offset": 8,
      27. "end_offset": 12,
      28. "type": "<ALPHANUM>",
      29. "position": 2
      30. },
      31. {
      32. "token": "vu",
      33. "start_offset": 13,
      34. "end_offset": 15,
      35. "type": "<ALPHANUM>",
      36. "position": 3
      37. }
      38. ]
      39. }
      :::info
  • Analyzer 不仅将文本切分成了 tokens,还记录一些额外的信息:

    • 每个 token 的顺序或相对位置:用于词组查询或者是同义词查询;
    • 每个 token 在原始文本中的 start、end 字符的偏移量:用于搜索词命中的高亮显示处理; :::

      3)测试自定义Analyzer

  • 在一个索引中自定义一个 Analyzer,然后可以针对这个索引使用 Analyzer API 对 Custom Analyzer 进行测试:

    1. # settings 中定义一个analyzer叫做std_folded
    2. # mappings my_text 字段使用这个自定义的 analyzer
    3. PUT my_index
    4. {
    5. "settings": {
    6. "analysis": {
    7. "analyzer": {
    8. "std_folded": {
    9. "type": "custom",
    10. "tokenizer": "standard",
    11. "filter": [
    12. "lowercase",
    13. "asciifolding"
    14. ]
    15. }
    16. }
    17. }
    18. },
    19. "mappings": {
    20. "properties": {
    21. "my_text": {
    22. "type": "text",
    23. "analyzer": "std_folded"
    24. }
    25. }
    26. }
    27. }
    28. # my_index索引中使用 _analyzer 测试方式1:通过analyzer名称
    29. GET my_index/_analyze
    30. {
    31. "analyzer": "std_folded",
    32. "text": "Is this déjà vu?"
    33. }
    34. # my_index索引中使用 _analyzer 测试方式2:通过字段名,这个字段要在mappings中配置好使用自定义anaylzer
    35. GET my_index/_analyze
    36. {
    37. "field": "my_text",
    38. "text": "Is this déjà vu?"
    39. }

3.2 配置内置的 Analyzer

  • 内置 Analyzer 详解
  • 内置的 Analyzer 可以通过配置 options 选项参数来对分析器进行微调: ```json PUT my_index { “settings”: { “analysis”: {
    "analyzer": {
      "std_english": { 
        "type":      "standard",
        "stopwords": "_english_"
      }
    }
    
    } }, “mappings”: { “properties”: {
    "my_text": {
      "type":     "text",
      "analyzer": "standard", 
      "fields": {
        "english": {
          "type":     "text",
          "analyzer": "std_english" 
        }
      }
    }
    
    } } }

POST my_index/_analyze { “field”: “my_text”, “text”: “The old brown cow” }

POST my_index/_analyze { “field”: “my_text.english”, “text”: “The old brown cow” }

:::info

- 在 standard analyzer 基础上定义了一个名为 std_english Analyzer,并为它配置上 ES 预定义的 english stopwords 移除列表;
- my_text 字段使用 standard Analyzer 分词,不会移除 stopwords,结果:[ the, old, brown, cow ];
- my_text.english 子字段使用自定义的 std_english Analyzer,所以 English stopwords 会被移除,结果:[ old, brown, cow ]
:::

<a name="KwawW"></a>
## 3.3 配置自定义的 Analyzer
<a name="IeG9t"></a>
### 1)配置项

- **tokenizer**(必选项):一个内置或者自定义的 tokenizer;
- **char_filter**(非必选):0个或多个内置、自定义的 character filter 组成的数组;
- **filter**(非必选):0个或多个内置、自定义的 token filter 组成的数据;
- **position_increment_gap**:默认值100。如果一个字段是由多个 text 组成的数组,那么在索引时,ES 会给 text元素之间插入一个假空隙,使得两个元素之间的 position 保持一个距离,以防止搜索词组时跨越多个元素进行匹配。可以在 Mappings 中为 text 字段修改 position_increment_gap 的值。例如:
   - 有字段值:[ "John Abraham", "Lincoln Smith"];
   - match_phrase 搜索:"Abraham Lincoln";
   - 因为 ES 在元素间插入了间隙,所以不会跨元素搜索,没有匹配的结果;
   - 如果 match_phrase 添加参数 "slop": 101,那么就会把整个数组的所有元素当作一个 text 去匹配,元素间的 position 没有间隙。会匹配到结果。
<a name="O3Lu4"></a>
### 2)一个比较复杂的例子

- **Character Filter**
   - 自定义一个名称为 emoticons 的 Character Filter,其类型是 Mapping Character Filter,将 ":)" 表情符转换为 "_happy_", ":(" 表情符转换为 "_sad_";
- **Tokenizer**
   - 自定义一个名称为 punctuation 的 Tokenizer,其类型是 Pattern Tokenizer,根据数组中的空格及标点符号 [ .,!?] 进行分词;
- **Token Filters**
   - 内置的 Lowercase Token Filter
   - 自定义一个名为 english_stop 的 Token Filter,其类型是 Stop Token Filter, 使用 ES 预定义的 English stop words 移除列表;
:::info
字段值:"I'm a :) person, and you?"<br />分词结果:[ i'm, _happy_, person, you ]
:::
```json
UT my_index
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_custom_analyzer": { 
          "type": "custom",
          "char_filter": [
            "emoticons"
          ],
          "tokenizer": "punctuation",
          "filter": [
            "lowercase",
            "english_stop"
          ]
        }
      },
      "tokenizer": {
        "punctuation": { 
          "type": "pattern",
          "pattern": "[ .,!?]"
        }
      },
      "char_filter": {
        "emoticons": { 
          "type": "mapping",
          "mappings": [
            ":) => _happy_",
            ":( => _sad_"
          ]
        }
      },
      "filter": {
        "english_stop": { 
          "type": "stop",
          "stopwords": "_english_"
        }
      }
    }
  }
}

POST my_index/_analyze
{
  "analyzer": "my_custom_analyzer",
  "text": "I'm a :) person, and you?"
}

3.4 指定 Analyzer

  • ES 提供了不同 level、不同场景下指定内置,或者自定义的 Analyzer:

    • 字段、索引、查询 level;
    • 索引时,查询时; :::tips
  • 尽量保持简单

    • 虽然 ES 可以灵活的在不同 level、场景下指定 Analyzer,但是非必要的情况下不要这么做,尽量保持简单;
    • 大多数情况下,最简单最好的方法:为每个 text 字段指定一个 Analyzer;
    • 而且,ES 默认的 Analyzer 配置也很好,索引和搜索使用相同的 Analyzer;
    • 可以使用 GET my_index/_mapping 查看字段具体使用的 Analyzer; :::

      1)ES 如何确定 Index 的 Analyzer?

  • ES 通过按顺序检查下面的参数,来确定索引时使用什么 Analyzer:

    • mappings 中字段的 analyzer 参数;
    • settings 中 analysis.analyzer.default 参数;
    • 如果这些都没有设定,那么就使用默认的 standard analyzer;

      (1) 为字段指定 Analyzer

      PUT my_index
      {
      "mappings": {
      "properties": {
       "title": {
         "type": "text",
         "analyzer": "whitespace"
       }
      }
      }
      }
      

      (2) 为 Index 指定默认 Analyzer

      UT my_index
      {
      "settings": {
      "analysis": {
       "analyzer": {
         "default": {
           "type": "simple"
         }
       }
      }
      }
      }
      

      2)ES 如何确定 Search 的 Analyzer?

      :::warning
  • 通常没必要在 Search 时指定一个与 Index 不同的 Analyzer

    • 这样做,会对相关性造成负面的影响,导致预期外的搜索结果;
    • 如果为 Search 单独指定了一个 Analyzer,官方建议在上线前对文本分析多进行测试。 :::
  • ES 通过按顺序检查下面的参数,来确定搜索时使用什么 Analyzer:

    • search query 中的 analyzer 参数;
    • mappings 中字段的 search_analyzer 参数;
    • settings 中 analysis.analyzer.default_search 参数;
    • mappings 中字段的 analyzer 参数;
    • 如果这些都没有设定,那么就使用默认的 standard analyzer;

      (1) 为 query 指定 Search Analyzer

      GET my_index/_search
      {
      "query": {
      "match": {
       "message": {
         "query": "Quick foxes",
         "analyzer": "stop"
       }
      }
      }
      }
      

      (2) 为字段指定 Search Analyzer

      :::tips
  • 如果在 mappings 中为字段配置了 search_analyzer,那么该字段的 analyzer 也必须配置; :::

    PUT my_index
    {
    "mappings": {
      "properties": {
        "title": {
          "type": "text",
          "analyzer": "whitespace",
          "search_analyzer": "simple"
        }
      }
    }
    }
    

    (3) 为 Index 指定 default Search Analyzer

    :::tips

  • 如果在 settings 中指定了索引的默认 Search Analyzer,那么 settings 中的 analysis.analyzer.default 也必须设置; :::

    PUT my_index
    {
    "settings": {
      "analysis": {
        "analyzer": {
          "default": {
            "type": "simple"
          },
          "default_search": {
            "type": "whitespace"
          }
        }
      }
    }
    }