HOOOS

Elasticsearch Normalizer解密:让Keyword字段也能『不拘小节』地精确匹配

0 55 ES老司机阿强 ElasticsearchNormalizerKeyword精确匹配大小写不敏感
Apple

在 Elasticsearch (ES) 的世界里,keyword 字段类型是用于存储那些不需要分词、需要精确匹配的文本,比如标签、状态码、用户名、邮箱地址等等。它就像一个严谨的守门员,只有一模一样的值才能通过。

但有时候,这种『严谨』反而会带来麻烦。比如,你想根据用户输入的邮箱 USER@EXAMPLE.COM 查找对应的用户文档,而数据库里存的是 user@example.com。标准的 keyword 字段会告诉你:“抱歉,找不到!” 因为大小写不同,在它眼里就是两个完全不同的字符串。

类似的情况还有很多:

  • 大小写不敏感搜索Apple vs apple
  • 忽略口音/变音符号café vs cafe
  • 处理其他非关键字符变化

这时候,你可能会想:“难道我必须用 text 字段配合分析器(analyzer)吗?可我明明只需要精确匹配,不需要分词啊!”

别急,Elasticsearch 提供了一个专门为 keyword 字段量身定做的工具——Normalizer(规范器)

什么是 Normalizer?

normalizer 可以看作是 analyzer 的一个简化版,它专门应用于 keyword 字段,在索引时对字段的值进行一系列的规范化处理,但不会进行分词

它的核心作用是:在保留字段原子性的前提下(即不拆分成多个词元),对整个字符串进行统一的格式化处理。

你可以把它想象成一个『预处理器』,在数据存入索引之前,先把它『打磨』成一个标准格式。

Normalizer 如何工作?

一个 normalizer 主要由以下部分组成(和 analyzer 很像,但有关键区别):

  1. Character Filters (字符过滤器):可选,0个或多个。在处理的最开始阶段作用于原始字符串,可以进行添加、删除、替换字符等操作。比如,移除 HTML 标签的 html_strip 过滤器。
  2. Token Filters (词元过滤器):可选,0个或多个。注意!虽然叫 Token Filter,但normalizer 的上下文中,它作用于整个字符串,而不是分词后的词元(因为 normalizer 不分词)。常见的有 lowercase(转小写)、asciifolding(移除变音符号,如 é -> e)等。

关键区别:normalizer 没有 Tokenizer(分词器)! 这是它和 analyzer 最本质的不同。analyzer 的核心是分词,将文本拆分成多个词元;而 normalizer 始终将输入视为一个单一的词元进行处理。

处理流程如下:

原始 keyword 值 -> [字符过滤器(们)] -> [词元过滤器(们)] -> 最终规范化的 keyword 值 (存入倒排索引)

定义和使用 Normalizer

normalizer 需要在索引的设置(settings)中定义,然后在映射(mapping)中指定给 keyword 字段。

假设我们想实现一个忽略大小写、忽略常见英文变音符号的 keyword 字段,比如用于存储和查询邮箱地址。

第一步:在索引设置中定义 Custom Normalizer

我们需要在创建索引时,通过 analysis 设置来定义一个自定义的 normalizer。我们给它起个名字,比如 my_email_normalizer,并指定它使用的过滤器:lowercaseasciifolding

