Elasticsearch入门及整合springboot

1. Elasticsearch概述

1.1 搜索是什么

概念:用户输入想要的关键词,返回含有该关键词的所有信息。
场景:
	1. 互联网搜索:谷歌、百度、各种新闻首页
	2. 站内搜索(垂直搜索):企业OA查询订单、人员、部门,
	  					  电商网站内部搜索商品(淘宝、京东)场景。

1.2 数据库做搜索弊端

1.2.1 站内搜索(垂直搜索):
(若数据量小,简单搜索,可以使用数据库)

问题出现:

	1. 存储问题: 电商网站商品上亿条时,涉及到单表数据过大必须拆分表,
				数据库磁盘占用过大必须分库(mycat)。

	2. 性能问题:解决上面问题后,若查询“笔记本电脑”等关键词时,上亿条数据
				的商品名字段逐行扫描,性能跟不上。

	3. 不能分词搜索: 如搜索“笔记本电脑”,只能搜索完全和关键词一样的数据,
				那么数据量小时,搜索“笔记电脑”,“电脑”数据要不要给用户。
1.2.2互联网搜索:

肯定不会使用数据库搜索,数据量太大(PB级)

可以使用搜索引擎来解决数据库搜索的问题:
	搜索也是一款数据库,搜索可以进行分词搜索---搜索速度非常快

1.3 常见的搜索引擎

【solr】:  
	Solr是一个独立的企业级搜索应用服务器,
 	它对外提供类似于Web-service的API接口。
 	用户可以通过http请求,向搜索引擎服务器提交一定格式的XML文件,生成索引;
 	也可以通过Http Get操作提出查找请求,并得到XML格式的返回结果,
 	
 	特点:是一个高性能,采用Java开发,基于Lucene的全文搜索服务器.
 		同时对其进行了扩展,提供了比Lucene更为丰富的查询语言,
 		同时实现了可配置、可扩展并对查询性能进行了优化,
 		并且提供了一个完善的功能管理界面,是一款非常优秀的全文搜索引擎。

