Spring Cloud | ElasticSearch(es)的使用

一、簡述

ES(ElasticSearch)是一款分佈式全文檢索框架,每個字段可以被索引與搜索;以勝任上百個服務節點的擴展,並支持PB級別的結構化或者非結構化數據;底層基於基於Lucene實現。

ES與傳統數據的區別:

1、結構名稱不同:一個ES集羣可以包含多個索引(數據庫),每個索引又包含了很多類型(表),類型中包含了很多文檔(行),每個文檔使用 JSON 格式存儲數據,包含了很多字段(列)。

關係型數據庫

數據庫

ElasticSearch

索引

類型

文檔

字段

2、ES分佈式搜索,傳統數據庫遍歷式搜索

3、ES採用倒排索引,傳統數據庫採用B+樹索引

4、ES沒有用戶驗證和權限控制

5、ES沒有事務的概念,不支持回滾,誤刪不能恢復

......

本章案例源碼:

源碼:https://github.com/liujun19921020/SpringCloudDemo/blob/master/ProjectDemo/企業SpringCloud架構-xxljob-redis-elasticsearch

或 :鏈接:https://pan.baidu.com/s/1ooqaIRSeX6naOZ51aBWNHA  提取碼:cwsw

 

這裏是整了一個架構集合案例,該文主要源碼爲sc-elasticsearch-demo項目。

 

二、目標

通過接口傳遞list對象,將集合數據插入ES索引名/類型名爲”order_detail”對象中;然後再通過接口按條件查詢。

(下面講述通過插入數據時自動創建ES索引及mapping方式,也可通過sql+conf文件方式獲取數據源創建ES相關,詳見源碼目錄)

 

三、配置


項目中我們採用SearchGurageSSL認證方式配置,證書配置生成的可查看《SearchGuard證書配置》《searchguard配置》《SearchGuard客戶端連接》《SearchGuard集羣版》等相關文檔。

 

先引入ES配置

1、pom.xml中引入依賴

<!-- es插件 -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-elasticsearch</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>5.6.14</version>
</dependency>

<!--SearchGurageSSL認證-->
<dependency>
    <groupId>com.floragunn</groupId>
    <artifactId>search-guard-ssl</artifactId>
    <version>5.6.14-23</version>
</dependency>

2、在application.properties配置文件中添加配置變量,指明ssl證書存放位置、ES的IP端口賬號密碼

#----------------------------------elasticsearch配置---------------------------------------------
# resources.base_location=D:\\elasticsearch\\
resources.base_location=classpath:
resources.locations1=${resources.base_location}ssl/spock.key
resources.locations2=${resources.base_location}ssl/spock.pem
resources.locations3=${resources.base_location}ssl/root-ca.pem

elasticsearch.number=1
elasticsearch.host1=192.168.71.246
elasticsearch.host2=192.168.71.246
elasticsearch.host3=192.168.71.246
elasticsearch.port=9300

elasticsearch.cluster-name=ebuy-cloud-cluster
elasticsearch.ssl.transport.pemkey.password=3QgfFoYd8Ken

 

3、編寫ES的ElasticConfig.java文件,通過上述的配置資料,創建TransportClient,初始化Bean中(@EnableElasticsearchRepositories註解後配置的包路徑即掃描可使用ES對象地址)

/**
 * es配置
 */
@Configuration
@EnableElasticsearchRepositories(basePackages = "com.lj.scelasticsearchdemo.service")
public class ElasticConfig {
    @Value("${elasticsearch.number}")
    private Integer number;
    @Value("${elasticsearch.host1}")
    private String host1;
    @Value("${elasticsearch.host2}")
    private String host2;
    @Value("${elasticsearch.host3}")
    private String host3;
    @Value("${elasticsearch.port}")
    private Integer port;
    @Value("${elasticsearch.cluster-name}")
    private String clusterName;
    @Value("${elasticsearch.ssl.transport.pemkey.password}")
    private String password;
    @Value("${resources.locations1}")
    private String resourcesLocations1;
    @Value("${resources.locations2}")
    private String resourcesLocations2;
    @Value("${resources.locations3}")
    private String resourcesLocations3;
    /**
     * 注入的ElasticSearch實例
     */
    @Bean(name = "esClient")
    public TransportClient getclient()throws Exception {

        Settings settings = Settings.builder()
                // 本地開發使用
                .put(SSLConfigConstants.SEARCHGUARD_SSL_TRANSPORT_PEMKEY_FILEPATH, ResourceUtils.getFile(resourcesLocations1))
                .put(SSLConfigConstants.SEARCHGUARD_SSL_TRANSPORT_PEMCERT_FILEPATH, ResourceUtils.getFile(resourcesLocations2))
                .put(SSLConfigConstants.SEARCHGUARD_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH, ResourceUtils.getFile(resourcesLocations3))
                .put(SSLConfigConstants.SEARCHGUARD_SSL_TRANSPORT_PEMKEY_PASSWORD, password)
                .put("cluster.name",clusterName)
                .build();
        TransportClient client = null;
        if(1==number){
            client = new PreBuiltTransportClient(settings, SearchGuardSSLPlugin.class)
                    .addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName(host1), port));
        }else if(3==number){
            client = new PreBuiltTransportClient(settings, SearchGuardSSLPlugin.class)
                    .addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName(host1), port))
                    .addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName(host2), port))
                    .addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName(host3), port));
        }else{
            System.out.println("輸入節點數量異常!");
            return client;
        }
        // 獲取連接
