Schema for secondary index over accounts

Support searching over the basic fields in Account (ID, registered
on, etc.), as well as more complicated fields like AccountExternalId
and a fuzzy search over name parts.

Change-Id: I3eea825b0e6aa19d87311194ce386a0f07db41e5
This commit is contained in:
Dave Borowitz
2016-03-15 16:28:48 +01:00
parent 7dea9946dc
commit 83415257b4
3 changed files with 224 additions and 7 deletions

View File

@@ -30,8 +30,8 @@ import org.eclipse.jgit.lib.PersonIdent;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@@ -81,13 +81,28 @@ public class SchemaUtil {
if (person == null) {
return ImmutableSet.of();
}
HashSet<String> parts = Sets.newHashSet();
String email = person.getEmailAddress().toLowerCase();
parts.add(email);
parts.addAll(Arrays.asList(email.split("@")));
return getPersonParts(
person.getName(),
Collections.singleton(person.getEmailAddress()));
}
public static Set<String> getPersonParts(String name,
Iterable<String> emails) {
Splitter at = Splitter.on('@');
Splitter s = Splitter.on(CharMatcher.anyOf("@.- ")).omitEmptyStrings();
Iterables.addAll(parts, s.split(email));
Iterables.addAll(parts, s.split(person.getName().toLowerCase()));
HashSet<String> parts = Sets.newHashSet();
for (String email : emails) {
if (email == null) {
continue;
}
String lowerEmail = email.toLowerCase();
parts.add(lowerEmail);
Iterables.addAll(parts, at.split(lowerEmail));
Iterables.addAll(parts, s.split(lowerEmail));
}
if (name != null) {
Iterables.addAll(parts, s.split(name.toLowerCase()));
}
return parts;
}

View File

@@ -0,0 +1,140 @@
// Copyright (C) 2016 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.index.account;
import com.google.common.base.Function;
import com.google.common.base.Predicates;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Iterables;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.FieldType;
import com.google.gerrit.server.index.SchemaUtil;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.Set;
/** Secondary index schemas for accounts. */
public class AccountField {
public static final FieldDef<AccountState, Integer> ID =
new FieldDef.Single<AccountState, Integer>(
"id", FieldType.INTEGER, true) {
@Override
public Integer get(AccountState input, FillArgs args) {
return input.getAccount().getId().get();
}
};
public static final FieldDef<AccountState, Iterable<String>> EXTERNAL_ID =
new FieldDef.Repeatable<AccountState, String>(
"external_id", FieldType.EXACT, false) {
@Override
public Iterable<String> get(AccountState input, FillArgs args) {
return Iterables.transform(
input.getExternalIds(),
new Function<AccountExternalId, String>() {
@Override
public String apply(AccountExternalId in) {
return in.getKey().get();
}
});
}
};
/** Fuzzy prefix match on name and email parts. */
public static final FieldDef<AccountState, Iterable<String>> NAME_PART =
new FieldDef.Repeatable<AccountState, String>(
"name", FieldType.PREFIX, false) {
@Override
public Iterable<String> get(AccountState input, FillArgs args) {
String fullName = input.getAccount().getFullName();
Set<String> parts = SchemaUtil.getPersonParts(
fullName,
Iterables.transform(
input.getExternalIds(),
new Function<AccountExternalId, String>() {
@Override
public String apply(AccountExternalId in) {
return in.getEmailAddress();
}
}));
// Additional values not currently added by getPersonParts.
// TODO(dborowitz): Move to getPersonParts and remove this hack.
if (fullName != null) {
parts.add(fullName);
}
return parts;
}
};
public static final FieldDef<AccountState, String> ACTIVE =
new FieldDef.Single<AccountState, String>(
"inactive", FieldType.EXACT, false) {
@Override
public String get(AccountState input, FillArgs args) {
return input.getAccount().isActive() ? "1" : "0";
}
};
public static final FieldDef<AccountState, Iterable<String>> EMAIL =
new FieldDef.Repeatable<AccountState, String>(
"email", FieldType.PREFIX, false) {
@Override
public Iterable<String> get(AccountState input, FillArgs args) {
return FluentIterable.from(input.getExternalIds())
.transform(
new Function<AccountExternalId, String>() {
@Override
public String apply(AccountExternalId in) {
return in.getEmailAddress();
}
})
.append(
Collections.singleton(input.getAccount().getPreferredEmail()))
.filter(Predicates.notNull())
.transform(
new Function<String, String>() {
@Override
public String apply(String in) {
return in.toLowerCase();
}
});
}
};
public static final FieldDef<AccountState, Timestamp> REGISTERED =
new FieldDef.Single<AccountState, Timestamp>(
"registered", FieldType.TIMESTAMP, false) {
@Override
public Timestamp get(AccountState input, FillArgs args) {
return input.getAccount().getRegisteredOn();
}
};
public static final FieldDef<AccountState, String> USERNAME =
new FieldDef.Single<AccountState, String>(
"username", null, false) {
@Override
public String get(AccountState input, FillArgs args) {
return input.getUserName().toLowerCase();
}
};
private AccountField() {
}
}

View File

@@ -0,0 +1,62 @@
// Copyright (C) 2016 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.index.account;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.Schema;
import com.google.gerrit.server.index.SchemaUtil;
import java.util.Collection;
public class AccountSchemas {
static final Schema<AccountState> V1 = schema(
AccountField.ID,
AccountField.ACTIVE,
AccountField.EMAIL,
AccountField.EXTERNAL_ID,
AccountField.NAME_PART,
AccountField.REGISTERED,
AccountField.USERNAME);
private static Schema<AccountState> schema(
Collection<FieldDef<AccountState, ?>> fields) {
return new Schema<>(ImmutableList.copyOf(fields));
}
@SafeVarargs
private static Schema<AccountState> schema(
FieldDef<AccountState, ?>... fields) {
return schema(ImmutableList.copyOf(fields));
}
public static final ImmutableMap<Integer, Schema<AccountState>> ALL =
SchemaUtil.schemasFromClass(AccountSchemas.class, AccountState.class);
public static Schema<AccountState> get(int version) {
Schema<AccountState> schema = ALL.get(version);
checkArgument(schema != null, "Unrecognized schema version: %s", version);
return schema;
}
public static Schema<AccountState> getLatest() {
return Iterables.getLast(ALL.values());
}
}