关于java:基于-ElasticSearch-实现站内全文搜索写得太好了

35次阅读

共计 13305 个字符,预计需要花费 34 分钟才能阅读完成。

起源: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 分词插件。

  • 装置 elasticSearch elasticsearch 官网. 笔者应用的是 7.5.1。
  • ik 插件下载 ik 插件 github 地址. 留神下载和你下载 elasticsearch 版本一样的 ik 插件。
  • 将 ik 插件放入 elasticsearch 装置目录下的 plugins 包下,新建报名 ik,将下载好的插件解压到该目录下即可,启动 es 的时候会主动加载该插件。

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

3 我的项目架构

  • 获取数据应用 ik 分词插件
  • 将数据存储在 es 引擎中
  • 通过 es 检索形式对存储的数据进行检索
  • 应用 es 的 java 客户端提供内部服务

4 实现成果

4.1 搜寻页面

简略实现一个相似百度的搜寻框即可。

4.2 搜寻后果页面

点击第一个搜寻后果是我集体的某一篇博文,为了防止数据版权问题,笔者在 es 引擎中寄存的全是集体的博客数据。

5 具体代码实现

5.1 全文检索的实现对象

依照博文的根本信息定义了如下实体类,次要须要晓得每一个博文的 url,通过检索进去的文章具体查看要跳转到该 url。

package com.lbh.es.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;

import javax.persistence.*;

/**
 * PUT articles
 * {
 * "mappings":
 * {"properties":{* "author":{"type":"text"},
 * "content":{"type":"text","analyzer":"ik_max_word","search_analyzer":"ik_smart"},
 * "title":{"type":"text","analyzer":"ik_max_word","search_analyzer":"ik_smart"},
 * "createDate":{"type":"date","format":"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"},
 * "url":{"type":"text"}
 * } },
 * "settings":{
 *     "index":{
 *       "number_of_shards":1,
 *       "number_of_replicas":2
 *     }
 *   }
 * }
 * ---------------------------------------------------------------------------------------------------------------------
 * Copyright(c)lbhbinhao@163.com
 * @author liubinhao
 * @date 2021/3/3
 */
@Entity
@Table(name = "es_article")
public class ArticleEntity {
    @Id
    @JsonIgnore
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    @Column(name = "author")
    private String author;
    @Column(name = "content",columnDefinition="TEXT")
    private String content;
    @Column(name = "title")
    private String title;
    @Column(name = "createDate")
    private String createDate;
    @Column(name = "url")
    private String url;

    public String getAuthor() {return author;}

    public void setAuthor(String author) {this.author = author;}

    public String getContent() {return content;}

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

    public String getTitle() {return title;}

    public void setTitle(String title) {this.title = title;}

    public String getCreateDate() {return createDate;}

    public void setCreateDate(String createDate) {this.createDate = createDate;}

    public String getUrl() {return url;}

    public void setUrl(String url) {this.url = url;}
}

5.2 客户端配置

通过 java 配置 es 的客户端。

package com.lbh.es.config;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

/**
 * Copyright(c)lbhbinhao@163.com
 * @author liubinhao
 * @date 2021/3/3
 */
@Configuration
public class EsConfig {@Value("${elasticsearch.schema}")
    private String schema;
    @Value("${elasticsearch.address}")
    private String address;
    @Value("${elasticsearch.connectTimeout}")
    private int connectTimeout;
    @Value("${elasticsearch.socketTimeout}")
    private int socketTimeout;
    @Value("${elasticsearch.connectionRequestTimeout}")
    private int tryConnTimeout;
    @Value("${elasticsearch.maxConnectNum}")
    private int maxConnNum;
    @Value("${elasticsearch.maxConnectPerRoute}")
    private int maxConnectPerRoute;

    @Bean
    public RestHighLevelClient restHighLevelClient() {
        // 拆分地址
        List<HttpHost> hostLists = new ArrayList<>();
        String[] hostList = address.split(",");
        for (String addr : hostList) {String host = addr.split(":")[0];
            String port = addr.split(":")[1];
            hostLists.add(new HttpHost(host, Integer.parseInt(port), schema));
        }
        // 转换成 HttpHost 数组
        HttpHost[] httpHost = hostLists.toArray(new HttpHost[]{});
        // 构建连贯对象
        RestClientBuilder builder = RestClient.builder(httpHost);
        // 异步连贯延时配置
        builder.setRequestConfigCallback(requestConfigBuilder -> {requestConfigBuilder.setConnectTimeout(connectTimeout);
            requestConfigBuilder.setSocketTimeout(socketTimeout);
            requestConfigBuilder.setConnectionRequestTimeout(tryConnTimeout);
            return requestConfigBuilder;
        });
        // 异步连接数配置
        builder.setHttpClientConfigCallback(httpClientBuilder -> {httpClientBuilder.setMaxConnTotal(maxConnNum);
            httpClientBuilder.setMaxConnPerRoute(maxConnectPerRoute);
            return httpClientBuilder;
        });
        return new RestHighLevelClient(builder);
    }

}