【ElasticSearch】:
	ElasticSearch是一个基于Lucene的搜索服务器。
	它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。
	Elasticsearch是用Java语言开发的,并作为Apache许可条款下的开放源码发布,是一种流行的企业级搜索引擎。
	ElasticSearch用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。
	官方客户端在Java、.NET(C#)、PHP、Python、Apache Groovy、Ruby和许多其他语言中都是可用的。
	根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr,也是基于Lucene。

ElasticSearch与Solr比较:

  1. 当单纯的对已有数据进行搜索时,Solr更快
    Elasticsearch入门及整合springboot_第1张图片

  2. 当实时建立索引时,Solr会产生io阻塞,查询性能较差,ElasticSearch具有明显的优势
    Elasticsearch入门及整合springboot_第2张图片

  3. 随着数据量的增加,Solr的搜索效率会变得更低,而ElasticSearch却没有明显的变化
    Elasticsearch入门及整合springboot_第3张图片

     总结: 
     
     	1. es基本是开箱即用(解压就可以用!) ,非常简单。Solr 安装略微复杂一丢丢! 
     	2. Solr 利用Zookeeper进行分布式管理,而 Elasticsearch自身带有分布式协调管理功能. 
     	3. Solr 支持更多格式的数据,比如JSON、XML、CSV ,而 Elasticsearch仅支持json文件格式。 
     	4. Solr 官方提供的功能更多,而Elasticsearch本身更注重于核心功能,高级功能多有第三方插件提供,例如图形化界面需要kibana支撑 
     	5. Solr 查询快,但更新索引时慢(即插入删除慢) ,用于电商等查询多的应用; 
     	6. ES建立索引快(即查询慢) ,即实时性查询快,用于facebook新浪等搜索。 
     	7. Solr是传统搜索应用的有力解决方案,但Elasticsearch更适用于 新兴的实时搜索应用。 
     	8. Solr比较成熟,有一个更大,更成熟的用户、开发和贡献者社区, 而Elasticsearch相对开发维护者较少,更新太快,学习使用成本较高。
    

1.4 Elasticsearch介绍

The Elastic Stack, 包括 Elasticsearch【搜索,分析】、 Kibana【可视化】、 Beats 和 Logstash【数据的搜集】(也称为 ELK Stack)。

能够安全可靠地获取任何来源、任何格式的数据,然后实时地对数据进行搜索、分析和可视化。

Elaticsearch,简称为 ES, ES 是一个开源的高扩展的分布式全文搜索引擎, 是整个 ElasticStack 技术栈的核心。

它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据

它是搜索的一款数据库,支持的数据格式是json.

1.5 Elasticsearch的使用场景

国外:
	1. 维基百科,类似百度百科,“网络七层协议”的维基百科,全文检索,高亮,搜索推荐
	2. Stack Overflow(国外的程序讨论论坛),相当于程序员的贴吧。遇到it问题去上面发帖,热心网友下面回帖解答。
	3. GitHub(开源代码管理),搜索上千亿行代码。
	4. 电商网站,检索商品
	5. 日志数据分析,logstash采集日志,ES进行复杂的数据分析(ELK技术elasticsearch+logstash+kibana
	6. 商品价格监控网站,用户设定某商品的价格阈值,当低于该阈值的时候,
		发送通知消息给用户,比如说订阅《java编程思想》的监控,如果价格低于27块钱,就通知我,我就去买
	7. BI系统,商业智能(Business Intelligence)。大型连锁超市,分析全国网点传回的数据,
		分析各个商品在什么季节的销售量最好、利润最高。
		成本管理,店面租金、员工工资、负债等信息进行分析。从而部署下一个阶段的战略目标。

国内:
	1. 百度搜索,第一次查询,使用es。
	2.  OA、ERP系统站内搜索。

2. ES的安装

步骤:
(1)安装JDK,至少1.8.0_73以上版本,验证:java -version。

(2)下载和解压缩Elasticsearch安装包,查看目录结构。

	下载地址:https://www.elastic.co/cn/downloads/past-releases/
	这俩版本即可

Elasticsearch入门及整合springboot_第4张图片

Elasticsearch入门及整合springboot_第5张图片

目录结构
Elasticsearch入门及整合springboot_第6张图片
解压后,进入 bin 文件目录,点击 elasticsearch.bat 文件启动 ES 服务 。
注意: 9300 端口为 Elasticsearch 集群间组件的通信端口, 9200 端口为浏览器访问的 http协议 RESTful 端口。

打开浏览器,输入地址: http://localhost:9200,测试返回结果.
返回结果如下:
Elasticsearch入门及整合springboot_第7张图片

3. Kibana的安装

上面已经下载了kibanna直接解压 ,启动Kibana:bin目录下的kibana.bat
注意这个启动有点慢
用浏览器访问 http://localhost:5601
Elasticsearch入门及整合springboot_第8张图片
点这个小扳手之后就可以写相关代码了

4. ES中常见的概念

Elasticsearch 是面向文档型数据库,一条数据在这里就是一个文档。
为了方便理解,可以将 Elasticsearch 里存储文档数据和关系型数据库MySQL 存储数据的概念进行一个类比
Elasticsearch入门及整合springboot_第9张图片

 Index 可以看做一个库,
 Types 相当于表,
 Documents 则相当于表的行。
 
 Types 的概念已经被逐渐弱化, 
 Elasticsearch 6.X 中,一个 index 下已经只能包含一个type,
 Elasticsearch 7.X 中, Type 的概念已经被删除了。

5. ES常用API接口

Elasticsearch入门及整合springboot_第10张图片

5.1 基本操作

5.1.1 创建一个索引
#创建索引的第一种方式
#创建索引并往索引中添加一条文档
#结构 这里的1 表示id为1 若不加id 则为随机值
#  PUT /索引名称/类型名称/1
#  {
#    数据
#  }
PUT /qy152/student/1
{
  "name":"张三",
  "age":18
}

#创建索引的第二种方式
#创建索引并指定具体字段的数据类型
PUT /qy153
{
  "mappings": {
    "properties": {
      "name":{
        "type": "text"
      },
      "age":{
        "type": "integer"
      }
    }
  }
}

字段数据类型

字符串类型 :text、 keyword
	- text:支持分词,全文检索,支持模糊、精确查询,
		    不支持聚合,排序 操作;
		    text类型的最大支持的字符长度无限制,适合大字段存储; 
    - keyword:不进行分词,直接索引、支持模糊、支持精确匹配,支持聚合、排序操作。
    		   最大支持的长度为——32766个UTF-8 类型的字符,
    		   可以通过设置ignore_above指定自持字符长度,
    		   超过给定长度后的数据将不被索引,无法通过term精确匹配检索返回结果。

数值型 :long、Integer、short、byte、double、float、half float、scaled float
日期类型:date
布尔类型:boolean
二进制类型:binary
5.1.2 删除索引
#删除索引
DELETE /qy153
5.1.3 查询全部索引
GET /_cat/indices?v

Elasticsearch入门及整合springboot_第11张图片

5.1.4 查询具体索引结构
GET /qy151

Elasticsearch入门及整合springboot_第12张图片

5.1.5 添加文档
#指定id值  --该方式和建立索引一样
PUT /qy153/student/1
{
  "name":"李四",
  "age":25
}

#不指定id值
POST /qy153/student
{
  "name":"王五",
  "age":26
}
5.1.6 添加文档
# 删除文档 -- 根据id
DELETE /qy153/student/1
5.1.7 修改文档
#这种修改 必须指定所有列 若指定部分列 则原来的列会消失 如下图
PUT /qy153/student/2
{
  "name":"李四",
  "age":16
}


#只修改部分列
POST /qy153/student/3/_update
{
  "doc":{
    "name":"李四"
  }
}

Elasticsearch入门及整合springboot_第13张图片
Elasticsearch入门及整合springboot_第14张图片

	所以 
	 PUT /索引名称/类型名称/id值
  	{
  		 数据
	 }
	 这种结构可用于创建索引,添加文档,修改文档
	 若不存在则创建,若存在则修改
5.1.8 查询文档

查询的提交方式必须为GET

5.1.8.1 根据id查询
#GET /索引名称/类型名称/id值
GET /qy153/student/1

Elasticsearch入门及整合springboot_第15张图片

5.1.8.2 查询全部
#GET /索引名称/类型名称/_search
GET /qy153/student/_search

Elasticsearch入门及整合springboot_第16张图片

5.1.8.3 条件查询
	#GET /索引名称/类型名称/_search?q=字段名:值
	 #两种查询方式
	 #第一种 查询年龄为15的
	GET /qy153/student/_search?q=age:15
	
	#第二种查询 使用match匹配  (type也可以省略)  
	GET /qy153/student/_search
	{
	  "query": {
  	 	 "match": {
      		"age": 15
   		 	}
  		}
	}

Elasticsearch入门及整合springboot_第17张图片
Elasticsearch入门及整合springboot_第18张图片

5.1.8.4 范围(range)、匹配(match)、term(精准匹配)
# 数据准备 创建索引 插入三条数据
PUT /user/_doc/1 
{ 
  "name":"刘民谐", 
  "age": 18, 
  "desc": ["有趣","幽默","开朗"]
}
PUT /user/_doc/2
{
  "name":"马老师", 
  "age": 18,
  "desc": ["严谨","冷漠"]
}

PUT /user/_doc/3 
{ "name":"流油",
  "age": 3,
  "desc":["美丽","年轻","苗条"]
}
PUT /user/_doc/4 
{ 
  "name":"刘民X", 
  "age": 18, 
  "desc": ["有趣","幽默","开朗"]
}

GET /user

索引结构
Elasticsearch入门及整合springboot_第19张图片

可以看到name是keywords类型

Ⅰ. 范围匹配:range

range 查询可同时提供包含(inclusive)和不包含(exclusive)这两种范围表达式,
可供组合的选项如下:
	gt: > 大于(greater than)
	lt: < 小于(less than)
	gte: >= 大于或等于(greater than or equal to)
	lte: <= 小于或等于(less than or equal to)
#查询年龄大于3的人
GET /user/_search
{
  "query": {
    "range": {
      "age": {
       "gt":3
      }
    }
  }
}

Elasticsearch入门及整合springboot_第20张图片

Ⅱ. 匹配数组 match

match匹配会拆词 就是会将一个像搜索的词分为一个一个 
如:查询刘民谐这个人 它就会把刘民谐分为'刘' 、'民'、'谐' 然后去匹配
这里注意 汉字会被拆分 但是英文就仅仅是一个词 不会拆分
#查询刘民谐
GET /user/_search
{
  "query": {
    "match": {
      "name": "刘民谐"
    }
  }
}

Elasticsearch入门及整合springboot_第21张图片

Ⅲ. 精准匹配 term

精准匹配不会像match匹配,精准匹配不会拆词
所以查询刘民谐就只有一条记录
#查询刘民谐
GET /user/_search
{
  "query": {
    "term": {
      "name.keyword": "刘民谐"
    }
  }
}

Elasticsearch入门及整合springboot_第22张图片

5.1.8.5 查询部分列
	使用 _source


#查询部分列 查询年龄为18的人并且只查询其name和desc 使用_source
GET /user/_search
{
  "query":{
    "match": {
      "age": "18"
    }
  },
  "_source": ["name","desc"]
}

Elasticsearch入门及整合springboot_第23张图片

5.1.8.6 分页查询
#显示第二页的数据 每页显示1条
GET /user/_search
{
  "query":{
    "range": {
      "age":{
        "gt":16
      }
    }
  },
  "from": 1,
  "size":1
}

Elasticsearch入门及整合springboot_第24张图片

5.1.8.7 多条件查询

(1) 使用must ===>等价于and

GET /user/_search
{
  "query":{
    "bool": {
      "must": [
        {
          "match": {
            "name": "刘"
          }
        },
        {
          "match": {
            "age": "18"
          }
        }
      ]
    }
  }
}

Elasticsearch入门及整合springboot_第25张图片
(2) should ===>等价于 or

GET /user/_search
{
  "query":{
    "bool": {
      "should": [
        {
          "match": {
            "name": "刘"
          }
        },
        {
          "match": {
            "age": "18"
          }
        }
      ]
    }
  }
}

Elasticsearch入门及整合springboot_第26张图片
(3) must_not ===>等价于 ! 即取反

GET /user/_search
{
  "query":{
    "bool": {
      "must_not": [
        {
          "match": {
            "age": "18"
          }
        }
      ]
    }
  }
}

Elasticsearch入门及整合springboot_第27张图片

5.1.8.8 排序
#查询年龄大于16的人且按年龄降序排列
GET /user/_search
{
  "query":{
    "range": {
      "age":{
        "gt":2
      }
    }
  },
  "sort":[
    {
      "age":{
        "order":"desc"
      }
    }
  ],
  "from": 0,
  "size":10
}

Elasticsearch入门及整合springboot_第28张图片

5.1.8.9 高亮显示
#默认高亮
GET /user/_search
{
  "query": {
    "match": {
      "name": "刘"
    }
  },
  "highlight": {
    "fields": {
      "name":{}
    }
  }
  
  
}

Elasticsearch入门及整合springboot_第29张图片

#自定义高亮
GET /user/_search
{
  "query": {
    "match": {
      "name": "刘"
    }
  },
  "highlight": {
      "pre_tags": "",
      "post_tags": "",
      "fields": {
        "name":{}
      }
  }
}

Elasticsearch入门及整合springboot_第30张图片

6. springboot整合ES

(1)创建一个Springboot工程并加入相关的依赖 注意springboot的版本改为2.3.12.RELEASE

<dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.75</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

(2) 创建一个配置,获取ES工具类对象。

@Configuration
public class ESConfig {

    //该对象可以对我们的ES进行相关的操作
    @Bean
    public RestHighLevelClient client(){
        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(new HttpHost("127.0.0.1",9200,"http")));
        return client;
    }
}

(3)可进行相关对ES操作

6.1 ES相关操作

6.1.1 索引相关操作
//注入该对象
@Autowired
    private RestHighLevelClient client;
 /*创建索引*/
    @Test
    void test01() throws Exception{
        CreateIndexRequest createIndexRequest = new CreateIndexRequest("demo01");
        CreateIndexResponse createIndexResponse = client.indices().create(createIndexRequest, RequestOptions.DEFAULT);
        System.out.println(createIndexResponse.isAcknowledged( ));//查看是否创建成功
    }

    /*删除索引*/
    @Test
    void test02() throws Exception{
        DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("demo01");
        AcknowledgedResponse delete = client.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT);
        System.out.println(delete.isAcknowledged());
    }
    /*判断索引是否存在*/
    @Test
    void test03() throws Exception{
        GetIndexRequest getIndexRequest = new GetIndexRequest("demo01");
        boolean exists = client.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
        System.out.println(exists);
    }
