【组件开发实践】4倍提效的OpenSearch逆向工程框架

简介: 本框架对阿里云云产品——OpenSearch的Java SDK进一步封装,提供逆向工程能力:代码生成、隐藏原生API复杂性,使接入效率提高4倍以上。

背景:

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

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


改进思路:

开发一个逆向工程框架fluent-search,具有以下功能:

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


接入步骤:

1. 引入SDK包:

<dependency>
  <groupId>com.aliyun.gts.search</groupId>
  <artifactId>fluent-search-processor</artifactId>
  <version>2.0.0-SNAPSHOT</version>
</dependency>


2. 应用增加配置:

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


3. 代码生成

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

package com.aliyun.gts.fluent.search.test;
import com.aliyun.gts.fluent.search.processor.util.FluentSearchGenerator;
import com.aliyuncs.exceptions.ClientException;
import com.google.common.collect.Maps;
import java.io.IOException;
import java.util.HashMap;
/**
 * @author zouge
 */
public class TestGenerator {
    public static void main(String[] args) throws IOException, ClientException {
        //配置要生成代码的应用名称和应用id
        HashMap<String, Long> map = Maps.newHashMap();
        map.put("document_center_node", 150071013L);
        map.put("document_center_send_record", 150064670L);
        map.put("dayu_xaccount", 150053418L);
        map.put("dayu_xaccount_dayu_user", 150052991L);
        map.put("dayu_xaccount_dayu_account", 150058690L);
        FluentSearchGenerator build = FluentSearchGenerator.builder()
                //配置opensearch公网接入点
                .domain("opensearch.cn-shanghai.aliyuncs.com")
                //配置regionId
                .regionId("cn-shanghai")
                //填入ak和sk,注意只是临时填写,生成完代码之后一定删掉,不要提交到git里!
                .accessKey("")
                .accessSecret("")
                //应用
                .appNames(map)
                //生成代码放在哪个模块
                .srcDir("fluent-search-test/src/main/java")
                //生成代码放在哪个包
                .basePkg("com.aliyun.gts.fluent.search.test")
                .build();
        build.generate();
    }
}


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

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

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

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


4. 开始搜索:

来对比一个具体的账号搜索的例子:

使用openSearch原生SDK:

/**
 *
 * @param accountSearchParam 前端传入的搜索条件对象
 * @return
 */
