跳转到内容

1 功能概述

DS 提供了搜索模型功能,通过查询搜索引擎来提高查询性能。对于搜索模型,DS 支持将该搜索模型的业务数据从 DB 同步到 ES 建索引,并支持查询 ES 获取数据,从而提高业务模型数据搜索性能。 搜索模型查询是基于搜索引擎实现的, 限于搜索引擎的特性,目前查询为准实时的,数据时效为秒级延时

2 搜索模型定义

2.1 定义方式

模型定义的时候添加两个注解@Model 和@SearchModel。

搜索模型支持的字段类型: 支持 Trantor 中的普通类型以及 Json、Currency 等。

搜索模型不支持的字段类型: 关联字段和规则字段,Link 和 Relation 都不支持。

如下为搜索模型定义 demo

@EqualsAndHashCode(callSuper = true)
@SearchModel( desc = "交易订单搜索模型" ,name = "trade order search model",searchSyncEnabled = true)
@Model(desc = "交易订单搜索模型",name = "trade order search model")
public class TradeOrderSO extends BaseModel<Long> {
private static final long serialVersionUID = 7681321237625458519L;
/**
* 是否跨店铺订单
*/
@Field(name = "Is in group")
private Boolean isInGroup = Boolean.FALSE;
/**
* 使用积分数量
*/
@Field(name = "use point num")
private Long usedPointNum;
/**
* 履约版本
*/
@Field(name = "Fulfillment version", desc = "履约版本")
private Integer fulfillmentVersion = 0;
/**
* 交易数量
*/
@Field(name = "Trade quantity", desc = "交易数量")
private BigDecimal tradeQty;
/**
* 订单标题
*/
@TextMeta(length = 64)
@Field(name = "Trade order title")
private String tradeOrderTitle;
/**
* 支付状态
*/
@DictionaryMeta(value = PayStatusDict.class, length = 20)
@Field(name = "Pay status", desc = "支付状态", defaultValue = PayStatusDict.UNPAID)
private String payStatus;
/**
* 订单商品优惠总额
*/
@Field(name = "Sku discount amount")
private Currency skuDiscountAmt = new Currency(0);
@Field(name = "Write off time", type = FieldType.DateTime)
private Date certifiedAt;
/**
* 卖家备注
*/
@Field(name = "remark list", type = FieldType.Json)
private List<String> sellerRemarkList;
/**
* 组合商品关系
*/
@Field(name = "ownerOrderLineBO", type = FieldType.Json)
private TradeOrderLineBO ownerOrderLineBO;
/**
* 地理位置字段: 坐标点
*/
@Field(name = "GeoLocation")
private GeoLocation geoLocation;
/**
* 地理位置字段: 坐标区域
*/
@Field(name = "GeoArea")
private GeoArea geoArea;
}

2.2 生效方式

  • 模型发布后 DS 会根据模型定义自动到搜索引擎上创建索引,ds 生成索引的命名规则参见章节 3
  • 当模型定义发生变更时,DS 会自动执行全量同步,根据最新的模型定义来更新索引,并且将数据库中的数据同步到搜索引擎中(上述为默认方式,当业务数据量很大的时候,也可以选择采用最小粒度方式更新,详细参见 6.4 章节)
  • 如果发现搜索模型定义未生效,或者数据未同步,需要手动触发搜索模型全量同步,触发方式参见章节 6.1
  • 当模型数据发生变更时,DS 会自动将变化同步至搜索引擎,但是搜索内容会有所延迟,但大概率优化极限是接近 1 秒。 同步方式有两种,一种是全量同步,一种是增量同步。他们的实现方式和实现原理可查看章节 6

2.3 注意事项

*不支持规则字段和关联模型** 规则字段和关联字段在模型中定义方式如下(搜索模型不支持):

/**
* 交易合同单号
* 生成规则:TC+YYYYMMDD+6位数字流水
*/
@TextMeta(length = 32, rule = "STRING(TRC)+TIMES(yyyyMMdd)+INCRE(1,6,4,1)")
@Field(name = "Trade contract code")
private String tradeContractCode;
/**
* 交易合同模型
*/
@LinkMeta
@Field(name = "Trade contract")
private TradeContractBO tradeContract;

规则字段可以在 BO 模型里定义,然后同步给 SO。不需要再让 SO 重新生成一遍规则数据。 关联模型可以使用 JSON 字段来实现相关的功能,使用 JSON 字段冗余整个模型的数据,但是不建议这么操作。

因为如果在搜索模型里定义了很多 TO 对象作为 JSON 字段,由于 JSON 字段在创建索引采用的是动态映射,每一个二级字段都会作为一个字段映射到索引上。字段数就容易超过了 ES 的限制,ES 会报 Limit of total fields [1000] in index 的错。

搜索模型的设计是尽量保证轻量化,保证搜索模型的字段只被用来搜索,这样对搜索引擎的查询压力可控,数据同步和数据查询性能都有保障。

3 索引命名规则

3.1 索引命名规则

DS 会根据业务的 projectId 和 modelName 生成对应的索引名和索引别名,生成规则为: 索引名: idx_{projectId}_{modelName}_(UUID+索引创建时间) 索引别名: alias_{projectId}_{modelName}

比如: projectId: trantor6_slave; modelName: TradeOrderSO; 那么在 ES 上生成的索引名为: idx_trantor6_slave_trade_orderso_ff56155177a844d69b1ad86b720d5bac20211117163412 索引别名为: alias_trantor6_slave_trade_orderso

3.2 查看当前模型正在使用的索引和别名

当进行一次全量同步后,索引名会变,索引别名不变。查询时使用别名进行查询

方式一: 从索引名生成规则着手,索引生成规则后 16 位为生成时间,时间最大的就是最新的索引 方式二: 进入 DS 元数据库,查询 search_model 表 select esIndex,modelStatus,esAlias from search_model where modelName=‘xxx’ and isDeleted=0 查询结果中 modelStatus=running 时,代表当前正在运行的搜索模型, syncing 代表正在全量同步的 此时 esindex 的值就代表该模型在 es 集群上正在使用的索引

4 搜索模型 DML

搜索模型的增删改和普通模型的增删改采用是同一个 API,搜索模型的数据同样也会先插入到数据库中,做一个持久化的操作,然后由 DS 自动同步到 ES 上。

搜索模型查询 API 参考如下章节介绍

4.1 查询 API 使用介绍

Search API 如何使用以及返回结果信息介绍可参考文档: https://trantor-docs.app.terminus.io/v1.x/doc/ds-search/ds-search-interface

4.2 查询方式

查询搜索模型有三种方式:

查询方式是否推荐推荐理由
SearchQuery 方式推荐SearchQuery 参考的是 ES 原生的 DSL,比较符合 ES 规范,功能更加强大,理论上是可以实现 ES 支持的所有的功能
TSQL 方式其次TSQL 方式不支持 ES 的高级功能,(比如权重、地理位置、分词器等)
QModel 查询模型不推荐功能受限,性能较差

4.2.1 SearchQuery 方式

DS 主要推荐使用 SearchQuery 方式来查询搜索模型,下面简单介绍一下 SearchQuery 中常见的查询方法。

方法名功能使用方法查询条件分词性能
termsQuery精确匹配多个 value,相当于数据的 innew TermsQuery().field(fieldName).values(list)不支持很高
termQuery精确匹配单个 value,相当于数据的=new TermQuery().field(fieldName).value(value)不支持很高
containsQuery模糊匹配单个 value,相当于数据的 like ‘%value%‘containsQuery(fieldName, value)不支持在全局搜索时比较慢
startsWithQuery模糊匹配单个 value,相当于数据的 like ‘value%‘startsWithQuery(fieldName, value)不支持在全局搜索时速度比 containsQuery 快,比 termQuery 搜索慢
endsWithQuery模糊匹配单个 value,相当于数据的 like ‘%value’endsWithQuery(fieldName, value)不支持在全局搜索时比较慢
matchQuery类似精确匹配,如需使用分词查询,请使用 matchAnalyzeQuerynew MatchQuery(fieldName, text)不支持很高
existsQuery判断字段是否为空new TermQuery().field(fieldName).value(value)不支持很高
rangeQuery范围查询new RangeQuery(fieldName, from, to)不支持如果是数字类型,搜索性能很高。时间和字符字段性能很差,text 格式不建议使用范围查询
matchAnalyzeQuery对 fieldName 字段内容做分词搜索,查询内容也分词new MatchQuery(fieldName, text)支持,并可选择不同的分词器来分词,不设置则使用默认分词器 Stanred很高
termAnalyzeQuery精确查询new termAnalyzeQuery().field(fieldName).value(value)不支持对 fieldName 字段内容做分词搜索,查询内容不分词
termsAnalyzeQueryin 查询new termsAnalyzeQuery().field(fieldName).value(value)不支持分词版本 in 查询,values 中多个值都会进行分词,当有一个分词后包含查询内容,则认为命中

4.2.1.1 组合查询

真实业务场景中,当然不可能只有一种条件的搜索,一般是多个条件进行组合查询。组合查询中多个条件就会形成 and 和 or 的关系。下面介绍 SearchQuery 中如何支持 and 和 or 的功能。

