索引类型

正向索引

定义与结构

  • 正向索引是从文档到单词的映射索引结构,它将文档中的每个单词与包含该单词的文档进行关联。这种索引结构适合于根据文档查找单词。
  • 正向索引通常是以文档的ID为关键字,表中记录文档中每个单词的位置信息。

优点

  • 结构简单:建立和维护相对简单。
  • 易于维护:当有新文档加入或旧文档删除时,索引的更新相对容易。

局限性

由于其在对包含关键字的查询中需要逐行排查,所以此种查询效率极低,只适合与数据库系统等较为简单的查询作用,并不适合庞大的搜索引擎

倒排索引

定义与结构

  • 倒排索引是一种索引方法,用于存储在全文搜索下某个单词在一个文档或一组文档中的存储位置的映射。它主要由“单词词典”和“倒排文件”两部分组成。单词词典记录了文档集合中出现过的所有单词,而倒排文件则包含了每个单词对应的倒排列表,即包含该单词的所有文档的列表及其相关信息(如文档编号、词频、位置等)。
  • 倒排索引的设计是从单词到文档的映射,即它能够快速根据单词找到包含该单词的所有文档。

查询过程

将搜索语句分为一个一个词,去词条列表中查询文档id,根据id去查询文档存入结果集

优点

  • 查询效率高:能够快速根据单词找到包含该单词的所有文档,适用于全文搜索和复杂查询。
  • 相关性分析:可以基于词频、文档频率等信息进行相关性分析,提高检索结果的准确性。

局限性

  • 存储需求大:对于大规模数据集,倒排索引可能需要大量的存储空间。
  • 更新复杂:当文档集合发生变化时,倒排索引的更新相对复杂。

此种索引由于其复杂查询的高效性与准确性,所以可以用于搜索引擎

区别

倒排索引(Inverted Index) 正向索引(Forward Index)
定义与结构 从单词到文档的映射 从文档到单词的映射
构建过程 文本分词、建立索引、存储索引 以文档ID为关键字,记录文档中每个单词的位置信息
优点 查询效率高、支持相关性分析 结构简单、易于维护
局限性 存储需求大、更新复杂 查询效率低、实用性有限
应用场景 搜索引擎、数据库系统、信息检索系统 文档内容稳定且需要频繁更新文档的场景

IK分词器

作用

顾名思义,作用就是对一大段话进行分词,例如创建倒排索引时,对文档分词,用户搜索时,对输入的内容分词,由于其是通过遍历一个语句中的任意相邻组合结果与自己的词库中对比,从而达到的分词效果,所以对一些新词需要自己通过配置加入到词库中

分词算法

  • ik_smartik_smart为最少切分,即做最粗粒度的拆分,已被分出的词语将不会再次被其它词语占有
  • ik_max_word:ik_max_word为最细粒度划分,将文本做最细粒度的拆分,尽可能多的拆分出词语。

扩展词条

利用config目录中的IKAnalyzer.cfg.xml文件添加拓展词典,在词典中添加拓展词条

相关概念

MySql Elasticsearch 说明
Table Index 索引(Index),就是文档的集合,类似数据库的表(Table)
Row Document 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是Json格式
Column Filed 字段(Filed),就是Json文档中的字段,类似数据库中的列(Column)
Schema Mapping Mapping(映射)时索引中文档的约束,例如字段类型的约束。类似数据库的表结构(Schema)
Sql DSL DSL是Elasticsearch提供的Json风格的请求语句,用来定义搜索条件

Mapping映射属性

mapping是索引库中对文档的约束,常见的mapping属性包括:

type:字段数据类型,常见的简单类型有

  • 字符串:text(可分词的文本,一般为较长的文本),keyword(精确值,例如:品牌,国家,ip地址,一般不可分词,因为此种类型分此后便失去了本来的含义)
  • 数值:long,integer,short,byte,double,float
  • 布尔:boolean
  • 日期:date
  • 对象:object

index:是否创建索引,默认为true,为true即代表会为这个字段创建倒排索引,将来可以根据这个字段去做搜索排序

analyzer:使用哪种分词器

properties:该字段的子字段,例如在json格式中嵌套了另一个json格式

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
Put /heima
{
"mapping":{
"properties":{
"info":{
"type":"text",
"analyzer":"ik_smart"
},
"age":{
"type":"byte"
},
"email":{
"type":"keyword",
"index":false
},
"name":{
"type":"object",
"properties":{
"firstName":{
"type":"keyword"
},
"lastName":{
"type":"keyword"
}
}
}
}
}
}

