天天看点

OpenSearch流式SDK分享背景:改进思路:接入步骤:后续规划:

背景:

在使用阿里云OpenSearch过程中发现官方SDK存在以下问题:

  1. 搜索条件设置需要拼接字符串来实现,不太友好;
  2. 搜索返回结果是字符串类型,需要业务自己解析;
  3. 应用结构schema只能hard coding;
  4. 有一定的学习成本,比如某种类型的索引查询语法有哪些限制。

改进思路:

封装一个starter,具有以下功能:

  1. 自动根据OpenSearch的应用结构schema生成代码(查询条件类和返回结果类);
  2. 搜索条件的封装使用流式API的方式,隐藏复杂性;用API引导用户对语法有差异的索引或属性字段设置查询或过滤条件。

接入步骤:

引入SDK包:

<dependency>
  <groupId>com.github.zougeren</groupId>
  <artifactId>fluent-search-processor</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>           

工程本地新建一个工具类,使用如下方式生成代码:

package com.aliyun.gts.xaccount.user.util;

import com.aliyun.opensearch.sdk.generated.commons.OpenSearchClientException;
import com.aliyun.opensearch.sdk.generated.commons.OpenSearchException;
import com.google.common.collect.Lists;

import java.io.IOException;

/**
 * @author zouge
 */
public class FluentSearchGenerator {

    public static void main(String[] args) throws OpenSearchClientException, OpenSearchException, IOException, ClassNotFoundException {

        com.github.zougeren.fluent.search.processor.util.FluentSearchGenerator.builder()
                //配置opensearch公网接入点
            	  .host("http://opensearch-cn-zhangjiakou.aliyuncs.com")
                //填入ak和sk,注意只是临时填写,生成完代码之后一定删掉,不要提交到git里!
                .accessKey("")
                .accessSecret("")
                //设置要生成哪些应用的代码,可以配置完整的应用名称,也可以配置*通配符
                //比如目前我这边应用命名规则是库名_表名,则可以配置"dayu_xaccount_*"来生成账号库涉及的所有应用代码
                .appNames(Lists.newArrayList("dayu_xaccount_*"))
                //生成代码放在哪个包
                .basePkg("com.aliyun.gts.xaccount.user.search")
                //生成代码放在哪个模块
                .srcDir("xuser-platform/user-core/src/main/java")
                .build()
                .generate();
    }
}           

运行之后,每个应用会生成两个类:

XxxSearchParam: 这个是搜索条件的封装;

XxxSearchResult: 这个是搜索返回结果的封装;

其中Xxx表示OpenSearch控制台中的应用名称。

应用增加配置:

fluent:
  search:
    #目前仅支持openSearch,后续支持es等
    engine: openSearch
    #配置ak和sk
    openSearchAccessKey: 
    openSearchAccessSecret: 
    #openSearch接入点
    openSearchHost: http://opensearch-cn-shanghai.aliyuncs.com           

开始搜索:

@Autowired
private OpenSearchClient openSearchClient;

public void test(){
	//此处省略了param的详细构造过程,可参考下方示例
  ZougetestSearchParam param = new ZougetestSearchParam();
  Page<ZougetestSearchResult> search = openSearchClient.search(param);
  int pageNo = search.getPageNo();
  int pageSize = search.getPageSize();
  long total = search.getTotal();
  List<ZougetestSearchResult> data = search.getData();
}           

XxxSearchParam详细语法示例:

我建了一个测试应用,应用名称:zougetest(starter本身支持多表关联,这里为了测试方便我只建了主表),包含了OpenSearch目前支持的所有类型:

OpenSearch流式SDK分享背景:改进思路:接入步骤:后续规划:

所有可建立索引的字段均建立索引,some_int_field当使用关键字或数值分析时,query子句语法会不同,所以我建了两个索引方便测试:

OpenSearch流式SDK分享背景:改进思路:接入步骤:后续规划:

所有可设置为属性字段的均设置为属性字段:

OpenSearch流式SDK分享背景:改进思路:接入步骤:后续规划:

示例代码:

ZougetestSearchParam param = new ZougetestSearchParam()
        //查询所有字段
        .selectAll()
        //或者指定要查询哪些字段,以end结束
        .select.someIntField().someFloatField().end

        //query子句,每个索引字段都有 andXxx() orXxx() andNotXxx() rankXxx() 四个方法,表示四种连接符
        //主要分为四类:关键字分析、地理位置分析、数值分析、其他分析
        .where

        //关键字分析
        //.rankId()
        //.orSomeIntArrayField()
        //.andNotSomeLiteralArrayField()
        .andSomeLiteralField()
        //AND some_literal_field:'hh'    每个方法都有一个含有Predicate的重载版本,后续示例不再赘述
        .equal("hh", Objects::nonNull)
        //AND some_literal_field:'hehe'|'haha'   当前字段继续增加条件,则连接符保持和上一步的一致
        .in(Lists.newArrayList("hehe", "haha"))
        //以end结束当前索引字段的条件
        .end

        //地理位置分析
        .orSomeGeoPointField()
        //OR some_geo_point_field:'point(50 51)'
        .point(50d, 51d)
        //OR some_geo_point_field:'circle(50 51,1000)'
        .circle(50d, 51d, 1000)
        //OR some_geo_point_field:'rectangle(51 52,53 54)'
        .rectangle(51d, 52d, 53d, 54d)
        .end

        //其他分析
        //.rankSomeTextField()
        .rankSomeShortTextField()
        //RANK some_short_text_field:'haha'
        .like("haha")
        //RANK some_short_text_field:'heh'|'haha'
        .likes(Lists.newArrayList("heh", "haha"))
        //RANK some_short_text_field:"hehe"
        .phaseLike("hehe")
        //RANK some_short_text_field:"heh"|"haha"
        .phaseLikes(Lists.newArrayList("heh", "haha"))
        .end

        //数值分析
        //.andSomeIntField()
        .andSomeTimestampField()
        //AND some_timestamp_field:[-9223372036854775808,123]
        .le(123L)
        //AND some_timestamp_field:[123,9223372036854775807]
        .ge(123L)
        //AND some_timestamp_field:[-9223372036854775808,123)
        .lt(123L)
        //AND some_timestamp_field:(123,9223372036854775807]
        .gt(123L)
        //AND some_timestamp_field:[12,23]
        .rangeAllInclusive(12L, 23L)
        //AND some_timestamp_field:(12,23)
        .rangeAllExclusive(12L, 23L)
        //AND some_timestamp_field:(12,23]
        .rangeLeftExclusiveRightInclusive(12L, 23L)
        //AND some_timestamp_field:[12,23)
        .rangeLeftInclusiveRightExclusive(12L, 23L)
        .end

        //实际使用场景中,四个操作符的优先级可能大家不一定记得清楚,会使用()定义优先级,所以这里有额外的四个方法来生成带括号的嵌套查询
        //and() or() andNot() rank()
        .and(query -> query.andSomeIntField().gt(123).end
                .orId().equal(123).end)

        //结束query子句,生成queryStr时.where后紧跟的第一个条件和所有括号中的第一个条件的连接符会被trim掉
        .end

        //filter子句,每个属性字段都有 andXxx() orXxx() 两个方法,表示两种连接符
        //主要分为四类:数字、数字数组、字符串、字符串数组
        .filter

        //数字
        //.andSomeTimestampField()
        //.andSomeIntField()
        //.andSomeDoubleField()
        .andSomeFloatField()
        //AND some_float_field>123
        .gt(123)
        //AND some_float_field>=123
        .ge(123)
        //AND some_float_field<123
        .lt(123)
        //AND some_float_field<=123
        .le(123)
        //AND some_float_field=123
        .equal(123)
        //AND in(some_float_field,"123|345")
        .in(Lists.newArrayList(123, 345))
        //AND some_float_field!=123
        .notEqual(123)
        //AND notin(some_float_field,"123|345")
        .notIn(Lists.newArrayList(123, 345))
        .end

        //数字数组
        //.andSomeGeoPointField()
        //.andSomeDoubleArrayField()
        //.andSomeIntArrayField()
        .orSomeFloatArrayField()
        //OR some_float_array_field=123
        .equal(123)
        //OR some_float_array_field!=123
        .notEqual(123)
        //OR (some_float_array_field=123 OR some_float_array_field=345)
        .in(Lists.newArrayList(123, 345))
        //OR (some_float_array_field!=123 AND some_float_array_field!=345)
        .notIn(Lists.newArrayList(123, 345))
        .end

        //字符串
        .andSomeLiteralField()
        //AND some_literal_field="haha"
        .equal("haha")
        //AND some_literal_field!="haha"
        .notEqual("hehe")
        //AND in(some_literal_field,"heh|jaj")
        .in(Lists.newArrayList("heh", "jaj"))
        //AND notin(some_literal_field,"e|a")
        .notIn(Lists.newArrayList("e", "a"))
        .end

        //字符串数组
        .andSomeLiteralArrayField()
        //AND some_literal_array_field="haha"
        .equal("haha")
        //AND some_literal_array_field!="hah"
        .notEqual("hah")
        //AND (some_literal_array_field="hah" OR some_literal_array_field="heh")
        .in(Lists.newArrayList("hah", "heh"))
        //AND (some_literal_array_field!="a" AND some_literal_array_field!="e")
        .notIn(Lists.newArrayList("a", "e"))
        .end

        //同样支持嵌套查询 and() or()
        .and(filter -> filter.andSomeFloatField().le(123).end
                .orSomeDoubleField().gt(123).end)

        //结束filter子句,生成filterStr时.filter后紧跟的第一个条件和所有括号中的第一个条件的连接符会被trim掉
        .end

        //排序,支持自定义列名
        .orderBy("some_float_field").desc().end
        //或者根据具体字段排序
        .orderBy.someDoubleArrayField().asc().someDoubleField().desc().end

        //分页参数
        .limit(1, 10);           

目前还有一些高级的查询没有封装好,如果仅仅想使用结果自动转javaBean,而查询条件仍然使用原生的SDK,则可以使用第二个方法:

com.github.zougeren.fluent.search.processor.client.OpenSearchClient#search(com.aliyun.opensearch.sdk.generated.search.SearchParams, java.lang.Class<R>)

第二个参数传入对应的XxxSearchResult

后续规划:

其他高级用法(aggregate、distinct、scroll查询、底纹、热搜、下拉选)的封装还在开发中,欢迎交流~