基于 ElasticSearch 实现站内全文搜索
3 项目架构
4 实现效果
4.1 搜索页面
4.2 搜索结果页面
5 具体代码实现
5.1 全文检索的实现对象
5.2 客户端配置
5.3 业务代码编写
5.4 对外接口
5.5 页面
6 小结
摘要
--
对于一家公司而言,数据量越来越多,如果快速去查找这些信息是一个很难的问题,在计算机领域有一个专门的领域 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 这样的搜索引擎服务,通过关系型数据库检索即可。
? 计算机电子书籍推荐
主要包括 Java 电子书籍(Java 基础,Java 多线程,spring、springboot、springcloud,分布式,微服务等)、Python,Linux,Go,C,C++,数据结构与算法,人工智能,计算机基础,面试,设计模式,数据库,前端等书籍。
获取方式
关注下方公众号回复:「好好学 Java」,即可获取。
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 引擎中存放的全是个人的博客数据。另外推荐:Java进阶视频资源
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 业务代码编写
包括一些检索文章的信息,可以从文章标题,文章内容以及作者信息这些维度来查看相关信息。另外推荐:Java进阶视频资源
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 程序相同。
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
评论