一.使用 Kibana 操作 ES
下载 Kibana 镜像
docker pull kibana:7.9.3
启动 Kibana 容器
docker run \
-d \
--name kibana \
--net es-net \
-p 5601:5601 \
-e ELASTICSEARCH_HOSTS='["http://node1:9200","http://node2:9200","http://node3:9200"]' \
--restart=always \
kibana:7.9.3
启动后,浏览器访问 Kibana,进入 Dev Tools:
索引、分片和副本
索引
Elasticsearch索引用来存储我们要搜索的数据,以倒排索引结构进行存储。
例如,要搜索商品数据,可以创建一个商品数据的索引,其中存储着所有商品的数据,供我们进行搜索:
当索引中存储了大量数据时,大量的磁盘io操作会降低整体搜索新能,这时需要对数据进行分片存储。
索引分片
在一个索引中存储大量数据会造成性能下降,这时可以对数据进行分片存储。
每个节点上都创建一个索引分片,把数据分散存放到多个节点的索引分片上,减少每个分片的数据量来提高io性能:
每个分片都是一个独立的索引,数据分散存放在多个分片中,也就是说,每个分片中存储的都是不同的数据。搜索时会同时搜索多个分片,并将搜索结果进行汇总。如果一个节点宕机分片不可用,则会造成部分数据无法搜索:
为了解决这一问题,可以对分片创建多个副本来解决。
索引副本
对分片创建多个副本,那么即使一个节点宕机,其他节点中的副本分片还可以继续工作,不会造成数据不可用:
分片的工作机制:
1.主分片的数据会复制到副本分片
2.搜索时,以负载均衡的方式工作,提高处理能力
3.主分片宕机时,其中一个副本分片会自动提升为主分片
下面我们就以上图的结构来创建 products 索引
创建索引
创建一个名为 products 的索引,用来存储商品数据。
分片和副本参数说明:
number_of_shards:分片数量,默认值是 5
number_of_replicas:副本数量,默认值是 1
我们有三个节点,在每个节点上都创建一个分片。每个分片在另两个节点上各创建一个副本。
# 创建索引,命名为 products
PUT /products
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 2
}
}
用索引名称过滤,查看 products 索引:
粗框为主分片,细框为副本分片
映射(数据结构)
类似于数据库表结构,索引数据也被分为多个数据字段,并且需要设置数据类型和其他属性。
映射,是对索引中字段结构的定义和描述。
字段的数据类型
常用类型:
数字类型:
- byte、short、integer、long
- float、double
- unsigned_long
字符串类型: - text : 会进行分词
- keyword : 不会进行分词,适用于email、主机地址、邮编等
日期和时间类型: - date
类型参考:
创建映射
在 products 索引中创建映射。
分词器设置:
- analyzer:在索引中添加文档时,text类型通过指定的分词器分词后,再插入倒排索引
- search_analyzer:使用关键词检索时,使用指定的分词器对关键词进行分词
查询时,关键词优先使用 search_analyzer 设置的分词器,如果 search_analyzer 不存在则使用 analyzer 分词器。
# 定义mapping,数据结构
PUT /products/_mapping
{
"properties": {
"id": {
"type": "long"
},
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"category": {
"type": "text",
"analyzer": "ik_smart",
"search_analyzer": "ik_smart"
},
"price": {
"type": "float"
},
"city": {
"type": "text",
"analyzer": "ik_smart",
"search_analyzer": "ik_smart"
},
"barcode": {
"type": "keyword"
}
}
}
查看映射
GET /products/_mapping
添加文档
添加的文档会有一个名为_id的文档id,这个文档id可以自动生成,也可以手动指定,通常可以使用数据的id作为文档id。
# 添加文档
PUT /products/_doc/10033
{
"id":"10033",
"title":"SONOS PLAY:5(gen2) 新一代PLAY:5无线智能音响系统 WiFi音箱家庭,潮酷数码会场",
"category":"潮酷数码会场",
"price":"3980.01",
"city":"上海",
"barcode":"527848718459"
}
PUT /products/_doc/10034
{
"id":"10034",
"title":"天猫魔盒 M13网络电视机顶盒 高清电视盒子wifi 64位硬盘播放器",
"category":"潮酷数码会场",
"price":"398.00",
"city":"浙江杭州",
"barcode":"522994634119"
}
PUT /products/_doc/10035
{
"id":"10035",
"title":"BOSE SoundSport耳塞式运动耳机 重低音入耳式防脱降噪音乐耳机",
"category":"潮酷数码会场",
"price":"860.00",
"city":"浙江杭州",
"barcode":"526558749068"
}
PUT /products/_doc/10036
{
"id":"10036",
"title":"【送支架】Beats studio Wireless 2.0无线蓝牙录音师头戴式耳机",
"category":"潮酷数码会场",
"price":"2889.00",
"city":"上海",
"barcode":"37147009748"
}
PUT /products/_doc/10037
{
"id":"10037",
"title":"SONOS PLAY:1无线智能音响系统 美国原创WiFi连接 家庭桌面音箱",
"category":"潮酷数码会场",
"price":"1580.01",
"city":"上海",
"barcode":"527783392239"
}
也可以自动生成 _id 值:
POST /products/_doc
{
"id":"10027",
"title":"vivo X9前置双摄全网通4G美颜自拍超薄智能手机大屏vivox9",
"category":"手机会场",
"price":"2798.00",
"city":"广东东莞",
"barcode":"541396973568"
}
查看文档:
GET /products/_doc/10037
查看指定文档title字段的分词结果:
GET /products/_doc/10037/_termvectors?fields=title
修改文档
底层索引数据无法修改,修改数据实际上是先删除再重新添加。
两种修改方式:
PUT:对文档进行完整的替换
POST:可以修改一部分字段
修改价格字段的值:
# 修改文档 - 替换
PUT /products/_doc/10037
{
"id":"10037",
"title":"SONOS PLAY:1无线智能音响系统 美国原创WiFi连接 家庭桌面音箱",
"category":"潮酷数码会场",
"price":"9999.99",
"city":"上海",
"barcode":"527783392239"
}
查看文档:
GET /products/_doc/10037
修改价格和城市字段的值:
# 修改文档 - 更新部分字段
POST /products/_update/10037
{
"doc": {
"price":"8888.88",
"city":"深圳"
}
}
查看文档:
GET /products/_doc/10037
删除文档
DELETE /products/_doc/10037
清空
POST /products/_delete_by_query
{
"query": {
"match_all": {}
}
}
删除索引
# 删除 products 索引
DELETE /products
可以尝试用不同的分片和副本值来重新创建 products 索引
二.搜索
导入测试数据
为了测试搜索功能,我们首先导入测试数据,3160条商品数据,数据样例如下
{ "index": {"_index": "pditems", "_id": "536563"}}
{ "id":"536563","brand":"联想","title":"联想(Lenovo)小新Air13 Pro 13.3英寸14.8mm超轻薄笔记本电脑","sell_point":"清仓!仅北京,武汉仓有货!","price":"6688.0","barcode":"","image":"/images/server/images/portal/air13/little4.jpg","cid":"163","status":"1","created":"2015-03-08 21:33:18","updated":"2015-04-11 20:38:38"}
下载测试数据
将压缩文件中的 pditems.json 上传到服务器
创建索引和映射
PUT /pditems
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 2
},
"mappings": {
"properties": {
"id": {
"type": "long"
},
"brand": {
"type": "text",
"analyzer": "ik_smart"
},
"title": {
"type": "text",
"analyzer": "ik_max_word"
},
"sell_point": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"price": {
"type": "float"
},
"image": {
"type": "keyword"
},
"cid": {
"type": "long"
},
"status": {
"type": "byte"
},
"created": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
},
"updated": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
}
}
}
}
用 head 查看索引:
导入数据
在服务器上,进入 pditems.json 所在的文件夹,执行批量数据导入:
curl -XPOST 'localhost:9200/pditems/_bulk' \
-H 'Content-Type:application/json' \
--data-binary @pditems.json
查看数据
搜索 pditems 索引中全部 3160 条数据:
GET /pditems/_search
{
"query": {
"match_all": {}
},
"size": 3160
}
搜索文档
搜索所有数据
# 搜索 pditems 索引中全部数据
POST /pditems/_search
{
"query": {
"match_all": {}
}
}
关键词搜索
# 查询 pditems 索引中title中包含"电脑"的商品
POST /pditems/_search
{
"query": {
"match": {
"title": "电脑"
}
}
}
搜索结果过滤器
# 价格大于2000,并且title中包含"电脑"的商品
POST /pditems/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "电脑"
}
}
],
"filter": [
{
"range": {
"price": {
"gte": "2000"
}
}
}
]
}
}
}
搜索结果高亮显示
em标签高亮
highlight高亮设置
multi_match多字段匹配
POST /pditems/_search
{
"query": {
"multi_match":{
"query": "手机",
"fields": ["title", "sell_point"]
}
},
"highlight" : {
"pre_tags" : ["<i class=\"highlight\">"],
"post_tags" : ["</i>"],
"fields" : {
"title" : {},
"sell_point" : {
"pre_tags": "<em>",
"post_tags": "</em>"
}
}
}
}
三.Spring Data Elasticsearch - 增删
Spring Data Elasticsearch
Spring Data Elasticsearch 是 Elasticsearch 搜索引擎开发的解决方案。它提供:
模板对象,用于存储、搜索、排序文档和构建聚合的高级API。
例如,Repository 使开发者能够通过定义具有自定义方法名称的接口来表达查询。
案例说明
在 Elasticsearch 中存储学生数据,并对学生数据进行搜索测试。
数据结构:
案例测试以下数据操作:
1.创建 students 索引和映射
2.C - 创建学生数据
3.R - 访问学生数据
4.U - 修改学生数据
5.D - 删除学生数据
6.使用 Repository 和 Criteria 搜索学生数据
创建项目
1.新建工程
2.新建 springboot module,添加 spring data elasticsearch 依赖
3.项目的 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.3.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tedu</groupId>
<artifactId>es-springboot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>es-springboot</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</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>
</project>
application.yml 配置
logging.level.tracer=TRACE 作用是在控制台中显示底层的查询日志
spring:
elasticsearch:
rest:
uris: http://192.168.64.181:9200
logging:
level:
tracer: TRACE
Student 实体类
package cn.tedu.es.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
// spring data es API 可以根据这里的设置
// 在服务器新建索引
// 一般情况下,索引应该自己在服务器上手动创建
@Document(indexName = "students",shards = 3,replicas = 2)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
@Id // 使用学号作为索引id
private Long id;
private String name;
private Character gender;
@Field("birthDate") // es索引中的字段名,与变量名相同可以省略
private String birthDate;
}
@Document 注解
@Documnet注解对索引的参数进行设置。
上面代码中,把 students 索引的分片数设置为3,副本数设置为2。
@Id 注解
在 Elasticsearch 中创建文档时,使用 @Id 注解的字段作为文档的 _id 值
@Field 注解
通过 @Field 注解设置字段的数据类型和其他属性。
文本类型 text 和 keyword
text 类型会进行分词。
keyword 不会分词。
analyzer 指定分词器
通过 analyzer 设置可以指定分词器,例如 ik_smart、ik_max_word 等。
我们这个例子中,对学生姓名字段使用的分词器是 ngram 分词器,其分词效果如下面例子所示:
通过 ElasticsearchRepository 实现 CRUD 操作
Spring Data 的 Repository 接口提供了一种声明式的数据操作规范,无序编写任何代码,只需遵循 Spring Data 的方法定义规范即可完成数据的 CRUD 操作。
ElasticsearchRepository 继承自 Repository,其中已经预定义了基本的 CURD 方法,我们可以通过继承 ElasticsearchRepository,添加自定义的数据操作方法。
Repository 方法命名规范
自定义数据操作方法需要遵循 Repository 规范,示例如下:
关键词 | 方法名 | es查询 |
And | findByNameAndPrice | { “query” : { “bool” : { “must” : [ { “query_string” : { “query” : “?”, “fields” : [ “name” ] } }, { “query_string” : { “query” : “?”, “fields” : [ “price” ] } } ] } }} |
Or | findByNameOrPrice | { “query” : { “bool” : { “should” : [ { “query_string” : { “query” : “?”, “fields” : [ “name” ] } }, { “query_string” : { “query” : “?”, “fields” : [ “price” ] } } ] } }} |
Is | findByName | { “query” : { “bool” : { “must” : [ { “query_string” : { “query” : “?”, “fields” : [ “name” ] } } ] } }} |
Not | findByNameNot | { “query” : { “bool” : { “must_not” : [ { “query_string” : { “query” : “?”, “fields” : [ “name” ] } } ] } }} |
Between | findByPriceBetween | { “query” : { “bool” : { “must” : [ {“range” : {“price” : {“from” : ?, “to” : ?, “include_lower” : true, “include_upper” : true } } } ] } }} |
LessThan | findByPriceLessThan | { “query” : { “bool” : { “must” : [ {“range” : {“price” : {“from” : null, “to” : ?, “include_lower” : true, “include_upper” : false } } } ] } }} |
LessThanEqual | findByPriceLessThanEqual | { “query” : { “bool” : { “must” : [ {“range” : {“price” : {“from” : null, “to” : ?, “include_lower” : true, “include_upper” : true } } } ] } }} |
GreaterThan | findByPriceGreaterThan | { “query” : { “bool” : { “must” : [ {“range” : {“price” : {“from” : ?, “to” : null, “include_lower” : false, “include_upper” : true } } } ] } }} |
GreaterThanEqual | findByPriceGreaterThan | { “query” : { “bool” : { “must” : [ {“range” : {“price” : {“from” : ?, “to” : null, “include_lower” : true, “include_upper” : true } } } ] } }} |
Before | findByPriceBefore | { “query” : { “bool” : { “must” : [ {“range” : {“price” : {“from” : null, “to” : ?, “include_lower” : true, “include_upper” : true } } } ] } }} |
After | findByPriceAfter | { “query” : { “bool” : { “must” : [ {“range” : {“price” : {“from” : ?, “to” : null, “include_lower” : true, “include_upper” : true } } } ] } }} |
Like | findByNameLike | { “query” : { “bool” : { “must” : [ { “query_string” : { “query” : “?*”, “fields” : [ “name” ] }, “analyze_wildcard”: true } ] } }} |
StartingWith | findByNameStartingWith | { “query” : { “bool” : { “must” : [ { “query_string” : { “query” : “?*”, “fields” : [ “name” ] }, “analyze_wildcard”: true } ] } }} |
EndingWith | findByNameEndingWith | { “query” : { “bool” : { “must” : [ { “query_string” : { “query” : “*?”, “fields” : [ “name” ] }, “analyze_wildcard”: true } ] } }} |
Contains/Containing | findByNameContaining | { “query” : { “bool” : { “must” : [ { “query_string” : { “query” : “?”, “fields” : [ “name” ] }, “analyze_wildcard”: true } ] } }} |
In (when annotated as FieldType.Keyword) | findByNameIn(Collectionnames) | { “query” : { “bool” : { “must” : [ {“bool” : {“must” : [ {“terms” : {“name” : ["?","?"]}} ] } } ] } }} |
In | findByNameIn(Collectionnames) | { “query”: {“bool”: {“must”: [{“query_string”:{“query”: “”?" “?”", “fields”: [“name”]}}]}}} |
NotIn (when annotated as FieldType.Keyword) | findByNameNotIn(Collectionnames) | { “query” : { “bool” : { “must” : [ {“bool” : {“must_not” : [ {“terms” : {“name” : ["?","?"]}} ] } } ] } }} |
NotIn | findByNameNotIn(Collectionnames) | {“query”: {“bool”: {“must”: [{“query_string”: {“query”: “NOT(”?" “?”)", “fields”: [“name”]}}]}}} |
Near | findByStoreNear | Not Supported Yet ! |
True | findByAvailableTrue | { “query” : { “bool” : { “must” : [ { “query_string” : { “query” : “true”, “fields” : [ “available” ] } } ] } }} |
False | findByAvailableFalse | { “query” : { “bool” : { “must” : [ { “query_string” : { “query” : “false”, “fields” : [ “available” ] } } ] } }} |
OrderBy | findByAvailableTrueOrderByNameDesc | { “query” : { “bool” : { “must” : [ { “query_string” : { “query” : “true”, “fields” : [ “available” ] } } ] } }, “sort”:[{“name”:{“order”:“desc”}}] } |
StudentRepository
package cn.tedu.esspringboot.es;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
public interface StudentRepository extends ElasticsearchRepository<Student, Long> {
List<Student> findByName(String name);
List<Student> findByNameOrBirthDate(String name, String birthDate);
}
业务类 StudentService
package cn.tedu.esspringboot.es;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class StudentService {
@Autowired
private StudentRepository studentRepo;
public void save(Student student) {
studentRepo.save(student);
}
public void delete(Long id) {
studentRepo.deleteById(id);
}
public void update(Student student) {
save(student);
}
public List<Student> findByName(String name) {
return studentRepo.findByName(name);
}
public List<Student> findByNameOrBirthDate(String name, String birthDate) {
return studentRepo.findByNameOrBirthDate(name, birthDate);
}
}
在 Elasticsearch 中创建 students 索引
在开始运行测试之前,在 Elasticsearch 中先创建 students 索引:
PUT /students
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 2,
"index.max_ngram_diff":30,
"analysis": {
"analyzer": {
"ngram_analyzer": {
"tokenizer": "ngram_tokenizer"
}
},
"tokenizer": {
"ngram_tokenizer": {
"type": "ngram",
"min_gram": 1,
"max_gram": 30,
"token_chars": [
"letter",
"digit"
]
}
}
}
},
"mappings": {
"properties": {
"id": {
"type": "long"
},
"name": {
"type": "text",
"analyzer": "ngram_analyzer"
},
"gender": {
"type": "keyword"
},
"birthDate": {
"type": "date",
"format": "yyyy-MM-dd"
}
}
}
}
测试学生数据的 CRUD 操作
添加测试类,对学生数据进行 CRUD 测试
package cn.tedu.esspringboot;
import cn.tedu.esspringboot.es.Student;
import cn.tedu.esspringboot.es.StudentService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
public class Test1 {
@Autowired
private StudentService studentService;
@Test
public void test1() {
studentService.save(new Student(998L,"张三",'男',"2020-12-04"));
}
@Test
public void test2() {
studentService.update(new Student(1L,"李四",'女',"2020-12-04"));
}
@Test
public void test3() {
List<Student> stu = studentService.findByName("四");
System.out.println(stu);
}
@Test
public void test4() throws Exception {
List<Student> stu;
stu = studentService.findByNameOrBirthDate("四", "1999-09-09");
System.out.println(stu);
stu = studentService.findByNameOrBirthDate("SFSDFS", "2020-12-04");
System.out.println(stu);
}
}
依次运行每个测试方法,并使用 head 观察测试结果
使用 Criteria 构建查询
Spring Data Elasticsearch 中,可以使用 SearchOperations 工具执行一些更复杂的查询,这些查询操作接收一个 Query 对象封装的查询操作。
Spring Data Elasticsearch 中的 Query 有三种:
- CriteriaQuery
- StringQuery
- NativeSearchQuery
多数情况下,CriteriaQuery 都可以满足我们的查询求。下面来看两个 Criteria 查询示例:
StudentSearcher
package cn.tedu.esspringboot.es;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class StudentSearcher {
@Autowired
private ElasticsearchOperations searchOperations;
public List<Student> searchByBirthDate(String birthDate) {
Criteria c = new Criteria("birthDate").is(birthDate);
return criteriaSearch(c);
}
public List<Student> searchByBirthDate(String ge, String le) {
Criteria c = new Criteria("birthDate").between(ge, le);
return criteriaSearch(c);
}
private List<Student> criteriaSearch(Criteria c) {
CriteriaQuery q = new CriteriaQuery(c);
SearchHits<Student> hits = searchOperations.search(q, Student.class);
List<Student> list = hits.stream().map(SearchHit::getContent).collect(Collectors.toList());
return list;
}
}
修改 StudentService
在 StudentService 中,调用 StudentSearcher,执行查询:
package cn.tedu.esspringboot.es;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class StudentService {
@Autowired
private StudentRepository studentRepo;
@Autowired
private StudentSearcher studentSearcher;
public void save(Student student) {
studentRepo.save(student);
}
public void delete(Long id) {
studentRepo.deleteById(id);
}
public void update(Student student) {
save(student);
}
public List<Student> findByName(String name) {
return studentRepo.findByName(name);
}
public List<Student> findByNameOrBirthDate(String name, String birthDate) {
return studentRepo.findByNameOrBirthDate(name, birthDate);
}
public List<Student> findByBirthDate(String birthDate) {
return studentSearcher.searchByBirthDate(birthDate);
}
public List<Student> findByBirthDate(String ge, String le) {
return studentSearcher.searchByBirthDate(ge, le);
}
}
在测试类中添加测试方法
package cn.tedu.esspringboot;
import cn.tedu.esspringboot.es.Student;
import cn.tedu.esspringboot.es.StudentService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
public class Test1 {
@Autowired
private StudentService studentService;
@Test
public void test1() {
studentService.save(new Student(998L,"张三",'男',"2020-12-04"));
}
@Test
public void test2() {
studentService.update(new Student(1L,"李四",'女',"2020-12-04"));
}
@Test
public void test3() {
List<Student> stu = studentService.findByName("四");
System.out.println(stu);
}
@Test
public void test4() throws Exception {
List<Student> stu;
stu = studentService.findByNameOrBirthDate("四", "1999-09-09");
System.out.println(stu);
stu = studentService.findByNameOrBirthDate("SFSDFS", "2020-12-04");
System.out.println(stu);
}
@Test
public void test5() throws Exception {
List<Student> stu;
stu = studentService.findByBirthDate("2020-12-04");
System.out.println(stu);
}
@Test
public void test6() throws Exception {
List<Student> stu;
stu = studentService.findByBirthDate("2020-12-05", "2020-12-09");
System.out.println(stu);
}
}
四.拼多商城商品搜索+高亮
1. 添加 spring data es 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
2. yml 配置 es 服务器地址
spring:
elasticsearch:
rest:
uris:
- http://192.168.64.181:9200
- http://192.168.64.181:9201
- http://192.168.64.181:9202
3. 新建实体类 Item,封装从 es 搜索的数据
package com.pd.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
@Document(indexName = "pditems")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Item {
@Id
private Long id;
private String brand;
private String title;
@Field("sell_point")
private String sellPoint;
private String price;
private String image;
}
4. 新建 ItemRepository 接口
package com.pd.es;
import com.pd.pojo.Item;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.annotations.Highlight;
import org.springframework.data.elasticsearch.annotations.HighlightField;
import org.springframework.data.elasticsearch.annotations.HighlightParameters;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
/**
* 做高亮显示
*/
public interface ItemRepository extends ElasticsearchRepository<Item,Long> {
/**
* 如果要做高亮显示,高亮结果会封装到SearchHit对象
* @param kye1
* @param key2
* @param pageable
* @return
*/
@Highlight(parameters = @HighlightParameters(
preTags = "<em>",
postTags = "</em>"
),
fields = {
@HighlightField(name="title"),
@HighlightField(name = "sellPoint")
})
List<SearchHit<Item>> findByTitleOrSellPoint(String kye1, String key2, Pageable pageable);
}
5. 添加搜索方法: findByTitleOrSellPoint()
6. SearchService
package com.pd.service;
import com.pd.pojo.Item;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.SearchHit;
import java.util.List;
public interface SearchService {
List<SearchHit<Item>> search(String key, Pageable pageable);
}
7. SearchController
package com.pd.controller;
import com.pd.pojo.Item;
import com.pd.service.SearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.ArrayList;
import java.util.List;
@Controller
public class SearchController {
@Autowired
private SearchService searchService;
@GetMapping("/search/toSearch.html") // ?key=手机&page=0&size=20
public String search(Model model, String key, Pageable pageable) {
List<SearchHit<Item>> r = searchService.search(key, pageable);
// 把所有 SearchHit 中的 Item 对象拿出来,放入一个新的 List<Item> 集合
List<Item> list = new ArrayList<>();
for (SearchHit<Item> sh : r) {
Item item = sh.getContent();//从 SearchHit 取出上商品对象
// SearchHit 对象中的高亮数据
// ["xxx", "<em>", "手机", "</em>", "xxxxx"]
List<String> titleHighlight = sh.getHighlightField("title");
// 把高亮的 title 放入 item,替换原始的 title
item.setTitle(highlightTiele(titleHighlight));
list.add(item);
}
// 集合放入model对象,传递到 jsp 界面进行显示
model.addAttribute("list", list);
model.addAttribute("p", pageable);
return "/search.jsp";
}
private String highlightTiele(List<String> titleHighlight) {
StringBuilder sb = new StringBuilder();
for (String s : titleHighlight) {
sb.append(s);
}
return sb.toString();
}
}
8. search.jsp
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<%@ taglib prefix="c"
uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page isELIgnored="false" %>
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>商品搜索页面</title>
<link rel="stylesheet" href="../css/header.css" />
<link rel="stylesheet" href="../css/search.css" />
<link rel="stylesheet" href="../css/footer.css" />
<style>
div.describe p em {
color: #f00;
}
</style>
</head>
<jsp:include page="commons/header.jsp"></jsp:include>
<body>
<div class="big">
<form name="" action="" method="post">
<section id="section">
<p class="header"> 搜索结果 > ${param.key} </p>
<div id="content_box">
<%-- ${list} 从Model获取list属性:List<Item> --%>
<c:forEach items="${list}" var="solrItem">
<div class="lf" id="d1">
<div class="img">
<!-- ../images/search/product_img.png -->
<img src="${solrItem.image}" alt="" onclick="toItemInfo(${solrItem.id})" />
</div>
<div class="describe">
<p onclick="toItemInfo(${solrItem.id})">
${solrItem.title}
</p>
<span class="price"><b>¥</b>
<span class="priceContent">
${solrItem.price}</span></span>
<span class="addCart"><img id="collect" src="../images/search/care.png" alt="" /><a href="javascript:void(0);" class="add_cart">加入购物车</a></span>
<!--<span class="succee" style="display: none">
<img src="/images/search/product_true.png" alt="" />
<span>已移入购物车</span>
</span>-->
</div>
</div>
</c:forEach>
</div>
<c:if test="${list.size() == 0}">
没有更多商品了!
</c:if>
<c:if test="${p.pageNumber > 0}">
<a href="?key=${param.key}&page=${p.pageNumber-1}&size=${p.pageSize}">上一页</a>
</c:if>
<c:if test="${list.size() != 0}">
<a href="?key=${param.key}&page=${p.pageNumber+1}&size=${p.pageSize}">下一页</a>
</c:if>
</section>
</form>
</div>
<!-- 尾部-->
<!-- 页面底部-->
<div class="foot_bj">
<div id="foot">
<div class="lf">
<p class="footer1"><img src="../images/footer/logo.png" alt="" class=" footLogo"/></p>
<p class="footer2"><img src="../images/footer/footerFont.png"alt=""/></p>
</div>
<div class="foot_left lf" >
<ul>
<li><a href="#"><h3>买家帮助</h3></a></li>
<li><a href="#">新手指南</a></li>
<li><a href="#">服务保障</a></li>
<li><a href="#">常见问题</a></li>
</ul>
<ul>
<li><a href="#"><h3>商家帮助</h3></a></li>
<li><a href="#">商家入驻</a></li>
<li><a href="#">商家后台</a></li>
</ul>
<ul>
<li><a href="#"><h3>关于我们</h3></a></li>
<li><a href="#">关于拼多</a></li>
<li><a href="#">联系我们</a></li>
<li>
<img src="../images/footer/wechat.png" alt=""/>
<img src="../images/footer/sinablog.png" alt=""/>
</li>
</ul>
</div>
<div class="service">
<p>拼多商城客户端</p>
<img src="../images/footer/ios.png" class="lf">
<img src="../images/footer/android.png" alt="" class="lf"/>
</div>
<div class="download">
<img src="../images/footer/erweima.png">
</div>
<!-- 页面底部-备案号 #footer -->
<div class="record">
©2017 拼多集团有限公司 版权所有 京ICP证xxxxxxxxxxx
</div>
</div>
</div>
<div class="modal" style="display:none">
<div class="modal_dialog">
<div class="modal_header">
操作提醒
</div>
<div class="modal_information">
<img src="../images/model/model_img2.png" alt=""/>
<span>将您的宝贝加入购物车?</span>
</div>
<div class="yes"><span>确定</span></div>
<div class="no"><span>取消</span></div>
</div>
</div>
<script src="../js/jquery-3.1.1.min.js"></script>
<script src="../js/index.js"></script>
<script src="../js/jquery.page.js"></script>
<script>
$(".add_cart").click(function(){
$(".modal").show();
$(".modal .modal_information span").html("将您的宝贝加入购物车?");
})
$(".yes").click(function(){
$(".modal").hide();
})
$('.no').click(function(){
$('.modal').hide();
})
</script>
<!--<script type="text/javascript">
// var status = ${status};
var pages = ${pageBean.totalPages};
var index = ${pageBean.pageIndex};
$(".tcdPageCode").createPage({
// 总页数
pageCount:pages,
// 起始页
current:index,
backFn:function(p){
// 执行代码
window.location.href="http://localhost:18888/search.html?q=${q}&page="+p;
}
});
</script>-->
<!--<script type="text/javascript">
/* 商品详情页 */
function toItemInfo(id) {
if (id) {
window.location.href="/toItemInfo/"+id+".html";
}else {
alert("商品id不存在");
}
}
</script>-->
<script type="text/javascript">
/**添加到收藏**/
$("#collect").click(function(e){
$(".modal").show();
$(".modal .modal_information span").html("将您的宝贝加入收藏夹");
})
$(".yes").click(function(){
$(".modal").hide();
$('#collect').attr("src","../images/search/care1.png");
})
</script>
</body>
</html>
9.Header.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!-- 页面顶部-->
<header id="top">
<div id="logo" class="lf">
<a href="/"> <img src="/images/server/images/portal/header/logo.png" alt="logo" />
</a>
</div>
<div id="top_input" class="lf">
<c:choose>
<c:when test="${not empty param.key}">
<input id="input" type="text" value="${param.key}" />
</c:when>
<c:otherwise>
<input id="input" type="text" placeholder="请输入您要搜索的内容" />
</c:otherwise>
</c:choose>
<div class="seek" tabindex="-1">
<div class="actived" ><span>分类搜索</span> <img src="/images/server/images/portal/header/header_normal.png" alt=""/></div>
<div class="seek_content" >
<div id="shcy" >生活餐饮</div>
<div id="xxyp" >学习用品</div>
<div id="srdz" >私人订制</div>
</div>
</div>
<a href="javascript:void(0);" class="rt" onclick="search1()"><img id="search"
src="/images/server/images/portal/header/search.png" alt="搜索"/></a>
</div>
<div class="rt">
<ul class="lf" id="iul">
<li><a href="/collect/toMyCollect.html" title="我的收藏"> <img class="care"
src="/images/server/images/portal/header/care.png"
alt="" />
</a><b>|</b></li>
<li><a href="/order/toMyOrder.html" title="我的订单"> <img class="order"
src="/images/server/images/portal/header/order.png" alt="" />
</a><b>|</b></li>
<li><a href="/cart/toCart.html" title="我的购物车"> <img class="shopcar"
src="/images/server/images/portal/header/shop_car.png" alt="" />
</a><b>|</b></li>
<li></li>
</ul>
</div>
<br />
</header>
<nav id="nav">
<ul>
<li><a href="/">首页</a></li>
<li><a href="/food/toItemFood.html">生活餐饮</a></li>
<li><a href="/toCate.html">学习用品</a></li>
<li><a href="/lookforward.html">私人定制</a></li>
</ul>
</nav>
<script src="/js/jquery-3.1.1.min.js"></script>
<script src="/js/slide.js"></script>
<script type="text/javascript">
function logout() {
$.ajax({
url : '/user/logout.html',
type : 'post',
dataType:'json',
success:function(result) {
if (result != null && result != "" && result != undefined) {
if (result.status == 200) {
//alert(result.msg);
window.location.href = "/user/toLogin.html";
}else {
alert(result.msg);
}
}
},
error:function() {
alert('退出失败!');
}
});
}
</script>
<script>
$('#nav>ul>li').click(function(){
$(this).children().addClass('active');
$(this).siblings().children().removeClass('active');
})
</script>
<script src="/js/jquery.cookie.js"></script>
<script type="text/javascript">
$(function () {
//请求本网站checkLogin.html,checkLogin()用httpClient做代理去访问sso
$.ajax({
type:"POST",
url:"/user/checkLogin.html",
xhrFields:{withCredentials:true},
dataType:"json",
success:function(result){
var user = result.data;
console.log(result);
if (result.status === 200) {
$("#iul").append('<li><a href="/lookforward.html">'+user.username+'</a><b>|</b></li><li><a href="/address/list.html">地址管理</a> | <a href="javascript:;" οnclick="logout()">退出</a></li>');
}else if(result.status === 500){
$("#iul").append('<li><a href="/user/toLogin.html">登录</a></li>');
}
},
error:function(textStatus,XMLHttpRequest){
//alert("系统异常!");
}
});
//$.cookie出异常
//var ticket = $.cookie("DN_TICKET");
//服务器返回的是js,这种处理跨域的方式叫jsonp
/* $.ajax({
type:"post",
url:"http://sso.ajstore.com:90/user/checkLoginForJsonp.html",
dataType:"jsonp",
jsonp:"jsonpCallback",//jsonpCallback是服务器端接收参数的参数名
xhrFields:{withCredentials:true},//ajax默认不发送cookie
//浏览器收到的是jquery(json字符串)
//函数名jquery
//执行函数jquery,得到的是json字符串,再调用success,把json字符串传过来了
success:function(result){
var user = result.data;
console.log(result);
if (result.status === 200) {
$("#iul").append('<li><a href="/lookforward.html">'+user.username+'</a><b>|</b></li><li><a href="javascript:;" οnclick="logout()">退出</a></li>');
}else if(result.status === 500){
$("#iul").append('<li><a href="http://sso.ajstore.com:90/user/toLogin.html?callback=http://www.ajstore.com">登录</a></li>');
}
},
error:function(textStatus,XMLHttpRequest){
alert("系统异常!"+JSON.stringify(textStatus)+" ------ "+XMLHttpRequest);
}
}); */
//服务器返回的是json
/* $.ajax({
type:"post",
url:"http://sso.ajstore.com:90/user/checkLogin.html",
dataType:"json",//原先是jsonp要改成json
xhrFields:{withCredentials:true},//ajax默认不发送cookie
success:function(result){
var user = result.data;
console.log(result);
if (result.status === 200) {
$("#iul").append('<li><a href="/lookforward.html">'+user.username+'</a><b>|</b></li><li><a href="javascript:;" οnclick="logout()">退出</a></li>');
}else if(result.status === 500){
$("#iul").append('<li><a href="http://sso.ajstore.com:90/user/toLogin.html?callback=http://www.ajstore.com">登录</a></li>');
}
},
error:function(textStatus,XMLHttpRequest){
alert("系统异常!"+JSON.stringify(textStatus)+" ------ "+XMLHttpRequest);
}
}); */
})
</script>
<script>
function search1(){
var q=$("#input").val();
console.log(q);
window.location.href = "/search/toSearch.html?key="+q;
}
</script>
<script type="text/javascript">
document.onkeydown=keyDownSearch;
function keyDownSearch(e) {
var theEvent = e || window.event;
var code = theEvent.keyCode || theEvent.which || theEvent.charCode;
if (code == 13) {
search1();
return false;
}
return true;
}
</script>
10.测试
搜索出来高亮