Skip to content

Commit 47b14bb

Browse files
matteo-mara-sonarsourceemma44-m
authored andcommitted
SONAR-26417 Migrate RuleIndex to the ES8 Java Api Client methods
Co-authored-by: Emma Mansalier <[email protected]> Co-authored-by: Matteo Mara <[email protected]>
1 parent 5806a45 commit 47b14bb

File tree

15 files changed

+1144
-679
lines changed

15 files changed

+1144
-679
lines changed

server/sonar-server-common/src/it/java/org/sonar/server/rule/index/RuleIndexIT.java

Lines changed: 137 additions & 138 deletions
Large diffs are not rendered by default.
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/*
2+
* SonarQube
3+
* Copyright (C) 2009-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.server.es;
21+
22+
import co.elastic.clients.elasticsearch._types.FieldValue;
23+
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
24+
import co.elastic.clients.elasticsearch._types.query_dsl.ChildScoreMode;
25+
import co.elastic.clients.elasticsearch._types.query_dsl.Operator;
26+
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
27+
import java.util.Collection;
28+
import java.util.List;
29+
import java.util.function.Consumer;
30+
import java.util.stream.Stream;
31+
import javax.annotation.Nullable;
32+
33+
/**
34+
* Helper class for building ES 8 Query objects.
35+
* Provides methods similar to ES 7's QueryBuilders.
36+
*/
37+
public final class ES8QueryHelper {
38+
39+
private ES8QueryHelper() {
40+
// Utility class
41+
}
42+
43+
// ========== MATCH ALL ==========
44+
45+
public static Query matchAllQuery() {
46+
return Query.of(q -> q.matchAll(m -> m));
47+
}
48+
49+
// ========== TERM QUERIES ==========
50+
51+
public static Query termQuery(String field, String value) {
52+
return Query.of(q -> q.term(t -> t.field(field).value(value)));
53+
}
54+
55+
public static Query termQuery(String field, long value) {
56+
return Query.of(q -> q.term(t -> t.field(field).value(value)));
57+
}
58+
59+
public static Query termQuery(String field, boolean value) {
60+
return Query.of(q -> q.term(t -> t.field(field).value(value)));
61+
}
62+
63+
public static Query termsQuery(String field, String... values) {
64+
List<FieldValue> fieldValues = Stream.of(values)
65+
.map(FieldValue::of)
66+
.toList();
67+
return Query.of(q -> q.terms(t -> t.field(field).terms(tv -> tv.value(fieldValues))));
68+
}
69+
70+
public static Query termsQuery(String field, @Nullable Collection<String> values) {
71+
if (values == null || values.isEmpty()) {
72+
return matchAllQuery();
73+
}
74+
List<FieldValue> fieldValues = values.stream()
75+
.map(FieldValue::of)
76+
.toList();
77+
return Query.of(q -> q.terms(t -> t.field(field).terms(tv -> tv.value(fieldValues))));
78+
}
79+
80+
// ========== BOOL QUERIES ==========
81+
82+
public static Query boolQuery() {
83+
return Query.of(q -> q.bool(b -> b));
84+
}
85+
86+
public static Query boolQuery(Consumer<BoolQuery.Builder> fn) {
87+
return Query.of(q -> q.bool(b -> {
88+
fn.accept(b);
89+
return b;
90+
}));
91+
}
92+
93+
// ========== MATCH QUERIES ==========
94+
95+
public static Query matchQuery(String field, String text) {
96+
return Query.of(q -> q.match(m -> m.field(field).query(text)));
97+
}
98+
99+
public static Query matchQuery(String field, String text, Operator operator) {
100+
return Query.of(q -> q.match(m -> m.field(field).query(text).operator(operator)));
101+
}
102+
103+
// ========== RANGE QUERIES ==========
104+
105+
/**
106+
* ES 8: Creates a range query for a specific field.
107+
* Note: In ES8, we use the NumberRangeQuery variant for numeric comparisons.
108+
*/
109+
public static Query rangeQuery(String field) {
110+
return Query.of(q -> q.range(r -> r.number(n -> n.field(field))));
111+
}
112+
113+
public static Query rangeQueryGte(String field, long value) {
114+
return Query.of(q -> q.range(r -> r.number(n -> n.field(field).gte((double) value))));
115+
}
116+
117+
public static Query rangeQueryLte(String field, long value) {
118+
return Query.of(q -> q.range(r -> r.number(n -> n.field(field).lte((double) value))));
119+
}
120+
121+
public static Query rangeQueryGt(String field, long value) {
122+
return Query.of(q -> q.range(r -> r.number(n -> n.field(field).gt((double) value))));
123+
}
124+
125+
public static Query rangeQueryLt(String field, long value) {
126+
return Query.of(q -> q.range(r -> r.number(n -> n.field(field).lt((double) value))));
127+
}
128+
129+
public static Query rangeQueryBetween(String field, long from, long to) {
130+
return Query.of(q -> q.range(r -> r.number(n -> n.field(field).gte((double) from).lte((double) to))));
131+
}
132+
133+
public static Query rangeQueryGte(String field, double value) {
134+
return Query.of(q -> q.range(r -> r.number(n -> n.field(field).gte(value))));
135+
}
136+
137+
public static Query rangeQueryLte(String field, double value) {
138+
return Query.of(q -> q.range(r -> r.number(n -> n.field(field).lte(value))));
139+
}
140+
141+
public static Query rangeQueryGt(String field, double value) {
142+
return Query.of(q -> q.range(r -> r.number(n -> n.field(field).gt(value))));
143+
}
144+
145+
public static Query rangeQueryLt(String field, double value) {
146+
return Query.of(q -> q.range(r -> r.number(n -> n.field(field).lt(value))));
147+
}
148+
149+
// ========== EXISTS QUERIES ==========
150+
151+
public static Query existsQuery(String field) {
152+
return Query.of(q -> q.exists(e -> e.field(field)));
153+
}
154+
155+
// ========== PREFIX QUERIES ==========
156+
157+
public static Query prefixQuery(String field, String prefix) {
158+
return Query.of(q -> q.prefix(p -> p.field(field).value(prefix)));
159+
}
160+
161+
// ========== WILDCARD QUERIES ==========
162+
163+
public static Query wildcardQuery(String field, String value) {
164+
return Query.of(q -> q.wildcard(w -> w.field(field).value(value)));
165+
}
166+
167+
// ========== NESTED QUERIES ==========
168+
169+
public static Query nestedQuery(String path, Query query) {
170+
return Query.of(q -> q.nested(n -> n.path(path).query(query)));
171+
}
172+
173+
public static Query nestedQuery(String path, Query query, ChildScoreMode scoreMode) {
174+
return Query.of(q -> q.nested(n -> n.path(path).query(query).scoreMode(scoreMode)));
175+
}
176+
177+
// ========== HAS CHILD/PARENT QUERIES ==========
178+
179+
public static Query hasChildQuery(String type, Query query) {
180+
return Query.of(q -> q.hasChild(h -> h.type(type).query(query)));
181+
}
182+
183+
public static Query hasChildQuery(String type, Query query, ChildScoreMode scoreMode) {
184+
return Query.of(q -> q.hasChild(h -> h.type(type).query(query).scoreMode(scoreMode)));
185+
}
186+
187+
public static Query hasParentQuery(String parentType, Query query) {
188+
return Query.of(q -> q.hasParent(h -> h.parentType(parentType).query(query)));
189+
}
190+
191+
/**
192+
* Wraps a query with a boost
193+
*/
194+
public static Query withBoost(Query query, float boost) {
195+
return Query.of(q -> q.bool(b -> b.must(query).boost(boost)));
196+
}
197+
}

