ElasticSearch

初始ES

什么是ES

elasticsearch是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容
elasticsearch结合kibana,Logstash,Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析,实时监控等领域。
elasticsearch是elastic stack的核心,负责存储,搜索,分析数据

正向索引和倒排索引

传统数据库(如MySQL)采用正向索引。
es采用倒排索引:

  1. 文档(document):每条数据就是一个文档
  2. 词条(term):文档按照语义分成的词语
    在这里插入图片描述

文档

es是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化成json格式后存储在es中

索引

索引(index):相同类型的文档的集合
映射(mapping):索引中文档的字段约束信息,类似表的结构约束

概念对比

MySQLES说明
TableIndex索引(Index),就是文档的集合类似数据库的表(table)
RowDocument文档(Document’),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式
ColumnField字段(Field),就是JSON文档中的字段,类似数据库中的列(Column)
SchemaMappingMapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema)
SQLDSLDSL是es提供的JSON风格的请求语句,用来操作es,实现CRUD

架构

MySQL:擅长事务类型操作,可以确保数据的安全和一致性
ES:擅长海量数据的搜索,分析,计算

索引库操作

mapping映射

mapping是对索引库中文档的约束,常见的mapping属性包括:

  1. type:字段数据类型,常见的简单类型有:
    1. 字符串:text(可分词的文本)
    2. keyword(精确值,例如:品牌,国家,ip地址)
    3. 数值:long,integet,short,byte,double,float
    4. 布尔:boolean
    5. 日期:date
    6. 对象:object
  2. index:是否创建索引,默认味true
  3. analyzer:使用哪种分词器
  4. properties:该字段的子字段

创建索引库

ES中通过RESTful请求操作索引库,文档。请求内容用DSL语句来表示。创建索引库和mapping的DSL语法如下:
在这里插入图片描述
在这里插入图片描述

查看/删除索引库

查看索引库
GET /索引库名
删除索引库
DELETE /索引库名
修改索引库(只允许添加新的字段,不允许修改)
PUT /索引库名/_mapping
{
	"properties":{
		"新字段名":{
			"type“:“integer”
		}
	}
}

文档操作

新增文档的DSL语法如下:

POST /索引库名/_doc/文档id
{
	"字段1":"值1",
	//....
}

查看/删除文档语法:

GET /索引库名/_doc/文档id
DELETE /索引库名/_doc/文档id

修改文档
方式一:全量修改,会删除旧文档,添加新文档

PUT /索引库名/_doc/文档id
{
	"字段1":"值1"
	//......
}

方式二:增量修改,修改指定字段值

POST /索引库名/_update/文档id
{
	"doc":{
		"字段名":"新的值"
	}
}

RestClient操作索引库

什么是RestClient

ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发给ES。

操作索引库

public class test {
    private RestHighLevelClient client;

    @Test
    void testInit(){
        System.out.println(client);
    }
    @Test
    void createHotelIndex() throws IOException {
        // 1. 创建Request对象
        CreateIndexRequest request = new CreateIndexRequest("hotel");
        // 2.准备请求的参数:DSL语句
        request.source("{\n" +
                "    \"mappings\": {\n" +
                "        \"properties\": {\n" +
                "            \"id\": {\n" +
                "                \"type\": \"keyword\"\n" +
                "            },\n" +
                "            \"name\": {\n" +
                "                \"type\": \"text\",\n" +
                "                \"analyzer\": \"ik_max_word\"\n" +
                "            }\n" +
                "        }\n" +
                "    }\n" +
                "}", XContentType.JSON);
        // 3.发送请求
        client.indices().create(request, RequestOptions.DEFAULT);
    }
    @Test
    void testDeleteHotelIndex() throws IOException {
        // 1. 创建Request对象
        DeleteIndexRequest request = new DeleteIndexRequest("hotel");
        // 2.发送请求
        client.indices().delete(request, RequestOptions.DEFAULT);
    }
    @Test
    void testExistHotelIndex() throws IOException {
        // 1. 创建Request对象
        GetIndexRequest request = new GetIndexRequest("hotel");
        // 2.发送请求
        boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
        // 3.输出
        System.err.println(exists ? "索引库已经存在" : "索引库不存在");
    }
    @BeforeEach
    void setUp(){
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://101.35.251.74:9200")
        ));
    }
    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }
}