实现组合查询前,需要先了解 bool 查询。bool 查询可以理解为 sql 中的()。相当于把多个查询条件组合的结果返回成一个 bool 值,然后再与其他条件组合查询。

  • must: 字段必须匹配这些条件才能被包含进来。
  • must_not: 字段必须不 匹配这些条件才能被包含进来。
  • should: 字段满足其中一种条件即可。
  • filter: 必须 匹配,但它以不评分、过滤模式来进行。功能和 must 相同,由于不加评分,而且有缓存,因此性能高于 must。

简单介绍一下 ES 的评分查询: must、should 都是带有评分的查询,评分查询不但要匹配字段还要计算每一个字段与此查询的相关程度,同时将这个相关程度分配给表示相关性的字段 _score,并且按照相关性对匹配到的文档进行排序。相关程度如何计算比如:查询‘防水涂料’ ,如果添加了分词,ES 会将分解为‘防水’和‘涂料’去搜索。那么包含‘防水涂料’就比只包含‘防水’和‘涂料’的字段评分要高。 ES 官方文档解释:

  • 查找与 full text search 这个词语最佳匹配的文档
  • 包含 run 这个词,也能匹配 runs 、 running 、 jog 或者 sprint
  • 包含 quick 、 brown 和 fox 这几个词 — 词之间离的越近,文档相关性越高
  • 标有 lucene 、 search 或者 java 标签 — 标签越多,相关性越高

4.2.1.2 如何选择查询与过滤

通常的规则是,使用查询(must)语句来进行全文搜索或者其它任何需要影响相关性得分的搜索。除此以外的情况都使用过滤(filter)。

4.2.1.3 must 和 should 的关系

  1. 当 must 存在的时候,should 中的条件是可有可无的,就是 must 条件满足就行,should 的一个都不用满足也可以。
  2. 当 must 不存在的时候,should 中的条件至少要满足一个。

如果想当 must 存在,又想让 should 的条件至少满足一个添加 minimum_should_match 这个参数。

"query": {
"bool": {
"must": [
{"term": {"color": "red"}}
],
"should": [
{"term": {"size": 33}},
{"term": {"size": 55}}
],
"minimum_should_match":1
}
}

这个搜索表示满足 color 等于 red 和 size 等于 33 或者 size 等于 55 的数据。minimum_should_match=1 表示满足一个 should 条件即可。 如果不想使用 minimum_should_match 参数,也可以使用 must》bool》must》should 的方式。

组合查询举例请参考: 复杂查询

4.2.1.4 must、filter、should 并存时注意事项

举例说明

@Test
public void select_bool_query_test11() {
SearchQuery searchQuery = new SearchQuery();
searchQuery.where(f -> f.bool()
.must(f.matchQuery("floatField2", 10000))
.should(f.matchQuery("intField1", 18))
.should(f.matchQuery("intField1", 12)))
.from(0).size(20).index("test_SearchModel1");
searchDSLAssertAllField(searchQuery, 6);
}
@Test
public void select_bool_query_test12() {
SearchQuery searchQuery = new SearchQuery();
searchQuery.where(f -> f.bool()
.filter(f.matchQuery("floatField2", 10000))
.should(f.matchQuery("intField1", 18))
.should(f.matchQuery("intField1", 12)))
.from(0).size(20).index("test_SearchModel1");
searchDSLAssertAllField(searchQuery, 6);
}

如上 select_bool_query_test11 所示,当 must 和 should 位于 bool 下同一级时,会导致 must 条件失效 如上 select_bool_query_test12 所示,当 filter 和 should 位于 bool 下同一级时,会导致 filter 条件失效

上述两种问题的解法是:最外层增加一个 must,然后再把原有条件依次增加到外层 must 条件下

4.2.2 TSQL/TSQL-DSL 方式

TSQL 支持两种方式手拼 TSQL 和 TSQL-DSL 方式,如果要使用 TSQL 方式的话,这里推荐使用 TSQL-DSL 方式。因为手拼方式存在 SQL 拼写痛苦、动态 SQL 实现困难、SQL 注入问题等问题。TSQL-DSL 方式在最新的 Trantor 版本已支持。

4.2.3 QModel 查询模型

QModel 查询模型使用方式可参考 4.2 QModel 查询模型

  • 查询模型是在编译期与 Trantor 模型 1:1 自动生成出来,无需手动编写。也因此不可修改或增加其中的字段或方法。
  • 查询模型会跟随 Trantor 一起被拓展字段,由机制保证。
  • 查询模型包含查询字段、查询参数和查询条件的信息,可以手动使用该对象组装 SQL,也可以使用 Trantor 提供的 API 根据查询模型对象值推断查询 SQL(使用方便,性能会稍差,按需使用)。

例如 TradeOrderSO 在程序编译后生成对应的查询模型如下:

@Generated("io.terminus.trantorframework")
public class QTradeOrderSO extends QModel<TradeOrderSO1> {
private QLongId id;
private QString bizAppKey;
private QInteger _version;
private QDate createdAt;
private QDate updatedAt;
private QUser createdBy;
private QUser updatedBy;
private QBoolean isInGroup;
private QLong usedPointNum;
private QInteger fulfillmentVersion;
private QBigDecimal tradeQty;
private QString tradeOrderTitle;
private QString payStatus;
private QType skuDiscountAmt;
private QDate certifiedAt;
private QType sellerRemarkList;
private QTradeOrderLineBO ownerOrderLineBO;
private QType geoLocation;
private QType geoArea;
public QLongId getId() {
return this.id;
}
public void setId(QLongId id) {
this.id = id;
__hasFields.add("id");
}
public QString getBizAppKey() {
return this.bizAppKey;
}
public void setBizAppKey(QString bizAppKey) {
this.bizAppKey = bizAppKey;
__hasFields.add("bizAppKey");
}
public QInteger get_version() {
return this._version;
}
public void set_version(QInteger _version) {
this._version = _version;
__hasFields.add("_version");
}
public QDate getCreatedAt() {
return this.createdAt;
}
public void setCreatedAt(QDate createdAt) {
this.createdAt = createdAt;
__hasFields.add("createdAt");
}
public QDate getUpdatedAt() {
return this.updatedAt;
}
public void setUpdatedAt(QDate updatedAt) {
this.updatedAt = updatedAt;
__hasFields.add("updatedAt");
}
public QUser getCreatedBy() {
return this.createdBy;
}
public void setCreatedBy(QUser createdBy) {
this.createdBy = createdBy;
__hasFields.add("createdBy");
}
public QUser getUpdatedBy() {
return this.updatedBy;
}
public void setUpdatedBy(QUser updatedBy) {
this.updatedBy = updatedBy;
__hasFields.add("updatedBy");
}
public QBoolean getIsInGroup() {
return this.isInGroup;
}
public void setIsInGroup(QBoolean isInGroup) {
this.isInGroup = isInGroup;
__hasFields.add("isInGroup");
}
public QLong getUsedPointNum() {
return this.usedPointNum;
}
public void setUsedPointNum(QLong usedPointNum) {
this.usedPointNum = usedPointNum;
__hasFields.add("usedPointNum");
}
public QInteger getFulfillmentVersion() {
return this.fulfillmentVersion;
}
public void setFulfillmentVersion(QInteger fulfillmentVersion) {
this.fulfillmentVersion = fulfillmentVersion;
__hasFields.add("fulfillmentVersion");
}
public QBigDecimal getTradeQty() {
return this.tradeQty;
}
public void setTradeQty(QBigDecimal tradeQty) {
this.tradeQty = tradeQty;
__hasFields.add("tradeQty");
}
public QString getTradeOrderTitle() {
return this.tradeOrderTitle;
}
public void setTradeOrderTitle(QString tradeOrderTitle) {
this.tradeOrderTitle = tradeOrderTitle;
__hasFields.add("tradeOrderTitle");
}
public QString getPayStatus() {
return this.payStatus;
}
public void setPayStatus(QString payStatus) {
this.payStatus = payStatus;
__hasFields.add("payStatus");
}
public QType getSkuDiscountAmt() {
return this.skuDiscountAmt;
}
public void setSkuDiscountAmt(QType skuDiscountAmt) {
this.skuDiscountAmt = skuDiscountAmt;
__hasFields.add("skuDiscountAmt");
}
public QDate getCertifiedAt() {
return this.certifiedAt;
}
public void setCertifiedAt(QDate certifiedAt) {
this.certifiedAt = certifiedAt;
__hasFields.add("certifiedAt");
}
public QType getSellerRemarkList() {
return this.sellerRemarkList;
}
public void setSellerRemarkList(QType sellerRemarkList) {
this.sellerRemarkList = sellerRemarkList;
__hasFields.add("sellerRemarkList");
}
public QTradeOrderLineBO getOwnerOrderLineBO() {
return this.ownerOrderLineBO;
}
public void setOwnerOrderLineBO(QTradeOrderLineBO ownerOrderLineBO) {
this.ownerOrderLineBO = ownerOrderLineBO;
__hasFields.add("ownerOrderLineBO");
}
public QType getGeoLocation() {
return this.geoLocation;
}
public void setGeoLocation(QType geoLocation) {
this.geoLocation = geoLocation;
__hasFields.add("geoLocation");
}
public QType getGeoArea() {
return this.geoArea;
}
public void setGeoArea(QType geoArea) {
this.geoArea = geoArea;
__hasFields.add("geoArea");
}
}