5.3 业务代码编写

包含一些检索文章的信息,能够从文章题目,文章内容以及作者信息这些维度来查看相干信息。

package com.lbh.es.service;

import com.google.gson.Gson;
import com.lbh.es.entity.ArticleEntity;
import com.lbh.es.repository.ArticleRepository;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.IOException;

import java.util.*;

/**
 * Copyright(c)lbhbinhao@163.com
 * @author liubinhao
 * @date 2021/3/3
 */
@Service
public class ArticleService {

    private static final String ARTICLE_INDEX = "article";

    @Resource
    private RestHighLevelClient client;
    @Resource
    private ArticleRepository articleRepository;

    public boolean createIndexOfArticle(){Settings settings = Settings.builder()
                .put("index.number_of_shards", 1)
                .put("index.number_of_replicas", 1)
                .build();
// {"properties":{"author":{"type":"text"},
// "content":{"type":"text","analyzer":"ik_max_word","search_analyzer":"ik_smart"}
// ,"title":{"type":"text","analyzer":"ik_max_word","search_analyzer":"ik_smart"},
// ,"createDate":{"type":"date","format":"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"}
// }
        String mapping = "{\"properties\":{\"author\":{\"type\":\"text\"},\n" +
                "\"content\":{\"type\":\"text\",\"analyzer\":\"ik_max_word\",\"search_analyzer\":\"ik_smart\"}\n" +
                ",\"title\":{\"type\":\"text\",\"analyzer\":\"ik_max_word\",\"search_analyzer\":\"ik_smart\"}\n" +
                ",\"createDate\":{\"type\":\"date\",\"format\":\"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd\"}\n" +
                "},\"url\":{\"type\":\"text\"}\n" +
                "}";
        CreateIndexRequest indexRequest = new CreateIndexRequest(ARTICLE_INDEX)
                .settings(settings).mapping(mapping,XContentType.JSON);
        CreateIndexResponse response = null;
        try {response = client.indices().create(indexRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {e.printStackTrace();
        }
        if (response!=null) {System.err.println(response.isAcknowledged() ? "success" : "default");
            return response.isAcknowledged();} else {return false;}
    }

    public boolean deleteArticle(){DeleteIndexRequest request = new DeleteIndexRequest(ARTICLE_INDEX);
        try {AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
            return response.isAcknowledged();} catch (IOException e) {e.printStackTrace();
        }
        return false;
    }

    public IndexResponse addArticle(ArticleEntity article){Gson gson = new Gson();
        String s = gson.toJson(article);
        // 创立索引创建对象
        IndexRequest indexRequest = new IndexRequest(ARTICLE_INDEX);
        // 文档内容
        indexRequest.source(s,XContentType.JSON);
        // 通过 client 进行 http 的申请
        IndexResponse re = null;
        try {re = client.index(indexRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {e.printStackTrace();
        }
        return re;
    }

    public void transferFromMysql(){articleRepository.findAll().forEach(this::addArticle);
    }

    public List<ArticleEntity> queryByKey(String keyword){SearchRequest request = new SearchRequest();
        /*
         * 创立  搜寻内容参数设置对象:SearchSourceBuilder
         * 绝对于 matchQuery,multiMatchQuery 针对的是多个 fi eld,也就是说,当 multiMatchQuery 中,fieldNames 参数只有一个时,其作用与 matchQuery 相当;* 而当 fieldNames 有多个参数时,如 field1 和 field2,那查问的后果中,要么 field1 中蕴含 text,要么 field2 中蕴含 text。*/
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

        searchSourceBuilder.query(QueryBuilders
                .multiMatchQuery(keyword, "author","content","title"));
        request.source(searchSourceBuilder);
        List<ArticleEntity> result = new ArrayList<>();
        try {SearchResponse search = client.search(request, RequestOptions.DEFAULT);
            for (SearchHit hit:search.getHits()){Map<String, Object> map = hit.getSourceAsMap();
                ArticleEntity item = new ArticleEntity();
                item.setAuthor((String) map.get("author"));
                item.setContent((String) map.get("content"));
                item.setTitle((String) map.get("title"));
                item.setUrl((String) map.get("url"));
                result.add(item);
            }
            return result;
        } catch (IOException e) {e.printStackTrace();
        }
        return null;
    }

    public ArticleEntity queryById(String indexId){GetRequest request = new GetRequest(ARTICLE_INDEX, indexId);
        GetResponse response = null;
        try {response = client.get(request, RequestOptions.DEFAULT);
        } catch (IOException e) {e.printStackTrace();
        }
        if (response!=null&&response.isExists()){Gson gson = new Gson();
            return gson.fromJson(response.getSourceAsString(),ArticleEntity.class);
        }
        return null;
    }
}

5.4 对外接口

和应用 springboot 开发 web 程序雷同。

Spring Boot 根底就不介绍了,举荐下这个实战教程:
https://github.com/javastacks…

package com.lbh.es.controller;

import com.lbh.es.entity.ArticleEntity;
import com.lbh.es.service.ArticleService;
import org.elasticsearch.action.index.IndexResponse;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.List;

/**
 * Copyright(c)lbhbinhao@163.com
 * @author liubinhao
 * @date 2021/3/3
 */
@RestController
@RequestMapping("article")
public class ArticleController {

    @Resource
    private ArticleService articleService;

    @GetMapping("/create")
    public boolean create(){return articleService.createIndexOfArticle();
    }

    @GetMapping("/delete")
    public boolean delete() {return articleService.deleteArticle();
    }

    @PostMapping("/add")
    public IndexResponse add(@RequestBody ArticleEntity article){return articleService.addArticle(article);
    }

    @GetMapping("/fransfer")
    public String transfer(){articleService.transferFromMysql();
        return "successful";
    }

    @GetMapping("/query")
    public List<ArticleEntity> query(String keyword){return articleService.queryByKey(keyword);
    }
}

5.5 页面

此处页面应用 thymeleaf,次要起因是笔者真滴不会前端,只懂一丢丢简略的 h5,就轻易做了一个能够展现的页面。

搜寻页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>YiyiDu</title>
    <!--
        input:focus 设定当输入框被点击时,呈现蓝色外边框
        text-indent: 11px; 和 padding-left: 11px; 设定输出的字符的起始地位与左边框的间隔
    -->
    <style>
        input:focus {border: 2px solid rgb(62, 88, 206);
        }
        input {
            text-indent: 11px;
            padding-left: 11px;
            font-size: 16px;
        }
    </style>
    <!--input 初始状态 -->
    <style class="input/css">
        .input {
            width: 33%;
            height: 45px;
            vertical-align: top;
            box-sizing: border-box;
            border: 2px solid rgb(207, 205, 205);
            border-right: 2px solid rgb(62, 88, 206);
            border-bottom-left-radius: 10px;
            border-top-left-radius: 10px;
            outline: none;
            margin: 0;
            display: inline-block;
            background: url(/static/img/camera.jpg?watermark/2/text/5YWs5LyX5Y-377ya6IqL6YGT5rqQ56CB/font/5a6L5L2T/fontsize/400/fill/cmVk) no-repeat 0 0;
            background-position: 565px 7px;
            background-size: 28px;
            padding-right: 49px;
            padding-top: 10px;
            padding-bottom: 10px;
            line-height: 16px;
        }
    </style>
    <!--button 初始状态 -->
    <style class="button/css">
        .button {
            height: 45px;
            width: 130px;
            vertical-align: middle;
            text-indent: -8px;
            padding-left: -8px;
            background-color: rgb(62, 88, 206);
            color: white;
            font-size: 18px;
            outline: none;
            border: none;
            border-bottom-right-radius: 10px;
            border-top-right-radius: 10px;
            margin: 0;
            padding: 0;
        }
    </style>
</head>
<body>
<!-- 蕴含 table 的 div-->
<!-- 蕴含 input 和 button 的 div-->
    <div style="font-size: 0px;">
        <div align="center" style="margin-top: 0px;">
            <img src="../static/img/yyd.png" th:src = "@{/static/img/yyd.png}"  alt="一亿度" width="280px" class="pic" />
        </div>
        <div align="center">
            <!--action 实现跳转 -->
            <form action="/home/query">
                <input type="text" class="input" name="keyword" />
                <input type="submit" class="button" value="一亿度下" />
            </form>
        </div>
    </div>
</body>
</html>

搜寻后果页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/4.3.1/css/bootstrap.min.css">
    <meta charset="UTF-8">
    <title>xx-manager</title>
</head>
<body>
<header th:replace="search.html"></header>
<div class="container my-2">
    <ul th:each="article : ${articles}">
        <a th:href="${article.url}"><li th:text="${article.author}+${article.content}"></li></a>
    </ul>
</div>
<footer th:replace="footer.html"></footer>
</body>
</html>

6 小结

下班撸代码,上班持续撸代码写博客,花了两天钻研了以下 es,其实这个玩意儿还是挺有意思的,当初 IR 畛域最根底的还是基于统计学的,所以对于 es 这类搜索引擎而言在大数据的状况下具备良好的体现。

每一次写实战笔者其实都感觉有些无从下手,因为不晓得做啥?所以也心愿失去一些有意思的点子笔者会将实战做进去。

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿 (2021 最新版)

2. 劲爆!Java 协程要来了。。。

3. 玩大了!Log4j 2.x 再爆雷。。。

4.Spring Boot 2.6 正式公布,一大波新个性。。

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0