操作文档

public class DocumentTest {
    private RestHighLevelClient client;

    @Test
    void testAddDocument() throws IOException {
        Hotel hotel = new Hotel();
        hotel.setId(1);
        hotel.setName("测试酒店");
        // 1.准备Request对象
        IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
        // 2.准备JSON文档
        request.source(JSON.toJSONString(hotel), XContentType.JSON);
        // 3.发送请求
        client.index(request, RequestOptions.DEFAULT);
    }
    @Test
    void testGetDocuemtById() throws IOException {
        // 1.准备Request对象
        GetRequest request = new GetRequest("hotel","1");
        // 2.发送请求,得到结果
        GetResponse response = client.get(request, RequestOptions.DEFAULT);
        // 3.解析结果
        String json = response.getSourceAsString();
        Hotel hotel = JSON.parseObject(json, Hotel.class);
        System.out.println(hotel);
    }
    @Test
    void testUpdateDocumentById() throws IOException {
        // 1.准备Request对象
        UpdateRequest request = new UpdateRequest("hotel","1");
        // 2.准备参数,每2个参数为一对key value
        request.doc(
                "name","测试酒店test"
        );
        // 3.更新文档
        client.update(request, RequestOptions.DEFAULT);
    }
    @Test
    void testDeleteDocument() throws IOException {
        // 1.准备request对象
        DeleteRequest request = new DeleteRequest("hotel","1");
        // 2.发送请求
        client.delete(request, RequestOptions.DEFAULT);
    }
    @Test
    void testBulkRequest() throws IOException {
        // 1.创建request
        BulkRequest request = new BulkRequest();
        // 2.准备参数,添加多个新增的Request
        List<Hotel> list = new ArrayList<>();
        Hotel hotel = new Hotel();
        hotel.setId(1);
        hotel.setName("测试酒店");
        Hotel hotel1 = new Hotel();
        hotel.setId(2);
        hotel.setName("测试酒店2");
        list.add(hotel);
        list.add(hotel1);
        for (Hotel hotelEntity : list){
            request.add(new IndexRequest("hotel").id(hotelEntity.getId().toString()).source(JSON.toJSONString(hotelEntity), XContentType.JSON));
        }
        // 3.发送请求
        client.bulk(request, RequestOptions.DEFAULT);
    }
    @BeforeEach
    void setUp(){
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://101.35.251.74:9200")
        ));
    }
    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }
}

DSL查询语法

DSL Query的分类

ES提供了基于JSON的DSL(Domain Specfic Language)来定义查询。常见的查询类型包括:

  1. 查询所有:查询出所有数据,一般测试用。例如:match_all
  2. 全文检索(full text)查询:利用分词器和用户输入内容分词,然后去倒排索引库中匹配。例如:match_query,multi_match_query
  3. 精确查询:根据精确词条值查询数据,一般是查找keyword,数值,日期,boolean等类型字段。例如:ids,range,term
  4. 地理(geo)查询:根据经纬度查询。例如:geo_distance,geo_bounding_box
  5. 复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:bool,function_score

全文检索查询

全文检索查询,会对用户输入内容进行分词,常用语搜索框搜索:

  1. match查询:全文检索查询的一种,会对用户输入的内容分词,然后去倒排索引库检索,语法:
    在这里插入图片描述
  2. multi_match:与match查询类似,只不过允许同时查询多个字段,语法:
    在这里插入图片描述

精确查询

精确查询一般是查找keyword,数值,日期,boolean等类型字段。所以不会对搜索条件分词。常见的有:

  1. term:根据词条精确值查询
    在这里插入图片描述

  2. range:根据值的范围查询
    在这里插入图片描述

地理查询

根据经纬度查询,例如:

  1. geo_boundin_box
    在这里插入图片描述
  2. geo_distance:查询到指定地点小于某个距离值的所有文档
    在这里插入图片描述

