从“搜商品名”说起
MySQL 里搜商品名,常见写法是 LIKE '%手机%'。B+ 树索引只能做前缀匹配,前后都带通配符就只能全表扫描,几百万行数据下接口直接超时。Elasticsearch 的思路完全不同:它把文本拆成词,建立“词 → 文档 ID”的映射,查询时先找到词,再取出对应的文档集合,不需要逐行扫描。
这就是倒排索引。正排索引是“文档 → 包含哪些词”,倒排索引反过来,是“词 → 出现在哪些文档”。搜索、聚合、排序都建立在这个结构上,理解它,才能理解为什么 mapping 和分词器这么重要。
倒排索引怎么工作
写入一条文档时,Elasticsearch 先做字符过滤、分词、词项过滤,得到一组 term,然后更新 term dictionary 和 posting list。term dictionary 是有序的,查询时可以用二分或 FST 快速定位;posting list 记录包含该 term 的文档 ID,以及词频、位置等信息,用于算相关性和短语匹配。
分词方式直接决定召回。英文按空格和标点切分比较自然,中文用默认的 standard 分词器会按单字切,“手机壳”变成“手”“机”“壳”,搜“手机”能匹配,但语义已经散了。生产环境一般安装 IK 插件,写入用 ik_max_word 细粒度切,查询用 ik_smart 粗粒度切,兼顾召回和性能。
curl -X POST "http://localhost:9200/_analyze" \
-H 'Content-Type: application/json' \
-d '{"analyzer":"ik_max_word","text":"Spring Boot 3.2 实战"}'
返回的 tokens 就是实际写入倒排索引的词。分词结果和预期不一致时,先调这个接口,不要直接去查数据。
建索引:mapping 要先想清楚
Elasticsearch 8.x 默认开启安全认证,本地用 curl 记得带上用户名密码或证书。下面是一个商品索引的简化 mapping:
PUT products
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"brand": { "type": "keyword" },
"price": { "type": "scaled_float", "scaling_factor": 100 },
"created_at": { "type": "date" }
}
}
}
text 会分词,适合全文检索;keyword 不分词,适合精确匹配、聚合和排序。商品名用 text,品牌用 keyword,价格用 scaled_float 避免浮点误差。字段类型一旦写入数据就不能直接修改,要改只能新建索引、reindex、再切别名,所以 mapping 要在建索引时定好。
写入和查询:
POST products/_doc/1
{
"name": "无线降噪耳机",
"brand": "Example",
"price": 899.00,
"created_at": "2024-07-01T10:00:00Z"
}
GET products/_search
{
"query": {
"match": { "name": "降噪耳机" }
}
}
match 会对查询词分词后再匹配,term 则拿原始值精确匹配。对 text 字段用 term 查“降噪耳机”,除非写入时分词器恰好把它切成一个词,否则查不到;对 keyword 字段用 match 做全文检索,也只会整串匹配。这两个混用是最常见的“搜不到数据”原因。
查询与相关性
实际搜索很少只用单个 match。常见结构是 bool 查询:must 参与算分,filter 只过滤不算分并且可以缓存,should 提高匹配度,minimum_should_match 控制至少命中几个词。商品搜索里,分类、价格区间、上下架状态放 filter,关键词放 must,品牌可以放 should 做加权。
分页要注意 from + size 默认上限是 10000。超过这个值会直接报错,不是性能慢的问题。深分页要用 search_after 配合 PIT(point in time),保证翻页期间索引不变;需要跳页的场景,应该重新设计交互,而不是硬翻。
常见坑
1. 用 term 查 text 字段
写入时“无线降噪耳机”被 IK 切成多个词,term 查整串自然没有结果。全文检索用 match,精确匹配用 keyword 字段加 term。
2. 动态 mapping 把类型推断错
第一条文档里 price 是整数,第二条写入 899.50 就可能报错;日期字符串格式不统一也会导致解析失败。生产索引应该显式定义 mapping,关闭不必要的动态字段,避免字段爆炸。
3. 中文分词器没装或没生效
analyzer 配置了 ik_max_word,但插件没安装,建索引时直接报错。升级 ES 版本时,IK 插件版本必须和 ES 主版本一致,否则节点起不来。
4. 写完立刻查不到
Elasticsearch 默认每秒 refresh 一次,写入成功后要等 refresh 才能被搜索到。测试里可以用 refresh=wait_for,业务上要接受这个延迟,不要把它当成数据丢失。
5. 分片和副本拍脑袋定
分片数在索引创建后不能改,副本可以改。单分片数据量建议控制在几十 GB 以内,分片过多会增加集群元数据和查询开销。小集群先按 1 到 3 个主分片起步,结合数据增长再调整。
排查步骤
先看集群和索引状态:
curl -u elastic:password "http://localhost:9200/_cluster/health?pretty"
curl -u elastic:password "http://localhost:9200/_cat/indices?v"
curl -u elastic:password "http://localhost:9200/products/_mapping?pretty"
搜不到数据时,按“分词 → mapping → 查询语句”的顺序排查:先用 _analyze 确认写入和查询的分词结果,再看字段是 text 还是 keyword,最后确认查询用的是 match 还是 term。慢查询可以用 profile: true 看每个阶段的耗时,但不要在生产对高并发查询长期开启。
小结
Elasticsearch 的核心是倒排索引,倒排索引的质量由分词和 mapping 决定。把字段类型、分词器、查询方式三件事对齐,大部分“搜不到、搜不准”的问题都能避免。它适合搜索和分析,不适合当主数据库,写入和事务能力都要弱于 MySQL。
评论