一. Elastic Search简介
Elasticsearch 是一个分布式、RESTful 风格,基于Lucene封装和增强,的搜索和数据分析引擎,能够解决不断涌现出的各种用例。它的目的是通过简单的RESTful API来隐藏Lucene的复杂性,从而让全文搜索变得简单。
功能:
- 全文搜索
- 结构化搜索
- 分析
Elastic Search与solr对比:
- 当单纯对已有数据搜索,solr更快,常用于电商等查询多的应用
- 当实时建立索引时,solr会产生io阻塞,查询性能会比较差,es无明显变化,常用于实时搜索
- 随数据量增加,solr的搜索效率会大幅下降,es无明显变化
- solr使用zookeeper进行分布式管理,es自带分布式协调管理
- solr支持多种格式,es只支持json
- solr具有很多功能,es更注重核心功能,支持插件扩展
- solr社区更加完善,es开发维护者少、更新快、学习成本高
作为搜索引擎,es比solr效率高50倍左右。
ELK:由ElasticSearch、Logstash和Kiabana三个开源工具组成,常用作开源实时日志分析平台。
建议使用kibana去调试开发es,非常好用,还提供了中文的界面;同时可以配合使用谷歌的插件或者安装es header。下面都是使用这两个工具进行演示。
二. 核心概念
2.1. 设计
es可以类似为文档型数据库,一切都是json。
逻辑设计:索引 > 类型
物理设计:将每个索引划分成多个分片,每个分片可以在集群中的不同服务器间迁移。
一个集群至少有一个节点,一个节点就是一个es进程,节点可以有多个索引,如果创建索引,那么索引将会由5个(默认)主分片构成,每一个主分片有一个副本。一个分片就是一个Lucene索引,一个包含倒排索引的文件目录。倒排索引使得es在不扫描全部文档的情况下,搜索到包含关键字的文档。
2.2 倒排索引
正排索引指的是:key为文档1,value为文档中的词;
倒排索引指的是:key为某个词,value为包含该词的文档;
采用Lucene倒排索引作为底层,有利于全文搜索。
步骤1:将每个文档拆分成独立的词,然后创建一个包含所有不重复词条的排序列表,列出每个词条是否出现在某个文档中。
当搜索某个字符串,比如 to forever ,通过上面的表,我们可以得知,doc_1匹配2个词,doc_2匹配1个词,我们将匹配的词数称为权重(score),它会按照权重的大小由高到低返回结果。
2.3 类型
如果不设置,系统会自己猜。已经被废弃使用了。
建议全部不写类型,下面示列中有些写了类型的,请无视。
默认类型为_doc
补充. IK分词器插件
下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.14.0
算法:最少切分ik_smart;最细粒度切分ik_max_word
可以自己扩展词典,新建dic文件,每写一个词换一行,然后在下方的xml中注入。
三. 基础操作
官方教程:https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html
官方教程版本过于落后,只能参考看看。
官方文档:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html
版本可以选择,但是全是英语QAQ
3.1. 创建索引以及字段
PUT /索引名
类型属性创建完就无法更改
java api: https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-document-index.html
使用IndexRequest:
public boolean createIndex(String index, String id, Map<String, Object> parameters) throws IOException {
IndexRequest indexRequest = new IndexRequest(index).id(id).source(parameters);
IndexResponse indexResponse = client.index(indexRequest, RequestOptions.DEFAULT);
return indexResponse.getResult() == DocWriteResponse.Result.CREATED;
}
测试代码:
@Test
void contextLoads() throws IOException {
Map<String, Object> map= new HashMap();
contentService.createIndex("create", "1", map);
}
3.2. 添加数据
没有索引的话,自动创建索引】
PUT /索引名/类型名 /文档id
{请求体}
因为es7已经废弃type,es8以后会删除type,所以这边建议不使用它。
默认用_doc的来创建索引,它会非常智能地选择匹配的类型。
3.3. 获取数据
GET 索引名
GET _cat/ 可以获得索引情况
GET _cat/health
GET _cat/indices
java源码注释:
根据 id 从索引中获取文档(其源)的请求。 最好使用org.elasticsearch.client.Requests.getRequest(String)创建。
该操作需要设置index() 、 type(String)和id(String) 。
请参见:
GetResponse , org.elasticsearch.client.Requests.getRequest(String) , org.elasticsearch.client.Client.get(GetRequest)
一般通过GetRequest(String index, String id)来获取文档【type类型已经过时了】。
3.4. 修改索引
修改完成后,版本号会自增1【CAS乐观锁】,状态修改为updated;
- 直接put同文件id覆盖
- post + update
第二种可以部分更新,是新版本推荐的方法
3.5 删除操作
DELETE 索引名
DELETE 索引名/类型
四. 文档操作(重点)
4.1. 条件查询
GET 索引名/类型
public Map<String, Object> getIndex(String index, String id) throws IOException{
GetRequest getRequest = new GetRequest(index, id);
GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT);
if(getResponse.isExists()) {
return getResponse.getSourceAsMap();
}
return null;
}
@Test
void getContent() throws IOException {
Map<String, Object> map = contentService.getIndex("hzh", "1");
if(map == null) {
return;
}
for (Map.Entry<String, Object> entry : map.entrySet()) {
System.out.println(entry.getKey() + " " + entry.getValue());
}
}
模糊查找
score为匹配度
缩写写法:
(这里请不要加user这些类型)
通过_source可以指定需要的信息:
这里选择获取name,不过滤属性。include是获取的属性,exclude是过滤的属性,第一个参数必须为true。
public Map<String, Object> getSource(String index, String id) throws IOException {
GetSourceRequest getSourceRequest = new GetSourceRequest(index, id);
String[] includes = new String[]{"name"};
String[] excludes = Strings.EMPTY_ARRAY;
getSourceRequest.fetchSourceContext(new FetchSourceContext(true, includes, excludes));
GetSourceResponse getSourceResponse = client.getSource(getSourceRequest, RequestOptions.DEFAULT);
return getSourceResponse.getSource();
}
通过sort可以排序,asc为升序,desc为降序
分页
from 和 size
从第from个数据开始【从零开始】,返回size条数据
must / should / must_not
等同于and / or / !(and)
但需要注意的是,中文匹配,只要其中一个字匹配上了,就算匹配成功。
精确匹配 term/terms ;模糊匹配 match/matches 会调用分词器【keyword类型仍然不会被分词】
中文匹配还是很诡异,只能匹配字符串中的任意一个字,你多输一个字就出不来。
因为当我们用 term 查询查找精确值的时候,它并不在我们的倒排索引中,中文实际上已经被分词了。当我们将这个中文的类型定义为keyword的时候,我们就可以获取到它的值。
filiter过滤器,放在bool下;
range:可以指定范围 lt<,lte ≤,gt>,gte≥
高亮查询
自定义高亮:设置前缀和后缀
五. 集成SpringBoot
5.1. 配置环境
- springboot导入es模块
- 配置类
package com.hzh.es.config;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ElasticSearchConfig {
@Bean
public RestHighLevelClient restHighLevelClient() {
return new RestHighLevelClient(
RestClient.builder(
new HttpHost("localhost", 9200, "http")));
}
}
如果你很不幸地和我一样,手贱地配置了账号密码,那么还得加亿点代码
package com.hzh.es.config;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
@Configuration
public class ElasticSearchConfig {
@Value("${spring.elasticsearch.rest.username}")
private String USERNAME;
@Value("${spring.elasticsearch.rest.password}")
private String PASSWORD;
@Bean
public RestHighLevelClient restHighLevelClient() throws IOException {
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(USERNAME, PASSWORD));
return new RestHighLevelClient(
RestClient.builder(
new HttpHost("localhost", 9200, "http")).setHttpClientConfigCallback(httpClientBuilder -> {
httpClientBuilder.disableAuthCaching();
return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
}));
}
}
记得去yml里设置一下账号密码哦
spring:
elasticsearch:
rest:
username: elastic
password: xRTtazkDKeRC0KhuOH5e
5.2. 增删改查
package com.hzh.es;
import com.alibaba.fastjson.JSON;
import com.hzh.es.pojo.User;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
import java.util.ArrayList;
@SpringBootTest
class EsApplicationTests {
@Autowired
@Qualifier("restHighLevelClient")
private RestHighLevelClient client;
/**
* description: 测试创建的索引
*
* @return void
* @author 九幽孤翎
* @date 16:56
*/
@Test
void createIndex() throws IOException {
CreateIndexRequest request = new CreateIndexRequest("hzh_index");
CreateIndexResponse createIndexResponse = client.indices().create(request, RequestOptions.DEFAULT);
System.out.println(createIndexResponse);
}
@Test
void isExistsIndex() throws IOException {
GetIndexRequest request = new GetIndexRequest("hzh_index");
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
System.out.println(exists);
}
@Test
void deleteIndex() throws IOException {
DeleteIndexRequest request = new DeleteIndexRequest("hzh_index");
AcknowledgedResponse delete = client.indices().delete(request, RequestOptions.DEFAULT);
System.out.println(delete.isAcknowledged());
}
@Test
void addDocument() throws IOException {
User user = new User("hzh", 21);
IndexRequest request = new IndexRequest("hzh_index");
request.id("1").timeout("1s").source(JSON.toJSONString(user), XContentType.JSON);
IndexResponse indexResponse = client.index(request, RequestOptions.DEFAULT);
System.out.println(indexResponse);
System.out.println(indexResponse.status());
}
@Test
void isExistsDocument() throws IOException {
GetRequest request = new GetRequest("hzh_index", "1");
boolean exists = client.exists(request, RequestOptions.DEFAULT);
System.out.println(exists);
}
@Test
void getDocument() throws IOException {
GetRequest request = new GetRequest("hzh_index", "1");
GetResponse response = client.get(request, RequestOptions.DEFAULT);
System.out.println(response.getSourceAsString());
System.out.println(response);
}
@Test
void updateDocument() throws IOException {
UpdateRequest request = new UpdateRequest("hzh_index", "1");
User user = new User("hzh_张三", 21);
request.doc(JSON.toJSONString(user), XContentType.JSON);
UpdateResponse update = client.update(request, RequestOptions.DEFAULT);
System.out.println(update.status());
}
@Test
void deleteDocument() throws IOException {
DeleteRequest request = new DeleteRequest("hzh_index", "1");
DeleteResponse delete = client.delete(request, RequestOptions.DEFAULT);
System.out.println(delete.status());
}
/**
* description: 批量导入数据
*
* @return void
* @author 九幽孤翎
* @date 19:55
*/
@Test
void testBulkRequest() throws IOException {
BulkRequest request = new BulkRequest();
request.timeout("10s");
ArrayList<User> userList = new ArrayList();
for (int i = 0; i < 10; i++) {
userList.add(new User("hzh_" + i, i));
}
for (int i = 0; i < 10; i++) {
request.add(new IndexRequest("hzh_index").id(String.valueOf(i + 1)).source(JSON.toJSONString(userList.get(i)), XContentType.JSON));
}
BulkResponse responses = client.bulk(request, RequestOptions.DEFAULT);
System.out.println(responses.hasFailures());
}
@Test
void searchRequest() throws IOException {
SearchRequest request = new SearchRequest("hzh_index");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.highlighter();
TermQueryBuilder queryBuilder = QueryBuilders.termQuery("name", "hzh_0");
sourceBuilder.query(queryBuilder);
request.source(sourceBuilder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
System.out.println(JSON.toJSONString(response.getHits()));
System.out.println("---------------------------------------");
for (SearchHit hit : response.getHits()) {
System.out.println(hit.getSourceAsMap());
}
}
}
六. 仿写京东实战
内容参考狂神,不过狂神没有导入cookie,按他那么写,基本上是不大可能爬到数据。
然后controller部分也按照我个人的理解,进行了微调。
6.1. jsoup爬虫
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>version</version>
</dependency>
工具类
这里注意一下,因为京东淘宝都有设置基础的反爬,当检测到我们的访问请求是通过java等编程语言代码提交的,他就会直接跳转到登录页面,导致访问失败。我们这里可以使用一个cookie,将你访问京东的网页cookie【log.gif?search】导入到程序即可。jsoup允许我们通过connection来设置header,这里不仅可以设置cookie还可以设置很多参数。我们观察到京东的网页中是用J_goodsList来保存商品的信息,其中每个li就是一个商品的信息,我们通过api获取并注入到list中。
注意,这边即使设置了cookie,也不能保证一定能查出来,所以可以通过一个for循环来完成一定查询出来。
pojo
package com.es.jd.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author : 九幽孤翎
* @date : 2021/8/20 15:15
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Content {
private String title;
private String price;
private String img;
}
util
package com.es.jd.utils;
import com.es.jd.pojo.Content;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* @author : 九幽孤翎
* @date : 2021/8/20 14:28
*/
public class HtmlParseUtil {
// 测试函数
public static void main(String[] args) throws Exception {
parseJD("hello");
}
public static List<Content> parseJD(String keyword) throws IOException {
Element element = null;
while (element == null) {
System.out.println("正在进行一次连接尝试");
String url = "https://search.jd.com/search?keyword=" + keyword;
Connection connect = Jsoup.connect(url);
connect.header("cookie", new Scanner(new File("C:\\Users\\Riter\\Desktop\\program\\work\\javaProject\\jd\\src\\main\\resources\\cookie.txt")).next());
//解析网页
Document document = connect.get();
element = document.getElementById("J_goodsList");
}
Elements elements = element.getElementsByTag("li");
List<Content> goodList = 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();
goodList.add(new Content(title, price, img));
//测试输出
// System.out.println("===============================================");
// System.out.println(img);
// System.out.println(price);
// System.out.println(title);
}
return goodList;
}
}
6.2. service
package com.es.jd.service;
import com.alibaba.fastjson.JSON;
import com.es.jd.pojo.Content;
import com.es.jd.utils.HtmlParseUtil;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author : 九幽孤翎
* @date : 2021/8/20 15:26
*/
@Service
public class ContentService {
@Autowired
@Qualifier("restHighLevelClient")
private RestHighLevelClient client;
public boolean parseContent(String keywords) throws IOException, InterruptedException {
List<Content> contents = HtmlParseUtil.parseJD(keywords);
BulkRequest bulkRequest = new BulkRequest();
bulkRequest.timeout("1h");
for (int i = 0; i < contents.size(); i++) {
bulkRequest.add(new IndexRequest("jd_goods").source(JSON.toJSONString(contents.get(i)), XContentType.JSON));
}
BulkResponse responses = client.bulk(bulkRequest, RequestOptions.DEFAULT);
return !responses.hasFailures();
}
public List<Map<String, Object>> searchPage(String keyword, int pageNo, int pageSize) throws IOException {
System.out.println("开始查询数据!");
if (pageNo <= 1) {
pageNo = 1;
}
// 搜索
SearchRequest searchRequest = new SearchRequest("jd_goods");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 分页
sourceBuilder.from(pageNo);
sourceBuilder.size(pageSize);
// 精准匹配
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 = client.search(searchRequest, RequestOptions.DEFAULT);
int num = 0;
// 解析结果
ArrayList<Map<String, Object>> list = new ArrayList<>();
for (SearchHit hit : searchResponse.getHits().getHits()) {
// 解析高亮字段
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
HighlightField title = highlightFields.get("title");
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
if (title != null) {
StringBuilder n_title = new StringBuilder();
// 将高亮字段替换原字段
for (Text fragment : title.fragments()) {
n_title.append(fragment);
}
sourceAsMap.put("title", new String(n_title));
}
list.add(sourceAsMap);
System.out.println("输出第" + (++num) + "条数据成功");
}
return list;
}
}
6.4. controller
package com.es.jd.controller;
import com.es.jd.service.ContentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* @author : 九幽孤翎
* @date : 2021/8/20 15:25
*/
@RestController
public class ContentController {
@Autowired
private ContentService contentService;
@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, InterruptedException {
contentService.parseContent(keyword);
System.out.println("数据导入成功");
Thread.sleep(1000);
return contentService.searchPage(keyword, pageNo, pageSize);
}
}