6.1.2 文档相关操作

(1)添加文档

    /*添加文档*/
    @Test
    void test04() throws Exception{
        IndexRequest indexRequest = new IndexRequest("demo01");
        indexRequest.id("1"); //指定文档多的id
        //指定文档的内容:指定文档的json内容,XContentType xContentType:以什么格式
        indexRequest.source(JSON.toJSONString(new User("张三","北京",18)), XContentType.JSON);
        IndexResponse indexResponse = client.index(indexRequest, RequestOptions.DEFAULT);
        System.out.println(indexResponse.getResult());
    }
}

(2)查询文档–通过id

 /*通过id查询文档*/
    @Test
    void test05() throws Exception{
        GetRequest getRequest = new GetRequest("demo01");
        getRequest.id("1");
        GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT);
       //得到"_source"中的值
        String string = getResponse.getSourceAsString();
        //转为user对象
        User user = JSON.parseObject(string, User.class);
        System.out.println(user);

        //取出部分值 第二种写法
        Map<String, Object> sourceAsMap = getResponse.getSourceAsMap();
        System.out.println(sourceAsMap.get("address"));
    }

(3)判断文档是否存在

/*判断文档是否存在*/
    @Test
    void test06() throws Exception{
        GetRequest getRequest = new GetRequest("demo01");
        getRequest.id("1");
        boolean exists = client.exists(getRequest, RequestOptions.DEFAULT);
        System.out.println(exists);
    }

