项目演示代码:https://github.com/kudryavka1/es-demo
对于Elasticsearch 作为主数据库需要注意:Elasticsearch 当大批量并发写入数据时,写入的数据不能立即查询到,会有1-3秒的延迟。
ElasticSearch,简称es,es是一个开源的高拓展的分布式全文检索引擎,它可以近乎实施的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理PB级别的数据。es也使用java开发并使用Lucene 作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的RESTful API来隐藏Lucene的复杂性,从而让全文搜索变得简单。
Solr是Apache下的一个顶级开源项目,采用java开发,是基于Lucene的全文搜索服务器。Solr提供了比Lucene更为丰富的查询语言,同时实现了可配置、可扩展、并对索引、搜索性能进行了优化。它可以独立运行,是一个独立的企业及搜索应用服务器,它对外提供类似于web-service的API接口。用户可以通过http请求,像搜索引擎服务器提交一定格式的文件,生成索引;也可以通过提出查找请求,并得到返回结果。
两者比较
个人总结: Elasticsearch 是基于Lucene封装的搜索工具(底层还是使用Lucene),Elasticsearch(Lucene)性能强大在于它使用了倒排索引进行保存数据。在数据量较少时,Solr的速度高于Elasticsearch,随着数据量的增加,Solr的搜索效率会变得更低,而Elasticsearch却没有明显的变化。具体可查阅博客https://blog.csdn.net/jetty_welcome/article/details/104342595
Elasticsearch 中的名词和传统数据库进行类比:
倒排索引
es的搜索快的原因
Elasticsearch 的安装,需要用到Elasticsearch的本体,和ik分词器。同时可以再部署一个Elasticsearch head 来监控Elasticsearch的运行状态。
注意:Elasticsearch插件的版本 必须和主版本一致!(所有文件版本一致)
可以参考博客https://www.cnblogs.com/tjp40922/p/12194739.html
记得安装完,要开放9200端口!
elasticsearch.yml 的配置(目前ES的配置)
#集群名称
cluster.name: my-application
#节点名称
node.name: node-1
network.host: 0.0.0.0
http.port: 9200
cluster.initial_master_nodes: ["node-1"]
#开启跨域访问支持,默认为false
http.cors.enabled: true
#跨域访问允许的域名地址,(允许所有域名)以上使用正则
http.cors.allow-origin: /.*/
xpack.ml.enabled: false
比如会将「中华人民共和国国歌」拆分为:中华人民共和国、中华人民、中华、华人、人民共和国、人民、人、民、共和国、共和、和、国国、国歌,会穷尽各种可能的组合;
比如会将「中华人民共和国国歌」拆分为:中华人民共和国、国歌。
参考 https://zq99299.github.io/note-book/elasticsearch-senior/ik/30-ik-introduce.html
下载 https://github.com/mobz/elasticsearch-head
进入项目目录
npm install
npm run start
在浏览器访问http://localhost:9100 进入主页面
PUT /shopping // 创建一个索引
GET /shopping //查看一个索引
DELETE /shopping // 删除shopping索引
POST /shopping/_doc/1 //如果不指定ID,则es随机帮我们生成
{
"title":"小米11手机",
"category":"小米11",
"images":"http://www.gulixueyuan.com/xm.jpg",
"price":3999.00
}
返回结果如下
{
"_index": "shopping",
"_type": "_doc",
"_id": "1", //自定义唯一性标识
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 1,
"_primary_term": 1
}
POST /shopping/_update/1
{
"doc": {
"title":"小米手机",
"category":"小米"
}
}
返回结果如下
{
"_index": "shopping",
"_type": "_doc",
"_id": "1",
"_version": 3,
"result": "updated", // 表示数据被更新
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 3,
"_primary_term": 1
}
删除一个文档不会立即从磁盘上移除,它只是被标记成已删除(逻辑删除)。
DELETE /shopping/_doc/1
返回结果如下
{
"_index": "shopping",
"_type": "_doc",
"_id": "1",
"_version": 4,
"result": "deleted", //删除成功
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 4,
"_primary_term": 1
}
再重新请求,查看是否删除成功
GET /shopping/_doc/1
{
"_index": "shopping",
"_type": "_doc",
"_id": "1",
"found": false
}
GET /shopping/_search
{
"query":{
"match":{
"category":"小米"
}
}
}
返回结果如下
{
"took": 3,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 3,
"relation": "eq"
},
"max_score": 1.3862942,
"hits": [
{
"_index": "shopping",
"_type": "_doc",
"_id": "ANQqsHgBaKNfVnMbhZYU",
"_score": 1.3862942,
"_source": {
"title": "小米手机",
"category": "小米",
"images": "http://www.gulixueyuan.com/xm.jpg",
"price": 3999
}
},
{
"_index": "shopping",
"_type": "_doc",
"_id": "A9R5sHgBaKNfVnMb25Ya",
"_score": 1.3862942,
"_source": {
"title": "小米手机",
"category": "小米",
"images": "http://www.gulixueyuan.com/xm.jpg",
"price": 1999
}
},
{
"_index": "shopping",
"_type": "_doc",
"_id": "BNR5sHgBaKNfVnMb7pal",
"_score": 1.3862942,
"_source": {
"title": "小米手机",
"category": "小米",
"images": "http://www.gulixueyuan.com/xm.jpg",
"price": 1999
}
}
]
}
}
GET /_cat/count/index_name?v
返回数据:
epoch timestamp count
1629278159 09:15:59 10372015
在pom目录中 引入 es 注意版本要对应
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>${elasticsearch.version}</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>${elasticsearch.version}</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>${elasticsearch.version}</version>
</dependency>
配置Config类
@Configuration
public class ElasticSearchClientConfig {
private final static String HOST = "121.40.68.176";
private final static Integer PORT = 9200;
private final static String SCHEME = "http";
@Bean
public RestHighLevelClient restHighLevelClient(){
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost(HOST,PORT,SCHEME))
);
return client;
}
}
这里以潍柴项目作为举例
public String save(EsNews esNews) throws IOException {
IndexRequest request = new IndexRequest(indexName);
request.id(esNews.getId());
request.source(JSONObject.toJSONString(esNews),XContentType.JSON);
IndexResponse index = restHighLevelClient.index(request, RequestOptions.DEFAULT);
return index.status().toString();
}
public String deleted(String id) throws IOException {
DeleteRequest deleteRequest = new DeleteRequest(indexName,id);
DeleteResponse delete = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
return delete.status().toString();
}
public String update(EsNews esNews) throws IOException {
UpdateRequest request = new UpdateRequest(indexName, esNews.getId());
request.doc(JSONObject.toJSONString(esNews),XContentType.JSON);
UpdateResponse update = restHighLevelClient.update(request, RequestOptions.DEFAULT);
return update.status().toString();
}
public EsNews getById(String id) throws IOException {
GetRequest getRequest = new GetRequest(indexName, id);
GetResponse response = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
EsNews esNews = JSONObject.parseObject(response.getSourceAsString(), EsNews.class);
return esNews;
}
当ES新建,需要大量的数据从其他库导入时,可以调用以下方法。
该方法以MongoDB举例,MySQL同理
public Boolean saveNewsFromDbToEs() throws IOException {
for (int i = 0; ; i++) {
Integer size = 100;
Query query = new Query();
query.limit(size);
query.skip(i * size); // 分页保存,每次保存100条
List<EsNews> list = mongoTemplate.find(query, EsNews.class);
BulkRequest bulkRequest = new BulkRequest();
bulkRequest.timeout("10s");
if (list.isEmpty()){
break;
}
for (EsNews news : list) {
bulkRequest.add(new IndexRequest(indexName)
.id(news.getId()).source(JSONObject.toJSONString(news),XContentType.JSON));
}
restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT); //执行保存
}
return true;
}
es的搜索分为两种,**match和term。**match在匹配时会对所查找的关键词进行分词,然后按分词匹配查找,而term会直接对关键词进行查找。一般模糊查找的时候,多用match,而精确查找时可以使用term。
潍柴项目中,需要根据title和context进行模糊查询,所以这里使用match搜索,并使用should将两个条件关联(只要满足其一即可)
public List<Map<String, Object>> search(String title,String content) throws IOException {
SearchRequest searchRequest = new SearchRequest(indexName);
//构建搜索
MatchQueryBuilder matchQueryBuilder1 = QueryBuilders.matchQuery("title", title);
MatchQueryBuilder matchQueryBuilder2 = QueryBuilders.matchQuery("contentText", content);
BoolQueryBuilder must = QueryBuilders.boolQuery()
.must(matchQueryBuilder1)
.should(matchQueryBuilder2);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder()
.query(must)
// .query(matchQuery)
.timeout(TimeValue.timeValueSeconds(10))
.size(20);
//执行搜索
searchRequest.source(searchSourceBuilder);
SearchResponse search = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
SearchHits hits = search.getHits();
List<Map<String ,Object>> list = new ArrayList<>();
for (SearchHit hit : hits) {
if (hit.getScore() >= minScore){
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
sourceAsMap.put("source",hit.getScore());
list.add(sourceAsMap);
}
}
System.out.println("搜索出了"+hits.getHits().length+"条");
return list;
}
public List<Map<String, Object>> searchHighLight(String title,String content) throws IOException {
SearchRequest request = new SearchRequest(indexName);
MatchQueryBuilder matchQueryBuilder1 = QueryBuilders.matchQuery("title", title);
MatchQueryBuilder matchQueryBuilder2 = QueryBuilders.matchQuery("contentText", content);
BoolQueryBuilder must = QueryBuilders.boolQuery()
.must(matchQueryBuilder1)
.should(matchQueryBuilder2);
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("title").preTags("<span style='color:red'>").postTags("</span>");
highlightBuilder.field("contentText").preTags("<span style='color:red'>").postTags("</span>");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder()
.query(must)
// .query(matchQuery)
.timeout(TimeValue.timeValueSeconds(10))
.highlighter(highlightBuilder)
.size(20);
request.source(searchSourceBuilder);
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
//分析结果
List<Map<String,Object>> list = new ArrayList<>();
for (SearchHit hit : response.getHits()) {
// 解析高亮字段
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
HighlightField restitle = highlightFields.get("title");
HighlightField resContent = highlightFields.get("contentText");
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
if (restitle != null){
// 将原来的字段 换位新的高亮字段
Text[] fragments = restitle.getFragments();
String newTitle = "";
for (Text fragment : fragments) {
newTitle+=fragment;
}
sourceAsMap.put("title",newTitle); // 替换原先的字段
}
if (resContent != null){
// 将原来的字段 换位新的高亮字段
Text[] fragments = resContent.getFragments();
String newContentText = "";
for (Text fragment : fragments) {
newContentText+=fragment;
}
sourceAsMap.put("contentText",newContentText); // 替换原先的字段
}
list.add(sourceAsMap);
}
return list;
}
// 排序条件
FieldSortBuilder timeSortBuilder = SortBuilders.fieldSort("sourceTime").order(SortOrder.DESC);
// 默认score是倒序
ScoreSortBuilder scoreSortBuilder = new ScoreSortBuilder();
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder()
.query(must)
.timeout(TimeValue.timeValueSeconds(10))
.sort(timeSortBuilder) //先根据时间排序
.sort(scoreSortBuilder) //再根据得分排序
.size(10);
根据输入的字段进行聚合搜索,相当于sql语句中的group by
public Map<String,Object> aggsByField(String field) throws IOException {
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
TermsAggregationBuilder aggregation = AggregationBuilders.terms(field).field(field)
.subAggregation(AggregationBuilders.sum("sum_id").field("id")). //求和
subAggregation(AggregationBuilders.avg("avg_id").field("id")); //求平均值
sourceBuilder.aggregation(aggregation);
sourceBuilder.size(0);
SearchRequest searchRequest = new SearchRequest().indices(indexName).source(sourceBuilder);
SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
Aggregations aggregations = response.getAggregations();
ParsedStringTerms parsedStringTerms = aggregations.get(field);
Map<String,Object> map = new HashMap<>();
List<? extends Terms.Bucket> buckets = parsedStringTerms.getBuckets();
for (Terms.Bucket bucket : buckets) {
//获取数据
Aggregations bucketAggregations = bucket.getAggregations();
ParsedSum sumId = bucketAggregations.get("sum_id");
ParsedAvg avgId = bucketAggregations.get("avg_id");
map.put(bucket.getKey().toString(),bucket.getDocCount());
}
return map;
}
在创建索引时,如果不指定mapping,通过接口直接保存Java对象,遇见yyyy-mm-dd类型的字符串,ES不会将其识别为date类型,而是会识别为text。
如果想让ES存储类型改为date类型,方便日后进行搜索,那么在创建索引时,需要指定mapping属性;
例如: fetchTime 和 sourceTime 都需要指定为date类型
PUT /weichai_dev
{
"mappings": {
"properties" : {
...
"content" : {
"type" : "text",
"analyzer":"ik_smart"
},
...
"fetchTime" : {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"
},
...
"imageNames" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
...
}
}
}
ES想修改文档,可以有两种方式。
PUT 覆盖文档
PUT 会 根据ID,对文档进覆盖,如果PUT的文档包含空字段,那么空字段也会覆盖现有文档。
UPDATE 更新文档
UPDATE也是根据ID进行更新文档,但和PUT的覆盖不同,如果new一个对象,只赋值一个属性,再去更新,那么只会更新一个字段(不更新值为null的字段)。
term
term会直接对关键词进行查找,对文档不进行分词查询,精确查找时可以使用term。
terms
terms查询时,需要传值一个数组。相当于SQL中的in语法。对数组中的值进行term查找。
match
match在匹配时会对所查找的关键词进行分词,然后按分词匹配查找,一般模糊查找的时候,多用match。
matchAll
查询所有,相当于select * from table
PUT my_index/_mapping
{
"properties": {
"字段":{
"type": "类型"
}
}
}
POST my_index/_update_by_query
{
"script": {
"lang": "painless",
"source": "if (ctx._source.字段== null) {ctx._source.字段= '0'}"
}
}
注意:索引的字段mapping只能添加,不能修改,也不能修改对应的类型。如果需要修改字段,请删索引重新创。
在Elasticsearch中,如果不进行设置,往索引中保存文档时,当索引没有文档中的字段,索引会自动把该字段添加到mapping中去。
在保存文档时,我们不应该把具体的value当成mapping,这会造成索引的mapping随着文档的增加而无限制的增加。
目前潍柴项目中,mapping的设置就是反例:
面对这种场景,我们需要设置嵌套对象。
例:我们需要获取一个网站中所有的cookies,而cookies的类型是非常多的,如果不断的保存,会造成索引的mapping不断增加。
这里我们定义一个cookies嵌套对象。类似于我们Java中,给类定义一个List属性。
其中,cookies字段的type为nested。
定义mapping:
PUT /map_test
{
"mappings": {
"properties" : {
"url" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"cookies" :{
"type": "nested",
"properties": {
"name": {
"type": "keyword"
},
"value": {
"type": "text"
}
}
}
}
}
}
向索引中添加数据:
PUT /map_test/_doc/1
{
"url":"www.baidu.com",
"cookies": [
{
"name": "userId",
"value": "123456"
},
{
"name": "userName",
"value": "zhanghao"
},
{
"name": "system",
"value": "win10"
}
]
}
通过查询,我们可以看到我们刚刚添加的数据
{
"_index" : "map_test",
"_type" : "_doc",
"_id" : "1",
"_version" : 2,
"_seq_no" : 1,
"_primary_term" : 1,
"found" : true,
"_source" : {
"url" : "www.baidu.com",
"cookies" : [
{
"name" : "userId",
"value" : "123456"
},
{
"name" : "userName",
"value" : "zhanghao"
},
{
"name" : "system",
"value" : "win10"
}
]
}
}
查询mapping,发现mapping中也没有添加多余的字段。
GET /map_test/_mapping
{
"map_test" : {
"mappings" : {
"properties" : {
"cookies" : {
"type" : "nested",
"properties" : {
"name" : {
"type" : "keyword"
},
"value" : {
"type" : "text"
}
}
},
"url" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}
这里我们介绍如何创建nested,具体使用、查询nested字段篇幅太大,可以参考博客
https://blog.csdn.net/laoyang360/article/details/82950393
使用nested也有一定的缺点,更新nested字段时会修改整篇文档,同时查询会变得复杂。
如果嵌套字段需要频繁修改,建议不要使用nested,而去使用父子文档。
在我们聚合查询时,如果聚合的字段设置为null,则该文档聚合时对外不可见,会造成聚合不准。
例:求平均值
首先我们创建一个mapping
PUT /aggtest
{
"mappings": {
"properties" : {
"score" : {
"type": "float"
}
}
}
}
插入两条数据,其中一条的score为null
PUT /aggtest/_doc/1
{
"score": 5
}
PUT /aggtest/_doc/2
{
"score": null
}
然后我们对score字段聚合求平均数
POST /aggtest/_search
{
"size": 0,
"aggs": {
"avg": {
"avg": {
"field": "score"
}
}
}
}
查看返回结果
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"avg" : {
"value" : 5.0
}
}
可以看到,我们虽然有2个文档,一个为值为5一个为null,但平均数求出来为5,而total为2,这是不准确的。
要避免这种情况,我们需要在创建mapping时,指定null_value
PUT /aggtest
{
"mappings": {
"properties" : {
"score" : {
"type": "float",
"null_value": "1.0"
}
}
}
}
再重新聚合,求平均数,可以看到求出的平均数是准的了。
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"avg" : {
"value" : 3.0
}
}
为什么会出现该问题?
null 不能被索引或搜索。 当字段设置为 null(或空数组或 所有值为 null 值的数组)时,将其视为该字段没有值。使用 null_value
参数可以用指定的值替换显式的空值,以便可以对其进行索引和搜索。
ES插入大量的数据时报错:
TOO_MANY_REQUESTS/12/disk usage exceeded flood-stage watermark, index has read-only-allow-delete block
原因:
是因为一次请求中批量插入的数据条数巨多,以及短时间内的请求次数巨多引起ES节点服务器内存超过限制。
解决方法:
PUT _all/_settings
{
"index.blocks.read_only_allow_delete": null
}
[2021-08-18T11:15:24,911][WARN ][o.e.c.r.a.DiskThresholdMonitor] [node-1] flood stage disk watermark [95%] exceeded on [c0yTJqovTfyY3AFwR-FKHw][node-1][/home/elasticsearch/elasticsearch-7.14.0/data/nodes/0] free: 1.7gb[4.1%], all indices on this node will be marked read-only
原因:
es的磁盘不够了
解决方法:
对磁盘扩容,具体参考文章 https://www.cnblogs.com/straycats/p/11261364.html
ES 默认安装后设置的内存是 1GB,对于任何一个现实业务来说,这个设置都太小了。
如果是通过解压安装的 ES,则在 ES 安装文件中包含一个 jvm.option 文件,添加如下命令来设置 ES 的堆大小,Xms 表示堆的初始大小,Xmx 表示可分配的最大内存,都是 1GB。
确保 Xmx 和 Xms 的大小是相同的,其目的是为了能够在 Java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源,可以减轻伸缩堆大小带来的压力。
假设你有一个 64G 内存的机器,按照正常思维思考,你可能会认为把 64G 内存都给 ES 比较好,但现实是这样吗, 越大越好?虽然内存对 ES 来说是非常重要的,但是答案是否定的!
因为 ES 堆内存的分配需要满足以下两个原则:
假设你有个机器有 128 GB 的内存,你可以创建两个节点,每个节点内存分配不超过 32 GB。 也就是说不超过 64 GB 内存给 ES 的堆内存,剩下的超过 64 GB 的内存给 Lucene
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。