SpringBoot整合ElasticSearch(控制度查询,父子关系创建)

 

   花了一个多月的时间,终于从懵懵懂懂到如今基本弄出了一个比较完整的结合需求的搜索引擎。中间遇到了不少问题,踩过不少的坑,中间也查阅过不少资料。可是感受这方面深刻一点的只是仍是蛮少的,如今就将一个多月里作出来的东西作一个总结,但愿你们共勉。html

  ElasticSearch安装什么的我就很少说了,安装完以后记得顺带装上Elastic-head和Sense(Beta)两个插件。在Chrome里面有,能够轻松的看到索引数据等信息。这几块资料不少,不会的同窗自行百度一下。这里强调一下千万要记得你使用的版本,就我我的这些天的使用来讲,版本不同,可能里面的方法会有这很大的不一样。反正尽可能将版本统一。java

 

1、集群配置

  集群好处不言而喻,如今基本上这种微服务的框架都会支持集群的配置。ES集群的配置也很简单,只须要将下载的ES文件复制几份就能够了。我这里用的是ES5.2.0,也就是2.X的版本。node

  复制完成以后,打开目录下面的config,找到elasticsearch.yml文件,并编辑。mysql

主机配置:web

cluster.name: market  #集群名称
 node.name: master     #该节点名称
 transport.tcp.port: 9300 #对应内部端口
 http.port: 9200          #外部访问端口
 network.host: 127.0.0.1  #对应ip
 discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300"] #主机ip
 node.master: true        #是否为主机

从机配置:算法

cluster.name: market
 node.name: server1
 transport.tcp.port: 9301
 http.port: 9201
 node.master: false
 network.host: 127.0.0.1
 discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300"]

多个从机只须要改变其中的 node.name, transport.tcp.port, http.port三个变量就能够了。等所有配置完成以后,先重启一下服务,注意三个服务所有重启。而后打开elasticsearch-head插件查看集群状态,若是为绿色的,则证实搭建成功。若是为黄色的,多是配置有问题,须要查看一下配置。spring

搭建成功示意。sql

2、SpringBoot+ElasticSearch

首先新建一个SpringBoot项目,而后导入Maven包。我Pom文件里东西太多了,我下面就只写与ES有关的包,其余的须要请自行添加。数据库

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
      <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>transport</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.elasticsearch</groupId>
                    <artifactId>elasticsearch</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.elasticsearch.plugin</groupId>
            <artifactId>transport-netty4-client</artifactId>
        </dependency>
</dependencies>

application.properties配置文件中加上以下配置,若是有集群的话,集群间使用逗号隔开,注意集群名字不要写错。apache

# elasticsearch搜索引擎
#节点名字,默认elasticsearch
spring.data.elasticsearch.cluster-name=market
#节点地址,多个节点用逗号隔开
spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300,127.0.0.1:9301,127.0.0.1:9302
#是否开启本地存储
spring.data.elasticsearch.repositories.enabled=true

配置完以后去新建ES实体。这里的ES实体和普通的实体区别不大,只是须要加上@Document和@Field注解。 

package com.isale.market.entity.es;

import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.*;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
@Slf4j
@NoArgsConstructor
@AllArgsConstructor
@Data
@Document(indexName = "listing",type = "salesListing", shards = 3,replicas = 0, refreshInterval = "-1")
public class SalesListingES  implements Serializable {
    @Id
    private Integer id;

    @Field(type = FieldType.Text, searchAnalyzer="ik_max_word", analyzer="ik_max_word")
    private String enTitle;

    @Field(type = FieldType.Text, searchAnalyzer="ik_max_word", analyzer="ik_max_word")
    private String cnTitle;

    @Field(type = FieldType.Integer,store=true)
    private int categoryId;

    @Field(type = FieldType.Text, searchAnalyzer="ik_max_word", analyzer="ik_max_word")
    private String description;


}

@Document中indexName对应的是索引名称,type对应的是该片断的名称,shard表明分片的数量,replicas表明副本数量。这些注解的具体意思能够到下面这个地址去看,写的很详细。

https://www.cnblogs.com/huangfox/p/3543351.html

这里注意es的indexName是不能用大写的,全部字母必须是小写。我当时某次使用大写,而后搞了半天索引没法建立。最后才发现是由于indexName写成了大写的缘由。type名称没有要求。

