非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容
什么是elasticsearch
一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控
什么是elastic stack
是以elasticsearch为核心的技术栈,包括beats、Logstash、Kibana、elasticsearcg
什么是Lucene
是Apache的开源搜索引擎类库,提供了搜索引擎的核心API
什么是文档和词条
什么是正向索引
什么是倒排索引
elasticsearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单消息。文档数据会把小米,华为转换为json格式后存储在elasticsearch中
MySql | Elasticsearch | 说明 |
---|---|---|
Table(table structure) | Index | 索引,就是文档的集合,类似数据库的表 |
Row | Document | 文档,就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 |
Column | Field(字段) | 字段,就是JSON文档中的字段,类似数据库的列 |
Schema | Mapping | Mapping是索引中文档的约束,列如字段类型约束。类似数据库的表结构 |
SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD |
Mysql:擅长事务类型操作,可以确保数据的安全和一致性(适用于支付之类的需要安全的场景)
Elasticsearch:擅长海量数据的搜索、分析、计算(适用于页面搜索,查询商品之类的海量搜索)
elasticsearch于数据库的关系:
部署kibana容器,因此需要让es和kibana容器互联,首先需要建立一个网络
docker network create es-net
运行docker命令,部署单点es:
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms1024m -Xmx1024m" \
-e "discovery.type=single-node" \
-v /usr/elasticsearch/es-data:/usr/share/elasticsearch/data \
-v /usr/elasticsearch/es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es-net \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1
解释
docker run -d \ #-d后台运行
--name es \ #指定名字
-e "ES_JAVA_OPTS=-Xmx512m -Xmx512m" \ #配置环境变量设置内存
-e "discovery.type=single-node" \ #运行模式是单点模式,一般是集群模式
-v es-data:/usr/share/elasticsearch/data \ #挂载数据卷,冒号前面的需要自己去虚拟机创建,如果不指定路径则会被创建到docker根目录的volume文件夹下
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es-net \ #让es容器加入网络
-p 9200:9200 \ #暴露在http下的端口,供用户访问
-p 9300:9300 \ #暴露在内部,es各个节点互联的端口
elasticsearch:7.12.1 #最后指定版本
kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习。Elasticsearch和Kibana的版本需要保持一致
运行docker命令,部署Kibana
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601 \
kibana:7.12.1
es在创建倒排索引时需要对文档分词;在搜索时,需要对用户输入内容分词。但默认的分词规则对中文处理并不友好。我们在kibana的DevToos中测试
POST /_analyze
{
"analyzer":"standard",
"text":"今天天气不错"
}
语法说明
下载ik分词器
在官网下载ik分词器 https://github.com/medcl/elasticsearch-analysis-ik/releases
解压放到在es的data目录中
这里的data目录是之前es所挂载的那个目录,如果没有挂载的话就要开启容器之后使用命令
docker exec -it xxx(自己创建的容器名称) bash
重启es容器 docker restart es配置完成
ik分词器的分析器参数
底层就是在内部庞大的数据字典中检索
当需要拓展或限制ik分词器中的内容时,可以去ik分词器目录中的ikAnalyzer.cfg.xml文件中修改配置
mapping映射是对索引库中文档的约束,常见的mapping属性包括
{
"age":xxx
"weight":xxx
"isMarried":xxxx
"info":xxx
"name":{
"firstname":xxx
"lastname":xxx
}
}
创建索引库
PUT /索引名
{
"mappings":{
"properties":{
根据自己的需要创建字段
}
}
}
查看、删除索引库
GET /索引名
DELETE /索引名
修改索引库
索引库和mapping一旦创建无法修改,但是可以添加新的字段
PUT /索引名/_mapping
{
"properties":{
"新字段名":{
"type":"integer"
}
}
}
索引库操作有那些
新增文档的DSL语法如下:
POST /索引库名(实际上类似于表,这个索引规定了表结构)/_doc/1(这是指定的id,方便以后删除和查找)
GET /索引库名/_doc/1(刚才创建文档所指定的id)
DELETE /索引库名/_doc/1(刚才创建文档所指定的id)
方式一:全量修改,会删除旧文档,添加新文档(特点,既能新增也能修改,检索对应id来修改,如果没有对应的id则直接新建)
PUT /索引名/_doc/id
{
"字段1":xxx,
"字段2":xxx,
etc
}
方式二:局部修改,修改指定字段值(优先使用)
POST /索引名/_update/文档id
{
"doc":{
"字段名":"新的值"
etc
}
}
创建文档:POST /索引名/__doc/文档id 或者PUT /索引名/_doc/文档id
后者是先删除在创建,实际开发中选择POST
查询文档:GET /索引名/_doc/文档id
删除文档:DELETE /索引名/_doc/文档id
修改文档:
ES官方提供了各种不同语言的客户端,用来操作ES,这些客户端的本质就是组装DSL语句,通过http请求发送给ES。
导入资料demo
分析数据结构,定义mapping属性
mapping要考虑的问题
字段名、数据类型、是否参与搜索、是否分词、如果分词,分词器是什么
ES中支持两种地理坐标数据类型
geo_point:由维度(latitude)和经度(longitude)确定的一个点列如”xx.xxxx ,xx.xxxx“
geo_shape:有多个geo_point组成的复杂几何图形,列如一条直线
linestring(-77.xxxxxxx 38.xxxxx, -77.xxxxx 38.xxxxxx)
字段拷贝可以使用copy_to属性将当前字段拷贝到指定字段。
"all":{
"type":"text",
"analyzer":"ik_max_word"
},
"brand":{
"type":"keyword",
"copy_to":"all"
}
初始化JavaRestClient
引入es的RestHighLevelClient依赖
org.elasticsearch.client
elasticsearch-rest-high-level-client
因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本
在pom文件中加入这句话强制控制版本,版本不一会导致报错
7.12.1
初始化RestHighLevelClient
private RestHighLevelClient client;
@Test
void testInit(){
System.out.println(client);
}
//当你需要这个RestHighLevelClient的时候,就不用每次都去创建一个关联的连接
@BeforeEach
void setUp() {
this.client=new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.26.131:9200")
));
}
利用JavaRestClient创建索引库
//1.创建Request对象
CteateIndexRequest request=new CreateIndexRequest("索引名"); //这里这个名字就是索引的名字
//2.请求参数,MAPPING_TEMPLATE是静态字符串常量,内容是创建索引库的DSL语句
request.source(MAPPING_TEMPLATE,XCntentType.JSON);
//3.发起请求
client.indices().create(request,RequestOptions.DEFAULT);
利用JavaRestClient删除索引库
//创建一个DeleteIndexRequest
DeleteIndexRequest delete=new DeleteIndexRequest("索引名");
//调用client的删除命令
client.indices().delete(delete,RequestOptions.DEFAULT);
利用JavaRestClient判断索引库是否存在
//创建一个GetIndexRequest
GetIndexRequest get=new GetIndexRequest("索引名");
client.indices().exists(get,RequestOptions.DEFAULT);
//1.首先准备数据,先从数据库中查询数据
调用方法得到比如说hotel数据
//2.准备request对象
IndexRequest request=new IndexRequest("索引名").id("文档id");
//3.准备JSON文档
request.source(JSON.toJSONString(hotel),XContentType.JSON);
//4.发送请求
client.index(request,RequestOptions.DEFAULT);
//1.准备request对象
GetRequest request=new GetRequest("索引名").id("文档id");
//2.发送请求
GetResponse response=client.get(request,RequestOptions.DEFAULT);
//3.解析响应结果
String json=response.getSourceAsString();
HotelDoc result=JSON.parseObject(json,HotelDoc.class);
Sysem.out.print(result);
//准备request对象
DeleteRequest request=new DeleteRequest("索引名","文档id");
//发送请求
client.delete(request,RequestOptions.DEFAULT);
局部更新,这个不会删除
POST /hotel/_update/id
{
"doc":{
字段名
}
}
全局更新,先删除在创建
PUT /hotel/_doc/id
{
"字段1":{
},
"字段2":{
}
}
以上是在es内部的写法
在JAVA中
//1.准备request对象
UpdateRequest request=new UpdateRequest("hotel","xxxx"); //第二个参数是需要更新的id
//2.修改的数据是什么
request.doc("price",12313,"city","Washington"); //格式key value对应,逗号分隔
//3.发送请求
client.update(request,RequestOptions.Default);
//从数据库中批量查询数据
Hotel hotel=xxxservice.list();
//1.创建request
BulkRequest request=new BulkRequest();
//2.准备参数,添加多个新增的Request
for(Hotel hotel : hotels){
HotelDoc hotelDoc=new HotelDoc(hotel);
request.add(new IndexRequest("hotel")
.id(hotel.getId().toString())
.source(JSON.toJSONString(hotelDoc),XContentType.JSON)) //转换文档类型
}
//3.发送请求
client.bulk(request,RequestOptions.DEFAULT)
fastJSON可以把数据序列化为JSON的格式具体操作为
JSON.toJSONString() 把想要序列化的值放入其中
在es中批量查询的语句
GET /索引名/_search
分类 | 用法 | 命令 |
---|---|---|
查询所有 | 查询出所有数据,一般测试用 | match_all |
全文检索full-text | 利用分词器对用户输入内容分词,然后再去倒排索引库中匹配 | match_query multi_match_query |
精确查询 | 根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段 | ids range term |
地理查询 | 根据经纬度查询 | geo_distance geo_bounding_box |
复合查询 | 复合查询可以将上述各种查询天配件结合起来,合并查询条件 | bool function_score |
DSL的基本语法
GET /索引名/_search
{
"query":{
"查询类型":{"FIELD":"TEXT"}
}
}
match查询:全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索
GET /索引名/_search
{
"query":{
"match":{
"FIELD":"TEXT"
}
}
}
multi_match:于match查询类似,但他允许同时查询多个字段
GET /索引名/_search
{
"query":{
"multi_match":{
"query":"",
"字段名1":["查询条件1","查询条件2"]
}
}
}
match和multi_match的区别是什么
查找keyword、数值、日期、boolean等类型字段,所以不会对搜索条件分词,常见的类型如下
命令 | 解释 |
---|---|
term | 根据词条精确值查询 |
range | 根据值的范围查询 |
语法
term:
GET /索引名/_search
{
"query":{
"term":{
"field":{
"value":想要精确查找的值
}
}
}
}
range
GET /索引名/_search
{
"query":{
"range":{
"field":{
"gte":大于等于的数的范围, # greater than equal 这里还可以选择仅大于就是gt greater than
"lte":小于等于的数的范围 # less than equal 同理小于就是lt less than
}
}
}
}
精确查询常见的有那些
根据经纬度查询。常见的使用常见包含
根据经纬度查询,官方文档
ES中的语法
GET /索引名/_search
{
"query":{
"geo_bounding_box":{
"FIELD":{
"top_left":{ #左上角的坐标
"lat":xxx, #latitude 纬度
"lon":xxx #longitude 经度
},
"bottom_right":{ #右下角的坐标
"lat":xxx,
"lon":xxx
}
}
}
}
}
解析:得到左上和右下的坐标之后四条线连接这两个点得到一个检索的范围,如下图,方框内则是你定位坐标内的范围
ES中的用法
GET /索引名/_search
{
"query":{
"geo_distance":{
"distance":"xxxkm",
"FIELD":"xxx.xx,xxx.xx" #类型为geo_point
}
}
}
解析:以一个坐标点为中心,以定义的distance为半径所画圆的范围,如下图
复合(compound)查询:复合查询可以将其他简单查询组合起来,实现更复杂的搜索逻辑,比如
使用function score query
GET /索引名/_search
{
"query":{
"function_score":{
"query":{"match":{"all":"外滩"}},
"functions":[
{
"filter":{"term":{"id":"1"}},
"weight":10
}
],
"boost_mode":"multiply"
}
}
}
复合查询Boolean Query
布尔查询是一个或多个子句的组合,子查询的组合方式有:
must:必须匹配每个子查询,类似”与“
should:选择性匹配子查询,类似”或“
must_not:必须不匹配,不参与算分,类似”非“
filter:必须匹配,不参与算分
GET /hotel/_search
{
"query":{
"bool":{
"must":[],
"should":[],
"must_not":[],
"filter":[]
}
}
}
再组合查询pool中的子查询中可以写精确查询比如,Term和Range
elasticsearch中的相关性打分算法是什么
function_score定义的三要素
elasticsearch支持对搜索结果排序,默认是根据相关度算分(_score)来排序,可以排序字段类型有,keyword类型、数值类型、地理坐标类型、日期类型等。
GET /indexName/_search
{
"query":{
"match_all":{}
},
"sort":{
"FIELD":"desc" //排序字段的方式desc降序和ASC升序
}
}
根据地理位置排序
GET /indexName/_search
{
"query":{
"match_all":{}
},
"sort":[
{
"_geo_distance":{
"FIELD":"维度lat","经度lon"
"order":"desc/asc",
"unit":"xxxkm"
}
}
]
}
案例:对酒店数据按照用户评价降序排序,评价相同按照价格升序排序
GET /hotel/_search
{
"query":{"match_all":{}},
"sort":[
{"score":"desc"},
{"price":asc}
]
}
案例:实现对酒店数据按照到你的位置坐标的距离升序排序
GET /index/_search
{
"query":{"match_all":{}},
"sort":[
{"_geo_distance":{
"order":"asc", //absc降序 asc升序
"unit":"km", //距离单位
"location":{"lat":33.1,"lon":121.5} // 字段,维度和经度
}}
]
}
elasticsearch默认情况下只返回top10的数据,而如果要查询更多的数据就需要修改分页参数了。
作为分布式第三方插件,ES会面临深度分页的问题,列如按price排序后,获取from=990,size=10的数据
ES的处理逻辑是
如果搜索页数过深,或者结果集(From+size)越大,对内存和CPU的消耗也越高,因此ES设定结果集查询的上限是10000条。
ES深度分页解决方案
from+size
after search:
scroll:
再搜索结果中把搜索关键字突出显示
//准备request
SearchRequest request=new SearchRequest("hotel");
//准备DSL语句
request.source().query(QueryBuilders.matchAllQuery());
//发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//解析结果
SearchHits searchHits = response.getHits();
//查询的总条数
TotalHits totalHits = searchHits.getTotalHits();
//查询的结果数组
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
//得到source
String json = hit.getSourceAsString();
System.out.println(json);
}
System.out.println(response);
}
准备searchRequest对象
准备Request.source(),相当于ES中内部的DSL语句
DSL语句:
GET /索引名/_search
{
"query":{}
}
JAVA代码
request.source().query(QueryBuilders.matchAll)
发送请求
SearchResponse response=client.search(request,RequestOptions.DEFAULT);
得到ES端发过来的JSON数据类似于
反馈的数据
took133
timed_outfalse
_shards
total1
successful1
skipped0
failed0
hits
total
max_score2.0645075
hits
0
_indexhotel
_type_doc
_id339952837
_score2.0645075
_source
address良乡西路7号
brand如家
business房山风景区
city北京
id339952837
location39.73167, 116.132482
name如家酒店(北京良乡西路店)
pichttps://m.tuniucdn.com/fb3/s1/2n9c/3Dpgf5RTTzrxpeN5y3RLnRVtxMEA_w200_h200_c1_t0.jpg
price159
score46
starName二钻
highlight
name
<em>如家</em>酒店(北京良乡西路店)
通过JAVA取出值
SearchHist searchHits=response.getHits();
for (SearchHit searchHit: searchHits){
//得到所有的数据 System.out.println(searchHit.getSourceAsString());
}
分为match和multi_match两种
Query | Syntax on JAVA | Syntax on ES |
---|---|---|
单字段查询 | queryBuilders.matchQuery(“FIeld”,“Text”) | “match”:{“FIELD”:“Text”} |
多字段查询 | queryBuilders.multiMatchQuery(“Text”,“FIeld1”,“Field2”) | “multi_match”:{“Text”,“FIELD1”,“FIELD2”} |
Ctrl+Alt+M 抽取JAVA中的代码
常见的精确查询是term和range
Query | Syntax on JAVA | Syntax on ES |
---|---|---|
Term | QueryBuilders.termQuery(“FIELD”,“Text”) | “term”:{“FIELD”:{“indexName”:“query condition”}} |
Range | QueryBuilders.RangeQuery(“FIELD”).gte(“xxx”).lte(“xxx”) | “range”:{“FIELD”:{“gte”:xxx,“lte”:xxx}} |
首先需要创建一个复合查询
BoolQuery boolQuery=QueryBuilders.boolQuery(); 这里涉及到嵌套写法,是再bool的基础语法上嵌套精确查询
Query | Syntax on JAVA | Sytax on ES |
---|---|---|
Must | boolQuery.must(QueryBuilders.termQuery(“Field”,“Text”)); | |
Filter | boolQuery.Filter(QueryBuilders.rangeQuery(“Field”).lte(xxx)) |
构建查询条件,只需要一个类QueryBuilders
搜索结果的排序和分页是于query同级的参数
//查询
request.source().query(QueryBuilders.matchAllQuery());
//分页
rquest.source().from(0).size(5);
//价格排序
request.source().sort("price",desc)
abscent descent
GET /indexName/_search
{
"query":{
"match_all":{}
},
"from":0,
"size":5,
"sort":[
{
"FIELD":"desc"
},
]
}
高亮API包括请求DSL构建和结果解析两部分。
request.source().highlighter(new HightBuilder()
.field("name")
//是否需要于查询字段匹配
.requireFieldMatch(false)
)
GET /hotel/_search
{
"query":{
"match":{
"all":"如家"
}
},
"highlight":{
"fields":{
"name":{
"require_field_match":"false"
}
}
}
}
完成了基于RestClient的语句实现的排序、分页和高亮
completed order、separate page、highlight based on the statement of RestClient implemented