server/sonar-server-common/src/main/java/org/sonar/server/es/EsUtils.java

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
*/
2020
package org.sonar.server.es;
2121

22+
import co.elastic.clients.elasticsearch._types.FieldValue;
23+
import co.elastic.clients.elasticsearch._types.SortOptions;
24+
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
25+
import co.elastic.clients.elasticsearch.core.SearchRequest;
26+
import co.elastic.clients.elasticsearch.core.search.Hit;
2227
import java.util.ArrayDeque;
2328
import java.util.ArrayList;
2429
import java.util.Collections;
@@ -46,9 +51,10 @@
4651
public class EsUtils {
4752

4853
public static final int SCROLL_TIME_IN_MINUTES = 3;
54+
public static final int SEARCH_AFTER_PAGE_SIZE = 500;
4955

5056
/**
51-
* See https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html
57+
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html">Elasticsearch Regexp query documentation</a>
5258
*/
5359
private static final Pattern SPECIAL_REGEX_CHARS = Pattern.compile("[#@&~<>\"{}()\\[\\].+*?^$\\\\|]");
5460

@@ -99,12 +105,29 @@ public static String formatDateTime(@Nullable Date date) {
99105

100106
/**
101107
* Optimize scolling, by specifying document sorting.
102-
* See https://www.elastic.co/guide/en/elasticsearch/reference/2.4/search-request-scroll.html#search-request-scroll
108+
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/2.4/search-request-scroll.html#search-request-scroll">Elasticsearch scroll documentation</a>
103109
*/
110+
@Deprecated(since = "2025.6", forRemoval = true)
104111
public static void optimizeScrollRequest(SearchSourceBuilder esSearch) {
105112
esSearch.sort("_doc", SortOrder.ASC);
106113
}
107114

115+
/**
116+
* ES 8: Optimize search_after pagination by specifying document sorting.
117+
* This provides efficient pagination without the overhead of maintaining scroll contexts.
118+
* Note: _doc and _id sorting cannot be used with search_after:
119+
* <ul>
120+
* <li>_doc is only for scroll API</li>
121+
* <li>_id requires fielddata to be enabled (disabled by default in ES 8)</li>
122+
* </ul>
123+
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#search-after">Elasticsearch search_after documentation</a>
124+
*/
125+
public static void optimizeSearchAfterRequest(SearchRequest.Builder esSearch, String sortField) {
126+
// Sort by sortField to ensure deterministic ordering for search_after pagination
127+
// This sortField has doc_values enabled and provides a unique sort key for reliable pagination
128+
esSearch.sort(s -> s.field(f -> f.field(sortField).order(co.elastic.clients.elasticsearch._types.SortOrder.Asc)));
129+
}
130+
108131
/**
109132
* Escapes regexp special characters so that text can be forwarded from end-user input
110133
* to Elasticsearch regexp query (for instance attributes "include" and "exclude" of
@@ -114,10 +137,21 @@ public static String escapeSpecialRegexChars(String str) {
114137
return SPECIAL_REGEX_CHARS.matcher(str).replaceAll("\\\\$0");
115138
}
116139

140+
@Deprecated(since = "2025.6", forRemoval = true)
117141
public static <I> Iterator<I> scrollIds(EsClient esClient, SearchResponse scrollResponse, Function<String, I> idConverter) {
118142
return new IdScrollIterator<>(esClient, scrollResponse, idConverter);
119143
}
120144

145+
/**
146+
* ES 8: Iterate through search results using search_after API
147+
* This is the recommended approach for deep pagination in ES 8.
148+
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#search-after">Elasticsearch search_after documentation</a>
149+
*/
150+
public static <T, I> Iterator<I> searchAfterIds(EsClient esClient, SearchRequest initialRequest, Class<T> tClass, Function<String, I> idConverter) {
151+
return new SearchAfterIterator<>(esClient, initialRequest, tClass, idConverter);
152+
}
153+
154+
@Deprecated(since = "2025.6", forRemoval = true)
121155
private static class IdScrollIterator<I> implements Iterator<I> {
122156

123157
private final EsClient esClient;
@@ -156,4 +190,100 @@ public void remove() {
156190
throw new UnsupportedOperationException("Cannot remove item when scrolling");
157191
}
158192
}
193+
194+
private static class SearchAfterIterator<T, I> implements Iterator<I> {
195+
196+
private final EsClient esClient;
197+
private final SearchRequest initialRequest;
198+
private final Class<T> tClass;
199+
private final Function<String, I> idConverter;
200+
201+
private final Queue<Hit<T>> hits = new ArrayDeque<>();
202+
private List<FieldValue> lastSortValues = null;
203+
private boolean hasMore = true;
204+
private boolean isFirstRequest = true;
205+
206+
private SearchAfterIterator(EsClient esClient, SearchRequest initialRequest, Class<T> tClass, Function<String, I> idConverter) {
207+
this.esClient = esClient;
208+
this.initialRequest = initialRequest;
209+
this.tClass = tClass;
210+
this.idConverter = idConverter;
211+
fetchNextBatch();
212+
}
213+
214+
private void fetchNextBatch() {
215+
if (!hasMore) {
216+
return;
217+
}
218+
219+
co.elastic.clients.elasticsearch.core.SearchResponse<T> response;
220+
221+
if (isFirstRequest) {
222+
// For the first request, use the initial request but disable source fetching
223+
// since we're using Void.class and only need document IDs
224+
response = esClient.searchV2(b -> b
225+
.index(initialRequest.index())
226+
.query(initialRequest.query())
227+
.size(initialRequest.size())
228+
.sort(initialRequest.sort())
229+
.source(s -> s.fetch(false)),
230+
tClass);
231+
isFirstRequest = false;
232+
} else {
233+
// For subsequent requests, add search_after with the last sort values
234+
final List<String> indexNames = initialRequest.index();
235+
final Query query = initialRequest.query();
236+
final Integer size = initialRequest.size();
237+
final List<SortOptions> sortOptions = initialRequest.sort();
238+
239+
response = esClient.searchV2(b -> {
240+
b.index(indexNames)
241+
.query(query)
242+
.size(size)
243+
.source(s -> s.fetch(false));
244+
245+
// Apply the same sort configuration as the initial request
246+
if (sortOptions != null && !sortOptions.isEmpty()) {
247+
b.sort(sortOptions);
248+
}
249+
250+
// Add search_after for pagination
251+
b.searchAfter(lastSortValues);
252+
253+
return b;
254+
}, tClass);
255+
}
256+
257+
if (response.hits() != null && response.hits().hits() != null && !response.hits().hits().isEmpty()) {
258+
List<Hit<T>> newHits = response.hits().hits();
259+
hits.addAll(newHits);
260+
// Save the sort values from the last hit for the next request
261+
Hit<T> lastHit = newHits.get(newHits.size() - 1);
262+
lastSortValues = lastHit.sort();
263+
} else {
264+
hasMore = false;
265+
}
266+
}
267+
268+
@Override
269+
public boolean hasNext() {
270+
if (hits.isEmpty() && hasMore) {
271+
fetchNextBatch();
272+
}
273+
return !hits.isEmpty();
274+
}
275+
276+
@Override
277+
public I next() {
278+
if (!hasNext()) {
279+
throw new NoSuchElementException();
280+
}
281+
return idConverter.apply(hits.poll().id());
282+
}
283+
284+
@Override
285+
public void remove() {
286+
throw new UnsupportedOperationException("Cannot remove item when iterating");
287+
}
288+
}
159289
}

0 commit comments

Comments
 (0)