查询模型中的字段也会与源模型的字段一一对应,其中字段的基本类型为 QType,包装了可能的查询类型,比如单值,多值和区间。queryParams 字段包含了查询相关的参数:select、page、order。page 和 order 分别对应分页和排序,select 字段描述了本次需要查询的目标字段,在调用 Trantor 的查询模型 API 时,如果该字段值为 null,则自动会查询源模型所有字段,类似 select *。

4.2.3.1 运行原理

当调用 Search.paging(QModel<Model> queryModel) 时,会根据传入的查询模型中的 QType 字段推断查询条件,推断遵循以下几个原则:
值则根据 type 类型读取对应的字段获取:
* type = One -> value;
* type = Collection -> values;
* type = Range -> rangeValues(对象内部包含 start 和 end 的值);
1. 如果字段中的 type 是 One,即为单值时:
* 如果字段类型为文本:
* 当 fullMatch = false 时,推断为 like。
* 当 fullMatch = true 时,推断为 = 。
* 其余类型则为 =。
2. 如果字段中的 type 是 Collection,则推断为 in。
3. 如果字段中的 type 是 Range,则推断为 field \<= xxx and field >= xxx

4.2.3.2 使用示例

QTradeOrderSO qTradeOrderSO = new QTradeOrderSO();
// where id in (?) and tradeOrderTitle like '%?%'
QLongId ids = new QLongId(Arrays.asList(1L,2L,3L));
qTradeOrderSO.setId(ids);
// fullMatch 默认是false ,可以通过tradeOrderTitle.setFullMatch(Boolean.TRUE) 设置
QString tradeOrderTitle = new QString("title");
qTradeOrderSO.setTradeOrderTitle(tradeOrderTitle);
// usedPointNum <=10 and usedPointNum >=1
RangeValue<Integer> rangeValue = new RangeValue<>();
rangeValue.setEndValue(10);
rangeValue.setStartValue(1);
qTradeOrderSO.getUsedPointNum().setRangeValue(rangeValue);
QParams params = new QParams();
// select usedPointNum
Select select = new Select();
select.addField(QTradeOrderSO.usedPointNum_field);
params.setSelect(select);
// order by createdAt desc
params.setOrder(new Order(QTradeOrderSO.createdAt_field, false));
// limit 0,20
params.setPage(new Page(1, 20));
qItemBO.setQueryParams(params);

对应生成的 sql:

select usedPointNum from tradeOrderSO where id in (1,2,3) and tradeOrderTitle like '%title%' and usedPointNum <=10 and usedPointNum >=1 order by createdAt desc limit 0,20;

4.2.3.3 使用原则

  • 前端视图获取数据时,优先使用查询模型。
  • 如无必要,尽可能少使用查询模型,因为性能比较差,很多查询都不支持,比如 or 查询、List 字段的查询等。

详细原理以及特性请参考文档: https://trantor-docs.app.terminus.io/v1.x/doc/developer-guide/model/query-model#%E6%9F%A5%E8%AF%A2%E6%A8%A1%E5%9E%8B%EF%BC%88Query%20Model%EF%BC%89

4.3 搜索 API 示例用法

4.3.1 根据 id 搜索

4.3.1.1 SearchQuery 的实现方式

SearchQuery searchQuery = new SearchQuery();
searchQuery.where(f -> f.bool()
.must(f.idsQuery("1")));
// 不推荐写法
//Search.searchDSL(TradeOrderSO.class,new Page(0,20),searchQuery);
// 推荐写法,查询总数和查询结果一起返回
Search.searchDSLWithAggregation(TradeOrderSO.class,new Page(0,20),searchQuery);

4.3.1.2 手拼 TSQL 的实现方式

String select = "*";
String where = "id=1"
Search.paging(TradeOrderSO.class,select,where,new Page(0,20));

4.3.1.3 TSQL-DSL 的方式

List<SelectField<Object>> selectFields = new ArrayList<>();
selectFields.add(TSQL.field("*"));
Condition condition = TSQL.trueCondition();
condition = condition.and(TSQL.field(TradeOrderSO.id_field).eq(1));
Paging<Order> result = Search.paging(Order.class, selectFields, condition, null, new Page(0,20));

4.3.2 模糊查询

模糊查询是一次很消耗性能的查询,如果是大文本字段,请做分词后精确查询。 DS 提供三种函数做模糊查询:

  • containsQuery: 包含查询,类似 *queryValue*
  • startsWithQuery: 前缀查询,类似 queryValue*
  • endsWithQuery: 后缀查询,类似 *queryValue

4.3.2.1 包含查询

SearchQuery 的实现方式
SearchQuery searchQuery = new SearchQuery();
searchQuery.where(f -> f.bool()
.should(f.containsQuery(TradeOrderSO.tradeOrderTitle_field,"title"))
.should(f.matchQuery(TradeOrderSO.payStatus_field,"UNPAID")));
// 不推荐写法
//Search.searchDSL(TradeOrderSO.class,new Page(0,20),searchQuery);
// 推荐写法,查询总数和查询结果一起返回
Search.searchDSLWithAggregation(TradeOrderSO.class,new Page(0,20),searchQuery);
TSQL 的实现方式
String select = "*";
String where = "tradeOrderTitle like '%title%' or payStatus = 'UNPAID'"
Search.paging(TradeOrderSO.class,select,where,new Page(0,20));
TSQL-DSL 的方式
List<SelectField<Object>> selectFields = new ArrayList<>();
selectFields.add(TSQL.field("*"));
Condition condition = TSQL.trueCondition();
condition = condition.and(TSQL.field(TradeOrderSO.tradeOrderTitle_field).like("%title%"));
Paging<Order> result = Search.paging(Order.class, selectFields, condition, null, new Page(0,20));

4.3.2.2 前缀查询

SearchQuery 的实现方式
SearchQuery searchQuery = new SearchQuery();
searchQuery.where(f -> f.bool()
.should(f.startsWithQuery(TradeOrderSO.tradeOrderTitle_field,"title"))
.should(f.matchQuery(TradeOrderSO.payStatus_field,"UNPAID")));
// 不推荐写法
//Search.searchDSL(TradeOrderSO.class,new Page(0,20),searchQuery);
// 推荐写法,查询总数和查询结果一起返回
Search.searchDSLWithAggregation(TradeOrderSO.class,new Page(0,20),searchQuery);
TSQL 的实现方式
String select = "*";
String where = "tradeOrderTitle like 'title%' or payStatus = 'UNPAID'"
Search.paging(TradeOrderSO.class,select,where,new Page(0,20));
TSQL-DSL 的方式
List<SelectField<Object>> selectFields = new ArrayList<>();
selectFields.add(TSQL.field("*"));
Condition condition = TSQL.trueCondition();
condition = condition.and(TSQL.field(TradeOrderSO.tradeOrderTitle_field).like("title%"));
Paging<Order> result = Search.paging(Order.class, selectFields, condition, null, new Page(0,20));

4.3.2.3 后缀查询

SearchQuery 的实现方式
SearchQuery searchQuery = new SearchQuery();
searchQuery.where(f -> f.bool()
.should(f.endsWithQuery(TradeOrderSO.tradeOrderTitle_field,"title"))
.should(f.matchQuery(TradeOrderSO.payStatus_field,"UNPAID")));
// 不推荐写法
//Search.searchDSL(TradeOrderSO.class,new Page(0,20),searchQuery);
// 推荐写法,查询总数和查询结果一起返回
Search.searchDSLWithAggregation(TradeOrderSO.class,new Page(0,20),searchQuery);
TSQL 的实现方式
String select = "*";
String where = "tradeOrderTitle like '%title' or payStatus = 'UNPAID'"
Search.paging(TradeOrderSO.class,select,where,new Page(0,20));
TSQL-DSL 的方式
List<SelectField<Object>> selectFields = new ArrayList<>();
selectFields.add(TSQL.field("*"));
Condition condition = TSQL.trueCondition();
condition = condition.and(TSQL.field(TradeOrderSO.tradeOrderTitle_field).like("%title"));
Paging<Order> result = Search.paging(Order.class, selectFields, condition, null, new Page(0,20));

4.3.3 根据字段分组

分组字段尽量遵照严格模式来执行,并且给需要返回参数添加别名,以便获取到返回值。

4.3.3.1 SearchQuery 的实现方式

SearchQuery searchQuery = new SearchQuery();
searchQuery.where(f -> f.bool()
.must(f.matchQuery(TradeOrderSO.tradeOrderTitle_field,"title")))
.aggregation(a->a.terms("payStatus",TradeOrderSO.payStatus_field));
// 不推荐写法
//Search.searchDSL(TradeOrderSO.class,new Page(0,20),searchQuery);
// 推荐写法,查询总数和查询结果和聚合结果一起返回
Search.searchDSLWithAggregation(TradeOrderSO.class,new Page(0,20),searchQuery);