//        client.admin().cluster().nodesInfo(new NodesInfoRequest()).actionGet();

        return client;
    }
}

4、ES主要配置基本上已完成,我們可以寫個繼承AbstractResultMapper的類,作爲查詢ES後的自定義結果映射類

/**
 * 類名稱:ExtResultMapper
 * 類描述:自定義結果映射類
 */
@Component
public class ExtResultMapper extends AbstractResultMapper {

    private static final Logger logger = LoggerFactory.getLogger(ExtResultMapper.class);

    private MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;

    public ExtResultMapper() {
        super(new DefaultEntityMapper());
    }

    public ExtResultMapper(MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext) {
        super(new DefaultEntityMapper());
        this.mappingContext = mappingContext;
    }

    public ExtResultMapper(EntityMapper entityMapper) {
        super(entityMapper);
    }

    public ExtResultMapper(
            MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext,
            EntityMapper entityMapper) {
        super(entityMapper);
        this.mappingContext = mappingContext;
    }

    @Override
    public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
        long totalHits = response.getHits().getTotalHits();
        List<T> results = new ArrayList<>();
        for (SearchHit hit : response.getHits()) {
            if (hit != null) {
                T result = null;
                if (StringUtils.hasText(hit.getSourceAsString())) {
                    result = JSONObject.parseObject(hit.getSourceAsString(),clazz);
//                    result = mapEntity(hit.sourceAsString(), clazz);
                } else {
                    result = JSONObject.parseObject(hit.getSourceAsString(),clazz);
//                    result = mapEntity(hit.getFields().values(), clazz);
                }
                setPersistentEntityId(result, hit.getId(), clazz);
                setPersistentEntityVersion(result, hit.getVersion(), clazz);
                populateScriptFields(result, hit);
                if (!org.apache.commons.lang.StringUtils.isBlank(hit.getId())){
                    //設置文檔id
                    JSONObject jsonObject = JSONObject.parseObject(JSONObject.toJSONString(result));
                    jsonObject.put("esId",hit.getId());
                    result = jsonObject.toJavaObject(clazz);
                }


                // 高亮查詢
                populateHighLightedFields(result, hit.getHighlightFields());
                results.add(result);
            }
        }

