Skip to content

Commit acd4068

Browse files
kderussoelasticsearchmachine
andauthored
Add support for extended search usage telemetry (#135306)
* Add extended telemetry * Cleanup * Update docs/changelog/135306.yaml * [CI] Auto commit changes from spotless * [CI] Update transport version definitions * Fix serialization * Add tests * Update transport * Tests * [CI] Auto commit changes from spotless * Yaml * Fix error in SearchUsageStatsTests due to randomization * Ensure mutation in serialization tests * [CI] Auto commit changes from spotless * Revert "Yaml" This reverts commit 9f4e40d. * [CI] Update transport version definitions * Remove old debug breakpoint * PR feedback: Refactor extended stats into objects instead of forcing to Map<String,Long> * Make ExtendedSeearchUsageMetric a NamedWriteable * Regenerate transport version * Fix serialization * [CI] Auto commit changes from spotless * PR Feedback * [CI] Auto commit changes from spotless * PR feedback * Regenerate transport version --------- Co-authored-by: elasticsearchmachine <[email protected]>
1 parent 89bb134 commit acd4068

File tree

23 files changed

+546
-20
lines changed

23 files changed

+546
-20
lines changed

docs/changelog/135306.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 135306
2+
summary: Add support for extended search usage telemetry
3+
area: Relevance
4+
type: enhancement
5+
issues: []

server/src/main/java/org/elasticsearch/action/ActionModule.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@
6969
import org.elasticsearch.action.admin.cluster.snapshots.status.TransportSnapshotsStatusAction;
7070
import org.elasticsearch.action.admin.cluster.state.ClusterStateAction;
7171
import org.elasticsearch.action.admin.cluster.state.TransportClusterStateAction;
72+
import org.elasticsearch.action.admin.cluster.stats.ExtendedSearchUsageLongCounter;
73+
import org.elasticsearch.action.admin.cluster.stats.ExtendedSearchUsageMetric;
7274
import org.elasticsearch.action.admin.cluster.stats.TransportClusterStatsAction;
7375
import org.elasticsearch.action.admin.cluster.storedscripts.GetScriptContextAction;
7476
import org.elasticsearch.action.admin.cluster.storedscripts.GetScriptLanguageAction;
@@ -1078,4 +1080,14 @@ public RestController getRestController() {
10781080
public ReservedClusterStateService getReservedClusterStateService() {
10791081
return reservedClusterStateService;
10801082
}
1083+
1084+
public List<NamedWriteableRegistry.Entry> getNamedWriteables() {
1085+
return List.of(
1086+
new NamedWriteableRegistry.Entry(
1087+
ExtendedSearchUsageMetric.class,
1088+
ExtendedSearchUsageLongCounter.NAME,
1089+
ExtendedSearchUsageLongCounter::new
1090+
)
1091+
);
1092+
}
10811093
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.action.admin.cluster.stats;
11+
12+
import org.elasticsearch.common.io.stream.StreamInput;
13+
import org.elasticsearch.common.io.stream.StreamOutput;
14+
import org.elasticsearch.xcontent.XContentBuilder;
15+
16+
import java.io.IOException;
17+
import java.util.Collections;
18+
import java.util.Map;
19+
import java.util.Objects;
20+
21+
/**
22+
* An {@link ExtendedSearchUsageMetric} implementation that holds a map of values to counts.
23+
*/
24+
public class ExtendedSearchUsageLongCounter implements ExtendedSearchUsageMetric<ExtendedSearchUsageLongCounter> {
25+
26+
public static final String NAME = "extended_search_usage_long_counter";
27+
28+
private final Map<String, Long> values;
29+
30+
public ExtendedSearchUsageLongCounter(Map<String, Long> values) {
31+
this.values = values;
32+
}
33+
34+
public ExtendedSearchUsageLongCounter(StreamInput in) throws IOException {
35+
this.values = in.readMap(StreamInput::readString, StreamInput::readLong);
36+
}
37+
38+
public Map<String, Long> getValues() {
39+
return Collections.unmodifiableMap(values);
40+
}
41+
42+
@Override
43+
public ExtendedSearchUsageLongCounter merge(ExtendedSearchUsageMetric<?> other) {
44+
assert other instanceof ExtendedSearchUsageLongCounter;
45+
ExtendedSearchUsageLongCounter otherLongCounter = (ExtendedSearchUsageLongCounter) other;
46+
Map<String, Long> values = new java.util.HashMap<>(this.values);
47+
otherLongCounter.getValues().forEach((key, otherValue) -> { values.merge(key, otherValue, Long::sum); });
48+
return new ExtendedSearchUsageLongCounter(values);
49+
}
50+
51+
@Override
52+
public void writeTo(StreamOutput out) throws IOException {
53+
out.writeMap(values, StreamOutput::writeString, StreamOutput::writeLong);
54+
}
55+
56+
@Override
57+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
58+
for (String key : values.keySet()) {
59+
builder.field(key, values.get(key));
60+
}
61+
return builder;
62+
}
63+
64+
@Override
65+
public boolean equals(Object o) {
66+
if (this == o) return true;
67+
if (o == null || getClass() != o.getClass()) return false;
68+
ExtendedSearchUsageLongCounter that = (ExtendedSearchUsageLongCounter) o;
69+
return Objects.equals(values, that.values);
70+
}
71+
72+
@Override
73+
public int hashCode() {
74+
return values.hashCode();
75+
}
76+
77+
@Override
78+
public String getWriteableName() {
79+
return NAME;
80+
}
81+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.action.admin.cluster.stats;
11+
12+
import org.elasticsearch.common.io.stream.NamedWriteable;
13+
import org.elasticsearch.xcontent.ToXContentFragment;
14+
15+
/**
16+
* Represents a metric colleged as part of {@link ExtendedSearchUsageStats}.
17+
*/
18+
public interface ExtendedSearchUsageMetric<T extends ExtendedSearchUsageMetric<T>> extends NamedWriteable, ToXContentFragment {
19+
20+
/**
21+
* Merges two equivalent metrics together for statistical reporting.
22+
* @param other Another {@link ExtendedSearchUsageMetric}.
23+
* @return ExtendedSearchUsageMetric The merged metric.
24+
*/
25+
T merge(ExtendedSearchUsageMetric<?> other);
26+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.action.admin.cluster.stats;
11+
12+
import org.elasticsearch.common.Strings;
13+
import org.elasticsearch.common.io.stream.StreamInput;
14+
import org.elasticsearch.common.io.stream.StreamOutput;
15+
import org.elasticsearch.common.io.stream.Writeable;
16+
import org.elasticsearch.usage.SearchUsage;
17+
import org.elasticsearch.xcontent.ToXContent;
18+
import org.elasticsearch.xcontent.XContentBuilder;
19+
20+
import java.io.IOException;
21+
import java.util.Collections;
22+
import java.util.HashMap;
23+
import java.util.Map;
24+
import java.util.Objects;
25+
26+
/**
27+
* Provides extended statistics for {@link SearchUsage} beyond the basic counts provided in {@link SearchUsageStats}.
28+
*/
29+
public class ExtendedSearchUsageStats implements Writeable, ToXContent {
30+
31+
/**
32+
* A map of categories to extended data. Categories correspond to a high-level search usage statistic,
33+
* e.g. `queries`, `rescorers`, `sections`, `retrievers`.
34+
*
35+
* Extended data is further segmented by name, e.g., collecting specific statistics for certain retrievers only.
36+
*/
37+
private final Map<String, Map<String, ExtendedSearchUsageMetric<?>>> categorizedExtendedData;
38+
39+
public static final ExtendedSearchUsageStats EMPTY = new ExtendedSearchUsageStats();
40+
41+
public ExtendedSearchUsageStats() {
42+
this.categorizedExtendedData = new HashMap<>();
43+
}
44+
45+
public ExtendedSearchUsageStats(Map<String, Map<String, ExtendedSearchUsageMetric<?>>> categorizedExtendedData) {
46+
this.categorizedExtendedData = categorizedExtendedData;
47+
}
48+
49+
public ExtendedSearchUsageStats(StreamInput in) throws IOException {
50+
this.categorizedExtendedData = in.readMap(
51+
StreamInput::readString,
52+
i -> i.readMap(StreamInput::readString, p -> p.readNamedWriteable(ExtendedSearchUsageMetric.class))
53+
);
54+
}
55+
56+
public Map<String, Map<String, ExtendedSearchUsageMetric<?>>> getCategorizedExtendedData() {
57+
return Collections.unmodifiableMap(categorizedExtendedData);
58+
}
59+
60+
@Override
61+
public void writeTo(StreamOutput out) throws IOException {
62+
out.writeMap(
63+
categorizedExtendedData,
64+
StreamOutput::writeString,
65+
(o, v) -> o.writeMap(v, StreamOutput::writeString, (p, q) -> out.writeNamedWriteable(q))
66+
);
67+
}
68+
69+
public void merge(ExtendedSearchUsageStats other) {
70+
other.categorizedExtendedData.forEach((key, otherMap) -> {
71+
categorizedExtendedData.merge(key, otherMap, (existingMap, newMap) -> {
72+
Map<String, ExtendedSearchUsageMetric<?>> mergedMap = new HashMap<>(existingMap);
73+
newMap.forEach(
74+
(innerKey, innerValue) -> { mergedMap.merge(innerKey, innerValue, (existing, incoming) -> (existing).merge(incoming)); }
75+
);
76+
return mergedMap;
77+
});
78+
});
79+
}
80+
81+
@Override
82+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
83+
84+
builder.startObject();
85+
for (String category : categorizedExtendedData.keySet()) {
86+
builder.startObject(category);
87+
Map<String, ExtendedSearchUsageMetric<?>> names = categorizedExtendedData.get(category);
88+
for (String name : names.keySet()) {
89+
builder.startObject(name);
90+
names.get(name).toXContent(builder, params);
91+
builder.endObject();
92+
}
93+
builder.endObject();
94+
}
95+
builder.endObject();
96+
return builder;
97+
}
98+
99+
@Override
100+
public boolean equals(Object o) {
101+
if (this == o) return true;
102+
if (o == null || getClass() != o.getClass()) return false;
103+
ExtendedSearchUsageStats that = (ExtendedSearchUsageStats) o;
104+
return Objects.equals(categorizedExtendedData, that.categorizedExtendedData);
105+
}
106+
107+
@Override
108+
public int hashCode() {
109+
return Objects.hash(categorizedExtendedData);
110+
}
111+
112+
@Override
113+
public String toString() {
114+
return Strings.toString(this, true, true);
115+
}
116+
}

server/src/main/java/org/elasticsearch/action/admin/cluster/stats/SearchUsageStats.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
package org.elasticsearch.action.admin.cluster.stats;
1111

12+
import org.elasticsearch.TransportVersion;
1213
import org.elasticsearch.common.Strings;
1314
import org.elasticsearch.common.io.stream.StreamInput;
1415
import org.elasticsearch.common.io.stream.StreamOutput;
@@ -31,11 +32,15 @@
3132
* accumulate stats for the entire cluster and return them as part of the {@link ClusterStatsResponse}.
3233
*/
3334
public final class SearchUsageStats implements Writeable, ToXContentFragment {
35+
36+
static final TransportVersion EXTENDED_SEARCH_USAGE_TELEMETRY = TransportVersion.fromName("extended_search_usage_telemetry");
37+
3438
private long totalSearchCount;
3539
private final Map<String, Long> queries;
3640
private final Map<String, Long> rescorers;
3741
private final Map<String, Long> sections;
3842
private final Map<String, Long> retrievers;
43+
private final ExtendedSearchUsageStats extendedSearchUsageStats;
3944

4045
/**
4146
* Creates a new empty stats instance, that will get additional stats added through {@link #add(SearchUsageStats)}
@@ -46,6 +51,7 @@ public SearchUsageStats() {
4651
this.sections = new HashMap<>();
4752
this.rescorers = new HashMap<>();
4853
this.retrievers = new HashMap<>();
54+
this.extendedSearchUsageStats = ExtendedSearchUsageStats.EMPTY;
4955
}
5056

5157
/**
@@ -57,13 +63,15 @@ public SearchUsageStats(
5763
Map<String, Long> rescorers,
5864
Map<String, Long> sections,
5965
Map<String, Long> retrievers,
66+
ExtendedSearchUsageStats extendedSearchUsageStats,
6067
long totalSearchCount
6168
) {
6269
this.totalSearchCount = totalSearchCount;
6370
this.queries = queries;
6471
this.sections = sections;
6572
this.rescorers = rescorers;
6673
this.retrievers = retrievers;
74+
this.extendedSearchUsageStats = extendedSearchUsageStats;
6775
}
6876

6977
public SearchUsageStats(StreamInput in) throws IOException {
@@ -72,6 +80,9 @@ public SearchUsageStats(StreamInput in) throws IOException {
7280
this.totalSearchCount = in.readVLong();
7381
this.rescorers = in.getTransportVersion().onOrAfter(V_8_12_0) ? in.readMap(StreamInput::readLong) : Map.of();
7482
this.retrievers = in.getTransportVersion().onOrAfter(V_8_16_0) ? in.readMap(StreamInput::readLong) : Map.of();
83+
this.extendedSearchUsageStats = in.getTransportVersion().supports(EXTENDED_SEARCH_USAGE_TELEMETRY)
84+
? new ExtendedSearchUsageStats(in)
85+
: ExtendedSearchUsageStats.EMPTY;
7586
}
7687

7788
@Override
@@ -86,6 +97,9 @@ public void writeTo(StreamOutput out) throws IOException {
8697
if (out.getTransportVersion().onOrAfter(V_8_16_0)) {
8798
out.writeMap(retrievers, StreamOutput::writeLong);
8899
}
100+
if (out.getTransportVersion().supports(EXTENDED_SEARCH_USAGE_TELEMETRY)) {
101+
extendedSearchUsageStats.writeTo(out);
102+
}
89103
}
90104

91105
/**
@@ -96,6 +110,7 @@ public void add(SearchUsageStats stats) {
96110
stats.rescorers.forEach((rescorer, count) -> rescorers.merge(rescorer, count, Long::sum));
97111
stats.sections.forEach((query, count) -> sections.merge(query, count, Long::sum));
98112
stats.retrievers.forEach((query, count) -> retrievers.merge(query, count, Long::sum));
113+
this.extendedSearchUsageStats.merge(stats.extendedSearchUsageStats);
99114
this.totalSearchCount += stats.totalSearchCount;
100115
}
101116

@@ -112,6 +127,8 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
112127
builder.map(sections);
113128
builder.field("retrievers");
114129
builder.map(retrievers);
130+
builder.field("extended");
131+
extendedSearchUsageStats.toXContent(builder, params);
115132
}
116133
builder.endObject();
117134
return builder;
@@ -133,6 +150,10 @@ public Map<String, Long> getRetrieversUsage() {
133150
return Collections.unmodifiableMap(retrievers);
134151
}
135152

153+
public ExtendedSearchUsageStats getExtendedSearchUsage() {
154+
return extendedSearchUsageStats;
155+
}
156+
136157
public long getTotalSearchCount() {
137158
return totalSearchCount;
138159
}
@@ -150,12 +171,13 @@ public boolean equals(Object o) {
150171
&& queries.equals(that.queries)
151172
&& rescorers.equals(that.rescorers)
152173
&& sections.equals(that.sections)
153-
&& retrievers.equals(that.retrievers);
174+
&& retrievers.equals(that.retrievers)
175+
&& extendedSearchUsageStats.equals(that.extendedSearchUsageStats);
154176
}
155177

156178
@Override
157179
public int hashCode() {
158-
return Objects.hash(totalSearchCount, queries, rescorers, sections, retrievers);
180+
return Objects.hash(totalSearchCount, queries, rescorers, sections, retrievers, extendedSearchUsageStats);
159181
}
160182

161183
@Override

server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterStatsAction.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ public class RestClusterStatsAction extends BaseRestHandler {
3434
"verbose-dense-vector-mapping-stats",
3535
"ccs-stats",
3636
"retrievers-usage-stats",
37-
"esql-stats"
37+
"esql-stats",
38+
"extended-search-usage-stats"
3839
);
3940
private static final Set<String> SUPPORTED_QUERY_PARAMETERS = Set.of("include_remotes", "nodeId", REST_TIMEOUT_PARAM);
4041

server/src/main/java/org/elasticsearch/rest/action/search/SearchCapabilities.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ private SearchCapabilities() {}
5959
private static final String KNN_FILTER_ON_NESTED_FIELDS_CAPABILITY = "knn_filter_on_nested_fields";
6060
private static final String BUCKET_SCRIPT_PARENT_MULTI_BUCKET_ERROR = "bucket_script_parent_multi_bucket_error";
6161
private static final String EXCLUDE_SOURCE_VECTORS_SETTING = "exclude_source_vectors_setting";
62+
private static final String CLUSTER_STATS_EXTENDED_USAGE = "extended-search-usage-stats";
6263

6364
public static final Set<String> CAPABILITIES;
6465
static {
@@ -88,6 +89,7 @@ private SearchCapabilities() {}
8889
capabilities.add(KNN_FILTER_ON_NESTED_FIELDS_CAPABILITY);
8990
capabilities.add(BUCKET_SCRIPT_PARENT_MULTI_BUCKET_ERROR);
9091
capabilities.add(EXCLUDE_SOURCE_VECTORS_SETTING);
92+
capabilities.add(CLUSTER_STATS_EXTENDED_USAGE);
9193
CAPABILITIES = Set.copyOf(capabilities);
9294
}
9395
}

0 commit comments

Comments
 (0)