4.3.3.2 TSQL 的实现方式

String select = "count(*) as doc_count,payStatus";
String where = "tradeOrderTitle = 'title' group by payStatus "
Search.paging(TradeOrderSO.class,select,where,new Page(0,20));

4.3.3.3 TSQL-DSL 的方式

List<SelectField<Object>> selectFields = new ArrayList<>();
selectFields.add(TSQL.count(TSQL.field("*")).as("doc_count"));
selectFields.add(TSQL.field(TradeOrderSO.payStatus_field));
Condition condition = TSQL.trueCondition();
condition = condition.and(TSQL.field(TradeOrderSO.tradeOrderTitle_field).eq("title"));
List<Field<Object>> groupBy = Lists.newArrayList();
groupBy.add(TSQL.field(TradeOrderSO.payStatus_field));
Paging<Order> result = Search.paging(Order.class, selectFields, condition, groupBy,null, new Page(0,20));

注意: 使用 SearchQuery 方式进行分组必须指定别名,因为分组后返回的数据是 List<Map<String,Object>>格式,需要根据别名来返回 key 的参数值。

ES 根据字段分组后,默认会返回分组后字段的数据值以及每一组的数据个数 doc_count,使用 terms 指定的别名作为分组后的字段数据返回。 返回数据举例:

[
{
"doc_count": 6,
"payStatus": "UNPAID"
},
{
"doc_count": 6,
"payStatus": "PAID"
}
]

4.3.4 根据字段排序

4.3.4.1 SearchQuery 的实现方式

SearchQuery searchQuery = new SearchQuery();
searchQuery.where(f -> f.bool()
.must(f.matchQuery(TradeOrderSO.payStatus_field,"UNPAID")))
.sort(TradeOrderSO.usedPointNum_field,Order.DESC);
// 不推荐写法
//Search.searchDSL(TradeOrderSO.class,new Page(0,20),searchQuery);
// 推荐写法,查询总数和查询结果一起返回
Search.searchDSLWithAggregation(TradeOrderSO.class,new Page(0,20),searchQuery);

4.3.4.2 TSQL 的实现方式

String select = "*";
String where = "payStatus = 'UNPAID' order by usedPointNum desc"
Search.paging(TradeOrderSO.class,select,where,new Page(0,20));

4.3.4.3 TSQL-DSL 的方式

List<SelectField<Object>> selectFields = new ArrayList<>();
selectFields.add(TSQL.field("*"));
Condition condition = TSQL.trueCondition();
condition = condition.and(TSQL.field(TradeOrderSO.payStatus_field).eq("UNPAID"));
List<Field<Object>> orderBy = Lists.newArrayList();
orderBy.add(TSQL.field(TradeOrderSO.usedPointNum_field).desc());
Paging<Order> result = Search.paging(Order.class, selectFields, condition, orderBy, new Page(0,20));

4.3.5 搜索条件添加权重写法

4.3.5.1 SearchQuery 的实现方式

//查询payStatus不为null(权重为2)和fulfillmentVersion=100(权重为1)的所有数据。
SearchQuery searchQuery = new SearchQuery();
searchQuery.where(f -> f.bool()
.must(f.existsQuery(TradeOrderSO.payStatus_field).boosts(2.00f))
.should(f.matchQuery(TradeOrderSO.fulfillmentVersion_field, 100)));
// 不推荐写法
//Search.searchDSL(TradeOrderSO.class,new Page(0,20),searchQuery);
// 推荐写法,查询总数和查询结果一起返回
Search.searchDSLWithAggregation(TradeOrderSO.class,new Page(0,20),searchQuery);

4.3.5.2 目前 TSQL 方式不支持权重

4.3.6 地理位置

地理位置功能不支持 TSQL 方式,只能使用 SearchQuery 方式来实现。

4.3.6.1 给定坐标点,搜索出覆盖该坐标的多边形列表

对应业务的需求:搜索出包含该坐标的仓库列表。 Search API 接口实现

// WarehouseSo是一个仓库搜索模型
SearchQuery searchQuery = new SearchQuery();
//声明需要覆盖的坐标点
List<List<Double>> coordinates=new LinkedList<>();
coordinates.add(Arrays.asList(13.0, 53.0));
coordinates.add(Arrays.asList(14.0, 52.0));
searchQuery.where(a -> a.bool().must(a.matchAllQuery())
.filter(a.GeoAreaSearch(TradeOrderSO.geoArea_field)
.relation(ShapeRelation.contains)
.coordinates(coordinates)
.type(ShapeType.point)));
// 不推荐写法
//Search.searchDSL(TradeOrderSO.class,new Page(0,20),searchQuery);
// 推荐写法,查询总数和查询结果一起返回
Search.searchDSLWithAggregation(TradeOrderSO.class,new Page(0,20),searchQuery);

4.3.6.2 给定坐标点,根据仓库和坐标点之间距离排序

对应业务需求: 给定一个坐标点,根据仓库和坐标点之间距离排序 Search API 接口实现

// WarehouseSo是一个仓库搜索模型
SearchQuery searchQuery = new SearchQuery();
searchQuery.where(a -> a.bool().must(a.termsQuery(TradeOrderSO.fulfillmentVersion_field,1,2)))
.geoDistanceSort(s->
s.field(TradeOrderSO.geoArea_field)
.location(120.174381,31.254079)
.order(Order.ASC)
.unit(UnitEnum.km));
Search.searchDSL(TradeOrderSO.class,new Page(0,20),searchQuery);

4.3.7 JSON 字段的查询

Trantor 支持两种类型转成 JSON 字符串存储到数据库中,分别是 List 和 Object,DS 在同步到 ES 上时会生成 Array 和 Object。可参考模型 TradeOrderSO 模型的 sellerRemarkList 和 ownerOrderLineBO 字段。 下面介绍一下这两种结构的搜索方式。

4.3.7.1 Array 类型搜索

Array 类型字段支持 List<Integer>、List<Long>、List<Float>、List<BigDecimal>、List<String>等常见类型的,下面先以 List<String>类型的 sellerRemarkList 举例。

SearchQuery 的搜索方式

单个字段精确匹配:

SearchQuery searchQuery = new SearchQuery();
searchQuery.where(f -> f.bool()
.should(f.termQuery(TradeOrderSO.sellerRemarkList_field, 'se')));
// 不推荐写法
//Search.searchDSL(TradeOrderSO.class ,new Page(0,20),searchQuery);
// 推荐写法,查询总数和查询结果一起返回
Search.searchDSLWithAggregation(TradeOrderSO.class,new Page(0,20),searchQuery);

这个查询表示: sellerRemarkList 中任意一个元素等于 se,则这条数据会被搜索到。

多个字段精确匹配:

SearchQuery searchQuery = new SearchQuery();
searchQuery.where(f -> f.bool()
.should(f.termsQuery(TradeOrderSO.sellerRemarkList_field, 'se','sa','sc')));
// 不推荐写法
//Search.searchDSL(TradeOrderSO.class ,new Page(0,20),searchQuery);
// 推荐写法,查询总数和查询结果一起返回
Search.searchDSLWithAggregation(TradeOrderSO.class,new Page(0,20),searchQuery);

这个查询表示: sellerRemarkList 中任意一个元素等于集合(se,sa,sc)中的任何一个,则这条数据会被搜索到。

Tsql 的搜索方式
String select = "*";
String where = "sellerRemarkList in ('se','sa','sc')"
Search.paging(TradeOrderSO.class,select,where,new Page(0,20));
TSQL-DSL 的方式
List<SelectField<Object>> selectFields = new ArrayList<>();
selectFields.add(TSQL.field("*"));
Condition condition = TSQL.trueCondition();
condition = condition.and(TSQL.field(TradeOrderSO.sellerRemarkList_field).in("se","sa","sc"));
Paging<Order> result = Search.paging(Order.class, selectFields, condition, null, new Page(0,20));

4.3.7.2 Object 类型搜索

Object 类型的字段不支持 Tsql 方式搜索,下面只介绍 SearchQuery 方式的查询。 假设 ownerOrderLineBO 的数据格式:

"ownerOrderLineBO": {
"bundledCode": "正品与换购品的关系",
"combinedAddress": "县组合出来的地址字符串"
}
SearchQuery searchQuery = new SearchQuery();
searchQuery.where(a -> a.bool()
.must(a.matchQuery("ownerOrderLineBO.bundledCode", "正品与换购品的关系").boosts(2.00f)));
// 不推荐写法
//Search.searchDSL(TradeOrderSO.class ,new Page(0,20),searchQuery);
// 推荐写法,查询总数和查询结果一起返回
Search.searchDSLWithAggregation(TradeOrderSO.class,new Page(0,20),searchQuery);

Array 类型和 Object 是支持 SearchQuery 所有的查询函数的,但是不支持分词器,因此 matchQuery 函数没有使用的必要。

  • 加上 Tsql-DSL 的接口使用方法和举例