@Field注解主要是对字段进行一些限制,type设置字段类型,由于我用的这个版本没有String类型,可使用Text代替。后面SearchAnalyzer是设置分词器,若是在安装阶段没有安装分词器插件或者没有这个需求,就无需设置。分词器的安装可自行百度。

建完索引以后,去建相关的ESRepository接口,并继承,ElasticsearchRepository接口。

@Repository
public interface SalesListingESRepository extends ElasticsearchRepository<SalesListingES, Integer> {


}

而后Service层中注入该接口,就和JAP的使用方法同样,能够调用其中的CRUD方法对ES中的数据进行操做。这步比较简单,间不用多说了。

3、批量数据插入

想要把MySQL的数据批量插入到ES实体中,如今比较流行是使用logstash-input-jdbc插件。我本人也使用过这个插件,可是这个插件有一个巨大的问题,就是假如已经生成了ES实体,并指定某些字段的格式,如分词,类型等。若是使用logstash-input-jdbc批量插入,则会覆盖你指定的格式,也就是相似于从新生成一份index。若是须要指定mapping的同窗仍是不要使用这个工具,若是只是开始学,想加入测试数据的,可使用这个工具。具体安装我就很少逼逼了,想要使用这个,必须安装node.js和logstash,不会的同窗自行百度,这里主要讲一下配置。安装好以上两个插件以后,打开logstash目录下面的bin目录。在bin目录中新建一个后缀为.conf的文件,我这里就叫mysql.conf。而后打开这个配置文件,写入以下代码。

input {
    stdin {
    }
    jdbc {
      jdbc_connection_string => "jdbc:mysql://localhost/item"#数据库名称    
      jdbc_user => "root"#用户名    
      jdbc_password => "1234"#密码
      jdbc_driver_library => "F:/downsoft/logstash-5.3.2/jdbc/mysql-connector-java-5.1.47.jar"#mysql驱动位置
      jdbc_driver_class => "com.mysql.jdbc.Driver"
      jdbc_paging_enabled => "true"
      jdbc_page_size => "50"
      statement => "select * FROM item_cat"#想要插入的表名
      schedule => "* * * * *"
      type => "itemCat"  #type名称
    }
}
 
filter {
    json {
        source => "message"
        remove_field => ["message"]
    }
}
 
output {
    elasticsearch {
        hosts => ["localhost:9200"]
        index => "item"
        document_id => "%{cid}"#索引id对应的数据库id
#        template_name => "test"
#        template => "F:/downsoft/logstash-5.3.2/jdbc/item.json"
    }
    stdout {
        codec => json_lines
    }
}

这里注意记得导入mysql的jar包存放的位置,而后把数据库的信息改为本身对应的信息就能够了。改完以后,保存一下。而后打开dos,进入该配置文件存放的地址。输入logstash -f  +配置文件名。而后按回车。

若是这里有success,就配置文件没有问题,若是现实unsuccess,则会把配置文件的错误提示出来,根据提示修改配置就能够了。

等待全部数据跑完,能够去elasticsearch-head里面查看索引情况。

这边多说一句,若是是指定了maping(ES字段)的格式,是没法使用这个工具批量导入的。目前来讲我没找到比较好用的批量导入的工具。若是各位朋友有什么好用的,也能够在下面留言告诉我。我目前导入数据的方式仍是使用比较笨的,查一条提交一条,这个方法超级慢。可是能够保证不会去覆盖已定义的mapping。

4、查询控制相关度

这个功能在平常开发中也比较经常使用,主要做用就是查询某个字段,而后经过设置条件去将查询结果和设置条件的结果整合,获得一个最指望拿到的数据。好比淘宝搜索一个商品,搜出来的结果可能会加上销量,价格等条件,综合考虑去给出结果。我的以为ES在这方面作了不少的设定,用户能够经过设置不一样的算法和参数,去拿到本身最指望获得的值。先附上ES的开发文档地址,弄不懂的同窗能够去这里查看一下,注意换到本身对应的版本,不一样版本可能会有一些出入。

https://www.elastic.co/guide/cn/elasticsearch/guide/current/controlling-relevance.html

