docker pull kibana:7.9.3
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 索引:
粗框为主分片,细框为副本分片
类似于数据库表结构,索引数据也被分为多个数据字段,并且需要设置数据类型和其他属性。
映射,是对索引中字段结构的定义和描述。
常用类型:
数字类型:
在 products 索引中创建映射。
分词器设置:
# 定义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>"
}
}
}
}
https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#reference
Spring Data Elasticsearch 是 Elasticsearch 搜索引擎开发的解决方案。它提供:
模板对象,用于存储、搜索、排序文档和构建聚合的高级API。
例如,Repository 使开发者能够通过定义具有自定义方法名称的接口来表达查询。
在 Elasticsearch 中存储学生数据,并对学生数据进行搜索测试。
案例测试以下数据操作:
1.创建 students 索引和映射
2.C - 创建学生数据
3.R - 访问学生数据
4.U - 修改学生数据
5.D - 删除学生数据
6.使用 Repository 和 Criteria 搜索学生数据
<?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>
logging.level.tracer=TRACE 作用是在控制台中显示底层的查询日志
spring:
elasticsearch:
rest:
uris: http://192.168.64.181:9200
logging:
level:
tracer: TRACE
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;
}
@Documnet注解对索引的参数进行设置。
上面代码中,把 students 索引的分片数设置为3,副本数设置为2。
在 Elasticsearch 中创建文档时,使用 @Id 注解的字段作为文档的 _id 值
通过 @Field 注解设置字段的数据类型和其他属性。
text 类型会进行分词。
keyword 不会分词。
通过 analyzer 设置可以指定分词器,例如 ik_smart、ik_max_word 等。
我们这个例子中,对学生姓名字段使用的分词器是 ngram 分词器,其分词效果如下面例子所示:
Spring Data 的 Repository 接口提供了一种声明式的数据操作规范,无序编写任何代码,只需遵循 Spring Data 的方法定义规范即可完成数据的 CRUD 操作。
ElasticsearchRepository 继承自 Repository,其中已经预定义了基本的 CURD 方法,我们可以通过继承 ElasticsearchRepository,添加自定义的数据操作方法。
自定义数据操作方法需要遵循 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”}}] } |
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);
}
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 索引:
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 测试
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 观察测试结果
Spring Data Elasticsearch 中,可以使用 SearchOperations 工具执行一些更复杂的查询,这些查询操作接收一个 Query 对象封装的查询操作。
Spring Data Elasticsearch 中的 Query 有三种:
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 中,调用 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);
}
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
spring:
elasticsearch:
rest:
uris:
- http://192.168.64.181:9200
- http://192.168.64.181:9201
- http://192.168.64.181:9202
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;
}
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);
}
findByTitleOrSellPoint()
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);
}
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();
}
}
<%@ 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>
<%@ 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>
搜索出来高亮
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/weixin_53244569/article/details/121145533
内容来源于网络,如有侵权,请联系作者删除!