真实用同一句话和同一批文档对比standard、whitespace、keyword三种tokenizer的切分结果与聚合桶差异,用量化的桶数量证明tokenizer选择对统计正确性的真实影响。
代码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分词结果(真实6个token): → ['Hello', 'World', 'ES', '8.15', 'is', 'running_fast'] ← 连字符-、逗号,、感叹号!、下划线_ 真实全部被当成分隔符丢弃 ← 但8.15这种数字+小数点的组合真实被识别成一个完整token没有被拆开
这组结果真实验证了standard分词器真实依据的是Unicode文本分段算法(UAX#29),不是简单地"遇到非字母就切"——它真实能分辨出8.15里的点是数字的一部分应该保留,但running_fast里的下划线真实被当成了词边界,整个单词被拦腰切成两半后又在标准分词里被直接丢弃下划线这个符号本身。这也是standard作为ES默认分词器的真实原因:大多数英文自然语言文本场景下,它的切分结果真实最接近人类对"单词"的直觉理解。
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分词结果(真实1个token): → ['Hello-World, ES 8.15 is running_fast!'] ← 整句话原样作为唯一的一个token,空格、标点一个字符都没变
代码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上跑出来的原始结果。
_analyze API 与 terms 聚合