我在这边主要去讲一下function_score查询。这个查询算比较完整的控制了用户想要考虑的各个因素。这是弱弱的吐槽一句,ES这个文档弄得有点恼火,你看懂了ES的语句,若是想要得到javaAPI,又要花大量的时间去找对应版本的API。还不如把两个开发文档合在一块儿,将某个功能javaAPI的连接加在该功能介绍的最下面。这样能减轻不少负担。function_score对应的javaAPI地址。

https://www.elastic.co/guide/en/elasticsearch/client/java-api/5.2/java-compound-queries.html

package com.elasticsearch.demo;


import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import static org.elasticsearch.index.query.QueryBuilders.boostingQuery;
import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
import static org.elasticsearch.index.query.QueryBuilders.rangeQuery;
import static org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders.*;

import java.net.InetAddress;

/**
 * @Author: lk
 * @Date: 2018/10/3 0003 下午 3:22
 * @Description:
 */
@RestController
@RequestMapping("test2")
public class Test2 {
    private String host = "127.0.0.1";
    private int port = 9300;
    @GetMapping("test")
    public String test(String search) throws Exception {
        search = "0.46/0.71MM 20PCS Smooth Acoustic Guitar Pick Picks Plectrum Celluloid Electric";
        Settings settings = Settings.builder()
                .put("client.transport.sniff", true)
                .put("cluster.name", "market")
                .build();
        TransportClient client = new PreBuiltTransportClient(settings)
                .addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("127.0.0.1"), 9300))
                .addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("127.0.0.1"), 9301))
                .addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("127.0.0.1"), 9302));
        float a = (float) 1;
        float b = (float) 2;
        FunctionScoreQueryBuilder.FilterFunctionBuilder[] functions = {
                new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                        QueryBuilders.termQuery("sold", "0"), weightFactorFunction(a)
                ),
                new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                        rangeQuery("price").from("15").to("30"), weightFactorFunction(b)
                ),
        };
        SearchRequest searchRequest = new SearchRequest("market");
        searchRequest.types("item");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.functionScoreQuery(QueryBuilders.termQuery("description", search), functions));
        searchRequest.source(sourceBuilder);
        SearchResponse searchResponse = client.search(searchRequest).get();
        return "success";
    }
}

这里写了个小例子。首先我想去查询item中的description字段。而后设置了两个条件去影响查询结果。分别是sold的值为0,为它设置权重为1,price范围在15-30直接,为它设置权重为2。这两个字段的结果和查询的description结果想结合,最终获得的结果一点是越接近上面的条件,得到的分数越高,则排名越靠上。固然这里只是写了个简单的小例子,实际当中确定没有这么简单,仍是要多去读开发文档,而后按照实际的需求去选择不一样的计算方法。

5、父子关系 (parent-child)创建

首先说明为何要创建父子关系,由于在ES不能像日常的SQL同样经过设置主外键去查询其余type中的数据,因此必须创建父子关系(其实和主外键同样),才能同时查询到多个type的数据。

在实际的使用中,这个也是坑我好久的一个地方。由于不论是开发文档,仍是其余资料。大多数都只是写ES语句去进行查询,若是要放在SpringBoot中,使用java语句去操做,不多有这方面的资料。我踩了不少的坑,才慢慢地摸索了出来。下面是官方文档的介绍。

https://www.elastic.co/guide/cn/elasticsearch/guide/current/parent-child.html

首先,假如你要创建父子关系,须要加上@Parent这个注解。这个注解加在子元素的实体中的某个字段上,去对应父元素的数据。

package com.isale.market.entity.es;

import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.*;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
@Slf4j
@NoArgsConstructor
@AllArgsConstructor
@Data
@Document(indexName = "listing",type = "salesListing", shards = 3,replicas = 0, refreshInterval = "-1")
public class SalesListingES  implements Serializable {
    @Id
    private Integer id;

    @Field(type = FieldType.Text, searchAnalyzer="ik_max_word", analyzer="ik_max_word")
    private String enTitle;

    @Field(type = FieldType.Text, searchAnalyzer="ik_max_word", analyzer="ik_max_word")
    private String cnTitle;

    @Field(type = FieldType.Integer,store=true)
    private int categoryId;

    @Field(type = FieldType.Text, searchAnalyzer="ik_max_word", analyzer="ik_max_word")
    private String description;


}

父元素实体。

package com.isale.market.entity.es;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.*;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;

/**
 * @Author: lk
 * @Date: 2018/10/18 0018 下午 4:33
 * @Description:
 */
