ES全称ElasticSearch,是一种分布式全文检索引擎,用于全文搜索、分析。近乎实时的存储及检索效率,可以在上百台服务器上运行处理PB级数据的扩展性都让ES成为炙手可热的搜索引擎。除此外,ES通过简单的RESTful API屏蔽了Lucence的复杂语法,在使用上ES也变得简单易上手。
一、ES核心概念
1.1 ES和普通关系型数据库的映射关系
ES集群中可以包含多个索引(数据库),每个索引可以包含多个类型(表),每个文档(行)中又包含多个词(字段)。
当然上面的映射也只是为了易于理解,实际上两者并无关联。
1.2 ES的逻辑设计——倒排索引
详见: 为啥ElasticSearch搜索那么快?倒排索引又是啥?
1.3 ES集群
一个ES集群就是由一个或多个节点组织在一起,它们共同持整个的数据,并一起提供索引和搜索功能。一个集群有一个唯一的名字标识,这个名字默认是“elasticsearch”。这个名字是重要的,每个节点只能通过指定这个集群名字来加入这个集群。
1.4 ES的物理设计——分片
如果倒排索引是ES的逻辑设计,那么分片就是它的物理设计。ElasticSearch在后台把每个索引划分成多个分片,每片分片可以在集群中的不同服务器之间迁移。如果你创建索引,那么索引会至少有5个分片(primary shard ,又称为主分片)构成的,每一个主分片会有一个副本(replica shard,又称为复制分片),实际上一个分片就是一个Lucene索引。
1.4.1 怎么理解分片,或者说为啥要分片?
一个索引可以存储超出单个节点硬件限制的大量数据。比如一个索引需要1TB的空间,但是每个节点的物理存储最大才500M,那怎么存?显然这个索引放在哪个节点都是太大了,为了解决这个问题,Elasticsearch提供了将索引划分成多份的能力,这些份叫做分片,这个索引的分片分散在不同的节点上,这样就能实现一个索引可以存储超出单个节点硬件限制的大量数据的目标,提供检索时则整个集群一起提供。
1.4.2 分片的好处是啥?
有两点:
(1)水平分割/扩展容量
(2)可进行分布式的、并行的操作,进而提高性能/吞吐量
1.4.3 怎么理解分片复制?
Elasticsearch允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片,或者直接叫复制。
复制之所以重要,有两个原因:
1) 在分片/节点失败的情况下,提供了高可用性。因为此原因,复制分片一般不与原/主(original/primary)分片置于同一节点上。
2)扩展搜索量/吞吐量,因为搜素可以在所有的复制上并行运行。
1.4.4 节点和分片 如何工作?
一个集群至少有一个节点,而一个节点就是一个ElasticSearch进程。
上图是一个有3个节点的集群,可以看到主分片和对应的复制分片都不会在同一个节点内,这样有利于某个节点挂掉了,数据也不至于丢失,保证了ES的可用性。
二、ES操作准备
这里演示采用windows版的ES,结合Kibana、KI分词及Head插件进行操作。Kibana可以理解成是ES的命令控制台、Head可以理解成Navicat可视化工具、IK则是分词插件。这些安装配置等环境准备工作不详细说明。若不想自己安装了也可以使用我的腾讯云服务器上部署的ES服务,IP:49.234.28.149 。另外附上ES各版本相关工具合集下载地址:https://elasticsearch.cn/download/ 。
2.1 Head插件界面
2.2 Kibana界面
2.3 IK分词器
ik 分词器有两种分词模式:ik_max_word 和 ik_smart 模式。
- ik_max_word
会将文本做最细粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为“中华人民共和国、中华人民、中华、
华人、人民共和国、人民、共和国、大会堂、大会、会堂等词语。ik_smart
会做最粗粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为中华人民共和国、人民大会堂。
三、ES基本操作
ES数据的curd操作是遵循RESTful风格的:
3.1 添加数据
可以通过head查看:
可以看到已经正常新增数据到对应的索引下。
3.2 获取数据
3.3 更新数据
3.4 按条件查询
四、SpringBoot与ES集成
实例代码下载地址: https://github.com/ImOk520/myspringcloud
4.1 引入依赖
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>${elasticsearch}</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>${elasticsearch}</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>${elasticsearch}</version>
</dependency>
4.2 配置ES客户端
@Configuration
public class EsConfig {
@Bean
public RestHighLevelClient initRHLC(){
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("localhost", 9200)
)
);
return client;
}
}
4.3 代码实例
@Slf4j
@RequestMapping("/es")
@RestController
public class EsController {
@Autowired
// @Qualifier("restHighLevelClient")
private RestHighLevelClient client;
@ApiOperation("创建索引")
@PostMapping("/createIndex")
public void createIndex() throws IOException {
// 1.创建索引请求
CreateIndexRequest request = new CreateIndexRequest("index-2");
// 2.客户端执行请求
CreateIndexResponse response = client
.indices()
.create(request, RequestOptions.DEFAULT);
Console.log(response);
}
@ApiOperation("获取索引")
@PostMapping("/getIndex")
public boolean getIndex() throws IOException {
// 1.创建索引请求
GetIndexRequest request = new GetIndexRequest("index-2");
// 2.客户端执行请求
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
GetIndexResponse getIndexResponse = client.indices().get(request, RequestOptions.DEFAULT);
System.out.println("[getIndexResponse:]" + getIndexResponse);
return exists;
}
@ApiOperation("删除索引")
@PostMapping("/delIndex")
public AcknowledgedResponse delIndex() throws IOException {
// 1.创建索引请求
DeleteIndexRequest request = new DeleteIndexRequest("index-2");
// 2.客户端执行请求
AcknowledgedResponse delete = client.indices().delete(request, RequestOptions.DEFAULT);
return delete;
}
@ApiOperation("添加文档")
@PostMapping("/addDoc")
public IndexResponse addDoc() throws IOException {
// 1.创建文档信息
B b = new B("鲁智深", 18);
// 2.创建请求
IndexRequest request = new IndexRequest("index-2");
request.id("1");
request.timeout("1s");
request.source(JSONUtil.toJsonStr(b), XContentType.JSON);
IndexResponse indexResponse = client.index(request, RequestOptions.DEFAULT);
return indexResponse;
}
@ApiOperation("获取文档")
@PostMapping("/getDoc")
public String getDoc() throws IOException {
// 1.创建请求
GetRequest request = new GetRequest("index-2", "1");
// 2.客户端执行请求
GetResponse documentFields = client.get(request, RequestOptions.DEFAULT);
return documentFields.getSourceAsString();
}
@ApiOperation("更新文档")
@PostMapping("/updateDoc")
public RestStatus updateDoc() throws IOException {
B b = new B("鲁智深", 19);
UpdateRequest request = new UpdateRequest("index-2", "1");
request.doc(JSONUtil.toJsonStr(b), XContentType.JSON);
UpdateResponse updateResponse = client.update(request, RequestOptions.DEFAULT);
return updateResponse.status();
}
@ApiOperation("删除文档")
@PostMapping("/delDoc")
public RestStatus delDoc() throws IOException {
DeleteRequest request = new DeleteRequest("index-2", "1");
DeleteResponse deleteResponse = client.delete(request, RequestOptions.DEFAULT);
return deleteResponse.status();
}
@ApiOperation("批量添加文档")
@PostMapping("/batchAddDoc")
public String batchAddDoc() throws IOException {
List<B> bList = new ArrayList<B>();
bList.add(new B("林冲", 20));
bList.add(new B("武松", 21));
bList.add(new B("扈三娘", 18));
bList.add(new B("张青", 22));
bList.add(new B("孙二娘", 18));
bList.add(new B("王英", 30));
bList.add(new B("花荣", 20));
BulkRequest bulkRequest = new BulkRequest();
for (int i = 0; i < bList.size(); i++) {
bulkRequest.add(
new IndexRequest("index-2").id("" + (i + 1)).source(JSONUtil.toJsonStr(bList.get(i)), XContentType.JSON)
);
}
BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);
return bulkResponse.buildFailureMessage();
}
@ApiOperation("按条件查询文档")
@PostMapping("/searchDoc")
public String searchDoc() throws IOException {
SearchRequest searchRequest = new SearchRequest("index-2");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("", 20);
sourceBuilder.query(termQueryBuilder);
sourceBuilder.timeout();
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
log.info(JSONUtil.toJsonStr(searchResponse.getHits()));
for (SearchHit hit : searchResponse.getHits()) {
System.out.println(hit.getSourceAsMap());
System.out.println("============================");
System.out.println(hit.getSourceAsString());
}
return JSONUtil.toJsonStr(searchResponse.getHits());
}
@ApiOperation("将爬虫数据存入ES")
@PostMapping("/saveHtmlDoc")
public boolean saveHtmlDoc(String keyword, String indexName) throws IOException {
List<Good> goods = HtmlParseUtil.parseJD(keyword);
// 商品批量入ES
BulkRequest bulkRequest = new BulkRequest();
bulkRequest.timeout(TimeValue.MAX_VALUE);
for (int i = 0; i < goods.size(); i++) {
int i1 = RandomUtil.randomInt(1, 100000000);
bulkRequest.add(
new IndexRequest(indexName).id("" + i1).source(JSONUtil.toJsonStr(goods.get(i)), XContentType.JSON)
);
}
BulkResponse response = client.bulk(bulkRequest, RequestOptions.DEFAULT);
return response.hasFailures();
}
@ApiOperation("搜索ES中的爬虫数据")
@GetMapping("/searchHtmlDoc")
public List<Map<String, Object>> searchHtmlDoc(String keyword, int pageIndex, int pageSize, String indexName) throws IOException {
SearchRequest request = new SearchRequest(indexName);
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.from(pageIndex);
sourceBuilder.size(pageSize);
// 精准匹配
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("name", keyword);
sourceBuilder.query(termQueryBuilder);
sourceBuilder.timeout(TimeValue.MAX_VALUE);
// 高亮处理
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("name");
highlightBuilder.requireFieldMatch(false);
highlightBuilder.preTags("<span style='color:red'>");
highlightBuilder.postTags("</span>");
sourceBuilder.highlighter(highlightBuilder);
request.source(sourceBuilder);
SearchResponse search = client.search(request, RequestOptions.DEFAULT);
SearchHit[] hits = search.getHits().getHits();
ArrayList<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
for (SearchHit hit : hits) {
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
HighlightField name = highlightFields.get("name");
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
if (name != null) {
Text[] fragments = name.fragments();
String result_name = "";
for (Text text : fragments) {
result_name += text;
}
sourceAsMap.put("name", result_name);
}
list.add(sourceAsMap);
}
return list;
}
}
上面集成了一下jsoup网页爬虫工具:
@Slf4j
@Component
public class HtmlParseUtil {
public static List<Good> parseJD(String keyword) throws IOException {
String url = "https://search.jd.com/Search?keyword=" + keyword;
Document document = Jsoup.parse(new URL(url), 30000);
Element element = document.getElementById("J_goodsList");
Elements elements = element.getElementsByTag("li");
ArrayList<Good> goods = new ArrayList<Good>();
for (Element ele : elements) {
String img = ele.getElementsByTag("img").eq(0).attr("data-lazy-img");
String price = ele.getElementsByClass("p-price").eq(0).text();
String name = ele.getElementsByClass("p-name").eq(0).text();
System.out.println(img);
System.out.println(price);
System.out.println(name);
Good good = new Good();
good.setImg(img);
good.setName(name);
good.setPrice(price);
goods.add(good);
}
return goods;
}
}
可以看到爬取的网页数据正常保存到ES中:
再检索查询下:
可以看到正常获取。