diff options
author | Daniel Schwarz <daniel.schwarz@sonarsource.com> | 2017-04-14 17:41:47 +0200 |
---|---|---|
committer | Daniel Schwarz <bartfastiel@users.noreply.github.com> | 2017-04-20 09:48:52 +0200 |
commit | af7327e59b88d1a7738b28bad169941fdb2845f6 (patch) | |
tree | ea8e9d4a0a6a6e2d9ad6dfedaa0eaea6895d4954 | |
parent | 38030307f50b11df0be8872ee6402fdaf634fab8 (diff) | |
download | sonarqube-af7327e59b88d1a7738b28bad169941fdb2845f6.tar.gz sonarqube-af7327e59b88d1a7738b28bad169941fdb2845f6.zip |
SONAR-9074 score recently browsed components higher in suggestions
9 files changed, 154 insertions, 10 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndex.java b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndex.java index d9176c40a13..5ee71624704 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndex.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndex.java @@ -127,6 +127,7 @@ public class ComponentIndex { .setQueryText(query.getQuery()) .setFieldKey(FIELD_KEY) .setFieldName(FIELD_NAME) + .setRecentlyBrowsedKeys(query.getRecentlyBrowsedKeys()) .build(); return esQuery.must(ComponentTextSearchQueryFactory.createQuery(componentTextSearchQuery, features)); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexQuery.java b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexQuery.java index 5080db2e23a..1c2f05c3006 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexQuery.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexQuery.java @@ -22,6 +22,7 @@ package org.sonar.server.component.index; import java.util.Collection; import java.util.Collections; import java.util.Optional; +import java.util.Set; import javax.annotation.CheckForNull; import javax.annotation.Nullable; @@ -32,12 +33,14 @@ public class ComponentIndexQuery { private final String query; private final Collection<String> qualifiers; + private final Set<String> recentlyBrowsedKeys; @CheckForNull private final Integer limit; private ComponentIndexQuery(Builder builder) { this.query = requireNonNull(builder.query); this.qualifiers = requireNonNull(builder.qualifiers); + this.recentlyBrowsedKeys = requireNonNull(builder.recentlyBrowsedKeys); this.limit = builder.limit; } @@ -49,6 +52,10 @@ public class ComponentIndexQuery { return query; } + public Set<String> getRecentlyBrowsedKeys() { + return recentlyBrowsedKeys; + } + public Optional<Integer> getLimit() { return Optional.ofNullable(limit); } @@ -60,6 +67,7 @@ public class ComponentIndexQuery { public static class Builder { private String query; private Collection<String> qualifiers = Collections.emptyList(); + private Set<String> recentlyBrowsedKeys = Collections.emptySet(); private Integer limit; private Builder() { @@ -76,6 +84,11 @@ public class ComponentIndexQuery { return this; } + public Builder setRecentlyBrowsedKeys(Set<String> recentlyBrowsedKeys) { + this.recentlyBrowsedKeys = Collections.unmodifiableSet(recentlyBrowsedKeys); + return this; + } + public Builder setLimit(@Nullable Integer limit) { checkArgument(limit == null || limit > 0, "Limit has to be strictly positive: %s", limit); this.limit = limit; diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java index 39dde1a4e10..8db06733b45 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java @@ -47,7 +47,9 @@ import org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Project; import org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Suggestion; import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableSet.copyOf; import static java.util.Arrays.stream; +import static java.util.Collections.emptySet; import static java.util.Collections.singletonList; import static java.util.Optional.ofNullable; import static org.sonar.core.util.stream.MoreCollectors.toList; @@ -62,6 +64,7 @@ public class SuggestionsAction implements ComponentsWsAction { static final String PARAM_QUERY = "s"; static final String PARAM_MORE = "more"; + static final String PARAM_RECENTLY_BROWSED = "recentlyBrowsed"; static final String SHORT_INPUT_WARNING = "short_input"; static final int DEFAULT_LIMIT = 6; @@ -103,19 +106,27 @@ public class SuggestionsAction implements ComponentsWsAction { .setDescription("Category, for which to display " + EXTENDED_LIMIT + " instead of " + DEFAULT_LIMIT + " results") .setPossibleValues(stream(SuggestionCategory.values()).map(SuggestionCategory::getName).toArray(String[]::new)) .setSince("6.4"); + + action.createParam(PARAM_RECENTLY_BROWSED) + .setDescription("Comma separated list of component keys, that have recently been browsed by the user.") + .setSince("6.4") + .setExampleValue("org.sonarsource:sonarqube,some.other:project") + .setRequired(false); } @Override public void handle(Request wsRequest, Response wsResponse) throws Exception { String query = wsRequest.param(PARAM_QUERY); String more = wsRequest.param(PARAM_MORE); + List<String> recentlyBrowsedParam = wsRequest.paramAsStrings(PARAM_RECENTLY_BROWSED); + Set<String> recentlyBrowsedKeys = recentlyBrowsedParam == null ? emptySet() : copyOf(recentlyBrowsedParam); - ComponentIndexQuery.Builder queryBuilder = ComponentIndexQuery.builder().setQuery(query); + ComponentIndexQuery.Builder queryBuilder = ComponentIndexQuery.builder().setQuery(query).setRecentlyBrowsedKeys(recentlyBrowsedKeys); List<ComponentHitsPerQualifier> componentsPerQualifiers = getComponentsPerQualifiers(more, queryBuilder); String warning = getWarning(query); - SuggestionsWsResponse searchWsResponse = toResponse(componentsPerQualifiers, warning); + SuggestionsWsResponse searchWsResponse = toResponse(componentsPerQualifiers, recentlyBrowsedKeys, warning); writeProtobuf(searchWsResponse, wsRequest, wsResponse); } @@ -144,7 +155,7 @@ public class SuggestionsAction implements ComponentsWsAction { return index.search(componentIndexQuery); } - private SuggestionsWsResponse toResponse(List<ComponentHitsPerQualifier> componentsPerQualifiers, @Nullable String warning) { + private SuggestionsWsResponse toResponse(List<ComponentHitsPerQualifier> componentsPerQualifiers, Set<String> recentlyBrowsedKeys, @Nullable String warning) { SuggestionsWsResponse.Builder builder = newBuilder(); if (!componentsPerQualifiers.isEmpty()) { Map<String, OrganizationDto> organizationsByUuids; @@ -171,7 +182,7 @@ public class SuggestionsAction implements ComponentsWsAction { .collect(MoreCollectors.uniqueIndex(ComponentDto::uuid)); } builder - .addAllSuggestions(toCategories(componentsPerQualifiers, componentsByUuids, organizationsByUuids, projectsByUuids)) + .addAllSuggestions(toCategories(componentsPerQualifiers, recentlyBrowsedKeys, componentsByUuids, organizationsByUuids, projectsByUuids)) .addAllOrganizations(toOrganizations(organizationsByUuids)) .addAllProjects(toProjects(projectsByUuids)); } @@ -179,12 +190,12 @@ public class SuggestionsAction implements ComponentsWsAction { return builder.build(); } - private static List<Category> toCategories(List<ComponentHitsPerQualifier> componentsPerQualifiers, Map<String, ComponentDto> componentsByUuids, + private static List<Category> toCategories(List<ComponentHitsPerQualifier> componentsPerQualifiers, Set<String> recentlyBrowsedKeys, Map<String, ComponentDto> componentsByUuids, Map<String, OrganizationDto> organizationByUuids, Map<String, ComponentDto> projectsByUuids) { return componentsPerQualifiers.stream().map(qualifier -> { List<Suggestion> suggestions = qualifier.getHits().stream() - .map(hit -> toSuggestion(hit, componentsByUuids, organizationByUuids, projectsByUuids)) + .map(hit -> toSuggestion(hit, recentlyBrowsedKeys, componentsByUuids, organizationByUuids, projectsByUuids)) .collect(toList()); return Category.newBuilder() @@ -195,8 +206,8 @@ public class SuggestionsAction implements ComponentsWsAction { }).collect(toList()); } - private static Suggestion toSuggestion(ComponentHit hit, Map<String, ComponentDto> componentsByUuids, Map<String, OrganizationDto> organizationByUuids, - Map<String, ComponentDto> projectsByUuids) { + private static Suggestion toSuggestion(ComponentHit hit, Set<String> recentlyBrowsedKeys, Map<String, ComponentDto> componentsByUuids, + Map<String, OrganizationDto> organizationByUuids, Map<String, ComponentDto> projectsByUuids) { ComponentDto result = componentsByUuids.get(hit.getUuid()); String organizationKey = organizationByUuids.get(result.getOrganizationUuid()).getKey(); checkState(organizationKey != null, "Organization with uuid '%s' not found", result.getOrganizationUuid()); @@ -207,6 +218,7 @@ public class SuggestionsAction implements ComponentsWsAction { .setKey(result.getKey()) .setName(result.longName()) .setMatch(hit.getHighlightedText().orElse(HtmlEscapers.htmlEscaper().escape(result.longName()))) + .setIsRecentlyBrowsed(recentlyBrowsedKeys.contains(result.getKey())) .build(); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/textsearch/ComponentTextSearchFeature.java b/server/sonar-server/src/main/java/org/sonar/server/es/textsearch/ComponentTextSearchFeature.java index 82c397c8bab..86aa7e9b57e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/es/textsearch/ComponentTextSearchFeature.java +++ b/server/sonar-server/src/main/java/org/sonar/server/es/textsearch/ComponentTextSearchFeature.java @@ -33,6 +33,7 @@ import org.sonar.server.es.textsearch.ComponentTextSearchQueryFactory.ComponentT import static org.elasticsearch.index.query.QueryBuilders.boolQuery; import static org.elasticsearch.index.query.QueryBuilders.matchQuery; +import static org.elasticsearch.index.query.QueryBuilders.termsQuery; import static org.sonar.server.es.DefaultIndexSettingsElement.SEARCH_GRAMS_ANALYZER; import static org.sonar.server.es.DefaultIndexSettingsElement.SEARCH_PREFIX_ANALYZER; import static org.sonar.server.es.DefaultIndexSettingsElement.SEARCH_PREFIX_CASE_INSENSITIVE_ANALYZER; @@ -79,6 +80,12 @@ public enum ComponentTextSearchFeature { return matchQuery(SORTABLE_ANALYZER.subField(query.getFieldKey()), query.getQueryText()) .boost(50f); } + }, + RECENTLY_BROWSED { + @Override + public QueryBuilder getQuery(ComponentTextSearchQuery query) { + return termsQuery(query.getFieldKey(), query.getRecentlyBrowsedKeys()).boost(100f); + } }; public abstract QueryBuilder getQuery(ComponentTextSearchQuery query); diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/textsearch/ComponentTextSearchQueryFactory.java b/server/sonar-server/src/main/java/org/sonar/server/es/textsearch/ComponentTextSearchQueryFactory.java index 81e85881041..46c7ec84f4e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/es/textsearch/ComponentTextSearchQueryFactory.java +++ b/server/sonar-server/src/main/java/org/sonar/server/es/textsearch/ComponentTextSearchQueryFactory.java @@ -19,7 +19,10 @@ */ package org.sonar.server.es.textsearch; +import com.google.common.collect.ImmutableSet; import java.util.Arrays; +import java.util.Collections; +import java.util.Set; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; @@ -51,11 +54,13 @@ public class ComponentTextSearchQueryFactory { private final String queryText; private final String fieldKey; private final String fieldName; + private final Set<String> recentlyBrowsedKeys; private ComponentTextSearchQuery(Builder builder) { this.queryText = builder.queryText; this.fieldKey = builder.fieldKey; this.fieldName = builder.fieldName; + this.recentlyBrowsedKeys = builder.recentlyBrowsedKeys; } public String getQueryText() { @@ -70,6 +75,10 @@ public class ComponentTextSearchQueryFactory { return fieldName; } + public Set<String> getRecentlyBrowsedKeys() { + return recentlyBrowsedKeys; + } + public static Builder builder() { return new Builder(); } @@ -78,6 +87,7 @@ public class ComponentTextSearchQueryFactory { private String queryText; private String fieldKey; private String fieldName; + private Set<String> recentlyBrowsedKeys = Collections.emptySet(); /** * The text search query @@ -103,10 +113,19 @@ public class ComponentTextSearchQueryFactory { return this; } + /** + * Component keys of recently browsed items + */ + public Builder setRecentlyBrowsedKeys(Set<String> recentlyBrowsedKeys) { + this.recentlyBrowsedKeys = ImmutableSet.copyOf(recentlyBrowsedKeys); + return this; + } + public ComponentTextSearchQuery build() { this.queryText = requireNonNull(queryText, "query text cannot be null"); this.fieldKey = requireNonNull(fieldKey, "field key cannot be null"); this.fieldName = requireNonNull(fieldName, "field name cannot be null"); + this.recentlyBrowsedKeys = requireNonNull(recentlyBrowsedKeys, "field recentlyBrowsedKeys cannot be null"); return new ComponentTextSearchQuery(this); } } diff --git a/server/sonar-server/src/main/resources/org/sonar/server/component/ws/components-example-suggestions.json b/server/sonar-server/src/main/resources/org/sonar/server/component/ws/components-example-suggestions.json index 7470618842c..6aece7da3c0 100644 --- a/server/sonar-server/src/main/resources/org/sonar/server/component/ws/components-example-suggestions.json +++ b/server/sonar-server/src/main/resources/org/sonar/server/component/ws/components-example-suggestions.json @@ -7,7 +7,8 @@ "organization": "default-organization", "key": "org.sonarsource:sonarqube", "name": "SonarSource :: SonarQube", - "match": "<mark>Sonar</mark>Source :: SonarQube" + "match": "<mark>Sonar</mark>Source :: SonarQube", + "recentlyBrowsed": true }, { "organization": "default-organization", diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureRecentlyBrowsedTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureRecentlyBrowsedTest.java new file mode 100644 index 00000000000..5ab1ae5561a --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureRecentlyBrowsedTest.java @@ -0,0 +1,58 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.component.index; + +import java.util.Collections; +import org.junit.Before; +import org.junit.Test; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.es.textsearch.ComponentTextSearchFeature; + +import static com.google.common.collect.ImmutableSet.of; +import static org.sonar.api.resources.Qualifiers.PROJECT; + +public class ComponentIndexFeatureRecentlyBrowsedTest extends ComponentIndexTest { + + @Before + public void before() { + features.set(ComponentTextSearchFeature.PREFIX, ComponentTextSearchFeature.EXACT_IGNORE_CASE, ComponentTextSearchFeature.RECENTLY_BROWSED); + } + + @Test + public void search_projects_by_exact_name() { + ComponentDto project1 = indexProject("sonarqube", "SonarQube"); + ComponentDto project2 = indexProject("recent", "SonarQube Recently"); + + ComponentIndexQuery query1 = ComponentIndexQuery.builder() + .setQuery("SonarQube") + .setQualifiers(Collections.singletonList(PROJECT)) + .setRecentlyBrowsedKeys(of(project1.getKey())) + .build(); + assertSearch(query1).containsExactly(uuids(project1, project2)); + + ComponentIndexQuery query2 = ComponentIndexQuery.builder() + .setQuery("SonarQube") + .setQualifiers(Collections.singletonList(PROJECT)) + .setRecentlyBrowsedKeys(of(project2.getKey())) + .build(); + assertSearch(query2).containsExactly(uuids(project2, project1)); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java index b249bb62486..2b0233b0f62 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java @@ -21,6 +21,7 @@ package org.sonar.server.component.ws; import java.util.List; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nullable; import org.assertj.core.groups.Tuple; import org.junit.Before; @@ -48,6 +49,7 @@ import org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Project; import org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Suggestion; import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.joining; import static java.util.stream.IntStream.range; import static java.util.stream.Stream.of; import static org.assertj.core.api.Assertions.assertThat; @@ -59,6 +61,7 @@ import static org.sonar.server.component.ws.SuggestionsAction.EXTENDED_LIMIT; import static org.sonar.server.component.ws.SuggestionsAction.SHORT_INPUT_WARNING; import static org.sonar.server.component.ws.SuggestionsAction.PARAM_MORE; import static org.sonar.server.component.ws.SuggestionsAction.PARAM_QUERY; +import static org.sonar.server.component.ws.SuggestionsAction.PARAM_RECENTLY_BROWSED; import static org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Category; import static org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Organization; import static org.sonarqube.ws.WsComponents.SuggestionsWsResponse.parseFrom; @@ -94,7 +97,14 @@ public class SuggestionsActionTest { assertThat(action.responseExampleAsString()).isNotEmpty(); assertThat(action.params()).extracting(WebService.Param::key).containsExactlyInAnyOrder( PARAM_MORE, - PARAM_QUERY); + PARAM_QUERY, + PARAM_RECENTLY_BROWSED); + + WebService.Param recentlyBrowsed = action.param(PARAM_RECENTLY_BROWSED); + assertThat(recentlyBrowsed.since()).isEqualTo("6.4"); + assertThat(recentlyBrowsed.exampleValue()).isNotEmpty(); + assertThat(recentlyBrowsed.description()).isNotEmpty(); + assertThat(recentlyBrowsed.isRequired()).isFalse(); } @Test @@ -198,6 +208,28 @@ public class SuggestionsActionTest { } @Test + public void should_mark_recently_browsed_items() throws Exception { + ComponentDto project = db.components().insertComponent(newProjectDto(organization)); + ComponentDto module1 = newModuleDto(project).setName("Module1"); + db.components().insertComponent(module1); + ComponentDto module2 = newModuleDto(project).setName("Module2"); + db.components().insertComponent(module2); + componentIndexer.indexProject(project.projectUuid(), ProjectIndexer.Cause.PROJECT_CREATION); + authorizationIndexerTester.allowOnlyAnyone(project); + + SuggestionsWsResponse response = actionTester.newRequest() + .setMethod("POST") + .setParam(PARAM_QUERY, "Module") + .setParam(PARAM_RECENTLY_BROWSED, Stream.of(module1.getKey()).collect(joining(","))) + .executeProtobuf(SuggestionsWsResponse.class); + + assertThat(response.getSuggestionsList()) + .flatExtracting(Category::getSuggestionsList) + .extracting(Suggestion::getIsRecentlyBrowsed) + .containsExactly(true, false); + } + + @Test public void should_propose_to_show_more_results_if_7_projects_are_found() throws Exception { check_proposal_to_show_more_results(7, DEFAULT_LIMIT, 1L, null); } diff --git a/sonar-ws/src/main/protobuf/ws-components.proto b/sonar-ws/src/main/protobuf/ws-components.proto index 638d7193592..0387c061bb4 100644 --- a/sonar-ws/src/main/protobuf/ws-components.proto +++ b/sonar-ws/src/main/protobuf/ws-components.proto @@ -65,6 +65,7 @@ message SuggestionsWsResponse { optional string match = 3; optional string organization = 4; optional string project = 5; + optional bool isRecentlyBrowsed = 6; } message Organization { |