目錄
話不多說,直接開幹。
一、使用版本介紹
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=公二
分詞高亮顯示,完成!!!
網上一直查閱資料,都各有依據,上面源碼部分裏面也有大多內容沒理清楚,也是個人四處搜尋碰壁查找理解的,歡迎對這方面有研究的夥伴們留下有幫助的文章鏈接,一起探討。
參看借鑑文章: