ElasticSearch
1. 概述
ElasticSearch是基于Lucene做了一些封装和增强。
简称ES,是一个开源的高扩展的分布式全文检索引擎,是使用java开发并使用Lucene作为其核心来实现所有索引和搜索的功能的,目的是通过简单的Restful API来隐藏Lucene的复杂性,让全文搜索变得简单。
2. ES和Solr的区别
ES是基于lucene的开源搜索引擎,使用Java开发来实现所有索引和搜索的功能,目的是通过简单的Restful API来隐藏Luence的复杂性。
Solr是基于lucene的全文搜索引擎,可以独立运行,运行在Jetty、Tomcat等容器中,Solr用POST方法向Solr服务器发送一个描述的Field及其内容的XML文档,根据XML文档添加、删除、更新索引。Solr是一个独立的企业级搜索应用服务器,对外提供类似Web-Service的API接口。用户通过http请求,向搜索引擎服务器提交一定格式的文件生成索引,也可以通过提出查找请求,并得到返回结果。
- Solr安装稍复杂,es开箱即用
- Solr利用Zookeeper进行分布式管理,ES自身带有分布式协调管理工具
- Solr支持更多格式的数据,JSON、xml、CSV,ES仅支持JSON
- Solr功能更多,ES更注重核心功能
- Solr查询快,但更新索引时慢,用于电商等查询多的应用,ES建立索引快(查询慢),实时查询快
- Solr比较成熟,ES维护者较少,更新快
3. ES安装
下载地址: > https://www.elastic.co/cn/downloads/past-releases/elasticsearch-7-6-2
下载解压缩即可使用
目录:
bin # 启动文件
config # 配置文件
elasticsearch.yml # elasticsearch的配置文件 默认 9200端口
jvm.options # java 虚拟机相关配置
log4j2.properties # 日志配置文件
jdk # jdk环境
lib # 相关jar包
logs # 日志
modules # 功能模块
plugins # 插件
启动
双击bin/elasticsearch.bat 访问9200
{
"name" : "LAPTOP-E88S4B8O",
"cluster_name" : "elasticsearch", # 集群名字
"cluster_uuid" : "njSkAD6rSVWWkOWUuAEXNA",
"version" : {
"number" : "7.6.2",
"build_flavor" : "default",
"build_type" : "zip",
"build_hash" : "ef48eb35cf30adf4db14086e8aabd07ef6fb113f",
"build_date" : "2020-03-26T06:34:37.794943Z",
"build_snapshot" : false,
"lucene_version" : "8.4.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
4. Head安装
下载地址:>https://github.com/599166320/elasticsearch-head
在文件目录下使用命令
npm install
npm run start
# 访问 http://localhost:9100
解决跨域问题:
在elasticsearch.yml 添加以下
http.cors.enabled: true
http.cors.allow-origin: "*"
重启 elasticsearch,再次连接
索引当作数据库,文档当作表
5. Kibana安装
Kibana要与ES版本一致
下载地址:
https://www.elastic.co/cn/downloads/past-releases/kibana-7-6-2
下载完之后解压
先启动ES(elasticsearch.bat),再启动Kibana(kibana.bat)。
汉化 在配置文件中修改 i18n.locale: “zh-CN” 之后重启
6. ES核心概念
Elasticsearch是面向文档的
Relational DB | Elasticsearch |
---|---|
数据库(database) | 索引(indices) |
表(tables) | types |
行(rows) | documents |
字段(columns) | fields |
Elasticsearch(集群)中可以包含多个索引(数据库),每个索引中可以包含多个类型(表),每个类型包含多个文档(行),每个文档包含多个字段(列)。
物理设计:
elasticsearch在后台把每个索引划分为多个分片,每个分片可以在集群中的不同服务器之间迁移。
逻辑设置:
一个索引类型中,包含多个文档,当索引一篇文档时,可以通过一下顺序,索引->类型->文档id
文档:一条条的数据
elastic search是面向文档的,也就是索引和搜索数据的最小单位是文档,elastic search中文档有几个重要的属性:
- 自我包含,一篇文档同时包含字段和对应的值,key:value
- 层次性的,一个文档中包含文档
- 灵活的结构,文档不依赖预先定义的模式,
类型:数据的类型是什么
索引:数据库
索引就是一个大的文档集合,索引存储了映射类型的字段和其他设置,然后就被存储到各个分片上。
倒排索引
elasticsearch使用的是一种倒排索引的结构,采用lucene倒排索引为底层,这种结构适合快速的全文搜索,一个索引由文档中所有不重复的列表组成,对于每个词,都有包含他的文档列表。
Study every day , good good up to forever # 文档一的内容
To forever, study every day, good good up # 文档二的内容
为了创建倒排索引,需要将每个文档拆分为独立的词,然后创建一个包含所有不重复的词条的排序列表,然后列出每个词条出现在那个文档。
term | doc 1 | doc 2 |
---|---|---|
Study | √ | × |
To | × | × |
every | √ | √ |
forever | √ | √ |
day | √ | √ |
study | × | √ |
good | √ | √ |
every | √ | √ |
to | √ | × |
up | √ | √ |
如果查询to forever,只需查看包含每个词条的文档
term | doc 1 | doc 2 |
---|---|---|
to | √ | × |
forever | √ | √ |
total | 2 | 1 |
两个文档都匹配,但是文档一的匹配程度更高,如果没有别的条件,这两个包含关键字的文档都将返回
ID | 标签 | 标签 | ID |
---|---|---|---|
1 | python | python | 1 2 3 |
2 | python | Linux | 3 4 |
3 | python,Linux | ||
4 | Linux |
如果想要搜索关于Linux的内容,相对于查找所有原始数据而言,查找倒排索引后的数据将会快的多,只需查看标签一栏,获取相关的文章ID即可。
7. IK分词器
分词就是将一段中文或者其他的划分为一个个的关键词,在搜索的时候会把自己的信息进行分词,会把数据库中或者索引库中的数据进行分词,然后进行一个匹配操作,默认的中文分词是将每个字看作一个词。
IK提供了两个分词语法:ik_smart 和 ik_max_word,其中ik_smart为最少划分,ik_max_word为最细粒度划分!
- 下载
https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.6.2
-
解压到elasticsearch的plugins目录下
-
重启elasticsearch 可以在日志中看到 ik插件被加载了
-
可以通过elasticsearch-plugin查看加载的插件
-
使用kibana测试
查看不同分词器的效果
ik_smart,ik_max_word,
GET _analyze
{
"analyzer": "ik_smart", # 最小粒度划分
"text": "我正在学习java"
}
{
"tokens" : [
{
"token" : "我",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_CHAR",
"position" : 0
},
{
"token" : "正在",
"start_offset" : 1,
"end_offset" : 3,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "学习",
"start_offset" : 3,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "java",
"start_offset" : 5,
"end_offset" : 9,
"type" : "ENGLISH",
"position" : 3
}
]
}
GET _analyze
{
"analyzer": "ik_max_word", # 最细粒度划分
"text": "我正在学习java"
}
{
"tokens" : [
{
"token" : "我",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_CHAR",
"position" : 0
},
{
"token" : "正在",
"start_offset" : 1,
"end_offset" : 3,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "在学",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "学习",
"start_offset" : 3,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 3
},
{
"token" : "java",
"start_offset" : 5,
"end_offset" : 9,
"type" : "ENGLISH",
"position" : 4
}
]
}
有些词需要组合在一起,但是被拆开了,需要将这些词自行加入到字典中
ik分词器增加自己的配置
增加自己的配置文件
我正
在学习
{
"tokens" : [
{
"token" : "我正", # 这样就识别出了自己定义的词
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "正在",
"start_offset" : 1,
"end_offset" : 3,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "在学习",
"start_offset" : 2,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "在学",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 3
},
{
"token" : "学习",
"start_offset" : 3,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 4
},
{
"token" : "java",
"start_offset" : 5,
"end_offset" : 9,
"type" : "ENGLISH",
"position" : 5
}
]
}
8. Restful风格操作
8.1 操作索引
method | URL地址 | 描述 |
---|---|---|
PUT | localhost:9200/索引名称/类型名称/文档id | 创建文档(指定文档的id) |
POST | localhost:9200/索引名称/类型名称 | 创建文档(随机文档的id) |
POST | localhost:9200/索引名称/类型名称/文档id/_update | 修改文档 |
DELETE | localhost:9200/索引名称/类型名称/文档id | 删除文档 |
GET | localhost:9200/索引名称/类型名称/文档id | 通过文档id查询文档 |
POST | localhost:9200/索引名称/类型名称/_search | 查询所有的数据 |
- 创建一个索引
PUT /索引名/(类型名)/(文档id)
{
请求体
}
数据类型:
- 字符串类型 text keyword
- 数值类型 long,integer, short ,byte ,double, float half, float ,scaled float
- 日期类型 date
- 布尔类型 boolean
- 二进制类型 binary
- …
- 指定字段的类型
创建索引规则,之后可以往里放数据
这里需要注意的是日期类型 日期要写完整,个位数的月份要补0
扩展:
GET _cat/health # 查看健康度
GET _cat/indices?v
- 修改的时候还是可以使用PUT即可,version会变,然后覆盖。
- 删除索引 DELETE test3 根据请求来判断是删除索引还是删除文档
8.2 操作文档(重点)
基本操作
- 添加数据
PUT /test1/user/1
{
"name":"summer",
"age":13,
"desc":"abcdefg",
"tags":["12","15","21"]
}
- 获取数据
- 更新数据
可以用PUT和POST
PUT /test1/user/1{}
POST /test1/user/1/_update
{
"doc":{
"name":"铁蛋",
"age":"25"
}
}
- 删除数据
DELETE test1/user/1 删除文档 DELETE test 删除索引
- 查询数据
# 简单查询
GET test1/user/1
# 根据默认的映射规则查询
GET /test1/user/_search?q=name:铁蛋
复杂操作
GET /test1/user/_search
{
"query": {
"match": {
"name":"铁"
}
}
}
GET /test1/user/_search
{
"query": {
"match": {
"name":"张"
}
},
"_source": ["name","age"], # 只查询那些字段
"sort": [
{
"age": {
"order": "desc" # 按照那个字段排序 升序还是降序
}
}
],
"from": 0, # 从第几个数据开始
"size": 2 # 每页显示几个数据 相当于 limit 0,2
}
GET /test1/user/_search
{
"query": {
"bool": { # 多条件查询
"must": [ # 所有条件都得符合 相当于 and
{
"match": {
"name": "张"
}
},
{
"match": {
"age": 23
}
}
]
}
}
}
GET /test1/user/_search
{
"query": {
"bool": {
"should": [ # 有一个条件符合就可以 相当于 or
{
"match": {
"name": "张"
}
},
{
"match": {
"age": 23
}
}
]
}
}
}
GET /test1/user/_search
{
"query": {
"bool": {
"must_not": [ # 只要有一个条件不符合就可以 名字中没有三以及年龄不等于25的所有人 相当于 not
{
"match": {
"name": "三"
}
},
{
"match": {
"age": 25
}
}
]
}
}
}
GET /test1/user/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "三"
}
}
],
"filter": { # 添加过滤器
"range": {
"age": { # 年龄在那个范围
"gte": 20, # 大于等于20 gt 大于 gte 大于等于
"lte": 22 # 小于等于22 lt 小于 lte 小于等于
}
}
}
}
}
}
GET /test1/user/_search
{
"query": {
"match": {
"tag": "男 技术" # 只要有一个匹配就查出来,多个条件用空格隔开,根据查询的匹配程度前后排序
}
}
}
term查询时直接通过倒排索引指定的词条进行精确查找
关于分词:
- term 完全匹配也就是精确查找,查找前不会对搜索词进行分词拆解,所以搜索词必须是文档分词集合中的一个,文档分不分词与词的类型有关,keyword不会分词,text可以分词。
- match 查找前会先对搜索词进行分词拆解,只要搜索词的分词集合中的一个或者多个存在于文档分词集合中即可,文档分不分词与词的类型有关,keyword不会分词,text可以分词。即查询中国青岛,搜索词会先拆分为中国和青岛,只要文档分词中包含中国和青岛的任意一个词就会被搜索到。
term查询keyword:term不会分词,keyword也不会分词,需要完全匹配才行。
term查询text:term不会分词,text会分词,term的查询条件必须是text字段分词后的一个才行。
match查询keyword:match会分词,keyword不会分词,match与keyword完全匹配才行。
match查询text,match会分词,text会分词,只要match的分词结果与text的分词结果有相同的匹配即可。
keyword text
text可以被分词解析器解析,keyword不会被分词解析器解析,就是不会被拆成一个个的词,当作一个整体了
使用term后,假设某个字段为keyword,则必须将全名进行查询才可以查到,只有一部分查不到,因为term的搜索名不会被分词,keyword也不会被分词
多值匹配的精确查询
GET /test1/user/_search
{
"query": {
"bool": {
"should": [
{
"term": {
"name":"张"
}
},
{
"term": {
"age": 20
}
}
]
}
}
}
高亮查询
9. SpringBoot集成ES
官方文档:
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.6/index.html
- 依赖
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.6.2</version>
</dependency>
- 初始化对象
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("localhost", 9200, "http"),
new HttpHost("localhost", 9201, "http")));
client.close();
- 分析该类的一些方法
新建springboot项目,导入默认工具、web、Spring Data Elasticsearch
解决版本问题,本机使用的是ElasticSearch7.6.2
-
ElasticsearchRestClientAutoConfiguration ElasticsearchRestClientProperties class ElasticsearchRestClientConfigurations { @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(RestClientBuilder.class) static class RestClientBuilderConfiguration { @Bean RestClientBuilderCustomizer defaultRestClientBuilderCustomizer(ElasticsearchRestClientProperties properties) { return new DefaultRestClientBuilderCustomizer(properties); } @Bean RestClientBuilder elasticsearchRestClientBuilder(ElasticsearchRestClientProperties properties, ObjectProvider<RestClientBuilderCustomizer> builderCustomizers) { HttpHost[] hosts = properties.getUris().stream().map(HttpHost::create).toArray(HttpHost[]::new); RestClientBuilder builder = RestClient.builder(hosts); builder.setHttpClientConfigCallback((httpClientBuilder) -> { builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(httpClientBuilder)); return httpClientBuilder; }); builder.setRequestConfigCallback((requestConfigBuilder) -> { builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(requestConfigBuilder)); return requestConfigBuilder; }); builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder; } } @Configuration(proxyBeanMethods = false) @ConditionalOnClass(RestHighLevelClient.class) static class RestHighLevelClientConfiguration { @Bean @ConditionalOnMissingBean RestHighLevelClient elasticsearchRestHighLevelClient(RestClientBuilder restClientBuilder) { return new RestHighLevelClient(restClientBuilder); } @Bean @ConditionalOnMissingBean RestClient elasticsearchRestClient(RestClientBuilder builder, ObjectProvider<RestHighLevelClient> restHighLevelClient) { RestHighLevelClient client = restHighLevelClient.getIfUnique(); if (client != null) { return client.getLowLevelClient(); } return builder.build(); } } @Configuration(proxyBeanMethods = false) static class RestClientFallbackConfiguration { @Bean @ConditionalOnMissingBean RestClient elasticsearchRestClient(RestClientBuilder builder) { return builder.build(); } } static class DefaultRestClientBuilderCustomizer implements RestClientBuilderCustomizer { private static final PropertyMapper map = PropertyMapper.get(); private final ElasticsearchRestClientProperties properties; DefaultRestClientBuilderCustomizer(ElasticsearchRestClientProperties properties) { this.properties = properties; } @Override public void customize(RestClientBuilder builder) { } @Override public void customize(HttpAsyncClientBuilder builder) { map.from(this.properties::getUsername).whenHasText().to((username) -> { CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); Credentials credentials = new UsernamePasswordCredentials(this.properties.getUsername(), this.properties.getPassword()); credentialsProvider.setCredentials(AuthScope.ANY, credentials); builder.setDefaultCredentialsProvider(credentialsProvider); }); } @Override public void customize(RequestConfig.Builder builder) { map.from(this.properties::getConnectionTimeout).whenNonNull().asInt(Duration::toMillis) .to(builder::setConnectTimeout); map.from(this.properties::getReadTimeout).whenNonNull().asInt(Duration::toMillis) .to(builder::setSocketTimeout); } } }
-
API测试
// 编写自己的RestHighLevelClient
@Configuration
public class ElasticSearchConfig {
@Bean
public RestHighLevelClient restHighLevelClient(){
RestHighLevelClient restHighLevelClient = new RestHighLevelClient(
RestClient.builder(
new HttpHost("localhost",9200,"http")
)
);
return restHighLevelClient;
}
}
9.1 关于索引的API测试
测试索引的创建、是否存在、删除
@SpringBootTest
class EsApiApplicationTests {
@Autowired
@Qualifier("restHighLevelClient")
RestHighLevelClient client;
@Test
// 1.测试索引创建
void test1() throws IOException {
// 1. 创建索引请求 PUT /test
CreateIndexRequest request = new CreateIndexRequest("test");
// 2. 执行请求 获得响应
CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
System.out.println(response);
}
// 2.测试索引是否存在
@Test
void test2() throws IOException {
GetIndexRequest request = new GetIndexRequest("test");
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
System.out.println(exists);
}
// 3.删除索引
@Test
void test3() throws IOException {
DeleteIndexRequest request = new DeleteIndexRequest("test1");
AcknowledgedResponse delete = client.indices().delete(request, RequestOptions.DEFAULT);
System.out.println(delete.isAcknowledged());
}
}
9.2 关于文档的API测试
创建一个实体类
@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String name;
private int age;
}
<!--引入fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
// 添加文档
@Test
void test4() throws IOException {
// 创建对象
User user = new User("小明", 23);
// 创建请求
IndexRequest request = new IndexRequest("test");
// 规则 PUT /index/_doc/1
request.id("1");
// request.timeout(TimeValue.timeValueSeconds(1));
request.timeout("1s");
// 将数据放入请求
request.source(JSON.toJSONString(user), XContentType.JSON);
// 客户端发送请求,获取响应的结果
IndexResponse indexResponse = client.index(request, RequestOptions.DEFAULT);
System.out.println(indexResponse.toString());// IndexResponse[index=test,type=_doc,id=1,version=1,result=created,seqNo=0,primaryTerm=1,shards={"total":2,"successful":1,"failed":0}]
System.out.println(indexResponse.status()); // CREATED
}
// 获取文档 是否存在
@Test
void test5() throws IOException {
GetRequest getRequest = new GetRequest("test","1");
getRequest.fetchSourceContext(new FetchSourceContext(false)); // 不获取返回的_source的上下文了
getRequest.storedFields("_none_");
System.out.println(client.exists(getRequest,RequestOptions.DEFAULT));
}
// 获取文档的信息
@Test
void test6() throws IOException {
GetRequest getRequest = new GetRequest("test","1");
GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT);
System.out.println(getResponse.getSourceAsString()); //打印文档的内容
System.out.println(getResponse); // 返回的内容与命令一样
}
// 更新文档的信息
@Test
void test7() throws IOException {
UpdateRequest updateRequest = new UpdateRequest("test", "1");
updateRequest.timeout("1s");
User user = new User("小红", 15);
updateRequest.doc(JSON.toJSONString(user), XContentType.JSON);
UpdateResponse updateResponse = client.update(updateRequest, RequestOptions.DEFAULT);
System.out.println(updateResponse.status());
System.out.println(updateResponse);
}
// 删除文档
@Test
void test8() throws IOException {
DeleteRequest deleteRequest = new DeleteRequest("test", "1");
DeleteResponse deleteResponse = client.delete(deleteRequest, RequestOptions.DEFAULT);
System.out.println(deleteResponse.status());
}
// 批量插入数据 所有数据需要外部导入
@Test
void test9() throws IOException {
BulkRequest bulkRequest = new BulkRequest();
bulkRequest.timeout("10s");
ArrayList<User> userList = new ArrayList<>();
userList.add(new User("小明1",25));
userList.add(new User("小明2",25));
userList.add(new User("小明3",25));
userList.add(new User("小明4",25));
userList.add(new User("小明5",25));
userList.add(new User("小明6",25));
userList.add(new User("小明7",25));
userList.add(new User("小明8",25));
userList.add(new User("小明9",25));
for (int i = 0; i < arrayList.size(); i++) {
IndexRequest indexRequest = new IndexRequest("test");
indexRequest.id(i+"");
indexRequest.source(JSON.toJSONString(arrayList.get(i)),XContentType.JSON);
bulkRequest.add(indexRequest);
}
BulkResponse bulk = client.bulk(bulkRequest, RequestOptions.DEFAULT);
System.out.println(bulk.hasFailures()); // 是否执行失败
}
// 查询
// SearchRequest 搜索请求
// SearchSourceBuilder 条件构造
// HighlightBuilder 构建高亮
// TermQueryBuilder 精确查询
// MatchAllQueryBuilder 匹配全部
// ......Builder
// 查询条件
// QueryBuilders.termQuery 精确匹配
// QueryBuilders.matchAllQuery(); 匹配所有
@Test
void test10() throws IOException {
SearchRequest searchRequest = new SearchRequest("test");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
TermQueryBuilder queryBuilder = QueryBuilders.termQuery("name.keyword", "小明2");
sourceBuilder.query(queryBuilder);
searchRequest.source(sourceBuilder); //可以对称来看,下方需要的都是上方创建出来的
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
System.out.println(JSON.toJSONString(searchResponse.getHits()));
for (SearchHit hit : searchResponse.getHits().getHits()) {
System.out.println(hit.getSourceAsMap());
}
}
10. 实战
10.1 环境搭建
导入前端界面,编写对应的controller,测试跳转页面
@Controller
public class IndexController {
@GetMapping("/")
public String index(){
return "index";
}
}
10.2 爬取数据
public class HTMLParseUtils {
public static void main(String[] args) throws IOException {
List<Content> contentList = new HTMLParseUtils().parseTD("python从入门到实践");
for (Content content : contentList) {
if(content!=null){
System.out.println(content);
}
}
}
public List<Content> parseTD(String keywords) throws IOException {
String url = "https://search.jd.com/Search?keyword="+keywords+"enc=utf-8";
// 返回js页面对象
Document document = Jsoup.parse(new URL(url),30000);
Element element = document.getElementById("J_goodsList");
// 找到所有的li元素
Elements elements = document.getElementsByTag("li");
List<Content> contentList = new ArrayList<>();
// 获取元素的内容
for (Element el : elements) {
// 关于图片特别多的网站,所有的图片都是延迟加载的
String img = el.getElementsByTag("img").eq(0).attr("data-lazy-img");
String price = el.getElementsByClass("p-price").eq(0).text();
String title = el.getElementsByClass("p-name").eq(0).text();
Content content = new Content();
if (img!=""&&price!=""&title!=""){
content.setTitle(title);
content.setPrice(price);
content.setImg(img);
contentList.add(content);
}
}
return contentList;
}
}
10.3 业务编写
service层
@Service
public class ContentService {
@Autowired
RestHighLevelClient restHighLevelClient;
// 1. 解析数据 放入ex
public boolean parseContent(String keyword) throws IOException {
List<Content> contentList = new HTMLParseUtils().parseTD(keyword);
// 把查询的数据放入ES中
BulkRequest bulkRequest = new BulkRequest();
bulkRequest.timeout("2m");
for (int i = 0; i < contentList.size(); i++) {
IndexRequest indexRequest = new IndexRequest("test3");
indexRequest.source(JSON.toJSONString(contentList.get(i)), XContentType.JSON);
bulkRequest.add(indexRequest);
}
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
return !bulk.hasFailures();
}
// 2. 获取这些数据 实现搜索功能
public List<Map<String,Object>> searchPage(String keyword,int pageNo,int pageSize) throws IOException {
if(pageNo <= 1){
pageNo = 1;
}
SearchRequest searchRequest = new SearchRequest("jd_goods");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 分页
sourceBuilder.from(pageNo);
sourceBuilder.size(pageSize); //默认为10
// 精准匹配关键字
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("title", keyword);
sourceBuilder.query(termQueryBuilder);
sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
//执行搜索
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 解析结果
ArrayList<Map<String, Object>> list = new ArrayList<>();
for (SearchHit hit : searchResponse.getHits().getHits()) {
list.add(hit.getSourceAsMap());
}
return list;
}
}
controller层
@RestController
public class ContentController {
@Autowired
ContentService contentService;
@RequestMapping("/parse/{keyword}")
public Boolean parse(@PathVariable("keyword") String keyword) throws IOException {
boolean java = contentService.parseContent(keyword);
return java;
}
@GetMapping("/search/{keyword}/{pageNo}/{pageSize}")
public List<Map<String, Object>> search(@PathVariable("keyword") String keyword,
@PathVariable("pageNo") int pageNo,
@PathVariable("pageSize") int pageSize) throws IOException {
List<Map<String, Object>> maps = contentService.highLight(keyword, pageNo, pageSize);
return maps;
}
}
10.4 前后端分离
引入vue需要的依赖 将axios.min.js 和 vue.min.js复制到js文件夹下
# 创建一个文件夹运行以下命令
npm init -y
npm install vue
npm install axios
# 打开node_modules\vue\dist 可以看到不同构建版本 将某个版本复制到项目的js文件夹下 这里引入vue.min.js 官网下载
# 官网 https://cn.vuejs.org/v2/guide/installation.html
Vue3中不再构建UMD模块化的方式,因为UMD会让代码有更多的冗余,它要支持多种模块化的方式。Vue3中将CJS、ESModule和自执行函数的方式分别打包到了不同的文件中。在packages/vue中有Vue3的不同构建版本。
cjs(两个版本都是完整版,包含编译器)
vue.cjs.js
vue.cjs.prod.js(开发版,代码进行了压缩)
global(这四个版本都可以在浏览器中直接通过scripts标签导入,导入之后会增加一个全局的Vue对象)
vue.global.js(完整版,包含编译器和运行时)
vue.global.prod.js(完整版,包含编译器和运行时,这是开发版本,代码进行了压缩)
vue.runtime.global.js
vue.runtime.global.prod.js
browser(四个版本都包含esm,浏览器的原生模块化方式,可以直接通过<script type="module" />的方式来导入模块)
vue.esm-browser.js
vue.esm-browser.prod.js
vue.runtime.esm-browser.js
vue.runtime.esm-browser.prod.js
bundler(这两个版本没有打包所有的代码,只会打包使用的代码,需要配合打包工具来使用,会让Vue体积更小)
vue.esm-bundler.js
bue.runtime.esm-bundler.js
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<title>狂神说Java-ES仿京东实战</title>
<link rel="stylesheet" th:href="@{/css/style.css}"/>
<script th:src="@{/js/jquery.min.js}"></script>
</head>
<body class="pg">
<div class="page" id="app">
<div id="mallPage" class=" mallist tmall- page-not-market ">
<!-- 头部搜索 -->
<div id="header" class=" header-list-app">
<div class="headerLayout">
<div class="headerCon ">
<!-- Logo-->
<h1 id="mallLogo">
<img th:src="@{/images/jdlogo.png}" 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" value="dd" id="mq"
class="s-combobox-input" aria-haspopup="true">
</div>
</div>
<button type="submit" id="searchbtn" @click.prevent="searchKey">搜索</button>
</div>
</fieldset>
</form>
<ul class="relKeyTop">
<li><a>狂神说Java</a></li>
<li><a>狂神说前端</a></li>
<li><a>狂神说Linux</a></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="result in results">
<div class="product-iWrap">
<!--商品封面-->
<div class="productImg-wrap">
<a class="productImg">
<img :src="result.img">
</a>
</div>
<!--价格-->
<p class="productPrice">
<em>{{result.price}}</em>
</p>
<!--标题-->
<p class="productTitle">
<a> {{result.title}} </a>
</p>
<!-- 店铺名 -->
<div class="productShop">
<span>店铺: 狂神说Java </span>
</div>
<!-- 成交信息 -->
<p class="productStatus">
<span>月成交<em>999笔</em></span>
<span>评价 <a>3</a></span>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!--前端使用Vue-->
<script th:src="@{/js/vue.min.js}"></script>
<script th:src="@{/js/axios.min.js}"></script>
<script>
new Vue({
el:'#app',
data:{
keyword:'', //搜索的关键字
results:[] // 搜索的结果
},
methods:{
searchKey(){
let keyword = this.keyword;
axios.get('search/'+keyword+'/1/20').then(response=>{
console.log(response);
this.results = response.data;
});
}
}
})
</script>
</body>
</html>
10.5 高亮实现
// 获取这个数据 实现高亮的功能
public List<Map<String,Object>> highLight(String keyword,int pageNo,int pageSize) throws IOException {
if(pageNo <= 1){
pageNo = 1;
}
SearchRequest searchRequest = new SearchRequest("jd_goods");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 分页
sourceBuilder.from(pageNo); // 如果不设置只读10条数据
sourceBuilder.size(pageSize);
// 精准匹配关键字
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("title", keyword);
sourceBuilder.query(termQueryBuilder);
// MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("title", keyword);
// sourceBuilder.query(matchQueryBuilder);
sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
// 构建高亮
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("title");
highlightBuilder.requireFieldMatch(false); //关闭多个高亮
highlightBuilder.preTags("<span style='color:red'>");
highlightBuilder.postTags("</span>");
sourceBuilder.highlighter(highlightBuilder);
//执行搜索
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 解析结果
ArrayList<Map<String, Object>> list = new ArrayList<>();
for (SearchHit hit : searchResponse.getHits().getHits()) {
// 解析高亮的字段 将原来的字段换为高亮的字段
Map<String, HighlightField> map = hit.getHighlightFields();
HighlightField title = map.get("title");
Map<String, Object> sourceAsMap = hit.getSourceAsMap();//原来的结果
if(title!=null){
Text[] fragments = title.fragments();
String newTitle = "";
for (Text fragment : fragments) {
newTitle += fragment;
}
sourceAsMap.put("title",newTitle); //高亮的字段换成原来的字段
}
list.add(sourceAsMap);
}
return list;
}
切换调用方法
List<Map<String, Object>> maps = contentService.highLight(keyword, pageNo, pageSize);
发现并没有高亮,需要高亮的地方都成了span标签,此时使用vue
<p class="productTitle">
<a v-html="result.title"> </a>
</p>