复合查询

复合(compound)查询:复合查询可以将其他的简单查询组合起来,实现更复杂的搜索逻辑,例如:

  1. function score:算分函数查询,可以控制文档相关性算分,控制文档排名。
  2. boolean query:布尔查询是一个或多个查询子句的组合。子查询的组合方式有:
    1. must:必须匹配每个子查询,类似“与”
    2. should:选择性匹配查询,类似“或”
    3. must_not:必须不匹配,不参与算分,类似“非”
    4. filter:必须匹配,不参与算分

搜索结果处理

排序

ES支持对搜索结果排序,默认是根据相关度算分(_score)来排序。可以排序字段类型有:keyword类型,数值类型,地理坐标类型,日期类型等。

分页

ES默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。
ES中通过修改from,size参数来控制要返回的分页结果:

深度分页问题

ES是分布式的,所以会面临深度分页问题。
如果搜索页数过深,或者结果集(from+size)越大,对内存和CPU的消耗也越高。因此ES设定结果集查询的上线是10000
针对深度分页,ES提供了两种解决方案:

  1. search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页的数据。官方推荐使用的方式
  2. scroll:原理将排序数据形成快照,保存在内存,官方已经不推荐使用

高亮

高亮:就是在搜索结果中把关键字突出显示
原理是这样的:

  1. 将搜索结果中的关键字用标签标记出来
  2. 在页面中的标签添加CSS样式
    语法:
    在这里插入图片描述

RestClient查询文档

public class HotelSearchTest {
    private RestHighLevelClient client;

    @Test
    void testMatchAll() throws IOException {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.准备DSL
        request.source().query(QueryBuilders.matchAllQuery());
        // 3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        handleResopnse(response);
    }
    @Test
    void testMatch() throws IOException {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.准备DSL
        request.source().query(QueryBuilders.matchQuery("name","测试"));
        // 3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        handleResopnse(response);
    }
    @Test
    void testBool() throws IOException {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.准备DSL
        // 2.1准备BooleanQuery
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        // 2.2 添加term
        boolQuery.must(QueryBuilders.termQuery("name", "测试酒店"));
        // 3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        handleResopnse(response);
    }
    @Test
    void testPageAndSort() throws IOException {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.准备DSL
        request.source().query(QueryBuilders.matchAllQuery());
        // 2.1 排序
        request.source().sort("price", SortOrder.ASC);
        // 2.2分页
        request.source().from(0).size(5);
        // 3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        handleResopnse(response);
    }
    @Test
    void testHighlight() throws IOException {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.准备DSL
        request.source().query(QueryBuilders.matchQuery("name","酒店"));
        request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
        // 3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        handleHighlightResopnse(response);
    }
    @BeforeEach
    void setUp(){
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://101.35.251.74:9200")
        ));
    }
    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }
    void handleResopnse(SearchResponse response){
        // 4.解析结果
        SearchHits searchHits = response.getHits();
        // 4.1查询的总条数
        long value = searchHits.getTotalHits().value;
        System.out.println("总条数:" + value);
        // 4.2文档数据
        SearchHit[] hits = searchHits.getHits();
        // 4.3 遍历
        for (SearchHit hit : hits) {
            // 获取文档source
            String json = hit.getSourceAsString();
            // 反序列化
            Hotel hotel = JSON.parseObject(json, Hotel.class);
            System.out.println(hotel);
        }
    }
    void handleHighlightResopnse(SearchResponse response){
        // 4.解析结果
        SearchHits searchHits = response.getHits();
        // 4.1查询的总条数
        long value = searchHits.getTotalHits().value;
        System.out.println("总条数:" + value);
        // 4.2文档数据
        SearchHit[] hits = searchHits.getHits();
        // 4.3 遍历
        for (SearchHit hit : hits) {
            // 获取文档source
            String json = hit.getSourceAsString();
            // 反序列化
            Hotel hotel = JSON.parseObject(json, Hotel.class);
            // 获取高亮结果
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            // 根据名字获取高亮结果
            HighlightField highlightField = highlightFields.get("name");
            // 获取高亮值
            String name = highlightField.getFragments()[0].string();
            // 覆盖非高亮结果
            hotel.setName(name);
            System.out.println(hotel);
        }
    }
}

