[ML] Add sort parameter for get buckets (elastic/x-pack-elasticsearch#1464)

* Add sort parameter for get buckets

* Add secondary sort by time

* Use default values from actions in rest requests

Original commit: elastic/x-pack-elasticsearch@a530c0bed6
This commit is contained in:
David Kyle 2017-05-18 16:40:54 +01:00 committed by GitHub
parent 27af24de6f
commit f3c94915b0
10 changed files with 131 additions and 13 deletions

View File

@ -33,6 +33,9 @@ This API presents a chronological view of the records, grouped by bucket.
`anomaly_score`:: `anomaly_score`::
(double) Returns buckets with anomaly scores higher than this value. (double) Returns buckets with anomaly scores higher than this value.
`desc`::
(boolean) If true, the buckets are sorted in descending order.
`end`:: `end`::
(string) Returns buckets with timestamps earlier than this time. (string) Returns buckets with timestamps earlier than this time.
@ -49,6 +52,10 @@ This API presents a chronological view of the records, grouped by bucket.
`size`::: `size`:::
(integer) Specifies the maximum number of buckets to obtain. (integer) Specifies the maximum number of buckets to obtain.
`sort`::
(string) Specifies the sort field for the requested buckets.
By default, the buckets are sorted by the `timestamp` field.
`start`:: `start`::
(string) Returns buckets with timestamps after this time. (string) Returns buckets with timestamps after this time.

View File

@ -20,7 +20,6 @@ influencers.
`desc`:: `desc`::
(boolean) If true, the results are sorted in descending order. (boolean) If true, the results are sorted in descending order.
//TBD: Using the "sort" value?
`end`:: `end`::
(string) Returns influencers with timestamps earlier than this time. (string) Returns influencers with timestamps earlier than this time.
@ -40,7 +39,7 @@ influencers.
`sort`:: `sort`::
(string) Specifies the sort field for the requested influencers. (string) Specifies the sort field for the requested influencers.
//TBD: By default the results are sorted on the influencer score? By default the influencers are sorted by the `influencer_score` value.
`start`:: `start`::
(string) Returns influencers with timestamps after this time. (string) Returns influencers with timestamps after this time.

View File

@ -5,6 +5,7 @@
*/ */
package org.elasticsearch.xpack.ml.action; package org.elasticsearch.xpack.ml.action;
import org.elasticsearch.Version;
import org.elasticsearch.action.Action; import org.elasticsearch.action.Action;
import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequest;
@ -69,6 +70,8 @@ public class GetBucketsAction extends Action<GetBucketsAction.Request, GetBucket
public static final ParseField END = new ParseField("end"); public static final ParseField END = new ParseField("end");
public static final ParseField ANOMALY_SCORE = new ParseField("anomaly_score"); public static final ParseField ANOMALY_SCORE = new ParseField("anomaly_score");
public static final ParseField TIMESTAMP = new ParseField("timestamp"); public static final ParseField TIMESTAMP = new ParseField("timestamp");
public static final ParseField SORT = new ParseField("sort");
public static final ParseField DESCENDING = new ParseField("desc");
private static final ObjectParser<Request, Void> PARSER = new ObjectParser<>(NAME, Request::new); private static final ObjectParser<Request, Void> PARSER = new ObjectParser<>(NAME, Request::new);
@ -81,6 +84,8 @@ public class GetBucketsAction extends Action<GetBucketsAction.Request, GetBucket
PARSER.declareStringOrNull(Request::setEnd, END); PARSER.declareStringOrNull(Request::setEnd, END);
PARSER.declareObject(Request::setPageParams, PageParams.PARSER, PageParams.PAGE); PARSER.declareObject(Request::setPageParams, PageParams.PARSER, PageParams.PAGE);
PARSER.declareDouble(Request::setAnomalyScore, ANOMALY_SCORE); PARSER.declareDouble(Request::setAnomalyScore, ANOMALY_SCORE);
PARSER.declareString(Request::setSort, SORT);
PARSER.declareBoolean(Request::setDescending, DESCENDING);
} }
public static Request parseRequest(String jobId, XContentParser parser) { public static Request parseRequest(String jobId, XContentParser parser) {
@ -99,6 +104,8 @@ public class GetBucketsAction extends Action<GetBucketsAction.Request, GetBucket
private String end; private String end;
private PageParams pageParams; private PageParams pageParams;
private Double anomalyScore; private Double anomalyScore;
private String sort = Result.TIMESTAMP.getPreferredName();
private boolean descending = false;
Request() { Request() {
} }
@ -179,7 +186,7 @@ public class GetBucketsAction extends Action<GetBucketsAction.Request, GetBucket
this.pageParams = ExceptionsHelper.requireNonNull(pageParams, PageParams.PAGE.getPreferredName()); this.pageParams = ExceptionsHelper.requireNonNull(pageParams, PageParams.PAGE.getPreferredName());
} }
public double getAnomalyScore() { public Double getAnomalyScore() {
return anomalyScore; return anomalyScore;
} }
@ -191,6 +198,22 @@ public class GetBucketsAction extends Action<GetBucketsAction.Request, GetBucket
this.anomalyScore = anomalyScore; this.anomalyScore = anomalyScore;
} }
public String getSort() {
return sort;
}
public void setSort(String sort) {
this.sort = sort;
}
public boolean isDescending() {
return descending;
}
public void setDescending(boolean descending) {
this.descending = descending;
}
@Override @Override
public ActionRequestValidationException validate() { public ActionRequestValidationException validate() {
return null; return null;
@ -207,6 +230,10 @@ public class GetBucketsAction extends Action<GetBucketsAction.Request, GetBucket
end = in.readOptionalString(); end = in.readOptionalString();
anomalyScore = in.readOptionalDouble(); anomalyScore = in.readOptionalDouble();
pageParams = in.readOptionalWriteable(PageParams::new); pageParams = in.readOptionalWriteable(PageParams::new);
if (in.getVersion().after(Version.V_5_4_0)) {
sort = in.readString();
descending = in.readBoolean();
}
} }
@Override @Override
@ -220,6 +247,10 @@ public class GetBucketsAction extends Action<GetBucketsAction.Request, GetBucket
out.writeOptionalString(end); out.writeOptionalString(end);
out.writeOptionalDouble(anomalyScore); out.writeOptionalDouble(anomalyScore);
out.writeOptionalWriteable(pageParams); out.writeOptionalWriteable(pageParams);
if (out.getVersion().after(Version.V_5_4_0)) {
out.writeString(sort);
out.writeBoolean(descending);
}
} }
@Override @Override
@ -243,13 +274,15 @@ public class GetBucketsAction extends Action<GetBucketsAction.Request, GetBucket
if (anomalyScore != null) { if (anomalyScore != null) {
builder.field(ANOMALY_SCORE.getPreferredName(), anomalyScore); builder.field(ANOMALY_SCORE.getPreferredName(), anomalyScore);
} }
builder.field(SORT.getPreferredName(), sort);
builder.field(DESCENDING.getPreferredName(), descending);
builder.endObject(); builder.endObject();
return builder; return builder;
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(jobId, timestamp, expand, excludeInterim, anomalyScore, pageParams, start, end); return Objects.hash(jobId, timestamp, expand, excludeInterim, anomalyScore, pageParams, start, end, sort, descending);
} }
@Override @Override
@ -268,7 +301,9 @@ public class GetBucketsAction extends Action<GetBucketsAction.Request, GetBucket
Objects.equals(anomalyScore, other.anomalyScore) && Objects.equals(anomalyScore, other.anomalyScore) &&
Objects.equals(pageParams, other.pageParams) && Objects.equals(pageParams, other.pageParams) &&
Objects.equals(start, other.start) && Objects.equals(start, other.start) &&
Objects.equals(end, other.end); Objects.equals(end, other.end) &&
Objects.equals(sort, other.sort) &&
Objects.equals(descending, other.descending);
} }
} }
@ -362,7 +397,9 @@ public class GetBucketsAction extends Action<GetBucketsAction.Request, GetBucket
.includeInterim(request.excludeInterim == false) .includeInterim(request.excludeInterim == false)
.start(request.start) .start(request.start)
.end(request.end) .end(request.end)
.anomalyScoreThreshold(request.anomalyScore); .anomalyScoreThreshold(request.anomalyScore)
.sortField(request.sort)
.sortDescending(request.descending);
if (request.pageParams != null) { if (request.pageParams != null) {
query.from(request.pageParams.getFrom()) query.from(request.pageParams.getFrom())

View File

@ -98,7 +98,7 @@ public class GetRecordsAction extends Action<GetRecordsAction.Request, GetRecord
private PageParams pageParams = new PageParams(); private PageParams pageParams = new PageParams();
private double recordScoreFilter = 0.0; private double recordScoreFilter = 0.0;
private String sort = Influencer.INFLUENCER_SCORE.getPreferredName(); private String sort = Influencer.INFLUENCER_SCORE.getPreferredName();
private boolean descending = false; private boolean descending = true;
Request() { Request() {
} }

View File

@ -384,8 +384,13 @@ public class JobProvider {
searchSourceBuilder.query(boolQuery); searchSourceBuilder.query(boolQuery);
searchSourceBuilder.from(query.getFrom()); searchSourceBuilder.from(query.getFrom());
searchSourceBuilder.size(query.getSize()); searchSourceBuilder.size(query.getSize());
// If not using the default sort field (timestamp) add it as a secondary sort
if (Result.TIMESTAMP.getPreferredName().equals(query.getSortField()) == false) {
searchSourceBuilder.sort(Result.TIMESTAMP.getPreferredName(), query.isSortDescending() ? SortOrder.DESC : SortOrder.ASC);
}
searchRequest.source(searchSourceBuilder); searchRequest.source(searchSourceBuilder);
MultiSearchRequest mrequest = new MultiSearchRequest(); MultiSearchRequest mrequest = new MultiSearchRequest();
mrequest.indicesOptions(addIgnoreUnavailable(mrequest.indicesOptions())); mrequest.indicesOptions(addIgnoreUnavailable(mrequest.indicesOptions()));
mrequest.add(searchRequest); mrequest.add(searchRequest);

View File

@ -71,6 +71,11 @@ public class RestGetBucketsAction extends BaseRestHandler {
request.setAnomalyScore( request.setAnomalyScore(
Double.parseDouble(restRequest.param(GetBucketsAction.Request.ANOMALY_SCORE.getPreferredName(), "0.0"))); Double.parseDouble(restRequest.param(GetBucketsAction.Request.ANOMALY_SCORE.getPreferredName(), "0.0")));
} }
if (restRequest.hasParam(GetBucketsAction.Request.SORT.getPreferredName())) {
request.setSort(restRequest.param(GetBucketsAction.Request.SORT.getPreferredName()));
}
request.setDescending(restRequest.paramAsBoolean(GetBucketsAction.Request.DESCENDING.getPreferredName(),
request.isDescending()));
// single and multiple bucket options // single and multiple bucket options
request.setExpand(restRequest.paramAsBoolean(GetBucketsAction.Request.EXPAND.getPreferredName(), false)); request.setExpand(restRequest.paramAsBoolean(GetBucketsAction.Request.EXPAND.getPreferredName(), false));

View File

@ -42,14 +42,16 @@ public class RestGetRecordsAction extends BaseRestHandler {
request = new GetRecordsAction.Request(jobId); request = new GetRecordsAction.Request(jobId);
request.setStart(restRequest.param(GetRecordsAction.Request.START.getPreferredName())); request.setStart(restRequest.param(GetRecordsAction.Request.START.getPreferredName()));
request.setEnd(restRequest.param(GetRecordsAction.Request.END.getPreferredName())); request.setEnd(restRequest.param(GetRecordsAction.Request.END.getPreferredName()));
request.setExcludeInterim(restRequest.paramAsBoolean(GetRecordsAction.Request.EXCLUDE_INTERIM.getPreferredName(), false)); request.setExcludeInterim(restRequest.paramAsBoolean(GetRecordsAction.Request.EXCLUDE_INTERIM.getPreferredName(),
request.isExcludeInterim()));
request.setPageParams(new PageParams(restRequest.paramAsInt(PageParams.FROM.getPreferredName(), PageParams.DEFAULT_FROM), request.setPageParams(new PageParams(restRequest.paramAsInt(PageParams.FROM.getPreferredName(), PageParams.DEFAULT_FROM),
restRequest.paramAsInt(PageParams.SIZE.getPreferredName(), PageParams.DEFAULT_SIZE))); restRequest.paramAsInt(PageParams.SIZE.getPreferredName(), PageParams.DEFAULT_SIZE)));
request.setRecordScore( request.setRecordScore(
Double.parseDouble(restRequest.param(GetRecordsAction.Request.RECORD_SCORE_FILTER.getPreferredName(), "0.0"))); Double.parseDouble(restRequest.param(GetRecordsAction.Request.RECORD_SCORE_FILTER.getPreferredName(), "0.0")));
request.setSort(restRequest.param(GetRecordsAction.Request.SORT.getPreferredName(), request.setSort(restRequest.param(GetRecordsAction.Request.SORT.getPreferredName(),
AnomalyRecord.RECORD_SCORE.getPreferredName())); AnomalyRecord.RECORD_SCORE.getPreferredName()));
request.setDescending(restRequest.paramAsBoolean(GetRecordsAction.Request.DESCENDING.getPreferredName(), true)); request.setDescending(restRequest.paramAsBoolean(GetRecordsAction.Request.DESCENDING.getPreferredName(),
request.isDescending()));
} }
return channel -> client.execute(GetRecordsAction.INSTANCE, request, new RestToXContentListener<>(channel)); return channel -> client.execute(GetRecordsAction.INSTANCE, request, new RestToXContentListener<>(channel));

View File

@ -37,6 +37,10 @@ public class GetBucketActionRequestTests extends AbstractStreamableXContentTestC
int size = randomInt(maxSize); int size = randomInt(maxSize);
request.setPageParams(new PageParams(from, size)); request.setPageParams(new PageParams(from, size));
} }
if (randomBoolean()) {
request.setSort("anomaly_score");
}
request.setDescending(randomBoolean());
} }
if (randomBoolean()) { if (randomBoolean()) {
request.setExpand(randomBoolean()); request.setExpand(randomBoolean());

View File

@ -46,6 +46,14 @@
"anomaly_score": { "anomaly_score": {
"type": "double", "type": "double",
"description": "Filter for the most anomalous buckets" "description": "Filter for the most anomalous buckets"
},
"sort": {
"type": "string",
"description": "Sort buckets by a particular field"
},
"desc": {
"type": "boolean",
"description": "Set the sort direction"
} }
} }
}, },