4.3.8 复杂查询

4.3.8.1 多个条件组合查询

比如我们要查询 条件字段 tradeOrderTitle 要包含’title’一定要满足,条件 sellerRemarkList 数组里包含’se’和条件字段 fulfillmentVersion 等于 100 满足一个即可 的数据,实现方式如下:

SearchQuery 的实现方式

组合查询逻辑较多,这里不再使用函数表达式。

SearchQuery searchQuery = new SearchQuery();
// 开启 bool查询
BoolQueryBuilderSearch bool=new BoolQueryBuilderSearch().bool();
// 条件一
QueryBuilderSearch queryBuilderSearch1=bool.containsQuery(TradeOrderSO.tradeOrderTitle_field,"title");
// 条件二
QueryBuilderSearch queryBuilderSearch2=bool.termQuery(TradeOrderSO.sellerRemarkList_field, "se");
// 条件三
QueryBuilderSearch queryBuilderSearch3=bool.matchQuery(TradeOrderSO.fulfillmentVersion_field, 100);
bool.must(queryBuilderSearch1);
bool.should(queryBuilderSearch2);
bool.should(queryBuilderSearch3);
// 这个参数不加,上面的should条件不会生效
bool.minimumShouldMatch("1");
searchQuery.setBoolQueryBuilderSearch(bool);
// 不推荐写法
//Search.searchDSL(TradeOrderSO.class ,new Page(0,20),searchQuery);
// 推荐写法,查询总数和查询结果一起返回
Search.searchDSLWithAggregation(TradeOrderSO.class,new Page(0,20),searchQuery);

不设置 minimumShouldMatch 参数的实现方式如下:

SearchQuery searchQuery = new SearchQuery();
// 开启 bool查询
BoolQueryBuilderSearch bool=new BoolQueryBuilderSearch().bool();
// 条件一
QueryBuilderSearch queryBuilderSearch1=bool.containsQuery(TradeOrderSO.tradeOrderTitle_field,"title");
// 条件二
QueryBuilderSearch queryBuilderSearch2=bool.termQuery(TradeOrderSO.sellerRemarkList_field, "se");
// 条件三
QueryBuilderSearch queryBuilderSearch3=bool.matchQuery(TradeOrderSO.fulfillmentVersion_field, 100);
BoolQueryBuilderSearch mustBool=new BoolQueryBuilderSearch().bool();
mustBool.must(queryBuilderSearch1)
bool.must(mustBool);
BoolQueryBuilderSearch shouldBool=new BoolQueryBuilderSearch().bool();
shouldBool.should(queryBuilderSearch2);
shouldBool.should(queryBuilderSearch3);
bool.must(shouldBool);
searchQuery.setBoolQueryBuilderSearch(bool);
// 不推荐写法
// Search.searchDSL(TradeOrderSO.class ,new Page(0,20),searchQuery);
// 推荐写法,查询总数和查询结果一起返回
Search.searchDSLWithAggregation(TradeOrderSO.class,new Page(0,20),searchQuery);
TSQL 的实现方式
String select = "*";
String where = "tradeOrderTitle like '%title%' and (sellerRemarkList = 'se' or fulfillmentVersion = 100) "
Search.paging(TradeOrderSO.class,select,where,new Page(0,20));
TSQL-DSL 的方式
List<SelectField<Object>> selectFields = new ArrayList<>();
selectFields.add(TSQL.field("*"));
Condition condition = TSQL.trueCondition();
condition = condition.and(TSQL.field(TradeOrderSO.tradeOrderTitle_field).like("%title%"));
condition = condition.and(TSQL.field(TradeOrderSO.fulfillmentVersion_field).eq("se")
.or(TSQL.field(TradeOrderSO.fulfillmentVersion_field).eq(100)));
Paging<Order> result = Search.paging(Order.class, selectFields, condition, null, new Page(0,20));

4.3.8.2 ES 对脚本的支持

ES 提供了脚本支持 —— 编写脚本所用的语言,默认为 painless。当然也支持其他的语言,比如表达式语言 expression。ES 也支持高级语言,不过需要下载插件。下面介绍 painless 语言的简单用法。 script 内各个字段的含义如下:

"script": {
// 脚本所用语言,默认是painless
"lang": "expression",
// 执行脚本,由于我们使用了expression语言,因此可以使用数学表达式来执行加减乘除
"source": "doc['age'] * multiplier", // 获取age字段的值进行计算
// 类似预编译模式,ES对第一次的脚本内容解析并进行缓存。如果只是变量参数改变的话,可以param代替
"params": {
"multiplier": 2
}
}

脚本有两种方式提取文档的其他字段内容,分别是 params [‘_source’]和 doc[]。 比如获取 tradeOrderTitle 字段的值: doc[‘tradeOrderTitle’].value 和 params[‘_source’][‘tradeOrderTitle’] 两种方式之间的区别:

  • 使用 doc 关键字: 将导致该字段的术语加载到内存(缓存)中,这样脚本的执行速度会更快,但也会带来更多的内存消耗。另外,doc […]符号只允许简单的值字段(不能从中返回 JSON 对象),并且它只对非分析或基于单个术语的字段有意义。
  • 使用 params 关键字: 每次使用时都必须加载和解析_source,这是非常缓慢的。

建议: 使用 doc 关键字,从文档中访问相关字段的值,这种方式更加高效。

4.3.8.3 脚本排序举例

业务场景: 搜索 TradeOrderSO,SO 里的支付状态字段有已支付和未支付,已支付的排序在前面。然后再根据积分数量正序搜索。

SearchQuery searchQuery = new SearchQuery();
Script script = new Script();
script.lang("painless");
script.source("doc['payStatus'].value=='UNPAID'?1:0");
searchQuery.sort(s -> s.script(script).order(Order.ASC).type("NUMBER"))
.sort(TradeOrderSO.usedPointNum_field, Order.ASC))
// 不推荐写法
//Search.searchDSL(TradeOrderSO.class ,new Page(0,20),searchQuery);
// 推荐写法,查询总数和查询结果一起返回
Search.searchDSLWithAggregation(TradeOrderSO.class,new Page(0,20),searchQuery);

如果还想将订单标题包含’化妆品’并且已支付的订单排在前面,可以这样写脚本语句:

SearchQuery searchQuery = new SearchQuery();
Script script = new Script();
script.lang("painless");
script.source("doc['payStatus'].value=='UNPAID' && doc['tradeOrderTitle'].value=='化妆品'?1:0");
searchQuery.sort(s -> s.script(script).order(Order.ASC).type("NUMBER"))
.sort(TradeOrderSO.usedPointNum_field, Order.ASC))
// 不推荐写法
//Search.searchDSL(TradeOrderSO.class ,new Page(0,20),searchQuery);
// 推荐写法,查询总数和查询结果一起返回
Search.searchDSLWithAggregation(TradeOrderSO.class,new Page(0,20),searchQuery);

4.3.8.4 分组的时候添加聚合函数

业务场景: ES 在分组后会默认返回每一个组的数据个数,以及分组字段数据,因此 ES 是天然支持 count 函数。除了 count,ES 同样支持 sum、min、max、avg 函数。

示例:

SearchQuery 的实现方式
SearchQuery searchQuery = new SearchQuery();
searchQuery.where(f -> f.bool()
.must(f.matchQuery(TradeOrderSO.tradeOrderTitle_field,"title")))
.aggregation(a->a.terms("payStatus",TradeOrderSO.payStatus_field)
.min("usedPointNumMin",TradeOrderSO.usedPointNum_field)
.max("usedPointNumMax",TradeOrderSO.usedPointNum_field)
.sum("usedPointNumSum",TradeOrderSO.usedPointNum_field)
.avg("usedPointNumAvg",TradeOrderSO.usedPointNum_field));
// 不推荐写法
// Search.searchDSL(TradeOrderSO.class,new Page(0,20),searchQuery);
// 推荐写法,查询总数和查询结果一起返回
Search.searchDSLWithAggregation(TradeOrderSO.class,new Page(0,20),searchQuery);
TSQL 的实现方式
String select = "count(*) as doc_count,payStatus,min(usedPointNum) as usedPointNumMin,max(usedPointNum) as usedPointNumMax,sum(usedPointNum) as usedPointNumSum,avg(usedPointNum) as usedPointNumAvg";
String where = "tradeOrderTitle = 'title' group by payStatus "
Search.paging(TradeOrderSO.class,select,where,new Page(0,20));
TSQL-DSL 的方式
List<SelectField<Object>> selectFields = new ArrayList<>();
selectFields.add(TSQL.count(TSQL.field("*")).as("doc_count"));
selectFields.add(TSQL.min(TSQL.field(TradeOrderSO.usedPointNum_field)).as("usedPointNumMin"));
selectFields.add(TSQL.max(TSQL.field(TradeOrderSO.usedPointNum_field)).as("usedPointNumMax"));
selectFields.add(TSQL.sum(TSQL.field(TradeOrderSO.usedPointNum_field)).as("usedPointNumSum"));
selectFields.add(TSQL.avg(TSQL.field(TradeOrderSO.usedPointNum_field)).as("usedPointNumAvg"));
selectFields.add(TSQL.field(TradeOrderSO.payStatus_field));
Condition condition = TSQL.trueCondition();
condition = condition.and(TSQL.field(TradeOrderSO.tradeOrderTitle_field).eq("title"));
List<Field<Object>> groupBy = Lists.newArrayList();
groupBy.add(TSQL.field(TradeOrderSO.payStatus_field));
Paging<Order> result = Search.paging(Order.class, selectFields, condition, groupBy,null, new Page(0,20));