@SneakyThrows
public ResultResponse<Page<SearchAccountResultDTO>> searchAccountList(AccountSearchParam accountSearchParam) {
    SearchParams searchParams = new SearchParams();//SearchParams是搜索的核心对象
    Config config = new Config();
    config.setSearchFormat(SearchFormat.FULLJSON);//设置返回格式为json
    config.setStart((accountSearchParam.getPageNum() - 1) * accountSearchParam.getPageSize());//分页offset
    config.setHits(accountSearchParam.getPageSize());//分页limit
    config.setAppNames(Lists.newArrayList("dayu_xaccount_dayu_account"));//应用名称
    searchParams.setConfig(config);
    //拼接query子句
    StringBuilder queryStr = new StringBuilder();
    queryStr.append("status:'1'");
    if (accountSearchParam.getAccountId() != null) {
        queryStr.append(" AND id:'").append(accountSearchParam.getAccountId()).append("'");
    }
    if (StringUtils.isNotBlank(accountSearchParam.getDisplayNameLike())) {
        queryStr.append(" AND display_name:'").append(accountSearchParam.getDisplayNameLike()).append("'");
    }
    if (StringUtils.isNotBlank(accountSearchParam.getPhoneLike())) {
        queryStr.append(" AND phone:'").append(accountSearchParam.getPhoneLike()).append("'");
    }
    if (!CollectionUtils.isEmpty(accountSearchParam.getUseStatus())) {
        String collect = accountSearchParam.getUseStatus().stream().map(Object::toString)
                .collect(Collectors.joining("'|'", "'", "'"));
        queryStr.append(" AND use_status:").append(collect);
    }
    if (accountSearchParam.getCreateTimeStart() != null) {
        queryStr.append(" AND gmt_create:[").append(accountSearchParam.getCreateTimeStart()).append(",4102416000000]");
    }
    if (accountSearchParam.getCreateTimeEnd() != null) {
        queryStr.append(" AND gmt_create:[0,").append(accountSearchParam.getCreateTimeEnd()).append("]");
    }
    if (accountSearchParam.getLastLoginDateStart() != null) {
        queryStr.append(" AND last_login_date:[").append(accountSearchParam.getLastLoginDateStart()).append(",4102416000000]");
    }
    if (accountSearchParam.getLastLoginDateEnd() != null) {
        queryStr.append(" AND last_login_date:[0,").append(accountSearchParam.getLastLoginDateEnd()).append("]");
    }
    //and (xxx or xxx)
    if (StringUtils.isNotBlank(accountSearchParam.getDisplayNameOrPhone())) {
        queryStr.append(" AND (display_name:'").append(accountSearchParam.getDisplayNameOrPhone())
                .append("' OR phone:'").append(accountSearchParam.getDisplayNameOrPhone()).append("')");
    }
    searchParams.setQuery(queryStr.toString());
    //设置filter子句
    StringBuilder filterStr = new StringBuilder();
    filterStr.append("status=1");
    if (accountSearchParam.getIsAuthenticated() != null) {
        filterStr.append(" AND is_authenticated=").append(accountSearchParam.getIsAuthenticated());
    }
    if (!CollectionUtils.isEmpty(accountSearchParam.getDeptIds())) {
        String collect = accountSearchParam.getDeptIds().stream()
                .collect(Collectors.joining("\" OR dept_id=\"", " AND (dept_id=\"", "\")"));
        filterStr.append(collect);
    }
    searchParams.setFilter(filterStr.toString());
    //设置sort子句
    Sort sort = new Sort();
    SortField sortField = new SortField();
    sortField.setField("gmt_create");
    sortField.setOrder(Order.DECREASE);
    sort.setSortFields(Lists.newArrayList(sortField));
    searchParams.setSort(sort);
    //开始搜索
    OpenSearch openSearch = new OpenSearch("AccessKey", "AccessSecret", "Host");
    com.aliyun.opensearch.OpenSearchClient openSearchClient = new com.aliyun.opensearch.OpenSearchClient(openSearch);
    SearcherClient searcherClient = new SearcherClient(openSearchClient);
    SearchParamsBuilder searchParamsBuilder = SearchParamsBuilder.create(searchParams);
    SearchResult searchResult = searcherClient.execute(searchParamsBuilder);
    //解析返回的String
    String data = searchResult.getResult();
    JSONObject jsonObject = JSONObject.parseObject(data);
    Object status = jsonObject.get("status");
    if (!"OK".equals(String.valueOf(status))) {
        return ResultResponse.errorResult(String.valueOf(jsonObject.get("errors")));
    }
    JSONObject result = (JSONObject) jsonObject.get("result");
    Integer total = (Integer) result.get("total");
    JSONArray items = (JSONArray) result.get("items");
    List<SearchAccountResultDTO> searchAccountResultDTOS = new ArrayList<>();
    for (Object item : items) {
        JSONObject obj = (JSONObject) item;
        JSONObject fields = (JSONObject) obj.get("fields");
        SearchAccountResultDTO searchAccountResultDTO = parseResult(fields);
        searchAccountResultDTOS.add(searchAccountResultDTO);
    }
    return ResultResponse.succResult(new Page<>(accountSearchParam.getPageNum(), accountSearchParam.getPageSize(), searchAccountResultDTOS, total));
}
/**
 * 从jsonObject获取DTO对象
 * @param jsonObject
 * @return
 */
