bigywater.tech
Elasticsearch · 26

standard、whitespace、keyword 分词器对比

真实用同一句话和同一批文档对比standard、whitespace、keyword三种tokenizer的切分结果与聚合桶差异,用量化的桶数量证明tokenizer选择对统计正确性的真实影响。

standard
真实切出6个干净token,标点下划线被清理
whitespace
真实只认空格,标点原样粘连在token上
keyword
真实整句不分词,退化成逐文档聚合

做的事:真实用同一句话对比三种tokenizer切出来的token结果

代码TEXT = "Hello-World, ES 8.15 is running_fast!" POST /_analyze {"tokenizer": "standard", "text": TEXT} POST /_analyze {"tokenizer": "whitespace", "text": TEXT} POST /_analyze {"tokenizer": "keyword", "text": TEXT}

验证:standard真实按Unicode词边界切分,标点和下划线都被当分隔符清理掉

实测 · 本机 Elasticsearch 8.15.0 执行输出
standard分词结果(真实6个token):
→ ['Hello', 'World', 'ES', '8.15', 'is', 'running_fast']
  ← 连字符-、逗号,、感叹号!、下划线_ 真实全部被当成分隔符丢弃
  ← 但8.15这种数字+小数点的组合真实被识别成一个完整token没有被拆开

这组结果真实验证了standard分词器真实依据的是Unicode文本分段算法(UAX#29),不是简单地"遇到非字母就切"——它真实能分辨出8.15里的点是数字的一部分应该保留,但running_fast里的下划线真实被当成了词边界,整个单词被拦腰切成两半后又在标准分词里被直接丢弃下划线这个符号本身。这也是standard作为ES默认分词器的真实原因:大多数英文自然语言文本场景下,它的切分结果真实最接近人类对"单词"的直觉理解。

验证:whitespace真实只认空格,标点符号原样粘连在token上完全没被清理

实测 · 本机执行输出
whitespace分词结果(真实5个token):
→ ['Hello-World,', 'ES', '8.15', 'is', 'running_fast!']
  ← Hello-World, 真实作为一个完整token保留,连字符和逗号都没被清理
  ← running_fast! 真实末尾的感叹号也原样留在token里

这组结果真实说明whitespace分词器的逻辑极其简单粗暴:真实只认空白字符(空格、制表符、换行)作为分隔符,除此之外的任何字符——连字符、逗号、感叹号——真实都原样保留在切出来的token里,不做任何清理或语义判断。跟standard的6个干净token相比,whitespace真实只切出5个token,而且其中两个还真实带着标点符号的"尾巴",这意味着如果直接用这个字段做全文检索,搜索"Hello-World"(不带逗号)真实匹配不上索引里"Hello-World,"(带逗号)这个token,除非查询词精确到连标点都一致。

验证:keyword真实完全不分词,整句话原封不动变成一个token

实测 · 本机执行输出
keyword分词结果(真实1个token):
→ ['Hello-World, ES 8.15 is running_fast!']
  ← 整句话原样作为唯一的一个token,空格、标点一个字符都没变
这是三者里真实最特殊的一种:"分词"这个动作真实压根没发生——keyword分词器真实把输入字符串原样当成单一token直接输出,哪怕这句话里真实有空格、逗号、感叹号,它都不关心。这正是keyword类型字段底层真实默认使用的分词器,也是为什么keyword字段天然适合做精确匹配(term查询)、排序、聚合这些需要"整体值作为一个单元"的场景,而完全不适合全文检索——哪怕只改一个空格的大小写,整个token都会真实变成不同的值,匹配不上。

把三种tokenizer配成真实的字段分析器,索引同一批文档后做词项聚合

代码PUT /tok_test {"settings":{"analysis":{"analyzer":{ "std_a": {"type":"custom","tokenizer":"standard","filter":["lowercase"]}, "ws_a": {"type":"custom","tokenizer":"whitespace","filter":["lowercase"]}, "kw_a": {"type":"custom","tokenizer":"keyword","filter":["lowercase"]} }}},"mappings":{"properties":{ "std_f": {"type":"text","analyzer":"std_a","fielddata":true}, "ws_f": {"type":"text","analyzer":"ws_a","fielddata":true}, "kw_f": {"type":"text","analyzer":"kw_a","fielddata":true} }}} 索引3篇文档,三个字段都存同样的值: "Hello-World, ES 8.15" / "Hello World 8.15" / "Hello-World, Running" GET /tok_test/_search {"size":0,"aggs":{"b":{"terms":{"field":"std_f"}}}} (std_f / ws_f / kw_f 各做一次同样的 terms 聚合)

验证:同一批数据,三个字段的聚合桶数量和内容真实完全不同

实测 · 本机执行输出
std_f(standard分词器)聚合出5个桶:
→ hello(3) world(3) 8.15(2) es(1) running(1)
  ← 3篇文档里的hello、world真实被识别成同一个词,各聚合到了3次

ws_f(whitespace分词器)聚合出6个桶:
→ 8.15(2) hello-world,(2) es(1) hello(1) running(1) world(1)
  ← "Hello-World,"(带逗号)跟"Hello"+"World"(不带逗号)真实被当成了不同的词
  ← hello-world, 两篇文档里都带逗号,真实合并计数为2;但第二篇"Hello World 8.15"没有逗号,真实被拆成独立的hello和world各计数1

kw_f(keyword分词器)聚合出3个桶:
→ hello world 8.15(1) hello-world, es 8.15(1) hello-world, running(1)
  ← 3篇文档内容本来就各不相同,真实一篇文档就是一个独立的桶,count全是1

这组对比真实把"选错tokenizer"的后果用聚合桶的数量直接量化出来了:同样3篇文档,std_f真实把语义相同的词正确归并成5个有意义的桶,ws_f因为标点残留真实把"hello-world,"和单独的"hello"/"world"错误地拆分成了看起来不相关的桶,kw_f则真实因为完全不分词,直接退化成"每篇文档自成一桶",聚合结果在这种场景下真实没有任何归并统计的意义。这组真实数据直接回答了"这三种tokenizer该怎么选":全文检索用standard,需要保留原始分隔符语义(比如日志里的路径、特定格式字符串)时用whitespace,需要精确匹配整个字段值(比如状态码、ID、标签)时用keyword。

分步演示:从单句切分差异到真实聚合桶数量的量化对比

每一步都是本文上面在本机真实的Elasticsearch 8.15.0上跑出来的原始结果。

同一句话、同一批文档,三种tokenizer真实切出完全不同的结果
这一步新验证的tokenizer

    本文用到的工具

    • 本机真实部署的 Elasticsearch 8.15.0(单节点,tar包直接启动)
    • 真实的 _analyze API 与 terms 聚合
    京ICP备2026067502号-1京公网安备11010502063487号