聚合

聚合的分类

聚合(aggregations)可以实现对文档数据的统计,分析,运算。聚合常见的有三类:

  1. 桶(Bucket)聚合:用来对文档做分组
    1. TermAggregation:按照文档字段值分组
    2. Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
  2. 度量(Metric)聚合:用以计算一些值,比如:最大值。最小值,平均值等
    1. avg:求平均值
    2. max:求最大值
    3. min:求最小值
    4. Stats:同时求max,min,avg,sum等
  3. 管道(pipeline)聚合:其他聚合的结果为基础做聚合

DSL实现Bucket聚合

现在,我们要统计所有数据中的酒店品牌有几种,此时可以根据酒店品牌的名称做聚合
类型为term类型,DSL示例:
在这里插入图片描述
默认情况下,Bucket聚合会统计Bucket内的文档数量,记为_count,并且按照_count降序排序。
我们可以修改结果排序方式:
在这里插入图片描述
默认情况下,Bucket聚合是对索引库的所有文档做聚合,我们可以限定要聚合的文档范围,只要添加query条件即可:
在这里插入图片描述

DSL实现Metrics聚合

例如,我们要求获取每个品牌的用户评分的min,max,avg等值。
我们可以利用stats聚合:
在这里插入图片描述

RestAPI实现聚合

我们以品牌聚合为例,演示java的RestClient使用

@Test
    void testAggregation() throws IOException {
        // 1.准备request
        SearchRequest request = new SearchRequest("hotel");
        // 2.准备DSL
        // 2.1 设置size
        request.source().size(0);
        // 2.2聚合
        request.source().aggregation(AggregationBuilders.terms("brand").field("brand").size(20));
        // 3.发出请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 4.解析结果
        Aggregations aggregations = response.getAggregations();
        // 4.1根据聚合名称获取聚合结果
        Terms brandTerms = aggregations.get("brand");
        // 获取buckets
        List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
        for (Terms.Bucket bucket : buckets) {
            String key = bucket.getKeyAsString();
            System.out.println(key);
        }
    }

自动补全

自定义分词器

ES中的分词器(analyzer)的组成包含三部分:

  1. character filters:在tokenizer之前对文本进行处理。例如删除字符,替换字符
  2. tokenizer:将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词;还有ik_smart
  3. tokenizer filter:将tokenizer输出的词条做进一步处理。例如大小写转换,同义词处理,拼音处理等。
    我们可以在创建索引库时,通过settings来配置自定义的analyzer(分词器)
    在这里插入图片描述

completion suggester查询

ES提供了completion suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中字段的类型有一些约束:

  1. 参与补全查询的字段必须是completion类型
  2. 字段的内容一般是用来补全的多个词条形成的数组。
    在这里插入图片描述

RestAPI

@Test
    void testSuggest() throws IOException {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.准备DSL
        request.source().suggest(new SuggestBuilder().addSuggestion(
                "suggestion"
        , SuggestBuilders.completionSuggestion("suggestion").prefix("cs").skipDuplicates(true).size(10)));
        // 3.发起请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 4.解析结果
        System.out.println(response);
        Suggest suggest = response.getSuggest();
        // 4.1 根据名称获取补全结果
        CompletionSuggestion suggestion = suggest.getSuggestion("suggestion");
        // 4.2 获取options并遍历
        for (CompletionSuggestion.Entry.Option option : suggestion.getOptions()) {
            // 4.3获取一个option中的text,也就是补全的词条
            String text = option.getText().string();
            System.out.println(text);
        }
    }

数据同步

问题分析

ES中的酒店数据来自于mysql数据库,因此mysql数据发生改变时,ES也必须跟着改变,这个就是ES与mysql之间的数据同步。
方案一:同步调用
在这里插入图片描述
方案二:异步通知
在这里插入图片描述
方案三:监听binlog
在这里插入图片描述

  • 22
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值