在开发过程中,我们一般使用mySql的like进行数据库模糊查询,mysql中,主键id建立b+树索引,然后通过目录页对应到数据页,然后找到数据,但是like的查询,是走不到索引的,需要全表扫描,大数据量情况下全表扫描速度非常慢,所以我们开始采用全文检索,今天我们了解一下ElasticSearch全文搜索引擎。
一、全文检索引擎
全文搜索引擎 : 就是把没有结构的数据,转换为有结构的数据,来加快对文本的快速搜索,通常而言,有结构的数据的查询是很快的。
为什么要用全文搜素引擎?
简而言之,全文搜素引擎速度快,可以实现搜索相似度高的数据排在前面,关键字的高亮,只处理文本不处理语义.
常见的全文搜索引擎有哪些?
我们在开发中常用的全文搜索引擎有Lucene,基于封装了lucene并扩展的Elastic Search(ES) 、Solr。
Lucene是apache下的一个开源的全文检索引擎工具包(一堆jar包)。它为软件开发人员提供一个简单易用的工具包(类库),以方便的在小型目标系统中实现全文检索的功能。全文搜索服务器 ,封装了lucene并扩展,适用于中大型项目。
Apache Lucene项目的开源企业搜索平台。其主要功能包括全文检索、命中标示、分面搜索、动态聚类、数据库集成,以及富文本(如Word、PDF)的处理。
Solr是高度可扩展的,并提供了分布式搜索和索引复制。Solr是最流行的企业级搜索引擎,Solr4 还增加了NoSQL支持。
二、Elastic Search的核心(Lucene)
Lucene是apache下的一个开源的全文检索引擎工具包(一堆jar包)。它为软件开发人员提供一个简单易用的工具包(类库),以方便的在小型目标系统中实现全文检索的功能。全文搜索服务器 ,封装了lucene并扩展,适用于中大型项目。
任何技术都有一些核心,Lucene也有核心,而它的核心分为:
索引创建,索引搜索
。
Lucene的流程
采集数据到数据库创建索引到索引库用户进行查询查询进行搜索索引数据展示
索引创建
使用一定的规则,将文本进行分词,创建一个倒排索引文档,倒排索引文档做搜索的速度非常快,这也是为什么Lucene全文检索快的原因。
倒排索引:倒排索引是目前几乎所有支持全文检索的搜索引擎都要依赖的一个数据结构,Lucene最终会把数据做成倒排索引,其实就是数据在前,索引在后,叫做倒排索引。
实现倒排索引的流程:
倒排索引:倒排索引是目前几乎所有支持全文检索的搜索引擎都要依赖的一个数据结构,Lucene最终会把数据做成倒排索引,其实就是数据在前,索引在后,叫做倒排索引
1.原始数据加索引
2分词
3.变为小写并且词的时态变换
4分词后按照字母顺序重排序
5.去重、索引合并形成倒排索引(它支持前缀匹配,也支持通配符匹配(类似模糊查询),所以单个字母查询也可以实现)
索引搜索
把要搜索的关键字去匹配倒排索引文档,拿到关键字对应的原始数据并进行排序,匹配度高的在前面。
1 关键字分词
2根据分词后的数据匹配倒排索引,拿到关键字对应的所有索引
3由索引值找到数据
4计算出匹配的值的相关度进行排序响应
三、ElasticSearch
ES的出现
随着开发的使用,Lucene出现了两个难以解决的问题。
1)数据越大,存不下来,那我就需要多台服务器存数据,那么我的Lucene不支持分布式的,那就需要安装多个Lucene然后通过代码来合并搜索结果。这样很不好
2)数据要考虑安全性,一台服务器挂了,那么上面的数据不就消失了。
所以,为了进一步的解决问题,ES开始变得流行使用。
ES的特点
- 分布式的实时文件存储
- 分布式全文搜索引擎,每个字段都被索引并可被搜索
- 能在分布式项目/集群中使用
- 本身支持集群扩展,可以扩展到上百台服务器
- 处理PB级结构化或非结构化数据
- 简单的 RESTful API通信方式
- 支持各种语言的客户端
- 基于Lucene封装,使操作简单
ES与Lucene的区别
- Lucene只支持Java,ES支持多种语言
- Lucene非分布式,ES支持分布式
- Lucene非分布式的,索引目录只能在项目本地 , ES的索引库可以跨多个服务分片存储
- Lucene使用非常复杂 , ES屏蔽了Lucene的使用细节,操作更方便
- 单体/小项目使用Lucene ,大项目,分布式项目使用ES
ES的安装与使用
下载地址:Download Elasticsearch | Elastic
直接解压就能用(针对中小型项目),大型项目还是要调一调参数的。
使用浏览器访问:http://localhost:9200
如果ES启动占用的内存比较大可以通过修改 jvm.options 文件来修改内存
Kibana5安装
在ES中我们一般安装Kibana5配合使用。
下载地址:Download Kibana Free | Get Started Now | Elastic
解压即可安装 , 执行bin\kibana.bat 即可启动Kibana
解压并编辑config/kibana.yml,设置elasticsearch.url的值为已启动的ES
默认情况下,Kibana会链接本地的默认ES http://localhost:9200
,如果需要修改链接的ES服务器,通过修改安装目录下 config/kibana.yml,将配置项 #elasticsearch.url: "http://localhost:9200"
取消注释即可修改连接的ES服务器地址。
浏览器访问 http://localhost:5601 Kibana默认地址
Kibana组件详细说明:https://www.cnblogs.com/hunttown/p/6768864.html
索引&文档操作
我们可以结合MySql来了解全文搜索的功能。
索引库的CRUD
- 1.创建一个索引:PUT http://localhost:9200/索引名称
- 2.删除一个索引:DELETE http://localhost:9200/索引名称
- 3.查询一个索引:GET http://localhost:9200/索引名称
- 3.1查询所有索引库:GET _cat/indices?v
- 3.2查询指定索引库:GET _cat/indices/aigou
文档的CRUD(方法/索引库/类型/Jsons数据)
1.GET /索引库/_doc/Id:查询单个
2.GET /索引库/_doc/_search:查询所有
3.PUT /索引库/_doc/Id {json数据}:指定ID添加
4.POST /索引库/_doc {json数据}:不指定ID添加
5.POST /索引库/_doc/ID/_update {"doc":{json数据}}:局部修改数据
6.POST /索引库/_doc/ID {json数据}:覆盖修改数据
7.DELETE /索引库/_doc/ID
DSL查询
对于简单查询,使用查询字符串比较好,但是对于复杂查询,由于条件多,逻辑嵌套复杂,查询字符串不易组织与表达,且容易出错,因此推荐复杂查询通过DSL使用JSON内容格式的请求体代替。
DSL查询是由ES提供丰富且灵活的查询语言叫做DSL查询(Query DSL),它允许你构建更加复杂、强大的查询
。DSL(Domain Specific Language特定领域语言)以JSON请求体的形式出现。DSL有两部分组成:DSL查询和DSL过滤。
- DSL查询:query DSL
可以理解为MySql中的模糊匹配,类似于like查询 模糊查询
- DSL过滤:filter DSL
可以理解为MySql中的等值比较,类似于等于或不等于 等值查询
常见DSL关键字及功能
-
query : 查询,所有的查询条件在query里面
-
bool : 组合搜索bool可以组合多个查询条件为一个查询对象,这里包含了 DSL查询和DSL过滤的条件
-
must : 必须匹配 :与(must) 或(should) 非(must_not)
-
match:分词匹配查询,会对查询条件分词 , multi_match :多字段匹配
-
filter: 过滤条件
-
term:词元查询,不会对查询条件分词
-
from,size :分页
-
_source :查询结果中需要哪些列
-
sort:排序
我们结合下面案例更容易理解
# 查询员工表
# name包含zs
# age在1~12之间
# sex=1
# 每页大小2
# 从二页开始查
# 按照age倒序
DELETE /pethome
post _bulk
{"create":{"_index":"pethome","_type":"employee","_id":1}}
{"id":"1","age":1,"name":"zs","sex":1}
{"create":{"_index":"pethome","_type":"employee","_id":2}}
{"id":"2","age":2,"name":"zs","sex":1}
{"create":{"_index":"pethome","_type":"employee","_id":3}}
{"id":"3","age":3,"name":"zs","sex":1}
{"create":{"_index":"pethome","_type":"employee","_id":4}}
{"id":"4","age":19,"name":"ls","sex":0}
{"create":{"_index":"pethome","_type":"employee","_id":5}}
{"id":"5","age":4,"name":"zs","sex":0}
{"create":{"_index":"pethome","_type":"employee","_id":6}}
{"id":"6","age":14,"name":"zl","sex":0}
{"create":{"_index":"pethome","_type":"employee","_id":7}}
{"id":"7","age":5,"name":"cq","sex":1}
GET /pethome/_search
GET /pethome/employee/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "zs"
}
}
],
"filter": [{
"term": {
"sex": 1
}},
{"range": {
"age": {
"gte": 1,
"lte": 12
}
}
}]
}
},
"size": 2,
"from": 2,
"sort": [
{
"age": "desc"
}
]
}
IK分词器
因为我们使用的ES为外国的工程师开发的,对中国的汉字存在一定的使用障碍,我们汉字的分词需要分词器支持,分词器的作用至关重要,数据的查询结果是否精准跟分词器有很大的关系。ES默认对英文文本的分词器支持较好,但和lucene一样,如果需要对中文进行全文检索,那么需要使用中文分词器,同lucene一样,在使用中文全文检索前,需要集成IK分词器 - 大家都在用IK
文件类型映射
ES的文档映射(mapping)机制用于进行字段类型确认,将每个字段匹配为一种确定的数据类型。就如果Mysql创建表时候指定的每个column列的类型。 为了方便字段的检索,我们会指定存储在ES中的字段是否进行分词,但是有些字段类型可以分词,有些字段类型不可以分词,所以对于字段的类型需要我们自己去指定。
Java整合ElasticSearch
1.导入Jar包
<dependencies>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>transport</artifactId>
<version>6.8.6</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
2.添加Java连接ES工具类
package cn.itsource.utils;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* @BelongsProject: elasticSearch
* @BelongsPackage: cn.itsource.utils
* @Author: Director
* @CreateTime: 2022-07-22 23:17
* @Description: Java连接ES工具类
* @Version: 1.0
*/
public class ESClientUtil {
public static TransportClient getClient(){
TransportClient client = null;
Settings settings = Settings.builder()
.put("cluster.name", "elasticsearch").build();
try {
client = new PreBuiltTransportClient(settings)
.addTransportAddress(new TransportAddress(InetAddress.getByName("127.0.0.1"), 9300));
} catch (UnknownHostException e) {
e.printStackTrace();
}
return client;
}
}
3.案例使用
/**
* @Description: 复杂查询
* @Author: Director
* @Date: 2022/7/23 0:04
* @return: void
**/
@Test
public void testComplex() throws Exception {
// 1.获取ES连接
TransportClient client = ESClientUtil.getClient();
// 查询条件:- 查询用户表,name包含:我在源码,age在1~12之间,每页大小2,从二页开始查,按照age倒序
// 2.得到搜索对象
SearchRequestBuilder builder = client.prepareSearch("pethome");
// 3.指定要搜索的类型
builder.setTypes("pet");
// 4.指定Query搜索对象,当参数是接口时我们可以传递:接口的工具类、接口实现、匿名内部类,此处传递接口工具类
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 4.1.指定DSL查询,name包含我在源码
boolQuery.must(QueryBuilders.matchQuery("name", "我在源码"));
// 4.2.指定DSL过滤,age在1~12之间
boolQuery.filter(QueryBuilders.rangeQuery("age").gte(1).lte(12)).filter(QueryBuilders.termQuery("sex", 1));
// 4.3.将搜索条件放入到搜索对象中
builder.setQuery(boolQuery);
// 5.设置分页起始位置
builder.setFrom(2);
// 6.设置分页每页展示条数
builder.setSize(2);
// 7.设置排序,age倒序
builder.addSort("age", SortOrder.DESC);
// 8.得到结果进行打印
SearchHits hits = builder.get().getHits();
System.out.println("命中结果:" + hits.getTotalHits());
for (SearchHit document : hits) {
System.out.println(document.getSourceAsMap());
}
}
springboot整合ElasticSearch
1.导入依赖
<!--SpringBoot-->
<parent>
<groupId> org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
2.yml配置
spring:
elasticsearch:
rest:
uris: http://localhost:9200
3.启动类
package cn.hc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ESApplication {
public static void main(String[] args) {
SpringApplication.run(ESApplication.class, args);
}
}
4.创建Doc对象
package cn.hc.doc;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.math.BigDecimal;
import java.util.Date;
@Data
@Document(indexName = "order",type = "_doc")
public class OrderDoc {
@Id
private Long id;
@Field(type = FieldType.Text,analyzer = "ik_smart" ,searchAnalyzer = "ik_smart")
private String title;
@Field(type = FieldType.Keyword)
private String brand;
@Field(type = FieldType.Double)
private BigDecimal amount;
@Field(type = FieldType.Date)
private Date createTime;
}
package cn.hc.repository;
import cn.hc.doc.OrderDoc;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Service;
@Service
public interface OrederEsRepository extends ElasticsearchRepository<OrderDoc,Long> {
}
5.测试
package cn.hc;
import cn.hc.doc.OrderDoc;
import cn.hc.repository.OrederEsRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import java.math.BigDecimal;
import java.util.Date;
import java.util.Optional;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ESApplication.class)
public class EsTest {
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
@Autowired
private OrederEsRepository repository;
@Test
public void CreatIndex(){
//索引
boolean index = elasticsearchRestTemplate.createIndex(OrderDoc.class);
System.out.println(index);
//映射
boolean mapping = elasticsearchRestTemplate.putMapping(OrderDoc.class);
System.out.println(mapping);
}
@Test
public void addTest() {
for (long i = 0; i < 20 ; i++) {
OrderDoc orderDoc = new OrderDoc();
orderDoc.setId(i);
orderDoc.setBrand("EVA");
orderDoc.setAmount(new BigDecimal(5));
orderDoc.setTitle(orderDoc.getBrand() + "机甲," + orderDoc.getAmount() + "个,强!");
orderDoc.setCreateTime(new Date());
repository.save(orderDoc);
}
Optional<OrderDoc> doc = repository.findById(10L);
System.out.println(doc.get());
repository.findById(10L);
}
@Test
public void updateTest(){
Optional<OrderDoc> doc = repository.findById(7L);
OrderDoc orderDoc = doc.get();
orderDoc.setTitle("哥斯拉");
repository.save(orderDoc);
Optional<OrderDoc> doc1 = repository.findById(7L);
OrderDoc orderDoc1 = doc1.get();
System.out.println(orderDoc1);
}
}