Elasticsearch整合Springboot实现基本的全文检索

本教程仅做个人工作笔记,可能不适用于他人的工作/学习

写在前面

实际生产中,除了考虑产品的性能跟用户体验之外,生产成本也是要考虑的,如果系统是小系统,想对某一个表(200w)做全文检索的话,可以考虑使用mysql自带的full_text索引,没必要使用elasticsearch,毕竟开发相关功能、维护还有服务器的费用,都是相当可观的一笔支出

以下是在mysql中的测试,数据是260w左右

-- 无设置索引 260万条数据查询大概4秒/新增索引之后消耗0.01秒
SELECT * FROM full_text WHERE title = '中国药学杂志'
-- 设置索引后模糊查询还是消耗4秒多,说明百分号前置的情况下索引不生效
SELECT * FROM full_text WHERE title LIKE '%中国药学杂志'
-- 如果将百分号后置,则能使用索引查询,消耗0.01秒
SELECT * FROM full_text WHERE title LIKE '中国药学杂志%'
-- 如果前后都放置%符号,也是不走索引查询的
SELECT * FROM full_text WHERE title LIKE '%中国药学杂志%'
-- 当表中有数据时,新增索引消耗了12秒
ALTER TABLE full_text ADD INDEX index_title (title)

适当的设置索引还是可以很大程度解决查询慢的问题的(前提是充分理解业务,将表设计好)

当然如果很有钱或者是想用ELK全套的,另说

Elasticsearch整合Springboot实现基本的全文检索

前期准备

  • 项目为springboot+maven
  • 正确部署可用elasticsearch服务

添加相关依赖

根据你的elasticsearch版本选择合适的maven依赖,添加到项目中,截至今天2019/12/27,maven上最新的spring-data-elasticsearch的版本是3.2.3.RELEASE,笔者使用es版本是6.6,maven依赖如下


            org.springframework.boot
            spring-boot-starter-data-elasticsearch
            2.0.0.RELEASE

创建实体类(Mappings)
Elasticsearch一个核心就是mappings,【Elasticsearch的mappings】
es的java高级客户端可以通过拼接json去创建一个mappings,代码相对繁琐一点,spring-data-elasticsearch提供了一种用注释创建mappings的方案,首先我们创建一个基本的实体类,如

public class Metadata implements Serializable {
    private static final long serialVersionUID = 1L;
    private Long recordId;
    private Integer metadataType;
    private Long bookRecNo;
    private String title;
}

稍微做一下改动

import org.springframework.data.elasticsearch.annotations.Field;

@Data
@Document(indexName = "your_index_name", type = "metadata")
public class Metadata implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @JsonSerialize(using = ToStringSerializer.class)
    private Long recordId;

    @Field(type = FieldType.Integer)
    private Integer metadataType;

    @Field(type = FieldType.Long)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long bookRecNo;

    @Field(type = FieldType.Text, analyzer = "ik_smart", fielddata = true)
    private String title;
}

解释一下

  • @Data:这个是lombok的,与es无关
  • @JsonSerialize(using = ToStringSerializer.class):Long类型数据到js渲染时会有精度缺失问题,这里是让数据在序列化的时候保持精度
  • @Document(indexName = “your_index_name”, type = “metadata”):这是索引的基本信息,索引名,索引类型
  • @Field(type = FieldType.Text, analyzer = “ik_smart”, fielddata = true):
    这个则是属于es的注解,type是es文档中字段的类型,如Long、Text、Keyword等;analyzer是指定分词器;当一个字段需要用来做聚合时,比如排序,则需要将fielddata设置为true,注意,fielddata=true时,会消耗索引内存,所以不需要将每一个的fielddata设置为true,其默认为false;另外需要注意一点,如果一个字段我们需要他做一些类似SQL中group by的操作,我们需要将其的type设置为Keyword,即@Field(type = FieldType.Keyword)

ElasticsearchRepository
创建一个接口继承ElasticsearchRepository用来对索引进行基本的数据操作,代码如下:

import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
@Component
public interface DataRepository extends ElasticsearchRepository {
}

配置文件 yml
spring-data-elasticsearch链接ES服务器的操作不用像java高级客户端那样在代码中实现,而是通过配置文件连接ES的节点,通过实体类的注解以及Repository来指定跟操作对应索引(index、type),以下是springboot项目application.yml关于ES的配置