文档CRUD

  • 创建文档:POST/索引库名/_doc/文档id{Json文档}
  • 查询文档:GET/索引库名/_doc/文档id
  • 删除文档:DELETE/索引库名/_doc/文档id
  • 修改文档:
    • 全量修改:PUT/索引库名/_doc/文档id{Json文档}全量修改的原理是删掉旧的文档,添加为这个文档,所以如果id值不存在的情况下,其作用等同于增加,注意:如果是修改,Json文档中的字段要写全
    • 增量修改:POST/索引库名/_update/文档id{"doc":{字段}}增量修改中,只需要写你需要修改的字段即可

批量处理

1
2
3
4
5
6
7
8
9
POST /_bulk
{"index":{"_index":"索引库名","_id":"新增id"}}
{"字段1":"值1","字段2":"值2"}
//这两行为新增,第一行为指定索引库以及数据id,第二行是具体数据,在批量处理中,可以同时执行多个
{"delete":{"_index":"索引库名","_id":"删除id"}}
//这一行为删除,只需指定删除的索引库名以及具体id即可
{"update":{"_index":"索引库名","_id":"更新id"}}
{"doc":{"字段1":"值1"}}
//这两行为修改,第一行指定索引库名和id,第二行是修改的具体内容

客户端JavaClient

索引库操作

客户端初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ElasticTest{
private RestHighLevelClient client;

@BeforeEach
void setUp(){
client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.150.101:9200")
));
}

@AfterEach
void tearDown() throw IOException {
if(client != null){
client.close();
}
}
}

添加索引库

1
2
3
4
5
6
7
8
9
@Test
void testCreateHotelIndex()throws IOException{
//1.创建request对象
CreteIndexRequest request = new CreteIndexRequest("索引库名");
//2.请求参数,MAPPING_TEMPLATE是静态常量字符串,内容是JSON格式请求体
request.source(MAPPING_TEMPLATE,XContentType.JSON);
//3.发起请求
client.indics.create(request,RequestOptions.DEFAULT);
}

删除索引库

1
2
3
4
5
6
7
@Test
void testCreateHotelIndex () throws IOException {
//1.创建Request对象
CreteIndexRequest request = new CreteIndexRequest("索引库名");
//2.发起请求
client.indics.delete(request,RequestOptions.DEFAULT);
}

查询索引库

1
2
3
4
5
6
7
@Test
void testCreateHotelIndex () throws IOException {
//1.创建Request对象
CreteIndexRequest request = new CreteIndexRequest("索引库名");
//2.发起请求
client.indics.get(request,RequestOptions.DEFAULT);
}

索引库一般不支持修改已有的字段,但支持修改未有的字段,换句话说,就是支持增加新的字段

文档操作

新增文档

1
2
3
4
5
6
7
8
9
@Test
void testIndexDocument () throws IOException {
//1.创建request对象
IndexRequest request = new IndexRequest("索引库名").id("新增id");
//2.准备Json文档
request.source(JSONUtil.toJsonStr(item),XContentType.JSON);
//3.发送请求
client.index(request,RequestOptions.DEFAULT);
}

删除文档

1
2
3
4
5
6
7
@Test
void testIndexDocument () throws IOException {
//1.创建request对象
DeleteRequest request = new DeleteRequest("索引库名","删除id");
//2.发送请求
client.delete(request,RequestOptions.DEFAULT);
}

查询文档

1
2
3
4
5
6
7
8
9
10
11
@Test
void testIndexDocument () throws IOException {
//1.创建request对象
GetRequest request = new GetRequest("索引库名","查询id");
//2.发送请求,得到结果
GetReponse response = client.get(request,RequestOptions.DEFAULT);
//3.解析结果
String json = response.getSourceAsString();
Item item = JSONUtil.toBean(json,Item.class);
System.out.println(item);
}

修改文档

  • 全量更新:与新增文档的API完全相同,只不过第一次使用如果没有id,就是新增,再次使用就是修改,注意要传入完整的参数

  • 局部更新:只更新指定字段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Test
    void testIndexDocument () throws IOException {
    //1.创建Request对象
    UpdateRequest request = new UpdateRequest("索引库名","修改id");
    //2.准备参数
    request.doc(
    "age",18,
    "name","Rose"
    );
    //3.更新文档
    client.update(request,RequestOptions.DEFAULT);
    }

