一、概述
倒排索引:在搜索引擎中,每个文档都有一个对应的文档ID,文档内容被表示为一系列关键词的集合。倒排索引就是关键词到文档ID的映射,每个关键词都对应着一系列的文件,这些文件中都出现了关键词。
倒排索引中的所有词项对应一个或多个文档;
倒排索引中的词项根据字典顺序升序排列
Luence是一个开源的全文检索引擎工具包(类似于Java API),而ElasticSerach底层是基于这些包,对其进行了扩展,提供了比Luence更为丰富的查询语言,可以非常方便的通过Elasticsearch的HTTP接口与底层Luence交互。
ElasticSearch和Solr:当实时建立索引的时候,Solr会产生阻塞,而es不会,es查询性能要高于solr;在不断动态添加数据的时候,solr的检索效率会变的低下,而es则没有什么变化;Solr利用zookeeper进行分布式管理,而es自身带有分布式系统管理功能。Solr一般都要部署到web服务器,比如tomcat,启动tomcat的时候需要配置tomcat和solr的管理,solr的本质是一个动态的web项目;solr支持更多的数据格式(xml、json、csv等),而es仅支持json文件格式;Solr是传统搜索应用的有力解决方案,但是es更适用于新兴的实时搜索应用。单纯的对已有数据进行检索的时候,solr的效率高于es;solr官网提供的功能更多,而es本身更注重于核心功能,攻击功能有第三方插件。
二、Elasticsearch入门
Elasticsearch简介:
一个分布式的、Restful风格(前后端交互的标准,http请求的格式)的搜索引擎;支持对各种类型的数据的检索;搜索速度快,可以提供实时的服务;便于水平扩展(集群时增加服务器),每秒可以处理PB级海量数据。
本质上是一个分布式数据库。
Elasticsearch术语:
索引、类型、文档、字段,与数据库的定义相对应,即database、table、row、column
集群、节点、分片(对索引进行划分,提高并发能力)、副本(备份)。
在多台机器上启动多个es进程实例,组成了一个es集群。
三、Elasticsearch安装配置
需要安装elasticsearch和分词插件两个工具。直接下载压缩文件解压缩即可,修改配置文件。
es下载网址:https://www.elastic.co/cn/downloads/past-releases/elasticsearch-6-4-3
解压缩后更改配置文件:cluster.name(集群名字)、path.data(存储数据的路径)、path.log(日志路径)
分词插件下载:https://github.com/medcl/elasticseatch-analysis-ik/releases/tag/v6.4.3
加压缩到es的plugins目录并新建ik文件夹下。
启动es:elasticsearch.bat
Kibana安装
下载地址:https://www.elastic.co/cn/downloads/kibana
与ElasticSearch一样,下载后解压,找到bin/kibana.bat文件双击启动kibana。(注意:需要版本对应)
验证Kibana:打开浏览器,输入http://127.0.0.1:5601/,(前提需要先启动ElasticSearch)
看到如下界面安装成功
这里面可以提供很多模拟数据,感兴趣的可以自己玩玩,咱们学习期间只要使用左下角Dev Tools(开发工具)就可以了,点击后,会出现如下界面:
大家可以看到我的界面是中文的,但默认是英文的。这个是可以自己更改的。
找到kibana下面的config/kibana.yml文件
在文件末追加i18n.locale: “zh-CN”,保存后,重启kibana。就可以看到汉化后的界面了
四、ES内置分词器
咱们知道Elasticsearch之所以模糊查询这么快,是因为采用了倒排索引,而倒排索引的核心就是分词,把text格式的字段按照分词器进行分词并编排索引。为了发挥自己的优势,Elasticsearch已经提供了多种功能强大的内置分词器,它们的作用都是怎样的呢?能处理中文吗?
首先咱们可以对Elasticsearch提供的内置分词器的作用进行如下总结:
下面讲解下常见的几个分词器:
Standard Analyzer(默认)
POST _analyze
{
"analyzer": "standard",
"text": "Like X 国庆放假的"
}
Simple Analyzer
POST _analyze
{
"analyzer": "simple",
"text": "Like X 国庆放假 的"
}
Whitespace Analyzer
POST _analyze
{
"analyzer": "whitespace",
"text": "Like X 国庆放假 的"
}
Keyword
GET _analyze
{
"analyzer": "keyword",
"text": "Like X 国庆放假的"
}
可以发现,这些内置分词器擅长处理单词和字母,所以如果咱们要处理的是英文数据的话,它们的功能可以说已经很全面了!那处理中文效果怎么样呢?下面咱们举例验证一下。
内置分词器对中文的局限性
首先咱们创建一个索引,并批量插入一些包含中文和英文的数据:
// 创建索引
PUT /ropledata
{
"settings": {
"number_of_shards": "2",
"number_of_replicas": "0"
}
}
es head中刷新即可看到新创建的索引
批量插入数据
// 批量插入数据
POST _bulk
{ "create" : { "_index" : "ropledata", "_id" : "1001" } }
{"id":1,"name": "且听风吟","hobby": "music and movie"}
{ "create" : { "_index" : "ropledata", "_id" : "1002" } }
{"id":2,"name": "静待花开","hobby": "music"}
{ "create" : { "_index" : "ropledata", "_id" : "1003" } }
{"id":3,"name": "大数据","hobby": "movie"}
{ "create" : { "_index" : "ropledata", "_id" : "1004" } }
{"id":4,"name": "且听_风吟","hobby": "run"}
在kibana的Dev Tools里执行情况:
五、 ElasticSearch基本概念
文档(Document)
我们知道Java是面向对象的,而Elasticsearch是面向文档的,也就是说文档是所有可搜索数据的最小单元。ES的文档就像MySql中的一条记录,只是ES的文档会被序列化成json格式,保存在Elasticsearch中;
这个json对象是由字段组成,字段就相当于Mysql的列,每个字段都有自己的类型(字符串、数值、布尔、二进制、日期范围类型);当我们创建文档时,如果不指定字段的类型,Elasticsearch会帮我们自动匹配类型;
每个文档都有一个ID,类似MySql的主键,咱们可以自己指定,也可以让Elasticsearch自动生成;
类型(Type)
类型就相当于MySql里的表,我们知道MySql里一个库下可以有很多表,最原始的时候ES也是这样,一个索引下可以有很多类型,但是从7.0版本开始,type已经废弃,一个索引就只能创建一个类型了(_doc)。
索引(Index)
索引就相当于MySql里的数据库,它是具有某种相似特性的文档集合。索引的名称必须全部是小写;
索引具有mapping和setting的概念,mapping用来定义文档字段的类型,setting用来定义不同数据的分布。除了这些常用的概念,我们还需要知道节点概念的作用,因此咱们接着往下看!
节点(node)
一个节点就是一个ES实例,其实本质上就是一个java进程;ES的节点类型主要分为如下几种:
Master Eligible节点:每个节点启动后,默认就是Master Eligible节点,可以通过设置node.master: false 来禁止。Master Eligible可以参加选主流程,并成为Master节点(当第一个节点启动后,它会将自己选为Master节点);注意:每个节点都保存了集群的状态,只有Master节点才能修改集群的状态信息。
Data节点:可以保存数据的节点。主要负责保存分片数据,利于数据扩展。
Coordinating 节点:负责接收客户端请求,将请求发送到合适的节点,最终把结果汇集到一起
分片(shard)
ES里面的索引可能存储大量数据,这些数据可能会超出单个节点的硬件限制。为了解决这个问题,ES提供了将索引细分为多个碎片的功能,这就是分片。
分片的好处
通过分片技术,咱们可以水平拆分数据量,同时它还支持跨碎片(可能在多个节点上)分布和并行操作,从而提高性能/吞吐量;
ES可以完全自动管理分片的分配和文档的聚合来完成搜索请求,并且对用户完全透明;
注意:主分片数在索引创建时指定,后续只能通过Reindex修改,但是较麻烦,一般不进行修改。
副本分片(replica shard)
为了实现高可用、遇到问题时实现分片的故障转移机制,ElasticSearch允许将索引分片的一个或多个复制成所谓的副本分片。
副本分片的好处
当分片或者节点发生故障时提供高可用性。因此,副本分片永远不会分配到复制它的原始或主分片所在的节点上;
可以提高扩展搜索量和吞吐量,因为ES允许在所有副本上并行执行搜索;
默认情况下,ES中的每个索引都分配5个主分片,并为每个主分片分配1个副本分片。主分片在创建索引时指定,不能修改,副本分片可以修改。
分数 score
关于查询时,分数越高排位更高。那么分数是如何计算的:
搜索的关键字在文档中出现的频次越高,分数就越高
指定的文档内容越短,分数就越高
我们在搜索时,指定的关键字也会被分词,这个被分词的内容,被分词库匹配的个数越多,分数越高
六、基本操作
索引ropledata中有如下数据
GET查询
GET全局搜索数据:
GET /ropledata/_search
指定文档id搜索数据:
GET /ropledata/_doc/1001
根据关键字搜索数据
GET /ropledata/_search?q=name:"且听风吟"
term查询
比如咱们查询id
字段为2的数据
terms查询
比如查询id
字段为1和3的数据:
match查询
match_all全局搜索数据
match查询
multi_match查询
bool查询
复合过滤器,将你的多个查询条件,以一定的逻辑组合在一起。
must: 所有的条件,用must组合在一起,表示And的意思
must_not:将must_not中的条件,全部都不能匹配,标识Not的意思
should:所有的条件,用should组合在一起,表示Or的意思
must(and),所有的条件都要符合
must_not(not)全部都不能匹配
范围查询
- gt大于
- gte大于等于
- lte小于
- lte小于等于
range查询
filter查询
聚合查询
首先咱们需要了解几个非常常用的数学统计函数:
avg:平均值
max:最大值
min:最小值
sum:求和
cardinality:去重
value_count:计数统计
terms:词聚合可以基于给定的字段,并按照这个字段对应的每一个数据为一个桶,然后计算每个桶里的文档个数。默认会按照文档的个数排序。
比如咱们求id
的平均值
terms词聚合可以基于给定的字段,并按照这个字段对应的每一个数据为一个桶,然后计算每个桶里的文档个数。默认会按照文档的个数排序。
七、springboot集成
创建一个springboot的项目 同时勾选上springboot-web
的包以及Nosql的elasticsearch
的包
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
注意下spring-boot的parent包内的依赖的es的版本是不是你对应的版本
不是的话就在pom文件下写个properties的版本
<!--这边配置下自己对应的版本-->
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.13.2</elasticsearch.version>
</properties>
注入RestHighLevelClient 客户端
@Configuration
public class ElasticSearchClientConfig {
@Bean
public RestHighLevelClient restHighLevelClient(){
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost("127.0.0.1",9200,"http"))
);
return client;
}
}
测试索引,文档增删改,即批量操作
package com.example.springbootes;
import com.alibaba.fastjson.JSON;
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
@SpringBootTest
class SpringbootEsApplicationTests {
@Autowired
@Qualifier("restHighLevelClient")
private RestHighLevelClient client;
//测试索引的创建
@Test
void testCreateIndex() throws IOException {
//1.创建索引的请求
CreateIndexRequest request = new CreateIndexRequest("lisen_index");
//2客户端执行请求,请求后获得响应
CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
System.out.println(response);
}
//测试索引是否存在
@Test
void testExistIndex() throws IOException {
//1.创建索引的请求
GetIndexRequest request = new GetIndexRequest("lisen_index");
//2客户端执行请求,请求后获得响应
boolean exist = client.indices().exists(request, RequestOptions.DEFAULT);
System.out.println("测试索引是否存在-----"+exist);
}
//删除索引
@Test
void testDeleteIndex() throws IOException {
DeleteIndexRequest request = new DeleteIndexRequest("lisen_index");
AcknowledgedResponse delete = client.indices().delete(request,RequestOptions.DEFAULT);
System.out.println("删除索引--------"+delete.isAcknowledged());
}
//测试添加文档
@Test
void testAddDocument() throws IOException {
User user = new User("lisen",27);
IndexRequest request = new IndexRequest("lisen_index");
request.id("1");
//设置超时时间
request.timeout("1s");
//将数据放到json字符串
request.source(JSON.toJSONString(user), XContentType.JSON);
//发送请求
IndexResponse response = client.index(request,RequestOptions.DEFAULT);
System.out.println("添加文档-------"+response.toString());
System.out.println("添加文档-------"+response.status());
// 结果
// 添加文档-------IndexResponse[index=lisen_index,type=_doc,id=1,version=1,result=created,seqNo=0,primaryTerm=1,shards={"total":2,"successful":1,"failed":0}]
// 添加文档-------CREATED
}
//测试文档是否存在
@Test
void testExistDocument() throws IOException {
//测试文档的 没有index
GetRequest request= new GetRequest("lisen_index","1");
//没有indices()了
boolean exist = client.exists(request, RequestOptions.DEFAULT);
System.out.println("测试文档是否存在-----"+exist);
}
//测试获取文档
@Test
void testGetDocument() throws IOException {
GetRequest request= new GetRequest("lisen_index","1");
GetResponse response = client.get(request, RequestOptions.DEFAULT);
System.out.println("测试获取文档-----"+response.getSourceAsString());
System.out.println("测试获取文档-----"+response);
// 结果
// 测试获取文档-----{"age":27,"name":"lisen"}
// 测试获取文档-----{"_index":"lisen_index","_type":"_doc","_id":"1","_version":1,"_seq_no":0,"_primary_term":1,"found":true,"_source":{"age":27,"name":"lisen"}}
}
//测试修改文档
@Test
void testUpdateDocument() throws IOException {
User user = new User("李逍遥", 55);
//修改是id为1的
UpdateRequest request= new UpdateRequest("lisen_index","1");
request.timeout("1s");
request.doc(JSON.toJSONString(user),XContentType.JSON);
UpdateResponse response = client.update(request, RequestOptions.DEFAULT);
System.out.println("测试修改文档-----"+response);
System.out.println("测试修改文档-----"+response.status());
// 结果
// 测试修改文档-----UpdateResponse[index=lisen_index,type=_doc,id=1,version=2,seqNo=1,primaryTerm=1,result=updated,shards=ShardInfo{total=2, successful=1, failures=[]}]
// 测试修改文档-----OK
// 被删除的
// 测试获取文档-----null
// 测试获取文档-----{"_index":"lisen_index","_type":"_doc","_id":"1","found":false}
}
//测试删除文档
@Test
void testDeleteDocument() throws IOException {
DeleteRequest request= new DeleteRequest("lisen_index","1");
request.timeout("1s");
DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
System.out.println("测试删除文档------"+response.status());
}
//测试批量添加文档
@Test
void testBulkAddDocument() throws IOException {
ArrayList<User> userlist=new ArrayList<User>();
userlist.add(new User("cyx1",5));
userlist.add(new User("cyx2",6));
userlist.add(new User("cyx3",40));
userlist.add(new User("cyx4",25));
userlist.add(new User("cyx5",15));
userlist.add(new User("cyx6",35));
//批量操作的Request
BulkRequest request = new BulkRequest();
request.timeout("1s");
//批量处理请求
for (int i = 0; i < userlist.size(); i++) {
request.add(
new IndexRequest("lisen_index")
.id(""+(i+1))
.source(JSON.toJSONString(userlist.get(i)),XContentType.JSON)
);
}
BulkResponse response = client.bulk(request, RequestOptions.DEFAULT);
//response.hasFailures()是否是失败的
System.out.println("测试批量添加文档-----"+response.hasFailures());
// 结果:false为成功 true为失败
// 测试批量添加文档-----false
}
//测试查询文档
@Test
void testSearchDocument() throws IOException {
SearchRequest request = new SearchRequest("lisen_index");
//构建搜索条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
//设置了高亮
sourceBuilder.highlighter();
//term name为cyx1的
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("name", "cyx1");
sourceBuilder.query(termQueryBuilder);
sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
request.source(sourceBuilder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
System.out.println("测试查询文档-----"+JSON.toJSONString(response.getHits()));
System.out.println("=====================");
for (SearchHit documentFields : response.getHits().getHits()) {
System.out.println("测试查询文档--遍历参数--"+documentFields.getSourceAsMap());
}
// 测试查询文档-----{"fragment":true,"hits":[{"fields":{},"fragment":false,"highlightFields":{},"id":"1","matchedQueries":[],"primaryTerm":0,"rawSortValues":[],"score":1.8413742,"seqNo":-2,"sortValues":[],"sourceAsMap":{"name":"cyx1","age":5},"sourceAsString":"{\"age\":5,\"name\":\"cyx1\"}","sourceRef":{"fragment":true},"type":"_doc","version":-1}],"maxScore":1.8413742,"totalHits":{"relation":"EQUAL_TO","value":1}}
// =====================
// 测试查询文档--遍历参数--{name=cyx1, age=5}
}