乐趣区

关于java:使用-Elasticsearch-搭建自己的搜索系统这个厉害了

作者:Hai Xiang \
起源:https://www.cnblogs.com/haixi…

什么是 elasticsearch

Elasticsearch 是一个开源的高度可扩大的全文搜寻和剖析引擎,领有查问近实时的超强性能。

赫赫有名的 Lucene 搜索引擎被宽泛用于搜寻畛域,然而操作简单繁琐,总是让开发者敬而远之。而 Elasticsearch 将 Lucene 作为其外围来实现所有索引和搜寻的性能,通过简略的 RESTful 语法来暗藏掉 Lucene 的复杂性,从而让全文搜寻变得简略

ES 在 Lucene 根底上,提供了一些分布式的实现:集群,分片,复制等。

搜寻为什么不必 MySQL 而用 es

咱们本文案例是一个迷你商品搜寻零碎,为什么不思考应用 MySQL 来实现搜寻性能呢?起因如下:

  • MySQL 默认应用 innodb 引擎,底层采纳 b + 树的形式来实现,而 Es 底层应用倒排索引的形式实现,应用倒排索引反对各种维度的分词,能够掌控不同粒度的搜寻需要。(MYSQL8 版本也反对了全文检索,应用倒排索引实现,有趣味能够去看看两者的差异)
  • 如果应用 MySQL 的 %key% 的含糊匹配来与 es 的搜寻进行比拟,在 8 万数据量时他们的耗时曾经达到 40:1 左右,毫无疑问在速度方面 es 完胜。

es 在大厂中的利用状况

  • es 使用最宽泛的是 elk 组合来对日志进行搜寻剖析
  • 58 安全部门、京东订单核心简直全采纳 es 来实现相干信息的存储与检索
  • es 在 tob 的我的项目中也用于各种检索与剖析
  • 在 c 端产品中,企业通常本人基于 Lucene 封装本人的搜寻零碎,为了适配公司营销策略、举荐零碎等会有更多定制化的搜寻需要

es 客户端选型

spring-boot-starter-data-elasticsearch

我置信你看到的网上各类公开课视频或者小我的项目均举荐应用这款 springboot 整合过的 es 客户端,然而咱们要 say no!

另外,ES 系列面试题和答案全副整顿好了,微信搜寻​Java 技术栈,在后盾发送:面试,​能够在线浏览。

此图是引入的最新版本的依赖,咱们能够看到它所应用的 es-high-client 也为 6.8.7,而 es7.x 版本都曾经更新很久了,这里许多新个性都无奈应用,所以版本滞后是他最大的问题。而且它的底层也是 highclient,咱们操作 highclient 能够更灵便。我呆过的两个公司均未采纳此客户端。

elasticsearch-rest-high-level-client

这是官网举荐的客户端,反对最新的 es,其实应用起来也很便当,因为是官网举荐所以在个性的操作上必定优于前者。而且该客户端与 TransportClient 不同,不存在并发瓶颈的问题,官网首推,必为精品!

搭建本人的迷你搜寻零碎

引入 es 相干依赖,除此之外需引入 springboot-web 依赖、jackson 依赖以及 lombok 依赖等。

Spring Boot 根底就不介绍了,举荐下这个实战教程:
https://www.javastack.cn/cate…

<properties>
    <es.version>7.3.2</es.version>
</properties>
<!-- high client-->
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>${es.version}</version>
    <exclusions>
        <exclusion>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-client</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.elasticsearch</groupId>
            <artifactId>elasticsearch</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>${es.version}</version>
</dependency>

<!--rest low client high client 以来低版本 client 所以须要引入 -->
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-client</artifactId>
    <version>${es.version}</version>
</dependency>

es 配置文件 es-config.properties

es.host=localhost
es.port=9200
es.token=es-token
es.charset=UTF-8
es.scheme=http

es.client.connectTimeOut=5000
es.client.socketTimeout=15000

封装 RestHighLevelClient

