相关概念 1 Elasticsearch概述 1.1 Elasticsearch是什么 Elasticsearch(ES)是一个基于Apache的开源索引库Lucene而构建的 开源、分布式、具有RESTful接口的全文搜索引擎 , 还是一个 分布式文档数据库 .
ES可以轻松扩展数以百计的服务器(水平扩展), 用于存储和处理数据. 它可以在很短的时间内存储、搜索和分析海量数据, 通常被作为复杂搜索场景下的核心引擎.
由于Lucene提供的API操作起来非常繁琐, 需要编写大量的代码, Elasticsearch对Lucene进行了封装与优化, 并提供了REST风格的操作接口, 开箱即用, 很大程度上方便了开发人员的使用.
1.2 Elasticsearch的优点
1、横向可扩展性: 作为大型分布式集群, 很容易就能扩展新的服务器到ES集群中; 也可运行在单机上作为轻量级搜索引擎使用. 2、更丰富的功能: 与传统关系型数据库相比, ES提供了全文检索、同义词处理、相关度排名、复杂数据分析、海量数据的近实时处理等功能. 3、分片机制提供更好地分布性: 同一个索引被分为多个分片(Shard), 利用分而治之的思想提升处理效率. 4、高可用: 提供副本(Replica)机制, 一个分片可以设置多个副本, 即使在某些服务器宕机后, 集群仍能正常工作. 5、开箱即用: 提供简单易用的API, 服务的搭建、部署和使用都很容易操作.
1.3 Elasticsearch的相关产品
1、Beats: 是一个代理, 将不同类型的数据发送到Elasticsearch中. 2、Shield: 提供基于角色的访问控制与审计, 加密通信、认证保护整个ES的数据, 为ES带来企业级的安全性 — 收费产品. 3、Watcher: 是ES的警报和通知工具, 检测ES的状态, 在异常发生时进行提醒 — 收费产品. 4、Marvel: 是ES的管理和监控工具, 检测ES集群的索引和节点的活动 — 收费产品.
1.4 Elasticsearch的使用场景
1、维基百科(类似百度百科): 全文检索, 高亮, 搜索推荐; 2、The Guardian(新闻网站): 用户行为日志(点击, 浏览, 收藏, 评论) + 社交网络数据(对某某新闻的相关看法), 数据分析(将公众对文章的反馈提交至文章作者); 3、Stack Overflow(IT技术论坛): 全文检索, 搜索相关问题和答案; 4、GitHub(开源代码管理), 搜索管理其托管的上千亿行代码; 5、日志数据分析: ELK技术栈(Elasticsearch + Logstash + Kibana)对日志数据进行采集&分析; 6、商品价格监控网站: 用户设定某商品的价格阈值, 当价格低于该阈值时, 向用户推送降价消息; 7、BI系统(Business Intelligence, 商业智能): 分析某区域最近3年的用户消费额的趋势、用户群体的组成结构等; 8、其他应用: 电商、招聘、门户等网站的内部搜索服务, IT系统(OA, CRM, ERP等)的内部搜索服务、数据分析(ES的又一热门使用场景).
2 Elasticsearch的功能概述 2.1 分布式的搜索引擎和数据分析引擎
1、搜索: 谷歌, 百度, 各大网站的站内搜索(如淘宝网的商品搜索), IT系统的检索(如OA内部的信息查询); 2、数据分析: 电商网站中, 对形如最近30天IT书籍销量排名前10的商家有哪些; 新闻网站中: 最近7天访问量排名Top 10的新闻是哪些……
总结: Elasticsearch适用于 在较大用户量、较高访问量的分布式系统中, 对数据进行搜索与分析 .
2.2 全文检索 结构化检索 数据分析
1、全文检索: 搜索商品名称包含”编程思想”的商品: select * from products where product_name like "%编程思想%"; 2、结构化检索: 搜索商品分类为”计算机科学”的所有商品: select * from products where category_name='计算机科学'; 3、数据分析: 分析每一种商品分类下有多少件商品: select category_name, count(*) from products group by category_name; 4、其他个性化搜索需求: 部分匹配、自动完成(输入联想)、搜索纠错、搜索推荐……
2.3 海量数据的近实时处理
1、分布式: Elasticsearch可将海量数据自动分发到多台服务器上, 进行存储和检索; 2、海量数据的处理: 分布式系统构建完成后, 就可通过大规模服务器集群去存储和检索数据 —— 服务器有了处理海量数据的能力; 3、基于Elasticsearch的搜索和分析服务可达到秒级响应.
关于近实时:
非近实时 : 检索x个数据要花费很长时间(这就不是近实时, 而是离线批处理, batch-processing).实时 : 数据的处理与响应都是立即呈现的, 几乎没有间隔, 这在大数据应用场景下是很难达到的要求.近实时(near real-time, NRT): 对海量数据进行搜索和分析的响应耗时控制在秒级以内, 方可称为近实时.
3 Elasticsearch的架构 结合Elasticsearch架构图进行相关概念的介绍:
3.1 gateway - 门户、网关
ES索引的持久化存储方式, 也就是各类文件系统. 默认是先把索引存放到内存中, 当内存满了时再持久化到硬盘. ES集群重新启动时就会从gateway中读取索引数据. ES支持多种类型的gateway: 本地文件系统(默认), 分布式文件系统, Hadoop的HDFS, 以及Amazon的S3云存储服务等.
3.2 Lucene - 分布式Lucene目录
Gateway的上层是一个分布式的Lucene框架, Lucene之上是ES的模块, 包括:索引模块、搜索模块、映射解析模块等.
3.2 Discovery - 发现服务
Discovery是ES的节点发现模块, 要组成集群, 不同的节点之间就需要进行通信. ES集群内部需要选举master节点, 这些工作都是由Discovery模块完成的. ES支持多种发现机制, 如Zen(默认)、EC2、Gce、Azure等. ES是一个基于p2p的系统: 先通过广播寻找存在的节点, 再通过多播协议进行节点之间的通信, 同时也支持点对点的交互. 5.x版本关闭了广播, 要开启就需要开发人员自定义.
3.3 Scripting - 脚本
ES支持在查询语句中插入JavaScript、Python等脚本 —— 由Scripting模块负责解析这些脚本. 使用脚本语句时查询性能可能会稍有降低.
3.4 3rd Plugins - 三方插件 ES对三方插件的支持非常友好, 因此其开源生态的构建也越发活跃、成熟.
3.5 Transport - 通信模块
ES内部节点或集群与客户端的交互方式, 节点间通信端口默认为: 9300 - 9400. ES默认使用TCP协议进行交互, 同时也支持HTTP协议(JSON格式)、Thrift、Servlet、Memcached、ZeroMQ等的传输协议(通过插件方式集成).
3.6 JMX - Java管理框架 ES通过Java管理框架JMX来管理其应用.
3.7 RESTful style API - 与集群进行交互 ES最上层是提供给用户的接口, 可以通过RESTful接口与ES集群进行交互.
4 Elasticsearch索引相关概念 4.1 term(索引词)
在ES中, 索引词(term)是一个能够被索引的精确值, 可以通过term query进行准确搜索. 比如: foo、Foo、FOO都是不同的索引词.
4.2 text(文本)
文本是一段普通的非结构化文字, 通常文本会被分析成多个Term, 存储在ES的索引库中. 文本字段一般需要先分析再存储, 查询文本中的关键词时, 需要根据搜索条件搜索出原文本.
4.3 analysis(分析)
分析是将文本转换为索引词的过程, 分析的结果依赖于分词器. 比如: FOO BAR、Foo-Bar和foo bar可能会被分析成相同的索引词foo和bar, 然后被存储到ES的索引库中. 当通过FoO:bAr进行全文搜索的时候, 搜索引擎根据匹配计算也能在索引库中查找到相关的内容.
4.4 cluster(集群)
集群由一至多个节点组成, 对外提供索引和搜索服务. 一个节点只能加入到一个集群中.集群中有且只能有一个节点会被选举为主节点 —— 主从节点是集群内部的说法, 对用户是透明的; ES做到了去中心化: 访问任一节点等价于访问整个集群.同一网络中, 每个ES集群都要有唯一的名称用于区分, 默认的集群名称为”elasticsearch”. 水平扩展时, 只需要将新增节点的集群名称设置为要扩容的集群名称, 该节点就会自动加入集群中.
4.5 node(节点)
节点是逻辑上独立的服务, 是集群的一部分, 可以存储数据, 并参与集群的索引和检索功能. 节点也有唯一的名称, 用于集群的管理和通信, 节点名称在节点启动时自动分配一个随机的UUID的前7个字符 —— 当然可以自定义.如果有多个节点在运行, 默认情况下, 这些节点会自动组成一个名为Elasticsearch的集群. 如果只有一个节点在运行, 该节点就会组成只有一个节点的名为Elasticsearch的集群. 每个节点属于哪个集群是通过”集群名称”来决定的.
4.6 shard(分片)
单台机器(节点)无法存储大量的索引数据, ES可以把一个完整的索引分成多个分片, 分布到不同的节点上, 从而构成分布式索引. 每个分片都是一个Lucene实例, 也就是说每个分片底层都有一个单独的Lucene提供独立的索引和检索服务, 它们可以托管在集群的任一节点上. 单个Lucene中存储的索引文档最大值为 lucene-5843, 极限是2147483519(=integer.max_value - 128) 个文档. 可使用_cat/shards API 监控分片的大小.
(1)分片的好处:
允许水平切分/扩展集群容量; 可在多个分片上进行分布式的、并行的操作, 提高系统的性能和吞吐量.
(2)使用注意事项:
分片的数量只能在创建索引前指定, 创建索引后不能修改. 5.x 版本默认不能通过配置文件elasticsearch.yml定义分片个数.
4.7 replica(副本)
ES支持为每个Shard创建多个副本, 相当于索引数据的冗余备份.分片有Primary Shard(主分片)、Replica Shard(副本分片), 建立索引时, 系统会先将索引存储在主分片中, 然后再将主分片中的索引复制到不同的副本中.
(1) 副本的重要性:
1、 解决单点问题, 提高可用性和容错性: 某个节点失败时服务不受影响, 可以从副本中恢复;2、 提高查询效率和查询时的吞吐量: 搜索可以在所有的副本上并行执行, 提高了服务的并发量.
(2)使用注意事项:
主分片在建立索引时设置, 后期不能修改;
主分片和副本分片不能存储在同一个节点中 —— 无法保证高可用.5.x版本中, 默认主分片为5个, 默认副本分片数为1个, 即每个主分片各有1个副本分片(共5个副本分片); 可随时修改副本分片的数量. 默认情况下, 每个索引共有 5 primary shard + 5 * 1 replica shard = 10 shard. 集群中至少要有2个节点, 这是最少的高可用配置.
4.8 river(数据源)
从其他存储方式 (如数据库) 中同步数据到ES的方法, 它是以插件方式存在的一个ES服务, 通过读取river中的数据并把它索引到ES中. 官方的river有CouchDB、RabbitMQ、Twitter、Wikipedia等.
4.9 index(索引)
索引是具有相似结构的文档的集合, 等同于Solr中的集合, 比如可以有一个商品分类索引, 订单索引. 每个索引都要有唯一的名称, 名称要小写, 通过索引名称来执行索引、搜索、更新和删除等操作.一个集群中可以有任意多个索引, 只要保证名称不同即可.
4.10 type(类型)
type是index的逻辑分类, 在ES 6.x版本之前, 每个索引中可以定义一个或多个type, 而在6.X版本之后, 一个index中只能定义一个type . 一种type一般被定义为具有一组公共field的document, 比如对博客系统中的数据建立索引, 可以定义用户数据type, 博客数据type, 评论数据type, 也就是每个document都必须属于某一个具体的type, 也就是说每个document都有_type属性.
4.11 document(文档)
文档是存储在ES中的一个个JSON格式的字符串, 是ES索引中的最小数据单元, 由field(字段)构成. 一个document可以是一条商品分类数据, 一条订单数据, 例如:
1 2 3 4 5 6 7 8 9 book document { "book_id" : "1" , "book_name" : "Thinking in Java(Java 编程思想)" , "book_desc" : "Java学习者不得不看的经典书籍" , "book_price" : 108.00 , "category_id" : "5" }
4.12 mapping(映射)
类似于关系数据库中的Table结构, 每个index都有一个映射: 定义索引中每个字段的类型.所有文档在写进索引之前都会先进行分析, 如何对文本进行分词、哪些词条又会被过滤, 这类行为叫做映射(mapping). 映射可以提前定义, 也可以在第一次存储文档时自动识别. 一般由用户自己定义规则. 类似于Solr中schema.xml约束文件的作用.
4.13 field(字段)
字段可以是一个简单的值(如字符串、数字、日期), 也可以是一个数组, 还可以嵌套一个对象或多个对象. 字段类似于关系数据库中表数据的列, 每个字段都对应一个类型. 可以指定如何分析某一字段的值, 即对field指定分词器.
ES的索引中, 各概念的关系为: Field --> Document --> Type --> Index , 索引结构图如下:
与关系型数据库的对比:
Elasticsearch
RDBMS
Index(索引)
DataBase(数据库)
Type(类型)
Table(表)
Document(文档)
Row(行)
Field(字段)
Column(列)
Mapping(映射)
Schema(约束)
Everything is indexed(存储的都是索引)
Index(索引)
Query DSL(ES独特的查询语言)
SQL(结构化查询语言)
4.14 recovery(数据恢复)
又叫数据重新分布: 当有节点加入或退出时, ES会根据机器的负载对索引分片进行重新分配, 挂掉的节点重新启动时也会进行数据恢复. Kibana工具中通过 GET _cat/health?v, 就可以看到集群所处的状态.
主要配置 1 elasticsearch.yml(ES服务配置) 文件位置: ${ES_HOME}/config/elasticsearch.yml
1 2 3 4 5 6 7 8 9 10 11 # ======================== Elasticsearch Configuration ========================= # # NOTE: Elasticsearch comes with reasonable defaults for most settings. # Before you set out to tweak and tune the configuration, make sure you # understand what are you trying to accomplish and the consequences. # # The primary way of configuring a node is via this file. This template lists # the most important settings you may want to configure for a production cluster. # # Please consult the documentation for further information on configuration options: # https:
1.1 Cluster集群配置 1 2 3 4 5 # ---------------------------------- Cluster ----------------------------------- # # Use a descriptive name for your cluster: # 集群名称, 具有相同名称的节点才能组成一个逻辑集群, 默认是"elasticsearch" , 建议修改为与项目相关的名称. cluster.name: heal_es
1.2 Node节点配置 1 2 3 4 5 6 7 8 9 # ------------------------------------ Node ------------------------------------ # # Use a descriptive name for the node: # 本节点的名称, 同一集群中各个node的名称不允许重复. 不配置则系统默认分配. # node.name: node-1 # # Add custom attributes to the node: # 指定节点的部落属性 --- 一个比集群更大的范围, 即定义一些通用属性, 用于集群分配碎片时的过滤. #node.attr.rack: r1
Elasticsearch 6.6.0版本中取消了下述配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # 指定本节点是否有资格被选举为主节点, 默认是true . # ES集群中第一台机器被默认为master, 若此节点挂掉, 将重新选举master. # node.master=true # # 指定本节点在集群中是否存储数据, 默认为true . # node.data=true # # 配置文件中给出了三种配置高性能集群拓扑结构的模式, 如下: # 1. 如果想让节点不被选举为主节点, 只用来存储数据, 可作为负载器: # node.master: false # node.data: true # node.ingest: true 默认为true # # 2. 如果想让节点成为主节点, 且不存储任何数据, 并保有空闲资源,可作为协调器: # node.master: true # node.data: false # node.ingest: true # # 3. 如果想让节点既不作主节点, 又不作数据节点, 那么可将其作为搜索器, 从节点中获取数据, 生成搜索结果等: # node.master: false # node.data: false # node.ingest: true # # 4. 仅作为协调器: # node.master: false # node.data: false # node.ingest: false
1.3 Paths路径配置 1 2 3 4 5 6 7 8 9 # ----------------------------------- Paths ------------------------------------ # # Path to directory where to store the data (separate multiple locations by comma) : # 如果不配置下述两项, ES将在其主目录下创建. 建议将程序与数据分离配置, 方便系统迁移与升级. # 存放索引数据的目录 path.data: /data/elk-6.6 .0 /data # # Path to log files: 存放日志信息的目录 path.logs: /data/elk-6.6 .0 /logs
1.4 Memory内存配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # ----------------------------------- Memory ----------------------------------- # # Lock the memory on startup: # 启动时是否锁定ES运行所需的内存, 默认为false . # true : 锁定---防止ES使用Swap交换空间, 效率较高. 此时要确保当前用户具有memlock的权限. # false : 将使用Swap交换空间. # bootstrap.memory_lock: false bootstrap.system_call_filter: false # # 确保ES_HEAP_SIZE参数的值设置为系统可用内存的一半左右, 不要超过, 因为Lucene底层索引和检索还需要一定的内存. # Make sure that the heap size is set to about half the memory available # on the system and that the owner of the process is allowed to use this # limit. # # 当系统使用内存交换, ES的性能将变得很差 # Elasticsearch performs poorly when the system is swapping the memory.
1.5 Network网络配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # ---------------------------------- Network ----------------------------------- # # Set the bind address to a specific IP (IPv4 or IPv6) : # 对外网关的IP, 默认为localhost, 此时只能通过localhost或127.0 .0 .1 访问. # 设置为0.0 .0 .0 , 即可被外部所有网络访问, 如此但是不够安全, 可以指定某一个网段: network.host: 0.0 .0 .0 #network.host: 192.168 .0 .1 # # Set a custom port for HTTP: # 对外提供的HTTP访问端口, 默认为9200. 为提高安全性, 建议设置为其他值. # 可以指定一个值或一个区间, 如果是区间就会采取区间内第一个可用的端口. #http.port: 9200 # # For more information, consult the network module documentation.
Elasticsearch 6.6.0版本取消了transport.tcp.port的设置:
1 2 3 4 # # 集群节点之间通信的TCP传输端口. 下述Discovery部分的设置、ES的Java API 也通过此端口传输数据. 默认为9300. # 可以指定一个值或一个区间, 如果是区间就会采取区间内第一个可用的端口. # transport.tcp.port: 9300
1、 旧版本的Java API中客户端TransportClient 使用的是9300端口, 它执行的是序列化的Java请求, 性能较低, 在7.x版本中将过期, 8.x版本中将移除;2、 新版本推荐使用Java High Level REST Client客户端, 也就是RestHighLevelClient, 它执行HTTP请求, 使用的端口是http.port, 默认就是9200.
1.6 Discovery节点发现配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # --------------------------------- Discovery ---------------------------------- # # 启动新节点时, 通过IP列表进行节点发现, 组建集群 # Pass an initial list of hosts to perform discovery when new node is started: # The default list of hosts is ["127.0.0.1" , "[::1]" ] # '127.0.0.1' 是ipv4的回环地址, '[::1]' 是ipv6的回环地址 # # 1. x中默认使用多播(multicast)协议: 自动发现同一网段中的ES节点并组建集群; # 2. x中默认使用单播(unicast)协议: 要组建集群, 就需要在这里指定集群的节点信息 -- 安全高效, 但不够灵活. # # 默认已经关闭了自动发现节点的多播(组播)协议功能: # discovery.zen.ping.multicast.enabled: false # # 指定单播模式的IP(或hostname)列表: # 也可配置为: ["ip:port" , "ip:port" ]. 若port未设置, 将使用transport.tcp.port的值. #discovery.zen.ping.unicast.hosts: ["host1" , "host2" ] # # Prevent the "split brain" by configuring the majority of nodes (total number of master-eligible nodes / 2 + 1 ) : # 配置此参数以防止集群出现"脑裂现象" : 集群中出现2 个及以上master节点, 将导致数据不一致. # 官方推荐: 选举master的最少节点数 = (具有master资格的节点数 / 2 ) + 1 #discovery.zen.minimum_master_nodes: # # 设置集群中自动发现其他节点的连接超时时长, 默认是3 秒. # 在网络不佳时增加这个值, 会增加节点等待响应的时间, 可以减少误判. # discover.zen.ping.timeout: 3s # # For more information, consult the zen discovery module documentation.
1.7 Gateway网关配置 1 2 3 4 5 6 7 # ---------------------------------- Gateway ----------------------------------- # # Block initial recovery after a full cluster restart until N nodes are started: # 配置集群中的N个节点启动后, 才允许进行数据恢复处理. 默认是1. #gateway.recover_after_nodes: 3 # # For more information, consult the gateway module documentation.
1.8 Various其他配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # ---------------------------------- Various ----------------------------------- # # 在一台服务器上禁止启动多个es服务 # Disable starting multiple nodes on a single system: # # node.max_local_storage_nodes: 1 # # 设置是否可以通过正则或者_all删除或者关闭索引库,默认true 表示必须需要显式指定索引库名称 # # Require explicit names when deleting indices: #action.destructive_requires_name: true # 是否压缩TCP传输的数据, 默认是false : # transport.tcp.compress: false # 是否使用HTTP协议对外提供服务, 默认是true : # http.cors.enabled: true # http传输内容的最大容量, 默认是100MB: # http.max_content_length: 100mb
在2.x版本的配置文件中, 存在 Index 配置项, 可配置包括分片数、副本分片数在内的配置. 在5.x版本中, 不支持在配置文件中设置此类配置项了, 请注意此区别.
1 2 # 查看某个文件上次修改的内容: grep '^[a-z]' /data/elk-6.6 .0 /es-node/config/elasticsearch.yml
2 jvm.options(JVM参数配置) 文件位置: ${ES_HOME}/config/jvm.options
关于JVM常见参数的配置, 可参考博主文章:
对Tomcat 8.0进行JVM层面的优化(基于Oracle JDK 8)
关于JVM的垃圾回收(GC) 这可能是你想了解的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 ## JVM configuration ################################################################ ## IMPORTANT: JVM heap size ################################################################ ## ## You should always set the min and max JVM heap ## size to the same value. For example, to set ## the heap to 4 GB, set: ## ## -Xms4g ## -Xmx4g ## ## See https: ## for more information ## ################################################################ # Xms represents the initial size of total heap space # Xmx represents the maximum size of total heap space # 下述配置最好不要超过节点物理内存的50 %, 留出50 %供Lucene底层索引与检索使用 -Xms1g -Xmx1g ################################################################ ## Expert settings ################################################################ ## ## All settings below this section are considered ## expert settings. Don't tamper with them unless ## you understand what you are doing ## ################################################################ ## GC configuration -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly ## G1GC Configuration # NOTE: G1GC is only supported on JDK version 10 or later. # To use G1GC uncomment the lines below. # 10-:-XX:-UseConcMarkSweepGC # 10-:-XX:-UseCMSInitiatingOccupancyOnly # 10-:-XX:+UseG1GC # 10-:-XX:InitiatingHeapOccupancyPercent=75 ## DNS cache policy # cache ttl in seconds for positive DNS lookups noting that this overrides the # JDK security property networkaddress.cache.ttl; set to -1 to cache forever -Des.networkaddress.cache.ttl=60 # cache ttl in seconds for negative DNS lookups noting that this overrides the # JDK security property networkaddress.cache.negative ttl; set to -1 to cache # forever -Des.networkaddress.cache.negative.ttl=10 ## optimizations # pre-touch memory pages used by the JVM during initialization -XX:+AlwaysPreTouch ## basic # explicitly set the stack size (reduce to 320k on 32-bit client JVMs) -Xss1m # set to headless, just in case -Djava.awt.headless=true # ensure UTF-8 encoding by default (e.g. filenames) -Dfile.encoding=UTF-8 # use our provided JNA always versus the system one -Djna.nosys=true # turn off a JDK optimization that throws away stack traces for common # exceptions because stack traces are important for debugging -XX:-OmitStackTraceInFastThrow # flags to configure Netty -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true -Dio.netty.recycler.maxCapacityPerThread=0 # log4j 2 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Dlog4j.skipJansi=true ## heap dumps # generate a heap dump when an allocation from the Java heap fails # heap dumps are created in the working directory of the JVM -XX:+HeapDumpOnOutOfMemoryError # specify an alternative path for heap dumps; ensure the directory exists and has sufficient space # 生产环境中指定当发生OOM异常时, Heap的Dump Path, 默认是 -XX:HeapDumpPath=data -XX:HeapDumpPath=/var/lib/elasticsearch # specify an alternative path for JVM fatal error logs -XX:ErrorFile=logs/hs_err_pid%p.log ## JDK 8 GC logging 8:-XX:+PrintGCDetails 8:-XX:+PrintGCDateStamps 8:-XX:+PrintTenuringDistribution 8:-XX:+PrintGCApplicationStoppedTime 8:-Xloggc:logs/gc.log 8:-XX:+UseGCLogFileRotation 8:-XX:NumberOfGCLogFiles=32 8:-XX:GCLogFileSize=64m # JDK 9+ GC logging 9-:-Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m # due to internationalization enhancements in JDK 9 Elasticsearch need to set the provider to COMPAT otherwise # time/date parsing will break in an incompatible way for some date patterns and locals 9-:-Djava.locale.providers=COMPAT # temporary workaround for C2 bug with JDK 10 on hardware with AVX-512 10-:-XX:UseAVX=2
Elasticsearch 6.6.0中已经移除了下述优化配置:
1 2 3 4 5 6 7 8 # disable calls to System#gc -XX:+DisableExplicitGC # force the server VM (remove on 32 -bit client JVMs) -server # use old-style file permissions on JDK9 -Djdk.io.permissionsUseCanonicalPath=true
其他说明:
1 2 3 4 -Xmx2g 这种参数没有限制JVM版本 8 : -Xmx2g 限制JVM版本为8 8 -: -Xmx2g 限制JVM版本为8 及8 以上8 -10 : -Xmx2g 限制JVM版本在8 -10 之间
3 log4j2.properties(日志配置) 文件位置: ${ES_HOME}/config/log4j2.properties
一般使用默认日志配置即可.
简单查询
请求或返回
解释
GET bank/_search
检索 bank 下所有信息,包括 type 和 docs
GET bank/_search?q=*&sort=account_number:asc
请求参数方式检索
响应结果解释:
took
Elasticsearch执行搜索的时间(臺秒)
time_out
告诉我们搜索是否超时
_shards
告诉我们多少个分片被搜索了,以及统计了成功/失败的搜索分片
hits
搜索结果
hits.total
搜索结果
hits.hits
实际的搜索结果数组(默认为前10的文档)
sort
结果的排序 key (键) (没有则按 score 排序)
score 和 max_score
相关性得分和最高得分(全文检索用)
1 Query String Search(查询串检索)
这种方法通过HTTP请求的Query String携带查询参数, 因此得名.
适用于临时性的查询请求, 比如在终端检查基础信息:
1 2 检索name中包含Java的文档, 并按价格降序排序: curl -XGET 'http://localhost:9301/book_shop/it_book/_search?q=name:Java&sort=price:desc'
生产环境中很少使用, 因为请求参数都封装到Query String中, 难以构建复杂的查询.
(1)查询全部商品:
直接在浏览器的URL地址栏内输入搜索参数:
(2)查询的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 { "took" : 8 , "timed_out" : false , "_shards" : { "total" : 5 , "successful" : 5 , "skipped" : 0 , "failed" : 0 }, "hits" : { "total" : 3 , "max_score" : 1 , "hits" : [ { "_index" : "book_shop" , "_type" : "it_book" , "_id" : "2" , "_score" : 1 , "_source" : { "name" : "深入理解Java虚拟机:JVM高级特性与最佳实践" , "author" : "周志明" , "category" : "编程语言" , "desc" : "Java图书领域公认的经典著作" , "price" : 79 , "date" : "2013-10-01" , "publisher" : "机械工业出版社" , "tags" : [ "Java" , "虚拟机" , "最佳实践" ] } }, ] } }
(3)查询结果中的各个参数的含义:
1、 took: 此次检索耗费的时间, 单位是毫秒;
2、 timed_out: 是否超出规定的检索时间, 这里没有设置, 后续会讲解此参数;
3、 _shards: 被查询的index被分散成多个分片, 所以搜索请求会分发到所有的primary shard(或primary shard对应的某个replica shard)上, 这里显示各个分片是否查询成功的信息;
4、 hits: 命中的文档情况, 有如下参数:
total: 符合条件的文档总数, 即hit(命中)数;max_score: Lucene底层对检索到的文档的相关度的评分, 相关度越高, 说明越匹配, score的值也就越高.hits: 命中的所有document的详细数据.
2 Query DSL(ES特定语法检索)
DSL: Domain Specified Language, 特定领域的语言, 一般需要Kibana等工具配合操作.
这种方式把查询参数构建成JSON格式的数据, 并封装到HTTP请求的Request Body(请求体)中, 可以构建各类复杂的查询语法, 功能要比Query String Search强大很多.
(1)查询全部商品:
1 2 3 4 GET book_shop/it_book/_search { "query" : { "match_all" : {} } }
(2)查询name中包含Java的商品, 并按price降序排序:
1 2 3 4 5 6 7 8 9 10 11 GET book_shop/it_book/_search { "query" : { "match" : { "name" : "Java" } }, "sort" : [ { "price" : "desc" } ] }
(3)分页查询商品 - 每页显示1条, 显示第3页:
1 2 3 4 5 6 GET book_shop/it_book/_search { "query" : { "match_all" : {} }, "from" : 2 , "size" : 1 }
(4)只查询商品的名称和价格:
1 2 3 4 5 GET book_shop/it_book/_search { "query" : {"match_all" : {}}, "_source" : ["name" , "price" ] }
——上述各类语法可以组合使用, 具体使用方法后续会陆续介绍.
3 Query Filter(过滤检索)
过滤查询, 比如: 查询name中包含Java, 且price不大于80元的商品:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 GET book_shop/it_book/_search { "query" : { "bool" : { "must" : { "match" : {"name" : "Java" } }, "filter" : { "range" : { "price" : {"lte" : 80.0 } } } } } }
4 Full Text Search(全文检索) (1)查询描述信息desc中包含”Java图书”的文档, 只显示name和desc的值:
1 2 3 4 5 6 7 GET book_shop/it_book/_search { "query" : { "match" : {"desc" : "Java图书" } }, "_source" : ["name" , "desc" ] }
(2)查询结果中有2条数据符合要求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 { "took" : 2 , "timed_out" : false , "_shards" : { "total" : 5 , "successful" : 5 , "skipped" : 0 , "failed" : 0 }, "hits" : { "total" : 2 , "max_score" : 0.8630463 , "hits" : [ { "_index" : "book_shop" , "_type" : "it_book" , "_id" : "2" , "_score" : 0.8630463 , "_source" : { "name" : "深入理解Java虚拟机:JVM高级特性与最佳实践" , "desc" : "Java图书领域公认的经典著作" } }, { "_index" : "book_shop" , "_type" : "it_book" , "_id" : "1" , "_score" : 0.2876821 , "_source" : { "name" : "Java编程思想(第4版)" , "desc" : "Java学习必读经典,殿堂级著作!" } } ] } }
(3) 全文检索的过程 —— 对查询结果的说明:
Elasticsearch会对字段”desc”的内容进行分词, 并建立倒排索引.
也就是说, 这里会把 “Java图书” 分词为 “Java”、”图”、”书” 3个, 检索时将匹配desc中含有 “Java”、”图”、”书” 中任意一个分词的文档.
—— 对于中文分词, 可以通过IK分词器, 把”Java图书”分解为”Java”、”图书” 2个词, 参考博主的文章:ES XX - Elasticsearch中使用IK中文分词器.
5 Phrase Search(短语检索) Full Text Search会对检索文本作分词处理 , 然后从倒排索引中作匹配查询, 如果一个文档的对应field中存在任意一个分解后的词, 那么这个文档就算匹配检索条件.
Phrase Search不会对检索串进行分词处理 , 只有一个文档的对应field中包含与检索文本完全一致的内容, 该文档才算匹配检索条件, 也才能作为结果返回 —— 可以理解为全文检索场景下的部分精确匹配.
(1)精确查询desc中包含”Java图书”的文档:
1 2 3 4 5 6 7 8 9 GET book_shop/it_book/_search { "query" : { "match_phrase" : { "desc" : "Java图书" } }, "_source" : ["name" , "desc" ] }
(2)查询结果只有一条数据符合要求了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 { "took" : 2 , "timed_out" : false , "_shards" : { "total" : 5 , "successful" : 5 , "skipped" : 0 , "failed" : 0 }, "hits" : { "total" : 1 , "max_score" : 0.8630463 , "hits" : [ { "_index" : "book_shop" , "_type" : "it_book" , "_id" : "2" , "_score" : 0.8630463 , "_source" : { "name" : "深入理解Java虚拟机:JVM高级特性与最佳实践" , "desc" : "Java图书领域公认的经典著作" } } ] } }
6 Highlight Search(高亮检索) (1)分页查询desc中包含”Java图书”的文档, 页大小为1, 显示第1页, 并对搜索条件高亮处理:
1 2 3 4 5 6 7 8 9 10 11 12 GET book_shop/it_book/_search { "query" : { "match" : {"desc" : "Java图书" } }, "from" : 0 , "size" : 1 , "highlight" : { "fields" : {"desc" : {}} }, "_source" : ["name" , "desc" ] }
(2)查询结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 { "took" : 6 , "timed_out" : false , "_shards" : { "total" : 5 , "successful" : 5 , "skipped" : 0 , "failed" : 0 }, "hits" : { "total" : 2 , "max_score" : 0.8630463 , "hits" : [ { "_index" : "book_shop1" , "_type" : "it_book" , "_id" : "2" , "_score" : 0.8630463 , "_source" : { "name" : "深入理解Java虚拟机:JVM高级特性与最佳实践" , "desc" : "Java图书领域公认的经典著作" }, "highlight" : { "desc" : [ "<em>Java</em><em>图</em><em>书</em>领域公认的经典著作" ] } } ] } }
从上述结果的"<em>Java</em><em>图</em><em>书</em>也可以看出, ES底层对desc字段的值”Java图书”进行了分词处理:
说明: 本文的六种查询方法, 只是一个简单的入门, 详细使用方法会在后续的学习中逐一演示.
索引操作
Elasticsearch中的index相当于RDBMS(关系型数据库, 比如MySQL)中的DataBase. 本篇文章通过Kibana插件, 演示了ES的基础语法: 对ES中的index进行CRUD(增删改查)以及关闭、开启操作.
阅读须知:
在ES 6.x之前的版本中, 每个index中可以有多个type, 类似于MySQL中每个数据库可以有多张表, 可在ES 6.0开始, 每个index都只能有1个type. 本篇文章写作较早, 用的是ES 5.6版本, 因此有些操作可能出现不支持等问题, 还请读者查阅解决:-)
1 创建index(配置mapping[映射]) (1)创建语法:
1 2 3 4 5 6 7 8 9 10 PUT index { "settings" : { ... some settings ... }, "mappings" : { "type1" : { ... some mappings ... }, "type2" : { ... some mappings ... }, ... } }
如果不指定settings和mappings, 直接插入数据时, ES会根据要插入数据的类型, 自动创建相关配置 —— 功能强大, 但扩展性不够, 后续若有其他原因需要修改mappings, 会很困难.
—— 所以创建index时, 推荐手动指定settings和mappings的相关配置. 可以参考文章: ES XX - ES的mapping的设置.
(2)创建示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 PUT address { "settings" : { "number_of_shards" : 1 , "number_of_replicas" : 0 }, "mappings" : { "province" : { "properties" : { "name" : { "type" : "text" }, "area" : { "type" : "float" } } } } }
(3)创建结果:
1 2 3 4 5 { "acknowledged" : true , "shards_acknowledged" : true , "index" : "address" }
2 查看index (1)查看示例:
1 2 3 4 5 6 7 8 9 10 GET address GET * GET _all GET *index* GET address,shop GET address/_settings,_mappings
查看索引时, 若通过”,”分隔要返回的结果, Elasticsearch将抛出如下警告:
1 ! Deprecation: Requesting comma-separated features is deprecated and will be removed in 6.0 +, retrieve all features instead.
意为: Elasticsearch不推荐使用逗号分隔功能, 将在6.0+中删除. 建议不要使用”,”, 而是直接检索全部数据, 或检索某一项的结果.
在ES 6.6.0中将直接出现:
1 2 3 4 { "error" : "Incorrect HTTP method for uri [/address/_settings,_mappings?pretty] and method [GET], allowed: [POST]" , "status" : 405 }
换做POST请求时, 必须携带请求体, 仍然不支持.
(2)查看的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 { "address" : { "aliases" : {}, "mappings" : { "province" : { "properties" : { "area" : { "type" : "float" }, "name" : { "type" : "text" } } } }, "settings" : { "index" : { "creation_date" : "1542108754899" , "number_of_shards" : "1" , "number_of_replicas" : "0" , "uuid" : "MMpLNHzZR8K1k48rJplWVw" , "version" : { "created" : "6060099" }, "provided_name" : "address" } } } }
3 修改index 修改索引的示例:
1 2 3 4 PUT address/_settings { "number_of_replicas" : 1 }
说明:Elasticsearch中的分片数(number_of_shards)只能在创建索引时设置, 无论是否添加过数据, 都不支持修改.
这与文档的路由有关, 而Solr的SPLITSHARD可以算作动态修改分片的另一种思路: 只对某一路由范围内的进行拆分, 可以参考 管理SolrCloud集群 (创建集合、切割分片、更新配置) 第5节的内容. 关于修改ES的分片数, 应该有其他思路, 后期了解到再作研究整理.
4 删除index
删除索引需要指明索引名称、别名或通配符. Elasticsearch支持同时删除多个索引, 或使用_all或通配符*删除全部索引.
删除示例:
1 2 3 4 DELETE address DELETE index1,index2 DELETE index_* DELETE _all
为避免_all操作误删除全部索引, 可在配置文件elasticsearch.yml中作如下配置:
1 2 # 要求操作索引时必须指定索引的名称 action.destructive_requires_name: true
5 打开/关闭index (1)操作说明:
1、 可以打开一个已经打开/关闭的索引, 以最后一次操作为准;2、 可以关闭一个已经关闭/打开的索引, 以最后一次操作为准;3、 关闭的索引只能查看index的配置信息, 不能对内部的索引数据进行读写操作.
(2)操作示例:
1 2 3 POST address/_close POST address/_open
说明事项:
1、 使用_all或通配符操作索引, 都会受到配置文件中action.destructive_requires_name=true的限制.2、 关闭的索引会继续占用磁盘空间, 却又不能使用 —— 造成磁盘空间的浪费.3、 可以在配置文件中禁止使用关闭索引的功能: settingscluster.indices.close.enable=false, 默认为true(开启).
6 常见问题及解决方法 (1)查看不存在的索引时, 将抛出如下错误信息:
如果要查看的索引不存在, 比如GET addre, 就会抛出类似下面的异常信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "error" : { "root_cause" : [ { "type" : "index_not_found_exception" , "reason" : "no such index" , "resource.type" : "index_or_alias" , "resource.id" : "addre" , "index_uuid" : "_na_" , "index" : "addre" } ], "type" : "index_not_found_exception" , "reason" : "no such index" , "resource.type" : "index_or_alias" , "resource.id" : "addre" , "index_uuid" : "_na_" , "index" : "addre" }, "status" : 404 }
(2)在6.0之前的版本中, 如果修改已经关闭了的索引, 会抛出类似于下面的错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 { "error" : { "root_cause" : [ { "type" : "illegal_argument_exception" , "reason" : "Can't update [index.number_of_replicas] on closed indices [[address/MMpLNHzZR8K1k48rJplWVw]] - can leave index in an unopenable state" } ], "type" : "illegal_argument_exception" , "reason" : "Can't update [index.number_of_replicas] on closed indices [[address/MMpLNHzZR8K1k48rJplWVw]] - can leave index in an unopenable state" }, "status" : 400 }
在本篇博客演示所用的Elasticsearch 6.6.0版本中, 并不存在此异常信息.
document数据类型
说在前面: Elasticsearch中每个field都要精确对应一个数据类型. 本文的所有演示, 都是基于Elasticsearch 6.6.0进行的, 不同的版本可能存在API发生修改、不支持的情况, 还请注意.
1 核心数据类型 1.1 字符串类型 - string(不再支持) (1)使用示例:
1 2 3 4 5 6 7 8 9 10 11 PUT website { "mappings" : { "blog" : { "properties" : { "title" : {"type" : "string" }, "tags" : {"type" : "string" , "index" : "not_analyzed" } } } } }
(2)ES 5.6.10中的响应信息:
1 2 3 4 5 6 7 #! Deprecation: The [string] field is deprecated, please use [text] or [keyword] instead on [tags] #! Deprecation: The [string] field is deprecated, please use [text] or [keyword] instead on [title] { "acknowledged" : true , "shards_acknowledged" : true , "index" : "website" }
(3)ES 6.6.0中的响应信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "error" : { "root_cause" : [ { "type" : "mapper_parsing_exception" , "reason" : "No handler for type [string] declared on field [title]" } ], "type" : "mapper_parsing_exception" , "reason" : "Failed to parse mapping [blog]: No handler for type [string] declared on field [title]" , "caused_by" : { "type" : "mapper_parsing_exception" , "reason" : "No handler for type [string] declared on field [title]" } }, "status" : 400 }
可知string类型的field已经被移除了, 我们需要用text或keyword类型来代替string.
1.1.1 文本类型 - text 在Elasticsearch 5.4 版本开始, text取代了需要分词的string.
—— 当一个字段需要用于全文搜索(会被分词), 比如产品名称、产品描述信息, 就应该使用text类型.
text的内容会被分词, 可以设置是否需要存储: "index": "true|false". text类型的字段不能用于排序, 也很少用于聚合.
使用示例:
1 2 3 4 5 6 7 8 9 10 PUT website { "mappings" : { "blog" : { "properties" : { "summary" : {"type" : "text" , "index" : "true" } } } } }
1.1.2 关键字类型 - keyword 在Elasticsearch 5.4 版本开始, keyword取代了不需要分词的string.
—— 当一个字段需要按照精确值进行过滤、排序、聚合等操作时, 就应该使用keyword类型.
keyword的内容不会被分词, 可以设置是否需要存储: "index": "true|false".
使用示例:
1 2 3 4 5 6 7 8 9 10 PUT website { "mappings" : { "blog" : { "properties" : { "tags" : {"type" : "keyword" , "index" : "true" } } } } }
1.2 数字类型 - 8种 数字类型有如下分类:
类型
说明
byte
有符号的8位整数, 范围: [-128 ~ 127]
short
有符号的16位整数, 范围: [-32768 ~ 32767]
integer
有符号的32位整数, 范围: [(-2^{31}) ~ (2^{31})-1]
long
有符号的64位整数, 范围: [(-2^{63}) ~ (2^{63})-1]
float
32位单精度浮点数
double
64位双精度浮点数
half_float
16位半精度IEEE 754浮点类型
scaled_float
缩放类型的的浮点数, 比如price字段只需精确到分, 57.34缩放因子为100, 存储结果为5734
使用注意事项:
尽可能选择范围小的数据类型, 字段的长度越短, 索引和搜索的效率越高; 优先考虑使用带缩放因子的浮点类型.
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 PUT shop { "mappings" : { "book" : { "properties" : { "name" : {"type" : "text" }, "quantity" : {"type" : "integer" }, "price" : { "type" : "scaled_float" , "scaling_factor" : 100 } } } } }
1.3 日期类型 - date JSON没有日期数据类型, 所以在ES中, 日期可以是:
包含格式化日期的字符串, “2018-10-01”, 或”2018/10/01 12:10:30”.
代表时间毫秒数的长整型数字.
代表时间秒数的整数.
如果时区未指定, 日期将被转换为UTC格式, 但存储的却是长整型的毫秒值. 可以自定义日期格式, 若未指定, 则使用默认格式: strict_date_optional_time||epoch_millis
(1)使用日期格式示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 PUT website { "mappings" : { "blog" : { "properties" : { "pub_date" : {"type" : "date" } } } } } PUT website/blog/11 { "pub_date" : "2018-10-10" } PUT website/blog/12 { "pub_date" : "2018-10-10T12:00:00Z" } PUT website/blog/13 { "pub_date" : "1589584930103" }
(2)多种日期格式:
多个格式使用双竖线||分隔, 每个格式都会被依次尝试, 直到找到匹配的. 第一个格式用于将时间毫秒值转换为对应格式的字符串.
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 PUT website { "mappings" : { "blog" : { "properties" : { "date" : { "type" : "date" , "format" : "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" } } } } }
1.4 布尔类型 - boolean 可以接受表示真、假的字符串或数字:
真值: true, “true”, “on”, “yes”, “1”…
假值: false, “false”, “off”, “no”, “0”, “”(空字符串), 0.0, 0
1.5 二进制型 - binary 二进制类型是Base64编码字符串的二进制值, 不以默认的方式存储, 且不能被搜索. 有2个设置项:
(1) doc_values: 该字段是否需要存储到磁盘上, 方便以后用来排序、聚合或脚本查询. 接受true和false(默认); (2) store: 该字段的值是否要和_source分开存储、检索, 意思是除了_source中, 是否要单独再存储一份. 接受true或false(默认).
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 PUT website { "mappings" : { "blog" : { "properties" : { "blob" : {"type" : "binary" } } } } } PUT website/blog/1 { "title" : "Some binary blog" , "blob" : "hED903KSrA084fRiD5JLgY==" }
注意: Base64编码的二进制值不能嵌入换行符\n, 逗号(0x2c)等符号.
1.6 范围类型 - range range类型支持以下几种:
类型
范围
integer_range
(-2^{31}) ~ (2^{31}-1)
long_range
(-2^{63}) ~ (2^{63}-1)
float_range
32位单精度浮点型
double_range
64位双精度浮点型
date_range
64位整数, 毫秒计时
ip_range
IP值的范围, 支持IPV4和IPV6, 或者这两种同时存在
(1)添加映射:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 PUT company { "mappings" : { "department" : { "properties" : { "expected_number" : { "type" : "integer_range" }, "time_frame" : { "type" : "date_range" , "format" : "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" }, "ip_whitelist" : { "type" : "ip_range" } } } } }
(2)添加数据:
1 2 3 4 5 6 7 8 9 10 11 12 PUT company/department/1 { "expected_number" : { "gte" : 10 , "lte" : 20 }, "time_frame" : { "gte" : "2018-10-01 12:00:00" , "lte" : "2018-11-01" }, "ip_whitelist" : "192.168.0.0/16" }
(3)查询数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 GET company/department/_search { "query" : { "term" : { "expected_number" : { "value" : 12 } } } } GET company/department/_search { "query" : { "range" : { "time_frame" : { "gte" : "208-08-01" , "lte" : "2018-12-01" , "relation" : "within" } } } }
查询结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 { "took" : 26 , "timed_out" : false , "_shards" : { "total" : 5 , "successful" : 5 , "skipped" : 0 , "failed" : 0 }, "hits" : { "total" : 1 , "max_score" : 1.0 , "hits" : [ { "_index" : "company" , "_type" : "department" , "_id" : "1" , "_score" : 1.0 , "_source" : { "expected_number" : { "gte" : 10 , "lte" : 20 }, "time_frame" : { "gte" : "2018-10-01 12:00:00" , "lte" : "2018-11-01" }, "ip_whitelist" : "192.168.0.0/16" } } ] } }
2 复杂数据类型 2.1 数组类型 - array ES中没有专门的数组类型, 直接使用[]定义即可;
数组中所有的值必须是同一种数据类型, 不支持混合数据类型的数组 :
1、 字符串数组: [“one”, “two”];2、 整数数组: [1, 2];3、 由数组组成的数组: [1, [2, 3]], 等价于[1, 2, 3];4、 对象数组: [{“name”: “Tom”, “age”: 20}, {“name”: “Jerry”, “age”: 18}].
注意:
动态添加数据时, 数组中第一个值的类型决定整个数组的类型;
不支持混合数组类型, 比如[1, “abc”];
数组可以包含null值, 空数组[]会被当做missing field —— 没有值的字段.
2.2 对象类型 - object JSON文档是分层的: 文档可以包含内部对象, 内部对象也可以包含内部对象.
(1)添加示例:
1 2 3 4 5 6 7 8 PUT employee/developer/1 { "name" : "ma_shoufeng" , "address" : { "region" : "China" , "location" : {"province" : "GuangDong" , "city" : "GuangZhou" } } }
(2)存储方式:
1 2 3 4 5 6 { "name" : "ma_shoufeng" , "address.region" : "China" , "address.location.province" : "GuangDong" , "address.location.city" : "GuangZhou" }
(3)文档的映射结构类似为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 PUT employee { "mappings" : { "developer" : { "properties" : { "name" : { "type" : "text" , "index" : "true" }, "address" : { "properties" : { "region" : { "type" : "keyword" , "index" : "true" }, "location" : { "properties" : { "province" : { "type" : "keyword" , "index" : "true" }, "city" : { "type" : "keyword" , "index" : "true" } } } } } } } } }
2.3 嵌套类型 - nested 嵌套类型是对象数据类型的一个特例, 可以让array类型的对象被独立索引和搜索.
2.3.1 对象数组是如何存储的 1、 添加数据:
1 2 3 4 5 6 7 8 PUT game_of_thrones/role/1 { "group" : "stark" , "performer" : [ {"first" : "John" , "last" : "Snow" }, {"first" : "Sansa" , "last" : "Stark" } ] }
2、 内部存储结构:
1 2 3 4 5 { "group" : "stark" , "performer.first" : [ "john" , "sansa" ], "performer.last" : [ "snow" , "stark" ] }
3、 存储分析:
可以看出, user.first和user.last会被平铺为多值字段, 这样一来, John和Snow之间的关联性就丢失了.
在查询时, 可能出现John Stark的结果.
2.3.2 用nested类型解决object类型的不足 如果需要对以最对象进行索引, 且保留数组中每个对象的独立性, 就应该使用嵌套数据类型.
——嵌套对象实质是将每个对象分离出来, 作为隐藏文档进行索引.
1、 创建映射:
1 2 3 4 5 6 7 8 9 10 PUT game_of_thrones { "mappings" : { "role" : { "properties" : { "performer" : {"type" : "nested" } } } } }
2、 添加数据:
1 2 3 4 5 6 7 8 PUT game_of_thrones/role/1 { "group" : "stark" , "performer" : [ {"first" : "John" , "last" : "Snow" }, {"first" : "Sansa" , "last" : "Stark" } ] }
3、 检索数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 GET game_of_thrones/_search { "query" : { "nested" : { "path" : "performer" , "query" : { "bool" : { "must" : [ { "match" : { "performer.first" : "John" }}, { "match" : { "performer.last" : "Snow" }} ] } }, "inner_hits" : { "highlight" : { "fields" : {"performer.first" : {}} } } } } }
3 地理数据类型 3.1 地理点类型 - geo point 地理点类型用于存储地理位置的经纬度对, 可用于:
查找一定范围内的地理点;
通过地理位置或相对某个中心点的距离聚合文档;
将距离整合到文档的相关性评分中;
通过距离对文档进行排序.
(1)添加映射:
1 2 3 4 5 6 7 8 9 10 PUT employee { "mappings" : { "developer" : { "properties" : { "location" : {"type" : "geo_point" } } } } }
(2)存储地理位置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 PUT employee/developer/1 { "text" : "小蛮腰-键值对地理点参数" , "location" : { "lat" : 23.11 , "lon" : 113.33 } } PUT employee/developer/2 { "text" : "小蛮腰-字符串地理点参数" , "location" : "23.11, 113.33" } PUT employee/developer/3 { "text" : "小蛮腰-数组参数" , "location" : [ 113.33 , 23.11 ] }
(3)查询示例:
1 2 3 4 5 6 7 8 9 10 11 GET employee/_search { "query" : { "geo_bounding_box" : { "location" : { "top_left" : { "lat" : 24 , "lon" : 113 }, "bottom_right" : { "lat" : 22 , "lon" : 114 } } } } }
3.2 地理形状类型 - geo_shape 是多边形的复杂形状. 使用较少, 这里省略.
可以参考这篇文章: Elasticsearch地理位置总结
4 专门数据类型 4.1 IP类型 IP类型的字段用于存储IPv4或IPv6的地址, 本质上是一个长整型字段.
(1)添加映射:
1 2 3 4 5 6 7 8 9 10 PUT employee { "mappings" : { "customer" : { "properties" : { "ip_addr" : { "type" : "ip" } } } } }
(2)添加数据:
1 2 PUT employee/customer/1 { "ip_addr" : "192.168.1.1" }
(3)查询数据:
1 2 3 4 5 6 GET employee/customer/_search { "query" : { "term" : { "ip_addr" : "192.168.0.0/16" } } }
4.2 计数数据类型 - token_count token_count类型用于统计字符串中的单词数量.
本质上是一个整数型字段, 接受并分析字符串值, 然后索引字符串中单词的个数.
(1)添加映射:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 PUT employee { "mappings" : { "customer" : { "properties" : { "name" : { "type" : "text" , "fields" : { "length" : { "type" : "token_count" , "analyzer" : "standard" } } } } } } }
(2)添加数据:
1 2 3 4 PUT employee/customer/1 { "name" : "John Snow" } PUT employee/customer/2 { "name" : "Tyrion Lannister" }
(3)查询数据:
1 2 3 4 5 6 GET employee/customer/_search { "query" : { "term" : { "name.length" : 2 } } }
DSL查询 1 什么是DSL DSL: Domain Specific Language, 领域特定语言, 指的是专注于某个应用程序领域的、具有高度针对性的计算机语言.
Query String 与 Query DSL之间的区别:
Query String: 在请求的URL后直接拼接查询条件; Query DSL: 在请求的Request Body中携带查询条件.
DSL功能强大, 可以构建复杂的查询、过滤、聚合条件, 所以这种查询方式的用途最广.
2 _validate - 校验查询语句是否合法 对于复杂的查询, 很有必要在查询前使用validate API进行验证, 保证DSL语句的正确有效:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 GET shop/it_book/_validate/query?explain { "query" : { "math" : { "name" : "java" } } } { "valid" : false , "error" : "org.elasticsearch.common.ParsingException: no [query] registered for [math]" } { "valid" : true , "_shards" : { "total" : 1 , "successful" : 1 , "failed" : 0 }, "explanations" : [ { "index" : "shop" , "valid" : true , "explanation" : "+name:java_type:it_book" } ] }
3 match query - 匹配查询 3.1 简单功能示例 3.1.1 查询所有文档 1 2 3 4 5 6 GET shop/it_book/_search { "query" : { "match_all" : {} } }
3.1.2 查询满足一定条件的文档 查询name中包含”java”的文档, 同时按照价格升序排序:
1 2 3 4 5 6 7 8 9 10 11 12 13 GET shop/it_book/_search { "query" : { "match" : { "name" : "java" } }, "sort" : [ { "price" : {"order" : "asc" } } ] }
3.1.3 分页查询文档 1 2 3 4 5 6 7 8 GET shop/it_book/_search { "query" : { "match_all" : {} }, "from" : 0 , "size" : 1 }
3.1.4 指定返回的结果中包含的字段 1 2 3 4 5 6 7 8 9 10 GET shop/it_book/_search { "query" : { "match_all" : {} }, "_source" : [ "name" , "price" ] }
3.2 精确查询 - match_phrase 不同的数据类型在建立倒排索引时, 有的会作为full text处理, 有的作为exact value处理.
对查询串分词时, 使用的分析器(analyzer)必须和创建index时使用的相同, 否则将检索不到准确的数据.
3.2.1 精确匹配 - exact value 常见的exact value类型有date - 日期类型.
ES检索时, 不会对String进行分词, 而是完全根据String的值去精确匹配, 查找相应的文档.
在DSL中, 通过match_phrase短语匹配达到精确匹配的目的 —— 不会对查询串进行分词, 而是直接精确匹配查找.
示例:查询name中包含”thinking in java”的文档, 不会对查询串进行分词:
1 2 3 4 5 6 7 8 GET shop/_search { "query" : { "match_phrase" : { "name" : "thinking in java" } } }
3.2.2 全文搜索 - full text 常见的full text类型有: text - 文本串.
ES检索时, 会对检索串进行分词, 包括缩写、时态、同义词等转换手段, 然后根据分词结果与倒排索引进行匹配, 查找相应的文档.
索引中只要有任意一个相关field的分词 匹配拆分后的词, 这个文档就可以出现在结果中, 只是匹配度越高的排名越靠前.
示例:查询name中包含”thinking in java”的文档, 会将查询串拆分为”think”, “in”, “java”三个词:
1 2 3 4 5 6 7 8 GET shop/_search { "query" : { "match" : { "name" : "thinking in java" } } }
3.3 控制匹配规则 - operator operator 操作符, 用来指定ES对分词后的词项如何进行检索过滤. 选项有:
and, 作用 == match_phrase, 即全部匹配; or, 作用 == match, 即部分匹配.
使用示例:
1 2 3 4 5 6 7 8 9 10 11 GET shop/_search { "query" : { "match" : { "name" : { "query" : "编程思想" , "operator" : "or" } } } }
3.4 指定命中的百分比 - minimum_should_match minimum_should_match 用来指定最少要匹配多少比例的分词, 才算符合条件并返回结果.
示例:搜索name中包含”并发编程的艺术”, 被拆分成”并发”, “编程”, “艺术”等词, 现在要求至少匹配50%的分词, 可以这样:
1 2 3 4 5 6 7 8 9 10 11 GET shop/_search { "query" : { "match" : { "name" : { "query" : "并发编程的艺术" , "minimum_should_match" : "50%" } } } }
当然这种需求也可以用 must、must_not、should 匹配同一个字段的方式进行组合查询.
3.5 多字段的匹配 - multi_match multi_match 用来对多个字段同时进行匹配: 任意一个字段中存在相应的分词, 就可作为结果返回.
示例 1、 : 查询 name 或 desc 字段中包含 “面试经典” 的文档 —— 会对查询串进行分词:
1 2 3 4 5 6 7 8 9 10 11 12 GET shop/_search { "query" : { "multi_match" : { "query" : "面试经典" , "fields" : [ "name" , "desc" ] } } }
示例 2、 : 查询 name 或 desc 字段中同时包含 “面试经典” 的文档 —— 不对查询串进行分词:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 GET shop/_search { "query" : { "multi_match" : { "query" : "面试经典" , "type" : "cross_fields" , "operator" : "and" , "fields" : [ "name" , "desc" ] } } }
4 bool query - 布尔查询(真假查询) bool query, 顾名思义, 就是 真假/有无 查询. 包括4个子查询:
1、 must - 必须匹配, 类似于SQL中的 = ;2、 must_not - 必须不匹配, 类似于SQL中的 != ;3、 should - 不强制匹配, 类似于SQL中的 or ;4、 filter - 过滤, 将满足一定条件的文档筛选出来.
除filter之外, 每个子查询都会根据自己的条件计算出每个文档的相关度分数, 然后bool综合所有分数, 合并为一个.
4.1 简单功能示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 GET shop/_search { "query" : { "bool" : { "must" :[ { "match" : { "name" : "Java" } } ], "must_not" : [ { "match" : { "desc" : "编程" } } ], "should" : [ { "match" : { "publisher" : "机械工业" } } ], "filter" : { "bool" : { "must" : [ { "range" : { "date" : { "gte" : "2010-01-01" }}}, { "range" : { "price" : { "lte" : 99.00 }}} ] } } } } }
4.2 嵌套使用bool query 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 GET shop/_search { "query" : { "bool" : { "should" : [ { "term" : { "name.keyword" : "Java编程思想" } }, { "bool" : { "must" : [ { "term" : { "product_desc" : "刷头" } } ] } } ] } } }
4.3 直接filter操作 - 使用constant_score
如果不指定query条件而直接filter, 将抛出no [query] registered for [filter], 此时通过constant_score即可实现直接filter.
1 2 3 4 5 6 7 8 9 10 GET shop/_search { "query" : { "constant_score" : { "filter" : { "range" : { "price" : { "gte" : 80 } } } } } }
4.4 指定should的匹配个数 - minimum_should_match 如果组合查询中没有must, 就会至少匹配一个should.
可以通过 minimum_should_match 指定匹配的should的个数.
1 2 3 4 5 6 7 8 9 10 11 12 13 GET shop/_search { "query" : { "bool" : { "should" : [ { "match" : { "name" : "java" } }, { "match" : { "desc" : "编程" } }, { "match" : { "price" : 109 } } ], "minimum_should_match" : 2 } } }
高级查询 1 term query - 索引词检索 1.1 term query - 不分词检索 term query: 把检索串当作一个整体来执行检索, 即不会对检索串分词.
term是完全匹配检索, 要用在不分词的字段上, 如果某个field在映射中被分词了, term检索将不起作用. 所以, 不分词的field, 要在mapping中设置为不分词.
——ES 5.x之后, 为每个text类型的字段新增了名为keyword的子字段, 是不分词的, 默认保留256个字符.
——可以使用keyword字段进行term检索. 示例:
1 2 3 4 5 6 7 8 GET shop/_search { "query" : { "term" : { "name.keyword" : "Java编程思想" } } }
1.2 terms query - in检索 terms, 相当于多个term检索, 类似于SQL中in关键字的用法, 即在某些给定的数据中检索:
1 2 3 4 5 6 7 8 9 10 GET shop/_search { "query" : { "terms" : { "name.keyword" : [ "Java编程思想" , "Java并发编程的艺术" ] } } }
2 prefix query - 前缀检索 prefix query, 就是前缀检索. 比如商品name中有多个以”Java”开头的document, 检索前缀”Java”时就能检索到所有以”Java”开头的文档.
—— 扫描所有倒排索引, 性能较差 .
1 2 3 4 5 6 GET shop/_search { "query" : { "prefix" : { "name" : "java" } } }
3 wildcard query - 通配符检索 扫描所有倒排索引, 性能较差 .
1 2 3 4 5 6 GET shop/_search { "query" : { "wildcard" : { "name" : "ja*" } } }
4 regexp query - 正则检索 扫描所有倒排索引, 性能较差 .
1 2 3 4 5 6 GET shop/_search { "query" : { "regexp" : { "name" : "jav[a-z]*" } } }
5 fuzzy query - 纠错检索 fuzziness的默认值是2 —— 表示最多可以纠错两次.
说明:fuzziness的值太大, 将削弱检索条件的作用, 也就是说纠错次数太多, 就会导致限定检索结果的检索条件被改变, 失去了限定作用.
示例:检索name中包含”Java”的文档, Java中缺失了一个字母a:
1 2 3 4 5 6 7 8 9 10 11 12 GET shop/_search { "query" : { "match" : { "name" : { "query" : "Jav" , "fuzziness" : 1 , "operator" : "and" } } } }
6 boost评分权重 - 控制文档的优先级别 通过boost参数, 令满足某个条件的文档的得分更高, 从而使得其排名更靠前.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 GET shop/_search { "query" : { "bool" : { "must" : [ { "match" : { "name" : "编程思想" } } ], "should" : [ { "match" : { "name" : { "query" : "艺术" , "boost" : 2 } } } ] } } }
7 dis_max的用法 - best fields策略 一般检索中, 检索条件会被分词, bool检索构建多个子检索 (must | must_not | should | filter), 这些子检索可能会包含多个field. 这时:
多个子检索的field各自匹配少量关键字的文档的分数 > 某个子检索的field匹配大量关键字的文档的分数 .
7.1 dis_max的提出 如果我们希望检索结果中 (检索串被分词后的) 关键字匹配越多, 这样的文档就越靠前, 而不是多个子检索中匹配少量分词的文档靠前.
⇒此时可以使用dis_max和tie_breaker.
tie_breaker的值介于0~1之间, Elasticsearch将 bool检索的分数 * tie_breaker的结果与dis_max的最高分进行比较, 除了取dis_max的最高分以外, 还会考虑其他的检索结果的分数.
7.2 使用示例 为了增加精准度, 常用的是配合boost、minimum_should_match等参数控制检索结果.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 GET shop/_search { "query" : { "dis_max" : { "queries" : [ { "match" : { "name" : "虚拟机" } }, { "match" : { "desc" : "经典" } } ], "tie_breaker" : 0.2 } } } GET shop/_search { "query" : { "dis_max" : { "queries" : [ { "match" : { "name" : { "query" : "虚拟机" , "minimum_should_match" : "50%" , "boost" : 2 } } }, { "match" : { "desc" : { "query" : "经典" , "minimum_should_match" : "50%" , "boost" : 3 } } } ], "tie_breaker" : 0.3 } } }
8 exist query - 存在检索, 已过期 这是Elasticsearch 2.x中的API, 后续版本不再支持.
9 复杂检索的使用范例 9.1 多条件过滤 - 包含
检索出版时间在2012-07之后, 且至少满足下述条件中一个的文档: a. 名称(name)中包含”并发”; b. 描述(desc)中包含”java”; c. 出版社(publisher)名称中不包含”电子”.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 GET shop/_search { "query" : { "bool" : { "filter" : { "range" : { "date" : {"gte" : "2012-07" } } }, "should" : [ { "match" : { "name" : "并发" } }, { "bool" : { "must" : { "match" : { "desc" : "java" } }, "must_not" : { "match" : { "publisher" : "电子" } } } } ], "minimum_should_match" : 1 } }, "sort" : [ { "price" : { "order" : "desc" } } ] }
注意:排序的字段最好是数字, 或日期, 因为字符串字段会被分词, ES会通过分词后的某个词去排序, 结果难以预测.
9.2 多条件拼接 - 包含+范围+排序
匹配检索: name中包含”java”却不包含”虚拟机”; 范围检索: 价格大于50、小于80; 结果排序: 按照价格升序排序.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 GET shop/_search { "query" : { "bool" : { "must" : { "match" : { "name" : "java" } }, "must_not" : { "match" : { "name" : "虚拟机" } }, "filter" : { "range" : { "price" : { "gte" : 40 , "lte" : 80 , "boost" : 2.0 } } } } } }
关于范围检索的使用, 请参考下篇文章: [ES 22 - Elasticsearch对数值或日期类型进行范围检索][ES 22 - Elasticsearch]
9.3 定制检索结果的排序规则 (1) 默认排序规则:
ES默认是按检索结果的分值(_score)降序排列的.
某些情况下, 可能存在无实际意义的_score, 比如filter时所有_score的值都相同:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 GET website/_search { "query" : { "bool" : { "filter" : { "term" : { "author_id" : 5520 } } } } } GET website/_search { "query" : { "constant_score" : { "filter" : { "term" : { "author_id" : 5520 } } } } }
(2) 定制排序规则:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 GET website/_search { "query" : { "constant_score" : { "filter" : { "term" : { "author_id" : 5520 } } } }, "sort" : [ { "post_date" : { "order" : "asc" } } ] }
聚合查询 1 普通聚合分析 1.1 直接聚合统计 (1) 计算每个tag下的文档数量, 请求语法:
1 2 3 4 5 6 7 8 9 10 11 GET book_shop/it_book/_search { "size" : 0 , "aggs" : { "group_by_tags" : { "terms" : { "field" : "tags" } } } }
(2) 发生错误:
说明:索引book_shop的mapping映射是ES自动创建的, 它把tag解析成了text类型, 在发起对tag的聚合请求后, 将抛出如下错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "error" : { "root_cause" : [ { "type" : "illegal_argument_exception" , "reason" : "Fielddata is disabled on text fields by default. Set fielddata=true on [tags] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead." } ], "type" : "search_phase_execution_exception" , "reason" : "all shards failed" , "phase" : "query" , "grouped" : true , "failed_shards" : [......] }, "status" : 400 }
(3) 错误分析:
错误信息: Set fielddata=true on [xxxx] ...... 错误分析: 默认情况下, Elasticsearch 对 text 类型的字段(field)禁用了 fielddata; text 类型的字段在创建索引时会进行分词处理, 而聚合操作必须基于字段的原始值进行分析; 所以如果要对 text 类型的字段进行聚合操作, 就需要存储其原始值 —— 创建mapping时指定fielddata=true, 以便通过反转倒排索引(即正排索引)将索引数据加载至内存中.
(4) 解决方案一: 对text类型的字段开启fielddata属性:
将要分组统计的text field(即tags)的fielddata设置为true:
1 2 3 4 5 6 7 8 9 PUT book_shop/_mapping/it_book { "properties" : { "tags" : { "type" : "text" , "fielddata" : true } } }
1 2 3 { "acknowledged" : true }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 { "took" : 153 , "timed_out" : false , "_shards" : { "total" : 5 , "successful" : 5 , "skipped" : 0 , "failed" : 0 }, "hits" : { "total" : 4 , "max_score" : 0.0 , "hits" : [] }, "aggregations" : { "group_by_tags" : { "doc_count_error_upper_bound" : 0 , "sum_other_doc_count" : 6 , "buckets" : [ { "key" : "java" , "doc_count" : 3 }, { "key" : "程" , "doc_count" : 2 }, ...... ] } } }
(5) 解决方法二: 使用内置keyword字段:
开启fielddata将占用大量的内存.
Elasticsearch 5.x 版本开始支持通过text的内置字段keyword作精确查询、聚合分析:
1 2 3 4 5 6 7 8 9 10 11 GET shop/it_book/_search { "size" : 0 , "aggs" : { "group_by_tags" : { "terms" : { "field" : "tags.keyword" } } } }
1.2 先检索, 再聚合 (1) 统计name中含有“jvm”的图书中每个tag的文档数量, 请求语法:
1 2 3 4 5 6 7 8 9 10 11 GET book_shop/it_book/_search { "query" : { "match" : { "name" : "jvm" } }, "aggs" : { "group_by_tags" : { "terms" : { "field" : "tags.keyword" } } } }
(2) 响应结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 { "took" : 7 , "timed_out" : false , "_shards" : { "total" : 5 , "successful" : 5 , "skipped" : 0 , "failed" : 0 }, "hits" : { "total" : 1 , "max_score" : 0.64072424 , "hits" : [ { "_index" : "book_shop" , "_type" : "it_book" , "_id" : "2" , "_score" : 0.64072424 , "_source" : { "name" : "深入理解Java虚拟机:JVM高级特性与最佳实践" , "author" : "周志明" , "category" : "编程语言" , "desc" : "Java图书领域公认的经典著作" , "price" : 79.0 , "date" : "2013-10-01" , "publisher" : "机械工业出版社" , "tags" : [ "Java" , "虚拟机" , "最佳实践" ] } } ] }, "aggregations" : { "group_by_tags" : { "doc_count_error_upper_bound" : 0 , "sum_other_doc_count" : 0 , "buckets" : [ { "key" : "Java" , "doc_count" : 1 }, { "key" : "最佳实践" , "doc_count" : 1 }, { "key" : "虚拟机" , "doc_count" : 1 } ] } } }
1.3 扩展: fielddata和keyword的聚合比较
为某个 text 类型的字段开启fielddata字段后, 聚合分析操作会对这个字段的所有分词分别进行聚合, 获得的结果大多数情况下并不符合我们的需求.
使用keyword内置字段, 不会对相关的分词进行聚合, 结果可能更有用.
—— 推荐使用text类型字段的内置keyword进行聚合操作.
2 嵌套聚合 2.1 先分组, 再聚合统计 (1) 先按tags分组, 再计算每个tag下图书的平均价格, 请求语法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 GET book_shop/it_book/_search { "size" : 0 , "aggs" : { "group_by_tags" : { "terms" : { "field" : "tags.keyword" }, "aggs" : { "avg_price" : { "avg" : { "field" : "price" } } } } } }
(2) 响应结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 "hits" : { "total" : 3 , "max_score" : 0.0 , "hits" : [ ] }, "aggregations" : { "group_by_tags" : { "doc_count_error_upper_bound" : 0 , "sum_other_doc_count" : 0 , "buckets" : [ { "key" : "Java" , "doc_count" : 3 , "avg_price" : { "value" : 102.33333333333333 } }, { "key" : "编程语言" , "doc_count" : 2 , "avg_price" : { "value" : 114.0 } }, ...... ] } }
2.2 先分组, 再统计, 最后排序 (1) 计算每个tag下图书的平均价格, 再按平均价格降序排序, 查询语法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 GET book_shop/it_book/_search { "size" : 0 , "aggs" : { "all_tags" : { "terms" : { "field" : "tags.keyword" , "order" : { "avg_price" : "desc" } }, "aggs" : { "avg_price" : { "avg" : { "field" : "price" } } } } } }
(2) 响应结果:
与#2.1节内容相似, 区别在于按照价格排序显示了.
2.3 先分组, 组内再分组, 然后统计、排序 (1) 先按价格区间分组, 组内再按tags分组, 计算每个tags组的平均价格, 查询语法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 GET book_shop/it_book/_search { "size" : 0 , "aggs" : { "group_by_price" : { "range" : { "field" : "price" , "ranges" : [ { "from" : 00 , "to" : 100 }, { "from" : 100 , "to" : 150 } ] }, "aggs" : { "group_by_tags" : { "terms" : { "field" : "tags.keyword" }, "aggs" : { "avg_price" : { "avg" : { "field" : "price" } } } } } } } }
(2) 响应结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 "hits" : { "total" : 3 , "max_score" : 0.0 , "hits" : [ ] }, "aggregations" : { "group_by_price" : { "buckets" : [ { "key" : "0.0-100.0" , "from" : 0.0 , "to" : 100.0 , "doc_count" : 1 , "group_by_tags" : { "doc_count_error_upper_bound" : 0 , "sum_other_doc_count" : 0 , "buckets" : [ { "key" : "Java" , "doc_count" : 1 , "avg_price" : { "value" : 79.0 } }, ...... ] } }, { "key" : "100.0-150.0" , "from" : 100.0 , "to" : 150.0 , "doc_count" : 2 , "group_by_tags" : { "doc_count_error_upper_bound" : 0 , "sum_other_doc_count" : 0 , "buckets" : [ { "key" : "Java" , "doc_count" : 2 , "avg_price" : { "value" : 114.0 } }, ...... } ] } } ] } }
集群搭建建议
在生产环境中, 要保证服务在各种极限情况下的稳定和高可用, 所以在部署ES集群时, 需要考虑服务器的内存、CPU、磁盘, 集群的网络、节点个数, 并且要优化JVM的各项参数. 首先从这些方面着手进行部署前的规划.
1 服务器的内存 ES非常消耗内存 —— 不是JVM用到的内存, 而是机器的物理内存, 因为ES在运行期间对JVM Heap(堆内存)的需求较小.
ES底层是基于Lucene创建的, 而Lucene是基于磁盘中的文件来读写和保存索引数据(包括倒排索引、正排索引); Lucene的特点是基于OS File System Cache(操作系统的文件缓存系统), 它会尽可能地把频繁访问的磁盘文件缓存在操作系统的内存中, 从而提高对磁盘文件的读写性能. 关于Lucene的更多资料, 可参考: https://www.cnblogs.com/shoufeng/category/1259723.html .
可以这样说: ES的性能很大程度上(80%)取决于分配给JVM Heap内存之后、服务器的剩余内存大小 . ——这些剩余内存会缓存Lucene的索引文件, 缓存的越多, 索引的读写性能都越高, 尤其是检索和聚合操作, 它们需要读取几乎所有的索引数据.
实践建议: 数据量过亿, 建议单台服务器的内存至少要有64GB.
2 服务器的CPU ES集群对CPU的要求比较低, 一般来说2~8个CPU Core即可满足集群的需求.
实践建议: 尽可能使用多核处理器, 因为并发处理能力会更好.
3 服务器的磁盘 ES生产环境中, 磁盘的读写能力是非常重要的, 尤其对于大量写操作的集群, 比如电商公司将每天的实时日志数据以高并发的方式写入ES集群.
(1) 在磁盘的使用上, 推荐使用SSD(固态硬盘), 而不是(HDD)机械硬盘.
使用SSD, 就需要配置SSD的I/O Scheduler —— 数据写入磁盘时, IO Scheduler会决定将数据从OS Cache刷入到磁盘的时机. 大部分SSD的默认IO Scheduler是CFQ (completely fair queuing), 它会为每个进程都分配一些时间片(time slice), 然后通过磁盘的物理布局来决定如何将数据写入磁盘 (对各个进程的数据写入进行优化), 进而提升写入磁盘的性能. 但是默认的CFQ并不高效. 对SSD来说, 推荐使用Deadline/Noop Scheduler, 它基于写操作被Pending的时间长短来进行写磁盘优化, 而Noop Scheduler就是一个简单的FIFO(先进先出)队列机制.
(2) 此外, 使用RAID 0也是一种提升磁盘读写速度的高效方式, 无论是HDD, 或者SSD都支持RAID 0.
RAID 0也被称之为条带式(striping)存储机制, 在RAID各种级别中性能是最高的, 它的基本原理是: 把连续的数据分散存储到多个磁盘上进行读写, 也就是对数据进行条带式存储 —— 磁盘的读写请求被分散到多个磁盘上并行执行. 没有必要使用镜像或者RAID的其他模式, 因为我们并不需要通过RAID来实现数据的高可用存储 —— 这方面的工作ES的Replica副本机制已经实现了.
(3)最后, 要避免使用与网络相关的存储模式 (network-attached storage, NAS), 比如基于网络的分布式存储模式.
虽然很多供应商都说他们的NAS解决方案性能非常高, 而且比本地存储的可靠性更高, 但在实际使用上还是会有很多风险: 网络传输可能存在比较高的时延, 还可能存在单点故障.
实践建议: 推荐使用SSD, 并调整其IO Scheduler为Deadline/Noop Scheduler, 这可以带来很大的性能提升, 理想情况下可能达到上百倍.
4 集群的网络 对ES这种分布式系统来说, 快速可靠的网络是非常重要的:
高速网络通信可以让ES节点间的通信时延降低; 高带宽可以让Shard的移动、恢复, 以及分配等操作更加快速.
不低于千兆网卡对大多数集群来说都已足够, 但要避免一个集群横跨多个数据中心 , 比如异地多机房部署一个集群 —— 跨地域跨机房会降低网络通信和数据传输的效率.
1、 ES集群是一种p2p模式的分布式系统架构, 并不是master-slave主从分布式系统.2、 ES集群中, 所有节点都是平等的, 任意两个节点之间的通信都很频繁 , 如果部署在异地多机房, 就会导致各个节点之间频繁跨地域通信, 通信时延会非常高, 甚至有可能造成集群运行频繁出现异常.3、 与NAS存储模式一样, 很多供应商都声称他们的跨地域多数据中心是可靠、低时延的, 即使果真如此, 一旦网络出现故障, 整个集群就会不可用. 大多数情况下, 跨地域多机房部署一个ES集群带来的效益要远远低于维护这类集群所付出的额外成本.
实践建议: 不低于千兆网卡, 且不要垮多个数据中心, 尤其不要跨地域多机房.
5 集群的节点个数 ES集群的节点个数:
1、 建议部署少个节点, 但每个节点对应服务器的资源都必须充足;2、 不建议在一台高性能服务器上部署多个节点: 不仅降低了集群的可用性, 而且集群的维护复杂度也变得更高了.
尽量避免部署大量的低资源的服务器, 因为对运维和管理而言, 管理5个物理机组成的集群, 要比管理10个虚拟机组成的集群要简单简单太多.
实践建议: 小规模、高配置, 但无需超高配置, 会造成资源的浪费.
6 JVM的参数设置 ES的版本越新, 使用的JDK的版本也应该越新, 既提高性能, 也避免一些不常见的系统Bug(包括Lucene和JDK的).
以本系列博文为例, 示例的ES版本为6.6.0, 使用的JDK版本是jdk1.8.0_151.
(1) 如果通过Java API操作ES服务, 那么编译Java程序的JVM版本最好与ES服务器所运行的JVM版本一致.
ES中用到了很多与JVM版本相关的特性, 比如本地序列化机制 (包括IP地址、异常信息等等), 而JVM在不同的minor版本中可能会修改序列化机制, 版本不同可能会导致序列化异常.
(2) 同时官方强烈建议: 不要随意调整JVM的参数设置 .
ES是一个非常复杂的分布式软件系统, 它默认的JVM配置经过了大量真实业务场景下的检验, 除非你很明确地知道自己的服务瓶颈出在哪几个参数上, 否则不要调整.
ES服务中, JVM Heap堆内存的大小一般不超过服务器物理内存的一半, 以1/4为宜, 且最多不宜超过32GB.
7 集群的数据量 对很多中小型公司, 建议ES集群承载的数据量在百亿级规模以内.
(1)ES的常见使用场景有:
1、 构建业务搜索功能模块, 且多是垂直领域的搜索: 以网站或APP为例, 数据规模相对比较大, 通常是百万级到亿级;2、 进行数据分析: 需要消耗更大的内存, 但支持的数据规模要小很多, 通常是十万级到千万级;3、 用于大规模数据的实时OLAP(联机处理分析), 经典的如ELK Stack, 数据规模可能达到千亿或更多, 集群规模可能达到几十上百节点.
(2)数据量特别大时的处理思路:
如果应用的数据量特别大, 日增量几十上百万, 那就不建议将数据全量写入ES中, ES也不适合这种数据无限膨胀的场景 —— ES消耗内存, 无限膨胀的数据量会导致无法提供足够的内存来支撑大规模数据的快速检索. 此时可以考虑: 将部分热数据 (比如最近一月的数据) 存放到ES中做高频高性能搜索, 将大量的、较少访问的冷数据存放至大数据系统 (比如Hadoop) 中做离线批量处理.
不同数据规模与内存容量下的检索性能表现: 如果服务器的内存可以将ES所需的文件全部纳入到OS Cache中, 就能达到ms(毫秒)级的检索性能; 否则检索会大量访问磁盘, 检索时间就会上升到s(秒)级.
8 总结 要提升ES的性能, 最重要的是规划合理的数据量, 配置足够的物理内存用作OS Cache, 尽可能减少从磁盘中访问数据.
ES的数据建模 1 什么是数据建模? 数据建模(Data modeling), 是创建数据模型的过程.
数据模型是对真实世界进行抽象描述的一种工具和方法, 实现对现实世界的映射. 比如影视作品、演员、观众评论…
数据建模有三个过程: 概念模型 => 逻辑模型 => 数据模型(第三范式)
数据模型, 需要结合使用的数据库类型, 在满足业务读写性能等需求的前提下, 制定出最终的定义.
2 如何对 ES 中的数据进行建模 ES中的数据建模:
由数据存储、检索等功能需求提炼出实体属性、实体之间的关系 =》形成逻辑模型;
由性能需求提炼制定索引模板、索引Mapping(包括字段的配置、关系的处理) ==》形成物理模型.
ES中存储、检索的基本单位是索引文档(document), 文档由字段(field)组成, 所以ES的建模就是对字段进行建模.
文档类似于关系型数据库中的一行数据, 字段对应关系型数据库中的某一列数据.
2.1 字段类型的建模方案 (1) text 与 keyword 比较:
text: 用于全文本字段, 文本会被 Analyzer 分词; 默认不支持聚合分析及排序, 设置 “fielddata”: true 即可支持;
keyword: 用于 id、枚举及不需要分词的文本, 比如身份证号码、电话号码,Email地址等; 适用于 Filter(精确匹配过滤)、Sorting(排序) 和 Aggregations(聚合).
设置多字段类型:
默认会为文本类型设置成 text, 并设置一个 keyword 的子字段; 在处理人类自然语⾔时, 可以添加“英⽂”、“拼⾳”、“标准”等分词器, 提高搜索结果的正确性.
(2) 结构化数据:
数值类型: 尽量选择贴近的类型, 例如可以用 byte, 就不要用 long;
枚举类型: 设置为 keyword, 即使是数字, 也应该设置成 keyword, 获取更好的性能; 另外范围检索使用keyword, 速度更快;
其他类型: 日期、二进制、布尔、地理信息等类型.
2.2 检索、聚合及排序的建模方案
如不需要检索、排序和聚合分析, 则可设置 “enable”: false ;
如不需要检索, 则可设置 “index”: false ;
如不需要排序、聚合分析功能, 则可设置 “doc_values”: false / “fielddate”: false ;
更新频繁、聚合查询频繁的 keyword 类型的字段, 推荐设置 “eager_global_ordinals”: true .
2.3 额外存储的建模方案
"store": true, 可以存储该字段的原始内容;
一般结合 "_source": { "enabled": false } 进行使用, 因为默认的 "_source": { "enabled": true }, 也就是添加索引时文档的原始 JSON 结构都会存储到 _source 中.
disable_source: 禁用 _source 元字段, 能节约磁盘, 适用于指标型数据 —— 类似于标识字段、时间字段的数据, 不会更新、高亮查询, 多用来进行过滤操作以快速筛选出更小的结果集, 用来支撑更快的聚合操作.
官方建议: 如果更多关注磁盘空间, 那么建议优先考虑增加数据的压缩⽐, 而不是禁用 _source;
无法看到 _source 字段, 就不能做 reindex、update、update_by_query 操作;
目前为止, Kibana 中无法对禁用了 _source 字段的索引进行 Discover 挖掘操作.
—— 谨慎禁用 _source 字段, 参考: https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-source-field.html
3 ES 数据建模实例演示 3.1 动态创建映射关系 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 # 直接写入一本图书信息: POST books/_doc { "title" : "Thinking in Elasticsearch 7.2.0" , "author" : "Heal Chow" , "publish_date" : "2019-10-01" , "description" : "Master the searching, indexing, and aggregation features in Elasticsearch." , "cover_url" : "https://healchow.com/images/29dMkliO2a1f.jpg" } # 查看自动创建的mapping关系: GET books/_mapping # 内容如下: { "books" : { "mappings" : { "properties" : { "author" : { "type" : "text" , "fields" : { "keyword" : { "type" : "keyword" , "ignore_above" : 256 } } }, "cover_url" : { "type" : "text" , "fields" : { "keyword" : { "type" : "keyword" , "ignore_above" : 256 } } }, "description" : { "type" : "text" , "fields" : { "keyword" : { "type" : "keyword" , "ignore_above" : 256 } } }, "publish_date" : { "type" : "date" }, "title" : { "type" : "text" , "fields" : { "keyword" : { "type" : "keyword" , "ignore_above" : 256 } } } } } } }
3.2 手动创建映射关系 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 # 删除自动创建的图书索引: DELETE books # 手动优化字段的mapping: PUT books { "mappings" : { "_source" : { "enabled" : true }, "properties" : { "title" : { "type" : "text" , "fields" : { "keyword" : { "type" : "keyword" , "ignore_above" : 100 } } }, "author" : { "type" : "keyword" }, "publish_date" : { "type" : "date" , "format" : "yyyy-MM-dd HH:mm:ss||yyyyMMddHHmmss||yyyy-MM-dd||epoch_millis" }, "description" : { "type" : "text" }, "cover_url" : { index 设置成 false , 不支持搜索, 但支持 Terms 聚合 "type" : "keyword" , "index" : false } } } }
说明:_source 元字段默认是开启的, 若禁用后, 就无法对搜索的结果进行展示, 也无法进行 reindex、update、update_by_query 操作.
3.3 新增需求 - 添加大字段
需求描述: 添加图书内容字段, 要求支持全文搜索, 并且能够高亮显示.
需求分析: 新需求会导致 _source 的内容过⼤, 虽然我们可以通过source filtering对要搜索结果中的字段进行过滤:
1 2 3 "_source" : { "includes" : ["title" ] 或 "excludes" : ["xxx" ] 排除某些字段, includes 优先级更高 }
但这种方式只是 ES 服务端传输给客户端时的过滤, 内部 Fetch 数据时, ES 各数据节点还是会传输 _source 中的所有数据到协调节点 —— 网络 IO 没有得到本质上的降低.
3.4 解决大字段带来的性能问题 (1)在创建 mapping 时手动关闭 _source 元字段: "_source": { "enabled": false} ;
(2)然后为每个字段设置 "store": true .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 # 关闭_source元字段, 设置store=true : PUT books { "mappings" : { "_source" : { "enabled" : false }, "properties" : { "title" : { "type" : "text" , "store" : true , "fields" : { "keyword" : { "type" : "keyword" , "ignore_above" : 100 } } }, "author" : { "type" : "keyword" , "store" : true }, "publish_date" : { "type" : "date" , "store" : true , "format" : "yyyy-MM-dd HH:mm:ss||yyyyMMddHHmmss||yyyy-MM-dd||epoch_millis" }, "description" : { "type" : "text" , "store" : true }, "cover_url" : { "type" : "keyword" , "index" : false , "store" : true }, "content" : { "type" : "text" , "store" : true } } } }
(3)加数据, 并进行高亮查询:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 添加包含新字段的文档: POST books/_doc { "title" : "Thinking in Elasticsearch 7.2.0" , "author" : "Heal Chow" , "publish_date" : "2019-10-01" , "description" : "Master the searching, indexing, and aggregation features in Elasticsearch." , "cover_url" : "https://healchow.com/images/29dMkliO2a1f.jpg" , "content" : "1. Revisiting Elasticsearch and the Changes. 2. The Improved Query DSL. 3. Beyond Full Text Search. 4. Data Modeling and Analytics. 5. Improving the User Search Experience. 6. The Index Distribution Architecture. .........." } # 通过 stored_fields 指定要查询的字段: GET books/_search { "stored_fields" : ["title" , "author" , "publish_date" ], "query" : { "match" : { "content" : "data modeling" } }, "highlight" : { "fields" : { "content" : {} } } }
查询结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 { "took" : 1 , "timed_out" : false , "_shards" : { "total" : 1 , "successful" : 1 , "skipped" : 0 , "failed" : 0 }, "hits" : { "total" : { "value" : 1 , "relation" : "eq" }, "max_score" : 0.5753642 , "hits" : [ { "_index" : "books" , "_type" : "_doc" , "_id" : "dukLoG0BdfGBNhbF13CJ" , "_score" : 0.5753642 , "highlight" : { "content" : [ "<em>Data</em> <em>Modeling</em> and Analytics. 5. Improving the User Search Experience. 6." ] } } ] } }
(4)结果说明:
返回结果中不包含 _source 字段;
对需要显示的信息, 要在查询中指定 “stored_fields”: [“xxx”, “yyy”] ;
禁⽌ _source 字段后, 仍然支持使用 Highlights API 的使用.
3.5 mapping中字段的常用参数 参考:https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-params.html
enabled – 设置成 false, 当前字段就只存储, 不支持搜索和聚合分析 (数据保存在 _source 中);
index – 是否构建倒排索引, 设置成 false, 就无法被搜索, 但还是支持聚合操作, 并会出现在 _source 中;
norms – 只⽤来过滤和聚合分析(指标数据)、不关心评分的字段, 建议关闭, 节约存储空间;
doc_values – 是否启用 doc_values, 用于排序和聚合分析;
field_data – 如果要对 text 类型启用排序和聚合分析, fielddata 需要设置成true;
coerce – 是否开启数据类型的自动转换 (如: 字符串转数字), 默认开启;
multifields - 是否开启多字段特性;
dynamic – 控制 mapping 的动态更新策略, 有 true / false / strict 三种.
doc_values 与 fielddata 比较:
doc_values: 聚合和排序的字段需要开启 —— 默认 为所有非text类型的字段 开启 —— 内存不够时, 会写入磁盘文件中;
fielddata: 是否为text类型开启, 以实现排序和聚合分析 —— 默认关闭 —— 全部加载进内存中.
3.6 mapping 设置小结 (1)支持加入新的字段 (包括子字段)、更换分词器等操作:
可以通过 update_by_query 令旧数据得到清洗.
(2)Index Template: 根据索引的名称匹配不同的 mappings 和 settings;
(3)Dynamic Template: 在一个 mapping 上动态设定字段类型;
(4)Reindex: 如果要修改、删除已经存在的字段, 或者修改分片个数等参数, 就要重建索引.
必须停机, 数据量大时耗时会比较久.
可借助 Index Alias (索引别名) 来实现零停机维护.
4 ES 数据建模最佳实践 4.1 如何处理关联关系 (1)范式化设计:
我们知道, 在关系型数据库中有“范式化设计”的概念, 有 1NF、2NF、3NF、BCNF 等等, 主要目标是减少不必要的更新, 虽然节省了存储空间, 但缺点是数据读取操作可能会更慢, 尤其是跨表操作, 需要 join 的表会很多.
反范式化设计: 数据扁平, 不使用关联关系, 而是在文档中通过 _source 字段来保存冗余的数据拷贝.
优点: 无需处理 join 操作, 数据读取性能好;
缺点: 不适合数据频繁修改的场景.
==》ES 不擅长处理关联关系, 一般可以通过对象类型(object)、嵌套类型(nested)、父子关联关系(child/parent)解决.
具体使用所占篇幅较大, 这里省略.
4.2 避免太多的字段 (1)一个⽂档中, 最好不要有⼤量的字段:
过多的字段导致数据不容易维护;
mapping 信息保存在 Cluster State 中, 数据量过⼤, 对集群性能会有影响 (Cluster State 信息需要和所有的节点同步);
删除或修改字段时, 需要 reindex;
(2)ES中单个索引最大字段数默认是 1000, 可以通过参数 index.mapping.total_fields.limt 修改最⼤字段数.
思考:什么原因会导致文档中有成百上千的字段?
ES 是无模式 (schemaless) 的, 默认情况下, 每添加一个字段, ES 都会根据该字段可能的类型自动添加映射关系.
如果业务处理不严谨, 会出现字段爆炸的现象. 为了避免这种现象的发生, 需要制定 dynamic 策略:
true - 未知字段会被自动加入, 是默认设置;
false - 新字段不会被索引, 但是会保存到 _source 中;
strict - 新增字段不会被索引, ⽂档写入失败, 抛出异常.
—— 生产环境中, 尽量不要使用默认的 “dynamic”: true .
4.3 避免正则查询 正则、前缀、通配符查询, 都属于 Term 查询, 但是性能很不好(扫描所有文档, 并逐一比对), 特别是将通配符放在开头, 会导致性能灾难.
(1)案例:
文档中某个字段包含了 Elasticsearch 的版本信息, 例如 version: “7.2.0” ;
搜索某系列的 bug_fix 版本(末位非0的版本号)? 每个主要版本号所关联的文档?
(2)通配符查询示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 插入2 条数据: PUT softwares/_doc/1 { "version" : "7.2.0" , "doc_url" : "https://www.elastic.co/guide/en/elasticsearch/.../.html" } PUT softwares/_doc/2 { "version" : "7.3.0" , "doc_url" : "https://www.elastic.co/guide/en/elasticsearch/.../.html" } # 通配符查询: GET softwares/_search { "query" : { "wildcard" : { "version" : "7*" } } }
(3)解决方案 - 将字符串类型转换为对象类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 # 创建对象类型的映射: PUT softwares { "mappings" : { "properties" : { "version" : { # 版本号设置为对象类型 "properties" : { "display_name" : { "type" : "keyword" }, "major" : { "type" : "byte" }, "minor" : { "type" : "byte" }, "bug_fix" : { "type" : "byte" } } }, "doc_url" : { "type" : "text" } } } } # 添加数据: PUT softwares/_doc/1 { "version" : { "display_name" : "7.2.0" , "major" : 7 , "minor" : 2 , "bug_fix" : 0 }, "doc_url" : "https://www.elastic.co/guide/en/elasticsearch/.../.html" } PUT softwares/_doc/2 { "version" : { "display_name" : "7.3.0" , "major" : 7 , "minor" : 3 , "bug_fix" : 0 }, "doc_url" : "https://www.elastic.co/guide/en/elasticsearch/.../.html" } # 通过filter过滤, 避免正则查询, 大大提升性能: GET softwares/_search { "query" : { "bool" : { "filter" : [ { "match" : { "version.major" : 7 } }, { "match" : { "version.minor" : 2 } } ] } } }
4.4 避免空值引起的聚合不准 (1)示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 # 添加数据, 包含1 条 null 值的数据: PUT ratings/_doc/1 { "rating" : 5 } PUT ratings/_doc/2 { "rating" : null } # 对含有 null 值的字段进行聚合: GET ratings/_search { "size" : 0 , "aggs" : { "avg_rating" : { "avg" : { "field" : "rating" } } } } # 结果如下: { "took" : 3 , "timed_out" : false , "_shards" : { "total" : 1 , "successful" : 1 , "skipped" : 0 , "failed" : 0 }, "hits" : { "total" : { "value" : 2 , # 2 条数据, avg_rating 结果不正确 "relation" : "eq" }, "max_score" : null , "hits" : [ ] }, "aggregations" : { "avg_rating" : { "value" : 5.0 } } }
(2)使用 null_value 解决空值的问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 # 创建 mapping 时, 设置 null_value: PUT ratings { "mappings" : { "properties" : { "rating" : { "type" : "float" , "null_value" : "1.0" } } } } # 添加相同的数据, 再次聚合, 结果正确: { "took" : 0 , "timed_out" : false , "_shards" : { "total" : 1 , "successful" : 1 , "skipped" : 0 , "failed" : 0 }, "hits" : { "total" : { "value" : 2 , "relation" : "eq" }, "max_score" : null , "hits" : [ ] }, "aggregations" : { "avg_rating" : { "value" : 3.0 } } }
案例、实例