SpringBoot+Mybatis+Elasticsearch实现高亮分词搜索

目录

一、使用版本介绍

二、搭建项目和ES环境

1、Elasticsearch客户端搭建

2、搭建SpringBoot服务及相关依赖

3、Elasticsearch的分词搜索实战

4、搜索方法源码分析

5、分词搜索高亮实现


话不多说,直接开干。

一、使用版本介绍

springboot  :1.5.2.RELEASE

spring-boot-starter-data-elasticsearch :1.5.2.RELEASE

Elasticsearch :2.3.5

JDK :1.7

  以上解决参考下面的对应关系

Spring Boot Version (x) Spring Data Elasticsearch Version (y) Elasticsearch Version (z)
x <= 1.3.5 y <= 1.3.4 z <= 1.7.2*
x >= 1.4.x 2.0.0 <=y < 5.0.0** 2.0.0 <= z < 5.0.0**
ES          JDK
0.90        1.6
----------------
1.3         1.7
...         1.7
2.4         1.7
----------------
5.0         1.8     
...         1.8
--------------------- 

二、搭建项目和ES环境

1、Elasticsearch客户端搭建

在 Elasticsearch 官网 https://www.elastic.co/downloads/past-releases 下载对应版本的客户端。此处下载windows版本。

解压后目录结构如下:

进入bin目录, 运行启动 elasticsearch.bat 

启动完成后,在浏览器输入 http://localhost:9200/

接下来安装 ES 的WEB端 展示

通过 cmd  的 dos 命令  进入ES安装的bin目录,运行  

plugin  install  mobz/elasticsearch-head

 

(注:低版本可使用bin目录的 plugin脚本命令,高版本可能不同,如 6.X版本以上是 elasticsearch-plugin)

运行期间可能会提示错误,但也能正常访问,这里就不管跳过了。访问 http://localhost:9200/_plugin/head/ 

2、搭建SpringBoot服务及相关依赖

此处引用之前一篇 SpringBoot 整合 Mybatis 的基础上改造

引入相关依赖

		<!-- elasticsearch -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
		</dependency>

application.properties相关内容


#elasticsearch
#开启 Elasticsearch 仓库(默认值:true)
spring.data.elasticsearch.local=true
#仓库中存储数据
spring.data.elasticsearch.repositories.enabled=true
#节点名字,默认elasticsearch
spring.data.elasticsearch.cluster-name=elasticsearch-cluster
#节点地址,多个节点用逗号隔开
#默认 9300 是 Java 客户端的端口。9200 是支持 Restful HTTP 的接口
spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300
#存储索引的位置
#spring.data.elasticsearch.properties.path.home=/data/project/target/elastic
#elasticsearch日志存储目录
#spring.data.elasticsearch.properties.path.logs=/data/project/target/elastic
#elasticsearch数据存储目录
#spring.data.elasticsearch.properties.path.data=/data/project/target/elastic
#连接超时的时间
spring.data.elasticsearch.properties.transport.tcp.connect_timeout=120s

但要注意的是,如何把服务和客户端关联起来

在配置中我们配置了 节点名称 elasticsearch-cluster 和 服务IP 127.0.0.1 

需要在 ES的安装目录下的 config 进行相应的配置 ,进入目录 E:\elasticSearch\elasticsearch-2.3.5\config

在 elasticsearch.yml 中搜索修改两个配置

 cluster.name: elasticsearch-cluster
 network.host: 127.0.0.1

这里是本地搭建,IP和端口就不多说,节点名称如果不正确匹配,项目可以启动成功,但运行链接时会报错,ES的web端界面也会找不到服务。

