初识ES
什么是ES(Elasticsearch)
- 一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能
什么是ELK(Elastic stack)
- 是以elasticsearch为核心的技术栈,包括beats、Logstash、kibana、elasticsearch。
倒排索引
文档与词条
- 文档: 每一条数据就是一个文档
- 词条: 对文档中的内容分词,得到的词语就是词条
正向索引
- 基于文档id创建索引。查询词条时必须先找到文档,而后判断是否包含词条
倒排索引
- 对文档内容分词,对词条创建索引,并记录词条所在文档的信息。查询时先根据词条查询到文档id,而后获取到文档
ES与MySql概念对比
MySql | ES | 说明 |
---|---|---|
Table | Index | 索引(Index)就是文档的集合,类似数据库的表(table) |
Row | Document | 文档(Document)就是一条条数据,类似于数据库中的行(Row),文档都是JSON格式 |
Column | Field | 字段(Filed)就是JSON文档中的字段,类似数据库中的列(Column) |
Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段类型约束,类似数据库的表结构(Schema) |
SQL | DSL | DSL是es提供的JSON风格的请求语句,用来操作es,实现CRUD |
- Mysql擅长事务类型操作,可以确保数据的安全和一致性
- ES擅长海量数据的搜索、分析和计算
索引库操作
mapping属性
mapping 是对索引库中文档的约束
常见的mapping属性:
- type 数据类型,常见的类型有:
- 字符串:text(可分词的文本)、keyword(精确值,如:国家、ip地址)
- 数字:long integer short byte double float
- 布尔:boolean
- 日期:date
- 对象:object
- (对于type的声明都默认作用于相应数组)
- index 是否创建索引,默认为true
- analyzer 使用哪种分词器
- properties 子字段
创建索引库
ES中通过Restful请求操作索引库和文档。请求内容用DSL语句表示。
- 创建索引库和mapping的DSL语法如下:
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名1": {
"type": "text",
"analyzer": "ik_smart"
},
"字段名2": {
"type": "keyword",
"index": "false"
},
"字段名3": {
"properties": {
"子字段名": {
"type": "keyword"
}
}
}
}
}
}
-
查看索引库
GET /索引库名
-
删除索引库
DELETE /索引库名
-
修改索引库
索引库和mapping一旦创建无法修改,只能添加新的字段
语法:PUT /索引库名/_mapping
PUT /索引库名/_mapping
{
"properties": {
"新字段名": {
"type": "integer"
}
}
}
文档操作
- 新增文档的DSL语法:
POST /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
}
}
-
查看文档
GET /索引库名/_doc/文档id
-
删除文档
DELETE /索引库名/_doc/文档id
-
修改文档
- 全量修改:删除旧文档,添加新文档。若文档id不存在,则执行新增操作
- 局部修改:修改指定字段值
PUT /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
}
}
POST /索引库名/_update/文档id
{
"doc": {
"字段名": "新值"
}
}
RestClient操作数据库
初始化JavaRestClient
- 引入es的RestHighLevelClient依赖
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
- 初始化RestHighLevelClient
@SpringBootTest
class HotelDemoApplicationTests {
private RestHighLevelClient client;
@BeforeEach
void setUp(){
// 43.143.xxx.xxx 是云服务器中的ip
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://43.143.xxx.xxx:9200")
));
}
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}
- 创建索引库
@Test
void testCreateHotelIndex() throws IOException {
// 1.创建Request对象
CreateIndexRequest request = new CreateIndexRequest("hotel");
// 2.请求参数,MAPPING_TEMPLATE是静态常量字符串,内容是创建索引库的DSL语句
request.source(MAPPING_TEMPLATE, 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 testExistsHotelIndex() throws IOException {
// 1。创建Request对象
GetIndexRequest request = new GetIndexRequest("hotel");
// 2.发起请求
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
// 3.输出结果
System.out.println(exists);
}
文档操作
新增文档
从MySql数据库查询数据,批量导入到es
@Test
void testAddDocument() throws IOException {
// 0.创建批量导入Request
BulkRequest request = new BulkRequest();
// 1.从MySQL中查询数据
List<Hotel> hotelList = hotelService.list();
for (Hotel hotel : hotelList){
// 2.转换文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
// 3.Request中添加数据
request.add(new IndexRequest("hotel")
.id(hotelDoc.getId().toString())
.source(JSON.toJSONString(hotelDoc),XContentType.JSON));
// 4.发送请求
client.bulk(request,RequestOptions.DEFAULT);
}
}
查询文档
@Test
void testGetDocumentById() throws IOException {
// 1.准备Request
GetRequest request = new GetRequest("hotel", "36934");
// 2.发送请求,得到响应
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 3.解析相应结果
String json = response.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
// 4.打印查看结果
System.out.println(hotelDoc);
}
更新文档
- 全量更新:再次写入id一样的文档,就会删除旧文档,添加新文档
- 局部更新:只更新部分字段
@Test
void testUUpdateDocumentById() throws IOException {
// 1.创建Request对象
UpdateRequest request = new UpdateRequest("hotel", "36934");
// 2.准备参数,每两个参数为一对k v
request.doc(
"price",347,
"starName","三钻"
);
// 3.更新文档
client.update(request,RequestOptions.DEFAULT);
}
删除文档
@Test
void testDeleteDocument() throws IOException {
// 1.准备Request
DeleteRequest request = new DeleteRequest("hotel", "36943");
// 2.发送请求
client.delete(request,RequestOptions.DEFAULT);
}
ES搜索功能
DSL查询文档
基本语法:
GET /索引库名/_search{
"query":{
"查询类型":{
"FIELD":"TEXT"
}
}
}
DSL查询分类
- 查询所有:查询出所有数据,一般测试使用
- 全文检索查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配
- 精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段
- 地理查询:根据经纬度查询
- 复合查询:将上述查询条件组合起来,合并查询条件
全文检索查询
match根据一个字段查询
muti_match根据多个字段查询,参与查询字段越多,查询性能越差
GET /hotel/_search
{
"query": {
"match": {
"FIELD": "TEXT"
}
}
}
GET /hotel/_search
{
"query": {
"multi_match": {
"query": "TEXT"
"fiekds": ["FIELD1","FIELD2"]
}
}
}
精准查询
精确查询是根据id、数值、keyword类型或bool字段来查询
- term查询
GET /indexName/_search
{
"query":{
"term": {
"FIELD": {
"value": "VALUE"
}
}
}
}
- range查询
GET /indexName/_search
{
"query":{
"range": {
"FIELD": {
"gte":10,
"lte": 20
}
}
}
}
地理坐标查询
- geo_bounding_box: 查询geo_point值落在某个矩形范围内的所有文档
GET /indexName/_search
{
"query": {
"geo_bounding_box":{
"FIELD":{
"top_left":{ // 左上角
"lat":33.3,
"lon":122.2
},
"bottom_right":{ // 右下角
"lat":30.9,
"lon":121.7
}
}
}
}
}
- geo_distance: 查询到指定中心点小于某个距离值的所有文档
GET /indexName/_search
{
"query": {
"geo_distance":{
"distance": "15km",
"FIELD": "31.21,121.5"
}
}
}
复合查询
复合查询可以将其它简单查询组合起来,实现复杂的搜索逻辑,如:fuction score算分函数查询,可以控制文档相关性得分,控制文档排名。
GET /hote;/_search{
"query":{
"function_score":{
"query":{
"match":{
"all":"外滩"
}
},
"functions"[{
"filter":{
"term":{
"brand":"如家"
}
},
"weight":10
}],
"boost_mode":"mltiply"
}
}
}
参数说明:
- query: 原始查询条件,搜索文档并根据相关性打分(query score)
- filter:过滤条件,符合条件的文档才会被重新算分
- weight:算分函数,算分函数的结果称为function score,将来会与query score运算,得到新算分,常见的算分函数有:
- weight:给一个常量值,作为函数结果
- field_value_factor:用文档中的某个字段值作为函数结果
- random_score:随机生成一个值,作为函数结果
- script_score:自定义计算公式,公式结果作为函数结果
- boost_mode: 加权模式,定义function score和query score的运算方式,包括:
- multiply:两者相乘。默认
- replace:用function score替换query score
- 其它:sum、avg、max、min
布尔查询是一个或多个查询子句的组合。子查询的组合方式有
- must:必须匹配每个子查询,与
- shoud:选择性匹配子查询,或
- must_not:必须不匹配,不参与算分,非
- filter:必须匹配,不参与算分
bool查询示例:
GET /hotel/_search
{ "query":{
"bool":{
"must":[
{"term":{"city":"上海"}}
],
"should":[
{"term":{"brand":"皇冠假日"}},
{"term":{"brand":"华美达"}}
],
"must_not":[
{"range":{"price":{"lte":500}}}
],
"filter":[
{"range":{"score":{"gte":45}}}
]
}
}
}
搜索结果处理
排序
ES支持对搜索结果排序,默认是根据相关度算分来排序。可排序的字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等
按照score降序,score相同,再按照price升序
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"score": {
"order": "desc"
},
"price": {
"order": "asc"
}
}
]
}
分页
ES默认情况下只返回top10的数据,如需查询更多数据,需要修改分页参数。
ES是分布式的,会面临深度分页问题。如果搜索页数过深,或者结果集(from + size)越大,对内存和cpu的消耗也越高,因此ES设定结果集查询的上限是10000。
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 10, // 分页开始的位置,默认为0
"size": 20, // 期望获取的文档总数
"sort": [
{"price": "asc"}
]
}
高亮
高亮原理:
- 将搜索结果中的关键字用标签标记出来
- 在页面中给标签添加css样式
GET /hotel/_search
{
"query": {
"match": {
"FIELD": "TEXT"
}
},
"highlight": {
"fields": {
"FIELD":{ // 指定要高亮的字段
"pre_tags": "<em>", // 标记高亮字段的前置标签
"post_tags": "</em>" // 标记高亮字段的后置标签
}
}
}
}
默认情况下,ES搜索字段必须与高亮字段一致,但是可以通过 require_field_match:false
更改
GET /hotel/_search
{
"query": {
"match": {
"all": "如家"
}
},
"highlight": {
"fields": {
"name":{
"pre_tags": "<em>",
"post_tags": "</em>",
"require_field_match": "false"
}
}
}
}
RestClient查询文档
查询基本步骤
- 创建SearchRequest对象
- 准备Request.source(),也就是DSL
- REQUEST.source()中 包含查询、排序、分页、高亮等所有功能
- QuerBuilders中包含各种查询方法
- 发送请求,得到结果
- 结果解析(参照JSON结果,从外到内,逐层解析)
@Test
void testMatchAll() throws IOException {
// 1.准备Request
SearchRequest searchRequest = new SearchRequest("hotel");
// 2.准备DSL,不同的查询类型只需要更改query()中的查询方法,注意使用QueryBuilders
searchRequest.source().query(QueryBuilders.matchAllQuery());
// 3.发送请求
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
// 4.解析结果
System.out.println(response);
}
分页和排序
@Test
void testPageAndSort() throws IOException {
// 0.模拟前端传过来的数据
int page = 2,size = 15;
// 1.准备Request
SearchRequest searchRequest = new SearchRequest("hotel");
// 2.准备DSL
// 2.1 query
searchRequest.source().query(QueryBuilders.matchAllQuery());
// 2.2 排序
searchRequest.source().sort("price", SortOrder.ASC);
// 2.3 分页
searchRequest.source().from((page - 1) * size).size(size);
// 3.发送请求
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
// 4.解析结果
System.out.println(response);
}
高亮
@Test
void testHighlight() throws IOException {
// 1.准备Request
SearchRequest searchRequest = new SearchRequest("hotel");
// 2.准备DSL
// 2.1 query
searchRequest.source().query(QueryBuilders.matchQuery("all","如家"));
// 2.2.高亮
searchRequest.source().highlighter(new HighlightBuilder()
// 高亮字段
.field("name")
// 是否需要与查询字段匹配
.requireFieldMatch(false)
);
// 3.发送请求
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
// 4.对高亮结果的处理
// 4.1 解析响应
SearchHits searchHits = response.getHits();
// 4.2 获取文档数组
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit:hits){
// 4.3 获取文档source并反序列化为对象
HotelDoc hotelDoc = JSON.parseObject(hit.getSourceAsString(), HotelDoc.class);
// 4.4 处理高亮
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (!CollectionUtils.isEmpty(highlightFields)){
// 4.5 获取高亮字段结果
HighlightField highlightField = highlightFields.get("name");
if (highlightField != null){
// 高亮结果数组中的第一个,即为酒店名称
String name = highlightField.getFragments()[0].string();
// 覆盖非高亮结果
hotelDoc.setName(name);
}
}
System.out.println(hotelDoc);
}
}
数据聚合
聚合(aggregations)可以实现对文档数据的统计、分析、运算。参与聚合的字段类型必须是:keyword、数值、日期、布尔
常见的聚合有三类:
- 桶(Bucket)聚合:对文档分组
- TermAggregation:按照文档字段值分组
- DataHistogram:按日期阶梯分组,例如一周一组、一月一组
- 度量(Metric)聚合:用来计算一些值,比如最大值、最小值、平均值等
- Avg:求平均值
- Max:求最大值
- Min:求最小值
- Stats:同时求max、min、avg、sum等
- 管道(pipeline)聚合: 以其它聚合的结果为基础做聚合
DSL实现Bucket聚合
默认情况下,Bucket聚合是对索引库中所有文档做聚合,一般需要使用query条件限定聚合文档范围
GET /hotel/_search
{
"query": {
"range": {
"price": {
"lte": 200 // 只对price<=200的文档做聚合
}
}
},
"size": 0, // 设置size为0,结果中不包含文档,只包含聚合结果
"aggs": { // 定义聚合
"brandAgg": { // 给聚合起名字
"terms": { // 聚合的类型,按照品牌值聚合,所以选term
"field": "brand", // 参与聚合的字段
"size": 10 // 获取的聚合结果的数量
}
}
}
}
DSL实现Metrics聚合
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 10,
"order": { // 对bucket中的数据按照scoreAgg.avg字段排序
"scoreAgg.avg": "desc"
}
},
"aggs": { // brandAgg的子聚合,即分组后对每组分别计算
"scoreAgg": { // 聚合名称
"stats": { // 聚合类型
"field": "score" // 聚合字段
}
}
}
}
}
}
RestClient实现聚合
@Test
void testAggregation() throws IOException {
// 1.准备Request
SearchRequest searchRequest = new SearchRequest("hotel");
// 2.准备DDL
// 2.1设置size,丢弃文档结果,只显示聚合结果
searchRequest.source().size(0);
// 2.2 聚合
searchRequest.source().aggregation(AggregationBuilders
.terms("brandAgg")
.field("brand")
.size(10)
);
// 3.发出请求
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
// 4. 解析结果
System.out.println(searchResponse);
}
数据同步
- 同步调用
- 优点:实现简单、粗暴
- 缺点:业务耦合度高
- 异步通知
- 优点:低耦合,实现难度一般
- 缺点:依赖mq的可靠性
- 监听binlog
- 优点:完全解除服务间耦合
- 缺点:开启binlog增加数据库负担、实现复杂度高