【ElasticSearch 从入门到架构师】第6章_分词器与文本检索


在 ElasticSearch 的世界中,分词器(Analyzer) 是决定搜索质量的第一道关口。很多团队折腾了半天"为什么搜不出来",最终发现根因就是分词器配置不当。本章从原理到实战,带你彻底吃透分词器。


一、分词器工作原理:字符过滤 → 分词 → 词汇过滤

1.1 一条文本的"变身"之旅

假设你往 ES 写入一条文档:

复制代码
"The 2 QUICK Brown-Foxes jumped over the lazy dog's bone! 🦊"

全文检索时,这条文本会依次经过三个处理阶段:

复制代码
原始文本
  │
  ▼
┌─────────────────────────────────┐
│  ① 字符过滤器 (Character Filter)  │  ← 预处理:去掉 HTML 标签、转换字符
└─────────────────────────────────┘
  │  "The 2 QUICK Brown-Foxes..."
  ▼
┌─────────────────────────────────┐
│  ② 分词器 (Tokenizer)             │  ← 核心:把字符串切成一个个词项
└─────────────────────────────────┘
  │  ["The", "2", "QUICK", "Brown", "Foxes"...]
  ▼
┌─────────────────────────────────┐
│  ③ 词汇过滤器 (Token Filter)      │  ← 后处理:大小写、停用词、同义词
└─────────────────────────────────┘
  │  ["quick", "brown", "fox", "jump", "lazy", "dog", "bone"]

1.2 三个阶段详解

① 字符过滤器 (Character Filter)

作用于原始文本,在分词之前对字符进行清洗和转换。ES 内置三种:

字符过滤器 功能 示例
html_strip 去掉 HTML 标签 <p>Hello</p>Hello
mapping 自定义字符映射 &and:)_happy_
pattern_replace 正则替换 \d{3}-\d{4} → 脱敏

配置示例:

json 复制代码
PUT /my_index
{
  "settings": {
    "analysis": {
      "char_filter": {
        "my_char_filter": {
          "type": "mapping",
          "mappings": ["& => and", ":) => _happy_"]
        }
      },
      "analyzer": {
        "my_analyzer": {
          "char_filter": ["html_strip", "my_char_filter"],
          "tokenizer": "standard"
        }
      }
    }
  }
}
② 分词器 (Tokenizer)

这是整个分析过程的核心,负责将字符串切分成独立的词项 (Term)。不同分词器策略差异巨大:

复制代码
"hello world"  →  standard 分词器  →  ["hello", "world"]
"hello world"  →  keyword 分词器   →  ["hello world"]
"hello-world"  →  letter 分词器    →  ["hello", "world"]
③ 词汇过滤器 (Token Filter)

对切分后的词项做二次加工

词汇过滤器 功能 示例
lowercase 转小写 "HELLO""hello"
stop 移除停用词 "the quick fox""quick", "fox"
stemmer 词干提取 "running""run"
synonym 同义词扩展 "happy""happy", "joyful"
shingle N-gram 组合 "hello world""hello", "hello world", "world"

1.3 完整自定义分析器

json 复制代码
PUT /my_index
{
  "settings": {
    "analysis": {
      "char_filter": {
        "remove_at": {
          "type": "mapping",
          "mappings": ["@ => "]
        }
      },
      "filter": {
        "english_stop": {
          "type": "stop",
          "stopwords": "_english_"
        },
        "english_stemmer": {
          "type": "stemmer",
          "language": "english"
        }
      },
      "analyzer": {
        "my_english_analyzer": {
          "type": "custom",
          "char_filter": ["html_strip", "remove_at"],
          "tokenizer": "standard",
          "filter": ["lowercase", "english_stop", "english_stemmer"]
        }
      }
    }
  }
}

验证分析效果:

json 复制代码
GET /my_index/_analyze
{
  "analyzer": "my_english_analyzer",
  "text": "The 2 QUICK Brown-Foxes jumped @hello over the lazy dog's bone!"
}

