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 進階,實現中文、拼音分詞,繁簡體轉換高級搜索

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