(4)删除文档

   /*删除文档*/
    @Test
    void test07() throws Exception{
        DeleteRequest deleteRequest = new DeleteRequest("demo01");
        deleteRequest.id("1");
        DeleteResponse deleteResponse = client.delete(deleteRequest, RequestOptions.DEFAULT);
        System.out.println(deleteResponse.getResult());
    }

(5)修改文档

/*修改文档*/
    @Test
    void test08() throws Exception{
        UpdateRequest updateRequest = new UpdateRequest("demo01","1");
        User user = new User();
        user.setName("李四");
        updateRequest.doc(JSON.toJSONString(user),XContentType.JSON);
        UpdateResponse update = client.update(updateRequest, RequestOptions.DEFAULT);
        System.out.println(update.getResult());
    }

(6)批量添加文档

/*批量添加文档*/
    @Test
    void test09() throws Exception{
        BulkRequest bulkRequest = new BulkRequest("demo01");
        List<User> list = new ArrayList<>();
        list.add(new User("1","张三","北京",18));
        list.add(new User("2","张三三","上海",19));
        list.add(new User("3","李四","北京",18));
        list.add(new User("4","李四四","上海",19));
        list.add(new User("5","王五","北京",18));
        list.add(new User("6","王五五","上海",19));
        /*用stream流循环添加*/
        list.stream().forEach(item->bulkRequest.add(new IndexRequest().id(item.getId()).source(JSON.toJSONString(item),XContentType.JSON)));
        BulkResponse bulk = client.bulk(bulkRequest, RequestOptions.DEFAULT);
        System.out.println(bulk.hasFailures());
        
    }

(7)条件查询

步骤:
	1. 搜索请求对象SearchRequest
	2. 构建一个条件对象SearchSourceBuilder
	3. 把条件对象放入搜索请求对象中
	4. 执行搜索功能
 /*条件查询*/
    @Test
    void test10() throws Exception{
        //1.搜索请求对象SearchRequest
        SearchRequest searchRequest = new SearchRequest("demo01");
        //2. 构建一个条件对象SearchSourceBuilder
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("name", "王");
        //3. 把条件对象放入搜索请求对象中
        SearchSourceBuilder query = sourceBuilder.query(termQueryBuilder);
        
        SearchRequest source = searchRequest.source(query);
        //4. 执行搜索功能
        SearchResponse search = client.search(source, RequestOptions.DEFAULT);

        SearchHit[] hits = search.getHits().getHits();
        Arrays.stream(hits).forEach(item-> System.out.println(item.getSourceAsString()));
    }
}

Elasticsearch入门及整合springboot_第31张图片

    /*条件查询*/
    @Test
    void test11() throws Exception{
        //1.搜索请求对象SearchRequest
        SearchRequest searchRequest = new SearchRequest("demo01");
        //2. 构建一个条件对象SearchSourceBuilder
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        /*查询年龄大于18且姓名中含有王*/
        BoolQueryBuilder must = QueryBuilders
                                .boolQuery()
                                .must(QueryBuilders.rangeQuery("age").gt(17))
                                .must(QueryBuilders.matchQuery("name", "王"));
        //3. 把条件对象放入搜索请求对象中
        SearchSourceBuilder query = sourceBuilder.query(must);
       /*分页:从第一页开始每页显示1条*/
        sourceBuilder.from(0);
        sourceBuilder.size(10);
        /*降序排列  可不写SortOrder 默认升序*/
        sourceBuilder.sort("age", SortOrder.DESC);

        /*高亮*/
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        HighlightBuilder field = highlightBuilder.field("name");
        highlightBuilder.preTags("");
        highlightBuilder.postTags("");
        sourceBuilder.highlighter(highlightBuilder);

        SearchRequest source = searchRequest.source(query);
        //4. 执行搜索功能
        SearchResponse search = client.search(source, RequestOptions.DEFAULT);
        SearchHit[] hits = search.getHits().getHits();
        /*查询条数 搭配分页的时候使用*/
        System.out.println("总条数: "+search.getHits().getTotalHits().value);
        Arrays.stream(hits).forEach(item-> System.out.println(item.getSourceAsString()));
        Arrays.stream(hits).forEach(item-> System.out.println(item.getHighlightFields()));

    }
}

