Kibana 操作 ES+搜索

x33g5p2x  于2021-11-09 转载在 Kibana  
字(37.5k)|赞(0)|评价(0)|浏览(291)

一.使用 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

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 搜索学生数据

创建项目

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">
            &copy;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.测试

搜索出来高亮

相关文章

微信公众号

最新文章

更多