批量处理

1
2
3
4
5
6
7
8
9
10
@Test
void testBulk () throws IOException{
//1.创建Bulk请求
BulkRequest request = new BulkRequest();
//2.添加要批量提交的请求
request.add(new IndexRequest("索引库名").id("添加id").source("json source",XContentType.JSON));
request.add(new IndexRequest("索引库名").id("添加id").source("json source",XContentType.JSON));
//3.发起bulkqingq
client.bulk(request,RequestOptions.DEFAULT);
}

DSL查询

由于以上的查询,都只能通过id查询,但是搜索引擎中一定要具备根据复杂条件查询的功能,所以DSL查询可以达到这种功能

基本语法

1
2
3
4
5
6
7
8
GET /索引库名/_search
{
"query":{
"查询类型":{
"查询条件":"条件值"
}
}
}

叶子查询

全文检索查询:利用分词器对用户输入内容分词,然后去词条列表中匹配,拿到相应的文档id,最后根据id查到文档。例如:match_querymulti_match_query

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET /索引库名/_search
{
"query":{
"match":{
"FILED":"TEXT"
}
}
}
//multi_match允许同时查询多个字段
GET /索引库名/_search
{
"query":{
"multi_match":{
"query":"TEXT",
"fields":["FIELD1","FIELD2"]
}
}
}

精确查询:不对用户输入的内容分词,直接精确匹配,一般是查找keyword,数值,日期,布尔等类型。例如:ids,range,term

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//term查询
GET /索引库名/_search
{
"query":{
"term":{
"FIELD":{
"value":"VALUE"
}
}
}
}
//range查询
GET /索引库名/_search
{
"query":{
"range":{
"FIELD":{
"gte":10,
"lte":20
}
}
}
}

地理查询:用于搜索地理位置,搜索方式有很多,例如:geo_distance,gep_bouding_box

复合查询

由多个叶子查询组成的称为符合查询,分为两类,一类是基于逻辑运算组合叶子查询,实现组合条件,例如布尔查询

另一类是基于某种算法修改查询时的文档相关性算分,从而改变文档排名

布尔查询是一个或多个查询子句的组合。子查询的组合方式有:

  • must:必须匹配每个子查询,类似”与”
  • should:选择性匹配子查询,类似”或”
  • must_not:必须不匹配,不参与算分,类似”非”
  • filter:必须匹配,不参与算分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Get /索引库名/_search
{
"query":{
"bool":{
"must":[
{"match":{"name":"手机"}}
],
"should":[
{"term":{"brand":{"value":"vivo"}}},
{"term":{"brand":{"value":"小米"}}}
],
"must_not":[
{"range":{"price":{"gte":2500}}}
],
"filter":[
{"range":{"price":{"lte":1000}}}
]
}
}
}

排序与分页

默认根据相关性算分来排序,也可以指定字段排序。可以排序字段类型有:keyword类型,数值类型,地理坐标类型,日期类型等

1
2
3
4
5
6
7
8
9
10
11
GET /索引库名/_search
{
"query":{
"match":{...}
},
"sort":[
{
"FIELD":"desc"
}
]
}

ES默认情况下只返回top10的数据。如果想要查询更多数据就需要修改分页参数。ES通过修改fromsize参数来控制要返回的分页结果,from时从第几个文档开始,size:总共查询几个文档

1
2
3
4
5
6
7
8
9
10
11
Get/索引库名/_search
{
"query":{
"match":{...}
},
"from":0,
"size":10,
"sort":[
{"price":"asc"}
]
}

深度分页问题

例如我要查第一千页的数据,每页有十条,那也就是第9990条数据到第一万条数据,然后我实际需要查到前一万条数据,再从其中选出需要的数据,在分片集群中,我就需要在每一个分片中查一万条数据汇总取出前一万,以此类推,随着查询页数的增加,我要在每个分片中查到的数据增多,最后又可能造成爆内存的现象,这就是深度分页问题

解决方案 search after