7.综合案例–爬取京东数据存储在es中并结合vue显示出来

(1)引入依赖
这里注意springboot的版本要低一点 2.3.12.RELEASE

 <dependency>
            <groupId>org.jsoupgroupId>
            <artifactId>jsoupartifactId>
            <version>1.11.3version>
        dependency>
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
            <version>1.2.75version>
        dependency>

(2)在kibana中建立索引

PUT /qy151-jd
{
  "mappings": {
    "properties": {
      "title":{
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "price":{
        "type":"keyword"
      },
      "imgUrl":{
        "type": "keyword"
      }
    }
  },
  "settings": {
    "index.analysis.analyzer.default.type":"ik_max_word"
  }
}

(3)工具类准备

public class HtmlParseUtil {
    public static List<Product> parseJd(String keyword) throws Exception {
        String path="https://search.jd.com/Search?keyword="+keyword;
        //Document整个网页对象
        Document document = Jsoup.parse(new URL(path), 30000);
        Element j_goodsList = document.getElementById("J_goodsList");
        Elements li = j_goodsList.getElementsByTag("li");
        List<Product> list=new ArrayList<>();
        for (Element element:li){
            String pprice = element.getElementsByClass("p-price").eq(0).text();
            String pname = element.getElementsByClass("p-name").eq(0).text();
            String img = element.getElementsByTag("img").eq(0).attr("data-lazy-img");
            list.add(new Product(pname,pprice,img));
        }
        return list;
    }
}
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Result {
    private Integer code;
    private String msg;
    private Object data;
}

(4)实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
    private String  title;
    private String  price;
    private String  imgUrl;
}

(5)controller层

@RestController
@RequestMapping("/product")
@CrossOrigin
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping ("/export/{keyword}")
    public Result export(@PathVariable String keyword) throws Exception{
            return productService.export(keyword);
    }


    @PostMapping("/search/{keyword}/{currentPage}/{pageSize}")
    public Result search(@PathVariable String keyword,@PathVariable Integer currentPage,@PathVariable Integer pageSize) throws Exception{
        return productService.search(keyword,currentPage,pageSize);
    }
}

(5)业务层service

@Service
public class ProductService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;


    /*存储es中爬取的数据*/
    public Result export(String keyword) throws Exception {
        List<Product> productList = HtmlParseUtil.parseJd(keyword);
        //这里先在kibana上建立这个索引 上述代码存在
        BulkRequest bulkRequest = new BulkRequest("qy151-jd");
        //批量添加
        productList.stream().forEach(item->bulkRequest.add(new IndexRequest().source(JSON.toJSONString(item),XContentType.JSON)));
        BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
        if(bulk.hasFailures()){
            return new Result(5000,"批量添加失败",null);
        }else{
            return new Result(2000,"批量添加成功",null);
        }

    }

    /*从es中搜索缓存的数据*/
    public Result search(String keyword,Integer currentPage,Integer pageSize) throws Exception{
        SearchRequest searchRequest = new SearchRequest("qy151-jd");
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //设置查询条件
        MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery("title", keyword);
        searchSourceBuilder.query(queryBuilder);
        //设置分页
        searchSourceBuilder.from((currentPage-1)*pageSize);
        searchSourceBuilder.size(pageSize);

        //设置高亮
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.field("title");
        highlightBuilder.preTags("<font style='color:red;font-size:14px;'>");
        highlightBuilder.postTags("font>");
        searchSourceBuilder.highlighter(highlightBuilder);

        searchRequest.source(searchSourceBuilder);

        SearchResponse search = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        SearchHits hits = search.getHits();
        //总条数
        long totalHits = hits.getTotalHits().value;
        List<String,Object>> result = new ArrayList<>();
        SearchHit[] hitsHits = hits.getHits();
        for(SearchHit hit:hitsHits){
            Map<String, Object> sourceAsMap = hit.getSourceAsMap();
            //获取高亮字段
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            Text[] texts = highlightFields.get("title").getFragments();
            StringBuffer sb = new StringBuffer();
            Arrays.stream(texts).forEach(item->sb.append(item));
            //替换原来的title中值始得现在的值有高亮效果
            sourceAsMap.put("title",sb);
            //将总条数保存在map中
            sourceAsMap.put("total",totalHits);
            result.add(sourceAsMap);
        }
        return new Result(2000,"搜索成功",result);
    }
}


(6)前端页面