输出:

json 复制代码
{
  "tokens": [
    { "token": "2", "position": 0 },
    { "token": "quick", "position": 1 },
    { "token": "brown", "position": 2 },
    { "token": "fox", "position": 3 },
    { "token": "jump", "position": 4 },
    { "token": "hello", "position": 5 },
    { "token": "lazi", "position": 6 },
    { "token": "dog", "position": 7 },
    { "token": "bone", "position": 8 }
  ]
}

二、ES 内置分词器:standard、simple、keyword 适用场景

ES 内置了 8 种分词器,其中最常用的三种足以覆盖 70% 的英文场景。

2.1 内置分词器速览

分词器 切分策略 输出示例(输入 "Hello World! 你好")
standard Unicode 标准切分,按词边界 ["hello", "world", "你", "好"]
simple 遇到非字母即切开,全小写 ["hello", "world", "你", "好"]
whitespace 按空白字符切开 ["Hello", "World!", "你好"]
keyword 不分词,整体作为一个词 ["Hello World! 你好"]
letter 遇到非字母即切开 ["Hello", "World", "你", "好"]
lowercase letter + 自动小写 ["hello", "world", "你", "好"]
stop letter + 去掉停用词 ["hello", "world"]
pattern 正则表达式切分 自定义

2.2 三大核心分词器深度对比