@Slf4j
@NoArgsConstructor
@AllArgsConstructor
@Data
@Document(indexName = "listing",type = "salesVariationInfo", shards = 3,replicas = 0, refreshInterval = "-1")
public class SalesVariationGroupListingInfoES  implements Serializable {
    @Id
    private Integer id;
    @Field(type = FieldType.Text,store=true)
    @Parent(type="salesListing")
    private String listingId;


}

子元素实体(在这里加上@Parent注解)

注意:

  1. 注解必须加载子元素的某个字段上,且该字段的类型为String,@Parent注解中type对应的值为父元素的type值。
  2. 父子元素必须在同一个index下,不然关系不会被创建。
  3. 若是使用SpringBoot生成index,这里可能会出错。大体意思是没法创建父子关系。缘由是父子关系必须在同一个请求中被建立,若是是SpringBoot生成index,是不一样的请求。因此index创建失败,会报错。解决这个问题的方法是在Sense插件中首先去创建他们的父子关系,而后再启动SpringBoot导入各个字段。
    PUT /listing
    {
      "mappings": {
        "salesListing": {},
        "salesVariationInfo": {
          "_parent": {
            "type": "salesListing" 
          }
        }
      }
    }

    创建两个索引,salesVariationInfo的parent就是salesListing。而后再启动SpringBoot,将不一样字段导入。

索引关系能够去elasticsearch-head插件中查看。点击索引信息,若是出啊先_parent,type属性,则证实关系建设成功。

建好关系以后,能够去查询,这里查询有两种方式,根据父亲的信息查询儿子的信息,根据儿子的信息查询父亲的信息。

package com.isale.market.service.impl;

import com.isale.market.entity.es.SalesListingES;
import com.isale.market.entity.es.SalesVariationGroupListingInfoES;
import com.isale.market.repository.es.SalesListingESRepository;
import com.isale.market.repository.es.SalesVariationGroupListingInfoESRepository;
import com.isale.market.service.SearchService;

import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.join.query.JoinQueryBuilders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpSession;

/**
 * @Author: lk
 * @Date: 2018/10/9 0009 下午 4:22
 * @Description:
 */
@Service
public class SearchServiceImpl implements SearchService {
    @Autowired
    SalesListingESRepository salesListingESRepository;
    @Autowired
    SalesVariationGroupListingInfoESRepository salesVariationGroupListingInfoESRepository;
    @Autowired
    HttpSession sesession;


    @Override
    public Page<SalesListingES> searchByTitle(int pageIndex ,int pageSize) {
        String title = (String) sesession.getAttribute("title");
        QueryBuilder queryBuilder = JoinQueryBuilders.hasChildQuery("salesVariationInfo", QueryBuilders.matchQuery("searchTitle", title), ScoreMode.Max);
        Pageable pageable = PageRequest.of(pageIndex, pageSize);
        SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(queryBuilder).withPageable(pageable).build();
        searchQuery.addIndices("listing");
        searchQuery.addTypes("salesListing");
        Page<SalesListingES> list = salesListingESRepository.search(searchQuery);
        long pageCount = list.getTotalPages();
        long pageSize1 = list.getTotalElements();
        return list;
    }
}

上面简单的写了个小例子,根据儿子的索引信息得出父亲的数据。这里有个ScoreMode参数的设置。ES颇有意思,若是这个参数设置为NONE的话,得出的结果是无序的。也就是说首先查到的儿子的信息是按照相关度从高到低去排列,而查询到的父亲的信息确实无序排列了。我就是这个坑饶了很久,最后将两块的数据同时查出来才发现相关度没有被排序。因此,若是想要父亲的结果也排序,建议加上这个字段。

假如须要根据父亲查儿子,主须要将上面的上方改为hasParent,而后将对应的索引名称修改一下就能够了。

这里须要注意的是hasChildQuery()和hasParent()两个方法,在ES5.5以前,是在QueryBuilders类中,在ES5.5以后,将这两个方法集成在了JoinQueryBuilders这个类中。因此,根据版本去选择不一样的方法。

上面还用到了JPA中的分页插件,这个我的感受用起来挺方便的,只须要给到对应的页数和每页的数据量就能自动返回查询后的页数,具体使用就很少说了。

总结的知识点大概就是这些,不少地方比较简单的我就没贴代码,可能没说清楚,看不懂的老哥能够在下面留言。