目录
2.解压elasticsearch-analysis-ik-6.8.6.zip文件
一.全文搜索Lucene入门
目前的技术实现有Lucene,Solr,ElasticSearch等。全文检索过程分为索引、搜索两个过程:
-
索引(Indexing)
-
从关系数据库中、互联网上、文件系统采集源数据(要搜索的目标信息),源数据的来源是非常广泛的。
-
将源数据采集到一个统一的地方,例如存储系统,要创建索引,将索引创建到一个索引库(文件系统)中,从源数据库中提取关键信息,从关键信息中抽取一个一个词,词和源数据是有关联的。也即创建索引时,词和源数据有关联,索引库中记录了这个关联,如果找到了词就说明找到了源数据。
-
-
搜索(Search)
-
用户执行搜索(全文检索)编写查询关键字。
-
从索引库中搜索索引,根据查询关键字搜索索引库中的一个一个词。
-
展示搜索的结果。
-
1.全文搜索概述
1.1.什么是全文搜索
全文搜索引擎:就是把没有数据结构的数据,转换为有数据结构的数据,来加快对文本的快速搜索。
1.2.为什么要使用全文搜索
- 搜索效率高;
- 相关度最高的排在页面最前面;
- 关键字高亮;
- 只处理文本不处理语义,以单词方式搜索。
1.3.常见的全文搜索
- 全文搜索工具包:Lucene
- 全文搜索服务器:Lucene,Solr,ElasticSearch
三.Lucene概述
1.什么是Lucene
- Lucene是apache下的一个开源的全文检索引擎工具包(一堆jar包)。
- 为软件开发人员提供一个简单易用的工具包(类库),以方便的在小型目标系统中实现全文检索的功能。
- Lucene适用于中小型项目 ,ES适用于中大型项目(它底层是基于lucene实现的) 。
4.4.批量查询
批量查询很重要,对相比单个查询来说,批量查询性能更高。
-
不同索引库查询
GET _mget { "docs" : [ { "_index" : "itsource", "_type" : "blog", "_id" : 2 }, { "_index" : "itsource", "_type" : "employee", "_id" : 1, "_source": "email,age" } ] }
-
同索引库同类型 - 推荐
GET itsource/blog/_mget { "ids" : [ "2", "1" ] }
五.DSL查询与DSL过滤
1.DSL查询
1.1.什么是DSL查询
对于简单查询,使用查询字符串比较好,但是对于复杂查询,由于条件多,逻辑嵌套复杂,查询字符串不易组织与表达,且容易出错,因此推荐复杂查询通过DSL使用JSON内容格式的请求体代替。
DSL查询是由ES提供丰富且灵活的查询语言叫做DSL查询(Query DSL),它允许你构建更加复杂、强大的查询
。DSL(Domain Specific Language特定领域语言)以JSON请求体的形式出现。DSL有两部分组成:DSL查询和DSL过滤。
1.2.DSL查询语法
一个常用的相对完整的DSL查询:
GET /crm/user/_search
{
"query": {
"match_all": {}
},
"from": 20,
"size": 10,
"_source": ["username", "age", "id"],
"sort": [{"join_date": "desc"},{"age": "asc"}]
}
-
match_all
表示 查询所有数据, -
_source :代表查询返回username,age和email几个列,
-
sort :按照加入日期和年龄进行排序
GET /crm/user/_search
{
"query" : {
"match" : {
"username" : "Hello Java"
}
}
}
-
match : 会对查询的内容分词,如同:where username = hello or username = java
-
term : 如果把match换成term, 就不会分词,如同: where username = "Hello Java"
查询username中包含“老郑”的内容,match
指的是“标准查询”,该查询方式会对查询的内容进行分词。DSL查询可以支持的查询方式很多,如term
词元查询 ,range
范围查询等等。
2.DSL过滤
2.1.什么是DSL过滤
DSL过滤语句和DSL查询语句非常相似,但是它们的使用目的却不同。通常我们把DSL查询和DSL过滤一起用,当我们把查询条件放到DSL查询语句中,那么对应的字段就会进行相关性排序(ES底层维护了一个分数,用来排序),如果是把查询条件放到DSL过滤语句中,则不会进行相关性排序,因此性能更高。
2.2.查询与过滤的区别
DSL过滤和DSL查询在性能上的区别:
-
过滤结果可以缓存并应用到后续请求。
-
查询语句同时匹配文档,计算相关性,所以更耗时,且不缓存。
-
过滤语句可有效地配合查询语句完成文档过滤。
总结:原则上,使用DSL查询做全文本搜索(关键字搜索),或其他需要进行相关性评分的场景,其它全用DSL过滤。
2.3.DSL查询+过滤语法
下面是把DSL过滤语法和DSL查询语法组合起来一起使用,如下:
GET /crm/user/_search
{
"query": {
"bool": { // 组合查询
"must": [{ //与(must) 或(should) 非(must not)
"match": { //match : 标准查询,会对查询的内容分词后去查询
"username": "hello world" //等同于:where username=hello or username=world
},
}],
"filter": { //过滤语句,写法和must一样
"term": { //次元查询,把查询的内容当成一个整体去查询
"name": "hello world" //等同于 where name = "hello world"
}
}
}
},
"from": 20,
"size": 10,
"_source": ["name", "age", "username"],
"sort": [{
"join_date": "desc"
}, {
"age": "asc"
}]
}
解释:
-
query : 查询,所有的查询条件在query里面
-
bool : 组合搜索bool可以组合多个查询条件为一个查询对象,这里包含了 DSL查询和DSL过滤的条件
-
must : 必须匹配 :与(must) 或(should) 非(must_not)
-
match:分词匹配查询,会对查询条件分词 , multi_match :多字段匹配
-
filter: 过滤条件
-
term:词元查询,不会对查询条件分词
-
from,size :分页
-
_source :查询结果中需要哪些列
-
sort:排序
2.4.综合案例
名称(name)中有 "zs" 的用户 ,性别sex是男生(1),年龄(age)在 18- 20之间,按照年龄(age)倒排序,查询第 1 页,每页10 条 ,查询结果中只需要 :id,name,username,age
GET /aigou/product/_search
{
"query":{
"bool": {
"must": [{
"match": {
"name": "zs"
}
}],
"filter": [
{
"range":{ //范围查询
"age":{
"gte":18,
"lte":20
}
}
},
{
"term": { //词元查询
"sex": 1
}
}
]
}
},
"from": 1,
"size": 10,
"_source": ["id", "name", "age","username"],
"sort": [{
"age": "desc"
}]
}
3.查询方式
在上面的案例中,我们接触了 match
, range
等查询方式(查询对象),在ES还有很多其他的查询方式,在不同的场景中我们需要根据情况进行合理的选择。
3.1.全匹配(match_all)
普通搜索(匹配所有文档)
GET _search
{
"query": {
"bool": {
"must": [
{
"match_all": {}
}
],
"filter": {
"term": {
"name": "zs1"
}
}
}
}
}
3.2.标准查询(match和multi_match)
标准查询,可以理解为,分词查询有点像模糊匹配(like),会对查询的内容进行分词后,得到多个单词,分别带着多个单词去检索ES库,只要有一个单词能查出结果,整个查询就有结果。不管你需要全文本查询还是精确查询基本上都要用到它。
如下面的搜索会对Steven King分词,并找到包含Steven或King的文档,然后给出排序分值。
{
"query": {
"match": {
"fullName": "Steven King"
}
}
}
注意:上面效果如同 where fullName like "%Steven%" or fullName like "%King%"
提示:match一般只用于全文字段的匹配与查询,一般不用于过滤。
multi_match 查询允许你做 match查询的基础上同时搜索多个字段:
{
"query": {"multi_match": {
"query": "Steven King",
"fields": ["fullName", "title"]
}
}
}
注意:上面的搜索同时在fullName和title字段中匹配。
如同:where fullName = Steven or fullName = King or title = steven or title = King
3.3.单词搜索与过滤(Term和Terms)
单词/词元查询 , 可以理解为等值查询,字符串,数字等都可以使用它,把查询的内容看成一个整体去检索ES库
{
"query": {
"bool": {
"must": {
"match_all": {
}
},
"filter": {
"term": {
"username": "Steven King"
}
}
}
}
}
提示:上面的“Steven King”会被当成一个当成去term中匹配,它跟match不同的地方在于match会把“Steven King”分成“steven”和“king”分别取username中查询。
Terms支持多个字段查询
{
"query": {
"terms": {
"tags": [
"jvm",
"hadoop",
"lucene"
],
"minimum_match": 1
}
}
}
提示:minimum_match:至少匹配个数,默认为1
如同:where tags in (jvm , hadoop , lucene)
3.4.组合条件搜索与过滤(Bool)
组合搜索bool可以组合多个查询条件为一个查询对象,查询条件包括must、should和must_not。
例如:查询爱好有美女,同时也有喜欢游戏或运动,且出生于1990-06-30及之后的人。
{
"query": {
"bool": {
"must": [
{
"term": {
"hobby": "美女"
}
}
],
"should": [
{
"term": {
"hobby": "游戏"
}
},
{
"term": {
"hobby": "运动"
}
}
],
"must_not": [
{
"range": {
"birth_date": {
"lt": "1990-06-30"
}
}
}
],
"filter": [
...
]
}
}
}
上面案例如同:Hobby=美女 and (hobby=游戏 or hobby=运动) and birth_date >= 1990-06-30
提示: 如果 bool 查询下没有must子句,那至少应该有一个should子句。但是 如果有 must子句,那么没有 should子句也可以进行查询。
3.5.范围查询与过滤(range)
range过滤允许我们按照指定范围查找一批数据
{
"query": {
"range": {
"age": {
"gte": 20,
"lt": 30
}
}
}
}
上例中查询年龄大于等于20并且小于30。
gt:> gte:>= lt:< lte:<=
3.6.存在和缺失过滤器(exists和missing)
{
"query": {
"bool": {
"must": [
{
"match_all": {
}
}
],
"filter": {
"exists": {
"field": "gps"
}
}
}
}
}
提示:exists和missing只能用于过滤结果。
3.7.前匹配搜索与过滤(prefix)
和term查询相似,前匹配搜索不是精确匹配,而是类似于SQL中的like ‘key%’
{
"query": {
"prefix": {
"fullName": "王"
}
}
}
提示:上例即查询姓倪的所有人。
3.8.通配符搜索(wildcard)
使用*代表0~N个,使用?代表1个。
{
"query": {
"wildcard": {
"fullName": "倪*华"
}
}
}
六.分词器安装和使用
1.基本概念
1.1.什么是分词
在全文检索理论中,文档的查询是通过关键字查询文档倒排索引来进行匹配
,因此将文本拆分为有意义的单词,对于搜索结果的准确性至关重要
,因此,在建立索引的过程中和分析搜索语句的过程中都需要对文本串分词
。ES的倒排索引是分词的结果
。
2.IK分词器
2.1.为什么用IK分词器
ES默认对英文文本的分词器支持较好,但和lucene一样,如果需要对中文进行全文检索,那么需要使用中文分词器,同lucene一样,在使用中文全文检索前,需要集成IK分词器 - 大家都在用IK
2.2.安装IK分词器
1.下载ES的IK分词器
插件源码地址:GitHub - medcl/elasticsearch-analysis-ik at v6.8.6
2.解压elasticsearch-analysis-ik-6.8.6.zip文件
并将解压后的内容放置于ES根目录/plugins/ik
3.IK分词器配置
在ik/config 目录可以对分词器进行配置,如停词 , 自定义字典等。
4.IK分词测试
POST _analyze
{
"analyzer":"ik_smart",
"text":"中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首"
}
提示:IK分词器指定:ik_smart ; ik_max_word , ik_max_word 相比 ik_smart 来说会将文本做最细粒度的拆分。
七.文档类型映射
1.基本概念
1.1.什么是文档映射
ES的文档映射(mapping)机制用于进行字段类型确认,将每个字段匹配为一种确定的数据类型。就如同Mysql创建表时候指定的每个column列的类型。 为了方便字段的检索,我们会指定存储在ES中的字段是否进行分词,但是有些字段类型可以分词,有些字段类型不可以分词,所以对于字段的类型需要我们自己去指定。
需要注意的是,我们在使用ES的正确流程应该是:①创建索引 ; ②文档映射 ;③文档CRUD;
1.2.默认的字段类型
查看索引类型的映射配置:GET {indexName}/_mapping/{typeName}
-
基本字段类型
字段类型 | 值 | 值 | 值 | 值 | 值 |
---|---|---|---|---|---|
字符串 | text(分词) ; | keyword(不分词) ; | StringField(不分词文本); | TextFiled(要分词文本) | |
数字 | long | integer | short | double | float |
日期 | date | ||||
逻辑 | boolean |
-
复杂字段类型
字段类型 | 取值 |
---|---|
对象类型 | object |
数组类型 | array |
地理位置 | geo_point,geo_shape |
-
默认映射
ES在没有配置Mapping的情况下新增文档,ES会尝试对字段类型进行猜测,并动态生成字段和类型的映射关系。
内容 | 默认映射类型 |
---|---|
JSON type | Field type |
Boolean: true or false | "boolean" |
Whole number: 123 | "long" |
Floating point: 123.45 | "double" |
String, valid date:"2014-09-15" | "date" |
String: "foo bar" | "text" |
1.3.映射规则
字段映射的常用属性配置列表 - 即给某个字段执行类的时候可以指定以下属性
type | 类型:基本数据类型,integer,long,date,boolean,keyword,text... |
---|---|
enable | 是否启用:默认为true。 false:不能索引、不能搜索过滤,仅在_source中存储 |
boost | 权重提升倍数:用于查询时加权计算最终的得分。 |
format | 格式:一般用于指定日期格式,如 yyyy-MM-dd HH:mm:ss.SSS |
ignore_above | 长度限制:长度大于该值的字符串将不会被索引和存储。 |
ignore_malformed | 转换错误忽略:true代表当格式转换错误时,忽略该值,被忽略后不会被存储和索引。 |
include_in_all | 是否将该字段值组合到_all中。 |
null_value | 默认控制替换值。如空字符串替换为”NULL”,空数字替换为-1 |
store | 是否存储:默认为false。true意义不大,因为_source中已有数据 |
index | 索引模式:analyzed (索引并分词,text默认模式), not_analyzed (索引不分词,keyword默认模式),no(不索引) |
analyzer | 索引分词器:索引创建时使用的分词器,如ik_smart,ik_max_word,standard |
search_analyzer | 搜索分词器:搜索该字段的值时,传入的查询内容的分词器。 |
fields | 多字段索引:当对该字段需要使用多种索引模式时使用。 |
多字段索引如:城市搜索New York, 既可以分词,又可以不分词
"city":{
"type": "text",
"analyzer": "ik_smart",
"fields": {
"raw": {
"type": "keyword"
}
}
}
解释:相当于给 city取了一个别名 city.raw,city的类型为text , city.raw的类型keyword; 搜索 city分词 ; 搜索city.raw 不分词那么以后搜索过滤和排序就可以使用city.raw字段名
2.添加映射
注意:如果索引库已经有数据了,就不能再添加映射了
2.1.创建新的索引库
put aigou
2.2.单类型创建映射
put aigou/goods/_mapping
{
"goods": {
"properties": {
"id": {
"type": "long"
},
"name": {
"type": "text",
"analyzer": "ik_smart",
"search_analyzer": "ik_smart"
}
}
}
}
解释:给aigou索引库中的是goods类型创建映射 ,id指定为long类型 , name指定为text类型(要分词),analyzer分词使用ik,查询分词器也使用ik
2.3.多类型创建映射
PUT aigou
{
"mappings": {
"user": {
"properties": {
"id": {
"type": "integer"
},
"info": {
"type": "text",
"analyzer": "ik_smart",
"search_analyzer": "ik_smart"
}
}
},
"dept": {
"properties": {
"id": {
"type": "integer"
},
....更多字段映射配置
}
}
}
}
解释:同时给user和dept创建文档映射
2.4.数组/对象映射
基本类型字段映射非常简单,直接配置对应的类型即可,但是数组和对象如何指定类型呢?
1.对象映射
{
"id" : 1,
"girl" : {
"name" : "王小花",
"age" : 22
}
}
文档映射
{
"properties": {
"id": {"type": "long"},
"girl": {
"properties":{
"name": {"type": "keyword"},
"age": {"type": "integer"}
}
}
}
}
2.数组映射
{
"id" : 1,
"hobby" : ["王小花","林志玲"]
}
文档映射
{
"properties": {
"id": {"type": "long"},
"hobby": {"type": "keyword"}
}
}
解释:数组的映射只需要映射一个元素即可,因为数组中的元素类型是一样的。
3.对象数组
{
"id" : 1,
"girl":[{"name":"林志玲","age":32},{"name":"赵丽颖","age":22}]
}
文档映射
"properties": {
"id": {
"type": "long"
},
"girl": {
"properties": {
"age": { "type": "long" },
"name": { "type": "text" }
}
}
}
在实际项目中,我们按照如下流程操作ES
1.建索引库
2.全局映射
3.自定义映射
4.Java-API做CRUD
八.JavaApi-SpringBoot操作ES
1.集成ES
官方文档API:Java Transport Client (deprecated) | Elastic
1.1.导入依赖
下面采用ES提供的Jar进行ES操作
<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>
2.2.配置ES
spring:
application:
name: service-search
elasticsearch:
rest:
uris: http://localhost:9200
2.3.定义DOC对象
该文档对象用来如下几个事情
-
索引库的创建
-
文档的映射
-
存储到ES的数据封装
/**
* 针对于 Employee 表的文档映射
* indexName:索引库
* type:类型(表类型)
*/
@Document(indexName = "hrm" , type = "employee")
public class EmployeeDoc {//对应文档的id PUT /index/type/id
@Id
private Long id;@Field(type = FieldType.Keyword) //指定为 不分词
private String userName;private int age;
@Field(type =FieldType.Text,analyzer = "ik_max_word",searchAnalyzer = "ik_max_word")
private String intro;
...
2.4.初始化索引库和文档映射
ElasticsearchTemplate 是一个ES的工具类
@RunWith(SpringRunner.class)
@SpringBootTest(classes = EsServiceApplication2050.class)
public class ESTest {@Autowired
private ElasticsearchRestTemplate template;@Test
public void testCreateIndex() {
//创建索引
template.createIndex(EmployeeDoc.class);
//做文档映射
template.putMapping(EmployeeDoc.class);
}
}
2.5.定义Repository
SpringBoot提供了一个接口 ElasticsearchRepository 其中包含了对ES的CRUD操作
@Repository
public interface EmployeeElasticsearchRepository extends ElasticsearchRepository<EmployeeDoc,Long> {}
泛型分别是:Repository管理的实体类对象 ,和ID的类型
2.ES的CRUD
2.1.添加
courseElasticsearchRepository.save(doc)
2.2.删除
courseElasticsearchRepository.deleteById(1L);
2.3.获取
Optional<CourseDoc> optional = courseElasticsearchRepository.findById(1L);
CourseDoc courseDoc = optional.get();
2.4.高级查询
//需求:查询课程名 name 中包含 java : DSL查询 - must - match
// 价格 price 在 1000 - 3000 : DSL过滤 - filter - range
// 每页 10 条,取第一页 ,按照价格倒排
@Test
public void testSearch(){//创建一个本机查询builder
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
//=================================================
//1.查询条件
//组合查询
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();//查询课程名 name 中包含 java : DSL查询 - must - match
boolQueryBuilder.must(QueryBuilders.matchQuery("name","php"));//价格 price 在 1000 - 3000 : DSL过滤 - filter - range
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(1000).lte(3000));builder.withQuery(boolQueryBuilder);
//2.排序 按照价格倒排
builder.withSort(new FieldSortBuilder("price").order(SortOrder.DESC));
//3.分页
builder.withPageable(PageRequest.of(0,10));
//=================================================
//构建一个查询对象
NativeSearchQuery searchQuery = builder.build();//执行查询,得到结果
Page<CourseDoc> page = courseElasticsearchRepository.search(searchQuery);
//page -> PageListSystem.out.println("总条数:"+page.getTotalElements());
System.out.println("总页数:"+page.getTotalPages());//结果列表
List<CourseDoc> content = page.getContent();content.forEach(courseDoc -> {
System.out.println(courseDoc);
});
}