standard --- 默认分词器,通用首选
复制代码
分词算法:Unicode 标准文本分割算法 (UAX #29)
特点:按词边界切分,保留 email、URL,去掉标点
json 复制代码
GET /_analyze
{
  "tokenizer": "standard",
  "text": "user@example.com is at http://example.com/a?b=1&c=2"
}

切分结果:

复制代码
["user@example.com", "is", "at", "http://example.com/a?b=1&c=2"]

适用场景:

  • 通用英文全文搜索
  • 包含 email / URL 的文本
  • 精细化场景的底层 base
simple --- 极简主义
复制代码
分词算法:遇到非字母字符就切开,全部转小写
特点:最"暴力"的分词器,但也是最可控的
json 复制代码
GET /_analyze
{
  "tokenizer": "simple",
  "text": "user@example.com Product-ID: ABC-123"
}

切分结果:

复制代码
["user", "example", "com", "product", "id", "abc"]
keyword --- 精确匹配的守护者
复制代码
分词算法:不分词。整条文本原样作为一个词项。
特点:用于结构化的、需要精确匹配的字段。
json 复制代码
GET /_analyze
{
  "tokenizer": "keyword",
  "text": "Order-2024-001"
}

切分结果:

复制代码
["Order-2024-001"]   ← 原样保留

适用场景:

  • 订单号、身份证号、手机号
  • 邮箱地址、用户名
  • 标签、状态码

最佳实践: 在 mapping 中对这类字段同时设置 keyword 类型和 text 类型(multi-field),既支持精确匹配又支持全文搜索。

json 复制代码
PUT /orders
{
  "mappings": {
    "properties": {
      "order_id": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "status": {
        "type": "keyword"    // 状态值不需要分词
      }
    }
  }
}

三、主流中文分词器:IK、Jieba、HanLP 安装与配置

中文和英文最大的不同------词与词之间没有空格分隔。所以必须使用专用的中文分词器。

3.1 三大中文分词器对比

分词器 特色 分词准确性 性能 维护 推荐度
IK Analyzer ES 社区首选,词典丰富 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ 活跃 ★★★★★
Jieba Python 社区主流,自带词性标注 ⭐⭐⭐⭐ ⭐⭐⭐⭐ 活跃 ★★★★
HanLP NLP 级别,语义理解最强 ⭐⭐⭐⭐⭐ ⭐⭐⭐ 活跃 ★★★★

3.2 IK 分词器 --- 统治 ES 中文分词

安装
bash 复制代码
# ES 8.x 版本安装(版本号需与 ES 匹配)
./bin/elasticsearch-plugin install https://get.infini.cloud/elasticsearch/analysis-ik/8.12.0

# 或手动安装
wget https://github.com/infinilabs/analysis-ik/releases/download/v8.12.0/elasticsearch-analysis-ik-8.12.0.zip
./bin/elasticsearch-plugin install file:///path/to/elasticsearch-analysis-ik-8.12.0.zip

安装后重启 ES

bash 复制代码
# 验证是否安装成功
./bin/elasticsearch-plugin list
# 输出应包含:analysis-ik
两种核心模式
json 复制代码
# ik_smart --- 最细粒度切分(粗粒度)
GET /_analyze
{
  "analyzer": "ik_smart",
  "text": "中华人民共和国国歌"
}
// 结果:["中华人民共和国", "国歌"]

# ik_max_word --- 最细粒度切分(穷举所有可能)
GET /_analyze
{
  "analyzer": "ik_max_word",
  "text": "中华人民共和国国歌"
}
// 结果:["中华人民共和国", "中华人民", "中华", "华人", "人民共和国", "人民", "共和国", "共和", "国", "国歌"]

3.3 Jieba 分词器

Jieba 自带词性标注,对短语理解有天然优势。

bash 复制代码
# 安装
./bin/elasticsearch-plugin install https://github.com/sing1ee/elasticsearch-jieba-plugin/releases/download/v8.12.0/elasticsearch-jieba-plugin-8.12.0.zip
json 复制代码
GET /_analyze
{
  "analyzer": "jieba_index",
  "text": "他来到了南京市长江大桥"
}
// 结果:["他", "来到", "了", "南京市", "南京", "市长", "长江大桥", "长江", "大桥"]

3.4 HanLP 分词器 --- NLP 级别分词

支持命名实体识别、词性标注、依存句法分析,适合对搜索质量有极致要求的场景。

json 复制代码
GET /_analyze
{
  "analyzer": "hanlp",
  "text": "刘德华在台北小巨蛋举办了演唱会"
}
// 结果:["刘德华/nr", "台北/ns", "小巨蛋/nz", "举办/v", "演唱会/n"]
// nr=人名, ns=地名, nz=专用名, v=动词, n=名词

四、IK 分词器精准模式 & 最大词长模式实战

4.1 两种模式的本质区别

复制代码
ik_smart (智能模式)                ik_max_word (最大词长模式)
───────────────────────           ─────────────────────────
只切分出有意义的词                  尽可能切分出所有可能的词
召回精度高,误召回少                召回率高,覆盖率好
适合: 精确搜索                    适合: 召回优先的场景

4.2 实战:电商商品搜索

场景: 用户搜索 "苹果手机壳"

json 复制代码
# 注册自定义索引分析器
PUT /shop
{
  "settings": {
    "analysis": {
      "analyzer": {
        "ik_smart_analyzer": { "type": "custom", "tokenizer": "ik_smart" },
        "ik_max_word_analyzer": { "type": "custom", "tokenizer": "ik_max_word" }
      }
    }
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "ik_max_word",       // 索引时:穷举分词,保证召回
        "search_analyzer": "ik_smart"     // 搜索时:智能分词,保证精度
      }
    }
  }
}

核心技巧: 索引时用 ik_max_word,搜索时用 ik_smart ------这是 IK 分词器的最优配置范式。

  • 索引时穷举 → 不漏掉任何可能的匹配
  • 搜索时精确 → 不引入无关噪音

4.3 验证分词效果

json 复制代码
# 验证索引时分词
GET /shop/_analyze
{
  "analyzer": "ik_max_word",
  "text": "苹果手机壳"
}
// 结果:["苹果", "手机", "手机壳", "机壳"]

# 验证搜索时分词
GET /shop/_analyze
{
  "analyzer": "ik_smart", 
  "text": "苹果手机壳"
}
// 结果:["苹果", "手机", "壳"]

4.4 实战:复杂场景分词对比