spring:
  data:
    elasticsearch:
      cluster-name: my-es-server
      cluster-nodes: 127.0.0.1:9300
      repositories:
        enabled: true

以上工作完成后,可以通过代码对索引进行一些基本的操作了

通过代码对索引进行基本操作

建议先去官方文档大致了解一下 Document Api

对索引的操作需要用到org.springframework.data.elasticsearch.core.ElasticsearchTemplate;它可以对索引进行一些基本的操作,也可以对索引中的数据进行操作,然后对数据操作,笔者使用的是上面提到的ElasticsearchRepository

	@Autowired
    ElasticsearchTemplate template;

    @Override
    public void createIndex() {
        //直接使用带注解的实体类创建索引,会有默认的setting跟mappings
        boolean index = template.createIndex(Metadata.class);
        //使用带注解的实体类跟自定义settings设置创建索引
        boolean index1 = template.createIndex(Metadata.class, null);
        //使用带注解的实体类创建索引,会有默认的setting跟mappings
        boolean indexName = template.createIndex("indexName");
        //使用带注解的实体类跟自定义settings设置创建索引
        boolean indexName1 = template.createIndex("indexName", null);
    }

    @Override
    public void createMappings() {
        /**
         * 在创建索引后,如果没有给一个确切的mappings是可以的,但是他的字段会根据你后面给什么数据而创建默认的mappings
         */
        boolean b = template.putMapping(Metadata.class);
        boolean b1 = template.putMapping("indexName", "typeName", Metadata.class);
    }

    @Override
    public void getMappingsInfo() {
        //根据实体类获取
        Map mapping = template.getMapping(Metadata.class);
        //指定index、type获取
        Map mapping1 = template.getMapping("indexName", "indexType");
    }

    @Override
    public void getSettingsInfo() {
        //获取settings信息
        Map setting = template.getSetting(Metadata.class);
        Map indexName = template.getSetting("indexName");
    }

    @Override
    public void deleteIndex() {
        //删除索引信息
        boolean b = template.deleteIndex(Metadata.class);
        boolean indexName = template.deleteIndex("IndexName");
    }

附上一个es索引的信息

{
	"state": "open",
	"settings": {
		"index": {
			"refresh_interval": "1s",
			"number_of_shards": "5",
			"provided_name": "data_collect",
			"creation_date": "1576738381899",
			"store": {
				"type": "fs"
			},
			"number_of_replicas": "1",
			"uuid": "kPp7n73uRYWO5U30BAbIMQ",
			"version": {
				"created": "6060099"
			}
		}
	},
	"mappings": {
		"metadata": {
			"properties": {
				"recordId": {
					"type": "text",
					"fields": {
						"keyword": {
							"ignore_above": 256,
							"type": "keyword"
						}
					}
				},
				"metadataType": {
					"type": "integer"
				},
				"title": {
					"fielddata": true,
					"analyzer": "ik_smart",
					"type": "text"
				},
				"bookRecNo": {
					"type": "long"
				}
			}
		}
	},
	"aliases": [],
	"primary_terms": {
		"0": 1,
		"1": 1,
		"2": 1,
		"3": 1,
		"4": 1
	},
	"in_sync_allocations": {
		"0": [
			"0ACHmsl-SpObHXqLjLRxGw"
		],
		"1": [
			"QZch3H8ESYaPMDIWxBz3kg"
		],
		"2": [
			"KFGEePccSZ-zfKuzXLvb0w"
		],
		"3": [
			"dPTdMlgzQDaE_tUBXCtnrQ"
		],
		"4": [
			"ZYRQp9biQ-2MuDAyVeJboA"
		]
	}
}

通过代码对Elasticsearch中的文档进行基本的增删改操作

先看代码

    @Autowired
    DataRepository repository;

    @Override
    public void addDocument() {
        //单个添加
        Metadata metadata = new Metadata();
        Metadata save = repository.save(metadata);
        //批量添加
        ArrayList list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Metadata data = new Metadata();
            list.add(data);
            i = i + 1;
        }
        Iterable dataList = repository.saveAll(list);
    }

    @Override
    public void deleteDocument() {
        //删除所有
        repository.deleteAll();
        //根据id删除
        repository.deleteById(1L);
        //传输一个实体类(不为空),查询到实体类之后删除其对应的文档,底层还是根据id删除
        repository.delete(new Metadata());
        //批量删除
        ArrayList list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Metadata data = new Metadata();
            list.add(data);
            i = i + 1;
        }
        repository.deleteAll(list);
    }

