【搜索引擎】淺談ElasticSearch在項目中的應用

  ElasticSearch(簡稱ES)是一個基於Lucene的搜索服務器。它提供了一個分佈式多用戶能力的全文搜索引擎,基於RESTful web接口。它也是目前最受歡迎的企業搜索引擎,其次是Apache Solr,也是基於Lucene。
  安裝ES、分詞插件IK、圖形插件Head,這些博主這裏就不介紹了。
  在講代碼之前,博主先說一下我對索引的理解,比如有個題目:背誦《憫農》,我們立刻就能背誦出來“鋤禾日當午……”,這是因爲我們的腦海裏面有“憫農=>鋤禾日當午”這個索引關係。但如果把題目改成:背誦含有“午”的詩句。那麼恐怕我們都要想一會才能背誦出來,這是因爲我們的腦海裏面沒有有“午=>鋤禾日當午”這個索引關係。如果我們能夠在腦海裏面建立“鋤=>憫農=>鋤禾日當午”、“禾=>憫農=>鋤禾日當午”、“日=>憫農=>鋤禾日當午”、“當=>憫農=>鋤禾日當午”、“午=>憫農=>鋤禾日當午”這些索引關係,那麼是不是無論題目怎麼問,我們都可以很快就能背誦出來了?
  ES中建立索引的過程肯定沒這麼簡單,我也沒具體研究過。還有就是,我們可以把ES中的“索引 => 類型 => 文檔 => 字段”想象成關係型數據庫中的“數據庫 => 表 => 行 => 列”,這樣看代碼的時候就比較容易理解了。
  先說明一下,SpringBoot繼承ES有兩種方式,第一種寫一個Repository實現類,繼承 ElasticsearchRepository ;第二種是用ElasticsearchTemplate,這個類是SpringBoot給封裝的,可以直接在 Service或者Controller層 中引入該 bean。這裏我們講第二種。
  博主是做java後臺開發的,平時不怎麼寫Web端代碼,所以這篇博客的代碼也是博主照着“java知識分享網”上面的一個百度雲資源分享論壇來敲的,也算是“現炒現賣”,和大家一起學習。
  第一步,要在pom.xml文件裏面添加ES依賴,如下。

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

  第二步,在application.yml文件中添加ES配置信息,如下。

spring:
  data:
    elasticsearch:
      #集羣名稱,默認爲elasticsearh
      cluster-name: elastic 
      #ES節點信息,逗號分隔
      cluster-nodes: 10.4.1.88:9300,10.4.1.89:9300

  第三步,寫一個ArticleInfo實體類,注意不要忘了寫@Document註解。

package com.zznode.entity.es;

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

import java.io.Serializable;

/**
 * 用於ES的帖子實體類
 * 
 * @author Administrator
 *
 */
@Document(indexName = "test2", type = "my")
public class ArticleInfo implements Serializable {

	private Long id;
	private String name;
	private String content;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getContent() {
		return content;
	}

	public void setContent(String content) {
		this.content = content;
	}
}

  第四步,編寫ES的Service層代碼,如下,這是一個百度雲資源分享論壇的Service層代碼,其中,search方法是根據關鍵字分頁查詢ES中的信息,並高亮顯示;searchNoHighLight方法是根據關鍵字分頁查詢ES中的信息,不高亮顯示;searchCount方法是根據關鍵字,查詢ES中符合條件信息的條數;deleteIndex方法是根據文檔id來刪除ES中對應的信息。

package com.zznode.service.impl;

import com.zznode.entity.Article;
import com.zznode.entity.es.ArticleInfo;
import com.zznode.repository.ArticleRepository;
import com.zznode.service.ArticleService;
import com.zznode.util.StringUtil;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.SearchResultMapper;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
import org.springframework.data.elasticsearch.core.query.DeleteQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 帖子Service實現類
 * 
 * @author Administrator
 *
 */
@Service("articleService")
public class ArticleServiceImpl implements ArticleService {

	@Autowired
	private ArticleRepository articleRepository;

	@Autowired
	private ElasticsearchTemplate elasticsearchTemplate;

	/**
	 * 分頁查詢(帶排序)所有帖子內容
	 * 
	 * @param page
	 * @param pageSize
	 * @param direction
	 * @param properties
	 * @return
	 */
	@Override
	public List<Article> list(Integer page, Integer pageSize, Sort.Direction direction, String... properties) {
		PageRequest pageRequest = PageRequest.of(page - 1, pageSize, direction, properties);
		Page<Article> pageArticle = articleRepository.findAll(new Specification<Article>() {
			@Override
			public Predicate toPredicate(Root<Article> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) {
				Predicate predicate = cb.conjunction();
				predicate.getExpressions().add(cb.equal(root.get("state"), 1));
				predicate.getExpressions().add(cb.equal(root.get("isIndex"), true));
				return predicate;
			}
		}, pageRequest);
		return pageArticle.getContent();
	}