        return new AggregatedPageImpl<T>(results, pageable, totalHits, response.getAggregations(), response.getScrollId());
    }

    private <T>  void populateHighLightedFields(T result, Map<String, HighlightField> highlightFields) {
        for (HighlightField field : highlightFields.values()) {
            try {
                PropertyUtils.setProperty(result, field.getName(), concat(field.fragments()));
            } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
                throw new ElasticsearchException("failed to set highlighted value for field: " + field.getName()
                        + " with value: " + Arrays.toString(field.getFragments()), e);
            }
        }
    }

    private String concat(Text[] texts) {
        StringBuffer sb = new StringBuffer();
        for (Text text : texts) {
            sb.append(text.toString());
        }
        return sb.toString();
    }

    private <T> void populateScriptFields(T result, SearchHit hit) {
        if (hit.getFields() != null && !hit.getFields().isEmpty() && result != null) {
            for (Field field : result.getClass().getDeclaredFields()) {
                ScriptedField scriptedField = field.getAnnotation(ScriptedField.class);
                if (scriptedField != null) {
                    String name = scriptedField.name().isEmpty() ? field.getName() : scriptedField.name();
                    SearchHitField searchHitField = hit.getFields().get(name);
                    if (searchHitField != null) {
                        field.setAccessible(true);
                        try {
                            field.set(result, searchHitField.getValue());
                        } catch (IllegalArgumentException e) {
                            throw new ElasticsearchException("failed to set scripted field: " + name + " with value: "
                                    + searchHitField.getValue(), e);
                        } catch (IllegalAccessException e) {
                            throw new ElasticsearchException("failed to access scripted field: " + name, e);
                        }
                    }
                }
            }
        }
    }

    private <T> T mapEntity(Collection<SearchHitField> values, Class<T> clazz) {
        return mapEntity(buildJSONFromFields(values), clazz);
    }

    private String buildJSONFromFields(Collection<SearchHitField> values) {
        JsonFactory nodeFactory = new JsonFactory();
        try {
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            JsonGenerator generator = nodeFactory.createGenerator(stream, JsonEncoding.UTF8);
            generator.writeStartObject();
            for (SearchHitField value : values) {
                if (value.getValues().size() > 1) {
                    generator.writeArrayFieldStart(value.getName());
                    for (Object val : value.getValues()) {
                        generator.writeObject(val);
                    }
                    generator.writeEndArray();
                } else {
                    generator.writeObjectField(value.getName(), value.getValue());
                }
            }
            generator.writeEndObject();
            generator.flush();
            return new String(stream.toByteArray(), Charset.forName("UTF-8"));
        } catch (IOException e) {
            return null;
        }
    }

    @Override
    public <T> T mapResult(GetResponse response, Class<T> clazz) {
        T result = mapEntity(response.getSourceAsString(), clazz);
        if (result != null) {
            setPersistentEntityId(result, response.getId(), clazz);
            setPersistentEntityVersion(result, response.getVersion(), clazz);
        }
        return result;
    }

    @Override
    public <T> LinkedList<T> mapResults(MultiGetResponse responses, Class<T> clazz) {
        LinkedList<T> list = new LinkedList<>();
        for (MultiGetItemResponse response : responses.getResponses()) {
            if (!response.isFailed() && response.getResponse().isExists()) {
                T result = mapEntity(response.getResponse().getSourceAsString(), clazz);
                setPersistentEntityId(result, response.getResponse().getId(), clazz);
                setPersistentEntityVersion(result, response.getResponse().getVersion(), clazz);
                list.add(result);
            }
        }
        return list;
    }

    private <T> void setPersistentEntityId(T result, String id, Class<T> clazz) {

        if (mappingContext != null && clazz.isAnnotationPresent(Document.class)) {
            ElasticsearchPersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(clazz);
            ElasticsearchPersistentProperty idProperty = persistentEntity.getIdProperty();

            // Only deal with String because ES generated Ids are strings !
            if (idProperty != null && idProperty.getType().isAssignableFrom(String.class)) {
                persistentEntity.getPropertyAccessor(result).setProperty(idProperty, id);
            }
        }
    }

    private <T> void setPersistentEntityVersion(T result, long version, Class<T> clazz) {
        if (mappingContext != null && clazz.isAnnotationPresent(Document.class)) {

            ElasticsearchPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(clazz);
            ElasticsearchPersistentProperty versionProperty = persistentEntity.getVersionProperty();

            // Only deal with Long because ES versions are longs !
            if (versionProperty != null && versionProperty.getType().isAssignableFrom(Long.class)) {
                // check that a version was actually returned in the response, -1 would indicate that
                // a search didn't request the version ids in the response, which would be an issue
                Assert.isTrue(version != -1, "Version in response is -1");
                persistentEntity.getPropertyAccessor(result).setProperty(versionProperty, version);
            }
        }
    }
}

 

四、業務代碼

1、service實現插入\查詢,置於之前配置的掃描路徑下(查詢的類似sql語句爲見註釋說明)。

@Service
public class EsDemoServiceImpl implements EsDemoService {

    private final static String ES_INDEX = "order_detail";//ES索引名
    private final static String ES_TYPE = "order_detail";//ES類型名

    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;

    @Autowired
    private TransportClient esClient;

    @Resource
    private ExtResultMapper extResultMapper;

    /**
     * 添加es案例信息 (數據來源爲多庫時 可以通到java代碼實現數據插入;數據來源爲同一機器時,建議通過sql+conf的形式導入數據)
     * @author Liujun
     * @param pushMapList
     * @return
     */
    @Override
    public Integer saveEsDemoInfo(List<Map<String, Object>> pushMapList) {
        Integer saveNum = 0;//返回的插入多少條數據
        for (Map<String, Object> pushMap : pushMapList) {
            pushMap.put("id",System.currentTimeMillis()+new Random().nextInt(100));//id不能相等,因爲要多次插入一條數據,我們生成不一樣的id
            IndexResponse response = esClient.prepareIndex(ES_INDEX, ES_TYPE)
                    .setSource(pushMap)
                    .get();
            String _id = response.getId();
            if (StringUtils.isNotBlank(_id)){
                saveNum++;
            }
        }
        return saveNum;
    }