@Configuration
@PropertySource("classpath:es-config.properties")
public class RestHighLevelClientConfig {@Value("${es.host}")
    private String host;
    @Value("${es.port}")
    private int port;
    @Value("${es.scheme}")
    private String scheme;
    @Value("${es.token}")
    private String token;
    @Value("${es.charset}")
    private String charSet;
    @Value("${es.client.connectTimeOut}")
    private int connectTimeOut;
    @Value("${es.client.socketTimeout}")
    private int socketTimeout;

    @Bean
    public RestClientBuilder restClientBuilder() {
        RestClientBuilder restClientBuilder = RestClient.builder(new HttpHost(host, port, scheme)
        );

        Header[] defaultHeaders = new Header[]{new BasicHeader("Accept", "*/*"),
                new BasicHeader("Charset", charSet),
                // 设置 token 是为了平安 网关能够验证 token 来决定是否发动申请 咱们这里只做象征性配置
                new BasicHeader("E_TOKEN", token)
        };
        restClientBuilder.setDefaultHeaders(defaultHeaders);
        restClientBuilder.setFailureListener(new RestClient.FailureListener(){
            @Override
            public void onFailure(Node node) {System.out.println("监听某个 es 节点失败");
            }
        });
        restClientBuilder.setRequestConfigCallback(builder ->
                builder.setConnectTimeout(connectTimeOut).setSocketTimeout(socketTimeout));
        return restClientBuilder;
    }

    @Bean
    public RestHighLevelClient restHighLevelClient(RestClientBuilder restClientBuilder) {return new RestHighLevelClient(restClientBuilder);
    }
}

封装 es 罕用操作 es 搜寻零碎封装源码

@Service
public class RestHighLevelClientService {
    
    @Autowired
    private RestHighLevelClient client;

    @Autowired
    private ObjectMapper mapper;

    /**
     * 创立索引
     * @param indexName
     * @param settings
     * @param mapping
     * @return
     * @throws IOException
     */
    public CreateIndexResponse createIndex(String indexName, String settings, String mapping) throws IOException {CreateIndexRequest request = new CreateIndexRequest(indexName);
        if (null != settings && !"".equals(settings)) {request.settings(settings, XContentType.JSON);
        }
        if (null != mapping && !"".equals(mapping)) {request.mapping(mapping, XContentType.JSON);
        }
        return client.indices().create(request, RequestOptions.DEFAULT);
    }

    /**
     * 判断 index 是否存在
     */
    public boolean indexExists(String indexName) throws IOException {GetIndexRequest request = new GetIndexRequest(indexName);
        return client.indices().exists(request, RequestOptions.DEFAULT);
    }
    
    /**
     * 搜寻
    */
    public SearchResponse search(String field, String key, String rangeField, String 
                                 from, String to,String termField, String termVal, 
                                 String ... indexNames) throws IOException{SearchRequest request = new SearchRequest(indexNames);

        SearchSourceBuilder builder = new SearchSourceBuilder();
        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
        boolQueryBuilder.must(new MatchQueryBuilder(field, key)).must(new RangeQueryBuilder(rangeField).from(from).to(to)).must(new TermQueryBuilder(termField, termVal));
        builder.query(boolQueryBuilder);
        request.source(builder);
        log.info("[搜寻语句为:{}]",request.source().toString());
        return client.search(request, RequestOptions.DEFAULT);
    }

