本篇博客将向各位详细介绍elasticsearch,也算是对我最近学完elasticsearch的一个总结,对于如何在Kibana中使用DSL指令,本篇文章不会进行介绍,这里只会介绍在java中如何进行使用,保证你看完之后就会在项目中进行上手,那我们开始本篇教程吧~
什么是ElasticSearch
- Elasticsearch,基于lucene.分布式的Restful实时搜索和分析引擎(实时)
- 分布式的实时文件存储,每个字段都被索引并可被搜索
- 高扩展性,可扩展至上百台服务器,处理PB级结构化或非结构化数据
- Elasticsearch用于全文检索,结构化搜索,分析/合并使用
ElasticSearch是如何做到这么快的
- 分布式存储:ElasticSearch把数据存储在多个节点上,从而减少单个节点的压力,从而提高性能
- 索引分片:ElasticSearch把索引分成多个分片,这样可以让查询操作并行化,从而提高性能
- 全文索引:ElasticSearch把文档转换成可搜索的结构化数据,从而提高效率
- 倒排索引:ElasticSearch将文档进行分词处理,把每个词在哪个文档中出现过进行映射,并存储这些信息,从而在搜索时,查询这些分词和搜索这些分词存在在哪些文档中,提高查询效率
- 异步请求处理:ElasticSearch能够在请求到达时立即返回,避免长时间等待,提高效率
全文索引和倒排索引是什么
全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。全文搜索搜索引擎数据库中的数据。
-
优点:
-
可以给多个字段创建索引
-
根据索引字段搜索、排序速度非常快
-
-
缺点:
-
根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。
-
倒排索引就和传统的索引结构相反,传统的索引是由文档组成的,每个文档中都包含了若干个词汇,然后根据这些词汇简历索引。而倒排索引则与其相反,倒排索引由词汇构成,每个词汇对应若干个文档,然后根据这些文档建立索引。
-
优点:
-
根据词条搜索、模糊搜索时,速度非常快
-
-
缺点:
-
只能给词条创建索引,而不是字段
-
无法根据字段做排序
-
ElasticSearch和Mysql之间的映射关系
Mysql类型 | ElasticSearch类型 | 说明 |
VARCHAR | text、keyword | 根据是否需要使用全文搜索或精确搜索选择使用text或keyword |
CHAR | keyword | 通常映射为keyword,因为它们存储较短的、不经常变化的字符序列 |
BLOB/TEXT | text | 大文本块使用text,适用于全文检索 |
INT,BIGINT | long | 大多数整数型使用long,以支持更大的数值 |
TINYINT | byte | 较小的整数可以映射为byte类型 |
DECIMAL,FLOAT,DOUBLE | double,float | |
DATE,DATETIME,TIMESTAMP | date | |
BOOLEAN | boolean |
倒排索引建立步骤
es中建立倒排索引需要两步,首先对文档进行分词,其次建立倒排索引
分词
分词的意思大概就是对文档中的数据通过es的分词器进行分割成一个个词项,比如 “我是银氨溶液” 这句话,经过分词过后就是 “我”、“是”、“银氨”、“溶液”,当然es的分词器分为ik_smart分词器和ik_max_word分词,所以实际操作时这句话会被分解为不同的词段。
es中的一些概念
文档
elasticsearch是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中。
字段
Json文档中往往包含很多的字段(Field),类似于数据库中的列。
索引
索引(Index),就是相同类型的文档的集合。因此,我们可以把索引当做是数据库中的表。
映射
数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。
对照图表:
MySQL | Elasticsearch | 说明 |
---|---|---|
Table | Index | 索引(index),就是文档的集合,类似数据库的表(table) |
Row | Document | 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 |
Column | Field | 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column) |
Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema) |
SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD |
RestAPI
首先,在这篇文章中不会将所有api都介绍完,所以这了贴上官方文档的地址,以共各位查看:
ElasticSearch官方文档
这里需要先引入es的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
同时,你需要在pom文件中修改es的版本和你本地的一样
然后,我们需要把es注入到spring容器中
@Configuration
public class ElasticSearchClientConfig {
@Bean
public RestHighLevelClient restHighLevelClient(){
RestHighLevelClient client=new RestHighLevelClient(
RestClient.builder(
new HttpHost("127.0.0.1", 9200, "http")
)
);
return client;
}
}
准备工作完成之后就可以开始接下来的学习啦~
索引的CRUD操作
索引的操作较为简单,而且项目中实际都是对文档进行操作,所以我这里贴出索引的相关操作代码,并做出解释,各位可以了解一下
@Autowired
@Qualifier("restHighLevelClient")
private RestHighLevelClient client;
// 测试索引的创建 Request
@Test
void testCreateIndex() throws Exception {
// 1、创建索引请求
CreateIndexRequest request = new CreateIndexRequest("yinan_index");
// 2、客户端执行请求,请求后获得响应
CreateIndexResponse createIndexResponse = client.indices().create(request, RequestOptions.DEFAULT);
System.out.println(createIndexResponse);
}
// 测试获取索引
@Test
void testGetIndex() throws IOException {
GetIndexRequest getIndex = new GetIndexRequest("yinan_index");
boolean exists = client.indices().exists(getIndex, RequestOptions.DEFAULT);
System.out.println(exists);
}
// 删除索引
@Test
void testDeleteIndex() throws Exception {
DeleteIndexRequest request = new DeleteIndexRequest("yinan_index");
AcknowledgedResponse delete = client.indices().delete(request, RequestOptions.DEFAULT);
System.out.println(delete.isAcknowledged());
}
其中indices中包含了操作索引库的所有方法,在创建索引的时候如果有多个字段,可以提前写好 一个字符串常量,例如:
public static final String MAPPING_TEMPLATE = "{\n" +
" \"mappings\": {\n" +
" \"properties\": {\n" +
" \"id\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"name\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"address\":{\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"price\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"score\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"brand\":{\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"city\":{\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"starName\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"business\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"location\":{\n" +
" \"type\": \"geo_point\"\n" +
" },\n" +
" \"pic\":{\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"all\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
然后将新增索引中的代码修改成以下:
文档的CRUD操作
对于文档这里将重点介绍查询的相关方法,其它操作只做简单介绍。
查询操作
MatchAll
/**
* 查询全部
*/
@Test
void testMatchAll() throws IOException {
//1.准备request
SearchRequest request = new SearchRequest("hotel");
//2.准备参数
request.source().query(QueryBuilders.matchAllQuery());
//3.发起请求得到响应结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4.解析响应
handleResponse(response);
}
结果如下:
从结果我们也可以看到这个api是查询所有数据的方法,但是控制台只显示了10条数据,这是因为这个方法自动进行分页处理,每页10条数据。
Match
/**
* 全文检索
*/
@Test
void testMatch() throws IOException {
//1.准备request
SearchRequest request = new SearchRequest("hotel");
//2.准备参数
request.source().query(QueryBuilders.matchQuery("all","如家"));
//3.发起请求得到响应结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4.解析响应
handleResponse(response);
}
结果:
match用来做基本的模糊匹配,在es中会对文本进行分词,在match查询的时候也会对查询条件进行分词,然后通过倒排索引找到匹配的数据。
term
@Test
void testMatch() throws IOException {
//1.准备request
SearchRequest request = new SearchRequest("hotel");
//2.准备参数
// request.source().query(QueryBuilders.matchQuery("all","如家"));
request.source().query(QueryBuilders.termQuery("city","北京"));
//3.发起请求得到响应结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4.解析响应
handleResponse(response);
}
结果:
从结果来看,term是做精确查询的,所以一般可以用在查询某个具体的属性的时候
multiMatchQuery
@Test
void testMatch() throws IOException {
//1.准备request
SearchRequest request = new SearchRequest("hotel");
//2.准备参数
// request.source().query(QueryBuilders.matchQuery("all","如家"));
// request.source().query(QueryBuilders.termQuery("city","北京"));
request.source().query(QueryBuilders.multiMatchQuery("如家", "city", "name"));
//3.发起请求得到响应结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4.解析响应
handleResponse(response);
}
结果:
multiMatchQuery接受两个参数,一个是text,一个是fieldname,前者表示要查询的内容,后者表示要在哪些字段中进行查询,如果后者中的数据只有一个,那该方法和matchall一致,如果后者有多个,那查询的结果必须要满足其中的一个。
rangeQuery
@Test
void testMatch() throws IOException {
//1.准备request
SearchRequest request = new SearchRequest("hotel");
//2.准备参数
// request.source().query(QueryBuilders.matchQuery("all","如家"));
// request.source().query(QueryBuilders.termQuery("city","北京"));
// request.source().query(QueryBuilders.multiMatchQuery("如家", "city", "name"));
request.source().query(QueryBuilders.rangeQuery("price").gte(100).lte(150));
//3.发起请求得到响应结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4.解析响应
handleResponse(response);
}
结果:
该方法主要做范围查询,相当于sql语句中的between....and...
布尔查询
/**
* 布尔查询
* @throws IOException
*/
@Test
void testBool() throws IOException {
//1.准备request
SearchRequest request = new SearchRequest("hotel");
//2.准备参数
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
boolQuery.must(QueryBuilders.termQuery("city","北京"));
boolQuery.filter(QueryBuilders.rangeQuery("price").lte(300));
request.source().query(boolQuery);
//3.发起请求得到响应结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
System.out.println( "========"+response.getHits());
//4.解析响应
handleResponse(response);
}
/**
* 解析响应结果
* @param response
*/
private void handleResponse(SearchResponse response) {
SearchHits searchHits = response.getHits();
//总条数
long total = searchHits.getTotalHits().value;
System.out.println("共搜索到"+total+"条数据");
//文档数组
SearchHit[] hits = searchHits.getHits();
//遍历
for (SearchHit hit : hits){
//获取文档source
String json = hit.getSourceAsString();
//反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
//获取高亮结果
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if(!CollectionUtils.isEmpty(highlightFields)){
//根据字段名获取高亮结果
HighlightField highlightField = highlightFields.get("name");
if(highlightField != null){
//获取高亮值
String name = highlightField.getFragments()[0].string();
//替换
hotelDoc.setName(name);
}
}
System.out.println("hotDoc = "+hotelDoc);
}
这里简单说一下怎么得到的 handleResponse函数
通过以上图片我们就不难看出 response的响应结构,因此我们就可以具体推出相关信息,所以没有学习过在Kibana上使用DSL指令的朋友可以先去学习一下,然后再来运用到java中事半功倍。
当然,如果你只需要获取数据和总条数,可以修改成以下形式:
private PageResult handleResponse(SearchResponse response) {
SearchHits hits = response.getHits();
long total = hits.getTotalHits().value;
SearchHit[] searchHits = hits.getHits();
List<HotelDoc> hotels = new ArrayList<>();
for (SearchHit searchHit : searchHits) {
String source = searchHit.getSourceAsString();
// 反序列化
HotelDoc hotelDoc = JSON.parseObject(source, HotelDoc.class);
hotels.add(hotelDoc);
}
return new PageResult(total,hotels);
}
分页和排序 对结果的处理
/**
* 分页和排序 对结果的处理
*/
@Test
void testPageAndSort() throws IOException {
int page = 1,size = 5;
//1.准备request
SearchRequest request = new SearchRequest("hotel");
//2.准备参数
request.source().query(QueryBuilders.matchAllQuery());
//排序
request.source().sort("price", SortOrder.ASC);
//分页
request.source().from((page-1)*size).size(size);
//3.发起请求得到响应结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4.解析响应
handleResponse(response);
}
当然,还有其它一些api这里还没有介绍到,各位可以去官网进行查看详细文档说明~
添加文档
@Data
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private String name;
private Integer age;
}
@Test
void testAddDocument() throws IOException {
// 1、创建对象
User user = new User("yinan", 20);
// 2、创建请求
IndexRequest request = new IndexRequest("yinan_index");
// 规则 put /yinan_index/_doc/1
request.id("1");
request.timeout(TimeValue.timeValueSeconds(1));
request.timeout("1s");
// 将数据放入请求 json
request.source(JSON.toJSONString(user), XContentType.JSON);
// 客户端发送请求,获取相应的结果
IndexResponse indexResponse = client.index(request, RequestOptions.DEFAULT);
System.out.println(indexResponse.toString());
System.out.println(indexResponse.status()); //对应我们命令返回状态
}
批量添加文档
@Test
void testBulkRequest() throws Exception {
BulkRequest bulkRequest = new BulkRequest();
bulkRequest.timeout("10s");
ArrayList<User> userList = new ArrayList<>();
userList.add(new User("yinan1", 21));
userList.add(new User("yinan1", 21));
userList.add(new User("yinan3", 26));
userList.add(new User("yinan4", 24));
userList.add(new User("yinan5", 27));
userList.add(new User("yinan12", 20));
for (int i = 0; i < userList.size(); i++) {
bulkRequest.add(
new IndexRequest("yinan_index")
.id("" + (i + 1))
.source(JSON.toJSONString(userList.get(i)), XContentType.JSON)
);
}
BulkResponse itemResponses = client.bulk(bulkRequest, RequestOptions.DEFAULT);
System.out.println(itemResponses.hasFailures());
}
修改文档
@Test
void testUpdateDocument() throws Exception {
UpdateRequest request = new UpdateRequest("yinan_index", "1");
request.timeout("1s");
User user = new User("yinan_update", 21);
request.doc(JSON.toJSONString(user), XContentType.JSON);
UpdateResponse updateResponse = client.update(request, RequestOptions.DEFAULT);
System.out.println(updateResponse.status());
}
删除文档
@Test
void testDeleteDocument() throws Exception {
DeleteRequest request = new DeleteRequest("yinan_index", "1");
request.timeout("1s");
DeleteResponse deleteResponse = client.delete(request, RequestOptions.DEFAULT);
System.out.println(deleteResponse.status());
}
以上就是对es部分api的讲解,当然只看api使用是远远不够的,所以我们需要做一个小训练来巩固我们学习的东西。
资料已经置顶在本篇博客,有需要的请自取~