Use account index for reviewer suggestion

Change-Id: Ia7777ac593723272e44705d064fae6d9446b40fb
Signed-off-by: Edwin Kempin <ekempin@google.com>
This commit is contained in:
Edwin Kempin
2016-07-01 14:41:35 +02:00
parent cdfd4d3f44
commit e2de9ccdc2
8 changed files with 134 additions and 40 deletions

View File

@@ -27,6 +27,7 @@ import com.google.gerrit.server.account.AccountByEmailCache;
import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.GroupCache; import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.VersionedAuthorizedKeys; import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.index.account.AccountIndexer;
import com.google.gerrit.server.ssh.SshKeyCache; import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.gwtorm.server.SchemaFactory; import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject; import com.google.inject.Inject;
@@ -52,6 +53,7 @@ public class AccountCreator {
private final SshKeyCache sshKeyCache; private final SshKeyCache sshKeyCache;
private final AccountCache accountCache; private final AccountCache accountCache;
private final AccountByEmailCache byEmailCache; private final AccountByEmailCache byEmailCache;
private final AccountIndexer indexer;
@Inject @Inject
AccountCreator(SchemaFactory<ReviewDb> schema, AccountCreator(SchemaFactory<ReviewDb> schema,
@@ -59,7 +61,8 @@ public class AccountCreator {
GroupCache groupCache, GroupCache groupCache,
SshKeyCache sshKeyCache, SshKeyCache sshKeyCache,
AccountCache accountCache, AccountCache accountCache,
AccountByEmailCache byEmailCache) { AccountByEmailCache byEmailCache,
AccountIndexer indexer) {
accounts = new HashMap<>(); accounts = new HashMap<>();
reviewDbProvider = schema; reviewDbProvider = schema;
this.authorizedKeys = authorizedKeys; this.authorizedKeys = authorizedKeys;
@@ -67,6 +70,7 @@ public class AccountCreator {
this.sshKeyCache = sshKeyCache; this.sshKeyCache = sshKeyCache;
this.accountCache = accountCache; this.accountCache = accountCache;
this.byEmailCache = byEmailCache; this.byEmailCache = byEmailCache;
this.indexer = indexer;
} }
public synchronized TestAccount create(String username, String email, public synchronized TestAccount create(String username, String email,
@@ -113,6 +117,8 @@ public class AccountCreator {
accountCache.evictByUsername(username); accountCache.evictByUsername(username);
byEmailCache.evict(email); byEmailCache.evict(email);
indexer.index(id);
account = account =
new TestAccount(id, username, email, fullName, sshKey, httpPass); new TestAccount(id, username, email, fullName, sshKey, httpPass);
accounts.put(username, account); accounts.put(username, account);

View File

@@ -170,6 +170,15 @@ public class SuggestReviewersIT extends AbstractDaemonTest {
@Test @Test
@GerritConfig(name = "suggest.fullTextSearch", value = "true") @GerritConfig(name = "suggest.fullTextSearch", value = "true")
public void suggestReviewersFullTextSearch() throws Exception { public void suggestReviewersFullTextSearch() throws Exception {
testSuggestReviewersFullTextSearch();
}
@Test
public void suggestReviewersFullTextSearchWithAccountIndex() throws Exception {
testSuggestReviewersFullTextSearch();
}
private void testSuggestReviewersFullTextSearch() throws Exception {
String changeId = createChange().getChangeId(); String changeId = createChange().getChangeId();
List<SuggestedReviewerInfo> reviewers; List<SuggestedReviewerInfo> reviewers;

View File

@@ -14,13 +14,13 @@
package com.google.gerrit.client.change; package com.google.gerrit.client.change;
import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.admin.Util; import com.google.gerrit.client.admin.Util;
import com.google.gerrit.client.changes.ChangeApi; import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.groups.GroupBaseInfo; import com.google.gerrit.client.groups.GroupBaseInfo;
import com.google.gerrit.client.info.AccountInfo; import com.google.gerrit.client.info.AccountInfo;
import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.Natives; import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.AccountSuggestOracle;
import com.google.gerrit.client.ui.SuggestAfterTypingNCharsOracle; import com.google.gerrit.client.ui.SuggestAfterTypingNCharsOracle;
import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JavaScriptObject;
@@ -42,7 +42,7 @@ public class ReviewerSuggestOracle extends SuggestAfterTypingNCharsOracle {
public void onSuccess(JsArray<SuggestReviewerInfo> result) { public void onSuccess(JsArray<SuggestReviewerInfo> result) {
List<RestReviewerSuggestion> r = new ArrayList<>(result.length()); List<RestReviewerSuggestion> r = new ArrayList<>(result.length());
for (SuggestReviewerInfo reviewer : Natives.asList(result)) { for (SuggestReviewerInfo reviewer : Natives.asList(result)) {
r.add(new RestReviewerSuggestion(reviewer)); r.add(new RestReviewerSuggestion(reviewer, req.getQuery()));
} }
cb.onSuggestionsReady(req, new Response(r)); cb.onSuggestionsReady(req, new Response(r));
} }
@@ -60,29 +60,29 @@ public class ReviewerSuggestOracle extends SuggestAfterTypingNCharsOracle {
} }
private static class RestReviewerSuggestion implements Suggestion { private static class RestReviewerSuggestion implements Suggestion {
private final SuggestReviewerInfo reviewer; private final String displayString;
private final String replacementString;
RestReviewerSuggestion(final SuggestReviewerInfo reviewer) { RestReviewerSuggestion(SuggestReviewerInfo reviewer, String query) {
this.reviewer = reviewer; if (reviewer.account() != null) {
this.replacementString = AccountSuggestOracle.AccountSuggestion
.format(reviewer.account(), query);
this.displayString = replacementString;
} else {
this.replacementString = reviewer.group().name();
this.displayString =
replacementString + " (" + Util.C.suggestedGroupLabel() + ")";
}
} }
@Override @Override
public String getDisplayString() { public String getDisplayString() {
if (reviewer.account() != null) { return displayString;
return FormatUtil.nameEmail(reviewer.account());
}
return reviewer.group().name()
+ " ("
+ Util.C.suggestedGroupLabel()
+ ")";
} }
@Override @Override
public String getReplacementString() { public String getReplacementString() {
if (reviewer.account() != null) { return replacementString;
return FormatUtil.nameEmail(reviewer.account());
}
return reviewer.group().name();
} }
} }

View File

@@ -42,25 +42,11 @@ public class AccountSuggestOracle extends SuggestAfterTypingNCharsOracle {
}); });
} }
private static class AccountSuggestion implements SuggestOracle.Suggestion { public static class AccountSuggestion implements SuggestOracle.Suggestion {
private final String suggestion; private final String suggestion;
AccountSuggestion(AccountInfo info, String query) { AccountSuggestion(AccountInfo info, String query) {
String s = FormatUtil.nameEmail(info); this.suggestion = format(info, query);
if (!s.toLowerCase().contains(query.toLowerCase())
&& info.secondaryEmails() != null) {
for (String email : Natives.asList(info.secondaryEmails())) {
AccountInfo info2 = AccountInfo.create(info._accountId(), info.name(),
email, info.username());
String s2 = FormatUtil.nameEmail(info2);
if (s2.toLowerCase().contains(query.toLowerCase())) {
s = s2;
break;
}
}
}
this.suggestion = s;
} }
@Override @Override
@@ -72,5 +58,30 @@ public class AccountSuggestOracle extends SuggestAfterTypingNCharsOracle {
public String getReplacementString() { public String getReplacementString() {
return suggestion; return suggestion;
} }
public static String format(AccountInfo info, String query) {
String s = FormatUtil.nameEmail(info);
if (!containsQuery(s, query) && info.secondaryEmails() != null) {
for (String email : Natives.asList(info.secondaryEmails())) {
AccountInfo info2 = AccountInfo.create(info._accountId(), info.name(),
email, info.username());
String s2 = FormatUtil.nameEmail(info2);
if (containsQuery(s2, query)) {
s = s2;
break;
}
}
}
return s;
}
private static boolean containsQuery(String s, String query) {
for (String qterm : query.split("\\s+")) {
if (!s.toLowerCase().contains(qterm.toLowerCase())) {
return false;
}
}
return true;
}
} }
} }

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.server;
import com.google.common.base.Function; import com.google.common.base.Function;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Ordering; import com.google.common.collect.Ordering;
@@ -35,20 +36,30 @@ import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountControl; import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountLoader; import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.GroupBackend; import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupMembers; import com.google.gerrit.server.account.GroupMembers;
import com.google.gerrit.server.account.AccountDirectory.FillOptions;
import com.google.gerrit.server.change.PostReviewers; import com.google.gerrit.server.change.PostReviewers;
import com.google.gerrit.server.change.ReviewerSuggestionCache; import com.google.gerrit.server.change.ReviewerSuggestionCache;
import com.google.gerrit.server.change.SuggestReviewers; import com.google.gerrit.server.change.SuggestReviewers;
import com.google.gerrit.server.index.account.AccountIndex;
import com.google.gerrit.server.index.account.AccountIndexCollection;
import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.query.QueryResult;
import com.google.gerrit.server.query.account.AccountQueryBuilder;
import com.google.gerrit.server.query.account.AccountQueryProcessor;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@@ -74,6 +85,9 @@ public class ReviewersUtil {
}); });
private final AccountLoader accountLoader; private final AccountLoader accountLoader;
private final AccountCache accountCache; private final AccountCache accountCache;
private final AccountIndexCollection indexes;
private final AccountQueryBuilder queryBuilder;
private final AccountQueryProcessor queryProcessor;
private final ReviewerSuggestionCache reviewerSuggestionCache; private final ReviewerSuggestionCache reviewerSuggestionCache;
private final AccountControl accountControl; private final AccountControl accountControl;
private final Provider<ReviewDb> dbProvider; private final Provider<ReviewDb> dbProvider;
@@ -84,14 +98,22 @@ public class ReviewersUtil {
@Inject @Inject
ReviewersUtil(AccountLoader.Factory accountLoaderFactory, ReviewersUtil(AccountLoader.Factory accountLoaderFactory,
AccountCache accountCache, AccountCache accountCache,
AccountIndexCollection indexes,
AccountQueryBuilder queryBuilder,
AccountQueryProcessor queryProcessor,
ReviewerSuggestionCache reviewerSuggestionCache, ReviewerSuggestionCache reviewerSuggestionCache,
AccountControl.Factory accountControlFactory, AccountControl.Factory accountControlFactory,
Provider<ReviewDb> dbProvider, Provider<ReviewDb> dbProvider,
GroupBackend groupBackend, GroupBackend groupBackend,
GroupMembers.Factory groupMembersFactory, GroupMembers.Factory groupMembersFactory,
Provider<CurrentUser> currentUser) { Provider<CurrentUser> currentUser) {
this.accountLoader = accountLoaderFactory.create(true); Set<FillOptions> fillOptions = EnumSet.of(FillOptions.SECONDARY_EMAILS);
fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
this.accountLoader = accountLoaderFactory.create(fillOptions);
this.accountCache = accountCache; this.accountCache = accountCache;
this.indexes = indexes;
this.queryBuilder = queryBuilder;
this.queryProcessor = queryProcessor;
this.reviewerSuggestionCache = reviewerSuggestionCache; this.reviewerSuggestionCache = reviewerSuggestionCache;
this.accountControl = accountControlFactory.get(); this.accountControl = accountControlFactory.get();
this.dbProvider = dbProvider; this.dbProvider = dbProvider;
@@ -122,11 +144,12 @@ public class ReviewersUtil {
return Collections.emptyList(); return Collections.emptyList();
} }
List<AccountInfo> suggestedAccounts; Collection<AccountInfo> suggestedAccounts;
if (useFullTextSearch) { if (useFullTextSearch) {
suggestedAccounts = suggestAccountFullTextSearch(suggestReviewers, visibilityControl); suggestedAccounts =
suggestAccountFullTextSearch(suggestReviewers, visibilityControl);
} else { } else {
suggestedAccounts = suggestAccount(suggestReviewers, visibilityControl); suggestedAccounts = suggestAccounts(suggestReviewers, visibilityControl);
} }
List<SuggestedReviewerInfo> reviewer = new ArrayList<>(); List<SuggestedReviewerInfo> reviewer = new ArrayList<>();
@@ -173,9 +196,39 @@ public class ReviewersUtil {
return results; return results;
} }
private List<AccountInfo> suggestAccount(SuggestReviewers suggestReviewers, private Collection<AccountInfo> suggestAccounts(SuggestReviewers suggestReviewers,
VisibilityControl visibilityControl) VisibilityControl visibilityControl)
throws OrmException { throws OrmException {
AccountIndex searchIndex = indexes.getSearchIndex();
if (searchIndex != null) {
return suggestAccountsFromIndex(suggestReviewers);
}
return suggestAccountsFromDb(suggestReviewers, visibilityControl);
}
private Collection<AccountInfo> suggestAccountsFromIndex(
SuggestReviewers suggestReviewers) throws OrmException {
try {
Map<Account.Id, AccountInfo> matches = new LinkedHashMap<>();
QueryResult<AccountState> result = queryProcessor
.setLimit(suggestReviewers.getLimit())
.query(queryBuilder.defaultQuery(suggestReviewers.getQuery()));
for (AccountState accountState : result.entities()) {
Account.Id id = accountState.getAccount().getId();
matches.put(id, accountLoader.get(id));
}
accountLoader.fill();
return matches.values();
} catch (QueryParseException e) {
return ImmutableList.of();
}
}
private Collection<AccountInfo> suggestAccountsFromDb(
SuggestReviewers suggestReviewers, VisibilityControl visibilityControl)
throws OrmException {
String query = suggestReviewers.getQuery(); String query = suggestReviewers.getQuery();
int limit = suggestReviewers.getLimit(); int limit = suggestReviewers.getLimit();

View File

@@ -197,7 +197,7 @@ public class QueryAccounts implements RestReadView<TopLevelResource> {
try { try {
Predicate<AccountState> queryPred; Predicate<AccountState> queryPred;
if (suggest) { if (suggest) {
queryPred = queryBuilder.defaultField(query); queryPred = queryBuilder.defaultQuery(query);
queryProcessor.setLimit(suggestLimit); queryProcessor.setLimit(suggestLimit);
} else { } else {
queryPred = queryBuilder.parse(query); queryPred = queryBuilder.parse(query);

View File

@@ -14,6 +14,8 @@
package com.google.gerrit.server.query.account; package com.google.gerrit.server.query.account;
import com.google.common.base.Function;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.primitives.Ints; import com.google.common.primitives.Ints;
import com.google.gerrit.common.errors.NotSignedInException; import com.google.gerrit.common.errors.NotSignedInException;
@@ -122,8 +124,19 @@ public class AccountQueryBuilder extends QueryBuilder<AccountState> {
return AccountPredicates.username(username); return AccountPredicates.username(username);
} }
public Predicate<AccountState> defaultQuery(String query) {
return Predicate.and(
Lists.transform(Splitter.on(' ').omitEmptyStrings().splitToList(query),
new Function<String, Predicate<AccountState>>() {
@Override
public Predicate<AccountState> apply(String s) {
return defaultField(s);
}
}));
}
@Override @Override
public Predicate<AccountState> defaultField(String query) { protected Predicate<AccountState> defaultField(String query) {
// Adapt the capacity of this list when adding more default predicates. // Adapt the capacity of this list when adding more default predicates.
List<Predicate<AccountState>> preds = Lists.newArrayListWithCapacity(4); List<Predicate<AccountState>> preds = Lists.newArrayListWithCapacity(4);
if ("self".equalsIgnoreCase(query)) { if ("self".equalsIgnoreCase(query)) {

View File

@@ -255,6 +255,8 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
assertQuery("Doe", user1); assertQuery("Doe", user1);
assertQuery("doe", user1); assertQuery("doe", user1);
assertQuery("DOE", user1); assertQuery("DOE", user1);
assertQuery("Jo Do", user1);
assertQuery("jo do", user1);
assertQuery("self", currentUserInfo, user3); assertQuery("self", currentUserInfo, user3);
assertQuery("name:John", user1); assertQuery("name:John", user1);
assertQuery("name:john", user1); assertQuery("name:john", user1);