View File

@ -23,6 +23,7 @@ setup:
"job_id": "jobs-get-result-buckets", "job_id": "jobs-get-result-buckets",
"result_type": "bucket", "result_type": "bucket",
"timestamp": "2016-06-01T00:00:00Z", "timestamp": "2016-06-01T00:00:00Z",
"anomaly_score": 90.0,
"bucket_span":1 "bucket_span":1
} }
@ -30,15 +31,29 @@ setup:
index: index:
index: .ml-anomalies-jobs-get-result-buckets index: .ml-anomalies-jobs-get-result-buckets
type: result type: result
id: "jobs-get-result-buckets_1464739200000_2" id: "jobs-get-result-buckets_1470009600000_2"
body: body:
{ {
"job_id": "jobs-get-result-buckets", "job_id": "jobs-get-result-buckets",
"result_type": "bucket", "result_type": "bucket",
"timestamp": "2016-08-01T00:00:00Z", "timestamp": "2016-08-01T00:00:00Z",
"anomaly_score": 60.0,
"bucket_span":1, "bucket_span":1,
"is_interim": true "is_interim": true
} }
- do:
index:
index: .ml-anomalies-jobs-get-result-buckets
type: result
id: "jobs-get-result-buckets_1470096000000_3"
body:
{
"job_id": "jobs-get-result-buckets",
"result_type": "bucket",
"timestamp": "2016-08-02T00:00:00Z",
"anomaly_score": 60.0,
"bucket_span":1,
}
- do: - do:
indices.refresh: indices.refresh:
@ -63,15 +78,19 @@ setup:
xpack.ml.get_buckets: xpack.ml.get_buckets:
job_id: "jobs-get-result-buckets" job_id: "jobs-get-result-buckets"
- match: { count: 2 } - match: { count: 3 }
- match: { buckets.0.timestamp: 1464739200000 } - match: { buckets.0.timestamp: 1464739200000 }
- match: { buckets.0.job_id: jobs-get-result-buckets} - match: { buckets.0.job_id: jobs-get-result-buckets}
- match: { buckets.0.result_type: bucket} - match: { buckets.0.result_type: bucket}
- match: { buckets.1.timestamp: 1470009600000 } - match: { buckets.1.timestamp: 1470009600000 }
- match: { buckets.1.job_id: jobs-get-result-buckets} - match: { buckets.1.job_id: jobs-get-result-buckets}
- match: { buckets.1.result_type: bucket} - match: { buckets.1.result_type: bucket}
- match: { buckets.2.timestamp: 1470096000000 }
- match: { buckets.2.job_id: jobs-get-result-buckets}
- match: { buckets.2.result_type: bucket}
- is_false: buckets.0.partition_scores - is_false: buckets.0.partition_scores
- is_false: buckets.1.partition_scores - is_false: buckets.1.partition_scores
- is_false: buckets.2.partition_scores
--- ---
"Test get buckets given exclude_interim is false": "Test get buckets given exclude_interim is false":
@ -80,7 +99,7 @@ setup:
job_id: "jobs-get-result-buckets" job_id: "jobs-get-result-buckets"
exclude_interim: false exclude_interim: false
- match: { count: 2 } - match: { count: 3 }
--- ---
"Test get buckets given exclude_interim is true": "Test get buckets given exclude_interim is true":
@ -89,9 +108,11 @@ setup:
job_id: "jobs-get-result-buckets" job_id: "jobs-get-result-buckets"
exclude_interim: true exclude_interim: true
- match: { count: 1 } - match: { count: 2 }
- match: { buckets.0.timestamp: 1464739200000 } - match: { buckets.0.timestamp: 1464739200000 }
- match: { buckets.0.is_interim: false } - match: { buckets.0.is_interim: false }
- match: { buckets.1.timestamp: 1470096000000 }
- match: { buckets.1.is_interim: false }
--- ---
"Test result single bucket api": "Test result single bucket api":
@ -189,3 +210,33 @@ setup:
catch: missing catch: missing
xpack.ml.get_buckets: xpack.ml.get_buckets:
job_id: "non-existent-job" job_id: "non-existent-job"
---
"Test get buckets with sort field and secondary sort by time":
- do:
xpack.ml.get_buckets:
job_id: "jobs-get-result-buckets"
sort: anomaly_score
- match: { count: 3 }
- match: { buckets.0.anomaly_score: 60.0 }
- match: { buckets.0.timestamp: 1470009600000 }
- match: { buckets.1.anomaly_score: 60.0 }
- match: { buckets.1.timestamp: 1470096000000 }
- match: { buckets.2.anomaly_score: 90.0}
- match: { buckets.2.timestamp: 1464739200000 }
- do:
xpack.ml.get_buckets:
job_id: "jobs-get-result-buckets"
sort: anomaly_score
desc: true
- match: { count: 3 }
- match: { buckets.0.anomaly_score: 90.0 }
- match: { buckets.0.timestamp: 1464739200000 }
- match: { buckets.1.anomaly_score: 60.0}
- match: { buckets.1.timestamp: 1470096000000 }
- match: { buckets.2.anomaly_score: 60.0}
- match: { buckets.2.timestamp: 1470009600000 }