十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Elasticsearch从入门到实践:索引创建、数据写入与搜索排查

Elasticsearch从入门到实践:索引创建、数据写入与搜索排查 做搜索引擎的都知道Elasticsearch基本是绕不开的那一个。不管是给站内做商品搜索、日志检索还是给业务系统做数据聚合分析ES靠一套HTTP JSON接口就能把海量数据的存储、索引和检索全部包揽下来。很多人第一次接触ES时容易被一堆名词弄晕——索引、文档、分片、副本、mapping、分词器看着好像都能理解真到自己上手建索引、写数据就各种抓瞎为什么我的中文搜不出来为什么字段类型选错了改都改不掉为什么写入的数据查不到这篇文章我就从最基础的“索引创建、数据插入、请求示例”讲起把ES日常使用中最常碰到的那几个环节完整过一遍。适合刚入门的开发者也适合部署完ES之后还没理清读写流程的运维同学。我会把每个请求都贴出来并解释清楚保证你可以照着敲一遍就通。1. 先把关键概念搞明白索引、文档、倒排索引1.1 ES的“索引”和MySQL的“索引”根本不是一回事很多从MySQL转过来的用户第一次用ES都会在这里栽跟头。MySQL里的索引是表结构上的一个辅助对象用来加速查询的B树而ES里的索引是一个完整的逻辑存储空间它包含了一系列的配置信息settings、字段结构定义mapping以及真正落盘的数据shard也就是分片。打个比方如果你把MySQL里的“数据库”和“表”合并成一个东西那大概就是ES里的索引。在7.0版本之前ES里还有个type概念一个索引下面可以分多个type结果把数据结构搞得不清不楚官方后来干脆把type给废掉了。现在一个索引基本就等价于一张二维表但它存的不是行而是JSON文档。每个文档都有一个_id相当于主键。文档里的字段类型靠mapping来定义比如字符串字段要选text还是keyword数字字段用什么精度日期字段的格式是什么这些都直接影响检索和聚合的结果。所以创建索引这件事的本质其实就是先规划好数据的存储结构再加上分片和副本策略。1.2 倒排索引为什么ES搜索能这么快ES能成为搜索引擎的老大哥核心武器就是倒排索引。传统的正排索引是按“文档→关键词”的方向存储比如一篇文档里有哪些词查询时得从头扫描所有文档才能知道谁命中了关键词。倒排索引反过来它维护的是“关键词→文档ID列表”的映射几乎每个词都对应一串包含它的文档ID查询时只要找到这个词就能瞬间定位到所有相关文档。具体到实现上ES会对每个text字段做分词再把分词结果写进倒排表。你搜“搜索引擎”的时候ES会先把“搜索引擎”拆成“搜索”“引擎”具体拆法取决于分词器再拿着这些词去倒排表里找。这也是为什么ES对中文搜索的效果高度依赖分词器——标准分词器处理中文时基本是一整句话当做一个词搜起来非常痛苦生产环境基本都会换用IK分词器或者其他的中文分词方案。理解了这点你就能明白为什么ES适合搜索而不适合做事务型存储它为了查询效率牺牲了复杂事务能力和强一致性但反过来你给ES丢几千万条数据进去它依然能在几百毫秒内把结果给你捞出来这种能力在传统关系型数据库里是做不到的。2. 动手之前环境准备与启动2.1 Windows和Linux下的启动方式Elasticsearch是Java写的东西所以环境里得有JDK。好消息是ES在8.x版本之后内置了捆绑的JDK你不需要自己再折腾JAVA_HOME只要把安装包解压出来就能用。Windows下直接双击bin/elasticsearch.batLinux下执行bin/elasticsearch等几秒钟看到started字样就代表启动成功了。Linux上有一个必须注意的坑ES拒绝用root账号启动。你如果用root执行启动命令控制台上会直接报can not run elasticsearch as root。这不是bug是安全策略ES要求使用非root用户运行。生产环境建议单独建一个用户比如adduser esuser然后把ES目录的属主改成这个用户再切过去启动。另外如果你准备部署集群或多节点还需要把elasticsearch.yml里的network.host从默认的127.0.0.1改成内网地址同时配置discovery.seed_hosts和cluster.initial_master_nodes否则节点之间互相找不到。启动完成后验证方式很简单浏览器或者curl访问http://localhost:9200返回一段包含cluster_name、version等信息的JSON就说明ES已经起来了。2.2 安装IK分词器和准备好用的调试工具中文场景下IK分词器几乎成了标配。安装它没有复杂的流程直接从GitHub release页下载和ES版本严格对应的zip包解压后放到ES安装目录的plugins/ik文件夹下重启ES就生效了。注意版本必须严格一致ES 8.15的版本就对应IK 8.15.x的插件包装错版本的话ES会因为插件校验失败直接拒绝启动。调试ES请求我最推荐Kibana的Dev Tools控制台。它会自动补全DSL语法还带历史记录比在终端里敲curl舒服太多了。如果你不想装KibanaPostman也可以但注意ES部分接口对Content-Type有严格要求比如application/json没设对ES会直接返回Content-Type header [application/x-www-form-urlencoded] is not supported之类的错误。还有一点8.x版本默认开启了安全认证首次启动会生成一个随机密码让你配置。如果你只是在本地学习使用想省去这层麻烦可以在elasticsearch.yml里关闭安全模块把xpack.security.enabled设为false同时把自带的TLS加密也关掉也就是把xpack.security.transport.ssl.enabled设为false然后重启。当然生产环境千万别这么干这是自己学习的偷懒办法。3. 索引创建一次完整的索引设计3.1 先设计字段类型再写创建请求我见过一句话总结得很准确ES的mapping是设计写在前面后悔留给后面。因为字段类型一旦写入是不能直接修改的想改只能重建索引。所以创建索引之前把业务字段捋清楚特别重要。假设我现在要做一个简单的博客文章搜索功能包含文章标题、正文内容、发布时间、作者ID、标签列表、浏览量这几个字段。跟MySQL建表类似ES每个字段都要选一个类型核心的对应关系如下标题和正文需要被全文检索用text类型并配置中文分词器发布时间用date类型同时指定格式比如yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis多个格式用双竖线分隔作者ID不需要分词用keyword标签列表也是keyword的数组ES原生支持数组存储浏览量用integer或long后续可能要排序和聚合字段类型选错的典型后果是你把一个keyword字段硬拿来全文检索结果发现怎么搜都搜不到或者你把数字存成了text聚合统计时报Text fields are not optimised for operations that require per-document field data。3.2 创建索引的请求示例与参数说明创建索引用的是PUT请求路径直接写索引名请求体里带上settings和mappings两块配置。看一个具体示例PUT /blog_article { settings: { number_of_shards: 3, number_of_replicas: 1, refresh_interval: 5s }, mappings: { properties: { title: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart }, content: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart }, publish_time: { type: date, format: yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis }, author_id: { type: keyword }, tags: { type: keyword }, views: { type: integer } } } }这部分有几个关键点要解释清楚。第一number_of_shards是分片数它决定了索引的数据在物理上被切成了几块。分片数在索引创建后就没法修改了因为ES是根据_id的哈希值来路由文档到分片的改分片数意味着所有数据的存放位置都要重算。所以规划分片数时不要太随意也绝不要为了图以后扩容方便就上来设个几百个分片。ES官方建议单个分片容量控制在30GB到50GB之间比如预估数据总量300GB设置6到10个分片是合理的。单分片性能有上限分片过多又会导致集群元数据膨胀、查询聚合的开销变大。第二number_of_replicas是副本数默认1。副本既保证数据高可用又分担读请求压力。和分片数不同副本数在索引创建后可以动态调整不需要重建索引。第三refresh_interval决定了索引的“可见性”刷新周期。这后面讲数据插入的时候会细说但配置成5s或10s对写入频繁的场景是更稳的选择。analyzer和search_analyzer这里我分别配置了ik_max_word和ik_smart。ik_max_word做最细粒度拆分“中华人民共和国”会被拆成“中华人民共和国”“中华人民”“中华”“华人”“人民共和国”“人民”“共和国”等多个词适合索引阶段提高召回率ik_smart做粗粒度拆分适合查询阶段提高精准率。这是比较经典的组合方式。3.3 mapping设计中的常见坑第一不要对keyword字段做全文检索。keyword不会分词存储时是一个完整字符串你拿一个长句子去match搜索怎么也匹配不上只有用term做精确匹配才会命中。第二text字段默认不能用于聚合、排序和脚本操作。ES在聚合时对分词后的text字段无能为力会直接报Fielddata is disabled on text fields by default。你需要聚合的字段一定要用keyword类型或者建一个keyword类型的子字段。第三动态映射是个双刃剑。ES默认开着动态映射你插入一个索引里没有定义过的字段时它会根据JSON值自动推断字段类型并写进mapping。这在开发期很省事但在生产环境特别容易出问题。比如你第一次插入的age字段是个数字ES建成了long后来有个文档把age传成了字符串写入直接报错。另一个更隐蔽的问题是日志类数据字段特别多动态映射会产生大量字段导致mapping膨胀、内存飙升。所以生产环境我建议要么关掉dynamic要么把dynamic设为strict只允许显式定义的字段写入至少也要对关键索引做严格的mapping管控。4. 数据插入从单条到批量4.1 单条插入的两种写法索引建好了接下来就是往里面塞数据。ES插入单条文档有两条路。第一条是不指定ID让ES自动生成随机ID用POST请求POST /blog_article/_doc { title: Elasticsearch入门指南, content: 本文介绍Elasticsearch的基础概念和使用方法……, publish_time: 2024-06-01 10:30:00, author_id: a1024, tags: [Elasticsearch, 搜索], views: 321 }第二条是指定业务ID用PUT请求或POST请求加在路径上PUT /blog_article/_doc/1001 { title: Elasticsearch入门指南, content: 本文介绍Elasticsearch的基础概念和使用方法……, publish_time: 2024-06-01 10:30:00, author_id: a1024, tags: [Elasticsearch, 搜索], views: 321 }两种写法怎么选如果你有自己的数据库主键强烈建议把它作为ES的_id。这样后续做增量同步时用同样的ID重复写入不会产生重复文档而是覆盖更新。同时文档路由也依赖_id固定的ID会让数据分布更可控。让ES自动生成随机ID更适合大量导入且没有业务主键的日志型数据字符串ID比自增数字ID的分布更均匀能避免热点分片。插入成功后的返回结果大概是{ _index: blog_article, _id: 1001, _version: 1, result: created, _shards: { total: 2, successful: 1, failed: 0 } }注意看_shardstotal是副本加主分片的总数successful表示成功写入的分片数。这里total为2、successful为1是因为我设置了1个副本但单机环境下副本无法分配到其他节点所以副本分片是未分配的unassigned状态只有主分片成功写入。这在单机学习中很正常无需紧张。但如果你在集群中看到successful长期小于total就要排查副本是否分配失败了。4.2 批量插入bulk接口的使用姿势单条插入方便理解但生产环境往ES灌数据肯定不能一条一条地发请求。每发一次HTTP请求都有网络开销而ES写入单个文档的耗时其实非常短真正慢的是请求传输。批量写入能把这些开销摊薄写入吞吐量能差出几个数量级。ES的批量接口是_bulk请求格式比较特殊要求每两行一组一行是操作元信息一行是文档数据。POST /blog_article/_bulk {index:{_id:1002}} {title:ES批量写入实践,content:bulk接口使用示例……,publish_time:2024-06-02 09:00:00,author_id:a1026,tags:[ES],views:120} {index:{_id:1003}} {title:搜索质量优化,content:查询调优与排序策略……,publish_time:2024-06-03 11:20:00,author_id:a1024,tags:[搜索],views:88}这个格式如果手写很容易多一个逗号或者少一个换行报错又不明显。实际项目中一般由客户端SDK在内存中拼装好NDJSON数据再一次性提交日志采集工具比如Logstash、Filebeat也是用这种方式推送的。有人会问bulk一次到底该提交多少条太多会占用过大内存太少又起不到批量效果。常规经验值是单次bulk请求体控制在5MB到15MB之间文档数在1000到5000条左右。具体最优值得靠压测去找你可以用curl反复调整这个参数观察ES监控面板里写入延迟和拒绝率的变化。除了index操作bulk里还能混用create、update、delete。区别在于create在文档已存在时会报版本冲突index则直接覆盖适合幂等写入的场景。4.3 写入后为什么可能查不到refresh机制详解很多初学者第一次往ES写数据紧接着就去搜索刚才的内容结果搜不到第一反应就是出bug了。其实这是ES的refresh机制在起作用。ES写入数据时文档不会立刻落盘到不可变的磁盘段segment中而是先进入内存缓冲区和translog日志。refresh操作会把内存缓冲区的数据生成一个新的segment让这部分数据变得可搜索。默认的refresh_interval是1秒也就是说写入成功后最多等1秒就能搜到。你可以在搜索请求上加refreshtrue参数强制先刷新再查询POST /blog_article/_doc/1004?refreshtrue { title: 强制刷新测试, content: 这个文档写入后立即可见, publish_time: 2024-06-04 00:00:00, author_id: a1030, tags: [测试], views: 1 }但请注意refreshtrue并不适合生产环境高频写入时使用。每次写入都立即刷新会让内存中的小segment数量暴涨触发频繁的segment合并写性能和磁盘IO都会很难看。常规做法是实时性要求高的业务保持默认的1秒刷新后台周期性批量导数据的场景反而可以把refresh_interval调大甚至导入期间临时设为-1关闭刷新等数据全部导入完再恢复刷新这样能大幅加快写入速度。批量导入期间关闭刷新后内存缓冲区的数据不会被刷成可搜索段但也不会丢数据在translog里有完整记录。导入完成后再执行一次POST /blog_article/_refresh强制刷新所有数据就能被搜到了。4.4 更新与删除文档的原理更新和删除看着是两类操作底层其实是同一个机制。ES的segment是不可变的所以不存在“改某条数据”这种物理操作。更新一个文档真实发生的事情是新版本文档先写入内存缓冲区旧版本文档不会被立刻物理删除而是被打上一个删除标记等后台segment合并时才会真正清理。POST /blog_article/_update/1001 { doc: { views: 999 } }上面这个请求会做一次部分字段更新只修改views字段其他字段不动。如果字段不存在就新增如果文档本身不存在会报document_missing_exception。如果想在文档不存在时自动创建可以在请求体里把doc_as_upsert设为true。删除就更直接了DELETE /blog_article/_doc/1001删除后你会看到result字段变成deleted。如果删除一个不存在的文档result会是not_found但HTTP状态码依然返回200这点和MySQL里DELETE影响行数为0不同ES不会把“没删到”当作错误处理代码判断时要留意。因为更新删除都是标记位机制频繁更新删除后再大量写入会导致磁盘上存在很多“虚”数据查询时会扫描到这些带删除标记的文档再过滤掉影响查询性能。常规维护手段是定期执行POST /blog_article/_forcemerge强制合并把segment里的删除标记彻底清掉。5. 用一次搜索验证索引和数据5.1 最常用的search请求示例数据插进去之后最直接的事就是搜一下。看最典型的match查询POST /blog_article/_search { query: { match: { title: Elasticsearch } } }返回结果的hits.total会告诉你命中了多少文档hits.hits里是命中的文档列表每个文档都带着_score相关度分数。相关度分数是ES按照词频、逆文档频率等算法算出来的默认按分数倒序排列。另一个常用查询是term它不会对搜索词做分词而是拿整个词去倒排索引里精确匹配。所以term查询适合keyword字段match查询适合text字段。很多人一开始搞混这两个记住一条经验查keyword用term查text用match基本不会出错。5.2 查看索引的mapping、settings和健康状态有时候你需要确认之前建的索引结构或者排查为什么某个字段行为不对。几个最常用的只读接口要记住。查看索引字段定义GET /blog_article/_mapping查看索引配置GET /blog_article/_settings一次性查看集群里所有索引的健康状态、分片数和文档数可以用cat接口GET /_cat/indices?v返回结果里会包含health列可能是green、yellow或red。单机环境下因为你只跑了1个ES节点副本分片没有可用的第二个节点来分配所以状态通常是yellow这不影响正常读写。但你需要在心里清楚yellow意味着副本处于不完整状态此时如果主分片所在节点宕机数据就存在丢失风险。想要真正达到green状态至少需要起两个ES节点让副本分片分配到别的机器上。另外查看所有分片落在哪些节点、为什么未分配可以用GET /_cat/shards?v每行会显示索引名、分片编号、是主分片还是副本、落在哪个节点、存储了多少文档。6. 常见问题与排查技巧实录6.1 集群状态变红索引显示不可读写集群变红表示有主分片缺失这意味着部分数据彻底不可用了。最普遍的原因是节点重启后磁盘空间不足分片无法重新分配。排查顺序建议这样走第一步GET /_cat/indices?v看哪个索引是red第二步GET /_cat/shards?v看缺失的是哪个分片第三步GET /_cluster/allocation/explain?pretty这个接口会直接用大白话告诉你为什么分片分配不上去比如the node is above the disk water mark之类的提示。如果是磁盘水位问题导致的变红处理和防范策略有这么几条及时清理数据或扩盘调大节点磁盘水位阈值不推荐属于临时手段同时给索引配置只读阈值以内的数据生命周期策略。还有一类比较隐蔽的情况同一集群里有不同版本的ES节点老节点无法读取新节点写入的segment格式分配也会失败这种情况需要统一集群版本。6.2 连接拒绝9200端口通不通ES的HTTP服务默认监听9200端口节点间通信走9300端口。你在浏览器访问9200觉得没问题但Java客户端或者应用容器连不上优先确认几件事ES所在机器的防火墙是否放行了9200ES的network.host是否还是localhost只有本机能连外部服务器当然连不上客户端和ES版本是否匹配Java客户端Maven坐标的版本必须和ES服务端主版本一致比如服务端8.15客户端必须用8.15.x用7.x连8.x会直接报版本不兼容异常。针对自身学习场景如果不想管太多网络安全配置可以保持network.host: 127.0.0.1只在本机curl访问即可。想让自己项目远程调试时再改同时配上ES自带的认证模块或者单独限制访问IP。6.3 写入性能明明不高到底卡在哪ES写入慢最常见的原因第一个是refresh太频繁第二个是段合并太频繁第三个是bulk批次太小第四个是磁盘性能不够。如果你的写入目标量级是每秒几万条那么把index.refresh_interval调成10s到30s甚至大批量导入时暂时设成-1一次bulk的条数调大到几千条把请求体撑到10MB左右观察监控中的段合并指标如果merges线程长期繁忙适当调大index.merge.scheduler.max_thread_count机械硬盘建议调低到1避免IO饱和SSD可以调高到4日志型数据写入前设置index.number_of_replicas: 0等数据写完后再把副本调回来整套组合拳打下来写入吞吐量翻几倍很正常。6.4 text字段聚合报错怎么办直接抄一个常见报错场景你写了这样的请求POST /blog_article/_search { size: 0, aggs: { by_tags: { terms: { field: tags } } } }如果tags被定义成了text类型聚合时会报错Fielddata is disabled on text fields by default。原因前面讲过text字段是分词后的词条数组拿它做terms聚合得到的是拆分后的词频不是原始字段的完整值。解决方案有三个一是改mapping字段类型为keyword二是保留text主字段的同时增加一个keyword子字段比如tags: { type: text, fields: { keyword: { type: keyword } } }聚合时用tags.keyword三是对已有text字段动态开启fielddata但这个是官方明确不建议的会大量占用堆内存属于临时救火方案。6.5 分片数设置不合理索引卡死最后提醒一个比较典型的新手陷阱——把分片数设得过多。比如数据量比较小的业务索引上来就设了30个分片每个分片只有几百MB数据。最终表现是集群里到处是分片读写慢、内存紧张、集群状态不稳定。分片数量规划的经验公式可以这样估算一个分片容量极限大约50GB但达到30GB后查询性能就开始下降。目标分片数 预计总数据量 / 30GB再乘以1.5左右的冗余系数。分片副本数默认1即可。如果规划后发现分片太少或太多因为创建后不能改分片数常用的解决方案是新建一个分片数合理的新索引用reindex接口把数据迁移过去再把索引别名切换到新索引上对业务做到平滑替换。提示索引别名是ES里非常实用的功能通过别名读写索引可以做到零停机切换。平时操作索引先用别名生产环境维护时就方便多了。写在后面的一点点建议这套流程走下来你其实已经有能力自己搭一套基础搜索服务了建索引、写数据、搜数据以及处理常见的读写问题。ES的入门曲线确实陡但最难的永远是前面这段概念混淆期。我的建议是不要急着记语法先把索引、映射、分片、倒排索引这几个底层概念吃透后面学查询DSL和集群运维都会顺很多。我个人在实际操作中还有一个体会学习ES一定要搭配Kibana一起用。它自带的Dev Tools能边敲边看响应还能自动补全比在终端里反复拼curl高效很多。另外新建索引的时候哪怕只是测试也养成写mapping的习惯别完全依赖动态映射。等数据多了再想回头改字段类型代价可不是一点半点。
返回列表