    /**
     * 批量导入
     * @param indexName
     * @param isAutoId 应用主动 id 还是应用传入对象的 id
     * @param source
     * @return
     * @throws IOException
     */
    public BulkResponse importAll(String indexName, boolean isAutoId, String  source) throws IOException{if (0 == source.length()){//todo 抛出异样 导入数据为空}
        BulkRequest request = new BulkRequest();
        JsonNode jsonNode = mapper.readTree(source);

        if (jsonNode.isArray()) {for (JsonNode node : jsonNode) {if (isAutoId) {request.add(new IndexRequest(indexName).source(node.asText(), XContentType.JSON));
                } else {request.add(new IndexRequest(indexName)
                            .id(node.get("id").asText())
                            .source(node.asText(), XContentType.JSON));
                }
            }
        }
        return client.bulk(request, RequestOptions.DEFAULT);
    }

创立索引,这里的 settings 是设置索引是否设置复制节点、设置分片个数,mappings 就和数据库中的表构造一样,用来指定各个字段的类型,同时也能够设置字段是否分词(咱们这里应用 ik 中文分词器)、采纳什么分词形式。

@Test
public void createIdx() throws IOException {String settings = ""+"  {\n"+"      \"number_of_shards\" : \"2\",\n"+"      \"number_of_replicas\" : \"0\"\n"+"}";
    String mappings = ""+"{\n"+"    \"properties\": {\n"+"      \"itemId\" : {\n"+"        \"type\": \"keyword\",\n"+"        \"ignore_above\": 64\n"+"},\n"+"      \"urlId\" : {\n"+"        \"type\": \"keyword\",\n"+"        \"ignore_above\": 64\n"+"},\n"+"      \"sellAddress\" : {\n"+"        \"type\": \"text\",\n"+"        \"analyzer\": \"ik_max_word\", \n"+"        \"search_analyzer\": \"ik_smart\",\n"+"        \"fields\": {\n"+"          \"keyword\" : {\"ignore_above\" : 256, \"type\" : \"keyword\"}\n"+"        }\n"+"      },\n"+"      \"courierFee\" : {\n"+"        \"type\": \"text\n" +
            "},\n" +
            "\"promotions\": {\n" +
            "\"type\": \"text\",\n" +
            "\"analyzer\": \"ik_max_word\", \n" +
            "\"search_analyzer\": \"ik_smart\",\n" +
            "\"fields\": {\n" +
            "\"keyword\": {\"ignore_above\": 256, \"type\": \"keyword\"}\n" +
            "}\n" +
            "},\n" +
            "\"originalPrice\": {\n" +
            "\"type\": \"keyword\",\n" +
            "\"ignore_above\": 64\n" +
            "},\n" +
            "\"startTime\": {\n" +
            "\"type\": \"date\",\n" +
            "\"format\": \"yyyy-MM-dd HH:mm:ss\"\n" +
            "},\n" +
            "\"endTime\": {\n" +
            "\"type\": \"date\",\n" +
            "\"format\": \"yyyy-MM-dd HH:mm:ss\"\n" +
            "},\n" +
            "\"title\": {\n" +
            "\"type\": \"text\",\n" +
            "\"analyzer\": \"ik_max_word\", \n" +
            "\"search_analyzer\": \"ik_smart\",\n" +
            "\"fields\": {\n" +
            "\"keyword\": {\"ignore_above\": 256, \"type\": \"keyword\"}\n" +
            "}\n" +
            "},\n" +
            "\"serviceGuarantee\": {\n" +
            "\"type\": \"text\",\n" +
            "\"analyzer\": \"ik_max_word\", \n" +
            "\"search_analyzer\": \"ik_smart\",\n" +
            "\"fields\": {\n" +
            "\"keyword\": {\"ignore_above\": 256, \"type\": \"keyword\"}\n" +
            "}\n" +
            "},\n" +
            "\"venue\": {\n" +
            "\"type\": \"text\",\n" +
            "\"analyzer\": \"ik_max_word\", \n" +
            "\"search_analyzer\": \"ik_smart\",\n" +
            "\"fields\": {\n" +
            "\"keyword\": {\"ignore_above\": 256, \"type\": \"keyword\"}\n" +
            "}\n" +
            "},\n" +
            "\"currentPrice\": {\n" +
            "\"type\": \"keyword\",\n" +
            "\"ignore_above\": 64\n" +
            "}\n" +
            "}\n" +
            "}";
    clientService.createIndex("idx_item", settings, mappings);
}

分词技巧

  • 索引时最小分词,搜寻时最大分词,例如 ”Java 知音 ” 索引时候词蕴含 Java、知音、音、知等,最小粒度分词能够让咱们匹配更多的检索需要,然而咱们搜寻时应该设置最大分词,用“Java”和“知音”去匹配索引库,失去的后果更贴近咱们的目标,
  • 对分词字段同时也设置 keyword,便于后续排查谬误时能够准确匹配搜寻,疾速定位。

咱们向 es 导入十万条淘宝双 11 流动数据作为咱们的样本数据,数据结构如下所示

{
    "_id": "https://detail.tmall.com/item.htm?id=538528948719\u0026skuId=3216546934499",
    "卖家地址": "上海",
    "快递费": "运费: 0.00 元",
    "优惠活动": "满 199 减 10, 满 299 减 30, 满 499 减 60, 可跨店",
    "商品 ID": "538528948719",
    "原价": "2290.00",
    "流动开始工夫": "2016-11-11 00:00:00",
    "流动完结工夫": "2016-11-11 23:59:59",
    "题目": "【天猫海内直营】ReFa CARAT RAY 黎珐 双球滚轮波光美容仪",
    "服务保障": "副品保障; 赠运费险; 极速退款; 七天退换",
    "会场": "进口尖货",
    "现价": "1950.00"
}

调用下面封装的批量导入办法进行导入

@Test
public void importAll() throws IOException {clientService.importAll("idx_item", true, itemService.getItemsJson());
}

咱们调用封装的搜寻办法进行搜寻,搜寻产地为武汉、价格在 11-149 之间的相干酒产品,这与咱们淘宝中设置筛选条件搜寻商品操作统一。

@Test
public void search() throws IOException {
    SearchResponse search = clientService.search("title", "酒", "currentPrice",
            "11", "149", "sellAddress", "武汉");
    SearchHits hits = search.getHits();
    SearchHit[] hits1 = hits.getHits();
    for (SearchHit documentFields : hits1) {System.out.println( documentFields.getSourceAsString());
    }
}

咱们失去以下搜寻后果,其中_score 为某一项的得分,商品就是依照它来排序。

{
  "_index": "idx_item",
  "_type": "_doc",
  "_id": "Rw3G7HEBDGgXwwHKFPCb",
  "_score": 10.995819,
  "_source": {
    "itemId": "525033055044",
    "urlId": "https://detail.tmall.com/item.htm?id=525033055044&skuId=def",
    "sellAddress": "湖北武汉",
    "courierFee": "快递: 0.00",
    "promotions": "满 199 减 10, 满 299 减 30, 满 499 减 60, 可跨店",
    "originalPrice": "3768.00",
    "startTime": "2016-11-01 00:00:00",
    "endTime": "2016-11-11 23:59:59",
    "title": "酒嗨酒 西班牙原瓶原装进口红酒蒙德干红葡萄酒 6 只装整箱送酒具",
    "serviceGuarantee": "破损包退; 副品保障; 公益宝贝; 不反对 7 天退换; 极速退款",
    "venue": "食品主会场",
    "currentPrice": "151.00"
  }
}

扩展性思考

  • 商品搜寻权重扩大,咱们能够利用多种免费形式智能为不同店家提供减少权重,减少曝光度适应本身的营销策略。同时咱们常常发现淘宝搜寻前列的商品许多为咱们之前查看过的商品,这是通过记录用户行为,跑模型等形式智能为这些商品减少权重。
  • 分词扩大,兴许因为某些商品的特殊性,咱们能够自定义扩大分词字典,更精准、人性化的搜寻。
  • 高亮性能,es 提供 highlight 高亮性能,咱们在淘宝上看到的商品展现中对搜寻关键字高亮,就是通过这种形式来实现。

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿(2022 最新版)

2. 劲爆!Java 协程要来了。。。

3.Spring Boot 2.x 教程,太全了!

4. 别再写满屏的爆爆爆炸类了,试试装璜器模式,这才是优雅的形式!!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

退出移动版