1.提出问题
1.mysql对于数据量大的时候查询执行效率低 elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容
2.专业名词解释
Mysql:分类: 关系型数据库概述: mysql在存储数据时,数据和数据之间有一定的关联关系存储介质: 存放在硬盘上优点: 不会导致数据丢失缺点: 执行效率低硬盘 ---> 内存 ---> CPU事务控制redis:分类: 非关系型数据库概述: 数据在存储时,数据和数据之间没有关联关系存储介质: 内存优点: 执行效率高缺点: 可能会导致数据丢失ElasticSearch: 灵活的搜索分类: 非关系型数据库概述: ES专门用于对海量数据的搜索存储介质: 内存优点: 搜索效率高缺点:数据量太少效率不高,维护成本高
正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程.
- 优点:
- 可以给多个字段创建索引
- 根据索引字段搜索、排序速度非常快
- 缺点:
- 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。

倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程。
- 优点:
- 根据词条搜索、模糊搜索时,速度非常快
- 缺点:
- 只能给词条创建索引,而不是字段
- 无法根据字段做排序
- 文档(Document):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息
- 词条(Term):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条

1.文档和字段:
1.elasticsearch是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中
2.Json文档中往往包含很多的字段(Field),类似于数据库中的列。
2.索引和映射:
1.索引(Index),就是相同类型的文档的集合。
2.映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。
- Mysql:擅长事务类型操作,可以确保数据的安全和一致性
- Elasticsearch:擅长海量数据的搜索、分析、计算
3.elasticsearch
1.Elasticsearch概述
:::info
elasticsearch是一款基于lucene的倒排索引的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容.
:::
2.ELK技术栈
:::info
elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)
:::
4.安装es、kibana(可视化界面)、分词器
1.IK分词器
:::info
- ik_smart:智能切分,粗粒度
-
5.Kibana浏览器-索引库(类似数据库表,mapping映射就类似表的结构)&文档(每一条数据)
1.mapping映射属性
:::info mapping是对索引库中文档的约束,常见的mapping属性包括:
type:字段数据类型,常见的简单类型有:
- 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
- 数值:long、integer、short、byte、double、float、
- 布尔:boolean
- 日期:date
- 对象:object
- index:是否创建索引,默认为true
- analyzer:使用哪种分词器
- properties:该字段的子字段
:::
2.索引库的CRUD
1.创建索引库和映射
PUT /索引库名称 { "mappings": { "properties": { "字段名":{ "type": "text",//数据类型 "analyzer": "ik_smart"//分词器 }, "字段名2":{ "type": "keyword", "index": "false" }, "字段名3":{ "properties": { "子字段": { "type": "keyword" } } } } } }2.查询索引库
GET /索引库名3.修改索引库(一旦创建,无法修改mapping,只能追加字段)
PUT /索引库名/_mapping { "properties":{ "新字段":{ "type":"数据类型(integer)" } } }4.删除索引库
DELETE /索引库名3.文档的CRUD
1.新增文档
POST /索引库名/_doc/文档id(不给会随机生成id) { "字段1": "值1", "字段2": "值2", "字段3": { "子属性1": "值3", "子属性2": "值4" } }2.查询文档
GET /索引库名/_doc/文档id3.删除文档
DELETE /索引库名/_doc/文档id4.修改文档
1.全量修改:直接覆盖原来的文档
PUT /索引库名/_doc/文档id { "字段1": "值1", "字段2": "值2" }2.增量修改:修改文档中的部分字段
POST /索引库名/_update/文档id { "doc": { "字段名": "新的值" } }6.JAVA代码(RestClient)-索引库&文档
1.导入es的RestHighLevelClient依赖
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </dependency>2.修改版本对应es
<properties> <java.version>1.8</java.version> <elasticsearch.version>7.12.1</elasticsearch.version> </properties>3.初始化连接(业务中需要创建bean对象)&关闭连接
```java private RestHighLevelClient client;
// 创建连接 @BeforeEach void setUp() { this.client = new RestHighLevelClient(RestClient.builder( HttpHost.create(“http://192.168.248.222:9200“)
/*RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("localhost", 9200, "http"),
new HttpHost("localhost", 9201, "http")));*/
));
}
// 关闭连接 @AfterEach void tearDown() throws IOException { this.client.close(); }
<a name="N7vxo"></a>
## 4.官网API
[官网api](https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.html)<br />
<a name="bOMXQ"></a>
## 5.索引库操作CRUD
<a name="mUlJP"></a>
### 1.创建索引库
```java
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
// 创建索引库
@Test
public void createIndex() throws IOException {
// 1.创建请求语义对象
CreateIndexRequest request = new CreateIndexRequest("hotel");
// 添加 source
request.source(HotelConstants.MAPPING_TEMPLATE, XContentType.JSON);
// 2.发送请求
CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
// 3.解析响应
boolean flag = response.isAcknowledged();
// 打印结果
System.out.println(flag);
}
2.删除索引库
// 删除索引库
@Test
public void deleteIndex() throws IOException {
// 1.创建请求语义对象
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
// 2.发送请求
AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
// 3.解析响应
boolean flag = response.isAcknowledged();
// 打印结果
System.out.println(flag);
}
3.查看索引库是否存在
// 查看索引库是否存在
@Test
public void existsIndex() throws IOException {
// 1.创建请求语义对象
GetIndexRequest request = new GetIndexRequest("hotel");
// 2.发送请求
boolean flag = client.indices().exists(request, RequestOptions.DEFAULT);
// 3.解析响应
// 打印结果
System.out.println(flag);
}
6.文档操作CRUD
1.添加文档数据
@Autowired
private IHotelService hotelService;
// 添加文档数据
@Test
public void add() throws IOException {
// 1.根据id获取数据
Hotel hotel = hotelService.getById(36934L);
// 2.转换成 ES 对应的数据
HotelDoc hotelDoc = new HotelDoc(hotel);
// 3.转换成JSON
String hotelDocJson = JSON.toJSONString(hotelDoc);
// 4.创建请求语义对象
IndexRequest request = new IndexRequest("hotel");
request.id(hotel.getId() + "");
request.source(hotelDocJson, XContentType.JSON);
// 5.发送请求
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
// 6.解析响应结果
DocWriteResponse.Result result = response.getResult();
System.out.println(result);
}
2.查询文档数据
// 查询文档数据
@Test
public void select() throws IOException {
// 1.创建请求语义对象
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);
System.out.println(hotelDoc);
}
3.修改文档数据
// 修改文档数据
@Test
public void update() throws IOException {
// 1.创建请求语义对象
UpdateRequest request = new UpdateRequest("hotel","36934");
request.doc(
"name", "7天连锁酒店(上海宝山路地铁站店)"
);
// 2.发送请求
UpdateResponse response = client.update(request, RequestOptions.DEFAULT);
// 3.解析响应结果
DocWriteResponse.Result result = response.getResult();
System.out.println(result);
}
4.删除文档数据
// 删除文档数据
@Test
public void delete() throws IOException {
// 1.创建请求语义对象
DeleteRequest request = new DeleteRequest("hotel","36934");
// 2.发送请求
DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
// 3.解析响应结果
DocWriteResponse.Result result = response.getResult();
System.out.println(result);
}
5.批量添加文档数据
@Autowired
private IHotelService hotelService;
// 批量添加文档数据
@Test
public void add() throws IOException {
// 1.批量查询酒店数据
List<Hotel> hotels = hotelService.list();
// 2.创建请求语义对象
BulkRequest request = new BulkRequest();
for (Hotel hotel : hotels) {
// 转换格式
HotelDoc hotelDoc = new HotelDoc(hotel);
String hotelDocJson = JSON.toJSONString(hotelDoc);
IndexRequest indexRequest = new IndexRequest("hotel");
indexRequest.id(hotel.getId() + "");
indexRequest.source(hotelDocJson, XContentType.JSON);
request.add(indexRequest);
}
// 5.发送请求
BulkResponse response = client.bulk(request, RequestOptions.DEFAULT);
// 6.解析响应结果
RestStatus status = response.status();
System.out.println(status);
}
7.Kibana浏览器-查询&结果处理
1.查询所有
GET /索引库名/_search
{
"query": {
"match_all": {
}
}
}
2.全文检索查询
1.multi_match:根据多个字段查询,参与查询字段越多,查询性能越差
GET /索引库名/_search
{
"query": {
"multi_match": {
"query": "内容",
"fields": ["字段1", "字段2"]
}
}
}
2.match:根据一个字段查询
GET /hotel/_search
{
"query": {
"match": {
"字段": "值"
}
}
}
3.精确查询
1.term:根据词条精确值查询
GET /索引库名/_search
{
"query": {
"term": {
"字段": {
"value": "值"
}
}
}
}
2.range:根据值的范围查询
GET /索引库名/_search
{
"query": {
"range": {
"字段": {
"gte": 10, // 这里的gte代表大于等于,gt则代表大于
"lte": 20 // lte代表小于等于,lt则代表小于
}
}
}
}
4.地理坐标查询
1.矩形范围查询
GET /索引库名/_search
{
"query": {
"geo_bounding_box": {
"字段": {
"top_left": { // 左上点
"lat": 31.1,
"lon": 121.5
},
"bottom_right": { // 右下点
"lat": 30.9,
"lon": 121.7
}
}
}
}
}
2.附近/圆形距离查询
GET /索引库名/_search
{
"query": {
"geo_distance": {
"distance": "15km", // 半径
"字段": "31.21,121.5" // 圆心
}
}
}
5.复合查询
1.fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名
:::info function score 查询中包含四部分内容:
- 原始查询条件:query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)
- 过滤条件:filter部分,符合该条件的文档才会重新算分
- 算分函数:符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
- weight:函数结果是常量
- field_value_factor:以文档中的某个字段值作为函数结果
- random_score:以随机数作为函数结果
- script_score:自定义算分函数算法
- 运算模式:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:
- multiply:相乘
- replace:用function score替换query score
- 其它,例如:sum、avg、max、min ::: ```yaml
- 过滤条件:决定哪些文档的算分被修改
- 算分函数:决定函数算分的算法
- 运算模式:决定最终算分结果
GET /hotel/_search { “query”: { “function_score”: { “query”: { // 原始查询条件 根据BM25算法进行算分 “match”: { “all”: “外滩” } }, “functions”: [ { // 过滤条件 符合条件重新算分 “filter”: { “term”: { “brand”: “如家” } }, //算分函数 weight是常量值 “weight”: 10 } ], //加权模式 默认是multiply(乘) 运算方法 “boost_mode”: “sum” } } }
<a name="rGYkJ"></a>
### 相关性算分

<a name="VMC6b"></a>
### 2.bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索(参与**打分的字段越多,查询的性能也越差**)
:::info
bool查询逻辑
- must:必须匹配的条件,可以理解为“与”****
- should:选择性匹配的条件,可以理解为“或”
- must_not:必须不匹配的条件,不参与打分
- filter:必须匹配的条件,不参与打分 ***
:::
```yaml
GET /hotel/_search
{
"query": {
"bool": {
//必须匹配
"must": [
{
"match": {
"all": "上海"
}
}
],
//选择匹配
"should": [
{
"term": {
"brand": {
"value": "皇冠假日"
}
}
},
{
"term": {
"brand": "华美达"
}
}
],
//必须不匹配 不参与算分
"must_not": [
{
"range": {
"price": {
"lte": 400
}
}
}
],
//必须匹配 不参与算分
"filter": [
{
"geo_distance": {
"distance": "10km",
"location": {
"lat": 31.21,
"lon": 121.5
}
}
}
]
}
}
}
6.排序(不算分)
1.普通字段
GET /索引库名/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"字段": "desc" // 排序字段、排序方式ASC、DESC
}
]
}
2.地理坐标
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance": {
"地理坐标相关字段": {
"lat": 31.21,
"lon": 121.5
},
"order":"asc",
//单位
"unit":"km"
}
}
]
}
7.分页
:::info
- from:从第几个文档开始
- size:总共查询几个文档
:::
1.基本分页
GET /hotel/_search { "query": { "match_all": {} }, "from": 0, // 分页开始的位置,默认为0 "size": 10, // 期望获取的文档总数 "sort": [ {"price": "asc"} ] }2.深度分页
:::info 当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力,因此elasticsearch会禁止from+ size 超过10000的请求。
针对深度分页,ES提供了两种解决方案,官方文档:
- search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
- scroll:原理将排序后的文档id形成快照,保存在内存。官方已经不推荐使用。
分页查询的常见实现方案以及优缺点:
- from + size:
- 优点:支持随机翻页
- 缺点:深度分页问题,默认查询上限(from + size)是10000
- 场景:百度、京东、谷歌、淘宝这样的随机翻页搜索
深度分页:
after search: 使用search_after必须要设置from=0。
- 优点:没有查询上限(单次查询的size不超过10000)
- 缺点:只能向后逐页查询,不支持随机翻页
- 场景:没有随机翻页需求的搜索,例如手机向下滚动翻页
scroll:
1)给文档中的所有关键字都添加一个标签,例如标签
2)页面给标签编写CSS样式 :::
GET /hotel/_search { "query": { "match": { "FIELD": "TEXT" // 查询条件,高亮一定要使用全文检索查询 } }, "highlight": { "fields": { // 指定要高亮的字段 "FIELD": { "pre_tags": "<em>", // 用来标记高亮字段的前置标签 "post_tags": "</em>", // 用来标记高亮字段的后置标签 "require_field_match": "false" } } } }8.java代码-查询&结果处理
1.查询所有
// 查询所有--match all @Test public void matchAll() throws IOException { // 1.创建 SearchRequest 对象 SearchRequest request = new SearchRequest("hotel"); // 2.组织 DSL 参数 request.source() .query(QueryBuilders.matchAllQuery()); // 3.发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 4.解析响应结果 handleResponse(response); }2.全文检索查询
// 全文检索查询--match @Test public void match() throws IOException { // 1.创建 SearchRequest 对象 SearchRequest request = new SearchRequest("hotel"); //分词查询一个字段 request.source().query(QueryBuilders.matchQuery("name","如家")); //分词查询多个字段 request.source().query(QueryBuilders.multiMatchQuery("如家酒店","name",brand)); // 3.发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 4.解析响应结果 handleResponse(response); }3.精确查询
1.term查询
// 精确查询--term @Test public void term() throws IOException { // 1.创建 SearchRequest 对象 SearchRequest request = new SearchRequest("hotel"); // 2.组织 DSL 参数 request.source() .query(QueryBuilders.termQuery("city","杭州")); // 3.发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 4.解析响应结果 handleResponse(response); }2.range查询
// 范围查询--range @Test public void range() throws IOException { // 1.创建 SearchRequest 对象 SearchRequest request = new SearchRequest("hotel"); // 2.组织 DSL 参数 request.source() .query(QueryBuilders.rangeQuery("price").gte(500).lte(1000)); // 3.发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 4.解析响应结果 handleResponse(response); }4.复合查询
1.布尔查询
// 布尔查询--bool @Test public void bool() throws IOException { // 1.创建 SearchRequest 对象 SearchRequest request = new SearchRequest("hotel"); // 2.组织 bool 查询参数 BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); //必须满足的条件 boolQuery.must(QueryBuilders.termQuery("city", "杭州")); //过滤条件 boolQuery.filter(QueryBuilders.rangeQuery("price").lt(1000)); // 3.组织 DSL 参数 request.source() .query(boolQuery); // 4.发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 5.解析响应结果 handleResponse(response); }5.排序分页
// 排序、分页--sort from size @Test public void sortAndPage() throws IOException { // 1.创建 SearchRequest 对象 SearchRequest request = new SearchRequest("hotel"); // 2.组织 DSL 参数 // 查询所有 request.source().query(QueryBuilders.matchAllQuery()); // 前 20条 request.source().from(0).size(20); // 按价格升序 request.source().sort("price", SortOrder.ASC); // 3.发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 4.解析响应结果 handleResponse(response); }6.高亮
// 高亮--highlights @Test public void highlights() throws IOException { // 1.创建 SearchRequest 对象 SearchRequest request = new SearchRequest("hotel"); // 2.组织 DSL 参数 // 查询所有带有如家 request.source().query(QueryBuilders.matchQuery("all","如家")); // 高亮处理 request.source().highlighter(new HighlightBuilder() .field("name") .requireFieldMatch(false)); // 3.发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 4.解析响应结果 -- 高亮特殊处理 handleResponse(response); }解析
7.周边位置排序
```java public PageResult search(RequestParams params) { try {
// 1.准备Request SearchRequest request = new SearchRequest("hotel"); // 2.准备DSL // 2.1.query buildBasicQuery(params, request); // 2.2.分页 int page = params.getPage(); int size = params.getSize(); request.source().from((page - 1) * size).size(size);
// 获取前端发送的地理位置
String location = params.getLocation();
//地理位置位置排序
if (location != null && !location.equals("")) {
request.source().sort(SortBuilders
.geoDistanceSort("location", new GeoPoint(location))
.order(SortOrder.ASC)
.unit(DistanceUnit.KILOMETERS)
);
}
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
return handleResponse(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
<a name="zPAVL"></a>
### 解析距离

<a name="iYOep"></a>
## 8.响应结果解析
```java
// 响应结果解析
private xxxx handleResponse(SearchResponse response) {
// 4.解析响应结果
SearchHits searchHits = response.getHits();
// 查询总条数
long total = searchHits.getTotalHits().value;
System.out.println("查询总条数:" + total);
// 查询结果数组
SearchHit[] hits = searchHits.getHits();
// 遍历
for (SearchHit hit : hits) {
// 得到json
String hotelDocJson = hit.getSourceAsString();
// 转换成Java对象
HotelDoc hotelDoc = JSON.parseObject(hotelDocJson, HotelDoc.class);
// 高亮特殊处理
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (!CollectionUtils.isEmpty(highlightFields)) {
// 高亮字段结果
HighlightField highlightField = highlightFields.get("name");
if (highlightField!=null) {
// 不为空时的第一个就是酒店名称
String name = highlightField.getFragments()[0].string();
hotelDoc.setName(name);
}
//获取距离排序值
// 得到json
Object[] sortValues = hit.getSortValues();
if(SortValues!=null && SortValues.length>0){
Object[] sortValues = sortValues[0];
hotelDoc.setDistance(sortValues);
}
}
// 打印结果
//System.out.println(hotelDoc);
//返回结果
return xxxx;
}
}
