一. Elastic Search简介

Elasticsearch 是一个分布式、RESTful 风格,基于Lucene封装和增强,的搜索和数据分析引擎,能够解决不断涌现出的各种用例。它的目的是通过简单的RESTful API来隐藏Lucene的复杂性,从而让全文搜索变得简单。

功能:

  1. 全文搜索
  2. 结构化搜索
  3. 分析

Elastic Search与solr对比:

  1. 当单纯对已有数据搜索,solr更快,常用于电商等查询多的应用
  2. 当实时建立索引时,solr会产生io阻塞,查询性能会比较差,es无明显变化,常用于实时搜索
  3. 随数据量增加,solr的搜索效率会大幅下降,es无明显变化
  4. solr使用zookeeper进行分布式管理,es自带分布式协调管理
  5. solr支持多种格式,es只支持json
  6. solr具有很多功能,es更注重核心功能,支持插件扩展
  7. solr社区更加完善,es开发维护者少、更新快、学习成本高

作为搜索引擎,es比solr效率高50倍左右。

ELK:由ElasticSearch、Logstash和Kiabana三个开源工具组成,常用作开源实时日志分析平台。

es7 模板_spring

建议使用kibana去调试开发es,非常好用,还提供了中文的界面;同时可以配合使用谷歌的插件或者安装es header。下面都是使用这两个工具进行演示。

二. 核心概念

2.1. 设计

es可以类似为文档型数据库,一切都是json。

逻辑设计:索引 > 类型

物理设计:将每个索引划分成多个分片,每个分片可以在集群中的不同服务器间迁移。

一个集群至少有一个节点,一个节点就是一个es进程,节点可以有多个索引,如果创建索引,那么索引将会由5个(默认)主分片构成,每一个主分片有一个副本。一个分片就是一个Lucene索引,一个包含倒排索引的文件目录。倒排索引使得es在不扫描全部文档的情况下,搜索到包含关键字的文档。

es7 模板_spring_02

2.2 倒排索引

正排索引指的是:key为文档1,value为文档中的词;
倒排索引指的是:key为某个词,value为包含该词的文档;

采用Lucene倒排索引作为底层,有利于全文搜索。

步骤1:将每个文档拆分成独立的词,然后创建一个包含所有不重复词条的排序列表,列出每个词条是否出现在某个文档中。

es7 模板_spring_03


当搜索某个字符串,比如 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

es7 模板_es7 模板_04

es7 模板_elasticsearch_05


可以自己扩展词典,新建dic文件,每写一个词换一行,然后在下方的xml中注入。

es7 模板_elasticsearch_06

三. 基础操作

官方教程: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

es7 模板_spring_07

3.1. 创建索引以及字段

PUT /索引名

类型属性创建完就无法更改

es7 模板_solr_08


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 模板_es7 模板_09


es7 模板_es7 模板_10


因为es7已经废弃type,es8以后会删除type,所以这边建议不使用它。

默认用_doc的来创建索引,它会非常智能地选择匹配的类型。

es7 模板_solr_11

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;

  1. 直接put同文件id覆盖
  2. 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这些类型)

es7 模板_spring_12


通过_source可以指定需要的信息:

es7 模板_spring_13


这里选择获取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为降序

es7 模板_spring_14


分页

from 和 size

从第from个数据开始【从零开始】,返回size条数据

es7 模板_spring_15


must / should / must_not

等同于and / or / !(and)

但需要注意的是,中文匹配,只要其中一个字匹配上了,就算匹配成功。

es7 模板_spring_16


精确匹配 term/terms ;模糊匹配 match/matches 会调用分词器【keyword类型仍然不会被分词】

es7 模板_spring_17


中文匹配还是很诡异,只能匹配字符串中的任意一个字,你多输一个字就出不来。

因为当我们用 term 查询查找精确值的时候,它并不在我们的倒排索引中,中文实际上已经被分词了。当我们将这个中文的类型定义为keyword的时候,我们就可以获取到它的值。

es7 模板_elasticsearch_18


filiter过滤器,放在bool下;

range:可以指定范围 lt<,lte ≤,gt>,gte≥

es7 模板_spring_19


高亮查询

es7 模板_elasticsearch_20


自定义高亮:设置前缀和后缀

es7 模板_solr_21

五. 集成SpringBoot

5.1. 配置环境

  1. springboot导入es模块
  2. 配置类
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);
    }
}