ElasticSearch
ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口
- Near Realtime
- 从写入数据到数据可以被搜索到有一个小延迟,大概是 1s
- 基于 es 执行搜索和分析可以达到秒级
优势
- 横向可扩展
- 分片机制提供更好的分布性
- 高可用
安装
使用 docker
docker run elasticsearch:7.3.1
docker network create somenetwork;docker run -d --name elasticsearch --net somenetwork -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:7.3.1
9300端口: ES节点之间通讯使用9200端口: ES节点 和 外部 通讯使用
图形化管理界面
概念
- 集群(cluster)
- 多个节点组成
- 节点(node)
- 服务器实例
- 索引(index)
- Databases 数据库
- 类型(type)
- Table 数据表
- 文档(Document)
- Row 行
- 字段(Field)
- Columns 列
- shard
- es 可以将一个索引中的数据切分为多个 shard,分布在多台服务器上存储
- replica
- 任何一个服务器随时可能故障或宕机,此时 shard 可能就会丢失,因此可以为每个 shard 创建多个replica 副本。replica 可以在 shard 故障时提供备用服务,所以同一片shard跟replica不能存放在同一节点
- 映射 mapping
索引结构
操作
创建索引
PUT /blog{ "settings": { "number_of_shards": 3, "number_of_replicas": 2 }}
- 获取索引库信息
GET /blog
- 删除索引库
DELETE /blog
添加映射
PUT /索引库名/_mapping/类型名称{ "properties": { "字段名": { "type": "类型", "index": true, "store": true, "analyzer": "分词器" } }}
数据类型
text:该类型被用来索引长文本,在创建索引前会将这些文本进行分词,转化为词的组合,建立索引;允许es来检索这些词,text类型不能用来排序和聚合。keyword:该类型不需要进行分词,可以被用来检索过滤、排序和聚合,keyword类型自读那只能用本身来进行检索(不可用text分词后的模糊检索)数值型:long、integer、short、byte、double、float日期型:date布尔型:boolean二进制型:binary
- 查看映射关系
GET /索引库名/_mapping
- 更新索引
POST http://my-pc:9200/blog/{indexName}
添加文档
POST /索引库名/类型名{ "key":"value"}
- 自定义id
POST /索引库名/类型/id值{...}
删除文档
DELETE http://my-pc:9200/blog/hello/1
修改文档
UPDATE http://my-pc:9200/blog/hello/1
查询
基本查询
GET /索引库名/_search{ "query":{ "查询类型":{ "查询条件":"查询条件值" } }}
- 根据ID查询
GET http://my-pc:9200/blog/hello/1
- 根据字段查询
Term Query为精确查询,在搜索时会整体匹配关键字,不再将关键字分词。
GET /shop/_search{ "_source": ["title","price"], "query": { "term": { "price": 2699 } }}
- queryString查询
{ "query":{ "query_string":{ "default_field":"content", "query":"内容" } }}
过滤
- includes:来指定想要显示的字段
- excludes:来指定不想要显示的字段
GET /shop/_search{ "_source": { "includes":["title","price"] }, "query": { "term": { "price": 2699 } }}
排序
GET /shop/_search{ ... "sort": [ { "price": { "order": "desc" } } ]}
模糊查询
GET /heima/_search{ "query": { "fuzzy": { "title": { "value":"appla", "fuzziness":1 } } }}
分词
内置的分词器
- Standard Analyzer
- Simple Analyzer
- Whitespace Analyzer
- Stop Analyzer
- Keyword Analyzer
- Pattern Analyzer
- Language Analyzers
- Fingerprint Analyzer
测试分词
GET /_analyze
{ "analyzer": "standard", "text": "中文测试分词"}
中文分词器
docker run --name elasticsearch --net somenetwork -v /root/plugin:/usr/share/elasticsearch/plugins -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:7.3.1
GET http://my-pc:9200/_analyze
{ "analyzer": "ik_max_word", "text": "中文测试分词"}
ik 的两种模式:
- max:会将文本做最细粒度的拆分 会穷尽所有的可能
- smart:最最粗粒度的划分
聚合
桶
ES集群
采用ES集群,将单个索引的分片到多个不同分布式物理机器上存储,从而可以实现高可用、容错性
架构
es 集群多个节点,会自动选举一个节点为 master 节点master 节点宕机了,那么会重新选举一个节点为 master 节点
非 master节点宕机了,那么会由 master 节点,让那个宕机节点上的 primary shard 的身份转移到其他机器上的 replica shard
可以使用三个节点,将索引分成三份,每个节点存放一份primary shard,两份replica,这样就算只剩下一台节点,也能保证服务可用
搭建
- 配置
# 集群名称,必须保持一致cluster.name: elasticsearch# 节点的名称node.name: node-1# 监听网段network.host: 0.0.0.0# 本节点rest服务端口http.port: 9201# 本节点数据传输端口transport.tcp.port: 9301# 集群节点信息discovery.seed_hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]cluster.initial_master_nodes: ["node-1","node-2","node-3"]
另外两个节点配置省略...
JAVA客户端
- 依赖
<dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>7.3.1</version></dependency><dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>transport</artifactId> <version>7.3.1</version></dependency>
- 连接
Settings settings = Settings.builder() .put("cluster.name","docker-cluster") .build();TransportClient client = new PreBuiltTransportClient(settings);client.addTransportAddress( new TransportAddress(InetAddress.getByName("my-pc"),9300));
- 创建索引
client.admin().indices().prepareCreate("index").get();
- 设置映射
XContentBuilder builder = XContentFactory.jsonBuilder() .startObject() .startObject("article") .startObject("properties") .startObject("id") .field("type", "long") .field("store", true) .endObject() .startObject("title") .field("type", "text") .field("store", true) .field("analyzer", "ik_smart") .endObject() .startObject("content") .field("type", "text") .field("store", true) .field("analyzer", "ik_smart") .endObject() .endObject() .endObject() .endObject(); client.admin().indices().preparePutMapping("index") .setType("article") .setSource(builder) .get();
- 添加文档
XContentBuilder builder = XContentFactory.jsonBuilder() .startObject() .field("id",1L) .field("title","央视快评:勇做敢于斗争善于斗争的战士") .field("content","9月3日,习近平总书记在中央党校(国家行政学院)中青年干部培训班开班式上发表重要讲话强调,广大干部特别是年轻干部要经受严格的思想淬炼、政治历练、实践锻炼,发扬斗争精神,增强斗争本领,为实现“两个一百年”奋斗目标、实现中华民族伟大复兴的中国梦而顽强奋斗。") .endObject();client.prepareIndex("index","article","1") .setSource(builder) .get();
- POJO添加文档
Article article = new Article(); article.setId(3L); article.setTitle("3央视快评:勇做敢于斗争善于斗争的战士"); article.setContent("9月3日3333,(国家行政学院)中青年干部培训班开班式上发表重要讲话强调,广大干部特别是年"); String json = new ObjectMapper().writeValueAsString(article);client.prepareIndex("index","article","3") .setSource(json, XContentType.JSON) .get();
查询
- 根据ID
QueryBuilder queryBuilder = QueryBuilders.idsQuery().addIds("1","2");SearchResponse response = client.prepareSearch("index") .setTypes("article") .setQuery(queryBuilder) .get();SearchHits hits = response.getHits();System.out.println("总记录:"+hits);SearchHit[] ret = hits.getHits();for (SearchHit documentFields : ret) { Map<String, Object> map = documentFields.getSourceAsMap(); System.out.println("id:"+map.get("id")); System.out.println("title:"+map.get("title")); System.out.println("content:"+map.get("content")); System.out.println("-------------------");}
- 根据term
QueryBuilder queryBuilder = QueryBuilders.termQuery("title","斗争");
- 根据queryString
QueryBuilder queryBuilder = QueryBuilders.queryStringQuery("青年强调") .defaultField("content");
- 分页查询
SearchResponse response = client.prepareSearch("index") .setTypes("article") .setQuery(queryBuilder) .setFrom(10) .setSize(5) .get();
- 高亮显示结果
HighlightBuilder highlightBuilder = new HighlightBuilder();highlightBuilder.field(highlight);highlightBuilder.preTags("<em>");highlightBuilder.postTags("</em>");SearchResponse response = client.prepareSearch("index") .setTypes("article") .setQuery(queryBuilder) .highlighter(highlightBuilder) .get();SearchHits hits = response.getHits();System.out.println("总记录:"+hits.getTotalHits());SearchHit[] ret = hits.getHits();for (SearchHit documentFields : ret) { Map<String, Object> map = documentFields.getSourceAsMap(); System.out.println("id:"+map.get("id")); System.out.println("content:"+map.get("content")); Map<String, HighlightField> highlightFields = documentFields.getHighlightFields(); System.out.println(highlightFields.get(highlight).getFragments()[0]); System.out.println("-------------------");}
es操作过程
写过程
客户端选择一个协调节点(coordinating node)发送请求,协调节点将请求转发给对应的node对应的node在primary shard上处理请求,并同步到replica shard上
写过程原理
数据先写入内存 buffer,然后每隔 1s,将数据 refresh 到 os cache,到了 os cache 数据就能被搜索到
每隔 5s,将数据写入 translog 文件(这样如果机器宕机,内存数据全没,最多会有 5s 的数据丢失),translog 大到一定程度,或者默认每隔 30mins,会触发 commit 操作,将缓冲区的数据都 flush 到 segment file 磁盘文件中
读过程
客户端选择一个协调节点(coordinating node)发送根据ID查询请求,协调节点会根据id进行哈希,得到doc所在的分片,将请求转发到对应的node这个node然后会在primary shard与replica中使用随机轮询,进行负载均衡,返回document给协调节点协调节点再把document返回给客户端
搜索过程
客户端发送搜索请求给协调节点,协调节点将这个请求发送给所有的shard每个shard将自己的搜索结构返回给协调节点由协调节点进行数据的合并、排序、分页等操作,产出最终结果接着协调节点根据id再去查询对应的document的数据,返回给客户端
删除/更新过程
删除操作,会生成一个对应document id的.del文件,标识这个document被删除如果是更新操作,就是将原来的 doc 标识为 deleted 状态,然后新写入一条数据
每refresh一次,会生成一个segment file,系统会定期合并这些文件,合并这些文件的时候,会物理删除标记.del的document
性能优化
杀手锏:filesystem cache
在es中,doc的字段尽量只存储要被搜索的字段,这样可以节省内存,存放更多数据,做缓存效果更好
数据预热
对于一些热点数据,也要通过一些方式让它在缓存中
冷热分离
保证热点数据都在缓存里,提高系统性能
doc模型设计
对于一些复杂的关联,最好在应用层面就做好,对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的
分页性能优化
由于分页操作是由协调节点来完成的,所以翻页越深,性能越差解决:
- 不允许深度翻页
- 将翻页设计成不允许跳页,只能一页一页翻
kibana
Kibana是一个基于Node.js的Elasticsearch索引库数据统计工具,可以利用Elasticsearch的聚合功能,生成各种图表,如柱形图,线状图,饼图等。
而且还提供了操作Elasticsearch索引数据的控制台,并且提供了一定的API提示,非常有利于我们学习Elasticsearch的语法。
- docker
docker pull kibana:5.6.8 # 拉取镜像docker run -d --name kibana --net somenetwork -p 5601:5601 kibana:5.6.8 # 启动
SpringDataElasticSearch
配置
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jpa="http://www.springframework.org/schema/data/jpa" xmlns:elasticsearch="http://www.springframework.org/schema/data/elasticsearch" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/data/jpa https://www.springframework.org/schema/data/jpa/spring-jpa.xsd http://www.springframework.org/schema/data/elasticsearch http://www.springframework.org/schema/data/elasticsearch/spring-elasticsearch.xsd"> <elasticsearch:transport-client id="esClient" cluster-name="docker-elasticsearch" cluster-nodes="my-pc:9300"/> <elasticsearch:repositories base-package="wang.ismy.es"/> <bean id="elasticsearchTemplate" class="org.springframework.data.elasticsearch.core.ElasticsearchTemplate"> <constructor-arg name="client" ref="esClient"/> </bean></beans>
@Document(indexName = "index1",type = "article")@Datapublic class Article { @Id @Field(type = FieldType.Long,store = true) private long id; @Field(type = FieldType.Text,store = true) private String title; @Field(type = FieldType.Text,store = true) private String content;}
@Repositorypublic interface ArticleDao extends ElasticsearchRepository<Article,Long> { }
创建索引
ElasticsearchTemplate template = context.getBean(ElasticsearchTemplate.class);template.createIndex(Article.class);
添加文档
Article article = new Article();article.setId(1L);article.setTitle("【中国稳健前行】“中国之治”的政治保证");article.setContent("新中国成立70年来,在中国共产党的坚强领导下,...");articleDao.save(article);
删除文档
articleDao.deleteById(1L);articleDao.deleteAll(); // 全部删除
修改文档
同添加文档
查询
- 查询全部
articleDao.findAll().forEach(System.out::println);
- 根据ID
System.out.println(articleDao.findById(2L).get());
自定义查询
@Repositorypublic interface ArticleDao extends ElasticsearchRepository<Article,Long> { List<Article> findAllByTitle(String title);}
- 分页查询
List<Article> findAllByTitle(String title, Pageable pageable);articleDao.findAllByTitle("中", PageRequest.of(0,5)).forEach(System.out::println);
- 原生查询
NativeSearchQuery query = new NativeSearchQueryBuilder() .withQuery(QueryBuilders.queryStringQuery("中国").defaultField("title")) .withPageable(PageRequest.of(0,5)) .build();template.queryForList(query,Article.class).forEach(System.out::println);