| // Copyright (C) 2014 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.server.change; |
| |
| import com.google.common.base.Splitter; |
| import com.google.common.cache.CacheBuilder; |
| import com.google.common.cache.CacheLoader; |
| import com.google.common.cache.LoadingCache; |
| import com.google.gerrit.extensions.common.AccountInfo; |
| import com.google.gerrit.reviewdb.client.Account; |
| import com.google.gerrit.reviewdb.server.AccountExternalIdAccess; |
| import com.google.gerrit.reviewdb.server.ReviewDb; |
| import com.google.gerrit.server.account.AccountState; |
| import com.google.gerrit.server.config.ConfigUtil; |
| import com.google.gerrit.server.config.GerritServerConfig; |
| import com.google.gwtorm.server.OrmException; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.Singleton; |
| |
| import org.apache.lucene.analysis.standard.StandardAnalyzer; |
| import org.apache.lucene.analysis.util.CharArraySet; |
| import org.apache.lucene.document.Document; |
| import org.apache.lucene.document.Field.Store; |
| import org.apache.lucene.document.IntField; |
| import org.apache.lucene.document.StringField; |
| import org.apache.lucene.document.TextField; |
| import org.apache.lucene.index.DirectoryReader; |
| import org.apache.lucene.index.IndexWriter; |
| import org.apache.lucene.index.IndexWriterConfig; |
| import org.apache.lucene.index.IndexWriterConfig.OpenMode; |
| import org.apache.lucene.index.Term; |
| import org.apache.lucene.search.BooleanClause.Occur; |
| import org.apache.lucene.search.BooleanQuery; |
| import org.apache.lucene.search.IndexSearcher; |
| import org.apache.lucene.search.PrefixQuery; |
| import org.apache.lucene.search.ScoreDoc; |
| import org.apache.lucene.search.TopDocs; |
| import org.apache.lucene.store.RAMDirectory; |
| import org.eclipse.jgit.lib.Config; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.IOException; |
| import java.util.Collections; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.TimeUnit; |
| /** |
| * The suggest oracle may be called many times in rapid succession during the |
| * course of one operation. |
| * It would be easy to have a simple {@code Cache<Boolean, List<Account>>} |
| * with a short expiration time of 30s. |
| * Cache only has a single key we're just using Cache for the expiration behavior. |
| */ |
| @Singleton |
| public class ReviewerSuggestionCache { |
| private static final Logger log = LoggerFactory |
| .getLogger(ReviewerSuggestionCache.class); |
| |
| private static final String ID = "id"; |
| private static final String NAME = "name"; |
| private static final String EMAIL = "email"; |
| private static final String USERNAME = "username"; |
| private static final String[] ALL = {ID, NAME, EMAIL, USERNAME}; |
| |
| private final LoadingCache<Boolean, IndexSearcher> cache; |
| private final Provider<ReviewDb> db; |
| |
| @Inject |
| ReviewerSuggestionCache(Provider<ReviewDb> db, |
| @GerritServerConfig Config cfg) { |
| this.db = db; |
| long expiration = ConfigUtil.getTimeUnit(cfg, |
| "suggest", null, "fullTextSearchRefresh", |
| TimeUnit.HOURS.toMillis(1), |
| TimeUnit.MILLISECONDS); |
| this.cache = |
| CacheBuilder.newBuilder().maximumSize(1) |
| .refreshAfterWrite(expiration, TimeUnit.MILLISECONDS) |
| .build(new CacheLoader<Boolean, IndexSearcher>() { |
| @Override |
| public IndexSearcher load(Boolean key) throws Exception { |
| return index(); |
| } |
| }); |
| } |
| |
| List<AccountInfo> search(String query, int n) throws IOException { |
| IndexSearcher searcher = get(); |
| if (searcher == null) { |
| return Collections.emptyList(); |
| } |
| |
| List<String> segments = Splitter.on(' ').omitEmptyStrings().splitToList( |
| query.toLowerCase()); |
| BooleanQuery q = new BooleanQuery(); |
| for (String field : ALL) { |
| BooleanQuery and = new BooleanQuery(); |
| for (String s : segments) { |
| and.add(new PrefixQuery(new Term(field, s)), Occur.MUST); |
| } |
| q.add(and, Occur.SHOULD); |
| } |
| |
| TopDocs results = searcher.search(q, n); |
| ScoreDoc[] hits = results.scoreDocs; |
| |
| List<AccountInfo> result = new LinkedList<>(); |
| |
| for (ScoreDoc h : hits) { |
| Document doc = searcher.doc(h.doc); |
| |
| AccountInfo info = new AccountInfo( |
| doc.getField(ID).numericValue().intValue()); |
| info.name = doc.get(NAME); |
| info.email = doc.get(EMAIL); |
| info.username = doc.get(USERNAME); |
| result.add(info); |
| } |
| |
| return result; |
| } |
| |
| private IndexSearcher get() { |
| try { |
| return cache.get(true); |
| } catch (ExecutionException e) { |
| log.warn("Cannot fetch reviewers from cache", e); |
| return null; |
| } |
| } |
| |
| private IndexSearcher index() throws IOException, OrmException { |
| RAMDirectory idx = new RAMDirectory(); |
| IndexWriterConfig config = new IndexWriterConfig( |
| new StandardAnalyzer(CharArraySet.EMPTY_SET)); |
| config.setOpenMode(OpenMode.CREATE); |
| |
| try (IndexWriter writer = new IndexWriter(idx, config)) { |
| for (Account a : db.get().accounts().all()) { |
| if (a.isActive()) { |
| addAccount(writer, a); |
| } |
| } |
| } |
| |
| return new IndexSearcher(DirectoryReader.open(idx)); |
| } |
| |
| private void addAccount(IndexWriter writer, Account a) |
| throws IOException, OrmException { |
| Document doc = new Document(); |
| doc.add(new IntField(ID, a.getId().get(), Store.YES)); |
| if (a.getFullName() != null) { |
| doc.add(new TextField(NAME, a.getFullName(), Store.YES)); |
| } |
| if (a.getPreferredEmail() != null) { |
| doc.add(new StringField(EMAIL, a.getPreferredEmail().toLowerCase(), |
| Store.YES)); |
| doc.add(new TextField(EMAIL, a.getPreferredEmail(), Store.YES)); |
| } |
| AccountExternalIdAccess extIdAccess = db.get().accountExternalIds(); |
| String username = AccountState.getUserName( |
| extIdAccess.byAccount(a.getId()).toList()); |
| if (username != null) { |
| doc.add(new StringField(USERNAME, username, Store.YES)); |
| } |
| writer.addDocument(doc); |
| } |
| } |