private SearchAccountResultDTO parseResult(JSONObject jsonObject) {
    SearchAccountResultDTO searchAccountResultDTO = new SearchAccountResultDTO();
    searchAccountResultDTO.setId(jsonObject.getLong("id"));
    searchAccountResultDTO.setUserName(jsonObject.getString("display_name"));
    searchAccountResultDTO.setDisplayName(jsonObject.getString("display_name"));
    searchAccountResultDTO.setAccountId(jsonObject.getLong("id"));
    searchAccountResultDTO.setExternalId(jsonObject.getString("external_id"));
    searchAccountResultDTO.setPartnerId(jsonObject.getString("partner_id"));
    searchAccountResultDTO.setPartnerName(jsonObject.getString("name"));
    searchAccountResultDTO.setDeptName(jsonObject.getString("dept_name"));
    searchAccountResultDTO.setPhone(jsonObject.getString("phone"));
    searchAccountResultDTO.setUseStatus(jsonObject.getInteger("use_status"));
    searchAccountResultDTO.setIsAuthenticated(jsonObject.getInteger("is_authenticated"));
    searchAccountResultDTO.setGmtCreated(jsonObject.getLong("gmt_create"));
    searchAccountResultDTO.setLastLoginDate(jsonObject.getLong("last_login_date"));
    return searchAccountResultDTO;
}


可以看到,使用原生sdk是很不方便的,从搜索条件构造到结果解析,大段胶水代码,总代码行数120行左右。


使用fluent-search框架:

@Autowired
private com.github.zougeren.fluent.search.processor.client.OpenSearchClient openSearchClient;
/**
 * @param accountSearchParam 前端传入的搜索条件对象
 * @return
 */
public ResultResponse<Page<DayuXaccountDayuAccountSearchResult>> searchAccountListV2(AccountSearchParam accountSearchParam) {
    DayuXaccountDayuAccountSearchParam param = new DayuXaccountDayuAccountSearchParam()
            .selectAll() //查询所有字段,也可以指定查询哪些
            .where //开始构造query子句
            .andStatus().equal(1).end
            .andId().equal(accountSearchParam.getAccountId(), Objects::nonNull).end
            .andDisplayName().like(accountSearchParam.getDisplayNameLike(), o -> o != null && StringUtils.isNotBlank(o.toString())).end
            .andPhone().like(accountSearchParam.getPhoneLike(), o -> o != null && StringUtils.isNotBlank(o.toString())).end
            .andUseStatus().in(accountSearchParam.getUseStatus(), CollectionUtils::isNotEmpty).end
            .andGmtCreate().ge(accountSearchParam.getCreateTimeStart(), Objects::nonNull).le(accountSearchParam.getCreateTimeEnd(), Objects::nonNull).end
            .andLastLoginDate().ge(accountSearchParam.getLastLoginDateStart(), Objects::nonNull).le(accountSearchParam.getLastLoginDateEnd(), Objects::nonNull).end
            //and (xxx or xxx)
            .and(query -> query.andDisplayName().like(accountSearchParam.getDisplayNameOrPhone()).end.orPhone().like(accountSearchParam.getDisplayNameOrPhone()).end,
                              () -> StringUtils.isNotBlank(accountSearchParam.getDisplayNameOrPhone()))      
            .end
            .filter //构造filter子句
            .andIsAuthenticated().equal(accountSearchParam.getIsAuthenticated(), Objects::nonNull).end
            .andDeptId().in(accountSearchParam.getDeptIds(), CollectionUtils::isNotEmpty).end
            .end
            .orderBy.gmtCreate().desc().end //设置sort子句
            .limit((accountSearchParam.getPageNum() - 1) * accountSearchParam.getPageSize(), accountSearchParam.getPageSize()); //设置分页参数
    //开始搜索
    com.github.zougeren.fluent.search.processor.base.Page<DayuXaccountDayuAccountSearchResult> search = openSearchClient.search(param);
    List<DayuXaccountDayuAccountSearchResult> data = search.getData();
    return ResultResponse.succResult(new Page<>(accountSearchParam.getPageNum(), accountSearchParam.getPageSize(), data, search.getTotal()));
}


使用fluent-search框架,总代码行数只有30行左右,只有原生SDK的四分之一,流式API看起来也很清晰,而且隐藏了不同openSearch索引类型在构造查询子句时的语法差异细节,降低使用者的学习成本。


XxxSearchParam详细语法示例:

由于OpenSearch中数据类型较多,不同索引类型的查询语法也有很多差异,以上示例不能完整展示该框架API的所有细节,为了更详细的介绍框架语法,我建了一个OpenSearch测试应用,应用名称zougetest,包含了OpenSearch目前支持的所有类型:

70C3E255-69F7-47FB-96AB-0417479E30D1.png


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

2BF8C41A-FCBC-400F-927C-949203811856.png


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

94FA49F4-0071-4596-A0C9-133784528FE1.png


语法说明:

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 (some_literal_field="heh" OR some_literal_field="jaj")
        .in(Lists.newArrayList("heh", "jaj"))
        //等效于 AND (some_literal_field!="e" AND some_literal_field!="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);



后续规划:

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

相关实践学习
以电商场景为例搭建AI语义搜索应用
本实验旨在通过阿里云Elasticsearch结合阿里云搜索开发工作台AI模型服务,构建一个高效、精准的语义搜索系统,模拟电商场景,深入理解AI搜索技术原理并掌握其实现过程。
相关文章
|
自然语言处理 分布式计算 Java
基于OpenSearch向量检索版和智能问答版搭建企业专属对话搜索系统
本文将介绍如何使用OpenSearch向量检索版和智能问答版,搭建灵活自定义的企业专属对话搜索系统。
2216 1
|
人工智能 自然语言处理 搜索推荐
阿里云开放搜索重磅发布!云时代搜索业务的价值重构
【云栖大会】阿里云开放搜索重磅发布~
7031 0
阿里云开放搜索重磅发布!云时代搜索业务的价值重构
|
人工智能 文字识别 API
OpenSearch & AI搜索开放平台,实现0代码图片搜索!
本文主要介绍了如何利用阿里云的 OpenSearch 和 AI 搜索开放平台来构建一个无需编写代码就能完成的图片搜索功能。
517 12
|
Web App开发 自然语言处理 搜索推荐
基于OpenSearch搭建高质量商品搜索服务
本场景主要介绍开放搜索(OpenSearch)打造独有的电商行业垂直解决方案,模板内置电商查询分析、排序表达式及行业算法能力,沉浸式体验更高性能和效果的智能搜索服务,助力企业在线业务智能增长。
|
SQL 监控 搜索推荐
Elasticsearch 与 OpenSearch:开源搜索技术的演进与选择
Elasticsearch 与 OpenSearch:开源搜索技术的演进与选择
|
自然语言处理 搜索推荐 开发者
OpenSearch 智能问答实验室上线,支持免费体验对话式问答搜索
本文介绍OpenSearch 智能问答实验室上线的场景功能体验。
1428 0
|
存储 数据采集 人工智能
重磅再推 | 基于OpenSearch向量检索版+大模型,搭建对话式搜索
阿里云OpenSearch再推面向企业开发者的PaaS方案:基于OpenSearch向量检索版,为企业开发者提供性能表现优秀、性价比优异的向量检索服务,并提供与大模型结合脚本工具,用户可在使用能力可靠的向量检索服务的同时,自由选择文档切片方案、向量化模型、大语言模型。
16549 1
重磅再推 | 基于OpenSearch向量检索版+大模型,搭建对话式搜索
|
自然语言处理 搜索推荐 算法
阿里云OpenSearch重磅推出LLM问答式搜索产品,助力企业高效构建对话式搜索服务
OpenSearch推出LLM智能问答版,面向行业搜索场景,提供企业专属问答搜索服务,基于内置的LLM大模型提供问答能力,一站式快速搭建问答搜索系统。
12979 7
《开放搜索在智能化行业搜索和业务增长领域的应用实践》电子版地址
《开放搜索在智能化行业搜索和业务增长领域的应用实践》PDF
173 0
《开放搜索在智能化行业搜索和业务增长领域的应用实践》电子版地址
|
自然语言处理 达摩院 搜索推荐
【新版本】开放搜索开源兼容版,支持Elasticsearch做搜索召回引擎
9月15日阿里云开放搜索重磅发布【开源兼容版】,搜索召回环节同时支持阿里云自研Ha3引擎与阿里云Elasticsearch引擎,并提供多行业的搜索算法能力,助力企业高效实现搜索效果深度优化。
955 0
【新版本】开放搜索开源兼容版,支持Elasticsearch做搜索召回引擎