org.elasticsearch.client.transport.NoNodeAvailableException: None of the configured nodes are available: [{#transport#-1}

其实这样就算springboot和elasticsearch整合完成了。

3、Elasticsearch的分词搜索实战

接下来是如何去使用所整合的服务,大招开启:

实体类:


@Document(indexName = "adminanswer" , type = "answer")
public class Answer {
	
	@Id
        private String id;

	@Field(type = FieldType.String)
        private String title;

        private Date createTime;

	@Field(type = FieldType.String)
        private String content;
    

        public Answer(){
    	
        }
    
        public Answer(String id, String title, String content,  Date createTime) {
		super();
		this.id = id;
		this.title = title;
		this.createTime = createTime;
		this.content = content;
	}

	//以下的  get - set 就省略
}

indexName; //索引库的名称,个人建议以项目的名称命名

type  //default ""; //类型,个人建议以实体的名称命名

更多参数可参考:spring data elasticsearch的 @Documnet 和 @Field 注解

重要!重要!重要!以下是 MyBatis和ES的冲突区别,当时被这个整懵了

先来个引用ES服务的 ElasticsearchRepository 接口

public interface AnswerElasticsearchMapper extends ElasticsearchRepository<Answer, String>{

}

这样我们就可以使用ES的服务,但是,链接的是ES的服务,数据库我们用的是 MyBatis

所以还要新建一个 mapper接口

@Mapper
public interface AnswerMapper {
	
    int insert(Answer record);

    int insertSelective(Answer record);
    
    //使用 mybatis - generator 工具生成,多余方法就省略
}

因为ElasticsearchRepository 和 Mybatis 使用 @Mapper 接口在使用的时候有冲突,所以是没法放在同一个类里面的,要分开写

将数据库数据和ES库同步,网上推荐的方法是使用  logstash-input-jdbc ,这里就不采用了,需要的自行查阅,我们使用简单的方式来实现。

定义实现类接口

public interface AnswerService {

	/** * 添加问答信息 * @param adminUser */
	public void addAnswer(Answer answer);
	
	/** * 根据标题查找问答信息 * @param title* @return */
	public List<Answer> findAnswerByTitle(String title);
	
	/** * 更新日期*  * @param date * @return */
	public long updateAllAnswerForTime();

}

实现类:

@Service
public class AnswerServiceImpl implements AnswerService {

	@Autowired
	private AnswerMapper answerMapper;
	@Autowired
	private AnswerElasticsearchMapper answerElasticsearchMapper;

	@Override
	public void addAnswer(Answer answer) {
        //注:这里没做ES和mysql数据同步操作,所以调用ES的save方法,只是在ES库中添加
        //为了完成两边数据同步,所以同时引用插入,实际开发不推荐这样写
		answerMapper.insertSelective(answer);
		answerElasticsearchMapper.save(answer);
	}

    //对title和content进行分词查询
	@Override
	public List<Answer> findAnswerByTitle(String title) {

		// 构建查询内容
		QueryStringQueryBuilder queryBuilder = new QueryStringQueryBuilder(title);
		// 查询的字段
		queryBuilder.field("title").field("content");
		Iterable<Answer> searchResult = answerElasticsearchMapper.search(queryBuilder);
		Iterator<Answer> iterator = searchResult.iterator();
		List<Answer> list = new ArrayList<Answer>();
		while (iterator.hasNext()) {
			list.add(iterator.next());
		}
		return list;
	}

	@Override
	public long updateAllAnswerForTime() {

		List<Answer> answerList = answerMapper.findAnswerAll();
		for (Answer answer : answerList) {
			answer.setCreateTime(new Date());
			answerElasticsearchMapper.save(answer);
		}
		return 1;
	}
}

写下控制器

@RestController
@RequestMapping(value = "/answer")
public class AnswerController extends BaseController {

	@Autowired
	private AnswerServiceImpl answerServiceImpl;

	/**添加问答信息 */
	@RequestMapping(value = "/addAnswer", method = RequestMethod.POST)
	public Message addAnswer(String title, String content) {

		Answer answer1 = new Answer(UUID.randomUUID().toString(), "测试标题一", "减肥速度快了福建省快递费到付件水电费", new Date());
		answerServiceImpl.addAnswer(answer1);

		return new Message(SystemCodeAndMsg.SUCCESS);
	}

	/**查找问答信息*/
	@RequestMapping(value = "/findAnswerByTitle", method = RequestMethod.GET)
	public Message findAnswerByTitle(String title) {

		List<Answer> answerList = answerServiceImpl.findAnswerByTitle(title);
		return new Message(SystemCodeAndMsg.SUCCESS,answerList);
	}

	/** 修改问答时间*/
	@RequestMapping(value = "/updateAnswerTime", method = RequestMethod.POST)
	public Message updateAnswerTime() {

		answerServiceImpl.updateAllAnswerForTime();
		return new Message(SystemCodeAndMsg.SUCCESS);
	}

}

 启动项目,用postman 访问 http://172.16.60.187:8081/answer/findAnswerByTitle?title=公二

你没看错哦,这样完成了对title和content分词查询,简单吧!!!

4、搜索方法源码分析

但我们要的是分词高亮显示,上面的需求又满足不了我的需求,ElasticsearchRepository提供的方法都是封装好的,没看到可以调用的,但网上都有别的实现方式,那我们就看下 search() 方法的源码实现。(注:不想看源码分析过程的,可以直接往后面跳,直接看方法实现)

AbstractElasticsearchRepository 抽象实现 ElasticsearchRepository

接着看 ElasticsearchTemplate 实现类

先看下 doSearch 方法

 可以实现类中看到如果有高亮字段,则添加高亮字段(但只是添加,没有渲染任何额外属性) 

我们再看下输入的 mapper.mapResults 实现

mapResults的实现 

 既然知道了在哪里可以更改实现我们想要的效果,那就开始动手↓↓↓↓↓↓↓↓↓↓↓↓↓↓

5、分词搜索高亮实现

因为ElasticsearchRepository提供的实现接口是封装好的,那我们就把接口的实现单独抽出来

@Autowired
private ElasticsearchTemplate elasticsearchTemplate;

重写下之前的 findAnswerByTitle 实现接口,主要是重写 SearchResultMapper的mapResults方法,修改如下

@Override
public List<Answer> findAnswerByTitle(String title) {

	// 定义高亮字段
	Field titleField = new HighlightBuilder.Field("title").preTags("<span>").postTags("</span>");
	Field contentField = new HighlightBuilder.Field("content").preTags("<span>").postTags("</span>");

	// 构建查询内容
	QueryStringQueryBuilder queryBuilder = new QueryStringQueryBuilder(title);
	// 查询匹配的字段
	queryBuilder.field("title").field("content");

	SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(queryBuilder)
			.withHighlightFields(titleField, contentField).build();
	long count = elasticsearchTemplate.count(searchQuery, Answer.class);
	System.out.println("系统查询个数:--》" + count);
	if (count == 0) {
		return new ArrayList<>();
	}
	//需要的话可以实现分页效果,注意,页面是从 0 开始
	searchQuery.setPageable(new PageRequest(0, (int) count));

	AggregatedPage<Answer> queryForPage = elasticsearchTemplate.queryForPage(searchQuery, Answer.class,
			new SearchResultMapper() {

				@Override
				public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz,
						Pageable pageable) {

					List<Answer> list = new ArrayList<Answer>();
					for (SearchHit searchHit : response.getHits()) {
						if (response.getHits().getHits().length <= 0) {
							return null;
						}
						Answer answer = JSONObject.parseObject(searchHit.getSourceAsString(), Answer.class);
						Map<String, HighlightField> highlightFields = searchHit.getHighlightFields();
						//匹配到的title字段里面的信息
						HighlightField titleHighlight = highlightFields.get("title");
						if (titleHighlight != null) {
							Text[] fragments = titleHighlight.fragments();
							String fragmentString = fragments[0].string();
							answer.setTitle(fragmentString);
						}
						//匹配到的content字段里面的信息
						HighlightField contentHighlight = highlightFields.get("content");
						if (contentHighlight != null) {
							Text[] fragments = contentHighlight.fragments();
							String fragmentString = fragments[0].string();
							answer.setContent(fragmentString);
						}
						list.add(answer);

					}
					if (list.size() > 0) {
						return new AggregatedPageImpl<T>((List<T>) list);
					}
					return null;
				}
			});
	List<Answer> list = queryForPage.getContent();

	return list;
}

别看内容很长,已经做好分隔,一段一段的看,你就知道其实没什么内容,基本都是从源码刚刚讲解的位置搬出来的。

修改好后,我们再来试试效果。访问: http://172.16.60.187:8081/answer/findAnswerByTitle?title=公二

分词高亮显示,完成!!!

网上一直查阅资料,都各有依据,上面源码部分里面也有大多内容没理清楚,也是个人四处搜寻碰壁查找理解的,欢迎对这方面有研究的伙伴们留下有帮助的文章链接,一起探讨。

参看借鉴文章:

Elasticsearch&JDK版本要求

springboot elasticsearch 集成注意事项

同步mysql数据到ElasticSearch的最佳实践

SpringBoot集成Elasticsearch 进阶,实现中文、拼音分词,繁简体转换高级搜索

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章