ElasticSearch 认识到实战
目录
- 搜索引擎介绍
- ElasticSearch知识
- 安装
- 使用restful风格查询ES
- SpringBoot配置ES
- SpringBoot集成使用
一、搜索引擎介绍
1、搜索引擎
搜索引擎是指一个庞大的互联网资源数据库,如网页、新闻组、程序、图像等。它有助于在万维网上定位信息。用户可以通过以关键字或者短语的形式将查询传递到搜索引擎中来搜索信息。然后搜索引擎搜索其数据库并向用户返回相关链接。
2、搜索引擎组件
一般来说,搜索引擎有三个基本组件
- web爬虫 -web爬虫也称为蜘蛛或者机器人。它是一个收集网络信息的软件组件。
- 数据库 - web上的所有信息都存储在数据库中。它们包含大量的web资源。
- 搜索接口- 此组件是用户和数据库之间的接口。它帮助用户搜索数据库。
3、如何工作的?
- 1、获取原始内容。
任何搜索应用程序的第一步是收集要进行搜索的目标内容 - 2、构建文档
从原始内容构建文档,让搜索应用程序可以很容易的理解和解释 - 3、分析文档
在索引开始之前,将对文档进行分析 - 4、索引文档
当文档被构建和分析后,下一步是对它们建立索引,以便可以基于特定键而不是文档的全部内容来检索该文档。索引类似于在书开始页和末尾处的目录索引,其中常见单词以页面显示,使得这些单词可以快速跟踪,而不是搜索整本书。 - 5、用于搜索的用户接口
当索引数据就绪,应用程序可以执行搜素操作。为了帮助用户进行搜索,应用必须提供用户接口。用户可以在其中输入文本并启动搜索过程 - 6、构建查询。
当用户做出查询文本的请求,应用程序应该使用该文本准备查询对象,然后可以使用该查询对象来查询索引数据库以获取相关的细节。 - 7、搜索查询
使用查询对象,检查索引数据库以获取相关详细信息和内容文档 - 8、渲染结果
当收到所需的结果,应用程序应决定如何使用其用户界面向用户显示结果。
二、ElasticSearch知识
1、介绍
Elasticesearch是一个分布式可扩展的实时搜索和分析引擎,一个建立在全文搜索引擎Apache Lucene™基础上的搜索引擎,当然ElsticeSearch并不仅仅是Lucene那么简单,它不仅包括了全文搜索功能,还可以进行一下工作。
- 1、分布式实时文件存储,并将每个字段都编入索引,使其可以被搜索
- 2、实时分析的分布式搜索引擎
- 可以扩展到上百台服务器,处理PB(TB)级别的结构化或非结构化数据。
2、基本概念
先说ElasticSearch的文件存储,ElasticSearch是面向文档型数据库,一条数据在这里就是一个文档,用JSON作为文档序列化的格式,比如下面这条数据
{
"name" : "John",
"sex" : "Male",
"age" : 25,
"birthDate": "1990/05/01",
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
用MySQL这样的数据库存储就会创建一张表,然后字段。在ElasticSearch里就是一个文档,而这个文档就会属于一种类型,各种各样的类型存在于一个索引当中。用一份简易的将ElasticSearch和关系型数据术语对照表:
关系型数据库 | ElasticSearch |
---|---|
数据库 | 索引(Index) |
表 | 类型(Type) |
行 | 文档(Docments) |
列 | 字段(Fields) |
一个ElaseicSearch集群可以包含多个索引(数据库),也就是说其中包含了很多类型(表),这些类型包含了很多的文档(行),然后每个文档中又包含了很多的字段(列)。ElasticSearch的交互可以使用JavaAPI,也可以直接使用HTTP的REstfulAPI方式,比如我们也可以直接使用HTTP的RestfulApi方式,比如我们打算插入一条数据可以简单发送个HTTP的请求。具体操作指引
3、ES索引介绍
es做关键的就是提供了强大的索引能力了,这边文章就详细的说明了索引的密码了
ES索引的精髓就一句话
一切设计都是为了提高搜索的性能
另一层意思:为了提高搜索的性能,难免会牺牲某些方面,比如插入/更新,否则其他数据库不用混了。在ES插入一条数据,就是PUT一个Json的对象,这个对象有fields,就是很多字段,那么再插入这些数据到ES的同时,ES还默默的为这些字段建立索引–倒排索引,因为ES最核心功能是搜索。
4、ES是如何做到快速索引的
上面讲到的那篇文章说es使用的索引比关系型数据库的B-Tree索引快呢,
什么是B-Tree索引呢?
二叉树查找效率是logN,同时插入新的节点不必移动全部节点,索引使用树形结构存储索引,能同时兼顾插入和查询的性能。因此在这个基础上,在结合磁盘的读取特性(顺序读/随机读),传统关系型数据库采用了B-Tree/B+Tree这种数据结构。
为了提高查询效率,减少磁盘寻道次数,将多个值作为一个数组通过连续区间存放,一次寻道读取多个数据,同时也降低树的高度。
什么是倒排索引,太多了具体开这篇文章
5、总结思考
ES索引思路
将磁盘里的东西尽量搬进内存,减少磁盘随机读取次数(同时也可用磁盘顺序读特性),结合各种奇技淫巧的压缩算法,用法及其苛刻的态度使用内存。
所以在使用ES进行索引时需要注意:
- 1、不需要索引的字段,一定要定义出来,因为默认是自动建立索引的。
- 2、同样的道理,对于String类型,不需要analysis的也需要明确定义出来,因为默认也是会analysis的
- 3、选择有规律的ID是很重要的,随机性太大的ID(比如Java的UUID)不利于查询
三、安装
1、es的安装+ik分词器
分词器和es的版本必须要一样才行
version: '3.1'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.10.1
restart: always
container_name: elasticsearch
ports:
- 9200:9200
environment:
discovery.type: single-node
volumes:
- ./plugins/:/usr/share/elasticsearch/plugins/
2、分词器下载
在plugins目录创建一个目录下分词器解压放进去
其他日志或者配置文件选择性做映射持久化
分词器下载地址 提取码:necr
四、使用restful风格查询es
1、路径说明
http://localhost:9200/company/_doc/_search
- 前面是es服务器路径,后面_search固定
- company:是索引名,不带的话就是查询所有索引下的数据。查询多个索引用,号隔开。比如 /company,school/_doc。也可以用* 号去去匹配q开头的索引名比如:q*。
- _doc:文档类型,不指定的话就是查询所有类型的。也可以像索引那些指定文档类型。
2、条件查询
1、单一查询
- match 模糊查询
{
"query":{
"match":{
"firstName":"吗?"
}
}
}
- term 精确查询
{
"query": {
"term": {
"firstName":"小明"
}
}
}
2、复合查询 bool,可以放多个指定条件
- must。里面放多个条件,必须满足里面的条件
{
"query": {
"bool": {
"must": [
{"term":{"firstName":"小明"}}
]
}
}
}
- should 。or条件查询。多个条件下满足某个条件就行了
{
"query": {
"bool": {
"should": [
{"match":{"firstName":"小明"}},
{"match": {"lastName": "小明"}}
]
}
}
}
- must_not 。!=条件。
{
"query": {
"bool": {
"must_not": [
{"match":{"firstName":"沈瑶"}}
]
}
}
}
3、排序
- sort里可以放多个字段排序。order是指定降序还是升序。
{
"query": {
"bool": {
"should": [
{"match":{"firstName":"小明"}},
{"match": {"lastName": "小明"}}
]
}
},
"sort": {
"firstName": {
"order": "desc"
}
}
}
4、分页
{
"query": {
"bool": {
"must_not": [
{"match":{"firstName":"沈瑶"}}
]
}
},
"from": 0,
"size": 10
}
5、highlight 高亮显示。
pre_tags 关键字前打标签。
post_tags 关键字结尾标签。
在这里,pre_tags和post_tags 可以放到字段里,也可以放到highlisht里。一个等于在在子类范围内,一个等于在父类范围内,优先使用子类的。
{
"query": {
"bool": {
"must_not": [
{"match":{"firstName":"沈瑶"}}
]
}
},
"highlight": {
"pre_tags": "<b class='key' style='color:red'>",
"post_tags": "</b>",
"fields": {
"firstName": {
"pre_tags": "<h2>",
"post_tags": "</h2>"
}
},
"fields": {
"lastName": {
}
}
},
"from": 0,
"size": 10
}
6、测试分词器
- 请求路径,POST
url/_analyze
- 请求体内容
{
"analyzer":"ik_max_word",
"text":"小明是个坏蛋"
}
五、SpringBoot配置使用ES
- 1、配置
@Bean
public RestHighLevelClient restHighLevelClient(){
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
//new HttpHost("localhost", 9200, "http"),
new HttpHost("localhost", 9200, "http")));
return client;
}
使用SpringBoot操作es的主要的两种方法,使用9200端口的是通过rest风格的方式操作es。用9300的是TCP连接的端口,是使用TransportClient直接操作es节点。
- 2、pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
六、SpringBoot集成使用
1、定义对象,映射到ES里的数据
package com.es.example.entity;
import lombok.Data;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
/**
* @author sy
* @date Created in 2020.6.27 15:21
* @description
*/
@Document(indexName = "test_index", shards = 1,replicas = 0)
@Data
public class Employee {
/**
* index 是否创建索引,默认创建
*/
@Field(index = false,type = FieldType.Text)
private String id;
/**
* analyzer 存入时是否使用分词
* type 在es里的类型。不指定可自动
* store 是否独立存储
* searchAnalyzer 查询分词
*/
@Field(analyzer = "ik_max_word",type = FieldType.Text,store = true,searchAnalyzer = "ik_max_word")
private String firstName;
@Field(analyzer = "ik_max_word",type = FieldType.Text,store = true,searchAnalyzer = "ik_max_word")
private String lastName;
@Field
private Integer age = 0;
@Field(analyzer = "ik_max_word",type = FieldType.Text,store = true,searchAnalyzer = "ik_max_word")
private String about;
}
- 字段属性说明
type:字段类型,取值是枚举:FieldType
index:是否索引,布尔类型,默认是true
store:是否存储,布尔类型,默认是false
analyzer:分词器名称:ik_max_word
searchAnalyzer : 查询使用分词
- 文档对象说明
indexName:对应索引库名称
type:对应在索引库中的类型
shards:分片数量,默认5
replicas:副本数量,默认1
2、使用Spring Data实现对ES的CURD
package com.es.example.dto;
import com.es.example.entity.Employee;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
/**
* @author sy
* @date Created in 2020.6.27 15:20
* @description
*/
public interface EmployeeRepository extends ElasticsearchRepository<Employee,String> {
/**
* 根据ID查询信息
* @param id
* @return
*/
Employee queryEmployeeById(String id);
/**
*
* @param firstName
* @param lastName
* @return
*/
Page<Employee> findByFirstNameOrLastName(String firstName, String lastName, Pageable pageable);
/**
* 根据范围
* @param age
* @param age2
* @return
*/
Page<Employee> findByAgeBetween(double age, double age2,Pageable pageable);
}
说明
- 1、在这里使用SpringDataES去映射发送请求给ES
如:
Page<Book> findByNameAndPrice(String name, Integer price);
上面会编译成
{ "bool" :
{ "must" :
[
{ "field" : {"name" : "?"} },
{ "field" : {"price" : "?"} }
]
}
}
- 2、所以可以使用@Query来定义一些自己需要的查询
@Query("{"bool" : {"must" : {"field" : {"name" : "?0"}}}}")
Page<Book> findByName(String name,Pageable pageable);
Pageable 是一定需要的,不然报错!
- 3、具体可以查看文档
https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#elasticsearch.misc
- 4、一些常用的解析
Keyword | Sample | Elasticsearch Query String |
---|---|---|
And | findByNameAndPrice | {“bool” : {“must” : [ {“field” : {“name” : “?”}}, {“field” : {“price” : “?”}} ]}} |
Or | findByNameOrPrice | {“bool” : {“should” : [ {“field” : {“name” : “?”}}, {“field” : {“price” : “?”}} ]}} |
Is | findByName | {“bool” : {“must” : {“field” : {“name” : “?”}}}} |
Not | findByNameNot | {“bool” : {“must_not” : {“field” : {“name” : “?”}}}} |
Between | findByPriceBetween | {“bool” : {“must” : {“range” : {“price” : {“from” : ?,“to” : ?,“include_lower” : true,“include_upper” : true}}}}} |
LessThanEqual | findByPriceLessThan | {“bool” : {“must” : {“range” : {“price” : {“from” : null,“to” : ?,“include_lower” : true,“include_upper” : true}}}}} |
GreaterThanEqual | findByPriceGreaterThan | {“bool” : {“must” : {“range” : {“price” : {“from” : ?,“to” : null,“include_lower” : true,“include_upper” : true}}}}} |
Before | findByPriceBefore | {“bool” : {“must” : {“range” : {“price” : {“from” : null,“to” : ?,“include_lower” : true,“include_upper” : true}}}}} |
After | findByPriceAfter | {“bool” : {“must” : {“range” : {“price” : {“from” : ?,“to” : null,“include_lower” : true,“include_upper” : true}}}}} |
Like | findByNameLike | {“bool” : {“must” : {“field” : {“name” : {“query” : “?*”,“analyze_wildcard” : true}}}}} |
StartingWith | findByNameStartingWith | {“bool” : {“must” : {“field” : {“name” : {“query” : “?*”,“analyze_wildcard” : true}}}}} |
EndingWith | findByNameEndingWith | {“bool” : {“must” : {“field” : {“name” : {“query” : “*?”,“analyze_wildcard” : true}}}}} |
Contains/Containing | findByNameContaining | {“bool” : {“must” : {“field” : {“name” : {“query” : “?”,“analyze_wildcard” : true}}}}} |
In | findByNameIn(Collectionnames) | {“bool” : {“must” : {“bool” : {“should” : [ {“field” : {“name” : “?”}}, {“field” : {“name” : “?”}} ]}}}} |
NotIn | findByNameNotIn(Collectionnames) | {“bool” : {“must_not” : {“bool” : {“should” : {“field” : {“name” : “?”}}}}}} |
Near | findByStoreNear | Not Supported Yet ! |
True | findByAvailableTrue | {“bool” : {“must” : {“field” : {“available” : true}}}} |
False | findByAvailableFalse | {“bool” : {“must” : {“field” : {“available” : false}}}} |
OrderBy | findByAvailableTrueOrderByNameDesc | {“sort” : [{ “name” : {“order” : “desc”} }],“bool” : {“must” : {“field” : {“available” : true}}}} |
3、使用构造器查询数据并且高亮
SearchRequest searchRequest = new SearchRequest("test_index");
// SearchSourceBuilder 是构建搜索条件的盒子。
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
//设置高亮的字段,也可以不设置。
HighlightBuilder firstName = new HighlightBuilder().field("firstName");
firstName.preTags("<span class = 'df'>");
firstName.postTags("</span>");
sourceBuilder.highlighter(firstName);
//设置分页。
sourceBuilder.from(0);
sourceBuilder.size(10);
/**
* 常用的查询方式有:
* .matchQuery() 这个是模糊查询
* .matchAllQuery() 查询全部
* .termQuery() 精确查询。
*/
MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("firstName", "小明");
//设置响应超时时间
sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
sourceBuilder.query(matchQueryBuilder);
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
//这个就是查询到的数据。
//searchResponse.getHits().getHits();
ArrayList<Map<String, Object>> maps = new ArrayList<>();
for (SearchHit hit: searchResponse.getHits().getHits()){
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
HighlightField title = highlightFields.get("firstName");
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
if (title!=null){
StringBuffer sb = new StringBuffer();
Text[] fragments = title.fragments();
for (Text s : fragments){
sb.append(s);
}
sourceAsMap.put("firstName",sb);
}
maps.add(sourceAsMap);
}
return maps;