什么是ElasticSearch
ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,提供基于Restful标准的API接口。
ElasticSearch基于Java实现,是当前最流行的企业级搜索引擎,设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。
ElasticSearch不仅能对海量规模的数据完成分布式索引与检索,还能提供数据聚合分析功能。
优点
- 速度快,近实时查询
- 索引和检索数据量大,支持TB级数据
- 高扩展,高可用
基本概念
关系数据库 | ES |
数据库Database | 索引Index |
表Table | 类型Type |
数据行Row | 文档Document |
数据列Column | 字段Field |
表结构Schema | 映射Mapping |
接近实时(NRT)
ES是一个接近实时的搜索平台。这意味着从索引一个文档到这个文档能够被搜索到有一个很小的延迟(通常不到1s)。
索引(index)
ES将它的数据存储在一个或多个索引(index)中。索引就像数据库,可以向索引写入文档或者从索引读取文档,并通过ES内部使用Lucene写入索引或检索索引。
文档(document)
文档(document)是ES中的主要实体。ES的搜索最终可以归结为对文档的搜索。文档由字段构成。
映射(mapping)
所有文档写入索引之前都会进行分析,如何将输入的文本分割为词条,哪些词条会被过滤,这种行为叫做映射(mapping)。一般由用户自定义规则。
类型(type)
每个文档都有与之对应的类型(type)定义。这允许用户在一个索引中存储多种文档类型,并为不同文档提供不同的映射。
数据源(river)
代表ES的一个数据源,也是其他存储方式(如数据库)同步数据到ES的一个方法。它是以插件方式存在的一个服务,通过读取river中的数据并将它索引到ES中。官方提供的River有couchDB、RabbitMQ等等。
网关(gateway)
代表ES索引的持久化存储方式,ES默认是先把索引写入内存,当内存满了再持久化到硬盘。当这个ES集群关闭再重启时,就会从网关中读取索引数据。ES支持多种类型的Gateway,有本地文件系统(默认)、分布式文件系统、Hadoop的HDFS、amazon的s3服务等。
自动发现(discovery.zen)
代表ES的自动发现节点机制,ES是一个基于p2p的系统,它先通过广播寻找存在的节点,再通过多播协议来进行节点之间的通讯,同时也支持点对点的交互。
通信(transport)
代表ES内部节点或集群与客户端的交互方式,默认内部是使用tcp协议交互,同时也支持http协议(json格式)、thrift、servlet、memcached、zeroMQ等传输协议(通过插件集成)。
节点见通信端口默认:9300-9400
集群(cluster)
代表一个集群,集群中有多个节点(node),其中一个为主节点,这个主节点是可以通过选举产生的,主从节点是对于集群内部来说的。
ES的一个概念就是去中心化,字面理解就是无中心节点,这是对于集群外部来说的。从外部来看ES集群,在逻辑上是一个整体,任何一个节点的通信都是等价的。
分片(shards)
代表索引分片,ES可以把一个完整的索引分为多个分片。这样的好处是可以把一个大的索引拆分成多个,分布到不同的节点上,构成分布式索引。分片的数量只能在索引创建前指定,并且索引创建后不能更改。
副本(replicas)
代表索引副本,ES可以设置多个索引的副本。副本可以提供系统的容错性,当某个节点某个分片损坏或丢失时可以从副本中恢复。副本也可以提高ES的查询效率,ES会自动对搜索请求进行负载均衡。
数据恢复(recovery)
也叫数据重新分布,ES在有节点加入或退出时会根据机器的负载对索引分片进行重新分配,挂掉的节点重新启动也会进行数据恢复。
GET /_cat/health?v #可以看到集群状态
分片和复制(shards and replicas)
一个索引可以存储超过单个节点硬件限制的大量数据,为了解决这个问题,ES提供了将索引划分成多片的能力,这些片叫分片。当你创建一个索引时,你可以指定想要的分片数量。每个分片本身也是一个功能完善且独立的索引,这个索引可以被放置到集群中的任何节点上。
分片的出现允许水平分割/扩展内容容量,允许在分片之上进行分布式的并行的操作,进而提高系统的性能吞吐量。
ES允许创建分片的一份或多份拷贝,作为故障转移机制的一部分,这些拷贝叫做复制分片,或者直接叫复制。
复制提高了系统的可用性,同时搜索也可以在复制上进行,可以扩展系统的搜索量/吞吐量。
每个索引可以被分成多个分片,一个索引也可以被复制多份。
分片和复制的数量可以在索引创建时指定。在索引创建之后,可以动态地改变复制的数量,但是不能改变分片的数量。
安装配置
解压,进入bin目录,可以看到启动脚本
运行启动
可以看到绑定了两个端口:
- 9300:Java程序访问的端口
- 9200:浏览器、postman访问的端口
访问:http://127.0.0.1:9200
安装ik分词器
下载插件:https://github.com/medcl/elasticsearch-analysis-ik/releases ,Elasticsearch和IK分词器必须版本一致。
直接解压放到\plugins目录。
启动ES,可以看到加载的插件
elasticsearch.yml文件
# 集群名称
cluster.name: es-demo
# 节点名称
node.name: node-1
# 既可以选举为主节点,也可以存储数据,也可作为负载器
node.master: true
node.data: true
# 数据存储地址
path.data: /opt/apps/es/data
# 日志
path.logs: /opt/apps/es/logs
# 临时文件
path.work: /opt/apps/es/tmp
# 索引分片数,默认5片
index.number_of_shards: 3
# 索引副本数,默认1个
index.number_of_replicas: 2
# 启动时锁住内存,保证不会swap
bootstrap.memory_lock: true
bootstrap.system_call_filter: false
# 绑定的ip地址
network.host: 0.0.0.0
# 参与集群的端口
transport.tcp.port: 9300
# http端口号
http.port: 9200
# 内容的最大容量
http.max_content_length: 100mb
# Discovery配置
discovery.zen.ping_timeout: 30s
# 防止集群发生脑裂
discovery.zen.minimun_master_nodes: 2
discovery.zen.fd.ping_timeout: 30s
# 单播发现地址
discovery.zen.ping.unicast.hosts: ["192.168.1.100:9300", "192.168.1.101:9300", "192.168.1.102:9300"]
# discovery.zen.ping.multicast.enabled: false
# 最多等待5分钟,如果没有上线就重新rebalance
gateway.recover_after_time: 5m
# 3个节点上线后,才会进行shard recovery
gateway.recover_after_nodes: 3
# 最小节点数量
gateway.expected_nodes: 3
# 删除索引库必须显示指定,禁止删除所有索引,推荐生产环境使用
action.destructive_requires_name: true
# cors配置
http.cors.enabled: true
http.cors.allow-origin: "*"
集群原理
ES集群包括三种角色。
master节点
整个集群只会有一个master节点,负责维护集群状态信息(元数据metadata),可以存储数据,但是不建议用master节点来存储索引数据。master节点需要从众多可以成为master的节点中选举产生一个。
- 负责集群节点的上下线,shard分配的重新分配。
- 创建、删除索引。
- 负责接收集群状态的变化,并推送给所有节点。每个节点都有一份完整的cluster state,只是master节点负责维护。
- 利用自身空闲资源,协调创建索引的请求或查询请求,并分发到其他节点。
配置
node.master: true
node.data: false
data节点
负责索引数据的存储和读写,data节点消耗内存和磁盘IO的性能较大。
配置
node.master: false
node.data: true
client节点(负载均衡节点)
不会被选为master节点,也不会存储索引数据。主要用于查询负载均衡,将请求分发给多个node节点,并对结果进行汇总。
配置
node.master: false
node.data: false
集群搭建规划
内存
ES很占内存,JVM占用的比较小,主要是Lucene。Lucene基于OS文件系统缓存,将频繁的读写磁盘文件到内存进行缓存以提高性能,所以需要足够的内存支持。
数据量:上亿建议每台机器64G内存。
CPU
ES集群对CPU要求比较低,核心数可以提高并发能力,一般多核即可。
磁盘
需要频繁读写磁盘,推荐SSD或RAID。
网络
ES集群是p2p模式的分布式架构,所有node都是相当的,任意两个node之间的互相通信都会很频繁。因此要避免异地多机房,保证低延迟+高速。
JVM
保证所有环境JDK版本一致,防止client和server由于版本不一致,出现序列化问题。尽量使用最新版本。
容量规划
建议数据在10亿以内,数据量最好恒定。
先计算数据硬盘容量,总内存数。ES会用一半给到JVM,剩下的一半,硬盘容量 * 1.5 就足够了。一般10亿数量,5台左右8核64G足够,如何查询太复杂,则需要加内存。
Java实战
搜索操作Demo
public class SearchDemo {
private TransportClient client;
@Before
public void init() throws Exception {
// 1. 指定ES集群
Settings settings = Settings.builder().put("cluster.name", "es—demo").build();
// 2. 创建访问ES服务器的客户端
client = new PreBuiltTransportClient(settings);
client.addTransportAddress(new TransportAddress(InetAddress.getByName("192.168.1.100"), 9300));
}
@Test
public void testSearchIndex() {
try {
// 3. 数据查询展示
GetResponse response = client.prepareGet("index1", "blog", "10").execute().actionGet();
String sourceAsString = response.getSourceAsString();
System.out.println(sourceAsString);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 批量MultiGet
*/
@Test
public void testMultiGet() {
try {
// 3. 查询 按索引 类型和id 返回的是数组
MultiGetResponse response = client.prepareMultiGet().add("index", "blog", "8", "10")
.add("lib", "user", "1", "2", "3").get();
// 4. 遍历输出一下
for (MultiGetItemResponse item : response) {
GetResponse gr = item.getResponse();
if (gr != null && gr.isExists()) {
System.out.println(gr.getSourceAsString());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 批量 - bulk
*/
@Test
public void testBulk() {
try {
BulkRequestBuilder builder = client.prepareBulk();
// 批量添加
builder.add(client.prepareIndex("lib2", "books", "8").setSource(XContentFactory.jsonBuilder().startObject()
.field("title", "python").field("price", 99).endObject()));
builder.add(client.prepareIndex("lib2", "books", "8").setSource(
XContentFactory.jsonBuilder().startObject().field("title", "VR").field("price", 199).endObject()));
// 执行操作
BulkResponse response = builder.get();
// 查看状态
System.out.println(response.status());
// 查看是否有失败情况
if (response.hasFailures()) {
System.out.println("error");
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 查询全部 matchAllQuery
*/
@Test
public void testMatchAllQuery() {
try {
QueryBuilder qb = QueryBuilders.matchAllQuery();
SearchResponse sr = client.prepareSearch("index1").setQuery(qb).setSize(3).get();
SearchHits hits = sr.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
Map<String, Object> map = hit.getSourceAsMap();
for (Object key : map.keySet()) {
System.out.println(key + " = " + map.get(key));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 查询 match
*/
@Test
public void testMatchQuery() {
try {
QueryBuilder queryBuilder = QueryBuilders.matchQuery("title", "搜索");
SearchResponse response = client.prepareSearch("index1").setQuery(queryBuilder).setSize(3).get();
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
Map<String, Object> map = hit.getSourceAsMap();
for (Object key : map.keySet()) {
System.out.println(key + " = " + map.get(key));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 查询 multiMatch
*/
@Test
public void testMultiMatchQuery() {
try {
QueryBuilder queryBuilder = QueryBuilders.multiMatchQuery("搜索", "title", "content");
SearchResponse response = client.prepareSearch("index1").setQuery(queryBuilder).setSize(3).get();
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
Map<String, Object> map = hit.getSourceAsMap();
for (Object key : map.keySet()) {
System.out.println(key + " = " + map.get(key));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* term查询
*/
@Test
public void testTermQuery() {
try {
QueryBuilder queryBuilder = QueryBuilders.termQuery("content", "搜索");
SearchResponse response = client.prepareSearch("index1").setQuery(queryBuilder).setSize(3).get();
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
Map<String, Object> map = hit.getSourceAsMap();
for (Object key : map.keySet()) {
System.out.println(key + " = " + map.get(key));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* terms 查询
*/
@Test
public void testTermsQuery() {
try {
QueryBuilder queryBuilder = QueryBuilders.termsQuery("content", "搜索", "功能");
SearchResponse response = client.prepareSearch("index1").setQuery(queryBuilder).setSize(3).get();
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
Map<String, Object> map = hit.getSourceAsMap();
for (Object key : map.keySet()) {
System.out.println(key + " = " + map.get(key));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* range prefix wildcard fuzzy ids 查询
*/
@Test
public void test14() {
try {
// range 查询
// QueryBuilder queryBuilder=
// QueryBuilders.rangeQuery("postdate").from("2021-01-01").to("2021-03-01").format("yyyy-MM-dd");
// prefix 查询
// QueryBuilder queryBuilder= QueryBuilders.prefixQuery("title", "搜索");
// whildcard 查询
// QueryBuilder queryBuilder= QueryBuilders.wildcardQuery("content", "在*");
// fuzzy 查询
// QueryBuilder queryBuilder= QueryBuilders.fuzzyQuery("content", "solr");
// ids 查询
QueryBuilder queryBuilder = QueryBuilders.idsQuery().addIds("8", "10");
SearchResponse response = client.prepareSearch("index1").setQuery(queryBuilder).setSize(3).get();
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
Map<String, Object> map = hit.getSourceAsMap();
for (Object key : map.keySet()) {
System.out.println(key + " = " + map.get(key));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 聚合查询
*/
@Test
public void test15() {
try {
// 最大值
// AggregationBuilder agg = AggregationBuilders.max("aggMax").field("postdate");
// SearchResponse response =
// client.prepareSearch("index1").addAggregation(agg).get();
// Max max = response.getAggregations().get("aggMax");
// System.out.println(max.getValue());
// 最小值
// AggregationBuilder agg = AggregationBuilders.min("aggMin").field("postdate");
// SearchResponse response =
// client.prepareSearch("index1").addAggregation(agg).get();
// Min min = response.getAggregations().get("aggMin");
// System.out.println(min.getValue());
// 平均值
// AggregationBuilder agg = AggregationBuilders.avg("aggAvg").field("postdate");
// SearchResponse response =
// client.prepareSearch("index1").addAggregation(agg).get();
// Avg avg = response.getAggregations().get("aggAvg");
// System.out.println(avg.getValue());
// 基数
AggregationBuilder agg = AggregationBuilders.cardinality("aggCardinality").field("postdate");
SearchResponse response = client.prepareSearch("index1").addAggregation(agg).get();
Cardinality cardinality = response.getAggregations().get("aggCardinality");
System.out.println(cardinality.getValue());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* query string
*
*/
@Test
public void test16() {
try {
// 全文检索(部分条件都满足即可) + 必须有 - 必须没有
QueryBuilder queryBuilder = QueryBuilders.simpleQueryStringQuery("+搜索 -ES");
SearchResponse response = client.prepareSearch("index1").setQuery(queryBuilder).setSize(3).get();
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
Map<String, Object> map = hit.getSourceAsMap();
for (Object key : map.keySet()) {
System.out.println(key + " = " + map.get(key));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 组合查询
*
* boolQuery must 必须有 mustNot 必须没有 should 或者 filter 过滤 constantscore
*
*/
@Test
public void test17() {
try {
// constantscore
QueryBuilder queryBuilder = QueryBuilders.constantScoreQuery(QueryBuilders.termQuery("name", "hah"));
SearchResponse response = client.prepareSearch("index1").setQuery(queryBuilder).setSize(3).get();
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
Map<String, Object> map = hit.getSourceAsMap();
for (Object key : map.keySet()) {
System.out.println(key + " = " + map.get(key));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 分组聚合
*/
@Test
public void testterms() {
try {
AggregationBuilder agg = AggregationBuilders.terms("terms").field("age");
SearchResponse response = client.prepareSearch("index1").addAggregation(agg).execute().get();
Terms terms = response.getAggregations().get("terms");
for (Terms.Bucket entry : terms.getBuckets()) {
System.out.println(entry.getKey() + ": " + entry.getDocCount());
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* filter聚合
*/
@Test
public void testfilter() {
try {
QueryBuilder query = QueryBuilders.termQuery("filter", "搜索");
AggregationBuilder agg = AggregationBuilders.filter("filter", query);
SearchResponse response = client.prepareSearch("index1").addAggregation(agg).execute().get();
Filter filter = response.getAggregations().get("filter");
System.out.println(filter.getDocCount());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* filters聚合
*/
@Test
public void testfilters() {
try {
// QueryBuilder query = QueryBuilders.termQuery("filters", "搜索");
AggregationBuilder agg = AggregationBuilders.filters("filters",
new FiltersAggregator.KeyedFilter("changge", QueryBuilders.termQuery("content", "change")),
new FiltersAggregator.KeyedFilter("hejiu", QueryBuilders.termQuery("content", "hejiu")));
SearchResponse response = client.prepareSearch("index1").addAggregation(agg).execute().get();
Filters filters = response.getAggregations().get("filters");
for (Filters.Bucket entry : filters.getBuckets()) {
System.out.println(entry.getKey() + ": " + entry.getDocCount());
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* range聚合
*/
@Test
public void testrange() {
try {
AggregationBuilder agg = AggregationBuilders.range("range").field("age").addUnboundedTo(50)// (,to)
.addRange(20, 50)// [form,)
.addUnboundedFrom(25);
SearchResponse response = client.prepareSearch("index1").addAggregation(agg).execute().get();
Range range = response.getAggregations().get("range");
for (Range.Bucket entry : range.getBuckets()) {
System.out.println(entry.getKey() + ": " + entry.getDocCount());
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* missing聚合
*/
@Test
public void testmissing() {
try {
AggregationBuilder agg = AggregationBuilders.missing("missing").field("age");
SearchResponse response = client.prepareSearch("index1").addAggregation(agg).execute().get();
Aggregation aggregation = response.getAggregations().get("missing");
System.out.println(aggregation.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
索引操作Demo
public class IndexDemo {
private TransportClient client;
@Before
public void init() throws Exception {
// 1. 指定ES集群
Settings settings = Settings.builder().put("cluster.name", "es—demo").build();
// 2. 创建访问ES服务器的客户端
client = new PreBuiltTransportClient(settings);
client.addTransportAddress(new TransportAddress(InetAddress.getByName("192.168.1.100"), 9300));
}
@Test
public void testCreateIndex() throws Exception {
// 3. 指定要添加的文档
XContentBuilder doc = XContentFactory.jsonBuilder().startObject().field("id", "1")
.field("title", "玩转搜索(二)-- Solr实战").field("content", "Solr是一个独立的企业级搜索应用服务器,它对外提供API接口。")
.field("postdate", "2021-02-06").field("url", "https://blog.csdn.net/zwt122755527/article/details/113553242").endObject();
// 4. 添加数据,添加到es服务器中
IndexResponse response = client.prepareIndex("index1", "blog", "10").setSource(doc).get();
// 5. 查看添加文档是否成功
System.out.println(response.status());
}
@Test
public void testDeleteIndex() {
// 3. 指定要删除的文档
DeleteResponse response = client.prepareDelete("index1", "blog", "10").get();
// 5. 查看添加文档是否成功//删除成功返回OK,否则返回NOT_FOUND
System.out.println(response.status());
}
/**
* 查询删除
*/
@Test
public void testDeleteByQuery() {
try {
MatchQueryBuilder builder = QueryBuilders.matchQuery("title", "搜索");
BulkByScrollResponse response = DeleteByQueryAction.INSTANCE.newRequestBuilder(client).filter(builder)
.source("index1").get();
long count = response.getDeleted();
System.out.println(count);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 更新文档 - 直接更新
*/
@Test
public void testUpdateIndex() {
try {
// 3. 编辑要修改的doc内容
XContentBuilder doc = XContentFactory.jsonBuilder().startObject().field("title", "Solr").endObject();
// 4. 创建update的请求
UpdateRequest request = new UpdateRequest();
// 5. 指定要修改的索引/类型/id
request.index("index1").type("blog").id("10").doc(doc);
// 6. 提交update请求 更新成功返回OK,否则返回NOT_FOUND
UpdateResponse response = client.update(request).get();
// 7. 查看添加文档是否成功
System.out.println(response.status());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 更新文档-upset方式
*/
@Test
public void testUpsertIndex() {
try {
// 3.1 指定要添加的文档-如果update 失败的话,没有这个值 会进行添加操作
XContentBuilder indexdoc = XContentFactory.jsonBuilder().startObject().field("id", "2")
.field("title", "玩转搜索(一)-- 全文检索与Lucene实现").field("content", "全文检索的概念")
.field("postdate", "2021-01-24").field("url", "https://blog.csdn.net/zwt122755527/article/details/112616773").endObject();
// 3.2 指定要修改的文档
XContentBuilder updatedoc = XContentFactory.jsonBuilder().startObject().field("title", "玩转搜索").endObject();
// 4.1 编辑要添加的doc的请求
IndexRequest indexRequest = new IndexRequest("index1", "blog", "8").source(indexdoc);
// 4.1 编辑要修改的doc的请求,如果修改失败会进行upsert操作
UpdateRequest updateRequest = new UpdateRequest("index1", "blog", "8").doc(updatedoc).upsert(indexRequest);
// 5. 提交update请求 操作成功返回OK,否则返回NOT_FOUND
UpdateResponse response = client.update(updateRequest).get();
// 6. 查看添加文档是否成功
System.out.println(response.status());
} catch (Exception e) {
e.printStackTrace();
}
}
@Test
public void test1() {
try {
ClusterHealthResponse healths = client.admin().cluster().prepareHealth().get();
String clusterName = healths.getClusterName();
System.out.println("clusterName = " + clusterName);
int numberOfNodes = healths.getNumberOfNodes();
System.out.println("numberOfNodes = " + numberOfNodes);
for (ClusterIndexHealth health : healths.getIndices().values()) {
String index = health.getIndex();
int numberOfShards = health.getNumberOfShards();
int numberOfReplicas = health.getNumberOfReplicas();
ClusterHealthStatus status = health.getStatus();
System.out.println(status.toString());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}