aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Schwarz <daniel.schwarz@sonarsource.com>2017-04-14 17:41:47 +0200
committerDaniel Schwarz <bartfastiel@users.noreply.github.com>2017-04-20 09:48:52 +0200
commitaf7327e59b88d1a7738b28bad169941fdb2845f6 (patch)
treeea8e9d4a0a6a6e2d9ad6dfedaa0eaea6895d4954
parent38030307f50b11df0be8872ee6402fdaf634fab8 (diff)
downloadsonarqube-af7327e59b88d1a7738b28bad169941fdb2845f6.tar.gz
sonarqube-af7327e59b88d1a7738b28bad169941fdb2845f6.zip
SONAR-9074 score recently browsed components higher in suggestions
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndex.java1
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexQuery.java13
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java28
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/es/textsearch/ComponentTextSearchFeature.java7
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/es/textsearch/ComponentTextSearchQueryFactory.java19
-rw-r--r--server/sonar-server/src/main/resources/org/sonar/server/component/ws/components-example-suggestions.json3
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureRecentlyBrowsedTest.java58
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java34
-rw-r--r--sonar-ws/src/main/protobuf/ws-components.proto1
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 {