修改的话,只要实体类的id不变,那么直接使用save的方法,修改后es上该文档的version会+1;

实现复杂检索

建议先去官方文档大致了解一下 Query DSL

先看一下基本的代码流程

    @Autowired
    DataRepository repository;

    @Override
    public Page getFromElastic(Map map) {
        //检索条件集合,后面需要实现的检索条件都存放在这里
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        //分页
        PageRequest pageRequest = PageRequest.of(Integer.valueOf(map.get("page").toString()),
                Integer.valueOf(map.get("limit").toString()));
        //排序
        SortBuilder sort = SortBuilders.fieldSort("publishDate").order(SortOrder.DESC);
        String sortBy = (String) map.get("sortBy");
        if (!StringUtil.isNullOrEmpty(sortBy)) {
            sort = SortBuilders.fieldSort(sortBy).unmappedType("Date");
            String sortRule = (String) map.get("sortRule");
            if (!StringUtil.isNullOrEmpty(sortRule) && "ASC".equals(sortRule)) {
                sort.order(SortOrder.ASC);
            }
        }
        //聚合
        TermsAggregationBuilder aggJobYear = AggregationBuilders
                .terms("jobYear").field("jobYear").showTermDocCountError(true);
        //整合构建
        NativeSearchQuery query = new NativeSearchQueryBuilder()
                .withQuery(boolQuery)//检索条件
                .withSort(sort)//排序条件
                .withPageable(pageRequest)//分页条件
                .addAggregation(aggJobYear)//聚合(分面即SQL的group by)条件
                .build();
        Page search = repository.search(query);
        return search;
    }

检索结果

{
	"content": [//数据省略],
	"pageable": {
		"sort": {
			"sorted": false,
			"unsorted": true,
			"empty": true
		},
		"offset": 0,
		"pageSize": 10,
		"pageNumber": 0,
		"paged": true,
		"unpaged": false
	},
	"facets": [{
		"name": "jobYear",
		"type": "term",
		"terms": [{
			"term": "2019",
			"count": 551
		}],
		"total": 1,
		"other": 0,
		"missing": 0
	}],
	"aggregations": {
		"asMap": {
			"jobYear": {
				"name": "jobYear",
				"metaData": null,
				"buckets": [{
					"docCount": 551,
					"docCountError": 0,
					"aggregations": {
						"asMap": {},
						"fragment": true
					},
					"key": 2019,
					"keyAsString": "2019",
					"keyAsNumber": 2019,
					"fragment": true
				}],
				"docCountError": 0,
				"writeableName": "lterms",
				"sumOfOtherDocCounts": 0,
				"type": "lterms",
				"fragment": true,
				"mapped": true
			}
		},
		"fragment": true
	},
	"scrollId": null,
	"maxScore": 1,
	"totalElements": 1135,
	"totalPages": 114,
	"number": 0,
	"size": 10,
	"sort": {
		"sorted": false,
		"unsorted": true,
		"empty": true
	},
	"first": true,
	"numberOfElements": 10,
	"last": false,
	"empty": false
}

大致的检索就如上面那样,关注点放在boolQuery中,其他的基本不变,但是这种检索存在问题,聚合addAggregation的问题,如果用来做分面的字段是Text类型(对应java中的String),检索结果的json序列化会有转化的问题,笔者尝试了很多方法都解决不了,网上相关的解决方案也不多,笔者的做法是将普通的检索跟分面操作分开,然后将两个结果手动封装后再返回给接口调用方;

接下来着重看一下QueryBuilders的各种操作,可以打印一下boolQuery,查看对应的DSL;比如上面的操作对应的DSL语句是:

{
  "bool" : {
    "adjust_pure_negative" : true,
    "boost" : 1.0
  }
}

ES常用检索

分页、排序不在赘述
实现类似SQL的Like查询
实现对指定字段的精确查询

你可能感兴趣的:(Elasticsearch整合Springboot实现基本的全文检索)