    /**
     * 查詢es案例信息 (假設條件爲類似sql:select order_id from sku=xx and site=xx and (order_status=1 or order_status = 2))
     * @param pageable
     * @param sku
     * @param site
     * @return
     */
    @Override
    public PageMap selectEsDemoInfo(Pageable pageable, String sku, String site) {
        //創建builder
        BoolQueryBuilder builder = QueryBuilders.boolQuery();
        //builder下有must、should以及mustNot 相當於sql中的and、or以及not

        //設置模糊搜索
        if (!StringUtils.isBlank(sku)) {
            builder.must(QueryBuilders.termQuery("sku.keyword", sku));
        }
        if (!StringUtils.isBlank(site)) {
            builder.must(QueryBuilders.termQuery("site.keyword", site));
        }

        BoolQueryBuilder purBuilder = QueryBuilders.boolQuery();
        purBuilder.should(QueryBuilders.matchPhraseQuery("order_status", "1"));
        purBuilder.should(QueryBuilders.matchPhraseQuery("order_status", "2"));
        builder.must(purBuilder);

        //構建查詢
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        //將搜索條件設置到構建中
        nativeSearchQueryBuilder.withQuery(builder);
        //將分頁設置到構建中
        nativeSearchQueryBuilder.withPageable(pageable);

        //生產NativeSearchQuery
        NativeSearchQuery query = nativeSearchQueryBuilder.build();
        //執行,返回結果的分頁
        Page<EsOrderDetail> resutlList = elasticsearchTemplate.queryForPage(query, EsOrderDetail.class, extResultMapper);
        return EsPageTool.getPageMap(resutlList, pageable);
    }

}

 

2、上面查詢的映射類,在實體類上表名ES索引、類型

/**
 * 查詢的es分片對應的index/type的字段
 */
@Document(indexName = "order_detail", type = "order_detail")
public class EsOrderDetail implements Serializable {
    private String order_id;
    private String order_status;
}

3、貼一下Controller接口

/**
 * ES 案例
 */
@RestController
@RequestMapping("/esDemo")
public class EsDemoController {

    private static Logger logger = LoggerFactory.getLogger(EsDemoController.class);

    @Autowired
    private EsDemoService esDemoService;

    /**
     * 查詢es案例信息 (假設條件爲類似sql:select order_id from sku=xx and site=xx and (order_status=1 or order_status = 2))
     * @param jsonObject
     * @author Liujun
     * @return
     */
    @PostMapping("/selectEsDemoInfo")
    public ResponseMsg selectEsDemoInfo(@RequestBody JSONObject jsonObject) {
        try {
            Integer page = jsonObject.getInteger("page");
            Integer size = jsonObject.getInteger("size");
            String sku = jsonObject.getString("sku");
            String site = jsonObject.getString("site");

            Pageable pageable = PageRequest.of(page == null ? 0 : page-1, size == null ? 50 : size);//分頁機制
            PageMap pageMap = esDemoService.selectEsDemoInfo(pageable, sku, site);
            return new ResponseMsg(Code.SUCCESS, pageMap, "根據條件查詢ES案例信息成功!");
        } catch (Exception e) {
            logger.error("EsDemoController.selectEsDemoInfo 異常", e);
            return new ResponseMsg(Code.FAIL, null, "根據條件查詢ES案例信息失敗!"+e.getMessage());
        }
    }

    /**
     * 添加es案例信息 (數據來源爲多庫時 可以通到java代碼實現數據插入;數據來源爲同一機器時,建議通過sql+conf的形式導入數據)
     * @param pushMapList
     * @author Liujun
     * @return
     */
    @PostMapping("/saveEsDemoInfo")
    public ResponseMsg saveEsDemoInfo(@RequestBody List<Map<String, Object>> pushMapList){
        try{
            Integer saveNum = esDemoService.saveEsDemoInfo(pushMapList);
            return new ResponseMsg(Code.SUCCESS, saveNum,"添加ES案例信息成功!");
        } catch (Exception e){
            logger.error("EsDemoController.saveEsDemoInfo 異常", e);
            return new ResponseMsg(Code.FAIL,null, "添加ES案例信息失敗!"+e.getMessage());
        }
    }
}

 

4、依次來看看我們的結果(這裏我們通過Kibana來操作ES)

執行之前,發現ES索引是不存在的

 

調用接口,插入list集合

 

此時再查看ES索引,發現已經生成了數據

 

調用查詢接口,查詢到分頁數據(ES查詢必帶分頁,若不寫分頁代碼,ES也會默認分頁10條數據)

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章