elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容
elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。
而elasticsearch是elastic stack的核心,负责存储、搜索、分析数据。
倒排索引的概念是基于MySQL这样的正向索引而言的。
但如果是基于title做模糊查询,只能是逐行扫描数据,流程如下:
1)用户搜索数据,条件是title符合"%手机%"
2)逐行获取数据,比如id为1的数据
3)判断数据中的title是否符合用户搜索条件
4)如果符合则放入结果集,不符合则丢弃。回到步骤1
逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。
Document
):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息Term
):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条创建倒排索引是对正向索引的一种特殊处理,流程如下:
"华为手机"
进行搜索。华为
、手机
。那么为什么一个叫做正向索引,一个叫做倒排索引呢?
正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程。
而倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程。
是不是恰好反过来了?
那么两者方式的优缺点是什么呢?
正向索引:
倒排索引:
elasticsearch中有很多独有的概念,与mysql中略有差别,但也有相似之处。
elasticsearch是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中:
而Json文档中往往包含很多的字段(Field),类似于数据库中的列。
索引(Index),就是相同类型的文档的集合。
例如:
数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。
我们统一的把mysql与elasticsearch的概念做一下对比:
MySQL | Elasticsearch | 说明 |
---|---|---|
Table | Index | 索引(index),就是文档的集合,类似数据库的表(table) |
Row | Document | 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 |
Column | Field | 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column) |
Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema) |
SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD |
elasticsearch、mysql两者各自有自己的擅长:
因此在企业中,往往是两者结合使用:
安装就不一一详细说了 百度中有很多
注意 : 三者的版本一定要一样
我这里安装的 8.2.3 下面的例子也使用的8.2.3
分词器到作用
IK分词器有几种模式?
IK分词器如何拓展词条?如何停用词条?
索引库就类似数据库表,mapping映射就类似表的结构。
我们要向es中存储数据,必须先创建“库”和“表”。
mapping是对索引库中文档的约束,常见的mapping属性包括:
{
"age": 21,
"weight": 52.1,
"isMarried": false,
"info": "Java讲师",
"email": "[email protected]",
"score": [99.1, 99.5, 98.9],
"name": {
"firstName": "sides",
"lastName": "three"
}
}
对应的每个字段映射(mapping):
这里使用Kibana编写DSL的方式来演示。
格式:
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
},
// ...略
}
}
}
示例:
PUT /ts
{
"mappings": {
"properties": {
"info":{
"type": "text",
"analyzer": "ik_smart"
},
"email":{
"type": "keyword",
"index": "false"
},
"name":{
"properties": {
"firstName": {
"type": "keyword"
}
}
}
}
}
}
基本语法:
GET /索引库名
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping。
虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。
语法说明:
PUT /索引库名/_mapping
{
"properties": {
"新字段名":{
"type": "类型"
}
}
}
语法:
格式:
DELETE /索引库名
语法:
POST /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
},
// ...
}
根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把文档id带上。
语法:
GET /{索引库名称}/_doc/{id}
删除使用DELETE请求,同样,需要根据id进行删除:
语法:
DELETE /{索引库名}/_doc/id值
修改有两种方式:
全量修改是覆盖原来的文档,其本质是:
注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。
语法:
PUT /{索引库名}/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
// ... 略
}
增量修改是只修改指定id匹配的文档中的部分字段。
语法:
POST /{索引库名}/_update/文档id
{
"doc": {
"字段名": "新的值",
}
}
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-generatorartifactId>
<version>3.5.1version>
dependency>
<dependency>
<groupId>org.apache.velocitygroupId>
<artifactId>velocity-engine-coreartifactId>
<version>2.3version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
@SpringBootApplication
public class ESApplication {
public static void main(String[] args) {
SpringApplication.run(ESApplication.class, args);
}
}
server:
port: 8082
spring:
datasource:
url: jdbc:mysql://localhost:3306/spring_cloud_study
username: root
password: root1234
application:
name: es-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8868
config:
server-addr: 127.0.0.1:8868
file-extension: yaml
# 支持多个共享dataId的配置,优先级小于extension-configs,shared-configs是一个集合
shared-configs[0]:
# 网关 通用配置可以定义在这个里面
dataId: demo-gateway.yaml # 配置文件名dataId
group: DEFAULT_GROUP # 默认为DEFAULT_GROUP
refresh: true # 是否动态刷新,默认为false
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #mybatis日志输出
map-underscore-to-camel-case: true #Mybatis 驼峰配置
项目doc中tb_hotel.sql
数据结构如下:
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;
PUT /hotel
{
"mappings": {
"properties": {
"id": {
"type": "long"
},
"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"
}
}
}
}
必须先完成这个对象的初始化,建立与elasticsearch的连接。
<dependency>
<groupId>co.elastic.clientsgroupId>
<artifactId>elasticsearch-javaartifactId>
dependency>
<dependency>
<groupId>org.elasticsearch.clientgroupId>
<artifactId>elasticsearch-rest-clientartifactId>
dependency>
<elasticsearch-version>8.2.3elasticsearch-version>
<dependency>
<groupId>org.glassfishgroupId>
<artifactId>jakarta.jsonartifactId>
<version>2.0.1version>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
<version>2.12.3version>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-coreartifactId>
<version>2.12.3version>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-annotationsartifactId>
<version>2.12.3version>
dependency>
配置文件 application.yaml中添加ES host、port
elasticsearch:
host: 127.0.0.1
port: 9200
启动类中添加初始化代码
@Value("${elasticsearch.host}")
private String host;
@Value("${elasticsearch.port}")
private Integer port;
@Bean
public ElasticsearchClient getClient() {
RestClient restClient = RestClient.builder(new HttpHost(host,port)).build();
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper());
return new ElasticsearchClient(transport);
}
EsClient
类 添加createIndex
方法@Component
public class EsClient {
@Resource
private ElasticsearchClient elasticsearchClient;
/**
* 创建索引:若索引存在,先删除再创建
*
* @param indexName 索引名称
* @param mappings 映射
*/
public void createIndex(String indexName, Map<String, Property> mappings) {
try {
//创建索引
CreateIndexRequest createIndexRequest = CreateIndexRequest.of(e -> e
.index(indexName)
.mappings(m -> m
.properties(mappings)
)
);
elasticsearchClient.indices().create(createIndexRequest);
} catch (IOException e) {
System.err.println("创建索引失败!");
}
}
}
ESIndexLibraryTest
测试类中,实现创建索引:@RunWith(SpringRunner.class)
@SpringBootTest(classes = ESApplication.class)
public class ESIndexLibraryTest {
@Autowired
private EsClient esClient;
@Test
public void test() {
// keyword类型
Property keywordProperty = Property.of(o -> o.keyword(kBuilder -> kBuilder));
// keyword类型CopyTo
Property keywordPropertyCopyTo = Property.of(o -> o.keyword(kBuilder -> kBuilder.copyTo("all")));
// keyword类型不创建索引
Property keywordPropertyFalseIndex = Property.of(o -> o.keyword(kBuilder -> kBuilder.index(false)));
// text类型分词 搜索分词
Property textProperty = Property.of(o -> o.text(tBuilder -> tBuilder.analyzer("ik_max_word").searchAnalyzer("ik_max_word")));
// text类型分词CopyTo 搜索分词
Property textPropertyCopyTo = Property.of(o -> o.text(tBuilder -> tBuilder.copyTo("all").analyzer("ik_max_word").searchAnalyzer("ik_max_word")));
// integer类型
Property integerProperty = Property.of(o -> o.integer(iBuilder -> iBuilder));
// long类型
Property longProperty = Property.of(o -> o.long_(lBuilder -> lBuilder));
// geoPoint类型
Property geoPointProperty = Property.of(o -> o.geoPoint(lBuilder -> lBuilder));
// date类型
Property dateProperty = Property.of(o -> o.date(dBuilder -> dBuilder.format("yyyy-MM-dd HH:mm:ss")));
Map<String, Property> esDTO = new HashMap<>();
esDTO.put("id", longProperty);
esDTO.put("name", textPropertyCopyTo);
esDTO.put("address", keywordPropertyFalseIndex);
esDTO.put("price", integerProperty);
esDTO.put("rating", integerProperty);
esDTO.put("brand", keywordPropertyCopyTo);
esDTO.put("city", keywordPropertyCopyTo);
esDTO.put("starName", keywordPropertyCopyTo);
esDTO.put("business", keywordProperty);
esDTO.put("location", geoPointProperty);
esDTO.put("pic", keywordPropertyFalseIndex);
esDTO.put("all", textProperty);
esClient.createIndex("hotel", esDTO);
}
}
EsClient
添加deleteIndex
方法 public void deleteIndex(String indexName) {
try {
DeleteIndexRequest deleteIndexRequest = DeleteIndexRequest
.of(e -> e
.index(indexName));
elasticsearchClient.indices().delete(deleteIndexRequest);
} catch (IOException e) {
System.err.println("删除索引失败!");
}
}
ESIndexLibraryTest
测试类中,实现删除索引:@Test
public void deleteIndexTest() {
try {
esClient.deleteIndex("hotel");
} catch (Exception e) {
e.printStackTrace();
}
}
数据库查询后的结果是一个Hotel类型的对象。结构如下:
@Data
@TableName("tb_hotel")
public class Hotel implements Serializable {
/**
* 酒店id
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 酒店名称
*/
@TableField("name")
private String name;
/**
* 酒店地址
*/
@TableField("address")
private String address;
/**
* 酒店价格
*/
@TableField("price")
private Integer price;
/**
* 酒店评分
*/
@TableField("score")
private Integer score;
/**
* 酒店品牌
*/
@TableField("brand")
private String brand;
/**
* 所在城市
*/
@TableField("city")
private String city;
/**
* 酒店星级,1星到5星,1钻到5钻
*/
@TableField("star_name")
private String starName;
/**
* 商圈
*/
@TableField("business")
private String business;
/**
* 纬度
*/
@TableField("latitude")
private String latitude;
/**
* 经度
*/
@TableField("longitude")
private String longitude;
/**
* 酒店图片
*/
@TableField("pic")
private String pic;
}
与我们的索引库结构存在差异:
@Data
@NoArgsConstructor
public class HotelDoc {
private Long id;
private String name;
private String address;
private Integer price;
private Integer rating;
private String brand;
private String city;
private String starName;
private String business;
private String location;
private String pic;
// 排序时的 距离值
private Object distance;
// 广告标记
private Boolean isAD;
public HotelDoc(Hotel hotel) {
this.id = hotel.getId();
this.name = hotel.getName();
this.address = hotel.getAddress();
this.price = hotel.getPrice();
this.rating = hotel.getScore();
this.brand = hotel.getBrand();
this.city = hotel.getCity();
this.starName = hotel.getStarName();
this.business = hotel.getBusiness();
this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
this.pic = hotel.getPic();
}
}
EsClient
添加importDocument
方法
/**
* 导入文档
*
* @param indexName 索引名称
* @param object 数据
* @param T
*/
public <T> void importDocument(String indexName, T object) {
try {
//通过反射获取Id属性
Field[] field = object.getClass().getDeclaredFields();
//设置对象的访问权限,保证对private的属性的访问
field[0].setAccessible(true);
String id = field[0].get(object).toString();
elasticsearchClient.index(i -> i
.index(indexName)
.id(id)
.document(object)
);
} catch (Exception e) {
e.printStackTrace();
}
}
ESImportDocumentTest
测试类中,实现新增文档:@RunWith(SpringRunner.class)
@SpringBootTest(classes = ESApplication.class)
public class ESImportDocumentTest {
@Autowired
private EsClient esClient;
@Autowired
HotelMapper hotelMapper;
@Test
public void importDocumentTest() {
Hotel hotel = hotelMapper.selectOne(new QueryWrapper<Hotel>().eq("id", 36934));
HotelDoc hotelDoc = new HotelDoc(hotel);
esClient.importDocument("hotel",hotelDoc);
}
}
EsClient
添加getDocumentById
方法public <T> T getDocumentById(String indexName,String id,Class<T> targetClass){
GetResponse<T> response = null;
try {
response = elasticsearchClient.get(g -> g
.index(indexName)
.id(id), targetClass
);
} catch (IOException e) {
e.printStackTrace();
}
return handleResponse(response);
}
private <T> T handleResponse(GetResponse<T> response) {
if (response == null) {
return null;
}
return response.source();
}
ESQueryDocumentTest
测试类中,实现id查询文档:@RunWith(SpringRunner.class)
@SpringBootTest(classes = ESApplication.class)
public class ESQueryDocumentTest {
@Autowired
private EsClient esClient;
@Test
public void QueryDocumentByIdTest() {
HotelDoc hotel = esClient.getDocumentById("hotel", "36934", HotelDoc.class);
System.out.println("hotel = " + hotel);
}
}
EsClient
添加deleteDocument
方法
/**
* 删除文档
*
* @param indexName 索引名
* @param id id
*/
public void deleteDocument(String indexName, String id) {
try {
elasticsearchClient.delete(DeleteRequest
.of(s -> s
.index(indexName)
.id(id)
)
);
} catch (IOException e) {
e.printStackTrace();
}
}
ESDeleteDocumentTest
测试类中,实现id删除文档:@RunWith(SpringRunner.class)
@SpringBootTest(classes = ESApplication.class)
public class ESDeleteDocumentTest {
@Autowired
private EsClient esClient;
@Test
public void deleteDocument() {
esClient.deleteDocument("hotel","36934");
}
}
EsClient
添加updateDocumentById
方法public <T> void updateDocumentById(String indexName,String id ,Object value,Class<T> targetClass){
try {
elasticsearchClient.update(g -> g
.index(indexName)
.id(id)
.doc(value),targetClass
);
} catch (IOException e) {
e.printStackTrace();
}
}
ESUpdateDocumentTest
测试类中,实现修改文档:@RunWith(SpringRunner.class)
@SpringBootTest(classes = ESApplication.class)
public class ESUpdateDocumentTest {
@Autowired
private EsClient esClient;
@Test
public void updateDocumentTest() {
Map<String, Object> objectMap = new HashMap<>();
objectMap.put("price","952");
objectMap.put("starName","四钻");
esClient.updateDocumentById("hotel", "36934",objectMap, HotelDoc.class);
}
}
EsClient
添加batchImportDocumentBuild
方法
/**
* 批量导入文档数据
*
* @param list 数据对象集合
* @param indexName 索引名
* @param 数据对象类型
*/
public <T> void batchImportDocumentBuild(String indexName, List<T> list) {
BulkRequest.Builder br = new BulkRequest.Builder();
try {
for (T object : list) {
//通过反射获取Id属性
Field[] field = object.getClass().getDeclaredFields();
//设置对象的访问权限,保证对private的属性的访问
field[0].setAccessible(true);
String id = field[0].get(object).toString();
br.operations(op -> op
.index(idx -> idx
.index(indexName)
.id(id)
.document(object)
)
);
}
elasticsearchClient.bulk(br.build());
} catch (Exception e) {
e.printStackTrace();
}
}
ESImportDocumentTest
测试类中,实现批量添加文档: @Test
public void batchImportDocumentBuildTest() {
List<Hotel> hotelList = hotelMapper.selectList(new QueryWrapper<Hotel>());
List<HotelDoc> hotelDocList = hotelList.stream().map(HotelDoc::new).collect(Collectors.toList());
esClient.batchImportDocumentBuild("hotel",hotelDocList);
}