json 复制代码
POST /_analyze
{
  "analyzer": "ik_max_word",
  "text": "深度学习自然语言处理框架"
}
分词器 结果 分析
ik_smart ["深度学习", "自然语言处理", "框架"] 精准,3 个词
ik_max_word ["深度学习", "深度", "学习", "自然语言处理", "自然", "自然语言", "语言", "处理", "框架"] 全面,9 个词

五、自定义词库、停用词配置(解决搜索不准确问题)

5.1 为什么需要自定义词库?

默认词库无法覆盖业务专有名词、新兴词汇、品牌名,导致分词错误:

复制代码
"麒麟芯片性能测试"  → 默认分词:["麒麟", "芯片", "性能", "测试"]  ✅
"鸿蒙Next版本更新"   → 默认分词:["鸿", "蒙", "Next", "版本", "更新"] ❌ 鸿蒙被拆开了!

5.2 IK 自定义词库配置

配置文件位置

IK 插件的配置目录在 ES 安装路径下:

复制代码
{ES_HOME}/config/analysis-ik/
├── IKAnalyzer.cfg.xml          ← 主配置文件
├── main.dic                     ← 系统主词典(1.1W+ 词条)
├── ext.dic                       ← 自定义扩展词典 ← 你在这里加词
├── stopword.dic                  ← 系统停用词
├── extra_stopword.dic            ← 自定义停用词典
└── custom/
    ├── my_tech.dic               ← 可以分文件管理
    └── my_brand.dic
修改主配置
xml 复制代码
<!-- IKAnalyzer.cfg.xml -->
<?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">ext.dic;custom/my_tech.dic;custom/my_brand.dic</entry>
    <!-- 用户扩展停用词 -->
    <entry key="ext_stopwords">extra_stopword.dic</entry>
</properties>
编写自定义词典

ext.dic(扩展词典):

复制代码
# 技术名词
鸿蒙
麒麟芯片
鲲鹏
昇腾
微服务
容器化
云原生
大模型
Prompt工程
RAG检索增强生成

# 品牌/产品名
蔡司镜头
索尼微单
戴森吸尘器
苹果MacBookPro

# 网络热词
躺平
内卷
摆烂
AI数字人

extra_stopword.dic(停用词词典):

复制代码
的
了
呢
吧
啊
嘛
哦
哈
咦
# 以及"吗、呀、哇、唉、嘿、呵"等语气词

重要提醒: 每次修改词典文件后,必须重启 ES 才能生效。IK 分词器在启动时加载词典到内存。

动态热更新(推荐)

IK 支持从远程 URL 加载词典,无需重启:

xml 复制代码
<!-- IKAnalyzer.cfg.xml -->
<properties>
    <entry key="ext_dict">http://your-server.com/ik/custom_dict.dic</entry>
    <entry key="ext_stopwords">http://your-server.com/ik/stopwords.dic</entry>
    <!-- 远程词典更新检测间隔,单位:毫秒 -->
    <entry key="remote_ext_dict_check_interval">60000</entry>
    <entry key="remote_ext_stopwords_check_interval">60000</entry>
</properties>

只需更新远端文件,IK 会在 60 秒内自动拉取新词典。

5.3 停用词实战

场景: 客服系统中,"请问"、"您好"、"麻烦问一下" 这类高频客套话不应参与检索。

json 复制代码
PUT /customer_service
{
  "settings": {
    "analysis": {
      "filter": {
        "cs_stop": {
          "type": "stop",
          "stopwords": ["请问", "您好", "谢谢", "麻烦问一下", "我想咨询"]
        }
      },
      "analyzer": {
        "cs_analyzer": {
          "type": "custom",
          "tokenizer": "ik_max_word",
          "filter": ["cs_stop"]
        }
      }
    }
  }
}

验证效果:

json 复制代码
GET /customer_service/_analyze
{
  "analyzer": "cs_analyzer",
  "text": "请问如何修改密码?谢谢"
}
// 结果:["如何", "修改", "密码"]  ← "请问"和"谢谢"被过滤