	/**
	 * 根據條件分頁查詢(帶排序)所有帖子內容
	 * 
	 * @param s_article
	 * @param page
	 * @param pageSize
	 * @param direction
	 * @param properties
	 * @return
	 */
	@Override
	public List<Article> adminList(Article s_article, Integer page, Integer pageSize, Sort.Direction direction,
			String... properties) {
		PageRequest pageRequest = PageRequest.of(page - 1, pageSize, direction, properties);
		Page<Article> pageArticle = articleRepository.findAll(new Specification<Article>() {
			@Override
			public Predicate toPredicate(Root<Article> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) {
				Predicate predicate = cb.conjunction();
				if (s_article != null) {
					if (s_article.getId() != null) {
						predicate.getExpressions().add(cb.equal(root.get("id"), s_article.getId()));
					}
					if (StringUtil.isNotEmpty(s_article.getName())) {
						predicate.getExpressions()
								.add(cb.like(root.get("name"), "%" + s_article.getName().trim() + "%"));
					}
				}
				return predicate;
			}
		}, pageRequest);
		return pageArticle.getContent();
	}

	/**
	 * 分頁查詢(帶排序)所有帖子的條數
	 * 
	 * @return
	 */
	@Override
	public Long getCount() {
		return articleRepository.count(new Specification<Article>() {
			@Override
			public Predicate toPredicate(Root<Article> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) {
				Predicate predicate = cb.conjunction();
				predicate.getExpressions().add(cb.equal(root.get("state"), 1)); // 資源解析成功
				predicate.getExpressions().add(cb.equal(root.get("isIndex"), true)); // es索引添加成功
				return predicate;
			}
		});
	}

	/**
	 * 根據條件分頁查詢(帶排序)所有帖子的條數
	 * 
	 * @param s_article
	 * @return
	 */
	@Override
	public Long getAdminCount(Article s_article) {
		return articleRepository.count(new Specification<Article>() {
			@Override
			public Predicate toPredicate(Root<Article> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) {
				Predicate predicate = cb.conjunction();
				if (s_article != null) {
					if (s_article.getId() != null) {
						predicate.getExpressions().add(cb.equal(root.get("id"), s_article.getId()));
					}
					if (StringUtil.isNotEmpty(s_article.getName())) {
						predicate.getExpressions()
								.add(cb.like(root.get("name"), "%" + s_article.getName().trim() + "%"));
					}
				}
				return predicate;
			}
		});
	}

	/**
	 * 根據帖子id查詢帖子實體
	 * 
	 * @param id
	 * @return
	 */
	@Override
	public Article get(Integer id) {
		return articleRepository.getOne(id);
	}

	/**
	 * 添加帖子
	 * 
	 * @param article
	 */
	@Override
	public void save(Article article) {
		articleRepository.save(article);
	}

	/**
	 * 根據帖子id刪除帖子實體
	 * 
	 * @param id
	 */
	@Override
	public void delete(Integer id) {
		Article article = articleRepository.getOne(id);
		articleRepository.delete(article);
	}

	/**
	 * 根據關鍵字分頁查詢ES中所有帖子的信息,並高亮顯示
	 * 
	 * @param page
	 * @param pageSize
	 * @param searchContent
	 * @return
	 */
	@Override
	public List<ArticleInfo> search(Integer page, Integer pageSize, String searchContent) {

		// 根據關鍵字分頁查詢ES中所有帖子的信息
		PageRequest pageRequest = PageRequest.of(page - 1, pageSize);
		BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
				.should(QueryBuilders.matchQuery("name", searchContent))
				.should(QueryBuilders.matchQuery("content", searchContent));

		// 對content和name中的關鍵字進行高亮顯示
		NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder)
				.withPageable(pageRequest).withIndices("test2")
				.withHighlightFields(new HighlightBuilder.Field("content"), new HighlightBuilder.Field("name"))
				.withHighlightBuilder(new HighlightBuilder().preTags("<font style='color:red'>").postTags("</font>"))
				.build();

