blob: 1a2551cb879330811269db0272de0ea4ef8f267a [file] [log] [blame]
// 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.apache.lucene.util.Version;
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 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)
.expireAfterWrite(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();
@SuppressWarnings("deprecation")
IndexWriterConfig config = new IndexWriterConfig(
Version.LUCENE_4_10_1,
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 TextField(EMAIL, a.getPreferredEmail(), Store.YES));
doc.add(new StringField(EMAIL, a.getPreferredEmail().toLowerCase(),
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);
}
}