ES结合kibana、Logstash、Beat,也就是elastic stack(ELK)。用于日志数据分析可视化,实时监控等领域
elasticseach是slastic stack的核心,负责存储,搜索,分析数据
elasticseach的底层是Lucene,Lucene是Java语言的一个搜索引擎类库,是Apache公司的顶级项目,官网https://lucene.apache.org/
基于Lucene做二次开发形成elasticseach
传统数据库,如MySQL采用正向索引
![image-20231103141052785](https://gitee.com/zwx0203/cloudimage/raw/master/202311031410896.png
文档(可以理解为mysql表中一行数据),序列化为json格式后存储在elasticseach中
索引(index):相同类型的文档的集合
概念对比
DSL使用JSON风格语句来CRUD。
在MySQL中SQL通过connection发给MySQL。
而DSL通过http来发送请求,因为es给的是restful接口,这种接口与语言无关,任何只要能发http请求的语言都能把它的DSL发给es的restful接口让es进行处理。
问题:什么时候用mysql什么时候用es ?
两者实际上是互补关系,而不是替代的关系
用户写数据直接写到mysql,因为mysql可确保数据安全&一致性。
用户搜索数据则通过es来进行。
通过中间组件将mysql数据同步给es
elasticsearch:Elasticsearch 7.12.0 | Elastic
下载解压后,双击bin中的elasticsearch.bat 然后访问http://localhost:9200/ 若生成如下界面 则成功
Kibana:Kibana 7.12.0 | Elastic
下载解压后,双击bin中kibana.bat 然后访问http://127.0.0.1:5601/ 若生成如下界面 则成功
ik分词器:https://github.com/medcl/elasticsearch-analysis-ik 将ik分词器解压后放到es目录的plugins下
ik_smart 分词算法 ,最少切分
ik_max_word 模式 最细切分
![image-20231103155325717](https://gitee.com/zwx0203/cloudimage/raw/master/202311031553762.png
ik分词器的使用:【精选】ElasticSearch——IK分词器的下载及使用_ik分词器下载-CSDN博客
ik分词器-拓展词库
ik分词器-禁用词库
什么时候分词?
答:有两个时间分词。第一个是将文档创建到索引的时候,要对文档某个内容进行分词,将词条创建倒排索引
第二个是当用户来搜索时,用户输入一大串话,需要给它进行分词
IK分词器总结
索引库对应MySQL中的表,文档对应MySQL表中一行行的数据。
在MySQL中先创建表,才能在表中存入数据。
在elasticsearch中同样先得建立索引库,才能存入文档。
因此,这里先学索引库操作,再学文档操作。
es中没有数组,但它允许同一种类型有多个值。数据类型则为数组中数据的类型
创建下面的索引表
#创建索引库
PUT /heima_all
{
"mappings":{
"properties": {
"age": {
"type": "integer"
},
"weigth": {
"type": "float"
},
"isMarried": {
"type": "boolean"
},
"info": {
"type": "text",
"analyzer": "ik_max_word"
},
"email": {
"type": "keyword",
"index": false
},
"score": {
"type": "float"
},
"name": {
"type": "object",
"properties": {
"firstname": {
"type": "keyword"
},
"lastName": {
"type": "keyword"
}
}
}
}
}
}
GET /索引库名
DELETE /索引库名
es禁止修改索引库,但可以添加新的字段
#在heimaa索引库中新增"address"字段
PUT /heimaa/_mapping
{
"properties": {
"address": {
"type": "text",
"analyzer": "ik_smart"
}
}
}
总结
案例
要根据多个字段,比如根据brand,name,business等来查,则可以使用copy_to定义一个字段,将brand,name,business等拷贝到指定字段如下。在通过指定字段去查就好了
###酒店demo的mapping
PUT /hotel
{
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"name": {
"type": "text",
"analyzer": "ik_max_word",
"copy_to": "all"
},
"address": {
"type": "keyword",
"index": false
},
"price": {
"type": "integer"
},
"score": {
"type": "integer"
},
"brand": {
"type": "keyword",
"copy_to": "all"
},
"city": {
"type": "keyword"
},
"starName": {
"type": "keyword"
},
"business": {
"type": "keyword",
"copy_to": "all"
},
"location": {
"type": "geo_point"
},
"pic": {
"type": "keyword",
"index": false
},
"all": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
步骤3详见 hotel-demo项目 D:\zwx\code3\hotel-demo
步骤4 用JavaRestClient来创建索引库
在测试类中先进行如下操作,http://localhost:9200为ES客户端的地址
private RestHighLevelClient client;
@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://localhost:9200")
));
}
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
总结Java RestClient索引库操作的步骤
案例
步骤1 初始化在操作索引库中已经完成
步骤2 新增文档
/**
* 创建文档(倒排索引)
* @throws IOException
*/
@Test
void AddHotelDocument() throws IOException {
//根据id查询酒店数据
Hotel hotel = hotelService.getById(47478L);
//转化为文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
//1 准备request对象
IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
//2 准备json 文档
//JSON.toJSONString(对象) :将对象序列化为Json
request.source(JSON.toJSONString(hotelDoc),XContentType.JSON);
//3 发送请求
client.index(request, RequestOptions.DEFAULT);
}
步骤3 查询文档
/**
* 查找文档
* @throws IOException
*/
@Test
void getDocumentById() throws IOException {
//1 准备请求
GetRequest request = new GetRequest("hotel","47478");
//2 发出响应,返回结果
GetResponse response = client.get(request, RequestOptions.DEFAULT);
//3 处理结果
String json = response.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}
步骤4 修改文档
/**
* 修改文档-局部更新
*/
@Test
void updateDocumentById() throws IOException {
//1 创建request对象
UpdateRequest request = new UpdateRequest("hotel","47478");
//2 准备参数
request.doc(
"name","速8酒店(上海松江中心店)"
);
//3
client.update(request, RequestOptions.DEFAULT);
}
步骤5 删除文档
/**
* 删除文档
*/
@Test
void deleteDocumentById() throws IOException {
//1 创建request对象
DeleteRequest request = new DeleteRequest("hotel","47478");
//2 删除
client.delete(request, RequestOptions.DEFAULT);
}
总结
/**
* 将mysql中酒店数据全导入es
* @throws IOException
*/
@Test
void testBulkRequest() throws IOException {
//批量查询酒店数据
List<Hotel> hotels = hotelService.list();
//1 创建Request
BulkRequest request = new BulkRequest();
//2 准备参数 添加多个Request对象
for (Hotel hotel : hotels) {
// 将hotel转成hotelDoc
HotelDoc hotelDoc = new HotelDoc(hotel);
// 创建新增文档的Request对象, add里面可以写Index,Delete等语句
request.add(new IndexRequest("hotel")
.id(hotelDoc.getId().toString())
.source(JSON.toJSONString(hotelDoc), XContentType.JSON));
}
//3 发送请求
client.bulk(request,RequestOptions.DEFAULT);
}
详见官网文档:[Elasticsearch Guide 8.11] | Elastic
全文检索查询,会对用户输入内容分词,常用于搜索
match查询:全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索
multi_match查询:根据多个字段查
查询语法:
###全文检索查询
#match查询 推荐使用 搜索的字段越多,查询的效率越低
GET /hotel/_search
{
"query": {
"match": {
"all": "外滩如家"
}
}
}
#multi_match查询
GET /hotel/_search
{
"query": {
"multi_match": {
"query": "外滩如家",
"fields": ["all","city"]
}
}
}
小结
建议使用copyto,把多个要查的字段拷贝到一个字段中。如下 “all” 字段包含了 name,brand,business 三个字段
###酒店demo的mapping
PUT /hotel
{
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"name": {
"type": "text",
"analyzer": "ik_max_word",
"copy_to": "all"
},
"address": {
"type": "keyword",
"index": false
},
"price": {
"type": "integer"
},
"score": {
"type": "integer"
},
"brand": {
"type": "keyword",
"copy_to": "all"
},
"city": {
"type": "keyword"
},
"starName": {
"type": "keyword"
},
"business": {
"type": "keyword",
"copy_to": "all"
},
"location": {
"type": "geo_point"
},
"pic": {
"type": "keyword",
"index": false
},
"all": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
查keyword、数值、日期、boolean等字段,不分词
例如:
###精确检索: 不分词,搜到的跟给的一模一样
#term查询
GET /hotel/_search
{
"query": {
"term": {
"city": {
"value": "深圳上海"
}
}
}
}
#range查询
GET /hotel/_search
{
"query": {
"range": {
"price": {
"gte": 1108,
"lte": 3000
}
}
}
}
小结
FIELD的类型为"geo_point"
geo_bounding_box 查询(用的比较少),
#geo_bounding_box: 查询geo_point值落在某个矩形范围的所有文档
GET /hotel/_search
{
"query": {
"geo_bounding_box": {
"location": {
"top_left": {
"lat": 31.1,
"lon": 121.5
},
"bottom_right": {
"lat": 30.9,
"lon": 121.7
}
}
}
}
}
geo_distance 查询 (用的多)
#geo_distance 查询
GET /hotel/_search
{
"query": {
"geo_distance": {
"distance": "2km",
"location": "31.21,121.5"
}
}
}
前面全文检索查询,精确查询,地理查询统称为简单查询。复合查询是将简单查询组合起来,实现更复杂的搜索逻辑
使用Function Score Query 来人为地修改相关性算分(比如针对RMB玩家,让人家的相关性算分高一点)
案例:给"如家"酒店排名靠前
#function score查询
GET /hotel/_search
{
"query": {
"function_score": {
"query": {
"match": {
"all": "外滩"
}
},
"functions": [
{
"filter": {
"term": {
"brand": "如家"
}
},
"weight": 10
}
],
"boost_mode": "sum"
}
}
}
案例
放到must中,会影响算分,算分的条件越多,性能越差,故把不重要的放到must_not中
#复合查询之bool查询 过滤查询
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "如家"
}
},
{
"geo_distance": {
"distance": "10km",
"location": {
"lat": 31.21,
"lon": 121.5
}
}
}
],
"must_not": [
{
"range": {
"price": {
"gt": 400
}
}
}
]
}
}
}
放到must_not中不影响算分
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "如家"
}
}
],
"must_not": [
{
"range": {
"price": {
"gt": 400
}
}
}
],
"filter": [
{
"geo_distance": {
"distance": "10km",
"location": {
"lat": 31.21,
"lon": 121.5
}
}
}
]
}
}
}
bool查询小结
案例1:酒店数据按照用户评价降序排序,评价相同则按照价格升序排序
#sort 排序
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"score": "desc"
},
{
"price": "asc"
}
]
}
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"score": {
"order": "desc"
},
"price": {
"order": "asc"
}
}
]
}
案例2:看地图上哪些酒店离你近,做一个升序排序
from = (第几页 - 1) * size,从1开始
GET /hotel/_search
{
"query": {"match_all": {}},
"sort": [{"price": "asc"}],
"from": 9990 ,
"size": 10
}
深度分页问题
把搜索结果中的关键字高亮显示
#高亮,#默认标签是。 默认情况下,ES搜索字段必须与高亮字段一致,但可以把"require_field_match" 设为false来实现,搜索字段与高亮字段不一致也可以高亮
GET /hotel/_search
{
"query": {
"match": {
"all": "如家"
}
},
"highlight": {
"fields": {
"name": {
"pre_tags": "", # 这两句可以不写,因为Es默认就是这两句
"post_tags": "", #
"require_field_match": "false"
}
}
}
}
match、multi_match、match_all
QueryBuilders.matchAllQuery()
精确查询
@Test
void testBool() throws IOException {
//1 准备request
SearchRequest request = new SearchRequest("hotel");
//2 准备DSL
// 准备booleanQuery
BoolQueryBuilder booledQuery = QueryBuilders.boolQuery();
// 添加term
booledQuery.must(QueryBuilders.termQuery("city","深圳"));
booledQuery.filter(QueryBuilders.rangeQuery("price").lte(350));
request.source().query(booledQuery);
//3 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4 处理请求
handleResponse(response);
}
private static 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) {
String json = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}
}
要构建查询只需要记住QueryBuilders类就行了
对搜索结果的排序和分页是与查询在同级的参数
@Test
void testPageAndSort() throws IOException {
//页码, 每页大小
int page = 2,size =5;
//1 准备request
SearchRequest request = new SearchRequest("hotel");
//2 准备DSL
//2.1 query
request.source().query(QueryBuilders.matchAllQuery());
//2.2 sort
request.source().sort("price", SortOrder.ASC);
//2.3 分页 from、size
request.source().from((page - 1) * size).size(5);
//3 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4 处理请求
handleResponse(response);
}
@Test
void testHighlight() throws IOException {
//页码, 每页大小
int page = 2,size =5;
//1 准备request
SearchRequest request = new SearchRequest("hotel");
//2 准备DSL
//2.1 query
request.source().query(QueryBuilders.matchQuery("all","如家 "));
//2.4 高亮 highlight
request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
//3 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4 处理请求
handleHighlightResponse(response);
}
private static void handleHighlightResponse(SearchResponse response) {
//4解析结果
SearchHits searchHits = response.getHits();
//4.1 获取总条数
long total = searchHits.getTotalHits().value;
System.out.println("一共搜索到" + total + "条数据");
//4.2 文档数组
SearchHit[] hits = searchHits.getHits();
//4.3 遍历
for (SearchHit hit : hits) {
//获取文档source
String json = hit.getSourceAsString();
//反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
//获取高亮结果
Map 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(hotelDoc);
}
}
启动hotel-deomo服务,进入http://localhost:8089/
#添加isAD字段
POST /hotel/_update/1908594080
{
"doc": {
"isAD": true
}
}
POST /hotel/_update/1725781423
{
"doc": {
"isAD": true
}
}