当前位置:   article > 正文

基于 ElasticSearch 实现站内全文搜索,写得太好了!

easy-es 实现全文检索

aff69f179231b67065d5f79574514946.png

程序员的成长之路

互联网/程序员/技术/资料共享 

关注

阅读本文大概需要 11 分钟。

来自:blog.csdn.net/weixin_44671737/article/details/114456257

摘要

对于一家公司而言,数据量越来越多,如果快速去查找这些信息是一个很难的问题,在计算机领域有一个专门的领域IR(Information Retrival)研究如果获取信息,做信息检索。

在国内的如百度这样的搜索引擎也属于这个领域,要自己实现一个搜索引擎是非常难的,不过信息查找对每一个公司都非常重要,对于开发人员也可以选则一些市场上的开源项目来构建自己的站内搜索引擎,本文将通过ElasticSearch来构建一个这样的信息检索项目。

1 技术选型

  • 搜索引擎服务使用ElasticSearch

  • 提供的对外web服务选则springboot web

1.1 ElasticSearch

Elasticsearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java语言开发的,并作为Apache许可条款下的开放源码发布,是一种流行的企业级搜索引擎。Elasticsearch用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。

官方客户端在Java、.NET(C#)、PHP、Python、Apache Groovy、Ruby和许多其他语言中都是可用的。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr,也是基于Lucene。1

现在开源的搜索引擎在市面上最常见的就是ElasticSearch和Solr,二者都是基于Lucene的实现,其中ElasticSearch相对更加重量级,在分布式环境表现也更好,二者的选则需考虑具体的业务场景和数据量级。对于数据量不大的情况下,完全需要使用像Lucene这样的搜索引擎服务,通过关系型数据库检索即可。

1.2 springBoot

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”.2

现在springBoot在做web开发上是绝对的主流,其不仅仅是开发上的优势,在布署,运维各个方面都有着非常不错的表现,并且spring生态圈的影响力太大了,可以找到各种成熟的解决方案。

1.3 ik分词器

elasticSearch本身不支持中文的分词,需要安装中文分词插件,如果需要做中文的信息检索,中文分词是基础,此处选则了ik,下载好后放入elasticSearch的安装位置的plugin目录即可。

2 环境准备

需要安装好elastiSearch以及kibana(可选),并且需要lk分词插件。

8494655861a995d0f1e1aa7c640e990d.png

  • 搭建springboot项目 idea ->new project ->spring initializer

53b921363db4f0df850b26587f6605ae.png

3 项目架构

  • 获取数据使用ik分词插件

  • 将数据存储在es引擎中

  • 通过es检索方式对存储的数据进行检索

  • 使用es的java客户端提供外部服务

f2e7cb12ec62a6d6ba0b9bb4a3762e70.png

4 实现效果

4.1 搜索页面

简单实现一个类似百度的搜索框即可。

569be2033ff0fd4c95f62084efe81dd1.png

4.2 搜索结果页面

aa4104f457e3ed738591a9515fb0e8c9.png

点击第一个搜索结果是我个人的某一篇博文,为了避免数据版权问题,笔者在es引擎中存放的全是个人的博客数据。

c992b7e9ac94949049731f5f90d6eb05.png

5 具体代码实现

5.1 全文检索的实现对象

按照博文的基本信息定义了如下实体类,主要需要知道每一个博文的url,通过检索出来的文章具体查看要跳转到该url。

  1. package com.lbh.es.entity;
  2. import com.fasterxml.jackson.annotation.JsonIgnore;
  3. import javax.persistence.*;
  4. /**
  5.  * PUT articles
  6.  * {
  7.  * "mappings":
  8.  * {"properties":{
  9.  * "author":{"type":"text"},
  10.  * "content":{"type":"text","analyzer":"ik_max_word","search_analyzer":"ik_smart"},
  11.  * "title":{"type":"text","analyzer":"ik_max_word","search_analyzer":"ik_smart"},
  12.  * "createDate":{"type":"date","format":"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"},
  13.  * "url":{"type":"text"}
  14.  * } },
  15.  * "settings":{
  16.  *     "index":{
  17.  *       "number_of_shards":1,
  18.  *       "number_of_replicas":2
  19.  *     }
  20.  *   }
  21.  * }
  22.  * ---------------------------------------------------------------------------------------------------------------------
  23.  * Copyright(c)lbhbinhao@163.com
  24.  * @author liubinhao
  25.  * @date 2021/3/3
  26.  */
  27. @Entity
  28. @Table(name = "es_article")
  29. public class ArticleEntity {
  30.     @Id
  31.     @JsonIgnore
  32.     @GeneratedValue(strategy = GenerationType.IDENTITY)
  33.     private long id;
  34.     @Column(name = "author")
  35.     private String author;
  36.     @Column(name = "content",columnDefinition="TEXT")
  37.     private String content;
  38.     @Column(name = "title")
  39.     private String title;
  40.     @Column(name = "createDate")
  41.     private String createDate;
  42.     @Column(name = "url")
  43.     private String url;
  44.     public String getAuthor() {
  45.         return author;
  46.     }
  47.     public void setAuthor(String author) {
  48.         this.author = author;
  49.     }
  50.     public String getContent() {
  51.         return content;
  52.     }
  53.     public void setContent(String content) {
  54.         this.content = content;
  55.     }
  56.     public String getTitle() {
  57.         return title;
  58.     }
  59.     public void setTitle(String title) {
  60.         this.title = title;
  61.     }
  62.     public String getCreateDate() {
  63.         return createDate;
  64.     }
  65.     public void setCreateDate(String createDate) {
  66.         this.createDate = createDate;
  67.     }
  68.     public String getUrl() {
  69.         return url;
  70.     }
  71.     public void setUrl(String url) {
  72.         this.url = url;
  73.     }
  74. }

5.2 客户端配置

通过java配置es的客户端。

  1. package com.lbh.es.config;
  2. import org.apache.http.HttpHost;
  3. import org.elasticsearch.client.RestClient;
  4. import org.elasticsearch.client.RestClientBuilder;
  5. import org.elasticsearch.client.RestHighLevelClient;
  6. import org.springframework.beans.factory.annotation.Value;
  7. import org.springframework.context.annotation.Bean;
  8. import org.springframework.context.annotation.Configuration;
  9. import java.util.ArrayList;
  10. import java.util.List;
  11. /**
  12.  * Copyright(c)lbhbinhao@163.com
  13.  * @author liubinhao
  14.  * @date 2021/3/3
  15.  */
  16. @Configuration
  17. public class EsConfig {
  18.     @Value("${elasticsearch.schema}")
  19.     private String schema;
  20.     @Value("${elasticsearch.address}")
  21.     private String address;
  22.     @Value("${elasticsearch.connectTimeout}")
  23.     private int connectTimeout;
  24.     @Value("${elasticsearch.socketTimeout}")
  25.     private int socketTimeout;
  26.     @Value("${elasticsearch.connectionRequestTimeout}")
  27.     private int tryConnTimeout;
  28.     @Value("${elasticsearch.maxConnectNum}")
  29.     private int maxConnNum;
  30.     @Value("${elasticsearch.maxConnectPerRoute}")
  31.     private int maxConnectPerRoute;
  32.     @Bean
  33.     public RestHighLevelClient restHighLevelClient() {
  34.         // 拆分地址
  35.         List<HttpHost> hostLists = new ArrayList<>();
  36.         String[] hostList = address.split(",");
  37.         for (String addr : hostList) {
  38.             String host = addr.split(":")[0];
  39.             String port = addr.split(":")[1];
  40.             hostLists.add(new HttpHost(host, Integer.parseInt(port), schema));
  41.         }
  42.         // 转换成 HttpHost 数组
  43.         HttpHost[] httpHost = hostLists.toArray(new HttpHost[]{});
  44.         // 构建连接对象
  45.         RestClientBuilder builder = RestClient.builder(httpHost);
  46.         // 异步连接延时配置
  47.         builder.setRequestConfigCallback(requestConfigBuilder -> {
  48.             requestConfigBuilder.setConnectTimeout(connectTimeout);
  49.             requestConfigBuilder.setSocketTimeout(socketTimeout);
  50.             requestConfigBuilder.setConnectionRequestTimeout(tryConnTimeout);
  51.             return requestConfigBuilder;
  52.         });
  53.         // 异步连接数配置
  54.         builder.setHttpClientConfigCallback(httpClientBuilder -> {
  55.             httpClientBuilder.setMaxConnTotal(maxConnNum);
  56.             httpClientBuilder.setMaxConnPerRoute(maxConnectPerRoute);
  57.             return httpClientBuilder;
  58.         });
  59.         return new RestHighLevelClient(builder);
  60.     }
  61. }

5.3 业务代码编写

包括一些检索文章的信息,可以从文章标题,文章内容以及作者信息这些维度来查看相关信息。

  1. package com.lbh.es.service;
  2. import com.google.gson.Gson;
  3. import com.lbh.es.entity.ArticleEntity;
  4. import com.lbh.es.repository.ArticleRepository;
  5. import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
  6. import org.elasticsearch.action.get.GetRequest;
  7. import org.elasticsearch.action.get.GetResponse;
  8. import org.elasticsearch.action.index.IndexRequest;
  9. import org.elasticsearch.action.index.IndexResponse;
  10. import org.elasticsearch.action.search.SearchRequest;
  11. import org.elasticsearch.action.search.SearchResponse;
  12. import org.elasticsearch.action.support.master.AcknowledgedResponse;
  13. import org.elasticsearch.client.RequestOptions;
  14. import org.elasticsearch.client.RestHighLevelClient;
  15. import org.elasticsearch.client.indices.CreateIndexRequest;
  16. import org.elasticsearch.client.indices.CreateIndexResponse;
  17. import org.elasticsearch.common.settings.Settings;
  18. import org.elasticsearch.common.xcontent.XContentType;
  19. import org.elasticsearch.index.query.QueryBuilders;
  20. import org.elasticsearch.search.SearchHit;
  21. import org.elasticsearch.search.builder.SearchSourceBuilder;
  22. import org.springframework.stereotype.Service;
  23. import javax.annotation.Resource;
  24. import java.io.IOException;
  25. import java.util.*;
  26. /**
  27.  * Copyright(c)lbhbinhao@163.com
  28.  * @author liubinhao
  29.  * @date 2021/3/3
  30.  */
  31. @Service
  32. public class ArticleService {
  33.     private static final String ARTICLE_INDEX = "article";
  34.     @Resource
  35.     private RestHighLevelClient client;
  36.     @Resource
  37.     private ArticleRepository articleRepository;
  38.     public boolean createIndexOfArticle(){
  39.         Settings settings = Settings.builder()
  40.                 .put("index.number_of_shards"1)
  41.                 .put("index.number_of_replicas"1)
  42.                 .build();
  43. // {"properties":{"author":{"type":"text"},
  44. // "content":{"type":"text","analyzer":"ik_max_word","search_analyzer":"ik_smart"}
  45. // ,"title":{"type":"text","analyzer":"ik_max_word","search_analyzer":"ik_smart"},
  46. // ,"createDate":{"type":"date","format":"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"}
  47. // }
  48.         String mapping = "{\"properties\":{\"author\":{\"type\":\"text\"},\n" +
  49.                 "\"content\":{\"type\":\"text\",\"analyzer\":\"ik_max_word\",\"search_analyzer\":\"ik_smart\"}\n" +
  50.                 ",\"title\":{\"type\":\"text\",\"analyzer\":\"ik_max_word\",\"search_analyzer\":\"ik_smart\"}\n" +
  51.                 ",\"createDate\":{\"type\":\"date\",\"format\":\"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd\"}\n" +
  52.                 "},\"url\":{\"type\":\"text\"}\n" +
  53.                 "}";
  54.         CreateIndexRequest indexRequest = new CreateIndexRequest(ARTICLE_INDEX)
  55.                 .settings(settings).mapping(mapping,XContentType.JSON);
  56.         CreateIndexResponse response = null;
  57.         try {
  58.             response = client.indices().create(indexRequest, RequestOptions.DEFAULT);
  59.         } catch (IOException e) {
  60.             e.printStackTrace();
  61.         }
  62.         if (response!=null) {
  63.             System.err.println(response.isAcknowledged() ? "success" : "default");
  64.             return response.isAcknowledged();
  65.         } else {
  66.             return false;
  67.         }
  68.     }
  69.     public boolean deleteArticle(){
  70.         DeleteIndexRequest request = new DeleteIndexRequest(ARTICLE_INDEX);
  71.         try {
  72.             AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
  73.             return response.isAcknowledged();
  74.         } catch (IOException e) {
  75.             e.printStackTrace();
  76.         }
  77.         return false;
  78.     }
  79.     public IndexResponse addArticle(ArticleEntity article){
  80.         Gson gson = new Gson();
  81.         String s = gson.toJson(article);
  82.         //创建索引创建对象
  83.         IndexRequest indexRequest = new IndexRequest(ARTICLE_INDEX);
  84.         //文档内容
  85.         indexRequest.source(s,XContentType.JSON);
  86.         //通过client进行http的请求
  87.         IndexResponse re = null;
  88.         try {
  89.             re = client.index(indexRequest, RequestOptions.DEFAULT);
  90.         } catch (IOException e) {
  91.             e.printStackTrace();
  92.         }
  93.         return re;
  94.     }
  95.     public void transferFromMysql(){
  96.         articleRepository.findAll().forEach(this::addArticle);
  97.     }
  98.     public List<ArticleEntity> queryByKey(String keyword){
  99.         SearchRequest request = new SearchRequest();
  100.         /*
  101.          * 创建  搜索内容参数设置对象:SearchSourceBuilder
  102.          * 相对于matchQuery,multiMatchQuery针对的是多个fi eld,也就是说,当multiMatchQuery中,fieldNames参数只有一个时,其作用与matchQuery相当;
  103.          * 而当fieldNames有多个参数时,如field1和field2,那查询的结果中,要么field1中包含text,要么field2中包含text。
  104.          */
  105.         SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
  106.         searchSourceBuilder.query(QueryBuilders
  107.                 .multiMatchQuery(keyword, "author","content","title"));
  108.         request.source(searchSourceBuilder);
  109.         List<ArticleEntity> result = new ArrayList<>();
  110.         try {
  111.             SearchResponse search = client.search(request, RequestOptions.DEFAULT);
  112.             for (SearchHit hit:search.getHits()){
  113.                 Map<String, Object> map = hit.getSourceAsMap();
  114.                 ArticleEntity item = new ArticleEntity();
  115.                 item.setAuthor((String) map.get("author"));
  116.                 item.setContent((String) map.get("content"));
  117.                 item.setTitle((String) map.get("title"));
  118.                 item.setUrl((String) map.get("url"));
  119.                 result.add(item);
  120.             }
  121.             return result;
  122.         } catch (IOException e) {
  123.             e.printStackTrace();
  124.         }
  125.         return null;
  126.     }
  127.     public ArticleEntity queryById(String indexId){
  128.         GetRequest request = new GetRequest(ARTICLE_INDEX, indexId);
  129.         GetResponse response = null;
  130.         try {
  131.             response = client.get(request, RequestOptions.DEFAULT);
  132.         } catch (IOException e) {
  133.             e.printStackTrace();
  134.         }
  135.         if (response!=null&&response.isExists()){
  136.             Gson gson = new Gson();
  137.             return gson.fromJson(response.getSourceAsString(),ArticleEntity.class);
  138.         }
  139.         return null;
  140.     }
  141. }

5.4 对外接口

和使用springboot开发web程序相同。

Spring Boot 基础就不介绍了,推荐下这个实战教程:https://github.com/javastacks/spring-boot-best-practice

  1. package com.lbh.es.controller;
  2. import com.lbh.es.entity.ArticleEntity;
  3. import com.lbh.es.service.ArticleService;
  4. import org.elasticsearch.action.index.IndexResponse;
  5. import org.springframework.web.bind.annotation.*;
  6. import javax.annotation.Resource;
  7. import java.util.List;
  8. /**
  9.  * Copyright(c)lbhbinhao@163.com
  10.  * @author liubinhao
  11.  * @date 2021/3/3
  12.  */
  13. @RestController
  14. @RequestMapping("article")
  15. public class ArticleController {
  16.     @Resource
  17.     private ArticleService articleService;
  18.     @GetMapping("/create")
  19.     public boolean create(){
  20.         return articleService.createIndexOfArticle();
  21.     }
  22.     @GetMapping("/delete")
  23.     public boolean delete() {
  24.         return articleService.deleteArticle();
  25.     }
  26.     @PostMapping("/add")
  27.     public IndexResponse add(@RequestBody ArticleEntity article){
  28.         return articleService.addArticle(article);
  29.     }
  30.     @GetMapping("/fransfer")
  31.     public String transfer(){
  32.         articleService.transferFromMysql();
  33.         return "successful";
  34.     }
  35.     @GetMapping("/query")
  36.     public List<ArticleEntity> query(String keyword){
  37.         return articleService.queryByKey(keyword);
  38.     }
  39. }

5.5 页面

此处页面使用thymeleaf,主要原因是笔者真滴不会前端,只懂一丢丢简单的h5,就随便做了一个可以展示的页面。

搜索页面
  1. <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4.     <meta charset="UTF-8" />
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6.     <title>YiyiDu</title>
  7.     <!--
  8.         input:focus设定当输入框被点击时,出现蓝色外边框
  9.         text-indent: 11px;和padding-left: 11px;设定输入的字符的起始位置与左边框的距离
  10.     -->
  11.     <style>
  12.         input:focus {
  13.             border: 2px solid rgb(6288206);
  14.         }
  15.         input {
  16.             text-indent: 11px;
  17.             padding-left: 11px;
  18.             font-size: 16px;
  19.         }
  20.     </style>
  21.     <!--input初始状态-->
  22.     <style class="input/css">
  23.         .input {
  24.             width: 33%;
  25.             height: 45px;
  26.             vertical-align: top;
  27.             box-sizing: border-box;
  28.             border: 2px solid rgb(207205205);
  29.             border-right: 2px solid rgb(6288206);
  30.             border-bottom-left-radius: 10px;
  31.             border-top-left-radius: 10px;
  32.             outline: none;
  33.             margin: 0;
  34.             display: inline-block;
  35.             background: url(/static/img/camera.jpg?watermark/2/text/5YWs5LyX5Y-377ya6IqL6YGT5rqQ56CB/font/5a6L5L2T/fontsize/400/fill/cmVk) no-repeat 0 0;
  36.             background-position: 565px 7px;
  37.             background-size: 28px;
  38.             padding-right: 49px;
  39.             padding-top: 10px;
  40.             padding-bottom: 10px;
  41.             line-height: 16px;
  42.         }
  43.     </style>
  44.     <!--button初始状态-->
  45.     <style class="button/css">
  46.         .button {
  47.             height: 45px;
  48.             width: 130px;
  49.             vertical-align: middle;
  50.             text-indent: -8px;
  51.             padding-left: -8px;
  52.             background-color: rgb(6288206);
  53.             color: white;
  54.             font-size: 18px;
  55.             outline: none;
  56.             border: none;
  57.             border-bottom-right-radius: 10px;
  58.             border-top-right-radius: 10px;
  59.             margin: 0;
  60.             padding: 0;
  61.         }
  62.     </style>
  63. </head>
  64. <body>
  65. <!--包含table的div-->
  66. <!--包含input和button的div-->
  67.     <div style="font-size: 0px;">
  68.         <div align="center" style="margin-top: 0px;">
  69.             <img src="../static/img/yyd.png" th:src = "@{/static/img/yyd.png}"  alt="一亿度" width="280px" class="pic" />
  70.         </div>
  71.         <div align="center">
  72.             <!--action实现跳转-->
  73.             <form action="/home/query">
  74.                 <input type="text" class="input" name="keyword" />
  75.                 <input type="submit" class="button" value="一亿度下" />
  76.             </form>
  77.         </div>
  78.     </div>
  79. </body>
  80. </html>
搜索结果页面
  1. <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4.     <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/4.3.1/css/bootstrap.min.css">
  5.     <meta charset="UTF-8">
  6.     <title>xx-manager</title>
  7. </head>
  8. <body>
  9. <header th:replace="search.html"></header>
  10. <div class="container my-2">
  11.     <ul th:each="article : ${articles}">
  12.         <a th:href="${article.url}"><li th:text="${article.author}+${article.content}"></li></a>
  13.     </ul>
  14. </div>
  15. <footer th:replace="footer.html"></footer>
  16. </body>
  17. </html>

6 小结

上班撸代码,下班继续撸代码写博客,花了两天研究了以下es,其实这个玩意儿还是挺有意思的,现在IR领域最基础的还是基于统计学的,所以对于es这类搜索引擎而言在大数据的情况下具有良好的表现。

每一次写实战笔者其实都感觉有些无从下手,因为不知道做啥?所以也希望得到一些有意思的点子笔者会将实战做出来。

<END>

推荐阅读:

战火之下,乌克兰开发者还在提交代码

7种方式,教你提升 SpringBoot 项目的吞吐量

互联网初中高级大厂面试题(9个G)

内容包含Java基础、JavaWeb、MySQL性能优化、JVM、锁、百万并发、消息队列、高性能缓存、反射、Spring全家桶原理、微服务、Zookeeper、数据结构、限流熔断降级......等技术栈!

⬇戳阅读原文领取!                                  朕已阅 cdf67667ea9341742399631d70cb2ebf.gif

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/305700
推荐阅读
相关标签
  

闽ICP备14008679号