5.4 同义词配置

场景: 用户搜 "笔记本",也应该召回含有 "笔记本电脑" 的文档。

json 复制代码
PUT /shop
{
  "settings": {
    "analysis": {
      "filter": {
        "my_synonym": {
          "type": "synonym",
          "synonyms_path": "analysis/synonyms.txt"
        }
      },
      "analyzer": {
        "ik_synonym": {
          "type": "custom",
          "tokenizer": "ik_smart",
          "filter": ["my_synonym"]
        }
      }
    }
  }
}

config/analysis/synonyms.txt 内容:

复制代码
笔记本, 笔记本电脑, laptop, 本本
手机, 电话, 移动电话, cellphone, mobile
优惠券, 优惠卷, 折扣券, 券

六、分词优化:解决分词不全、分词错误、搜索匹配失败问题

6.1 问题诊断三板斧

当搜索不准确时,第一步永远是------先看分词结果

json 复制代码
# 三板斧第一斧:_analyze API
GET /_analyze
{
  "field": "title",        // 用实际字段名
  "text": "你搜不到的那个词"
}

# 三板斧第二斧:_termvectors API(查看已索引的词项)
GET /my_index/_termvectors/1?fields=title

# 三板斧第三斧:Profile API(查看搜索执行细节)
GET /my_index/_search
{
  "profile": true,
  "query": {
    "match": { "title": "你搜不到的词" }
  }
}

6.2 问题场景与解决方案

问题①:分词不全 --- 新词/专有名词被切开

现象: 搜 "GPT-4o" 搜不到,因为被切成 ["gpt", "4o"]

解决方案A:加自定义词典

复制代码
# ext.dic 添加
GPT-4o
GPT-4
ChatGPT
Claude

解决方案B:使用 keyword 子字段兜底

json 复制代码
{
  "properties": {
    "model_name": {
      "type": "text",
      "analyzer": "ik_max_word",
      "fields": {
        "keyword": { "type": "keyword" }  // 兜底精确匹配
      }
    }
  }
}
问题②:分词错误 --- 歧义切分

经典歧义: "南京市长江大桥"

复制代码
错误切分:["南京", "市长", "江大桥"]  ← 把"市长"当成了一个词
正确切分:["南京市", "长江大桥"]

解决方案:自定义词典 + 词性标注

json 复制代码
# 1. 在 ext.dic 中添加
南京市
长江大桥

# 2. 如果对准确度要求极高,考虑升级到 HanLP
# HanLP 利用词性标注和语义理解,能正确识别"南京市"为地名
问题③:搜索匹配失败 --- 索引/搜索分词器不一致

陷阱: 如果索引和搜索使用不同的分词器,可能出现索引了的词搜索时切不出来的情况。

json 复制代码
# ❌ 错误配置
{
  "title": {
    "type": "text",
    "analyzer": "ik_smart"      // 索引
    // search_analyzer 未指定 → 默认也用 ik_smart
  }
}
// 索引"苹果手机壳" → ["苹果", "手机", "壳"]
// 用户搜"手机壳"   → ["手机壳"] 
// 索引里没有独立的"手机壳"词项 → 搜不到!❌
json 复制代码
# ✅ 正确配置
{
  "title": {
    "type": "text",
    "analyzer": "ik_max_word",       // 索引:穷举
    "search_analyzer": "ik_smart"    // 搜索:精确
  }
}
// 索引"苹果手机壳" → ["苹果", "手机", "手机壳", "机壳", "壳"]
// 用户搜"手机壳"   → ["手机壳"]
// 命中!✅

6.3 分词语义度调优:相似度算法

搜索结果的排序由相似度算法 决定。ES 默认使用 BM25

json 复制代码
PUT /my_index
{
  "settings": {
    "similarity": {
      "my_bm25": {
        "type": "BM25",
        "k1": 1.2,        // 词频饱和度参数(默认1.2,调高→词频影响更大)
        "b": 0.75         // 文档长度归一化(默认0.75,调低→短文档更占优)
      }
    }
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "similarity": "my_bm25",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart"
      }
    }
  }
}