PUT /my_users
{
  "settings": {
    "analysis": {
      "normalizer": {
        "my_email_normalizer": { 
          "type": "custom",
          "char_filter": [], 
          "filter": ["lowercase", "asciifolding"] 
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "email": {
        "type": "keyword",
        "normalizer": "my_email_normalizer" 
      },
      "username": {
        "type": "keyword" 
      }
    }
  }
}
  • settings.analysis.normalizer: 定义 normalizer 的地方。
  • my_email_normalizer: 我们自定义的 normalizer 的名字。
  • type: "custom": 表明是自定义的。
  • char_filter: []: 这里我们不需要字符过滤器。
  • filter: ["lowercase", "asciifolding"]: 指定使用的词元过滤器,顺序很重要。先转小写,再移除变音符号。
  • mappings.properties.email.normalizer: 在 email 字段的映射中,通过 normalizer 属性指定使用我们刚刚定义的 my_email_normalizer
  • username 字段则是一个普通的 keyword 字段,没有应用 normalizer,作为对比。

第二步:索引文档

现在我们索引一些文档:

POST /my_users/_doc/1
{
  "email": "User1@Example.COM",
  "username": "User1"
}

POST /my_users/_doc/2
{
  "email": "user2.café@example.net",
  "username": "User2.Café"
}

POST /my_users/_doc/3
{
  "email": "user3@example.org",
  "username": "user3"
}

发生了什么?

当这些文档被索引时,email 字段的值会经过 my_email_normalizer 的处理:

  • User1@Example.COM -> lowercase -> user1@example.com -> asciifolding -> user1@example.com
  • user2.café@example.net -> lowercase -> user2.café@example.net -> asciifolding -> user2.cafe@example.net
  • user3@example.org -> lowercase -> user3@example.org -> asciifolding -> user3@example.org

最终,Elasticsearch 的倒排索引中存储的是这些规范化之后的值:user1@example.com, user2.cafe@example.net, user3@example.org

username 字段因为没有 normalizer,所以存储的是原始值:User1, User2.Café, user3

Normalizer 对查询的影响:Term Query vs Match Query

理解 normalizer 如何影响查询行为至关重要,特别是 term 查询和 match 查询。

1. term 查询

term 查询用于查找未经分析的精确词元。这意味着,当你在一个应用了 normalizerkeyword 字段上使用 term 查询时,你提供的查询词 必须 与索引中存储的 规范化后 的值完全匹配! normalizer 不会 自动应用于 term 查询的查询文本本身。

  • 能匹配的情况:

    GET /my_users/_search
    {
      "query": {
        "term": {
          "email": "user1@example.com" 
        }
      }
    }
    

    这个查询会成功找到文档 1,因为查询词 user1@example.com 和索引中存储的规范化值完全一致。

    GET /my_users/_search
    {
      "query": {
        "term": {
          "email": "user2.cafe@example.net" 
        }
      }
    }
    

    这个查询也会成功找到文档 2,因为查询词 user2.cafe@example.net 和索引中存储的规范化值完全一致。

  • 不能匹配的情况:

    GET /my_users/_search
    {
      "query": {
        "term": {
          "email": "User1@Example.COM" 
        }
      }
    }
    

    失败! 因为查询词 User1@Example.COM 没有经过规范化,与索引中的 user1@example.com 不匹配。

    GET /my_users/_search
    {
      "query": {
        "term": {
          "email": "user2.café@example.net" 
        }
      }
    }
    

    失败! 因为查询词 user2.café@example.net 包含变音符号,没有经过规范化,与索引中的 user2.cafe@example.net 不匹配。

思考一下为什么会这样? term 查询的设计目标就是查找底层倒排索引中的精确词元。normalizer 只在索引时修改了存入倒排索引的值,它并不改变 term 查询本身的行为(即直接使用你给的查询词去索引里查找)。所以,你需要自己确保 term 查询的词是规范化后的形式。

这看起来有点反直觉,对吧?我用了 normalizer 不就是为了忽略大小写和变音符号吗,怎么查询的时候还得自己处理?别急,看 match 查询。

2. match 查询

match 查询通常用于全文搜索 (text 字段),它会对查询文本应用相同的分析器(analyzer),然后再去匹配索引中的词元。

那么,当 match 查询作用于一个带有 normalizerkeyword 字段时会发生什么呢?

一个有趣的现象发生了:Elasticsearch 会将该字段的 normalizer 应用于查询文本! 这意味着 match 查询可以自动处理大小写和变音符号等问题,实现我们期望的『不拘小节』的精确匹配。

  • 都能匹配的情况:

    GET /my_users/_search
    {
      "query": {
        "match": {
          "email": "User1@Example.COM" 
        }
      }
    }
    

    成功! ES 会将查询文本 User1@Example.COM 通过 my_email_normalizer 规范化为 user1@example.com,然后去匹配索引,找到文档 1。

    GET /my_users/_search
    {
      "query": {
        "match": {
          "email": "user2.café@example.net" 
        }
      }
    }
    

    成功! ES 会将查询文本 user2.café@example.net 通过 my_email_normalizer 规范化为 user2.cafe@example.net,然后去匹配索引,找到文档 2。

    GET /my_users/_search
    {
      "query": {
        "match": {
          "email": "user3@example.org" 
        }
      }
    }
    

    成功! 查询文本 user3@example.org 规范化后仍然是 user3@example.org,找到文档 3。

对比一下没有 normalizerusername 字段:

GET /my_users/_search
{
  "query": {
    "match": {
      "username": "user1" 
    }
  }
}

失败! 因为 username 是普通 keyword,没有 normalizermatch 查询也不会对其进行任何处理,直接用 user1 去匹配索引里的 User1,自然找不到。

GET /my_users/_search
{
  "query": {
    "term": {
      "username": "User1" 
    }
  }
}

成功! term 查询精确匹配索引中的 User1

总结一下查询行为:

字段类型 查询类型 查询文本处理方式 匹配逻辑
keyword (无 Normalizer) term 不处理 查询文本 == 索引值
keyword (无 Normalizer) match 不处理 查询文本 == 索引值
keyword (有 Normalizer) term 不处理 查询文本 == 规范化后的索引值
keyword (有 Normalizer) match 应用 Normalizer 规范化查询文本 规范化后的查询文本 == 规范化后的索引值
text (有 Analyzer) term 不处理 查询文本 == 某个分词后的词元
text (有 Analyzer) match 应用 Analyzer 分析查询文本 分析后的查询词元之一 == 某个分词后的索引词元

所以,如果你想在应用了 normalizerkeyword 字段上进行忽略大小写/变音符号等的精确匹配,使用 match 查询通常是更方便、更符合直觉的选择。 如果你坚持使用 term 查询,那么你需要确保你的应用程序在查询前,手动将查询词进行与 normalizer 相同的规范化处理。

normalizer vs analyzer:再次强调

虽然它们都用到 char_filtertoken_filter,但核心区别在于:

  • normalizer:
    • 用于 keyword 字段。
    • 没有分词器 (Tokenizer)
    • 处理结果是一个规范化后的词元 (token)。
    • 目的是在不分词的前提下,对整个值进行格式统一,以支持更灵活的精确匹配、聚合和排序。
  • analyzer:
    • 用于 text 字段。
    • 必须有分词器 (Tokenizer)
    • 处理结果是零个或多个词元 (token)。
    • 目的是将文本拆分成词元,并进行处理(如转小写、去停用词、词干提取等),以支持全文检索。

简单说:normalizer 是为了让 keyword 更『宽容』一点,而 analyzer 是为了让 text 能够被『理解』和搜索。

常见用例

  • 大小写不敏感的标签/分类/状态码匹配与聚合:比如标签 Java, java, JAVA 能被视为同一个。
  • 邮箱地址、用户名查询:忽略大小写。
  • URL 规范化:比如统一协议为小写,移除末尾斜杠等(可能需要自定义 char_filtertoken_filter)。
  • 需要忽略口音/变音符号的精确匹配场景:比如人名、地名。

注意事项

  1. 只在索引时生效normalizer 的处理发生在文档写入索引时。一旦定义并应用到字段上,它只对之后新索引或更新的文档生效。如果想对已有数据生效,需要进行 reindex 操作。
  2. 选择合适的过滤器:根据你的具体需求选择 char_filtertoken_filter。不是越多越好,过度规范化可能会导致意想不到的冲突。
  3. 对聚合的影响terms 聚合也会作用于规范化后的值。这意味着 User1@Example.COMuser1@example.com 会被聚合到同一个桶 user1@example.com 中,这通常是我们想要的结果。
  4. 性能考虑normalizer 的处理会增加索引时的一点点开销,但通常非常小,对于 keyword 字段的查询和聚合性能往往有正面影响,因为它减少了需要精确匹配的唯一词项数量。
  5. 无法通过 _analyze API 测试_analyze API 是用来测试 analyzer 的,不能直接用来测试 normalizer 的完整效果。你需要通过索引实际数据和执行查询来验证其行为。

结语

Elasticsearch 的 normalizer 是一个强大而精妙的工具,它填补了标准 keyword 的严格精确匹配和 text 字段分词分析之间的空白。当你需要在 keyword 字段上实现不区分大小写、忽略变音符号或其他形式的规范化精确匹配、聚合或排序时,normalizer 就是你的不二之选。

记住它和 analyzer 的核心区别(无分词器),以及它对 termmatch 查询的不同影响,你就能有效地利用 normalizer 让你的 Elasticsearch 查询更加灵活和智能!

点评评价

captcha
健康