4.3.8.5 根据多字段分组并添加聚合函数

分组字段尽量遵照严格模式来执行,并且给需要返回参数添加别名,以便获取到返回值。

SearchQuery 的实现方式
SearchQuery searchQuery = new SearchQuery();
searchQuery.where(f -> f.bool()
.must(f.matchQuery(TradeOrderSO.tradeOrderTitle_field,"title")))
.aggregation(a->a.terms("payStatus",TradeOrderSO.payStatus_field)
.terms("fulfillmentVersion",TradeOrderSO.fulfillmentVersion_field)
.max("usedPointNumMax",TradeOrderSO.usedPointNum_field)
.sum("usedPointNumSum",TradeOrderSO.usedPointNum_field));
// 不推荐写法
//Search.searchDSL(TradeOrderSO.class,new Page(0,20),searchQuery);
// 推荐写法,查询总数和查询结果一起返回
Search.searchDSLWithAggregation(TradeOrderSO.class,new Page(0,20),searchQuery);
TSQL 的实现方式
String select = "count(*) as doc_count,max(usedPointNum) as usedPointNumMax,sum(usedPointNum) as usedPointNumSum,payStatus,fulfillmentVersion";
String where = "tradeOrderTitle = 'title' group by payStatus,fulfillmentVersion"
Search.paging(TradeOrderSO.class,select,where,new Page(0,20));
TSQL-DSL 的方式
List<SelectField<Object>> selectFields = new ArrayList<>();
selectFields.add(TSQL.count(TSQL.field("*")).as("doc_count"));
selectFields.add(TSQL.max(TSQL.field(TradeOrderSO.usedPointNum_field)).as("usedPointNumMax"));
selectFields.add(TSQL.sum(TSQL.field(TradeOrderSO.usedPointNum_field)).as("usedPointNumSum"));
selectFields.add(TSQL.field(TradeOrderSO.payStatus_field));
selectFields.add(TSQL.field(TradeOrderSO.fulfillmentVersion_field));
Condition condition = TSQL.trueCondition();
condition = condition.and(TSQL.field(TradeOrderSO.tradeOrderTitle_field).eq("title"));
List<Field<Object>> groupBy = Lists.newArrayList();
groupBy.add(TSQL.field(TradeOrderSO.payStatus_field));
groupBy.add(TSQL.field(TradeOrderSO.fulfillmentVersion_field));
Paging<Order> result = Search.paging(Order.class, selectFields, condition, groupBy,null, new Page(0,20));

ES 根据字段分组后,默认会返回分组后字段的数据值以及每一组的数据个数 doc_count,使用 terms 指定的别名作为分组后的字段数据返回。

4.3.9 后置过滤器

4.3.9.1 任务需求

用户在商品界面上点击了一个分类。期望的结果是搜索结果被过滤了,而用户商品界面上的分类选项是不会变化的。之前这种需求都是通过请求两次 DS,一次请求通过聚合获得分类选项,一次请求获得搜索结果。这种对于业务来说比较影响性能,而且会有代码冗余。 DS 需要提供后置过滤器来解决这类问题, 后置过滤器内包装的过滤条件不影响聚合结果,是先聚合在执行后置过滤器内的过滤条件。

4.3.9.2 查询原理

先执行聚合,后执行后置过滤器内的查询

举例说明:

{
"query": {
"match": {
"make": "ford"
}
},
"post_filter": {
"term": {
"color": "green"
}
},
"aggs": {
"all_colors": {
"terms": { "field": "color" }
}
}
}

比如上面这次针对商品的查询,搜索结果会返回 make=ford 和 color=green 的数据,聚合结果是对满足 make=ford 条件的所有数据进行 color 颜色分类的数据, 聚合时 color=green 条件不生效。

4.3.9.3 使用方式

Search API:

public <Model extends BaseModel<Long>> SearchResult<Model> searchDSLWithAggregation(Class<Model> modelClass, Page page, SearchQuery searchQuery);

返回结果参数:

/**
* 搜索结果返回聚合类,包括分页查询结果和聚合数据
*
* @param <T>
*/
@Getter
@Setter
public class SearchResult<T> implements Serializable {
private static final long serialVersionUID = -476108507543266445L;
/**
* 查询命中结果的总数
*/
private Long total;
/**
* 当前分页数据集合
*/
private List<T> hits;
/**
* 聚合结果集
* 聚合结果对应入参List<AggregationBuilderSearch> aggregationBuilderSearchList
* 一个AggregationBuilderSearch聚合对应一个Map<String, Object>返回
* 其中AggregationBuilderSearch.aliasName为返回结果的Map-key,Object对应该聚合场景下的聚合结果集,可能是list,可能是单值,依赖实际聚合场景
*/
private List<Map<String, Object>> aggregations;
}

SearchQuery 使用方式

SearchQuery searchQuery = new SearchQuery();
// 开启 普通查询
BoolQueryBuilderSearch bool=new BoolQueryBuilderSearch().bool();
// 开启 postFilter查询
BoolQueryBuilderSearch postFilter=new BoolQueryBuilderSearch().bool();
// 条件一
QueryBuilderSearch queryBuilderSearch1=bool.termQuery("make","ford");
// 条件二
QueryBuilderSearch queryBuilderSearch2=postFilter.termQuery("color", "green");
bool.filter(queryBuilderSearch1);
postFilter.filter(queryBuilderSearch2);
searchQuery.setBoolQueryBuilderSearch(bool);
searchQuery.setBoolPostFilterBuilderSearch(postFilter);
List<AggregationBuilderSearch> aggregationBuilderSearchList = Lists.newArrayList();
AggregationBuilderSearch aggregationBuilderSearch = new AggregationBuilderSearch();
aggregationBuilderSearch.terms("all_colors","color");
aggregationBuilderSearchList.add(aggregationBuilderSearch);
searchQuery.setAggregationBuilderSearchList(aggregationBuilderSearchList);
Search.searchDSLWithAggregation(TradeOrderSO.class ,new Page(0,20),searchQuery);

4.3.9.4 性能考量

只有当你需要对搜索结果和聚合使用不同的过滤方式时才考虑使用 post_filter。常规搜索中不要使用 post_filter。post_filter 会在查询之后才会被执行,因此会失去过滤在性能上的优化(比如缓存)。post_filter 应该只和聚合一起使用,并且仅当你使用了不同的过滤条件时。

4.3.10 scroll 批量导出

4.3.10.1 使用场景

scroll 查询 可以用来对 Elasticsearch 有效地执行大批量的文档导出,而又不用付出深度分页那种代价。

简单原理: 游标查询会取某个时间点的快照数据。查询初始化之后索引上的任何变化会被它忽略。 它通过保存旧的数据文件来实现这个特性,结果就像保留初始化时的索引视图一样。

优点: 这种性能比浅分页性能要高出很多,基本都是毫秒级的。 缺点: scroll 不支持跳页查询,没有实时性(在一次查询后的快照缓存时间内,不会去查新变化的数据)。 适用场景:需要批量导出查询的所有结果用于二次分析,不关注导出时数据的实时变化

4.3.10.2 查询方式

// 首次查询,scrollId不需要设置
searchQuery.where(a -> a.bool().must(a.matchAllQuery()))
.size(100)
.setScrollRequest(ScrollRequest.builder().openScroll(true).build());
SearchResult<TradeOrderSO> result = Search.scroll(TradeOrderSO.class, searchQuery);
// 循环导出,每次需要将上一次返回的scrollId设置到ScrollRequest.scrollId中
while (!CollectionUtils.isEmpty(result.getHits())) {
searchQuery.setScrollRequest(ScrollRequest.builder()
.openScroll(true)
.scrollId(result.getScrollId())
.build());
result = Search.scroll(TradeOrderSO.class, searchQuery);
}

4.3.11 search_after 翻页查询

4.3.11.1 使用场景

search_after 可以实现顺序遍历所有查询接口,顺序翻页到最后一页

简单原理: 相对于 from,size, search_after 就是将查询 order by time offset 0 limit 100,改写成 order by time where time>0 limit 100。

优点: 和 scroll 一样,查询性能比浅查询要好,search_after 的查询顺序会在更新和删除时发生变化,也就是说支持实时的数据查询。 缺点:强依赖于排序字段,不能跳页查询,只是并行的滚屏查询。 适用场景:需要顺序查看所有查询结果时,该接口可以通过翻页方式查询到最后一页,但是只能一页一页往后翻,不支持跳页

4.3.11.2 查询方式

