一、版本说明
-
Elasticsearch: 7.3.2
-
Springboot: 2.1.7
-
Dubbo: 2.7.1
-
Vue: 3.12.0
-
Nuxt: 2.0.0
-
bootstrapVue: 2.0.0
二、整合es、springboot、dubbo
2.1、pom文件
2.1.1、service的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 http://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.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.yuyuda</groupId>
<artifactId>search-service</artifactId>
<version>1.0.0</version>
<name>search-service</name>
<description>搜索服务</description>
<properties>
<java.version>1.8</java.version>
<dubbo.version>2.7.1</dubbo.version>
<es.version>7.3.2</es.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>${dubbo.version}</version>
</dependency>
<!--es-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>${es.version}</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>${es.version}</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>${es.version}</version>
</dependency>
<!--dubbo-->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
</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>
</dependency>
</dependencies>
<distributionManagement>
<repository>
<id>nexus</id>
<name>Nexus</name>
<url>http://192.168.3.25:8081/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>nexus</id>
<name>Nexus</name>
<url>http://192.168.3.25:8081/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.1.2、controller的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 http://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.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.yuyuda</groupId>
<artifactId>search-edge</artifactId>
<version>1.0.0-release</version>
<name>search-edge</name>
<description>搜索相关边缘服务</description>
<properties>
<java.version>1.8</java.version>
<dubbo.version>2.7.1</dubbo.version>
<swagger.version>2.9.2</swagger.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
</dependencies>
<distributionManagement>
<repository>
<id>nexus</id>
<name>Nexus</name>
<url>http://192.168.3.25:8081/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>nexus</id>
<name>Nexus</name>
<url>http://192.168.3.25:8081/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.2、application.yml文件
2.2.1、service的application.yml文件
dubbo:
application:
name: search-service
registry:
address: zookeeper://127.0.0.1:2181
metadata-report:
address: zookeeper://127.0.0.1:2181
protocol:
name: dubbo
port: 20884
spring:
data:
elasticsearch:
cluster-nodes: 192.168.3.15:9200
server:
port: 10008
#分页每页显示多少条
pageSize: 3
2.2.2、controller的application.yml文件
dubbo:
application:
name: search-edge
registry:
address: zookeeper://127.0.0.1:2181
metadata-report:
address: zookeeper://127.0.0.1:2181
protocol:
name: dubbo
port: 20884
server:
port: 10009
spring:
profiles:
active: dev
2.3、代码整合springboot与es
EsConfig.java
package com.yuyuda.search.config;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 配置es交由spring管理
*
* @author LiuZhixian lzx@yuyuda.com
* @version 1.0
* @date 2019/9/27 10:23
*/
@Configuration
public class EsConfig {
@Value("${spring.data.elasticsearch.cluster-nodes}")
private String clusterNodes;
@Bean
public RestHighLevelClient client() {
String[] node = clusterNodes.split(":");
String host = node[0];
Integer port = Integer.valueOf(node[1]);
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(new HttpHost(host, port, "http")));
return client;
}
}
三、ES RESTful常用api介绍
3.1、Indices Api创建索引
3.1.1、索引设置
curl -X PUT "localhost:9200/index-product?pretty" -H 'Content-Type: application/json' -d'
{
"settings" : {
"index" : {
"number_of_shards" : 3, //分片数
"number_of_replicas" : 2 //副本数
}
}
}
'
3.1.2、索引mappings
//索引类型映射,即相当于MySQL的建表
curl -X PUT "localhost:9200/index-product?pretty" -H 'Content-Type: application/json' -d'
{
"settings" : {
"number_of_shards" : 1
},
"mappings" : {
"type1" : {
"properties" : {
"field1" : { "type" : "text" }
}
}
}
}
'
注:其他api去文档上查找,这里相当于介绍流程。查找链接
https://www.elastic.co/guide/en/elasticsearch/reference/6.0/indices.html
3.2、插入数据
curl -X PUT "localhost:9200/index-product/_doc/1?pretty" -H 'Content-Type: application/json' -d'
{
"user" : "kimchy",
"post_date" : "2009-11-15T14:12:12",
"message" : "trying out Elasticsearch"
}
'
https://www.elastic.co/guide/en/elasticsearch/reference/6.0/docs.html
3.3、查询数据
3.3.1、简单查询
curl -X GET "localhost:9200/twitter/_doc/1?pretty"
3.3.2、查询所有
curl -X GET "localhost:9200/index-product/_search?pretty" -H 'Content-Type: application/json' -d'
{
"query": {
"match_all": {}
}
}
'
3.3.3、match单个字段查询
curl -X GET "localhost:9200/index-product/_search?pretty" -H 'Content-Type: application/json' -d'
{
"query": {
"match" : {
"message" : { //查询的字段
"query" : "this is a test", //需要输入的字符串
"operator" : "and" //字符串被分词this、is、 a、test都必须满足,如果是or值,是或者的意思
}
}
}
}
'
3.3.4、mutliMatch不确定字段查询
curl -X GET "localhost:9200/index-product/_search?pretty" -H 'Content-Type: application/json' -d'
{
"query": {
"multi_match" : {
"query": "this is a test",
"fields": [ "subject", "message" ] //两个字段同时满足
}
}
}
'
四、Springboot中的ES操作类封装
4.1、EsUtil.java
package com.yuyuda.search.util;
import com.alibaba.fastjson.JSON;
import com.yate.search.dto.Page;
import com.yate.search.entity.EsEntity;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.reindex.DeleteByQueryRequest;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Elastic search 工具类
*
* @author lzx@yuyuda.com
* @version 1.0
* @date 2020/2/28 下午11:56
**/
@Component
public class EsUtil {
@Autowired
private RestHighLevelClient client;
/**
* 检验某个索引是否存在
*
* @param index
* @return boolean
* @author lzx@yuyuda.com
* @date 2020/2/29 上午12:44
*/
public boolean indexExist(String index) throws Exception {
GetIndexRequest request = new GetIndexRequest(index);
request.local(false);
request.humanReadable(true);
request.includeDefaults(false);
return client.indices().exists(request, RequestOptions.DEFAULT);
}
/**
* 插入或更新一条记录
*
* @param index
* @param entity
* @return void
* @author lzx@yuyuda.com
* @date 2020/2/29 上午12:52
*/
public void insertOrUpdateOne(String index, EsEntity entity) throws Exception {
IndexRequest request = new IndexRequest(index);
request.id(entity.getId() + "");
request.source(JSON.toJSONString(entity.getData()), XContentType.JSON);
client.index(request, RequestOptions.DEFAULT);
}
/**
* 批量插入数据
*
* @param index
* @param list
* @return void
* @author lzx@yuyuda.com
* @date 2020/2/29 上午12:58
*/
public void insertBatch(String index, List<EsEntity> list) throws Exception {
BulkRequest request = new BulkRequest();
list.forEach(item -> request.add(new IndexRequest(index).id(item.getId())
.source(JSON.toJSONString(item.getData()), XContentType.JSON)));
client.bulk(request, RequestOptions.DEFAULT);
}
/**
* 批量删除索引中的数据
*
* @param index index
* @param idList 待删除列表
* @return void
* @author lzx@yuyuda.com
* @date 2020/2/29 上午3:38
*/
public <T> void deleteDataBatch(String index, Collection<T> idList) throws IOException {
BulkRequest request = new BulkRequest();
idList.forEach(item -> request.add(new DeleteRequest(index, item.toString())));
client.bulk(request, RequestOptions.DEFAULT);
}
/**
* 根据条件删除索引中的数据
*
* @param index
* @param builder
* @return void
* @author lzx@yuyuda.com
* @date 2020/2/29 上午3:33
*/
public void deleteDataByQuery(String index, QueryBuilder builder) throws IOException {
DeleteByQueryRequest request = new DeleteByQueryRequest(index);
request.setQuery(builder);
//设置批量删除操作数量,最大为10000
request.setBatchSize(10000);
request.setConflicts("proceed");
client.deleteByQuery(request, RequestOptions.DEFAULT);
}
/**
* 搜索[带分页]
*
* @param index index
* @param builder 查询参数
* @param tClass 结果类对象
* @param pageSize 每页多少条
* @return java.util.List<T>
* @author lzx@yuyuda.com
* @date 2020/2/29 上午2:28
*/
public <T> Page<T> searchPage(String index, SearchSourceBuilder builder, Class<T> tClass, int pageSize) throws IOException {
SearchRequest request = new SearchRequest(index);
request.source(builder);
SearchResponse search = client.search(request, RequestOptions.DEFAULT);
SearchHit[] hits = search.getHits().getHits();
int total = (int) search.getHits().getTotalHits().value;
ArrayList<T> res = new ArrayList<>(hits.length);
for (SearchHit hit : hits) {
res.add(JSON.parseObject(hit.getSourceAsString(), tClass));
}
Page<T> page = new Page<>();
page.setTotal(total);
double count = (double) total / (double) pageSize;
page.setPageCount((int) Math.ceil(count));
page.setPageList(res);
return page;
}
/**
* 搜索[不带分页]
*
* @param index index
* @param builder 查询参数
* @param tClass 结果类对象
* @return java.util.List<T>
* @author lzx@yuyuda.com
* @date 2020/2/29 上午2:28
*/
public <T> List<T> search(String index, SearchSourceBuilder builder, Class<T> tClass) throws IOException {
SearchRequest request = new SearchRequest(index);
request.source(builder);
SearchResponse search = client.search(request, RequestOptions.DEFAULT);
SearchHit[] hits = search.getHits().getHits();
int total = (int) search.getHits().getTotalHits().value;
ArrayList<T> res = new ArrayList<>(hits.length);
for (SearchHit hit : hits) {
res.add(JSON.parseObject(hit.getSourceAsString(), tClass));
}
return res;
}
/**
* 删除索引
*
* @param index
* @return void
* @author lzx@yuyuda.com
* @date 2020/2/29 上午3:28
*/
public void deleteIndex(String index) throws IOException {
client.indices().delete(new DeleteIndexRequest(index), RequestOptions.DEFAULT);
}
}
4.2、EsEntity.java ES实体
package com.yuyuda.search.entity;
import lombok.Data;
/**
* ES基础实体
*
* @author lzx@yuyuda.com
* @version 1.0
* @date 2020/2/29 上午12:47
**/
@Data
public final class EsEntity<T> {
private String id;
private T data;
}
4.3、分页DTO Page.java
package com.yuyuda.search.dto;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 分页实体
*
* @author lzx@yuyuda.com
* @version 1.0
* @date 2020/3/1 上午4:28
**/
@Data
public class Page<T> implements Serializable {
/**
* 总记录数
*/
private int total;
/**
* 页数
*/
private int pageCount;
/**
* 列表
*/
private List<T> pageList;
}
五、后端业务流程
5.1、ProductDto.java定义
package com.yuyuda.search.dto;
import com.yate.product.entity.YateProductProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* ES产品实体
*
* @author lzx@yuyuda.com
* @version 1.0
* @date 2020/2/29 上午1:55
**/
@Data
public class Product implements Serializable {
/**
* 产品ID
*/
private Long id;
private String idStr;
/**
* 产品名称
*/
private String productTitle;
/**
* 产品竞价价格
*/
private BigDecimal biddingPrice;
/**
* 产品单价
*/
private BigDecimal productUnitPrice;
/**
* 产品图片
*/
private String productImg;
/**
* 产品添加时间
*/
private Date createTime;
/**
* 产品所属商家
*/
private String businessName;
/**
* 产品所属商家ID
*/
private String businessId;
/**
* 产品所属分类
*/
private String catagory;
/**
* 产品分类ID
*/
private String catId;
/**
* 产品属性
*/
private YateProductProperty productProperty;
}
5.2、interface定义
package com.yuyuda.search.service;
import com.yate.search.dto.Page;
import com.yate.search.dto.Product;
import java.io.IOException;
import java.util.List;
/**
* 产品搜索服务
*
* @author lzx@yuyuda.com
* @version 1.0
* @date 2020/2/29 上午5:08
**/
public interface IProductSearchService {
/**
* 向索引中插入一条产品信息
*
* @param product
* @return void
* @throws Exception
* @author lzx@yuyuda.com
* @date 2020/2/29 上午5:13
*/
void putOne(Product product) throws Exception;
/**
* 通过id查询索引中的某条产品数据
*
* @param id
* @return com.yuyuda.search.dto.Product
* @throws IOException
* @author lzx@yuyuda.com
* @date 2020/2/29 上午5:13
*/
Product getProductById(int id) throws IOException;
/**
* 获取所有产品数据
*
* @param
* @return java.util.List<com.yuyuda.search.dto.Product>
* @throws IOException
* @author lzx@yuyuda.com
* @date 2020/2/29 上午5:13
*/
List<Product> getAll() throws IOException;
/**
* 根据字段和关键字 查询产品数据
*
* @param field
* @param keyword
* @return java.util.List<com.yuyuda.search.dto.Product>
* @throws IOException
* @author lzx@yuyuda.com
* @date 2020/2/29 上午5:13
*/
List<Product> searchByKeyword(String field, String keyword) throws IOException;
/**
* 字段不确定查询产品数据
* @param keyword
* @param page
* @return com.yate.search.dto.Page<com.yuyuda.search.dto.Product>
* @author lzx@yuyuda.com
* @date 2020/3/1 上午4:34
*/
Page<Product> unSureFieldSearchByKeyword(String keyword, int page) throws IOException;
}
5.3、service即dubbo的producer
package com.yate.search.service;
import com.yate.search.dto.Page;
import com.yate.search.dto.Product;
import com.yuyuda.search.entity.EsEntity;
import com.yuyuda.search.util.EsUtil;
import org.apache.dubbo.config.annotation.Service;
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.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.util.List;
/**
* 产品搜索服务
*
* @author lzx@yuyuda.com
* @version 1.0
* @date 2020/2/29 上午1:02
**/
@Service
public class ProductSearchServiceImpl implements IProductSearchService {
@Autowired
private EsUtil esUtil;
@Autowired
private RestHighLevelClient client;
@Value("${pageSize}")
private int pageSize;
/**
* 产品索引名称
*/
private static final String INDEX_PRODUCT = "index-product";
/**
* 产品索引格式
*/
private static final String CREATE_INDEX = "{\n" +
" \"properties\": {\n" +
" \"id\": {\n" +
" \"type\": \"long\"\n" +
" },\n" +
" \"productTitle\":{\n" +
" \"type\": \"text\",\n" +
" \"index\": true\n" +
" },\n" +
" \"biddingPrice\": {\n" +
" \"type\": \"double\"\n" +
" },\n" +
" \"productUnitPrice\": {\n" +
" \"type\": \"double\"\n" +
" },\n" +
" \"productImg\": {\n" +
" \"type\": \"text\"\n" +
" },\n" +
" \"createTime\": {\n" +
" \"type\": \"date\",\n" +
" \"format\": \"strict_date_optional_time||yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis\"\n" +
" },\n" +
" \"businessName\": {\n" +
" \"type\": \"text\",\n" +
" \"index\": true\n" +
" },\n" +
" \"businessId\": {\n" +
" \"type\": \"text\"\n" +
" },\n" +
" \"catagory\": {\n" +
" \"type\": \"text\",\n" +
" \"index\": true\n" +
" },\n" +
" \"catId\": {\n" +
" \"type\": \"text\"\n" +
" },\n" +
" \"productProperties\": {\n" +
" \"type\": \"nested\"\n" +
" }\n" +
" }\n" +
"}";
/**
* 初始化创建索引
*
* @return void
* @author lzx@yuyuda.com
* @date 2020/2/29 上午4:59
*/
@PostConstruct
public void createProductIndex() {
try {
boolean indexExist = esUtil.indexExist(INDEX_PRODUCT);
if (indexExist) {
return;
}
CreateIndexRequest request = new CreateIndexRequest(INDEX_PRODUCT);
request.settings(Settings.builder().put("index.number_of_shards", 1).put("index.number_of_replicas", 1));
request.mapping(CREATE_INDEX, XContentType.JSON);
CreateIndexResponse createIndexResponse = client.indices().create(request, RequestOptions.DEFAULT);
if (!createIndexResponse.isAcknowledged()) {
throw new RuntimeException("创建索引失败");
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 向索引中插入一条产品信息
*
* @return void
* @throws Exception
* @author lzx@yuyuda.com
* @date 2020/2/29 上午5:13
*/
@Override
public void putOne(Product product) throws Exception {
EsEntity<Product> esEntity = new EsEntity<>();
esEntity.setId(product.getId() + "");
esEntity.setData(product);
esUtil.insertOrUpdateOne(INDEX_PRODUCT, esEntity);
}
/**
* 通过id查询索引中的某条产品数据
*
* @return com.yate.yuyuda.dto.Product
* @author lzx@yuyuda.com
* @date 2020/2/29 上午5:13
*/
@Override
public Product getProductById(int id) throws IOException {
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(new TermQueryBuilder("id", id));
List<Product> search = esUtil.search(INDEX_PRODUCT, builder, Product.class);
if (search.size() > 0) {
return search.get(0);
} else {
return null;
}
}
/**
* 获取所有产品数据
*
* @return java.util.List<com.yuyuda.search.dto.Product>
* @author lzx@yuyuda.com
* @date 2020/2/29 上午5:13
*/
@Override
public List<Product> getAll() throws IOException {
return esUtil.search(INDEX_PRODUCT, new SearchSourceBuilder(), Product.class);
}
/**
* 根据字段和关键字 查询产品数据
*
* @return java.util.List<com.yate.search.dto.Product>
* @author lzx@yuyuda.com
* @date 2020/2/29 上午5:13
*/
@Override
public List<Product> searchByKeyword(String field, String keyword) throws IOException {
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
boolQueryBuilder.must(QueryBuilders.matchQuery(field, keyword));
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.size(10).query(boolQueryBuilder);
return esUtil.search(INDEX_PRODUCT, builder, Product.class);
}
/**
* 字段不确定查询产品数据
*
* @param keyword
* @param page
* @return java.util.Page<com.yuyuda.search.dto.Product>
* @throws IOException
* @author lzx@yuyuda.com
* @date 2020/3/1 上午2:11
*/
@Override
public Page<Product> unSureFieldSearchByKeyword(String keyword, int page) throws IOException {
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
boolQueryBuilder.must(QueryBuilders.multiMatchQuery(keyword, "businessName", "catagory", "productTitle"));
SearchSourceBuilder builder = new SearchSourceBuilder();
int start = (page - 1) * pageSize;
builder.from(start).size(pageSize).query(boolQueryBuilder);
return esUtil.searchPage(INDEX_PRODUCT, builder, Product.class, pageSize);
}
}
5.4、controller即dubbo的consumer
package com.yuyuda.search.controller;
import com.yuyuda.search.dto.Page;
import com.yuyuda.search.dto.Product;
import com.yuyuda.search.service.IProductSearchService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
/**
* 产品搜索操作控制器
*
* @author lzx@yuyuda.com
* @date 2020/2/29 上午8:53
*/
@RestController
@RequestMapping("/search")
@Api(value = "SearchProductController", tags = "搜索操作控制器")
public class SearchProductController {
@Reference(interfaceClass = IProductSearchService.class, check = false)
private IProductSearchService productSearchService;
/**
* 根据关键词进行搜索
* @param keyword
* @return java.util.Page<com.yuyu.search.dto.Product>
* @author lzx@yuyuda.com
* @date 2020/2/29 上午9:05
*/
@GetMapping("/get_product_by_keyword")
@ApiOperation("根据关键词进行搜索")
public Page<Product> getProductByKeyword(
@RequestParam(value = "keyword", required = true) String keyword,
@RequestParam(value = "page") int page
) {
Page<Product> productPage = null;
try {
page = page > 0 ? page : 1;
productPage = productSearchService.unSureFieldSearchByKeyword(keyword, page);
} catch (IOException e) {
e.printStackTrace();
}
return productPage;
}
}
六、前端业务流程
6.1、使用nuxtjs的注意事项
npm install -g npx
npx create-nuxt-app project-name
使用npx时,将require() 改成import报错解决办法
1 在dev、start指定--exec babel-node
2 在.babelrc文件中指定 "presets": ["es2015"]
3 并安装babel-preset-es2015
npm install babel-preset-es2015
使用npx时,scss支持报错,解决办法
安装支持
npm install sass-loader node-sass
6.2、nuxt.config.js的配置
export default {
mode: 'universal',
/*
** Headers of the page
*/
head: {
title: '搜索服务',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
},
/*
** Customize the progress-bar color
*/
loading: { color: '#fff' },
/*
** Global CSS
*/
css: [
],
/*
** Plugins to load before mounting the App
*/
plugins: [
],
/*
** Nuxt.js dev-modules
*/
buildModules: [
],
/*
** Nuxt.js modules
*/
modules: [
'bootstrap-vue/nuxt',
'@nuxtjs/axios',
// 请求代理配置,解决跨域
'@gauseen/nuxt-proxy',
],
proxyTable: { //axios跨域处理
'/api': { //此处并非和url一致
target:'http://192.168.3.15:9999/',
changeOrigin:true, //允许跨域
pathRewrite:{
'^/api': ''
}
}
},
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
extend (config, ctx) {
}
},
router:{
middleware:['authenticated']
},
server: {
port: 3000, // default: 3000
host: '0.0.0.0' // default: localhost
}
}
6.3、pages目录下新建searchInput.vue
<template>
<div class="top-advice">
<b-container class="search-bar">
<b-row>
<b-col cols="6">
<div class="search-bar-area">
<b-input-group>
<b-form-input type="text" size="md" v-model="search" @keyup.enter="searchProduct"></b-form-input>
<b-input-group-append>
<b-button size="md" variant="warning" @click="searchProduct">搜索</b-button>
</b-input-group-append>
</b-input-group>
</div>
</b-col>
</b-row>
</b-container>
</div>
</template>
<script>
export default {
data() {
return {
search: ''
}
},
methods: {
searchProduct: function () {
if (this.search) {
window.location.href = "/search?keyword=" + this.search
}
}
}
}
</script>
6.4、在pages目录下新建search.vue
<template>
<div class="container wrap">
<div class="content">
<list
:productList="productList"
:pageTotal="pageTotal"
:pageCount="pageCount"
@currPage="updatePage"
/>
</div>
</div>
</template>
<script>
import axios from "axios";
import List from "../components/list/List";
export default {
components: {
List,
},
data() {
return {
productPage: null
}
},
computed: {
productList: function () {
return this.productPage ? this.productPage.pageList : []
},
pageTotal: function () {
return this.productPage ? this.productPage.total : 0
},
pageCount: function () {
return this.productPage ? this.productPage.pageCount : 0
}
},
methods: {
updatePage: function (page) {
const keyword = encodeURIComponent(this.$route.query.keyword.toString());
axios.get("/api/s/search/get_product_by_keyword?keyword=" + keyword + "&page=" + page).then(({status, data}) => {
if (status === 200 && data.code === 200) {
this.productPage = data.result
}
});
}
},
async asyncData({app}) {
let keyword = encodeURIComponent(app.context.query.keyword);
const {status, data} = await axios.get("http://192.168.3.15:3000/api/s/search/get_product_by_keyword?keyword=" + keyword + "&page=1");
let productPage = null;
if(status === 200 && data.code === 200) {
productPage = data.result;
}
return {
productPage
}
}
}
</script>
6.5、在components/list的目录下新建List.vue组件
<template>
<div class="list">
<b-container class="bv-example-row">
<b-row class="recommended">
<b-col class="none-padding">
<ul class="ibody">
<li
v-for="item in productList"
:key="item.productTitle" @click="jumpProductInfo(item.id)">
<img :src="item.productImg" class="image"/>
<ul class="cbody">
<li class="title">{{ item.productTitle }}</li>
<li class="market-price">市场价: <span class="yuan">¥</span><span class="price">{{ item.productUnitPrice }}</span>
</li>
<li class="manufacturer-price">厂价: <span class="yuan">¥</span><span
class="price">{{ item.productUnitPrice }}</span></li>
</ul>
</li>
</ul>
</b-col>
</b-row>
<div class="overflow-auto page-right">
<div class="mt-3">
<b-pagination
v-model="currentPage"
pills
align="right"
:total-rows="pageCount"
:per-page="perPage"
first-text="首页"
prev-text="<上一页"
next-text="下一页>"
last-text="末页"
@input="$emit('currPage', currentPage)"
></b-pagination>
</div>
</div>
</b-container>
</div>
</template>
<script>
export default {
props: ["productList", "pageTotal", "pageCount"],
data: () => {
return {
img: '/img/11.jpg',
perPage: 1,
currentPage: 1
}
},
methods: {
jumpProductInfo: function (id) {
window.location.href = "/info?id=" + id
}
}
}
</script>
<style lang="scss">
.list {
background-color: #FFFFFF;
margin-top: 15px;
.page-right {
a {
color: #646464;
}
}
.recommended {
.none-padding {
padding: 0;
}
}
}
.ibody {
background-color: #fff;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
box-sizing: border-box;
padding-left: 2px;
> li {
float: left;
list-style: none;
padding: 10px;
background: #fff;
transition: background-color .5s;
cursor: pointer;
.image {
height: 208px;
width: 348px;
}
.cbody {
list-style: none;
.title {
font-size: 16px;
color: #222;
margin-top: 10px;
margin-bottom: 8px;
margin-left: -40px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.manufacturer-price, .market-price {
margin-left: -40px;
.yuan {
color: #ff5000;
font-family: Arial;
font-size: 12px;
}
.price {
color: #ff5000;
font-family: Arial;
}
}
}
}
}
</style>
6.6、关于vuejs组件的传值介绍
6.6.1、父组件向子组件传值
-
父组件中传的值,在引用组件的地方加上属性,如
<child :name=”name”/>
-
子组件用props进行声明
props: [“name”]
父组件:parent.vue
<template>
<div>
<child :name=”name”/>
</div>
</template>
<script>
import Child from "./child";
export default {
components: {
Child
},
data(){
return {
name: "aaa"
}
}
}
</script>
子组件:child.vue
<template>
<div>
{{name}}
</div>
</template>
<script>
export default {
props: ["name"]
}
</script>
6.6.2、子组件向父组件传值
-
子组件需要利用事件进行触发,再配合$emit(“update”)进行触发发射
-
父组件中需要定义子组件发射过来的事件@update=”updateAge”
子组件:
<template>
<button @click="$emit('update', param)">发射</button>
</template>
父组件:
<template>
<div>
<child @update=”updateAge”/>
</div>
</template>
<script>
import Child from "./child";
export default {
components: {
Child
},
methods: {
updateAge: function(param) {
Console.log(param)
}
}
}
</script>
6.6.3、跨组件传值
-
即需要使用vuex来传递,我此处是nuxtjs3.0之后的写法,例如公用菜单,任何页面都有可能用。
-
nuxtServerInit的使用,nuxtServerInit方法只有在store目录下index.js才能被调用,其他模块中写是调用不到的,我实践的时候花了一晚上才调试通。
6.6.3.1、在store目录下建立菜单模块menu.js
const state = () => ({
menu: []
});
const mutations = {
setMenu(state, v) {
state.menu = v
}
};
export default {
namespaced: true,
state,
mutations
}
6.6.3.2、在index.js中使用menu.js
import Menu from "./menu"
const store = {
modules: {
Menu //这里非常重要,当时我就卡在这里,因为没有写这个
},
actions: {
async nuxtServerInit({commit}, {req, app}) {
const {status, data} = await app.$axios.post("/api/p/product/tree_product_catagory");
if (status === 200 && data.code === 200 && data.result.length > 0) {
for (let i = 0; i < data.result.length; i++) {
data.result[i]['type'] = data.result[i]['idStr'].toString();
}
commit('menu/setMenu', data.result)
} else {
commit('menu/setMenu', [])
}
}
}
};
export default store
最后,一篇关于Elasticsearch、Springboot、Dubbo、Vue、Nuxt实现的搜索框服务实战到这里就结束了。
其实,偶尔写写公众号也是对自己用过的知识的一种背书、巩固、分享,因为不积跬步,无以至千里;不积小流,无以成江海。今年虽然被疫情搞得经济破了产,经济压力很大,心像打翻的五味瓶,惶惶不可终日,心中也有一首<<天问>>,“悟过改更,我又何言?”。也许是天将降大任于是人也,必先苦其心志,劳其筋骨,饿其体肤,空乏其身,行拂乱其所为,所以动心忍性,曾益其所不能吧。现在虽悟过改更,我又何言,但在技术上有所突破的成就感算是给了我丝丝安慰。
网友评论