6.4 搜索精准度优化清单

优化项 具体操作 效果
索引/搜索分词器分离 analyzer: ik_max_word + search_analyzer: ik_smart 兼顾召回和精度
自定义业务词库 ext.dic 添加行业术语、产品名、品牌名 消除专有名词被切开
停用词过滤 过滤无语义价值的语气词、客套话 减少噪音匹配
同义词扩展 synonym filter 配置同义词表 提升召回率
ngram 兜底 对标题等短文本加 ngram 子字段 防止短词搜不到
multi-field keyword + text 双字段 精确匹配和全文搜索兼得
近实时更新词典 远程词典 + 自动热更新 无需重启即可更新词库

6.5 ngram 兜底方案

对于标题等短文本,增加 ngram 子字段作为最后的兜底:

json 复制代码
PUT /shop
{
  "settings": {
    "analysis": {
      "analyzer": {
        "title_ngram": {
          "type": "custom",
          "tokenizer": "ik_max_word",
          "filter": ["ngram_filter"]
        }
      },
      "filter": {
        "ngram_filter": {
          "type": "ngram",
          "min_gram": 2,
          "max_gram": 3
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart",
        "fields": {
          "ngram": {
            "type": "text",
            "analyzer": "title_ngram"
          },
          "keyword": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

搜索时使用 multi_match 多字段检索 + bool query 加权:

json 复制代码
GET /shop/_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { "title": { "query": "手机壳", "boost": 10 } } },
        { "match": { "title.ngram": { "query": "手机壳", "boost": 2 } } },
        { "term": { "title.keyword": { "value": "手机壳", "boost": 100 } } }
      ]
    }
  }
}

本章总结

复制代码
┌───────────────────────────────────────────────────────────┐
│                   分词器 → 搜索精准度的命门                   │
│                                                           │
│  ① 原理:char_filter → tokenizer → token_filter           │
│  ② 英文:standard/simple/keyword 覆盖主流场景               │
│  ③ 中文:IK (首选) / Jieba / HanLP (NLP级)                 │
│  ④ 范式:索引 ik_max_word + 搜索 ik_smart                  │
│  ⑤ 词库:ext.dic + 停用词 + 同义词 + 远程热更新             │
│  ⑥ 诊断:_analyze → _termvectors → Profile API            │
│  ⑦ 兜底:keyword + ngram + multi_match 加权               │
│                                                           │
│  "分词对了,搜索就对了一半。"                                  │
└───────────────────────────────────────────────────────────┘

下一章预告: 第7章 搜索排序与相关性调优 ------ BM25 算法深度解析、Function Score 实战、搜索点击反馈与 Learning to Rank 入门。

相关推荐
Flittly1 小时前
【AgentScope Java新手村系列】(6)Hook与Middleware
java·spring boot·笔记·spring·ai
向量引擎1 小时前
AI API 正在进入“请求生命周期治理”阶段:从模型迁移、Agent 接入到成本与安全排错的工程化方法
java·人工智能·python·aigc·ai编程·ai写作·gpu算力
徐子元竟然被占了!!1 小时前
Git学习
git·学习·elasticsearch
许彰午1 小时前
34_Java设计模式之单例模式
java·单例模式·设计模式
Java 码思客1 小时前
【ElasticSearch从入门到架构师】第7章-聚合查询——实现数据统计与分析
大数据·elasticsearch·jenkins
摇滚侠1 小时前
MyBatis 入门到项目实战 IDEA 配置模板 20-22
java·intellij-idea·mybatis
技术小结-李爽1 小时前
【工具】Maven二进制包目录结构说明
java·maven
zyl837211 小时前
前后端高并发解决方案
java·redis
Doker 多克1 小时前
Spring AI Alibaba—快速构建ReactAgent
java·开发语言·前端·ai编程