Skip to content

Commit 344f695

Browse files
authored
feat: add support for Search statistics (#2787)
1 parent 07760b7 commit 344f695

File tree

5 files changed

+285
-0
lines changed

5 files changed

+285
-0
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.bigquery;
18+
19+
import com.google.api.services.bigquery.model.TableReference;
20+
import com.google.auto.value.AutoValue;
21+
import java.io.Serializable;
22+
import javax.annotation.Nullable;
23+
24+
/** Represents Reason of why the index was not used in a SQL search. */
25+
@AutoValue
26+
public abstract class IndexUnusedReason implements Serializable {
27+
28+
@AutoValue.Builder
29+
public abstract static class Builder {
30+
31+
/**
32+
* Specifies the name of the unused search index, if available.
33+
*
34+
* @param indexName indexName or {@code null} for none
35+
*/
36+
public abstract Builder setIndexName(String indexName);
37+
38+
/**
39+
* Specifies the high-level reason for the scenario when no search index was used.
40+
*
41+
* @param code code or {@code null} for none
42+
*/
43+
public abstract Builder setCode(String code);
44+
45+
/**
46+
* Free form human-readable reason for the scenario when no search index was used.
47+
*
48+
* @param message message or {@code null} for none
49+
*/
50+
public abstract Builder setMessage(String message);
51+
52+
/**
53+
* Specifies the base table involved in the reason that no search index was used.
54+
*
55+
* @param tableReference tableReference or {@code null} for none
56+
*/
57+
public abstract Builder setBaseTable(TableReference tableReference);
58+
59+
/** Creates a @code IndexUnusedReason} object. */
60+
public abstract IndexUnusedReason build();
61+
}
62+
63+
public abstract Builder toBuilder();
64+
65+
public static Builder newBuilder() {
66+
return new AutoValue_IndexUnusedReason.Builder();
67+
}
68+
69+
/**
70+
* Returns the name of the unused search index, if available.
71+
*
72+
* @return value or {@code null} for none
73+
*/
74+
@Nullable
75+
public abstract String getIndexName();
76+
77+
/**
78+
* Returns the high-level reason for the scenario when no search index was used.
79+
*
80+
* @return value or {@code null} for none
81+
*/
82+
@Nullable
83+
public abstract String getCode();
84+
85+
/**
86+
* Returns free form human-readable reason for the scenario when no search index was used.
87+
*
88+
* @return value or {@code null} for none
89+
*/
90+
@Nullable
91+
public abstract String getMessage();
92+
93+
/**
94+
* Returns the base table involved in the reason that no search index was used.
95+
*
96+
* @return value or {@code null} for none
97+
*/
98+
@Nullable
99+
public abstract TableReference getBaseTable();
100+
101+
com.google.api.services.bigquery.model.IndexUnusedReason toPb() {
102+
com.google.api.services.bigquery.model.IndexUnusedReason indexUnusedReason =
103+
new com.google.api.services.bigquery.model.IndexUnusedReason();
104+
if (getIndexName() != null) {
105+
indexUnusedReason.setIndexName(indexUnusedReason.getIndexName());
106+
}
107+
if (getCode() != null) {
108+
indexUnusedReason.setCode(indexUnusedReason.getCode());
109+
}
110+
if (getMessage() != null) {
111+
indexUnusedReason.setMessage(indexUnusedReason.getMessage());
112+
}
113+
if (getBaseTable() != null) {
114+
indexUnusedReason.setBaseTable(indexUnusedReason.getBaseTable());
115+
}
116+
return indexUnusedReason;
117+
}
118+
119+
static IndexUnusedReason fromPb(
120+
com.google.api.services.bigquery.model.IndexUnusedReason indexUnusedReason) {
121+
Builder builder = newBuilder();
122+
if (indexUnusedReason.getIndexName() != null) {
123+
builder.setIndexName(indexUnusedReason.getIndexName());
124+
}
125+
if (indexUnusedReason.getCode() != null) {
126+
builder.setCode(indexUnusedReason.getCode());
127+
}
128+
if (indexUnusedReason.getMessage() != null) {
129+
builder.setMessage(indexUnusedReason.getMessage());
130+
}
131+
if (indexUnusedReason.getBaseTable() != null) {
132+
builder.setBaseTable(indexUnusedReason.getBaseTable());
133+
}
134+
return builder.build();
135+
}
136+
}

google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/JobStatistics.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ public static class QueryStatistics extends JobStatistics {
340340
private final List<QueryStage> queryPlan;
341341
private final List<TimelineSample> timeline;
342342
private final Schema schema;
343+
private final SearchStats searchStats;
343344
private final List<QueryParameter> queryParameters;
344345

345346
/**
@@ -424,6 +425,7 @@ static final class Builder extends JobStatistics.Builder<QueryStatistics, Builde
424425
private List<TimelineSample> timeline;
425426
private Schema schema;
426427
private List<QueryParameter> queryParameters;
428+
private SearchStats searchStats;
427429

428430
private Builder() {}
429431

@@ -471,6 +473,9 @@ private Builder(com.google.api.services.bigquery.model.JobStatistics statisticsP
471473
if (statisticsPb.getQuery().getSchema() != null) {
472474
this.schema = Schema.fromPb(statisticsPb.getQuery().getSchema());
473475
}
476+
if (statisticsPb.getQuery().getSearchStatistics() != null) {
477+
this.searchStats = SearchStats.fromPb(statisticsPb.getQuery().getSearchStatistics());
478+
}
474479
if (statisticsPb.getQuery().getDmlStats() != null) {
475480
this.dmlStats = DmlStats.fromPb(statisticsPb.getQuery().getDmlStats());
476481
}
@@ -572,6 +577,11 @@ Builder setSchema(Schema schema) {
572577
return self();
573578
}
574579

580+
Builder setSearchStats(SearchStats searchStats) {
581+
this.searchStats = searchStats;
582+
return self();
583+
}
584+
575585
Builder setQueryParameters(List<QueryParameter> queryParameters) {
576586
this.queryParameters = queryParameters;
577587
return self();
@@ -603,6 +613,7 @@ private QueryStatistics(Builder builder) {
603613
this.queryPlan = builder.queryPlan;
604614
this.timeline = builder.timeline;
605615
this.schema = builder.schema;
616+
this.searchStats = builder.searchStats;
606617
this.queryParameters = builder.queryParameters;
607618
}
608619

@@ -724,6 +735,15 @@ public Schema getSchema() {
724735
return schema;
725736
}
726737

738+
/**
739+
* Statistics for a search query. Populated as part of JobStatistics2. Provides information
740+
* about how indexes are used in search queries. If an index is not used, you can retrieve
741+
* debugging information about the reason why.
742+
*/
743+
public SearchStats getSearchStats() {
744+
return searchStats;
745+
}
746+
727747
/**
728748
* Standard SQL only: Returns a list of undeclared query parameters detected during a dry run
729749
* validation.
@@ -743,6 +763,7 @@ ToStringHelper toStringHelper() {
743763
.add("queryPlan", queryPlan)
744764
.add("timeline", timeline)
745765
.add("schema", schema)
766+
.add("searchStats", searchStats)
746767
.add("queryParameters", queryParameters);
747768
}
748769

@@ -765,6 +786,7 @@ public final int hashCode() {
765786
totalBytesProcessed,
766787
queryPlan,
767788
schema,
789+
searchStats,
768790
queryParameters);
769791
}
770792

@@ -807,6 +829,9 @@ com.google.api.services.bigquery.model.JobStatistics toPb() {
807829
if (schema != null) {
808830
queryStatisticsPb.setSchema(schema.toPb());
809831
}
832+
if (searchStats != null) {
833+
queryStatisticsPb.setSearchStatistics(searchStats.toPb());
834+
}
810835
if (queryParameters != null) {
811836
queryStatisticsPb.setUndeclaredQueryParameters(queryParameters);
812837
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.bigquery;
18+
19+
import com.google.api.services.bigquery.model.SearchStatistics;
20+
import com.google.auto.value.AutoValue;
21+
import java.io.Serializable;
22+
import java.util.List;
23+
import java.util.stream.Collectors;
24+
import javax.annotation.Nullable;
25+
26+
/** Represents Search statistics information of a search query. */
27+
@AutoValue
28+
public abstract class SearchStats implements Serializable {
29+
30+
@AutoValue.Builder
31+
public abstract static class Builder {
32+
33+
/**
34+
* Specifies index usage mode for the query.
35+
*
36+
* @param indexUsageMode, has three modes UNUSED, PARTIALLY_USED, and FULLY_USED
37+
*/
38+
public abstract Builder setIndexUsageMode(String indexUsageMode);
39+
40+
/**
41+
* When index_usage_mode is UNUSED or PARTIALLY_USED, this field explains why index was not used
42+
* in all or part of the search query. If index_usage_mode is FULLY_USED, this field is not
43+
* populated.
44+
*
45+
* @param indexUnusedReasons
46+
*/
47+
public abstract Builder setIndexUnusedReasons(List<IndexUnusedReason> indexUnusedReasons);
48+
49+
/** Creates a @code SearchStats} object. */
50+
public abstract SearchStats build();
51+
}
52+
53+
public abstract Builder toBuilder();
54+
55+
public static Builder newBuilder() {
56+
return new AutoValue_SearchStats.Builder();
57+
}
58+
59+
@Nullable
60+
public abstract String getIndexUsageMode();
61+
62+
@Nullable
63+
public abstract List<IndexUnusedReason> getIndexUnusedReasons();
64+
65+
SearchStatistics toPb() {
66+
SearchStatistics searchStatistics = new SearchStatistics();
67+
if (getIndexUsageMode() != null) {
68+
searchStatistics.setIndexUsageMode(getIndexUsageMode());
69+
}
70+
if (getIndexUnusedReasons() != null) {
71+
searchStatistics.setIndexUnusedReason(
72+
getIndexUnusedReasons().stream()
73+
.map(IndexUnusedReason::toPb)
74+
.collect(Collectors.toList()));
75+
}
76+
return searchStatistics;
77+
}
78+
79+
static SearchStats fromPb(SearchStatistics searchStatistics) {
80+
Builder builder = newBuilder();
81+
if (searchStatistics.getIndexUsageMode() != null) {
82+
builder.setIndexUsageMode(searchStatistics.getIndexUsageMode());
83+
}
84+
if (searchStatistics.getIndexUnusedReason() != null) {
85+
builder.setIndexUnusedReasons(
86+
searchStatistics.getIndexUnusedReason().stream()
87+
.map(IndexUnusedReason::fromPb)
88+
.collect(Collectors.toList()));
89+
}
90+
return builder.build();
91+
}
92+
}

google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/JobStatisticsTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ public class JobStatisticsTest {
159159
ImmutableList.of(TIMELINE_SAMPLE1, TIMELINE_SAMPLE2);
160160
private static final List<QueryStage> QUERY_PLAN = ImmutableList.of(QUERY_STAGE);
161161
private static final Schema SCHEMA = Schema.of(Field.of("column", LegacySQLTypeName.DATETIME));
162+
private static final String UNUSED_INDEX_USAGE_MODE = "UNUSED";
163+
private static final SearchStats SEARCH_STATS =
164+
SearchStats.newBuilder().setIndexUsageMode(UNUSED_INDEX_USAGE_MODE).build();
162165
private static final QueryStatistics QUERY_STATISTICS =
163166
QueryStatistics.newBuilder()
164167
.setCreationTimestamp(CREATION_TIME)
@@ -182,6 +185,7 @@ public class JobStatisticsTest {
182185
.setQueryPlan(QUERY_PLAN)
183186
.setTimeline(TIMELINE)
184187
.setSchema(SCHEMA)
188+
.setSearchStats(SEARCH_STATS)
185189
.build();
186190
private static final QueryStatistics QUERY_STATISTICS_INCOMPLETE =
187191
QueryStatistics.newBuilder()
@@ -190,6 +194,7 @@ public class JobStatisticsTest {
190194
.setStartTime(START_TIME)
191195
.setBillingTier(BILLING_TIER)
192196
.setCacheHit(CACHE_HIT)
197+
.setSearchStats(SEARCH_STATS)
193198
.build();
194199
private static final ScriptStackFrame STATEMENT_STACK_FRAME =
195200
ScriptStackFrame.newBuilder()
@@ -407,6 +412,8 @@ private void compareQueryStatistics(QueryStatistics expected, QueryStatistics va
407412
assertEquals(expected.getQueryPlan(), value.getQueryPlan());
408413
assertEquals(expected.getReferencedTables(), value.getReferencedTables());
409414
assertEquals(expected.getSchema(), value.getSchema());
415+
assertEquals(
416+
expected.getSearchStats().getIndexUsageMode(), value.getSearchStats().getIndexUsageMode());
410417
assertEquals(expected.getStatementType(), value.getStatementType());
411418
assertEquals(expected.getTimeline(), value.getTimeline());
412419
}

google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/it/ITBigQueryTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5088,6 +5088,31 @@ public void testQueryJobWithLabels() throws InterruptedException, TimeoutExcepti
50885088
}
50895089
}
50905090

5091+
@Test
5092+
public void testQueryJobWithSearchReturnsSearchStatistics() throws InterruptedException {
5093+
String tableName = "test_query_job_table";
5094+
String query =
5095+
"SELECT * FROM "
5096+
+ TABLE_ID.getTable()
5097+
+ " WHERE search(StringField, \"stringValue\")";
5098+
TableId destinationTable = TableId.of(DATASET, tableName);
5099+
try {
5100+
QueryJobConfiguration configuration =
5101+
QueryJobConfiguration.newBuilder(query)
5102+
.setDefaultDataset(DatasetId.of(DATASET))
5103+
.setDestinationTable(destinationTable)
5104+
.build();
5105+
Job remoteJob = bigquery.create(JobInfo.of(configuration));
5106+
remoteJob = remoteJob.waitFor();
5107+
assertNull(remoteJob.getStatus().getError());
5108+
JobStatistics.QueryStatistics stats = remoteJob.getStatistics();
5109+
assertNotNull(stats.getSearchStats());
5110+
assertEquals(stats.getSearchStats().getIndexUsageMode(), "UNUSED");
5111+
} finally {
5112+
bigquery.delete(destinationTable);
5113+
}
5114+
}
5115+
50915116
/* TODO(prasmish): replicate the entire test case for executeSelect */
50925117
@Test
50935118
public void testQueryJobWithRangePartitioning() throws InterruptedException {

0 commit comments

Comments
 (0)