// 首次查询,SearchAfterRequest.searchAfter可以不用设置
searchQuery.where(a -> a.bool().must(a.matchAllQuery()))
.size(100)
.setSearchAfterRequest(SearchAfterRequest.builder().openSearchAfter(true).build());
SearchResult<TradeOrderSO> result = Search.searchAfter(TradeOrderSO.class, searchQuery);
// 顺序翻页,需要将上次查询返回的searchAfterLatestSorts设置到SearchAfterRequest.searchAfter中
while (!CollectionUtils.isEmpty(result.getHits())) {
searchQuery.setSearchAfterRequest(SearchAfterRequest.builder()
.openSearchAfter(true)
.searchAfter(result.getSearchAfterLatestSorts())
.build());
result = Search.searchAfter(TradeOrderSO.class, searchQuery);
}

4.3.12 collapse 分组查询

4.3.12.1 使用场景

当需要针对某个字段进行分组且需要指定返回分组后的某一条记录时使用 demo: 一个商品有很多家店铺都在售卖, 业务上商品+店铺 id 是作为多条记录存储的, 此时搜索的时候如果业务希望按照商品维度做去重返回,也就是说每个商品只需要选取一家门店的记录做返回时,可以使用 collapse 做分组去重查询

collapse 对标 es 的 collapse 查询功能

注意事项:使用 collapse 后,查询结果支持分页,但是分组后的总组数需要额外通过特殊聚合查询去返回,使用 demo 参见 4.3.12.1

4.3.12.2 查询方式

SearchQuery searchQuery = new SearchQuery();
// collapse 查询结构封装
Collapse collapse = new Collapse();
// 需要进行分组的字段
collapse.field("intField1");
List<InnerHit> innerHits = Lists.newArrayList();
// 分组后,组内的返回结构配置,每个分组内返回的条数是可以配置的
InnerHit innerHit = new InnerHit();
// 每个分组内返回的记录条数
innerHit.size(1);
// 每个分组的别名
innerHit.name("myGroup");
innerHits.add(innerHit);
collapse.innerHits(innerHits);
searchQuery.where(f -> f.bool()
.must(f.matchAllQuery()))
.collapse(collapse)
// 此处需要注意,通过该聚合结构来获取分组后的实际分组总数,intField1为分组字段名
.aggregation(f -> f.cardinality("groupCount", "intField1"))
.from(0).size(2).index("test_SearchModel1");
searchQuery.setIsCount(true);
Response<SearchResult<Map<String, Object>>> resp = autumnClient.executeDSLWithAggregationByES(getProjectId(), null, searchQuery);
// 分组后总分组数获取,用于分页
List<Map<String, Object>> aggResult = resp.getRes().getAggregations();
aggResult.forEach(map -> {
map.forEach((key, value) -> {
Assert.assertEquals("groupCount", key);
Assert.assertEquals(6, Long.parseLong(String.valueOf(value)));
});
});
// collapse分组后,inner_hit内的内容,通过resp.getRes().getCollapseInnerHits()结果返回的, 该结构内的返回顺序同实际hits的返回顺序
List<Map<String, InnerHitResult<Map<String, Object>>>> resList = resp.getRes().getCollapseInnerHits();
resList.forEach(map -> {
Assert.assertTrue(map.containsKey("myGroup"));
map.forEach((name, innerHitResult) -> {
Assert.assertEquals(1, innerHitResult.getTotal().longValue());
Assert.assertFalse(CollectionUtils.isEmpty(innerHitResult.getHits()));
});
});

5 高级功能

5.1 分词器

ds 目前支持系统级别分词器和字段级别分词器,对应配置方式参见 5.1.1 和 5.1.2 章节

目前支持分词类型如下:

分词器 code中文描述demo
ik_max_wordik 分词器之细粒度分词器,一般用于索引分词器,索引分词器如果选择 ik_max_word,则对应查询分词器默认为 ik_smart中华人民共和国端点科技-〉中华人民共和国、中华人民、中华、华人、人民共和国、人民、共和国、共和、国、端点、科技
ik_smartik 分词器之粗粒度分词器,一般用于查询分词器,建议索引分词器使用 ik_max_word,查询分词器使用 ik_smart中华人民共和国端点科技->中华人民共和国、端点、科技
ngram_analyzer数字分词,应用于对手机号或者条码等短字符进行模糊查询场景,默认最小分割力度为 212345-〉12,123,1234,12345,23,234,2345,34…
pinyin_analyzer拼音分词器,需要确保 es 集群已安装 analysis-pinyin 插件中华人民共和国端点科技->中华人民共和国端点科技、zhonghuarenmingongheguoduandiankeji、zhrmghgddkj
ik_pinyin_analyzerik+拼音分词器,使用 ik 分词器切词,对切完的词同时应用 pinyin 插件过滤翻译,需要确保 es 集群已安装 analysis-pinyin 插件中华人民共和国端点科技->中华人民共和国、zhonghuarenmingongheguo、zhrmghg、中华人民、zhonghuarenmin、zhrm、中华、zhonghua、zh、华人、huaren、hr、人民共和国、renmingongheguo、rmghg、人民、renmin、rm、共和国、gongheguo、ghg、共和、gonghe、gh、国、guo、g、端点、duandian、dd、科技、keji、kj
char_analyzer单字拆分中华人民共和国端点科技->中、华、人、民、共、和、国、端、点、科、技

5.1.1 系统级别分词器

系统级别分词器 ds 会对所有模型中的所有 keyword 类型字段都添加二级字段 text,然后对二级字段 text 指定分词器

如果需要开启系统级别分词查询,需要配置 ds search 中对应环境变量,如下所示

变量名默认值取值说明
STRING_ANALYZERstring 字段类型分词查询时使用的索引端分词器,默认为空,为空代表不分词,可选 ik 分词配置为:ik_max_word 等
STRING_SEARCH_ANALYZERstring 字段类型分词查询时使用的查询端分词器,默认为空,为空代表不分词,可选 ik 分词配置为:ik_smart 等

变量配置完,重启 ds search 后,系统会自动对搜索模型进行全量同步使分词生效!

5.1.1.1 中文分词器

5.1.1.1.1 生效方式

中文分词器使用 ik 分词插件 配置 demo: STRING_ANALYZER=ik_max_word STRING_SEARCH_ANALYZER=ik_smart

变量配置完,重启 ds search 后,系统会自动对搜索模型进行全量同步使分词生效!

5.1.1.1.2 查询支持

目前查询方式只支持 SearchQuery 方式。 查询接口:

@Override
public static <Model extends BaseModel<Long>> Paging<Model> searchDSL(Class<Model> modelClass, Page page, SearchQuery searchQuery);

在构建 searchQuery 时,创建 matchAnalyzeQuery 对象进行分词查询,如下所示

SearchQuery searchQuery = new SearchQuery();
searchQuery.where(a -> a.bool()
.must(a.matchAnalyzeQuery(TradeOrderSO.tradeOrderTitle_field, "中华人")));

在构建 searchQuery 时,创建 termAnalyzeQuery 对象进行分词查询,如下所示

SearchQuery searchQuery = new SearchQuery();
searchQuery.where(a -> a.bool()
.must(a.termAnalyzeQuery(TradeOrderSO.tradeOrderTitle_field,, "Nike")));

analyzeQuery 默认将字段转换成 字段.text

简单解释一下这两种种搜索: 如果 tradeOrderTitle 在 ES 上存储的是”中华人民共和国”。 那 ES 上分词的结果就是: {中华},{人民},{共和国},{人},{国},{共和} 那 matchAnalyzeQuery 搜索{中华人}是能搜到数据的,因为 matchAnalyzeQuery 会将{中华人}分为 {中华}和{人}去搜索。 termAnalyzeQuery 搜索{中华人}是搜不到的,但是搜索{中华}是能搜到的,因为分词器已经 itemName 分为一个一个的 term。

5.1.1.2 拼音分词器

5.1.1.2.1 生效方式

拼音分词器使用 es-pinyin 插件,使用该分词器需要确保集群中已安装拼音插件,查看方式如下

GET /_cat/plugins
返回结果中如果存在analysis-pinyin 代表插件已安装

配置 demo: STRING_ANALYZER=pinyin_analyzer STRING_SEARCH_ANALYZER=pinyin_analyzer

变量配置完,重启 ds search 后,系统会自动对搜索模型进行全量同步使分词生效!

5.1.1.2.2 查询支持

查询方式同中文分词使用方法,详见 5.1.1.2

5.1.2 字段级别分词器

字段级别分词器 允许对指定字段进行分词器设置 ⚠️ 注意:目前仅支持“Text”,“Json”,“MultiText” 的字段类型可“设置搜索字段”

配置字段分词器后,ds 会自动触发全量同步,不需要手动触发

5.1.2.1 使用手册

路径: 交付控制台 -> 运行环境 -> 搜索模型 undefined

找到需要设置的模型,点击“详情” undefined

找到字段列表,在列表操作“设置搜索字段” undefined

点击后,在弹框中进行设置搜索分词,保存即可 undefined

5.1.2.2 注意事项

5.1.2.2.1 升级

