一、基础概念
1、基础了解
Elasticsearch是基于语言Lucene语言的一款强大的分布式搜索引擎。elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK技术栈),通常用于数据的搜索、监控、日志和日志等作用。
es和Mysql是有所区别的,Mysql作为关系型数据库,用于存储数据,因为其事务保证数据一致性,而ES则是用于搜索数据,没有事务。
2、es相关概念
倒排索引:所谓倒排索引是相对于正向索引来说的,举例而言,在下表中,是常规的正向索引,通过foodId查询到对应的一条数据。当查询name="芋泥奶茶"时,需要全表查询,遍历到第三条数据,如果数据量大,显然是不可取的。而倒排索引,则是根据文本查询数据,这样查询"芋泥奶茶",查询到的就是"芋泥:3,4"和"奶茶:3".
foodId | name | price |
---|---|---|
1 | 巧克力牛奶 | 5 |
2 | 牛奶麻薯 | 25 |
3 | 芋泥奶茶 | 12 |
4 | 芋泥千层 | 30 |
term | 数据id |
---|---|
巧克力 | 1 |
牛奶 | 1,2 |
芋泥 | 3,4 |
奶茶 | 3 |
文档:文档是Json的数据,比如下面面的数据,那么文档就是下图所示:
json
{
"foodId": 1,
"name": "巧克力牛奶",
"price": 5
}
字段:类似于数据库中的列,在上文中就是foodid这样。
索引:相同类型的文档集合,如下所示,就是一个索引.eg:所有食物文档,就可以组织在一起,称为食物的索引;所有人员的文档,可以组织在一起,称为人员的索引;
json
{
"foodId": 1,
"name": "巧克力牛奶",
"price": 5
}
{
"foodId": 2,
"name": "巧克力蛋糕",
"price": 30
}
{
"foodId": 3,
"name": "芋泥奶茶",
"price": 12
}
json
{
"nameId": 1,
"name": "张三",
"gender": "女",
"age": 15
}
{
"nameId": 2,
"name": "李四",
"gender": "女",
"age": 17
}
{
"nameId": 3,
"name": "王五",
"gender": "男",
"age": 15
}
映射:索引中文档字段的约束信息。
分词器:根据设置的分词器不同,分词的规则也不同,ik_smart:智能切分,粗粒度;ik_max_word:最细切分,细粒度。text字段必须使用分词器。
二、Es的基本操作
1、索引
创建索引:PUT/索引库名
json
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart" //中文分析器
},
"字段名2":{
"type": "keyword",
"index": "false" //不会被索引
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
},
// ...略
}
}
}
查询索引:GET/索引库名
删除索引:DELETE/索引库名
json
{
"acknowledged" : true
}
添加字段:PUT/索引库名/_mapping。不难发现索引一旦创建,就不允许修改了,因为索引库一旦创建,无法修改mapping。但是可以添加新的Mapping,不会对原来的索引造成影响。
bash
PUT /索引库名/_mapping
{
"properties": {
"新字段名":{
"type": "integer"
}
}
}
2、文档操作
新增文档:POST/索引库名/_doc/文档id { json文档 }
bash
POST /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
},
// ...
}
获取文档:GET/索引库名/_doc/文档id
bash
GET /heima/_doc/1
删除文档:DELETE/索引库名/_doc/文档id
bash
# 根据id删除数据
DELETE /heima/_doc/1
修改文档:有两种方式,全量修改或者增量修改
-
- 全量修改:本质是删除,原本的id,然后更新新的内容,PUT/索引库名/_doc/文档id {json文档}
- 增量修改:在原本的基础上,进行增加,POST/索引库名/_update/文档id {"doc":{字段}}
bash
PUT /{索引库名}/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
// ... 略
}
bash
POST /{索引库名}/_update/文档id
{
"doc": {
"字段名": "新的值",
}
}
3、查询
常见的查询类型包括:
- 查询所有:查询出所有数据,一般测试用。例如:match_all
- 全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
-
- match_query
- multi_match_query
- 精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如:
-
- ids
- range
- term
- 地理(geo)查询:根据经纬度查询。例如:
-
- geo_distance
- geo_bounding_box
- 复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:
-
- bool
- function_score
(1)、查询语法
虽然查询类型颇多,但是查询语句是一样的~,无非就是类型、条件、条件值的变化。
bash
GET /indexName/_search
{
"query": {
"查询类型": {
"查询条件": "条件值"
}
}
}
(2)、全文检索
常见的全文检索查询包括:
- match查询:单字段查询
- multi_match查询:多字段查询,任意一个字段符合条件就算符合查询条件
match查询语法如下:
bash
GET /indexName/_search
{
"query": {
"match": {
"FIELD": "TEXT"
}
}
}
mulit_match语法如下:
bash
GET /indexName/_search
{
"query": {
"multi_match": {
"query": "TEXT",
"fields": ["FIELD1", " FIELD12"]
}
}
}
(3)、精确检索
精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的有:
- term:根据词条精确值查询
- range:根据值的范围查询
term:因为精确查询的字段搜是不分词的字段,因此查询的条件也必须是不分词的词条。查询时,用户输入的内容跟自动值完全匹配时才认为符合条件。如果用户输入的内容过多,反而搜索不到数据。
bash
// term查询
GET /indexName/_search
{
"query": {
"term": {
"FIELD": {
"value": "VALUE"
}
}
}
}
range:范围查询,一般应用在对数值类型做范围过滤的时候。比如做价格范围过滤。
json
// range查询
GET /indexName/_search
{
"query": {
"range": {
"FIELD": {
"gte": 10, // 这里的gte代表大于等于,gt则代表大于
"lte": 20 // lte代表小于等于,lt则代表小于
}
}
}
}
(4)、地理坐标检索
所谓的地理坐标查询,其实就是根据经纬度查询,有以下常见的两种:
geo_bounding_box:矩形范围查询,也就是geo_bounding_box查询,查询坐标落在某个矩形范围的所有文档:查询时,需要指定矩形的左上 、右下两个点的坐标,然后画出一个矩形,落在该矩形内的都是符合条件的点。
语法如下:
json
// geo_bounding_box查询
GET /indexName/_search
{
"query": {
"geo_bounding_box": {
"FIELD": {
"top_left": { // 左上点
"lat": 31.1,
"lon": 121.5
},
"bottom_right": { // 右下点
"lat": 30.9,
"lon": 121.7
}
}
}
}
}
geo_distance:附近查询,也叫做距离查询(geo_distance):查询到指定中心点小于某个距离值的所有文档。也就是,在地图上找一个点作为圆心,以指定距离为半径,画一个圆,落在圆内的坐标都算符合条件:
json
// geo_distance 查询
GET /indexName/_search
{
"query": {
"geo_distance": {
"distance": "15km", // 半径
"FIELD": "31.21,121.5" // 圆心
}
}
}
(5)、复合检索
复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。常见的有两种:
- fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名;
- bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索,在使用es的时候非常常见。
当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。之前是TF-IDF算法,elasticsearch5.1版本后采用的BM25算法,TF-IDF算法有一各缺陷,就是词条频率越高,文档得分也会越高,单个词条对文档影响较大。而BM25则会让单个词条的算分有一个上限,曲线更加平滑。
(a)、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
function score的运行流程如下:
- 1)根据原始条件 查询搜索文档,并且计算相关性算分,称为原始算分(query score)
- 2)根据过滤条件,过滤文档
- 3)符合过滤条件 的文档,基于算分函数 运算,得到函数算分(function score)
- 4)将原始算分 (query score)和函数算分 (function score)基于运算模式做运算,得到最终结果,作为相关性算分。
json
GET /索引名/_search
{
"query": {
"function_score": { // 原始查询,可以是任意条件
"query": {
"match": {
"查询条件": "查询值"
}
},
"functions": [
{
"filter": {
"term": {
"查询条件": "查询值"
}
},
"weight": 10 // 算分权重为2
}
],
"boost_mode": "sum" // 加权模式,求和
}
}
}
(b)、bool query
布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询。子查询的组合方式有:
- must:必须匹配每个子查询,类似"与"
- should:选择性匹配子查询,类似"或"
- must_not:必须不匹配,不参与算分,类似"非"
- filter:必须匹配,不参与算分
搜索时,参与打分的字段越多,查询的性能也越差。因此这种多条件查询时,建议这样做:搜索框的关键字搜索,是全文检索查询,使用must查询,参与算分;其它过滤条件,采用filter查询。不参与算分。
bash
GET /索引名/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"查询条件": "查询值"
}
}
],
"should": [
{
"term": {
"查询条件": "查询值"
}
},
{
"term": {
"查询条件": "查询值"
}
}
],
"filter": [
{
"range": {
"price": {
"gte": 100,
"lte": 2000
}
}
}
]
}
}
}
三、Java客户端操作ES
以下是demo的数据库建表语句,使用es,建立索引很重要,如何完成es索引建立,需要考虑清楚,这个字段是什么,字段类型是什么,是否需要参与查询,是否需要分词,分词又该如何分词。
less
CREATE TABLE `tb_hotel` (
`id` bigint(20) NOT NULL COMMENT '酒店id',
`name` varchar(255) NOT NULL COMMENT '酒店名称;例:7天酒店',
`address` varchar(255) NOT NULL COMMENT '酒店地址;例:航头路',
`price` int(10) NOT NULL COMMENT '酒店价格;例:329',
`score` int(2) NOT NULL COMMENT '酒店评分;例:45,就是4.5分',
`brand` varchar(32) NOT NULL COMMENT '酒店品牌;例:如家',
`city` varchar(32) NOT NULL COMMENT '所在城市;例:上海',
`star_name` varchar(16) DEFAULT NULL COMMENT '酒店星级,从低到高分别是:1星到5星,1钻到5钻',
`business` varchar(255) DEFAULT NULL COMMENT '商圈;例:虹桥',
`latitude` varchar(32) NOT NULL COMMENT '纬度;例:31.2497',
`longitude` varchar(32) NOT NULL COMMENT '经度;例:120.3925',
`pic` varchar(255) DEFAULT NULL COMMENT '酒店图片;例:/img/1.jpg',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这是根据以上问题建立的索引,其中copy_to是将多个字段组合进行查询,有copy to的字段会被复制到all中,通过all进行查询。
json
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",
"copy_to": "all"
},
"starName":{
"type": "keyword"
},
"business":{
"type": "keyword"
},
"location":{
"type": "geo_point" //地理坐标,包括包括精度和维度。
},
"pic":{
"type": "keyword",
"index": false
},
"all":{
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
1、对索引进行操作
- 首先注册服务,初始化RestHighLevelClient;
- 通过xxxindexrequest,进行操作,xxx可以是Creat,Delete,GET,
- 准备DSL,只用在创建的时候需要;
- 然后通过RestHighLevelClient.indeces.xxx(),进行对应的操作,xxx是creat、exist、delect。
java
/**
* 创建索引
* @throws IOException
*/
@Test
void createHotelIndex() throws IOException {
// 1. Create Request object
CreateIndexRequest request = new CreateIndexRequest("hotel");
// 2. Prepare request parameters: DSL statement
request.source(MAPPING_TEMPLATE, XContentType.JSON);
// 3. Send request
highClient.indices().create(request,RequestOptions.DEFAULT);
System.out.println("Index created successfully");
}
/**
* 删除索引
* @throws IOException
*/
@Test
void deleteIndex() throws IOException{
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
//请求
highClient.indices().delete(request,RequestOptions.DEFAULT);
}
/**
* 获取索引
* 判断索引是否存在
* @throws IOException
*/
@Test
void existIndex() throws IOException{
GetIndexRequest request = new GetIndexRequest("hotel");
GetIndexResponse getIndexResponse = highClient.indices().get(request, RequestOptions.DEFAULT);
System.out.println(Arrays.toString(getIndexResponse.getIndices()));
boolean exists = highClient.indices().exists(request, RequestOptions.DEFAULT);
System.out.println(exists ? "索引存在":"索引不存在");
}
2、对文本进行操作:
如果需要进行序列化和反序列化操作,用fastjson就行。
- 初始化RestHighLevelClient;
- 创建XxxRequest。XXX是Index、Get、Update、Delete、Bulk;
- 准备参数(Index、Update、Bulk时需要);
- 发送请求。调用RestHighLevelClient#.xxx()方法,xxx是index、get、update、delete、bulk
- 解析结果(Get时需要)。
java
/**
* 创建文档 + 获取文档数据
*/
@Test
void CreateDoc() throws IOException {
//根据ID查询信息,并且完成序列化
Hotel hotel = hotelService.getById(47066L);
HotelDoc hotelDoc = new HotelDoc(hotel);
JSON.toJSONString(hotelDoc);
//发送请求
GetRequest request = new GetRequest("hotel").id("1");
GetResponse documentFields = highClient.get(request, RequestOptions.DEFAULT);
System.out.println(documentFields.getSourceAsString());
}
/**
* 删除文档
* @throws IOException
*/
@Test
void DeleteDoc() throws IOException{
// 1.准备Request
DeleteRequest request1 = new DeleteRequest("hotel", "1");
// 2.发送请求
System.out.println(highClient.delete(request1, RequestOptions.DEFAULT).getResult());
}
/**
* 增量修改
* 全量修改:字段数据一一对应即可
* @throws IOException
*/
@Test
void testUpdateDocument() {
// 1.准备Request
UpdateRequest request = new UpdateRequest("hotel", "1");
// 2.准备请求参数
request.doc(
"price", "999",
"starName", "钻"
);
// 3.发送请求
try {
highClient.update(request, RequestOptions.DEFAULT);
} catch (IOException e) {
System.out.println(JSON.toJSONString(e.getLocalizedMessage()));
}
}
/**
* 批量导入
* @throws IOException
*/
@Test
void testBulkRequest(){
//查询数据
List<Hotel> hotelList = hotelService.list();
//1、创建Request
BulkRequest request = new BulkRequest();
//2、准备参数
for (Hotel hotel : hotelList) {
//转换成为文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
IndexRequest indexRequest = new IndexRequest("hotel")
.id(hotelDoc.getId().toString())
.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
//创建新增的文档对象
request.add(indexRequest);
}
//3、发送请求
try {
highClient.bulk(request,RequestOptions.DEFAULT);
} catch (IOException e) {
System.out.println(JSON.toJSONString(e.getLocalizedMessage()));
}
}
3、查询:
建议参考:Java中ElasticSearch的各种查询(普通,模糊,前缀,高亮,聚合,范围)_java elasticsearch博客