<template>
    <div class="page">
        <div id="app" class=" mallist tmall- page-not-market ">
            
            <div id="header" class=" header-list-app">
                <div class="headerLayout">
                    <div class="headerCon ">
                        
                        <h1 id="mallLogo">
                            <img src="../assets/logo.webp" width="115px" alt="">
                        h1>
                        <div class="header-extra">
                            
                            <div id="mallSearch" class="mall-search">
                                <form name="searchTop" class="mallSearch-form clearfix">
                                    <fieldset>
                                        <legend>天猫搜索legend>
                                        <div class="mallSearch-input clearfix">
                                            <div class="s-combobox" id="s-combobox-685">
                                                <div class="s-combobox-input-wrap">
                                                    <input v-model="keyword"  type="text" autocomplete="off" id="mq"
                                                           class="s-combobox-input"  aria-haspopup="true">
                                                div>
                                            div>
                                            <button type="submit" @click.prevent="searchKey" id="searchbtn">搜索button>
                                        div>
                                    fieldset>
                                form>
                                <ul class="relKeyTop">
                                    <li><a>老闫说Javaa>li>
                                    <li><a>老闫说前端a>li>
                                    <li><a>老闫说Linuxa>li>
                                    <li><a>老闫说大数据a>li>
                                    <li><a>老闫聊理财a>li>
                                ul>
                            div>
                        div>
                    div>
                div>
            div>
            
            <div id="content">
                <div class="main">
                    
                    <form class="navAttrsForm">
                        <div class="attrs j_NavAttrs" style="display:block">
                            <div class="brandAttr j_nav_brand">
                                <div class="j_Brand attr">
                                    <div class="attrKey">
                                        品牌
                                    div>
                                    <div class="attrValues">
                                        <ul class="av-collapse row-2">
                                            <li><a href="#"> 老闫说 a>li>
                                            <li><a href="#"> Java a>li>
                                        ul>
                                    div>
                                div>
                            div>
                        div>
                    form>
                    
                    <div class="filter clearfix">
                        <a class="fSort fSort-cur">综合<i class="f-ico-arrow-d">i>a>
                        <a class="fSort">人气<i class="f-ico-arrow-d">i>a>
                        <a class="fSort">新品<i class="f-ico-arrow-d">i>a>
                        <a class="fSort">销量<i class="f-ico-arrow-d">i>a>
                        <a class="fSort">价格<i class="f-ico-triangle-mt">i><i class="f-ico-triangle-mb">i>a>
                    div>
                    
                    <div class="view grid-nosku" >
                        <div class="product" v-for="item in results">
                            <div class="product-iWrap">
                                
                                <div class="productImg-wrap">
                                    <a class="productImg">
                                        <img :src="item.imgUrl">
                                    a>
                                div>
                                
                                <p class="productPrice">
                                    <em>{{item.price}}em>
                                p>
                                
                                <p class="productTitle">
                                    <a v-html="item.title">  a>
                                p>
                                
                                <div class="productShop">
                                    <span>店铺: 老闫说Java span>
                                div>
                                
                                <p class="productStatus">
                                    <span>月成交<em>999笔em>span>
                                    <span>评价 <a>3a>span>
                                p>
                            div>
                        div>
                    div>

                    <el-pagination
                            @size-change="handleSizeChange"
                            @current-change="handleCurrentChange"
                            :current-page="currentPage"
                            :page-sizes="pageSizes"
                            :page-size="pageSize"
                            layout="total, sizes, prev, pager, next, jumper"
                            :total="total">
                    el-pagination>
                div>
            div>
        div>
    div>
template>

<script>
    export default {
        name: "jd",
        data(){
             return {
                  keyword: '', // 搜索的关键字
                  results:[], // 后端返回的结果
                 pageSizes:[5,10,15,20],
                 currentPage:1,
                 pageSize:15,
                 total:0,
             }
        },
        methods:{
            /*分页*/
            handleSizeChange(val) {
                this.pageSize = val;
                this.searchKey();
            },
            handleCurrentChange(val) {
                this.currentPage = val;
                this.searchKey();
            },
            searchKey(){
                var keyword = this.keyword;
                this.$http.post('product/search/'+keyword+"/"+this.currentPage+"/"+this.pageSize).then(response=>{
                    console.log(response.data)
                    this.results=response.data.data;
                    this.total = response.data.data[0].total;
                })
            }
        }

    }
script>

