1、面临问题
2、解决方案-ElasticSearch
2.1、简介
【elasticsearch】
:::tips elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容. :::
【ELK】
:::tips elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域:而elasticsearch是elastic stack的核心,负责存储、搜索、分析数据。 :::
【Lucene】
:::tips
elasticsearch底层是基于lucene来实现的。
Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发。
官网:https://lucene.apache.org/
:::
2.2、优缺点
【优点】
【缺点】
2.3、运行原理-倒排索引
【正向索引】
:::tips
正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程.
优点:
可以给多个字段创建索引
根据索引字段搜索、排序速度非常快
缺点:
根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。
:::
【倒排索引】
:::tips
倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程。
优点:
根据词条搜索、模糊搜索时,速度非常快
缺点:
只能给词条创建索引,而不是字段
无法根据字段做排序
:::
2.4、相关名词
【索引库】
:::tips 索引(Index),就是相同类型的文档的集合。类似于mysql中的表 :::
【mapping映射】
:::tips 映射(mapping),是索引中文档的字段约束信息,类似mysql表的结构约束。 :::
【文档】
:::tips 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 :::
【词条】
:::tips 词条(Term),对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。 :::
2.5、Kibana
:::tips 可视化工具,在浏览器输入DSL语句即可查看ES中的内容。 :::
3、使用步骤
3.1、Kibana浏览器操作-索引库&文档
【索引库操作】
【创建索引库】
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
}
}
}
}
【查看索引库】
【删除索引库】
【修改索引库(追加)】
PUT /索引库名/_mapping
{
"properties":{
"新字段":{
"type":"数据类型(integer)"
}
}
}
【文档操作】
【新增文档】
POST /索引库名/_doc/文档id(不给会随机生成id)
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
}
}
【查看文档】
:::tips GET /索引库名/_doc/文档id :::
【删除文档】
:::tips DELETE /索引库名/_doc/文档id :::
【修改文档】
【全量修改】
PUT /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2"
}
【增量修改】
POST /索引库名/_update/文档id
{
"doc": {
"字段名": "新的值"
}
}
3.2、java代码操作-索引库&文档
【导入依赖】
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
【与版本对应】
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
【初始化连接&关闭连接】
第一种模式:
private RestHighLevelClient client;
// 创建连接
@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.248.222:9200")
));
}
// 关闭连接
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
第二种模式:
/**
* 在单元测试方法执行前执行
*/
@BeforeEach
public void init(){
client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("192.168.200.129", 9200, "http")
)
);
}
/**
* 在单元测试类执行后执行
*/
@AfterEach
public void destory() throws IOException {
if (client!=null){
client.close();
}
}
【索引库操作】
【创建索引库】
// 创建索引库
@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);
}
【删除索引库】
@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);
}
【查看索引库是否存在】
// 查看索引库是否存在
@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);
}
【文档操作】
【添加文档数据】
// 添加文档数据
@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);
}
【查询文档数据】
@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);
}
【修改文档数据】
// 修改文档数据
@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);
}
【删除文档数据】
// 删除文档数据
@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);
}
【批量添加】
// 批量添加文档数据
@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);
}
3.3、Kibana浏览器-查询&结果处理
GET /索引库名/_search
{
"query": {
"match_all": {
}
}
}
【全文检索查询】
【多字段】
GET /索引库名/_search
{
"query": {
"multi_match": {
"query": "内容",
"fields": ["字段1", "字段2"]
}
}
}
【单字段】
GET /hotel/_search
{
"query": {
"match": {
"字段": "值"
}
}
}
【精确查询】
【term查询】
GET /索引库名/_search
{
"query": {
"term": {
"字段": {
"value": "值"
}
}
}
}
【range查询】
GET /索引库名/_search
{
"query": {
"range": {
"字段": {
"gte": 10, // 这里的gte代表大于等于,gt则代表大于
"lte": 20 // lte代表小于等于,lt则代表小于
}
}
}
}
【地理坐标查询】
GET /索引库名/_search
{
"query": {
"geo_bounding_box": {
"字段": {
"top_left": { // 左上点
"lat": 31.1,
"lon": 121.5
},
"bottom_right": { // 右下点
"lat": 30.9,
"lon": 121.7
}
}
}
}
}
【附近/距离查询】
GET /索引库名/_search
{
"query": {
"geo_distance": {
"distance": "15km", // 半径
"字段": "31.21,121.5" // 圆心
}
}
}
【复合查询】
【算分函数查询】
GET /hotel/_search
{
"query": {
"function_score": {
"query": {
// 原始查询条件 根据BM25算法进行算分
"match": {
"all": "外滩"
}
},
"functions": [
{
// 过滤条件 符合条件重新算分
"filter": {
"term": {
"brand": "如家"
}
},
//算分函数 weight是常量值
"weight": 10
}
],
//加权模式 默认是multiply
"boost_mode": "sum"
}
}
}
【布尔查询】
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
}
}
}
]
}
}
}
【排序】
【普通字段】
GET /索引库名/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"字段": "desc" // 排序字段、排序方式ASC、DESC
}
]
}
【地理坐标】
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance": {
"地理坐标相关字段": {
"lat": 31.21,
"lon": 121.5
},
"order":"asc",
//单位
"unit":"km"
}
}
]
}
【分页】
【基本分页】
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 0, // 分页开始的位置,默认为0
"size": 10, // 期望获取的文档总数
"sort": [
{"price": "asc"}
]
}
【深度分页】
【高亮】
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 0, // 分页开始的位置,默认为0
"size": 10, // 期望获取的文档总数
"sort": [
{"price": "asc"}
]
}
3.4.ES相关的RESTClient代码实现
@Test
public void search01() throws IOException {
//1.创建请求语义对象
SearchRequest request = new SearchRequest("hotel");
// QueryBuilders: 构建查询类型
//查询所有
request.source().query(QueryBuilders.matchAllQuery());
//条件查询
searchRequest.source().query(QueryBuilders.matchQuery("name","如家酒店"));
searchRequest.source().query(QueryBuilders.multiMatchQuery("如家酒店","name","brand"));
//精准查询
//term查询
searchRequest.source().query(QueryBuilders.termQuery("brand","如家"));
//range范围查询
searchRequest.source().query(QueryBuilders.rangeQuery("price").gte("100").lte("200"));
//布尔查询
// 2.1.准备BooleanQuery
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 2.2.添加term
boolQuery.must(QueryBuilders.termQuery("city", "杭州"));
// 2.3.添加range
boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));
request.source().query(boolQuery);
//排序、分页
// 2.2.排序 sort
request.source().sort("price", SortOrder.ASC);
// 2.3.分页 from、size
request.source().from((page - 1) * size).size(5);
//2.发送请求给ES
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//3.处理返回结果
handleResponse(response);
}
高亮显示
@Test
void testHighlight() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1.query
request.source().query(QueryBuilders.matchQuery("all", "如家"));
// 2.2.高亮
request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
1、controller
@Autowired
private IHotelService service;
/**
* 根据关键字查询
* @param requestParams
* @return
* @throws IOException
*/
@PostMapping("/list")
public PageResult searchAll(@RequestBody RequestParams requestParams) throws IOException {
return service.search(requestParams);
}
2、public interface IHotelService extends IService<Hotel> {
/**
* 根据关键字搜索酒店信息
* @param requestParams 请求参数对象,包含用户输入的关键字
* @return 酒店文档列表
*/
PageResult search(RequestParams requestParams) throws IOException;
}
3、 @Autowired
private RestHighLevelClient client;
/**
* 根据关键字搜索酒店信息
*
* @param requestParams 请求参数对象,包含用户输入的关键字
* @return 酒店文档列表
*/
@Override
public PageResult search(RequestParams requestParams) throws IOException {
//创建请求对象
SearchRequest request = new SearchRequest("hotel");
//根据条件进行查询
buildBasicQuery(request, requestParams);
//分页查询
request.source().from((requestParams.getPage() - 1) * requestParams.getSize()).size(requestParams.getSize());
//处理地址位置
if (requestParams.getLocation() != null && requestParams.getLocation().length() > 0) {
request.source().sort(SortBuilders.geoDistanceSort("location", new GeoPoint(requestParams.getLocation())).order(SortOrder.ASC)
.unit(DistanceUnit.KILOMETERS)
);
}
//发送请求到es
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//处理响应
return handlerResponse(response);
}
4、//响应结果处理
private PageResult handlerResponse(SearchResponse response) {
SearchHits hits = response.getHits();
//获取总条数
long count = hits.getTotalHits().value;
//获取数据
SearchHit[] hitsHits = hits.getHits();
//创建一个已经用来装HotelDoc
List<HotelDoc> docs = new ArrayList<>();
//遍历
for (SearchHit hit : hitsHits) {
String json = hit.getSourceAsString();
//反序列化
HotelDoc doc = JSON.parseObject(json, HotelDoc.class);
//获取距离
Object[] sortValue = hit.getSortValues();
for (Object val : sortValue) {
doc.setDistance(val);
}
docs.add(doc);
}
return new PageResult(count, docs);
}
5、查询过滤条件
//查询过滤条件
private void buildBasicQuery(SearchRequest request, RequestParams requestParams) {
// 1.构建BooleanQuery
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//判断关键字是否存在
if (requestParams.getKey() == null || requestParams.getKey() == "") {
boolQuery.must(QueryBuilders.matchAllQuery());
} else {
boolQuery.must(QueryBuilders.matchQuery("all", requestParams.getKey()));
}
//条件过滤
//城市过滤
if (requestParams.getCity() != null && requestParams.getCity().length() > 0) {
boolQuery.filter(QueryBuilders.termQuery("city", requestParams.getCity()));
}
//品牌过滤
if (requestParams.getBrand() != null && requestParams.getBrand().length() > 0) {
boolQuery.filter(QueryBuilders.termQuery("brand", requestParams.getBrand()));
}
//星级过滤
if (requestParams.getStarName() != null && requestParams.getStarName().length() > 0) {
boolQuery.filter(QueryBuilders.termQuery("starName", requestParams.getStarName()));
}
//价格过滤
if (requestParams.getMinPrice() != null && requestParams.getMaxPrice() != null) {
boolQuery.filter(QueryBuilders.rangeQuery("price").gte(requestParams.getMinPrice()).lte(requestParams.getMaxPrice()));
}
//添加算分函数控制权重
FunctionScoreQueryBuilder functionScoreQueryBuilder =
QueryBuilders.functionScoreQuery(
boolQuery, new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
//其中一个function score元素
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
//过滤条件
QueryBuilders.termQuery("isAD", true),
//算分函数
ScoreFunctionBuilders.weightFactorFunction(10)
)
}
);
request.source().query(functionScoreQueryBuilder);
}