Skip to content

Commit

Permalink
SONAR-9079 score favorite components higher in suggestions
Browse files Browse the repository at this point in the history
  • Loading branch information
Daniel Schwarz authored and bartfastiel committed Apr 20, 2017
1 parent af7327e commit 027e541
Show file tree
Hide file tree
Showing 18 changed files with 321 additions and 100 deletions.
Expand Up @@ -22,7 +22,6 @@
import com.google.common.annotations.VisibleForTesting;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
import org.elasticsearch.action.search.SearchRequestBuilder;
Expand All @@ -38,7 +37,6 @@
import org.elasticsearch.search.aggregations.metrics.tophits.InternalTopHits;
import org.elasticsearch.search.aggregations.metrics.tophits.TopHitsBuilder;
import org.elasticsearch.search.highlight.HighlightBuilder;
import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.server.es.EsClient;
import org.sonar.server.es.textsearch.ComponentTextSearchFeature;
import org.sonar.server.es.textsearch.ComponentTextSearchQueryFactory;
Expand Down Expand Up @@ -79,15 +77,15 @@ private static HighlightBuilder.Field createHighlighter() {
return field;
}

public List<ComponentHitsPerQualifier> search(ComponentIndexQuery query) {
public ComponentIndexResults search(ComponentIndexQuery query) {
return search(query, ComponentTextSearchFeature.values());
}

@VisibleForTesting
List<ComponentHitsPerQualifier> search(ComponentIndexQuery query, ComponentTextSearchFeature... features) {
ComponentIndexResults search(ComponentIndexQuery query, ComponentTextSearchFeature... features) {
Collection<String> qualifiers = query.getQualifiers();
if (qualifiers.isEmpty()) {
return Collections.emptyList();
return ComponentIndexResults.newBuilder().build();
}

SearchRequestBuilder request = client
Expand Down Expand Up @@ -128,16 +126,18 @@ private QueryBuilder createQuery(ComponentIndexQuery query, ComponentTextSearchF
.setFieldKey(FIELD_KEY)
.setFieldName(FIELD_NAME)
.setRecentlyBrowsedKeys(query.getRecentlyBrowsedKeys())
.setFavoriteKeys(query.getFavoriteKeys())
.build();
return esQuery.must(ComponentTextSearchQueryFactory.createQuery(componentTextSearchQuery, features));
}

private static List<ComponentHitsPerQualifier> aggregationsToQualifiers(SearchResponse response) {
private static ComponentIndexResults aggregationsToQualifiers(SearchResponse response) {
InternalFilters filtersAgg = response.getAggregations().get(FILTERS_AGGREGATION_NAME);
List<Bucket> buckets = filtersAgg.getBuckets();
return buckets.stream()
.map(ComponentIndex::bucketToQualifier)
.collect(MoreCollectors.toList(buckets.size()));
return ComponentIndexResults.newBuilder()
.setQualifiers(
buckets.stream().map(ComponentIndex::bucketToQualifier))
.build();
}

private static ComponentHitsPerQualifier bucketToQualifier(Bucket bucket) {
Expand Down
Expand Up @@ -34,13 +34,15 @@ public class ComponentIndexQuery {
private final String query;
private final Collection<String> qualifiers;
private final Set<String> recentlyBrowsedKeys;
private final Set<String> favoriteKeys;
@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.favoriteKeys = requireNonNull(builder.favoriteKeys);
this.limit = builder.limit;
}

Expand All @@ -64,10 +66,15 @@ public static Builder builder() {
return new Builder();
}

public Set<String> getFavoriteKeys() {
return favoriteKeys;
}

public static class Builder {
private String query;
private Collection<String> qualifiers = Collections.emptyList();
private Set<String> recentlyBrowsedKeys = Collections.emptySet();
private Set<String> favoriteKeys = Collections.emptySet();
private Integer limit;

private Builder() {
Expand All @@ -89,6 +96,11 @@ public Builder setRecentlyBrowsedKeys(Set<String> recentlyBrowsedKeys) {
return this;
}

public Builder setFavoriteKeys(Set<String> favoriteKeys) {
this.favoriteKeys = Collections.unmodifiableSet(favoriteKeys);
return this;
}

public Builder setLimit(@Nullable Integer limit) {
checkArgument(limit == null || limit > 0, "Limit has to be strictly positive: %s", limit);
this.limit = limit;
Expand Down
@@ -0,0 +1,66 @@
/*
* 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.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Collections.emptyList;
import static java.util.Objects.requireNonNull;

public class ComponentIndexResults {

private final List<ComponentHitsPerQualifier> qualifiers;

private ComponentIndexResults(Builder builder) {
this.qualifiers = requireNonNull(builder.qualifiers);
}

public Stream<ComponentHitsPerQualifier> getQualifiers() {
return qualifiers.stream();
}

public boolean isEmpty() {
return qualifiers.isEmpty();
}

public static Builder newBuilder() {
return new Builder();
}

public static class Builder {

private List<ComponentHitsPerQualifier> qualifiers = emptyList();

private Builder() {
}

public Builder setQualifiers(Stream<ComponentHitsPerQualifier> qualifiers) {
this.qualifiers = qualifiers.collect(Collectors.toList());
return this;
}

public ComponentIndexResults build() {
return new ComponentIndexResults(this);
}
}
}
Expand Up @@ -30,7 +30,6 @@
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.measures.Metric;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.RequestHandler;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.utils.text.JsonWriter;
Expand All @@ -51,7 +50,7 @@
import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01;
import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;

public class AppAction implements RequestHandler {
public class AppAction implements ComponentsWsAction {

private static final String PARAM_COMPONENT_ID = "componentId";
private static final String PARAM_COMPONENT = "component";
Expand All @@ -71,7 +70,8 @@ public AppAction(DbClient dbClient, UserSession userSession, ComponentFinder com
this.componentFinder = componentFinder;
}

void define(WebService.NewController controller) {
@Override
public void define(WebService.NewController controller) {
WebService.NewAction action = controller.createAction("app")
.setDescription("Coverage data required for rendering the component viewer.<br>" +
"Requires the following permission: 'Browse'.")
Expand Down
Expand Up @@ -19,17 +19,16 @@
*/
package org.sonar.server.component.ws;

import java.util.Arrays;
import org.sonar.api.server.ws.WebService;

import static org.sonarqube.ws.client.component.ComponentsWsParameters.CONTROLLER_COMPONENTS;

public class ComponentsWs implements WebService {

private final AppAction appAction;
private final ComponentsWsAction[] actions;

public ComponentsWs(AppAction appAction, ComponentsWsAction... actions) {
this.appAction = appAction;
public ComponentsWs(ComponentsWsAction... actions) {
this.actions = actions;
}

Expand All @@ -40,10 +39,8 @@ public void define(Context context) {
.setDescription("Get information about a component (file, directory, project, ...) and its ancestors or descendants. " +
"Update a project or module key.");

for (ComponentsWsAction action : actions) {
action.define(controller);
}
appAction.define(controller);
Arrays.stream(actions)
.forEach(action -> action.define(controller));

controller.done();
}
Expand Down
Expand Up @@ -40,7 +40,9 @@
import org.sonar.server.component.index.ComponentHitsPerQualifier;
import org.sonar.server.component.index.ComponentIndex;
import org.sonar.server.component.index.ComponentIndexQuery;
import org.sonar.server.component.index.ComponentIndexResults;
import org.sonar.server.es.textsearch.ComponentTextSearchFeature;
import org.sonar.server.favorite.FavoriteFinder;
import org.sonarqube.ws.WsComponents.SuggestionsWsResponse;
import org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Category;
import org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Project;
Expand Down Expand Up @@ -71,12 +73,14 @@ public class SuggestionsAction implements ComponentsWsAction {
static final int EXTENDED_LIMIT = 20;

private final ComponentIndex index;
private final FavoriteFinder favoriteFinder;

private DbClient dbClient;

public SuggestionsAction(DbClient dbClient, ComponentIndex index) {
public SuggestionsAction(DbClient dbClient, ComponentIndex index, FavoriteFinder favoriteFinder) {
this.dbClient = dbClient;
this.index = index;
this.favoriteFinder = favoriteFinder;
}

@Override
Expand Down Expand Up @@ -120,13 +124,17 @@ public void handle(Request wsRequest, Response wsResponse) throws Exception {
String more = wsRequest.param(PARAM_MORE);
List<String> recentlyBrowsedParam = wsRequest.paramAsStrings(PARAM_RECENTLY_BROWSED);
Set<String> recentlyBrowsedKeys = recentlyBrowsedParam == null ? emptySet() : copyOf(recentlyBrowsedParam);
Set<String> favoriteKeys = favoriteFinder.list().stream().map(ComponentDto::getKey).collect(Collectors.toSet());

ComponentIndexQuery.Builder queryBuilder = ComponentIndexQuery.builder().setQuery(query).setRecentlyBrowsedKeys(recentlyBrowsedKeys);
ComponentIndexQuery.Builder queryBuilder = ComponentIndexQuery.builder()
.setQuery(query)
.setRecentlyBrowsedKeys(recentlyBrowsedKeys)
.setFavoriteKeys(favoriteKeys);

List<ComponentHitsPerQualifier> componentsPerQualifiers = getComponentsPerQualifiers(more, queryBuilder);
ComponentIndexResults componentsPerQualifiers = getComponentsPerQualifiers(more, queryBuilder);
String warning = getWarning(query);

SuggestionsWsResponse searchWsResponse = toResponse(componentsPerQualifiers, recentlyBrowsedKeys, warning);
SuggestionsWsResponse searchWsResponse = toResponse(componentsPerQualifiers, recentlyBrowsedKeys, favoriteKeys, warning);
writeProtobuf(searchWsResponse, wsRequest, wsResponse);
}

Expand All @@ -138,31 +146,29 @@ private static String getWarning(String query) {
return null;
}

private List<ComponentHitsPerQualifier> getComponentsPerQualifiers(String more, ComponentIndexQuery.Builder queryBuilder) {
List<ComponentHitsPerQualifier> componentsPerQualifiers;
private ComponentIndexResults getComponentsPerQualifiers(@Nullable String more, ComponentIndexQuery.Builder queryBuilder) {
if (more == null) {
queryBuilder.setQualifiers(stream(SuggestionCategory.values()).map(SuggestionCategory::getQualifier).collect(Collectors.toList()))
.setLimit(DEFAULT_LIMIT);
} else {
queryBuilder.setQualifiers(singletonList(SuggestionCategory.getByName(more).getQualifier()))
.setLimit(EXTENDED_LIMIT);
}
componentsPerQualifiers = searchInIndex(queryBuilder.build());
return componentsPerQualifiers;
return searchInIndex(queryBuilder.build());
}

private List<ComponentHitsPerQualifier> searchInIndex(ComponentIndexQuery componentIndexQuery) {
private ComponentIndexResults searchInIndex(ComponentIndexQuery componentIndexQuery) {
return index.search(componentIndexQuery);
}

private SuggestionsWsResponse toResponse(List<ComponentHitsPerQualifier> componentsPerQualifiers, Set<String> recentlyBrowsedKeys, @Nullable String warning) {
private SuggestionsWsResponse toResponse(ComponentIndexResults componentsPerQualifiers, Set<String> recentlyBrowsedKeys, Set<String> favoriteKeys, @Nullable String warning) {
SuggestionsWsResponse.Builder builder = newBuilder();
if (!componentsPerQualifiers.isEmpty()) {
Map<String, OrganizationDto> organizationsByUuids;
Map<String, ComponentDto> componentsByUuids;
Map<String, ComponentDto> projectsByUuids;
try (DbSession dbSession = dbClient.openSession(false)) {
Set<String> componentUuids = componentsPerQualifiers.stream()
Set<String> componentUuids = componentsPerQualifiers.getQualifiers()
.map(ComponentHitsPerQualifier::getHits)
.flatMap(Collection::stream)
.map(ComponentHit::getUuid)
Expand All @@ -182,20 +188,20 @@ private SuggestionsWsResponse toResponse(List<ComponentHitsPerQualifier> compone
.collect(MoreCollectors.uniqueIndex(ComponentDto::uuid));
}
builder
.addAllSuggestions(toCategories(componentsPerQualifiers, recentlyBrowsedKeys, componentsByUuids, organizationsByUuids, projectsByUuids))
.addAllSuggestions(toCategories(componentsPerQualifiers, recentlyBrowsedKeys, favoriteKeys, componentsByUuids, organizationsByUuids, projectsByUuids))
.addAllOrganizations(toOrganizations(organizationsByUuids))
.addAllProjects(toProjects(projectsByUuids));
}
ofNullable(warning).ifPresent(builder::setWarning);
return builder.build();
}

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 -> {
private static List<Category> toCategories(ComponentIndexResults componentsPerQualifiers, Set<String> recentlyBrowsedKeys, Set<String> favoriteKeys,
Map<String, ComponentDto> componentsByUuids, Map<String, OrganizationDto> organizationByUuids, Map<String, ComponentDto> projectsByUuids) {
return componentsPerQualifiers.getQualifiers().map(qualifier -> {

List<Suggestion> suggestions = qualifier.getHits().stream()
.map(hit -> toSuggestion(hit, recentlyBrowsedKeys, componentsByUuids, organizationByUuids, projectsByUuids))
.map(hit -> toSuggestion(hit, recentlyBrowsedKeys, favoriteKeys, componentsByUuids, organizationByUuids, projectsByUuids))
.collect(toList());

return Category.newBuilder()
Expand All @@ -206,7 +212,7 @@ private static List<Category> toCategories(List<ComponentHitsPerQualifier> compo
}).collect(toList());
}

private static Suggestion toSuggestion(ComponentHit hit, Set<String> recentlyBrowsedKeys, Map<String, ComponentDto> componentsByUuids,
private static Suggestion toSuggestion(ComponentHit hit, Set<String> recentlyBrowsedKeys, Set<String> favoriteKeys, 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();
Expand All @@ -219,6 +225,7 @@ private static Suggestion toSuggestion(ComponentHit hit, Set<String> recentlyBro
.setName(result.longName())
.setMatch(hit.getHighlightedText().orElse(HtmlEscapers.htmlEscaper().escape(result.longName())))
.setIsRecentlyBrowsed(recentlyBrowsedKeys.contains(result.getKey()))
.setIsFavorite(favoriteKeys.contains(result.getKey()))
.build();
}

Expand Down
Expand Up @@ -21,6 +21,7 @@

import java.util.Arrays;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
import org.apache.commons.lang.StringUtils;
Expand Down Expand Up @@ -83,12 +84,32 @@ public QueryBuilder getQuery(ComponentTextSearchQuery query) {
},
RECENTLY_BROWSED {
@Override
public QueryBuilder getQuery(ComponentTextSearchQuery query) {
return termsQuery(query.getFieldKey(), query.getRecentlyBrowsedKeys()).boost(100f);
public Stream<QueryBuilder> getQueries(ComponentTextSearchQuery query) {
Set<String> recentlyBrowsedKeys = query.getRecentlyBrowsedKeys();
if (recentlyBrowsedKeys.isEmpty()) {
return Stream.empty();
}
return Stream.of(termsQuery(query.getFieldKey(), recentlyBrowsedKeys).boost(100f));
}
},
FAVORITE {
@Override
public Stream<QueryBuilder> getQueries(ComponentTextSearchQuery query) {
Set<String> favoriteKeys = query.getFavoriteKeys();
if (favoriteKeys.isEmpty()) {
return Stream.empty();
}
return Stream.of(termsQuery(query.getFieldKey(), favoriteKeys).boost(1000f));
}
};

public abstract QueryBuilder getQuery(ComponentTextSearchQuery query);
public Stream<QueryBuilder> getQueries(ComponentTextSearchQuery query) {
return Stream.of(getQuery(query));
}

public QueryBuilder getQuery(ComponentTextSearchQuery query) {
throw new UnsupportedOperationException();
}

public static Stream<String> split(String queryText) {
return Arrays.stream(
Expand Down

0 comments on commit 027e541

Please sign in to comment.