You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ProjectMeasuresIndex.java 31KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 SonarSource SA
  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.measure.index;
  21. import com.google.common.collect.ArrayListMultimap;
  22. import com.google.common.collect.Multimap;
  23. import java.util.Arrays;
  24. import java.util.Collection;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.Objects;
  28. import java.util.Set;
  29. import java.util.function.Consumer;
  30. import java.util.function.Function;
  31. import java.util.stream.Collectors;
  32. import java.util.stream.IntStream;
  33. import java.util.stream.Stream;
  34. import javax.annotation.Nullable;
  35. import org.apache.lucene.search.TotalHits;
  36. import org.apache.lucene.search.join.ScoreMode;
  37. import org.elasticsearch.action.search.SearchRequest;
  38. import org.elasticsearch.action.search.SearchResponse;
  39. import org.elasticsearch.core.TimeValue;
  40. import org.elasticsearch.index.query.BoolQueryBuilder;
  41. import org.elasticsearch.index.query.QueryBuilder;
  42. import org.elasticsearch.search.aggregations.AbstractAggregationBuilder;
  43. import org.elasticsearch.search.aggregations.AggregationBuilders;
  44. import org.elasticsearch.search.aggregations.BucketOrder;
  45. import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation.Bucket;
  46. import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder;
  47. import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregator.KeyedFilter;
  48. import org.elasticsearch.search.aggregations.bucket.nested.Nested;
  49. import org.elasticsearch.search.aggregations.bucket.range.RangeAggregationBuilder;
  50. import org.elasticsearch.search.aggregations.bucket.terms.IncludeExclude;
  51. import org.elasticsearch.search.aggregations.bucket.terms.Terms;
  52. import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
  53. import org.elasticsearch.search.aggregations.metrics.Sum;
  54. import org.elasticsearch.search.builder.SearchSourceBuilder;
  55. import org.elasticsearch.search.sort.FieldSortBuilder;
  56. import org.elasticsearch.search.sort.NestedSortBuilder;
  57. import org.sonar.api.measures.Metric;
  58. import org.sonar.api.resources.Qualifiers;
  59. import org.sonar.api.server.ServerSide;
  60. import org.sonar.api.utils.System2;
  61. import org.sonar.core.util.stream.MoreCollectors;
  62. import org.sonar.server.es.EsClient;
  63. import org.sonar.server.es.SearchIdResult;
  64. import org.sonar.server.es.SearchOptions;
  65. import org.sonar.server.es.newindex.DefaultIndexSettingsElement;
  66. import org.sonar.server.es.searchrequest.NestedFieldTopAggregationDefinition;
  67. import org.sonar.server.es.searchrequest.RequestFiltersComputer;
  68. import org.sonar.server.es.searchrequest.RequestFiltersComputer.AllFilters;
  69. import org.sonar.server.es.searchrequest.SimpleFieldTopAggregationDefinition;
  70. import org.sonar.server.es.searchrequest.SubAggregationHelper;
  71. import org.sonar.server.es.searchrequest.TopAggregationDefinition;
  72. import org.sonar.server.es.searchrequest.TopAggregationDefinition.NestedFieldFilterScope;
  73. import org.sonar.server.es.searchrequest.TopAggregationDefinition.SimpleFieldFilterScope;
  74. import org.sonar.server.es.searchrequest.TopAggregationHelper;
  75. import org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion;
  76. import org.sonar.server.permission.index.WebAuthorizationTypeSupport;
  77. import static com.google.common.base.Preconditions.checkArgument;
  78. import static java.util.Collections.emptyList;
  79. import static java.util.Optional.ofNullable;
  80. import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
  81. import static org.elasticsearch.index.query.QueryBuilders.nestedQuery;
  82. import static org.elasticsearch.index.query.QueryBuilders.rangeQuery;
  83. import static org.elasticsearch.index.query.QueryBuilders.termQuery;
  84. import static org.elasticsearch.index.query.QueryBuilders.termsQuery;
  85. import static org.elasticsearch.search.aggregations.AggregationBuilders.filters;
  86. import static org.elasticsearch.search.aggregations.AggregationBuilders.sum;
  87. import static org.elasticsearch.search.sort.SortOrder.ASC;
  88. import static org.elasticsearch.search.sort.SortOrder.DESC;
  89. import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY;
  90. import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY;
  91. import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES_DENSITY_KEY;
  92. import static org.sonar.api.measures.CoreMetrics.NCLOC_KEY;
  93. import static org.sonar.api.measures.CoreMetrics.NEW_COVERAGE_KEY;
  94. import static org.sonar.api.measures.CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY;
  95. import static org.sonar.api.measures.CoreMetrics.NEW_LINES_KEY;
  96. import static org.sonar.api.measures.CoreMetrics.NEW_MAINTAINABILITY_RATING_KEY;
  97. import static org.sonar.api.measures.CoreMetrics.NEW_RELIABILITY_RATING_KEY;
  98. import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_KEY;
  99. import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_RATING_KEY;
  100. import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_REVIEW_RATING_KEY;
  101. import static org.sonar.api.measures.CoreMetrics.RELIABILITY_RATING_KEY;
  102. import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_KEY;
  103. import static org.sonar.api.measures.CoreMetrics.SECURITY_RATING_KEY;
  104. import static org.sonar.api.measures.CoreMetrics.SECURITY_REVIEW_RATING_KEY;
  105. import static org.sonar.api.measures.CoreMetrics.SQALE_RATING_KEY;
  106. import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
  107. import static org.sonar.server.es.EsUtils.escapeSpecialRegexChars;
  108. import static org.sonar.server.es.EsUtils.termsToMap;
  109. import static org.sonar.server.es.IndexType.FIELD_INDEX_TYPE;
  110. import static org.sonar.server.es.SearchOptions.MAX_PAGE_SIZE;
  111. import static org.sonar.server.es.searchrequest.TopAggregationDefinition.STICKY;
  112. import static org.sonar.server.es.searchrequest.TopAggregationHelper.NO_EXTRA_FILTER;
  113. import static org.sonar.server.measure.index.ProjectMeasuresDoc.QUALITY_GATE_STATUS;
  114. import static org.sonar.server.measure.index.ProjectMeasuresIndex.Facet.ALERT_STATUS;
  115. import static org.sonar.server.measure.index.ProjectMeasuresIndex.Facet.LANGUAGES;
  116. import static org.sonar.server.measure.index.ProjectMeasuresIndex.Facet.TAGS;
  117. import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_ANALYSED_AT;
  118. import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_KEY;
  119. import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_LANGUAGES;
  120. import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_MEASURES;
  121. import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_MEASURES_MEASURE_KEY;
  122. import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_MEASURES_MEASURE_VALUE;
  123. import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_NAME;
  124. import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_NCLOC_DISTRIBUTION;
  125. import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_NCLOC_DISTRIBUTION_LANGUAGE;
  126. import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_NCLOC_DISTRIBUTION_NCLOC;
  127. import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_QUALIFIER;
  128. import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_QUALITY_GATE_STATUS;
  129. import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_TAGS;
  130. import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.SUB_FIELD_MEASURES_KEY;
  131. import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.TYPE_PROJECT_MEASURES;
  132. import static org.sonar.server.measure.index.ProjectMeasuresQuery.SORT_BY_LAST_ANALYSIS_DATE;
  133. import static org.sonar.server.measure.index.ProjectMeasuresQuery.SORT_BY_NAME;
  134. import static org.sonarqube.ws.client.project.ProjectsWsParameters.FILTER_LANGUAGES;
  135. import static org.sonarqube.ws.client.project.ProjectsWsParameters.FILTER_QUALIFIER;
  136. import static org.sonarqube.ws.client.project.ProjectsWsParameters.FILTER_TAGS;
  137. @ServerSide
  138. public class ProjectMeasuresIndex {
  139. private static final int FACET_DEFAULT_SIZE = 10;
  140. private static final double[] LINES_THRESHOLDS = {1_000D, 10_000D, 100_000D, 500_000D};
  141. private static final double[] COVERAGE_THRESHOLDS = {30D, 50D, 70D, 80D};
  142. private static final double[] SECURITY_REVIEW_RATING_THRESHOLDS = {30D, 50D, 70D, 80D};
  143. private static final double[] DUPLICATIONS_THRESHOLDS = {3D, 5D, 10D, 20D};
  144. private static final int SCROLL_SIZE = 5000;
  145. private static final TimeValue KEEP_ALIVE_SCROLL_DURATION = TimeValue.timeValueMinutes(1L);
  146. public enum Facet {
  147. NCLOC(new RangeMeasureFacet(NCLOC_KEY, LINES_THRESHOLDS)),
  148. NEW_LINES(new RangeMeasureFacet(NEW_LINES_KEY, LINES_THRESHOLDS)),
  149. DUPLICATED_LINES_DENSITY(new RangeWithNoDataMeasureFacet(DUPLICATED_LINES_DENSITY_KEY, DUPLICATIONS_THRESHOLDS)),
  150. NEW_DUPLICATED_LINES_DENSITY(new RangeWithNoDataMeasureFacet(NEW_DUPLICATED_LINES_DENSITY_KEY, DUPLICATIONS_THRESHOLDS)),
  151. COVERAGE(new RangeWithNoDataMeasureFacet(COVERAGE_KEY, COVERAGE_THRESHOLDS)),
  152. NEW_COVERAGE(new RangeWithNoDataMeasureFacet(NEW_COVERAGE_KEY, COVERAGE_THRESHOLDS)),
  153. SQALE_RATING(new RatingMeasureFacet(SQALE_RATING_KEY)),
  154. NEW_MAINTAINABILITY_RATING(new RatingMeasureFacet(NEW_MAINTAINABILITY_RATING_KEY)),
  155. RELIABILITY_RATING(new RatingMeasureFacet(RELIABILITY_RATING_KEY)),
  156. NEW_RELIABILITY_RATING(new RatingMeasureFacet(NEW_RELIABILITY_RATING_KEY)),
  157. SECURITY_RATING(new RatingMeasureFacet(SECURITY_RATING_KEY)),
  158. NEW_SECURITY_RATING(new RatingMeasureFacet(NEW_SECURITY_RATING_KEY)),
  159. SECURITY_REVIEW_RATING(new RatingMeasureFacet(SECURITY_REVIEW_RATING_KEY)),
  160. NEW_SECURITY_REVIEW_RATING(new RatingMeasureFacet(NEW_SECURITY_REVIEW_RATING_KEY)),
  161. SECURITY_HOTSPOTS_REVIEWED(new RangeMeasureFacet(SECURITY_HOTSPOTS_REVIEWED_KEY, SECURITY_REVIEW_RATING_THRESHOLDS)),
  162. NEW_SECURITY_HOTSPOTS_REVIEWED(new RangeMeasureFacet(NEW_SECURITY_HOTSPOTS_REVIEWED_KEY, SECURITY_REVIEW_RATING_THRESHOLDS)),
  163. ALERT_STATUS(new MeasureFacet(ALERT_STATUS_KEY, ProjectMeasuresIndex::buildAlertStatusFacet)),
  164. LANGUAGES(FILTER_LANGUAGES, FIELD_LANGUAGES, STICKY, ProjectMeasuresIndex::buildLanguageFacet),
  165. QUALIFIER(FILTER_QUALIFIER, FIELD_QUALIFIER, STICKY, ProjectMeasuresIndex::buildQualifierFacet),
  166. TAGS(FILTER_TAGS, FIELD_TAGS, STICKY, ProjectMeasuresIndex::buildTagsFacet);
  167. private final String name;
  168. private final TopAggregationDefinition<?> topAggregation;
  169. private final FacetBuilder facetBuilder;
  170. Facet(String name, String fieldName, boolean sticky, FacetBuilder facetBuilder) {
  171. this.name = name;
  172. this.topAggregation = new SimpleFieldTopAggregationDefinition(fieldName, sticky);
  173. this.facetBuilder = facetBuilder;
  174. }
  175. Facet(MeasureFacet measureFacet) {
  176. this.name = measureFacet.metricKey;
  177. this.topAggregation = measureFacet.topAggregation;
  178. this.facetBuilder = measureFacet.facetBuilder;
  179. }
  180. public String getName() {
  181. return name;
  182. }
  183. public TopAggregationDefinition<?> getTopAggregationDef() {
  184. return topAggregation;
  185. }
  186. public TopAggregationDefinition.FilterScope getFilterScope() {
  187. return topAggregation.getFilterScope();
  188. }
  189. public FacetBuilder getFacetBuilder() {
  190. return facetBuilder;
  191. }
  192. }
  193. private static final Map<String, Facet> FACETS_BY_NAME = Arrays.stream(Facet.values())
  194. .collect(uniqueIndex(Facet::getName));
  195. private final EsClient client;
  196. private final WebAuthorizationTypeSupport authorizationTypeSupport;
  197. private final System2 system2;
  198. public ProjectMeasuresIndex(EsClient client, WebAuthorizationTypeSupport authorizationTypeSupport, System2 system2) {
  199. this.client = client;
  200. this.authorizationTypeSupport = authorizationTypeSupport;
  201. this.system2 = system2;
  202. }
  203. public SearchIdResult<String> search(ProjectMeasuresQuery query, SearchOptions searchOptions) {
  204. SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder()
  205. .fetchSource(false)
  206. .trackTotalHits(true)
  207. .from(searchOptions.getOffset())
  208. .size(searchOptions.getLimit());
  209. AllFilters allFilters = createFilters(query);
  210. RequestFiltersComputer filtersComputer = createFiltersComputer(searchOptions, allFilters);
  211. addFacets(searchSourceBuilder, searchOptions, filtersComputer, query);
  212. addSort(query, searchSourceBuilder);
  213. filtersComputer.getQueryFilters().ifPresent(searchSourceBuilder::query);
  214. filtersComputer.getPostFilters().ifPresent(searchSourceBuilder::postFilter);
  215. SearchResponse response = client.search(EsClient.prepareSearch(TYPE_PROJECT_MEASURES.getMainType())
  216. .source(searchSourceBuilder));
  217. return new SearchIdResult<>(response, id -> id, system2.getDefaultTimeZone().toZoneId());
  218. }
  219. private static RequestFiltersComputer createFiltersComputer(SearchOptions searchOptions, AllFilters allFilters) {
  220. Collection<String> facetNames = searchOptions.getFacets();
  221. Set<TopAggregationDefinition<?>> facets = facetNames.stream()
  222. .map(FACETS_BY_NAME::get)
  223. .filter(Objects::nonNull)
  224. .map(Facet::getTopAggregationDef)
  225. .collect(Collectors.toSet());
  226. return new RequestFiltersComputer(allFilters, facets);
  227. }
  228. public ProjectMeasuresStatistics searchSupportStatistics() {
  229. SearchRequest projectMeasuresSearchRequest = buildProjectMeasureSearchRequest();
  230. SearchResponse projectMeasures = client.search(projectMeasuresSearchRequest);
  231. return buildProjectMeasuresStatistics(projectMeasures);
  232. }
  233. private static SearchRequest buildProjectMeasureSearchRequest() {
  234. SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder()
  235. .fetchSource(false)
  236. .size(0);
  237. BoolQueryBuilder esFilter = boolQuery()
  238. .filter(termQuery(FIELD_INDEX_TYPE, TYPE_PROJECT_MEASURES.getName()))
  239. .filter(termQuery(FIELD_QUALIFIER, Qualifiers.PROJECT));
  240. searchSourceBuilder.query(esFilter);
  241. searchSourceBuilder.aggregation(AggregationBuilders.terms(FIELD_LANGUAGES)
  242. .field(FIELD_LANGUAGES)
  243. .size(MAX_PAGE_SIZE)
  244. .minDocCount(1)
  245. .order(BucketOrder.count(false)));
  246. searchSourceBuilder.aggregation(AggregationBuilders.nested(FIELD_NCLOC_DISTRIBUTION, FIELD_NCLOC_DISTRIBUTION)
  247. .subAggregation(AggregationBuilders.terms(FIELD_NCLOC_DISTRIBUTION + "_terms")
  248. .field(FIELD_NCLOC_DISTRIBUTION_LANGUAGE)
  249. .size(MAX_PAGE_SIZE)
  250. .minDocCount(1)
  251. .order(BucketOrder.count(false))
  252. .subAggregation(sum(FIELD_NCLOC_DISTRIBUTION_NCLOC).field(FIELD_NCLOC_DISTRIBUTION_NCLOC))));
  253. searchSourceBuilder.aggregation(AggregationBuilders.nested(NCLOC_KEY, FIELD_MEASURES)
  254. .subAggregation(AggregationBuilders.filter(NCLOC_KEY + "_filter", termQuery(FIELD_MEASURES_MEASURE_KEY, NCLOC_KEY))
  255. .subAggregation(sum(NCLOC_KEY + "_filter_sum").field(FIELD_MEASURES_MEASURE_VALUE))));
  256. searchSourceBuilder.size(SCROLL_SIZE);
  257. return EsClient.prepareSearch(TYPE_PROJECT_MEASURES.getMainType()).source(searchSourceBuilder).scroll(KEEP_ALIVE_SCROLL_DURATION);
  258. }
  259. private static ProjectMeasuresStatistics buildProjectMeasuresStatistics(SearchResponse response) {
  260. ProjectMeasuresStatistics.Builder statistics = ProjectMeasuresStatistics.builder();
  261. statistics.setProjectCount(getTotalHits(response.getHits().getTotalHits()).value);
  262. statistics.setProjectCountByLanguage(termsToMap(response.getAggregations().get(FIELD_LANGUAGES)));
  263. Function<Terms.Bucket, Long> bucketToNcloc = bucket -> Math.round(((Sum) bucket.getAggregations().get(FIELD_NCLOC_DISTRIBUTION_NCLOC)).getValue());
  264. Map<String, Long> nclocByLanguage = Stream.of((Nested) response.getAggregations().get(FIELD_NCLOC_DISTRIBUTION))
  265. .map(nested -> (Terms) nested.getAggregations().get(nested.getName() + "_terms"))
  266. .flatMap(terms -> terms.getBuckets().stream())
  267. .collect(MoreCollectors.uniqueIndex(Bucket::getKeyAsString, bucketToNcloc));
  268. statistics.setNclocByLanguage(nclocByLanguage);
  269. return statistics.build();
  270. }
  271. private static TotalHits getTotalHits(@Nullable TotalHits totalHits) {
  272. return ofNullable(totalHits).orElseThrow(() -> new IllegalStateException("Could not get total hits of search results"));
  273. }
  274. private static void addSort(ProjectMeasuresQuery query, SearchSourceBuilder requestBuilder) {
  275. String sort = query.getSort();
  276. if (SORT_BY_NAME.equals(sort)) {
  277. requestBuilder.sort(DefaultIndexSettingsElement.SORTABLE_ANALYZER.subField(FIELD_NAME), query.isAsc() ? ASC : DESC);
  278. } else if (SORT_BY_LAST_ANALYSIS_DATE.equals(sort)) {
  279. requestBuilder.sort(FIELD_ANALYSED_AT, query.isAsc() ? ASC : DESC);
  280. } else if (ALERT_STATUS_KEY.equals(sort)) {
  281. requestBuilder.sort(FIELD_QUALITY_GATE_STATUS, query.isAsc() ? ASC : DESC);
  282. requestBuilder.sort(DefaultIndexSettingsElement.SORTABLE_ANALYZER.subField(FIELD_NAME), ASC);
  283. } else {
  284. addMetricSort(query, requestBuilder, sort);
  285. requestBuilder.sort(DefaultIndexSettingsElement.SORTABLE_ANALYZER.subField(FIELD_NAME), ASC);
  286. }
  287. // last sort is by key in order to be deterministic when same value
  288. requestBuilder.sort(FIELD_KEY, ASC);
  289. }
  290. private static void addMetricSort(ProjectMeasuresQuery query, SearchSourceBuilder requestBuilder, String sort) {
  291. requestBuilder.sort(
  292. new FieldSortBuilder(FIELD_MEASURES_MEASURE_VALUE)
  293. .setNestedSort(
  294. new NestedSortBuilder(FIELD_MEASURES)
  295. .setFilter(termQuery(FIELD_MEASURES_MEASURE_KEY, sort)))
  296. .order(query.isAsc() ? ASC : DESC));
  297. }
  298. private static void addFacets(SearchSourceBuilder esRequest, SearchOptions options, RequestFiltersComputer filtersComputer, ProjectMeasuresQuery query) {
  299. TopAggregationHelper topAggregationHelper = new TopAggregationHelper(filtersComputer, new SubAggregationHelper());
  300. options.getFacets().stream()
  301. .map(FACETS_BY_NAME::get)
  302. .filter(Objects::nonNull)
  303. .map(facet -> facet.getFacetBuilder().buildFacet(facet, query, topAggregationHelper))
  304. .forEach(esRequest::aggregation);
  305. }
  306. private static AbstractAggregationBuilder<?> createRangeFacet(String metricKey, double[] thresholds) {
  307. RangeAggregationBuilder rangeAgg = AggregationBuilders.range(metricKey)
  308. .field(FIELD_MEASURES_MEASURE_VALUE);
  309. final int lastIndex = thresholds.length - 1;
  310. IntStream.range(0, thresholds.length)
  311. .forEach(i -> {
  312. if (i == 0) {
  313. rangeAgg.addUnboundedTo(thresholds[0]);
  314. rangeAgg.addRange(thresholds[0], thresholds[1]);
  315. } else if (i == lastIndex) {
  316. rangeAgg.addUnboundedFrom(thresholds[lastIndex]);
  317. } else {
  318. rangeAgg.addRange(thresholds[i], thresholds[i + 1]);
  319. }
  320. });
  321. return AggregationBuilders.nested("nested_" + metricKey, FIELD_MEASURES)
  322. .subAggregation(
  323. AggregationBuilders.filter("filter_" + metricKey, termsQuery(FIELD_MEASURES_MEASURE_KEY, metricKey))
  324. .subAggregation(rangeAgg));
  325. }
  326. private static AbstractAggregationBuilder<?> createQualityGateFacet(ProjectMeasuresQuery projectMeasuresQuery) {
  327. return filters(
  328. ALERT_STATUS_KEY,
  329. QUALITY_GATE_STATUS
  330. .entrySet()
  331. .stream()
  332. .filter(qgs -> !(projectMeasuresQuery.isIgnoreWarning() && qgs.getKey().equals(Metric.Level.WARN.name())))
  333. .map(entry -> new KeyedFilter(entry.getKey(), termQuery(FIELD_QUALITY_GATE_STATUS, entry.getValue())))
  334. .toArray(KeyedFilter[]::new));
  335. }
  336. private static AbstractAggregationBuilder<?> createQualifierFacet() {
  337. return filters(
  338. FILTER_QUALIFIER,
  339. Stream.of(Qualifiers.APP, Qualifiers.PROJECT)
  340. .map(qualifier -> new KeyedFilter(qualifier, termQuery(FIELD_QUALIFIER, qualifier)))
  341. .toArray(KeyedFilter[]::new));
  342. }
  343. private AllFilters createFilters(ProjectMeasuresQuery query) {
  344. AllFilters filters = RequestFiltersComputer.newAllFilters();
  345. filters.addFilter(
  346. "__indexType", new SimpleFieldFilterScope(FIELD_INDEX_TYPE),
  347. termQuery(FIELD_INDEX_TYPE, TYPE_PROJECT_MEASURES.getName()));
  348. if (!query.isIgnoreAuthorization()) {
  349. filters.addFilter("__authorization", new SimpleFieldFilterScope("parent"), authorizationTypeSupport.createQueryFilter());
  350. }
  351. Multimap<String, MetricCriterion> metricCriterionMultimap = ArrayListMultimap.create();
  352. query.getMetricCriteria()
  353. .forEach(metricCriterion -> metricCriterionMultimap.put(metricCriterion.getMetricKey(), metricCriterion));
  354. metricCriterionMultimap.asMap().forEach((key, value) -> {
  355. BoolQueryBuilder metricFilters = boolQuery();
  356. value
  357. .stream()
  358. .map(ProjectMeasuresIndex::toQuery)
  359. .forEach(metricFilters::must);
  360. filters.addFilter(key, new NestedFieldFilterScope<>(FIELD_MEASURES, SUB_FIELD_MEASURES_KEY, key), metricFilters);
  361. });
  362. query.getQualityGateStatus().ifPresent(qualityGateStatus -> filters.addFilter(
  363. ALERT_STATUS_KEY, ALERT_STATUS.getFilterScope(),
  364. termQuery(FIELD_QUALITY_GATE_STATUS, QUALITY_GATE_STATUS.get(qualityGateStatus.name()))));
  365. query.getProjectUuids().ifPresent(projectUuids -> filters.addFilter(
  366. "ids", new SimpleFieldFilterScope("_id"),
  367. termsQuery("_id", projectUuids)));
  368. query.getLanguages()
  369. .ifPresent(languages -> filters.addFilter(FILTER_LANGUAGES, LANGUAGES.getFilterScope(), termsQuery(FIELD_LANGUAGES, languages)));
  370. query.getTags().ifPresent(tags -> filters.addFilter(FIELD_TAGS, TAGS.getFilterScope(), termsQuery(FIELD_TAGS, tags)));
  371. query.getQualifiers()
  372. .ifPresent(qualifiers -> filters.addFilter(FIELD_QUALIFIER, new SimpleFieldFilterScope(FIELD_QUALIFIER), termsQuery(FIELD_QUALIFIER, qualifiers)));
  373. query.getQueryText()
  374. .map(ProjectsTextSearchQueryFactory::createQuery)
  375. .ifPresent(queryBuilder -> filters.addFilter("textQuery", new SimpleFieldFilterScope(FIELD_NAME), queryBuilder));
  376. return filters;
  377. }
  378. private static QueryBuilder toQuery(MetricCriterion criterion) {
  379. if (criterion.isNoData()) {
  380. return boolQuery().mustNot(
  381. nestedQuery(
  382. FIELD_MEASURES,
  383. termQuery(FIELD_MEASURES_MEASURE_KEY, criterion.getMetricKey()),
  384. ScoreMode.Avg));
  385. }
  386. return nestedQuery(
  387. FIELD_MEASURES,
  388. boolQuery()
  389. .filter(termQuery(FIELD_MEASURES_MEASURE_KEY, criterion.getMetricKey()))
  390. .filter(toValueQuery(criterion)),
  391. ScoreMode.Avg);
  392. }
  393. private static QueryBuilder toValueQuery(MetricCriterion criterion) {
  394. String fieldName = FIELD_MEASURES_MEASURE_VALUE;
  395. switch (criterion.getOperator()) {
  396. case GT:
  397. return rangeQuery(fieldName).gt(criterion.getValue());
  398. case GTE:
  399. return rangeQuery(fieldName).gte(criterion.getValue());
  400. case LT:
  401. return rangeQuery(fieldName).lt(criterion.getValue());
  402. case LTE:
  403. return rangeQuery(fieldName).lte(criterion.getValue());
  404. case EQ:
  405. return termQuery(fieldName, criterion.getValue());
  406. default:
  407. throw new IllegalStateException("Metric criteria non supported: " + criterion.getOperator().name());
  408. }
  409. }
  410. public List<String> searchTags(@Nullable String textQuery, int page, int size) {
  411. int maxPageSize = 100;
  412. int maxPage = 20;
  413. checkArgument(size <= maxPageSize, "Page size must be lower than or equals to " + maxPageSize);
  414. checkArgument(page > 0 && page <= maxPage, "Page must be between 0 and " + maxPage);
  415. if (size <= 0) {
  416. return emptyList();
  417. }
  418. TermsAggregationBuilder tagFacet = AggregationBuilders.terms(FIELD_TAGS)
  419. .field(FIELD_TAGS)
  420. .size(size*page)
  421. .minDocCount(1)
  422. .order(BucketOrder.key(true));
  423. if (textQuery != null) {
  424. tagFacet.includeExclude(new IncludeExclude(".*" + escapeSpecialRegexChars(textQuery) + ".*", null));
  425. }
  426. SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder()
  427. .query(authorizationTypeSupport.createQueryFilter())
  428. .fetchSource(false)
  429. .aggregation(tagFacet);
  430. SearchResponse response = client.search(EsClient.prepareSearch(TYPE_PROJECT_MEASURES.getMainType())
  431. .source(searchSourceBuilder));
  432. Terms aggregation = response.getAggregations().get(FIELD_TAGS);
  433. return aggregation.getBuckets().stream()
  434. .skip((page-1) * size)
  435. .map(Bucket::getKeyAsString)
  436. .toList();
  437. }
  438. private interface FacetBuilder {
  439. FilterAggregationBuilder buildFacet(Facet facet, ProjectMeasuresQuery query, TopAggregationHelper topAggregationHelper);
  440. }
  441. /**
  442. * A sticky facet on field {@link ProjectMeasuresIndexDefinition#FIELD_MEASURES_MEASURE_KEY}.
  443. */
  444. private static class MeasureFacet {
  445. private final String metricKey;
  446. private final TopAggregationDefinition<?> topAggregation;
  447. private final FacetBuilder facetBuilder;
  448. private MeasureFacet(String metricKey, FacetBuilder facetBuilder) {
  449. this.metricKey = metricKey;
  450. this.topAggregation = new NestedFieldTopAggregationDefinition<>(FIELD_MEASURES_MEASURE_KEY, metricKey, STICKY);
  451. this.facetBuilder = facetBuilder;
  452. }
  453. }
  454. private static final class RangeMeasureFacet extends MeasureFacet {
  455. private RangeMeasureFacet(String metricKey, double[] thresholds) {
  456. super(metricKey, new MetricRangeFacetBuilder(metricKey, thresholds));
  457. }
  458. private static final class MetricRangeFacetBuilder implements FacetBuilder {
  459. private final String metricKey;
  460. private final double[] thresholds;
  461. private MetricRangeFacetBuilder(String metricKey, double[] thresholds) {
  462. this.metricKey = metricKey;
  463. this.thresholds = thresholds;
  464. }
  465. @Override
  466. public FilterAggregationBuilder buildFacet(Facet facet, ProjectMeasuresQuery query, TopAggregationHelper topAggregationHelper) {
  467. return topAggregationHelper.buildTopAggregation(
  468. facet.getName(), facet.getTopAggregationDef(),
  469. NO_EXTRA_FILTER,
  470. t -> t.subAggregation(createRangeFacet(metricKey, thresholds)));
  471. }
  472. }
  473. }
  474. private static final class RangeWithNoDataMeasureFacet extends MeasureFacet {
  475. private RangeWithNoDataMeasureFacet(String metricKey, double[] thresholds) {
  476. super(metricKey, new MetricRangeWithNoDataFacetBuilder(metricKey, thresholds));
  477. }
  478. private static final class MetricRangeWithNoDataFacetBuilder implements FacetBuilder {
  479. private final String metricKey;
  480. private final double[] thresholds;
  481. private MetricRangeWithNoDataFacetBuilder(String metricKey, double[] thresholds) {
  482. this.metricKey = metricKey;
  483. this.thresholds = thresholds;
  484. }
  485. @Override
  486. public FilterAggregationBuilder buildFacet(Facet facet, ProjectMeasuresQuery query, TopAggregationHelper topAggregationHelper) {
  487. return topAggregationHelper.buildTopAggregation(
  488. facet.getName(), facet.getTopAggregationDef(),
  489. NO_EXTRA_FILTER,
  490. t -> t.subAggregation(createRangeFacet(metricKey, thresholds))
  491. .subAggregation(createNoDataFacet(metricKey)));
  492. }
  493. private static AbstractAggregationBuilder<?> createNoDataFacet(String metricKey) {
  494. return AggregationBuilders.filter(
  495. "no_data_" + metricKey,
  496. boolQuery().mustNot(nestedQuery(FIELD_MEASURES, termQuery(FIELD_MEASURES_MEASURE_KEY, metricKey), ScoreMode.Avg)));
  497. }
  498. }
  499. }
  500. private static class RatingMeasureFacet extends MeasureFacet {
  501. private RatingMeasureFacet(String metricKey) {
  502. super(metricKey, new MetricRatingFacetBuilder(metricKey));
  503. }
  504. private static class MetricRatingFacetBuilder implements FacetBuilder {
  505. private final String metricKey;
  506. private MetricRatingFacetBuilder(String metricKey) {
  507. this.metricKey = metricKey;
  508. }
  509. @Override
  510. public FilterAggregationBuilder buildFacet(Facet facet, ProjectMeasuresQuery query, TopAggregationHelper topAggregationHelper) {
  511. return topAggregationHelper.buildTopAggregation(
  512. facet.getName(), facet.getTopAggregationDef(),
  513. NO_EXTRA_FILTER,
  514. t -> t.subAggregation(createMeasureRatingFacet(metricKey)));
  515. }
  516. private static AbstractAggregationBuilder<?> createMeasureRatingFacet(String metricKey) {
  517. return AggregationBuilders.nested("nested_" + metricKey, FIELD_MEASURES)
  518. .subAggregation(
  519. AggregationBuilders.filter("filter_" + metricKey, termsQuery(FIELD_MEASURES_MEASURE_KEY, metricKey))
  520. .subAggregation(filters(metricKey,
  521. new KeyedFilter("1", termQuery(FIELD_MEASURES_MEASURE_VALUE, 1D)),
  522. new KeyedFilter("2", termQuery(FIELD_MEASURES_MEASURE_VALUE, 2D)),
  523. new KeyedFilter("3", termQuery(FIELD_MEASURES_MEASURE_VALUE, 3D)),
  524. new KeyedFilter("4", termQuery(FIELD_MEASURES_MEASURE_VALUE, 4D)),
  525. new KeyedFilter("5", termQuery(FIELD_MEASURES_MEASURE_VALUE, 5D)))));
  526. }
  527. }
  528. }
  529. private static FilterAggregationBuilder buildLanguageFacet(Facet facet, ProjectMeasuresQuery query, TopAggregationHelper topAggregationHelper) {
  530. // optional selected languages sub-aggregation
  531. Consumer<FilterAggregationBuilder> extraSubAgg = t -> query.getLanguages()
  532. .flatMap(languages -> topAggregationHelper.getSubAggregationHelper()
  533. .buildSelectedItemsAggregation(FILTER_LANGUAGES, facet.getTopAggregationDef(), languages.toArray()))
  534. .ifPresent(t::subAggregation);
  535. return topAggregationHelper.buildTermTopAggregation(
  536. FILTER_LANGUAGES, facet.getTopAggregationDef(), FACET_DEFAULT_SIZE,
  537. NO_EXTRA_FILTER, extraSubAgg);
  538. }
  539. private static FilterAggregationBuilder buildAlertStatusFacet(Facet facet, ProjectMeasuresQuery query, TopAggregationHelper topAggregationHelper) {
  540. return topAggregationHelper.buildTopAggregation(
  541. facet.getName(), facet.getTopAggregationDef(),
  542. NO_EXTRA_FILTER,
  543. t -> t.subAggregation(createQualityGateFacet(query)));
  544. }
  545. private static FilterAggregationBuilder buildTagsFacet(Facet facet, ProjectMeasuresQuery query, TopAggregationHelper topAggregationHelper) {
  546. // optional selected tags sub-aggregation
  547. Consumer<FilterAggregationBuilder> extraSubAgg = t -> query.getTags()
  548. .flatMap(tags -> topAggregationHelper.getSubAggregationHelper()
  549. .buildSelectedItemsAggregation(FILTER_TAGS, facet.getTopAggregationDef(), tags.toArray()))
  550. .ifPresent(t::subAggregation);
  551. return topAggregationHelper.buildTermTopAggregation(
  552. FILTER_TAGS, facet.getTopAggregationDef(), FACET_DEFAULT_SIZE,
  553. NO_EXTRA_FILTER, extraSubAgg);
  554. }
  555. private static FilterAggregationBuilder buildQualifierFacet(Facet facet, ProjectMeasuresQuery query, TopAggregationHelper topAggregationHelper) {
  556. return topAggregationHelper.buildTopAggregation(
  557. facet.getName(), facet.getTopAggregationDef(),
  558. NO_EXTRA_FILTER,
  559. t -> t.subAggregation(createQualifierFacet()));
  560. }
  561. }