springboot+elasticsearch實現一個搜索引擎的功能
一、elasticsearch的安裝
ElasticSearch是一個基於Lucene的搜索服務器。它提供了一個分佈式多用戶能力的全文搜索引擎,基於RESTful web接口。Elasticsearch是用Java開發的,並作爲Apache許可條款下的開放源碼發佈,是當前流行的企業級搜索引擎。(如何安裝elasticsearch?)
二、解決ES跨域
可以將ES看作是一個數據庫,但是通過網絡協議連接他時會出現跨域問題,所以需要在啓動ES服務之前設置跨域的一些協議,開放自己端口協議等,以免瀏覽器跨域請求被攔截,在elasticsearch-5.6.16\config\elasticsearch.yml文件中配置如下代碼即可。
http.cors.enabled: true
http.cors.allow-origin: "*"
三、elasticsearch-head下載及安裝
1、谷歌瀏覽器可以直接在Chrome商城中安裝elasticsearch-head插件
2、其他瀏覽器安裝elasticsearch-head插件
四、完成springboot 和 elasticsearch的整合
1、創建spring boot項目,加載pom.xml。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.demo</groupId>
<artifactId>elasticsearch</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>elasticsearch</name>
<description>使用springboot+elasticsearch做一個搜索引擎</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- elasticsearch -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>aliyunmaven</id>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>
2、application.yml配置
server:
port: 8888
spring:
data:
elasticsearch:
cluster-name: elasticsearch
cluster-nodes: 127.0.0.1:9300
jackson:
default-property-inclusion: non_null
datasource:
url: jdbc:mysql://127.0.0.1:3306/elasticsearch?useSSL=false&serverTimezone=GMT%2B8
username: root
password: sasa
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
五、實體類
先說明一下,測試數據來源與商品分類信息,數據存放在mysql數據庫,我們需要將mysql的數據全部查詢出來然後存放到elasticsearch中,這兒用的springdata來方便我們連接mysql和elastic
1、對應mysql 實體類
package com.demo.elasticsearch.pojo;
import lombok.Data;
import javax.persistence.*;
import java.io.Serializable;
@Data
@Entity(name="wp_ex_source_goods_tb_cat_copy")
public class XcGoods implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cid")
private Long cid;
@Column(name = "name")
private String name;
@Column(name = "is_parent")
private String isParent;
@Column(name = "parent_id")
private String parentId;
@Column(name = "level")
private Long level;
@Column(name = "pathid")
private String pathid;
@Column(name = "path")
private String path;
}
2、對應es實體類
package com.demo.elasticsearch.pojo;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
@Data
@Document(indexName = "goodscat", type = "docs", shards = 1, replicas = 0)
public class Goods {
@Id
private Long cid;
// @Field(type = FieldType.Keyword, analyzer = "ik_max_word")
// private String all; // 所有需要被搜索的信息,包含標題,分類,甚至品牌
@Field(type = FieldType.Keyword, index = true, analyzer = "ik_max_word")
private String name;
private String isParent;
private String parentId;
private Long level;
private String pathid;
}
3、分頁工具類
package com.demo.elasticsearch.pojo;
public class SearchRequest {
private String key;// 搜索條件
private Integer page;// 當前頁
private static final Integer DEFAULT_SIZE = 20;// 每頁大小,不從頁面接收,而是固定大小
private static final Integer DEFAULT_PAGE = 1;// 默認頁
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public Integer getPage() {
if(page == null){
return DEFAULT_PAGE;
}
// 獲取頁碼時做一些校驗,不能小於1
return Math.max(DEFAULT_PAGE, page);
}
public void setPage(Integer page) {
this.page = page;
}
public Integer getSize() {
return DEFAULT_SIZE;
}
}
六、dao層
1、對應mysql
package com.demo.elasticsearch.repository;
import com.demo.elasticsearch.pojo.Goods;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface GoodsRepository extends ElasticsearchRepository<Goods,Long> {
}
2、對應es
package com.demo.elasticsearch.repository;
import com.demo.elasticsearch.pojo.XcGoods;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface XcGoodsRepository extends JpaRepository<XcGoods,Long>, JpaSpecificationExecutor<XcGoods> {
}
七、service層
1、service
package com.demo.elasticsearch.service;
import com.demo.elasticsearch.pojo.Goods;
import com.demo.elasticsearch.pojo.SearchRequest;
import com.demo.elasticsearch.pojo.XcGoods;
import com.demo.elasticsearch.utils.PageResult;
public interface SearchService {
Goods buildGoods(XcGoods goods);
PageResult<Goods> search(SearchRequest request);
}
2、impl
package com.demo.elasticsearch.service.impl;
import com.demo.elasticsearch.pojo.Goods;
import com.demo.elasticsearch.pojo.SearchRequest;
import com.demo.elasticsearch.pojo.XcGoods;
import com.demo.elasticsearch.repository.GoodsRepository;
import com.demo.elasticsearch.service.SearchService;
import com.demo.elasticsearch.utils.PageResult;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.index.query.QueryBuilders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import java.util.*;
@Slf4j
@Service
public class SearchServiceImpl implements SearchService {
@Autowired
private GoodsRepository goodsRepository;
@Autowired
private ElasticsearchTemplate template;
@Override
public Goods buildGoods(XcGoods xcgoods) {
//搜索字段
// String all = xcgoods.getName();
//構建goods對象
Goods goods = new Goods();
goods.setCid(xcgoods.getCid());
goods.setName(xcgoods.getName());
goods.setIsParent(xcgoods.getIsParent());
goods.setParentId(xcgoods.getParentId());
goods.setPathid(xcgoods.getPathid());
goods.setLevel(xcgoods.getLevel());
// 搜索字段,包含標題,分類,品牌,規格,等等
// goods.setAll(all);
return goods;
}
@Override
public PageResult<Goods> search(SearchRequest request) {
int page = request.getPage() - 1;
int size = request.getSize();
//創建查詢構建器
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//結果過濾
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"cid", "name"}, null));
//分頁
queryBuilder.withPageable(PageRequest.of(page, size));
//過濾
queryBuilder.withQuery(QueryBuilders.matchQuery("name", request.getKey()));
//查詢
//Page<Goods> result = goodsRepository.search(queryBuilder.build());
AggregatedPage<Goods> result = template.queryForPage(queryBuilder.build(), Goods.class);
//解析結果
//分頁結果解析
long total = result.getTotalElements();
Integer totalPages1 = result.getTotalPages(); //失效
Long totalPages = total % size == 0 ? total / size : total / size + 1;
List<Goods> goodsList = result.getContent();
//解析聚合結果
return new PageResult<>(total, totalPages, goodsList);
}
}
3、測試類及操作解釋
package com.demo.elasticsearch.service.impl;
import com.demo.elasticsearch.pojo.Goods;
import com.demo.elasticsearch.pojo.XcGoods;
import com.demo.elasticsearch.repository.GoodsRepository;
import com.demo.elasticsearch.repository.XcGoodsRepository;
import com.demo.elasticsearch.service.SearchService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.stream.Collectors;
@SpringBootTest
public class SearchServiceImplTest {
@Autowired
private GoodsRepository goodsRepository;
@Autowired
private ElasticsearchTemplate template;
@Autowired
private SearchService searchService;
@Autowired
private XcGoodsRepository xcGoodsRepository;
@Test
public void testCreateIndex() {
template.createIndex(Goods.class);
template.putMapping(Goods.class);
}
@Test
public void loadData() {
int page = 1;
int rows = 100;
int size = 0;
//查詢spu信息
do {
Page<XcGoods> result = xcGoodsRepository.findAll(PageRequest.of(page - 1, rows));
List<XcGoods> spuList = result.getContent();
if (CollectionUtils.isEmpty(spuList)){
break;
}
//構建成goods
List<Goods> goodsList = spuList.stream()
.map(searchService::buildGoods).collect(Collectors.toList());
//存入索引庫
goodsRepository.saveAll(goodsList);
//翻頁
page++;
size = spuList.size();
} while (size == 100);
}
}
最終樣式
八、前端VUE+ELEMENT
1、創建vue項目(如何創建vue項目?)
2、安裝 axios 插件,在當前項目下的終端輸入命令: npm install --save axios vue-axios
安裝 Element 插件,在當前項目下的終端輸入命令:npm i element-ui -S
3、在 src 文件夾下的程序入口 main.js 中導入
import axios from 'axios'
import VueAxios from 'vue-axios'
// element-ui 引入文件
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
//註冊 VueAxios, axios
Vue.use(VueAxios, axios)
Vue.use(ElementUI)
4、創建vue文件,編寫代碼
<template>
<div>
<el-autocomplete
v-model="state"
:fetch-suggestions="querySearchAsync"
placeholder="請輸入內容"
@select="handleSelect"
select-when-unmatched="true"
:debounce="0"
></el-autocomplete>
<el-button slot="append" icon="el-icon-search" @click="onSubmit"></el-button>
<div class="div2" v-show="con">
<p>
搜索
<span style="color: #F08080">{{state}}</span>的結果(總共搜索到
<span style="color: #F08080">{{total}}</span>條記錄)
</p>
<p v-for="entity in All" class="p2">
<a href="http://www.baidu.com">{{entity.name}}</a>
</p>
<el-pagination
background
layout="prev, pager, next"
:total="total"
:page-size="15"
@current-change="handleCurrentChange"
:current-page="page"
></el-pagination>
</div>
</div>
</template>
<script>
const axios = require("axios");
export default {
data () {
return {
con: false,
restaurants: [],
state: '',
timeout: null,
All: [],
total: 0,
page: 1,
};
},
watch: {
state: { // 監視字段,頁數
handler () {
if (this.state.length > 0) {
this.restaurants = [];
this.loadAll();
} else {
this.con = false;
this.restaurants = [];
this.All = [];
this.page = 1;
}
}
},
page: { // 監視字段,頁數
handler () {
this.loadAll();
}
}
},
methods: {
handleCurrentChange (val) {//當前頁
this.page = val;
console.log(`當前頁: ${val}`);
},
loadAll () {
var app = this;
axios.get("http://localhost:8888/search", {
params: {
'key': app.state,
'page': app.page
}
}).then(function (resp) {
app.total = resp.data.total;
var rs = resp.data.items;
app.All = rs;
if (rs.length > 0) {
for (var i = 0; i < 10; i++) {
app.restaurants[i] = { value: rs[i].name, cid: rs[i].cid }
}
}
}).catch(function (error) {
console.log(error);
});
},
querySearchAsync (queryString, cb) {
var results = this.restaurants;
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
cb(results);
}, 1000 * Math.random());
},
handleSelect (item) {
if (this.All != "") {
this.con = true;
}
},
onSubmit () {
if (this.All != "") {
this.con = true;
}
}
},
};
</script>
<style>
.el-autocomplete {
width: 400px;
}
.p2 {
margin-left: 160px;
text-align: left;
font-size: 20px;
}
a {
color: blue;
}
.div2 {
/* background: blue; */
margin-top: 25px;
padding-top: 25px;
margin-left: 270px;
width: 750px;
height: 650px;
/* border: 1px solid #b0c4de; */
}
</style>
最終樣式(因爲是自己編寫的vue頁面,不夠完善,運行會報一個小bug,不會影響,直接在瀏覽器訪問即可)