1. ES支持的两种地理坐标存储形式
  • geo_point: 纬度和经度确定的一个点
  • geo_shape: 多个geo_point组成的一个复杂图形, 比如直线, 一般是一个区域
  1. 如果ES中同时需要根据名称, 位置, 商圈, 城市等信息进行多个字段的搜索, 又希望性能高一些, 可以使用copy_to
1
2
3
4
5
6
7
8
9
"all": {
"type": "text",
"anaylzer": "ik_max_word"
},
"brand": {
"type": "keyword",
"copy_to": "all"
}
// 这样就可以在一个字段`all`中查询到所有字段中

JavaRestClient

初始化

  1. 引入ESHighLevelRestClient
1
2
3
4
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
  1. 覆盖SpringBoot默认的7.6.2版本设置
1
2
3
4
<properties>
<java.version>1.8</java.version>
<elasticsearch-version>7.12.1</elasticsearch-version>
</properties>
  1. 初始化RestHighLevelClient
1
2
3
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://<ipaddress>:9200")
));

创建索引库

1
2
3
4
5
6
7
8
9
void testCreateIndex() throws IOException {
// 1. 创建Request对象
CreateIndexRequest req = new CreateIndexRequest("hotel");
// 2. 请求参数
req.source(MAPPING_TEMPLATE, XContentType.JSON);
// 3. 发起请求
client.indices().create(req, RequestOptions.DEFAULT);
}
// indices()返回的对象中包含了索引库操作中的所有方法

添加数据到索引库

1
2
3
4
5
6
7
8
void IndexDocument() throws IOException {
// 1. 创建Request对象
IndexRequest req = new IndexRequest("idxName").id("1");
// 2. 准备JSON文档
req.source(MAPPING_TEMPLATE, XContentType.JSON);
// 3. 发送请求
client.index(req, RequestOptions.DEFAULT);
}

查询文档

1
2
3
4
5
6
7
8
9
void GetDocumentById() throws IOException {
// 1. 创建Request对象
GetRequest req = new GetRequest("idxName", "1");
// 2. 发送请求
GetResponse resp = client.get(req, RequestOptions.DEFAULT);
// 3. 解析结果
String json = resp.getSourceAsString();
}

根据ID修改数据

  1. 全量更新, 直接删除再重新插入数据
  2. 局部更新
1
2
3
4
5
6
7
8
9
10
11
void UpdateDocumentById() throws IOException {
// 1. 创建Request对象
UpdateRequest req = new UpdateRequest("idxName", "1");
// 2. 准备参数, 每两个参数为一对k-v
req.doc(
"age", 18,
"name", "rose"
);
// 3. 更新文档
client.update(req, RequestOptions.DEFAULT);
}

批量导入数据

  1. 利用mybatis-plus查询数据
  2. 将查询到的数据转换为文档数据
  3. 利用JavaRestClient中的Bulk批处理, 实现批量新增文档
1
2
3
4
5
6
7
8
9
void testBulk() throws IOException {
// 1. 创建Bulk请求
BulkRequest req = new BulkRequest();
// 2. 添加需要批量提交的请求, 比如添加两个新增文档的操作
req.add(new IndexRequest("hotel").id("101").source("json source1", XContentType.JSON));
req.add(new IndexRequest("hotel").id("102").source("json source2", XContentType.JSON));
// 3. 发起bulk请求
client.bulk(req, RequestOptions.DEFAULT);
}

DSL查询语法

分类

  1. 查询所有: 有分页, 一次20条, match_all
  2. 全文检索: 利用分词器对内容分词后使用倒排索引匹配, match_query, multi_match_query
  3. 精确查询: 根据精确词条查询, 一般是keyword, 数值, 日期, boolean, 不分词, ids, range, term
  4. 地理查询: 根据经纬度查询, geo_distance, geo_bounding_box
  5. 复合查询: 将上述查询组合查询, bool, function_score

语法

1
2
3
4
5
6
7
8
GET /idxName/_search
{
"query": {
<查询类型>: {
<查询条件>: <查询值>
}
}
}

全文检索

对用户输入内容分词以后用于搜索框检索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET /idxName/_search
{
"query": {
"match": {
"all": "xxxx"
}
}
}

GET /idxName/_search
{
"query": {
"multi_match":{
"query": "xxx",
"fields": ["1", "2"]
}
}
}
// 搜索字段越多, 搜索效率越低, 建议使用`copy_to`将多个字段拷贝到一个字段中

精确查询

对不可分词的条件查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
GET /idxName/_search
{
"query": {
"term": {
"city": {
"value": "xxx"
}
}
}
}

GET /idxName/_search
{
"query": {
"range": {
"price": {
"gte": 100,
"lte": 300
}
}
}
}

地理查询

查询附近的酒店, 打车, 附近的人

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 查询某个geo_point落在矩形框中的文档
GET /idxName/_search
{
"query": {
"geo_bounding_box": {
"FIELD": {
"top_left": {
"lat": 33.1,
"lon": 121.5
},
"bottom_right": {
"lat": 30.9,
"lon": 121.7
}
}
}
}
}
// 到指定中心点小于某个距离的文档
GET /idxName/_search
{
"query": {
"geo_distance": {
"distance": "10km",
"FIELD": "31.21, 121.5"
}
}
}

复合查询

  • 在简单查询基础上进行功能叠加, function_score算分函数查询, 给相关词条打分, 返回值按照倒序排列
  • TF(词条频率)=词条出现频率文档中词条总数TF(\text{词条频率}) = \frac{\text{词条出现频率}}{\text{文档中词条总数}}
  • 如果只使用TF算法, 某一个词条可能会在所有文档中都出现, 那计算这个词条的数量就没有意义, 所以产生TF-IDF算法
  • IDF(逆文档频率)=log(文档总数包含词条的文档数)IDF(\text{逆文档频率}) = log(\frac{\text{文档总数}}{\text{包含词条的文档数}})
  • score=in(TF(词条频率)IDF(逆文档频率))score = \sum_i^n(TF(\text{词条频率}) * IDF(\text{逆文档频率}))
  • 但是从ES5.1开始就没有使用这种TF-IDF算法了, 使用BM25算法
  • Score(Q,d)=inlog(1+Nn+0.5n+0.5)fifi+k1(1b+bdlavg(dl))Score(Q, d) = \sum_i^nlog(1 + \frac{N - n + 0.5}{n + 0.5}) \cdot \frac{f_i}{f_i + k_1 \cdot (1 - b + b \cdot \frac{d_l}{avg(d_l)})}
  • 因为传统算法词频越高, TF-IDF算法得分越高, BM25算法最终会趋于水平
    TF-BM25
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
GET /idxName/_search
{
"query": {
"function_score": {
"query": {
"mathch": {
"all": "xxx"
}
},
"functions": [
{
"filter": {
"term": {
"id": "1"
}
},
"weight": 10
}
],
"boost_mode": "multiply"
}
}
}
// 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 布尔查询是一个或多个子句组合
// must: 必须匹配, 与
// should: 选择性匹配, 或
// must_not: 必须不匹配, 非
// filter: 必须匹配, 不参与算分
// 如果子查询比较多, 每个都算分, filter会放到缓存中, 也不会算分, 所以性能更好
GET /idxName/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"city": "上海"
}
}
],
"should": [
{
"term": {
"brand": "皇冠假日"
}
},
{
"term": {
"brand": "华美达"
}
}
],
"must_not": [
{
"range": {
"price": {
"lte": 500
}
}
}
],
"filter": [
{
"range": {
"score": {
"gte": 45
}
}
}
]
}
}
}