Skip to content

Commit ffa78b0

Browse files
danotorreyclaude
andcommitted
Add effective query to search explain results (#14552)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 62074ed commit ffa78b0

8 files changed

Lines changed: 161 additions & 8 deletions

File tree

changelog/unreleased/issue-14552.toml

Lines changed: 0 additions & 4 deletions
This file was deleted.

graylog2-server/src/main/java/org/graylog/plugins/views/search/ExplainResults.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package org.graylog.plugins.views.search;
1818

19+
import com.fasterxml.jackson.annotation.JsonInclude;
1920
import org.graylog.plugins.views.search.errors.SearchError;
2021
import org.graylog2.indexer.indexset.MongoIndexSet;
2122
import org.graylog2.indexer.ranges.IndexRange;
@@ -32,7 +33,15 @@ public record SearchResult(Map<String, QueryExplainResult> queries) {
3233
public record QueryExplainResult(Map<String, ExplainResult> searchTypes) {
3334
}
3435

35-
public record ExplainResult(String queryString, Set<IndexRangeResult> searchedIndexRanges) {
36+
public record ExplainResult(String queryString, Set<IndexRangeResult> searchedIndexRanges,
37+
@JsonInclude(JsonInclude.Include.NON_NULL) String effectiveQuery) {
38+
public ExplainResult(String queryString, Set<IndexRangeResult> searchedIndexRanges) {
39+
this(queryString, searchedIndexRanges, null);
40+
}
41+
42+
public ExplainResult withEffectiveQuery(final String effectiveQuery) {
43+
return new ExplainResult(queryString, searchedIndexRanges, effectiveQuery);
44+
}
3645
}
3746

3847
public record IndexRangeResult(String indexName, long begin, long end, boolean isWarmTiered,

graylog2-server/src/main/java/org/graylog/plugins/views/search/engine/QueryEngine.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
import org.graylog.plugins.views.search.QueryResult;
2828
import org.graylog.plugins.views.search.Search;
2929
import org.graylog.plugins.views.search.SearchJob;
30+
import org.graylog.plugins.views.search.SearchType;
31+
import org.graylog.plugins.views.search.searchfilters.EffectiveQueryComposer;
32+
import org.graylog.plugins.views.search.searchfilters.model.UsedSearchFilter;
3033
import org.graylog.plugins.views.search.elasticsearch.ElasticsearchQueryString;
3134
import org.graylog.plugins.views.search.errors.QueryError;
3235
import org.graylog.plugins.views.search.errors.SearchError;
@@ -38,6 +41,7 @@
3841
import org.slf4j.LoggerFactory;
3942

4043
import java.util.Collection;
44+
import java.util.List;
4145
import java.util.Map;
4246
import java.util.Objects;
4347
import java.util.Set;
@@ -62,17 +66,20 @@ public class QueryEngine {
6266
private final Executor dataLakeJobsQueryPool;
6367
private final ElasticsearchBackendProvider elasticsearchBackendProvider;
6468
private final Map<String, QueryBackend<? extends GeneratedQueryContext>> unversionedBackends;
69+
private final EffectiveQueryComposer effectiveQueryComposer;
6570

6671
@Inject
6772
public QueryEngine(Configuration configuration,
6873
ElasticsearchBackendProvider elasticsearchBackendProvider,
6974
Map<String, QueryBackend<? extends GeneratedQueryContext>> unversionedBackends,
7075
Set<QueryMetadataDecorator> queryMetadataDecorators,
71-
QueryParser queryParser) {
76+
QueryParser queryParser,
77+
EffectiveQueryComposer effectiveQueryComposer) {
7278
this.elasticsearchBackendProvider = elasticsearchBackendProvider;
7379
this.unversionedBackends = unversionedBackends;
7480
this.queryMetadataDecorators = queryMetadataDecorators;
7581
this.queryParser = queryParser;
82+
this.effectiveQueryComposer = effectiveQueryComposer;
7683

7784
this.indexerJobsQueryPool = createThreadPool(
7885
configuration.searchQueryEngineIndexerJobsPoolSize(),
@@ -111,12 +118,31 @@ public ExplainResults explain(SearchJob searchJob, Set<SearchError> validationEr
111118
var backend = getBackendForQuery(q);
112119
final GeneratedQueryContext generatedQueryContext = backend.generate(q, Set.of(), timezone);
113120

114-
return backend.explain(searchJob, q, generatedQueryContext);
121+
return withEffectiveQueries(q, backend.explain(searchJob, q, generatedQueryContext));
115122
}));
116123

117124
return new ExplainResults(searchJob.getSearchId(), new ExplainResults.SearchResult(queries), validationErrors);
118125
}
119126

127+
private ExplainResults.QueryExplainResult withEffectiveQueries(final Query query, final ExplainResults.QueryExplainResult queryExplainResult) {
128+
final Map<String, SearchType> searchTypesById = query.searchTypes().stream()
129+
.collect(Collectors.toMap(SearchType::id, s -> s, (a, b) -> a));
130+
final String baseQuery = query.query().queryString();
131+
132+
final Map<String, ExplainResults.ExplainResult> enriched = queryExplainResult.searchTypes().entrySet().stream()
133+
.collect(Collectors.toMap(Map.Entry::getKey, entry -> {
134+
final List<UsedSearchFilter> filters = new java.util.ArrayList<>(
135+
query.filters() != null ? query.filters() : List.of());
136+
final SearchType searchType = searchTypesById.get(entry.getKey());
137+
if (searchType != null && searchType.filters() != null) {
138+
filters.addAll(searchType.filters());
139+
}
140+
return entry.getValue().withEffectiveQuery(effectiveQueryComposer.compose(baseQuery, filters));
141+
}));
142+
143+
return new ExplainResults.QueryExplainResult(enriched);
144+
}
145+
120146
@WithSpan
121147
public SearchJob execute(SearchJob searchJob, Set<SearchError> validationErrors, DateTimeZone timezone) {
122148
final Set<Query> validQueries = searchJob.getSearch().queries()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
package org.graylog.plugins.views.search.rest;
18+
19+
import com.fasterxml.jackson.annotation.JsonCreator;
20+
import com.fasterxml.jackson.annotation.JsonProperty;
21+
import org.graylog.plugins.views.search.searchfilters.model.UsedSearchFilter;
22+
23+
import java.util.List;
24+
25+
public record EffectiveQueryRequest(@JsonProperty("query_string") String queryString,
26+
@JsonProperty("filters") List<UsedSearchFilter> filters) {
27+
@JsonCreator
28+
public EffectiveQueryRequest {
29+
queryString = queryString == null ? "" : queryString;
30+
filters = filters == null ? List.of() : filters;
31+
}
32+
}

graylog2-server/src/main/java/org/graylog/plugins/views/search/rest/SearchResource.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package org.graylog.plugins.views.search.rest;
1818

19+
import com.google.common.collect.ImmutableSet;
1920
import com.google.common.eventbus.EventBus;
2021
import io.swagger.v3.oas.annotations.Operation;
2122
import io.swagger.v3.oas.annotations.Parameter;
@@ -41,20 +42,23 @@
4142
import jakarta.ws.rs.core.MediaType;
4243
import jakarta.ws.rs.core.Response;
4344
import org.apache.shiro.authz.annotation.RequiresAuthentication;
44-
import org.graylog.events.processor.systemnotification.TemplateRenderRequest;
4545
import org.graylog.plugins.views.audit.ViewsAuditEventTypes;
4646
import org.graylog.plugins.views.search.ExplainResults;
47+
import org.graylog.plugins.views.search.Query;
4748
import org.graylog.plugins.views.search.Search;
4849
import org.graylog.plugins.views.search.SearchDomain;
4950
import org.graylog.plugins.views.search.SearchJob;
5051
import org.graylog.plugins.views.search.db.SearchJobService;
52+
import org.graylog.plugins.views.search.elasticsearch.ElasticsearchQueryString;
5153
import org.graylog.plugins.views.search.engine.SearchExecutor;
5254
import org.graylog.plugins.views.search.events.SearchJobExecutionEvent;
5355
import org.graylog.plugins.views.search.permissions.SearchUser;
56+
import org.graylog.plugins.views.search.searchtypes.MessageList;
5457
import org.graylog2.audit.jersey.AuditEvent;
5558
import org.graylog2.audit.jersey.NoAuditEvent;
5659
import org.graylog2.indexer.searches.SearchesClusterConfig;
5760
import org.graylog2.plugin.cluster.ClusterConfigService;
61+
import org.graylog2.plugin.indexer.searches.timeranges.RelativeRange;
5862
import org.graylog2.plugin.rest.PluginRestResource;
5963
import org.graylog2.shared.rest.PublicCloudAPI;
6064
import org.graylog2.shared.rest.resources.RestResource;
@@ -65,6 +69,7 @@
6569

6670
import java.net.URI;
6771
import java.util.List;
72+
import java.util.Set;
6873
import java.util.stream.Collectors;
6974

7075
@PublicCloudAPI
@@ -203,6 +208,32 @@ public ExplainResults explainQuery(@Parameter(name = "id") @PathParam("id") Stri
203208
return searchExecutor.explain(id, searchUser, executionState);
204209
}
205210

211+
@POST
212+
@Operation(summary = "Explain an ad-hoc query plus search filters",
213+
description = "Synthesizes a search from the given query and search filters and explains it, so callers "
214+
+ "without a saved search (e.g. the event definition editor) can read the effective query from "
215+
+ "the returned explain results.")
216+
@ApiResponses({
217+
@ApiResponse(responseCode = "200", description = "Explain results (read effective_query per search type)",
218+
content = @Content(schema = @Schema(implementation = ExplainResults.class)))
219+
})
220+
@Path("effective_query")
221+
@NoAuditEvent("Does not return any actual data")
222+
public ExplainResults explainEffectiveQuery(@RequestBody(content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = EffectiveQueryRequest.class)))
223+
EffectiveQueryRequest request,
224+
@Context SearchUser searchUser) {
225+
final Query query = Query.builder()
226+
.id("effective-query")
227+
.searchTypes(Set.of(MessageList.builder().id("effective-query-message-list").build()))
228+
.query(ElasticsearchQueryString.of(request.queryString()))
229+
.filters(request.filters())
230+
.timerange(RelativeRange.allTime())
231+
.build();
232+
final Search search = Search.builder().queries(ImmutableSet.of(query)).build();
233+
234+
return searchExecutor.explain(search, searchUser, ExecutionState.empty());
235+
}
236+
206237
@POST
207238
@Operation(summary = "Execute a new synchronous search", description = "Executes a new search and waits for its result")
208239
@ApiResponses({
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
package org.graylog.plugins.views.search.searchfilters;
18+
19+
import org.graylog.plugins.views.search.searchfilters.model.UsedSearchFilter;
20+
21+
import java.util.List;
22+
23+
public class DefaultEffectiveQueryComposer implements EffectiveQueryComposer {
24+
25+
@Override
26+
public String compose(final String baseQuery, final List<UsedSearchFilter> filters) {
27+
return baseQuery == null ? "" : baseQuery;
28+
}
29+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
package org.graylog.plugins.views.search.searchfilters;
18+
19+
import org.graylog.plugins.views.search.searchfilters.model.UsedSearchFilter;
20+
21+
import java.util.List;
22+
23+
public interface EffectiveQueryComposer {
24+
25+
String compose(String baseQuery, List<UsedSearchFilter> filters);
26+
}

graylog2-server/src/main/java/org/graylog/plugins/views/search/searchfilters/module/SearchFiltersModule.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package org.graylog.plugins.views.search.searchfilters.module;
1818

1919
import com.google.inject.multibindings.OptionalBinder;
20+
import org.graylog.plugins.views.search.searchfilters.DefaultEffectiveQueryComposer;
21+
import org.graylog.plugins.views.search.searchfilters.EffectiveQueryComposer;
2022
import org.graylog.plugins.views.search.searchfilters.db.IgnoreSearchFilters;
2123
import org.graylog.plugins.views.search.searchfilters.db.SearchFiltersReFetcher;
2224
import org.graylog.plugins.views.search.searchfilters.db.UsedSearchFiltersToQueryStringsMapper;
@@ -33,5 +35,7 @@ protected void configure() {
3335
OptionalBinder.newOptionalBinder(binder(), SearchFiltersReFetcher.class)
3436
.setDefault().to(IgnoreSearchFilters.class);
3537

38+
OptionalBinder.newOptionalBinder(binder(), EffectiveQueryComposer.class)
39+
.setDefault().to(DefaultEffectiveQueryComposer.class);
3640
}
3741
}

0 commit comments

Comments
 (0)