1、需要修改 ds search 如下环境变量,DS_SEARCH_USE_SYSTEM_ANALYZER_CONFIG 设置为 false

变量名默认值变量描述
DS_SEARCH_USE_SYSTEM_ANALYZER_CONFIGtrue是否使用系统级别分词器和忽略大小写配置,默认是 true,如果需要使用字段级别分词器配置,则该变量需要设置为 false,并配套升级 trantor-framework、ms 等镜像

2、该功能为配套版本,需要 ds 和 trantor 一起升级到指定版本,详细参见https://trantor-community.app.terminus.io/article/443

trantor 版本为(大于等于下面版本) metastore-management 镜像:registry.cn-hangzhou.aliyuncs.com/terminus/trantor-metastore-management:220217.193920 t-console 镜像:registry.cn-hangzhou.aliyuncs.com/terminus/trantor-console:1.18.0-support-console-18.70 metastore-runtime 镜像:registry.cn-hangzhou.aliyuncs.com/terminus/trantor-metastore-runtime:220217.191714 trantor-framework 版本:1.0.8-SNAPSHOT

3、ds 版本为(大于等于下面版本) DS SDK 版本:6.0.0.66.RELEASE DS Server 镜像:registry.erda.cloud/trantor/datastore:6.0.0.66.RELEASE.20220221 DS Search 镜像:registry.erda.cloud/trantor/datastore-search:6.0.0.66.RELEASE.20220221

4、需要手动将 ds 元信息库中 search_model_field_config 表清空,表不存在则忽略

5.1.2.2.2 升级失败回滚

需要手动将 ds 元信息库中 search_model_field_config 表清空

5.2 忽略大小写搜索

5.2.1 系统级别忽略大小写搜索

针对 String 和 List<String>类型数据,ds 支持忽略大小写进行搜索.默认是不忽略大小写,如果需要开启忽略大小写,需要配置 ds search 中对应环境变量,如下所示

变量名默认值取值说明
NEED_NORMALIZE_KEYWORD_TO_CASEINSENSITIVEfalse是否需要对 String 类型开启不区分大小写模式,默认是 false
NEED_NORMALIZE_KEYWORD_ARRAY_TO_CASEINSENSITIVEfalse是否需要对 List<String>类型开启不区分大小写模式,默认是 false

trantor 侧 String 和 List<String>对应字段类型如下: String -> Text、MultiText List<String> -> Json

如上变量是系统级别的,配置后,将对项目下所有搜索模型都生效,模型下所有 String 类型或者 List<String>类型的数据都生效!

5.2.1.1 生效方式

String 类型忽略大小写 NEED_NORMALIZE_KEYWORD_TO_CASEINSENSITIVE=true List<String>类型忽略大小写 NEED_NORMALIZE_KEYWORD_ARRAY_TO_CASEINSENSITIVE=true

对需要忽略大小写的搜索模型进行全量同步,全量同步操作方式参见章节 6.1.1

5.2.1.2 查询方式

忽略大小写对查询方式没有特殊要求,对于使用者来说,忽略大小写查询不需要特殊语法,具体查询语法参见章节 4

5.2.2 字段级别忽略大小写搜索

使用方式参见 5.1.2 字段级别分词器

5.3 搜索模型索引拆分

对于业务层面来说,很多历史数据是不需要更新的,或者是不需要查询的,所以 ds 需要具备索引分区的功能,针对不同业务做不同索引拆分。

当查询明确需要查询某几个分区时,分区的作用才能凸显。

5.3.1 配置方式

  • 入口 undefined
  • 分区设置 undefined

更多详细介绍请参考文档: https://yuque.antfin.com/docs/share/cdcd183c-44d0-4ba8-a45b-85fb6dc348ad?# 《搜索模型索引拆分》

5.4 搜索模型使用独立 topic

需要配置如下变量,并重启 ds server 端

变量名默认值变量描述
MQ_SEND_EXCLUSIVE_TOPIC_SEARCH_MODELS使用独立 Topic 的搜索模型,配置 demo 如 trantor[base_xxx_so/base_Distinct_so];trantor_test[base_xxx_so/base_Distinct_so]; 格式为: projectId[搜索模型名 1/搜索模型名 2] 多个 project 间使用;分割

ds search 端

变量名默认值变量描述
DS_MQ_CONSUME_EXCLUSIVE_TOPIC_SEARCH_MODELS使用独立 Topic 的搜索模型,配置 demo 如 trantor[base_xxx_so:20/base_Distinct_so:20];trantor_test[base_xxx_so:20/base_Distinct_so:20]; 格式为: projectId[搜索模型名 1:模型 1 消费线程数/搜索模型名 2:模型 2 消费线程数] 多个 project 间使用;分割,消费线程数为必填

6 实现原理

搜索模型目前采用的方式先将搜索模型的数据存放到数据库中,然后通过同步的方式上传到 ES 上。查询的话,再通过 Search API 将搜索引擎上的数据搜索出来。

搜索模型流程图: undefined

按照功能划分,同步主要分为两种,一种是需要手动操作的全量同步,第二种是 DS 自动实现的增量同步。

6.1 全量同步

6.1.1 全量修复

全量修复会将根据 DS 中存储的搜索模型元信息生成对应的 ES Mapping,然后根据 Mapping 在 ES 上创建索引,并将数据库中的全部数据以分页的方式同步到 ES 上,每页 500 条。 当发现 ES 上索引字段定义和模型定义不一致时,或者 ES 数据不正确,以及有旧数据未同步到 ES 上,可通过全量修复按钮修复这些问题。 操作方法:可在交付控制台上通过按钮操作: undefined

6.1.2 增量修复

全量修复是一个比较耗时又损耗性能的操作。如果只有部分数据需要被修复,可以采用增量修复按钮来修复数据,增量修复并不会新建索引,并可以一次修复多条数据。 操作方法:可在交付控制台上通过按钮操作: undefined

6.1.3 全量修复原理

全量修复流程图: undefined undefined undefined

全量修复详细设计以及错误码整理: https://yuque.antfin.com/docs/share/6285dc77-0ce1-4561-95d2-0717aba4c064?# 《搜索模型全量同步流程以及改进点梳理》

6.1.4 增量修复原理

增量修复支持按照 ids、时间范围进行增量修复 修复流程类似于全量修复流程, 不同之处在于,增量修复是对当前 running 状态的索引进行数据的增量更新, 不会导致索引的新增

6.2 实时数据同步原理

增量同步是由 DS 内部实现,不需要业务端进行额外的操作。当业务进行数据增删改时,DS 会自动将变更的数据同步到 ES 上。具体原理是 DS 先将变更的数据保存到数据库,然后发消息给另一个服务 Search,通过 Search 服务将变更的数据同步到 ES 上。

具体流程和原理可参考: undefined

搜索模型默认 topic: DSSearch*{projectId} 搜索模型默认 consumer group: GID_DSSearch*{projectId}

6.3 Server 端查询搜索引擎流程

Server 端采用 Http 方式查询 ES,然后通过别名查询索引(为了保证全量同步时,新旧索引都可以被搜到)。

具体流程和原理可参考: undefined

6.4 模型定义最细粒度更新

模型定义发生修改时,DS 默认采用全量同步的方式进行索引和数据的重建,当数据量非常大时,全量同步数据会非常耗时,因此 DS 提供了当模型变更时最细粒度更新的功能.

DS 开启最细粒度更新的环境变量配置如下: AUTO_FULL_SYNC_WHEN_SEARCH_MODEL_DEFINITION_UPDATED: 搜索模型在修改之后是否立即进行全量同步,默认是 true NEED_MINIMUM_UPDATE_WHEN_SEARCH_MODEL_DEFINITION_UPDATED: 搜索模型在修改之后是否启用最细粒度索引更新,默认是 false,不开启

当开启最细粒度方式更新时,模型定义更新方案如下:

模型变更方式ds 生效机制
模型字段新增系统自动增加字段映射信息,立即生效
模型字段删除不需要额外操作,立即生效,删除字段跟随下次全量同步做清理
模型字段长度修改不需要额外操作,立即生效
其它修改全量同步完成后生效

环境变量 AUTO_FULL_SYNC_WHEN_SEARCH_MODEL_DEFINITION_UPDATED 决定模型定义修改后是否立即进行全量同步,默认是开启状态。但是当启用最细粒度更新策略时,模型字段新增、模型字段删除和模型字段删除并不会执行全量同步,只有其他修改会立即进行全量同步。如果设置了 AUTO_FULL_SYNC_WHEN_SEARCH_MODEL_DEFINITION_UPDATED 变量为 false,当启用最细粒度更新策略时,模型字段新增、模型字段删除和模型字段删除也不会执行全量同步,其他修改需要手动全量同步才能使模型定义修改生效。

7 错误文档链接

https://yuque.antfin.com/docs/share/ee91d4af-b921-4590-9005-cf63ed347f8c?# 《DS 搜索模型常见问题》

8 DS/DS Search 部署

https://yuque.antfin.com/docs/share/88365a37-a306-4c79-8275-98c5d8bb50e8?# 《DS erda 部署》