<style>
    /*** uncss> filename: http://localhost:9090/css/global.css ***/body,button,fieldset,form,h1,input,legend,li,p,ul{margin:0;padding:0}body,button,input{font:12px/1.5 tahoma,arial,"\5b8b\4f53";-ms-overflow-style:scrollbar}button,h1,input{font-size:100%}em{font-style:normal}ul{list-style:none}a{text-decoration:none}a:hover{text-decoration:underline}legend{color:#000}fieldset,img{border:0}#content,#header{margin-left:auto;margin-right:auto}html{zoom:expression(function(ele){ ele.style.zoom = "1"; document.execCommand("BackgroundImageCache", false, true); }(this))}@font-face{font-family:mui-global-iconfont;src:url(//at.alicdn.com/t/font_1401963178_8135476.eot);src:url(//at.alicdn.com/t/font_1401963178_8135476.eot?#iefix) format('embedded-opentype'),url(//at.alicdn.com/t/font_1401963178_8135476.woff) format('woff'),url(//at.alicdn.com/t/font_1401963178_8135476.ttf) format('truetype'),url(//at.alicdn.com/t/font_1401963178_8135476.svg#iconfont) format('svg')}#mallPage{width:auto;min-width:990px;background-color:transparent}#content{width:990px;margin:auto}#mallLogo{float:left;z-index:9;padding-top:28px;width:280px;height:64px;line-height:64px;position:relative}.page-not-market #mallLogo{width:400px}.clearfix:after,.clearfix:before,.headerCon:after,.headerCon:before{display:table;content:"";overflow:hidden}#mallSearch legend{display:none}.clearfix:after,.headerCon:after{clear:both}.clearfix,.headerCon{zoom:1}#mallPage #header{margin-top:-30px;width:auto;margin-bottom:0;min-width:990px;background:#fff}#header{height:122px;margin-top:-26px!important;background:#fff;min-width:990px;width:auto!important;position:relative;z-index:1000}#mallSearch #mq,#mallSearch fieldset,.mallSearch-input{position:relative}.headerLayout{width:990px;padding-top:26px;margin:0 auto}.header-extra{overflow:hidden}#mallSearch{float:right;padding-top:25px;width:390px;overflow:hidden}.mallSearch-form{border:solid #FF0036;border-width:3px 0 3px 3px}.mallSearch-input{background:#fff;height:30px}#mallSearch #mq{color:#000;margin:0;z-index:2;width:289px;height:20px;line-height:20px;padding:5px 3px 5px 5px;outline:0;border:none;font-weight:900;background:url(data:image/gif;base64,R0lGODlhAQADAJEAAObm5t3d3ff39wAAACH5BAAAAAAALAAAAAABAAMAAAICDFQAOw==) repeat-x;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}#mallSearch button{position:absolute;right:0;top:0;width:90px;border:0;font-size:16px;letter-spacing:4px;cursor:pointer;color:#fff;background-color:#FF0036;height:30px;overflow:hidden;font-family:'\5FAE\8F6F\96C5\9ED1',arial,"\5b8b\4f53"}#mallSearch .s-combobox{height:30px}#mallSearch .s-combobox .s-combobox-input:focus{outline:0}button::-moz-focus-inner{border:0;padding:0;margin:0}.page-not-market #mallSearch{width:540px!important}.page-not-market #mq{width:439px!important}
    /*** uncss> filename: http://localhost:9090/css/test.css ***/#mallSearch{float:none}.page-not-market #mallLogo{width:280px}.header-list-app #mallSearch{width:448px!important}.header-list-app #mq{width:347px!important}@media (min-width:1210px){#header .headerCon,#header .headerLayout,.main{width:1190px!important}.header-list-app #mallSearch{width:597px!important}.header-list-app #mq{width:496px!important}}@media (min-width:600px) and (max-width:800px) and (orientation:portrait){.pg .page{min-width:inherit!important}.pg #mallPage,.pg #mallPage #header{min-width:740px!important}.pg #header .headerCon,.pg #header .headerLayout,.pg .main{width:740px!important}.pg #mallPage #mallLogo{width:260px}.pg #header{min-width:inherit}.pg #mallSearch .mallSearch-input{padding-right:95px}.pg #mallSearch .s-combobox{width:100%!important}.pg #mallPage .header-list-app #mallSearch{width:auto!important}.pg #mallPage .header-list-app #mallSearch #mq{width:100%!important;padding:5px 0 5px 5px}}i{font-style:normal}.main,.page{position:relative}.page{overflow:hidden}@font-face{font-family:tm-list-font;src:url(//at.alicdn.com/t/font_1442456441_338337.eot);src:url(//at.alicdn.com/t/font_1442456441_338337.eot?#iefix) format('embedded-opentype'),url(//at.alicdn.com/t/font_1442456441_338337.woff) format('woff'),url(//at.alicdn.com/t/font_1442456441_338337.ttf) format('truetype'),url(//at.alicdn.com/t/font_1442456441_338337.svg#iconfont) format('svg')}::selection{background:rgba(0,0,0,.1)}*{-webkit-tap-highlight-color:rgba(0,0,0,.3)}b{font-weight:400}.page{background:#fff;min-width:990px}#content{margin:0!important;width:100%!important}.main{margin:auto;width:990px}.main img{-ms-interpolation-mode:bicubic}.fSort i{background:url(//img.alicdn.com/tfs/TB1XClLeAY2gK0jSZFgXXc5OFXa-165-206.png) 9999px 9999px no-repeat}#mallSearch .s-combobox{width:auto}::-ms-clear,::-ms-reveal{display:none}.attrKey{white-space:nowrap;text-overflow:ellipsis}.attrs{border-top:1px solid #E6E2E1}.attrs a{outline:0}.attr{background-color:#F7F5F5;border-color:#E6E2E1 #E6E2E1 #D1CCC7;border-style:solid solid dotted;border-width:0 1px 1px}.attr ul:after,.attr:after{display:block;clear:both;height:0;content:' '}.attrKey{float:left;padding:7px 0 0;width:10%;color:#B0A59F;text-indent:13px}.attrKey{display:block;height:16px;line-height:16px;overflow:hidden}.attrValues{position:relative;float:left;background-color:#FFF;width:90%;padding:4px 0 0;overflow:hidden}.attrValues ul{position:relative;margin-right:105px;margin-left:25px}.attrValues ul.av-collapse{overflow:hidden}.attrValues li{float:left;height:22px;line-height:22px}.attrValues li a{position:relative;color:#806F66;display:inline-block;padding:1px 20px 1px 4px;line-height:20px;height:20px;white-space:nowrap}.attrValues li a:hover{color:#ff0036;text-decoration:none}.brandAttr .attr{border:2px solid #D1CCC7;margin-top:-1px}.brandAttr .attrKey{padding-top:9px}.brandAttr .attrValues{padding-top:6px}.brandAttr .av-collapse{overflow:hidden;max-height:60px}.brandAttr li{margin:0 8px 8px 0}.brandAttr li a{text-overflow:ellipsis;overflow:hidden}.navAttrsForm{position:relative}.relKeyTop{padding:4px 0 0;margin-left:-13px;height:16px;overflow:hidden;width:100%}.relKeyTop li{display:inline-block;border-left:1px solid #ccc;line-height:1.1;padding:0 12px}.relKeyTop li a{color:#999}.relKeyTop li a:hover{color:#ff0036;text-decoration:none}.filter i{display:inline-block;overflow:hidden}.filter{margin:10px 0;padding:5px;position:relative;z-index:10;background:#faf9f9;color:#806f66}.filter i{position:absolute}.filter a{color:#806f66;cursor:pointer}.filter a:hover{color:#ff0036;text-decoration:none}.fSort{float:left;height:22px;line-height:20px;line-height:24px\9;border:1px solid #ccc;background-color:#fff;z-index:10}.fSort{position:relative}.fSort{display:inline-block;margin-left:-1px;overflow:hidden;padding:0 15px 0 5px}.fSort:hover,a.fSort-cur{color:#ff0036;background:#F1EDEC}.fSort i{top:6px;right:5px;width:7px;height:10px;line-height:10px}.fSort .f-ico-arrow-d{background-position:-22px -23px}.fSort-cur .f-ico-arrow-d,.fSort:hover .f-ico-arrow-d{background-position:-30px -23px}i.f-ico-triangle-mb,i.f-ico-triangle-mt{border:4px solid transparent;height:0;width:0}i.f-ico-triangle-mt{border-bottom:4px solid #806F66;top:2px}i.f-ico-triangle-mb{border-top:4px solid #806F66;border-width:3px\9;right:6px\9;top:12px}:root i.f-ico-triangle-mb{border-width:4px\9;right:5px\9}i.f-ico-triangle-mb,i.f-ico-triangle-mt{border:4px solid transparent;height:0;width:0}i.f-ico-triangle-mt{border-bottom:4px solid #806F66;top:2px}i.f-ico-triangle-mb{border-top:4px solid #806F66;border-width:3px\9;right:6px\9;top:12px}:root i.f-ico-triangle-mb{border-width:4px\9;right:5px\9}.view:after{clear:both;content:' '}.productImg,.productPrice em b{vertical-align:middle}.product{position:relative;float:left;padding:0;margin:0 0 20px;line-height:1.5;overflow:visible;z-index:1}.product:hover{overflow:visible;z-index:3;background:#fff}.product-iWrap{position:absolute;background-color:#fff;margin:0;padding:4px 4px 0;font-size:0;border:1px solid #f5f5f5;border-radius:3px}.product-iWrap *{font-size:12px}.product:hover .product-iWrap{height:auto;margin:-3px;border:4px solid #ff0036;border-radius:0;-webkit-transition:border-color .2s ease-in;-moz-transition:border-color .2s ease-in;-ms-transition:border-color .2s ease-in;-o-transition:border-color .2s ease-in;transition:border-color .2s ease-in}.productPrice,.productShop,.productStatus,.productTitle{display:block;overflow:hidden;margin-bottom:3px}.view:after{display:block}.view{margin-top:10px}.view:after{height:0}.productImg-wrap{display:table;table-layout:fixed;height:210px;width:100%;padding:0;margin:0 0 5px}.productImg-wrap a,.productImg-wrap img{max-width:100%;max-height:210px}.productImg{display:table-cell;width:100%;text-align:center}.productImg img{display:block;margin:0 auto}.productPrice{font-family:arial,verdana,sans-serif!important;color:#ff0036;font-size:14px;height:30px;line-height:30px;margin:0 0 5px;letter-spacing:normal;overflow:inherit!important;white-space:nowrap}.productPrice *{height:30px}.productPrice em{float:left;font-family:arial;font-weight:400;font-size:20px;color:#ff0036}.productPrice em b{margin-right:2px;font-weight:700;font-size:14px}.productTitle{display:block;color:#666;height:14px;line-height:12px;margin-bottom:3px;word-break:break-all;font-size:0;position:relative}.productTitle *{font-size:12px;font-family:\5FAE\8F6F\96C5\9ED1;line-height:14px}.productTitle a{color:#333}.productTitle a:hover{color:#ff0036!important}.productTitle a:visited{color:#551A8B!important}.product:hover .productTitle{height:14px}.productShop{position:relative;height:22px;line-height:20px;margin-bottom:5px;color:#999;white-space:nowrap;overflow:visible}.productStatus{position:relative;height:32px;border:none;border-top:1px solid #eee;margin-bottom:0;color:#999}.productStatus span{float:left;display:inline-block;border-right:1px solid #eee;width:39%;padding:10px 1px;margin-right:6px;line-height:12px;text-align:left;white-space:nowrap}.productStatus a,.productStatus em{margin-top:-8px;font-family:arial;font-size:12px;font-weight:700}.productStatus em{color:#b57c5b}.productStatus a{color:#38b}.productImg-wrap{position:relative}.product-iWrap{min-height:98%;width:210px}.view{padding-left:5px;padding-right:5px}.view{width:1023px}.view .product{width:220px;margin-right:33px}@media (min-width:1210px){.view{width:1210px;padding-left:5px;padding-right:5px}.view .product{width:220px;margin-right:20px}}@media (min-width:600px) and (max-width:800px) and (orientation:portrait){.view{width:775px;padding-left:5px;padding-right:5px}.view .product{width:220px;margin-right:35px}}.product{height:372px}.grid-nosku .product{height:333px}

style>

你可能感兴趣的:(Java,ElasticSearch,elasticsearch,spring,boot,搜索引擎)