		AggregatedPage<ArticleInfo> articleInfos = elasticsearchTemplate.queryForPage(nativeSearchQuery,
				ArticleInfo.class, new SearchResultMapper() {
					@Override
					public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass,
							Pageable pageable) {
						ArrayList<ArticleInfo> articleInfos = new ArrayList<>();
						SearchHits hits = searchResponse.getHits();
						for (SearchHit hit : hits) {
							if (hits.getHits().length <= 0) {
								return null;
							}
							Map<String, Object> sourceAsMap = hit.getSourceAsMap();
							String name = (String) sourceAsMap.get("name");
							String content = (String) sourceAsMap.get("content");
							String id = (String) sourceAsMap.get("id");
							ArticleInfo articleInfo = new ArticleInfo();
							articleInfo.setId(Long.valueOf(id));
							HighlightField contentHighlightField = hit.getHighlightFields().get("content");
							if (contentHighlightField == null) {
								articleInfo.setContent(content);
							} else {
								String highlightContent = hit.getHighlightFields().get("content").fragments()[0]
										.toString();
								// 過濾掉換行符、空格、下劃線
								articleInfo.setContent(highlightContent.replaceAll("br", "").replaceAll("&nbsp;", "")
										.replaceAll("_", ""));
							}
							HighlightField nameHighlightField = hit.getHighlightFields().get("name");
							if (nameHighlightField == null) {
								articleInfo.setName(name);
							} else {
								articleInfo.setName(hit.getHighlightFields().get("name").fragments()[0].toString());
							}
							articleInfos.add(articleInfo);
						}

						if (articleInfos.size() > 0) {
							return new AggregatedPageImpl<T>((List<T>) articleInfos);
						}
						return null;
					}
				});
		return articleInfos == null ? null : articleInfos.getContent();
	}

	/**
	 * 根據關鍵字分頁查詢ES中所有帖子的信息,不高亮顯示
	 * 
	 * @param page
	 * @param pageSize
	 * @param searchContent
	 * @return
	 */
	@Override
	public List<ArticleInfo> searchNoHighLight(Integer page, Integer pageSize, String searchContent) {
		PageRequest pageRequest = PageRequest.of(page - 1, pageSize);
		BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
				.should(QueryBuilders.matchQuery("name", searchContent))
				.should(QueryBuilders.matchQuery("content", searchContent));

		NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder)
				.withPageable(pageRequest).withIndices("test2").build();

		AggregatedPage<ArticleInfo> articleInfos = elasticsearchTemplate.queryForPage(nativeSearchQuery,
				ArticleInfo.class, new SearchResultMapper() {
					@Override
					public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass,
							Pageable pageable) {
						ArrayList<ArticleInfo> articleInfos = new ArrayList<>();
						SearchHits hits = searchResponse.getHits();
						for (SearchHit hit : hits) {
							if (hits.getHits().length <= 0) {
								return null;
							}
							Map<String, Object> sourceAsMap = hit.getSourceAsMap();
							String name = (String) sourceAsMap.get("name");
							String content = (String) sourceAsMap.get("content");
							String id = (String) sourceAsMap.get("id");
							ArticleInfo articleInfo = new ArticleInfo();
							articleInfo.setId(Long.valueOf(id));
							articleInfo.setName(name);
							articleInfo.setContent(content);
							articleInfos.add(articleInfo);
						}

						if (articleInfos.size() > 0) {
							return new AggregatedPageImpl<T>((List<T>) articleInfos);
						}
						return null;
					}
				});
		return articleInfos == null ? null : articleInfos.getContent();
	}

	/**
	 * 根據條件查詢ES中帖子的條數
	 * 
	 * @param searchContent
	 * @return
	 */
	@Override
	public Long searchCount(String searchContent) {
		BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
				.should(QueryBuilders.matchQuery("name", searchContent))
				.should(QueryBuilders.matchQuery("content", searchContent));

		NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder)
				.withIndices("test2").build();

		return elasticsearchTemplate.count(nativeSearchQuery);
	}

	/**
	 * 根據帖子id刪除ES中的帖子記錄
	 * 
	 * @param id
	 */
	@Override
	public void deleteIndex(String id) {
		QueryBuilder queryBuilder = QueryBuilders.termQuery("id", id);
		DeleteQuery deleteQuery = new DeleteQuery();
		deleteQuery.setIndex("test2");
		deleteQuery.setType("my");
		deleteQuery.setQuery(queryBuilder);
		elasticsearchTemplate.delete(deleteQuery);
	}
}

  下面,我們看一個controller是怎麼使用的,先看一個search的例子,這個方法是根據關鍵字分頁查詢ES中的信息,並且關鍵字高亮顯示。

	@RequestMapping("/search")
	public ModelAndView search(@RequestParam(value = "q", required = false) String q,
			@RequestParam(value = "page", required = false) String page) throws Exception {
		ModelAndView mav = new ModelAndView();
		if (StringUtil.isEmpty(q)) {
			mav.setViewName("index");
			mav.addObject("title", "首頁");
			return mav;
		}
		int pageSize = 10;
		if (StringUtil.isEmpty(page)) {
			page = "1";
		}
		mav.addObject("q", q);
		List<ArticleInfo> articleInfoList = articleService.search(Integer.parseInt(page), pageSize, q);
		Long total = articleService.searchCount(q);
		mav.addObject("articleInfoList", articleInfoList);
		mav.addObject("total", total);
		mav.addObject("title", q);
		mav.addObject("modelName", q + " - 搜索結果");
		mav.addObject("pageCode", PageUtil.genSearchPagination("/search", total, Integer.parseInt(page), pageSize, q));
		mav.setViewName("result");
		return mav;
	}

  再看一個delete的例子,在刪除帖子信息的時候,除了刪除數據庫中的記錄,還要刪除ES中的記錄。

	@RequestMapping("/delete")
	public Map<String, Object> delete(Integer id) throws Exception {
		articleService.delete(id);
		articleService.deleteIndex(String.valueOf(id));
		Map<String, Object> resultMap = new HashMap<String, Object>();
		resultMap.put("success", true);
		return resultMap;
	}

  至此,ElasticSearch就介紹完了。

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