search_after是一种基于游标的分页机制,它允许你指定从某个特定的文档之后开始检索数据,从而避免了传统的分页方式中需要跳过大量数据的问题。具体来说,search_after需要你在首次查询时指定排序字段和排序方向,并在查询结果中获取最后一个文档的排序字段值。在后续的查询中,你可以将这个值作为search_after参数传递给Elasticsearch,以从该文档之后开始检索数据。避免从零开始获取from + size条数据的低效方式。相反,search_after允许我们根据上一次查询结果中最后一个文档的排序字段值(即游标)来继续向后获取数据段。

  • 优点:显著提高查询性能,支持深度分页
  • 缺点:只能向后逐页查询,不能随即分页

高亮显示

要想使查询关键词在文档中高亮,我们需要在文档中拿到所有关键词,在html页面中为其加入一个统一的标签,在css样式中为这种标签加入同意的样式就可以实现高亮显示了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET /索引库名/_search
{
"query":{
"match":{
"FIELD":"TEXT"
}
},
"highlight":{
"fields":{ //指定要高亮的字段
"FIELD":{
"pre_tags":"<em>", //高亮的前置标签
"post_tags":"</em>" //高亮的后置标签
}
}
}
}

JavaClient查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
void testMAtchAll () throws IOException{
//1.准备request
SearchRequest request = new SearchRequest("索引库名");
//2.组织DSL参数
request.source()
.query(QueryBuilders.matchAllQuery());
//3.发送请求,得到响应结果
SearchResponse response = client.search(request,RequestOptions.DEFAULT);
//4.解析结果
SearchHits searchHits = response.getHits();
//4.1查询的总条数
Long total = searchHits.getTotalHits().value;
//4.2查询的结果数组
SearchHits[] hits = searchHits.getHits();
for(SearchHit : hits){
String json = hit.getSourceAsString();
Item item = JSONUtil.toBean(json,Item.class);
}
}

复杂查询以及分页排序高亮显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//组织DSL参数
request.source().query(QueryBuilders.boolQuery
.must(QueryBuilders.matchQuery("name","脱脂牛奶"))
.filter(QueryBuilders.termQuery("brand","德亚"))
.filter(QueryBuilders.rangeQuery("price").lt(30000))
);
//分页
request.source.from(0).size(5);
//排序
request.source.sort("price",SortOrder.ASC);
//高亮显示
request.source.highlighter(
SearchSourceBuilder.highlight()
.filed("name")
.preTags("<em>")
.postTags("</em>")
);

数据聚合

聚合可以实现对文档数据的统计,分析,运算。聚合常见的有三类

  • 桶(Bucket)聚合:用来对文档做分组

    TermAggregation:按照文档字段值分组

    Date Histogram:按照日期阶梯分组,例如一周为一组,一月为一组

  • 度量(Metric)聚合:用于计算一些值,比如:最大值,最小值,平均值等

    Avg:求平均值

    Max:求最大值

    Min:求最小值

    Stats:同时求maxminavgsum

  • 管道(popeline)聚合:其他聚合的结果为基础做聚合

DSL聚合

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
//普通桶聚合
GET /索引库名/_search
{
"query":{"match_all":{}}, //可以省略
"size": 0, //设置size为0,结果中不包含文档,只包含聚合结果
"aggs":{ //定义聚合
"cateAgg":{ //给聚合起个名字
"terms" :{ //聚合的类型,按照品牌值聚合,所以选term
"field":"category", //参与聚合的字段
"size":20 //希望获取的聚合结果数量
}
}
}
}
//度量聚合
GET /索引库名/_search
{
"query":{
"trem":{"category":"手机"}
},
"size": 0,
"aggs":{
"brand_agg":{
"terms":{
"field":"brand"
},
"aggs":{ //做度量聚合,要在桶聚合内在嵌套一个聚合
"price_stats":{
"stats":{
"field":"price"
}
}
}
}
}
}

Java客户端数据聚合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
request.source.query(QueryBuilders.term("category":"手机"));
request.source.size(0);
request.source.aggregation(
AggregationBuilders
.terms("brand_agg")
.field("brand")
.size(20)
);
//解析聚合结果
Aggregations aggregations = response.getAggregations();
//根据名称获取聚合结果
Terms brandTerms = aggregations.get("brand_agg");
//获取桶
List<? extends Term.Bucket> buckets = brandTerms.getBuckets();
//遍历
for(Terms.Bucket bucket : buckets){
//获取key,也就是品牌信息
String brandName = bucket.getKeyAsString();
System.out.println(brandName);
}