Skip to content

Commit

Permalink
Add Support for Normalizer in SimpleField and SearchableField (#29542)
Browse files Browse the repository at this point in the history
  • Loading branch information
alzimmermsft committed Jun 24, 2022
1 parent 1f84366 commit c16508c
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 61 deletions.
Expand Up @@ -485,9 +485,10 @@ the main ServiceBusClientBuilder. -->
<suppress checks="com.azure.tools.checkstyle.checks.HttpPipelinePolicyCheck"
files="com.azure.communication.callingserver.implementation.RedirectPolicy.java"/>

<!-- Suppress VisibilityModifier in MemberConverterImplTests.-->
<!-- Suppress VisibilityModifier in MemberConverterImplTests and FieldBuilderTests.-->
<suppress checks="VisibilityModifier"
files="com.azure.core.implementation.jackson.MemberNameConverterImplTests.java"/>
<suppress checks="VisibilityModifier" files="com.azure.search.documents.indexes.FieldBuilderTests.java"/>

<!-- Code generation doesn't add a space between '{' and '}' when the class is empty. -->
<suppress checks="WhitespaceAround" files="com.azure.search.documents.indexes.models.(DataChangeDetectionPolicy|DataDeletionDetectionPolicy|SearchIndexerDataIdentity|SimilarityAlgorithm)"/>
Expand Down
Expand Up @@ -4,6 +4,7 @@
package com.azure.search.documents.implementation.util;

import com.azure.core.models.GeoPoint;
import com.azure.core.util.CoreUtils;
import com.azure.core.util.logging.ClientLogger;
import com.azure.core.util.serializer.MemberNameConverter;
import com.azure.core.util.serializer.MemberNameConverterProviders;
Expand All @@ -13,6 +14,7 @@
import com.azure.search.documents.indexes.SimpleField;
import com.azure.search.documents.indexes.models.FieldBuilderOptions;
import com.azure.search.documents.indexes.models.LexicalAnalyzerName;
import com.azure.search.documents.indexes.models.LexicalNormalizerName;
import com.azure.search.documents.indexes.models.SearchField;
import com.azure.search.documents.indexes.models.SearchFieldDataType;
import reactor.util.annotation.Nullable;
Expand Down Expand Up @@ -51,6 +53,9 @@ public final class FieldBuilder {
private static final Map<Type, SearchFieldDataType> SUPPORTED_NONE_PARAMETERIZED_TYPE = new HashMap<>();
private static final Set<Type> UNSUPPORTED_TYPES = new HashSet<>();

private static final SearchFieldDataType COLLECTION_STRING
= SearchFieldDataType.collection(SearchFieldDataType.STRING);

static {
SUPPORTED_NONE_PARAMETERIZED_TYPE.put(Integer.class, SearchFieldDataType.INT32);
SUPPORTED_NONE_PARAMETERIZED_TYPE.put(int.class, SearchFieldDataType.INT32);
Expand Down Expand Up @@ -237,8 +242,8 @@ private static Type getComponentOrElementType(Type arrayOrListType) {
return ((Class<?>) arrayOrListType).getComponentType();
}

throw LOGGER.logExceptionAsError(new RuntimeException(String.format(
"Collection type %s is not supported.", arrayOrListType.getTypeName())));
throw LOGGER.logExceptionAsError(
new RuntimeException("Collection type '" + arrayOrListType.getTypeName() + "' is not supported."));
}

private static SearchField convertToBasicSearchField(String fieldName, Type type) {
Expand All @@ -252,55 +257,100 @@ private static SearchField enrichWithAnnotation(SearchField searchField, Member
SearchableField searchableField = getDeclaredAnnotation(member, SearchableField.class);

if (simpleField != null && searchableField != null) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException(String.format(
"@SimpleField and @SearchableField cannot be present simultaneously for %s", member.getName())));
throw LOGGER.logExceptionAsError(new IllegalArgumentException(
"@SimpleField and @SearchableField cannot be present simultaneously for " + member.getName()));
}

if (simpleField == null && searchableField == null) {
return searchField;
}

boolean key, hidden, filterable, sortable, facetable;
boolean searchable = searchableField != null;
String analyzerName = null;
String searchAnalyzerName = null;
String indexAnalyzerName = null;
String normalizerName;
String[] synonymMapNames = null;

if (simpleField != null) {
searchField.setSearchable(false)
.setSortable(simpleField.isSortable())
.setFilterable(simpleField.isFilterable())
.setFacetable(simpleField.isFacetable())
.setKey(simpleField.isKey())
.setHidden(simpleField.isHidden());
} else if (searchableField != null) {
if (!searchField.getType().equals(SearchFieldDataType.STRING)
&& !searchField.getType().equals(SearchFieldDataType.collection(SearchFieldDataType.STRING))) {
throw LOGGER.logExceptionAsError(new RuntimeException(String.format("SearchField can only be used on "
+ "string properties. Property %s returns a %s value.", member.getName(),
searchField.getType())));
}
key = simpleField.isKey();
hidden = simpleField.isHidden();
filterable = simpleField.isFilterable();
sortable = simpleField.isSortable();
facetable = simpleField.isFacetable();
normalizerName = simpleField.normalizerName();
} else {
key = searchableField.isKey();
hidden = searchableField.isHidden();
filterable = searchableField.isFilterable();
sortable = searchableField.isSortable();
facetable = searchableField.isFacetable();
analyzerName = searchableField.analyzerName();
searchAnalyzerName = searchableField.searchAnalyzerName();
indexAnalyzerName = searchableField.indexAnalyzerName();
normalizerName = searchableField.normalizerName();
synonymMapNames = searchableField.synonymMapNames();
}

searchField.setSearchable(true)
.setSortable(searchableField.isSortable())
.setFilterable(searchableField.isFilterable())
.setFacetable(searchableField.isFacetable())
.setKey(searchableField.isKey())
.setHidden(searchableField.isHidden());
String analyzer = searchableField.analyzerName();
String searchAnalyzer = searchableField.searchAnalyzerName();
String indexAnalyzer = searchableField.indexAnalyzerName();
if (!analyzer.isEmpty() && (!searchAnalyzer.isEmpty() || !indexAnalyzer.isEmpty())) {
throw LOGGER.logExceptionAsError(new RuntimeException(
"Please specify either analyzer or both searchAnalyzer and indexAnalyzer."));
}
if (!searchableField.analyzerName().isEmpty()) {
searchField.setAnalyzerName(LexicalAnalyzerName.fromString(
searchableField.analyzerName()));
StringBuilder errorMessage = new StringBuilder();
boolean isStringOrCollectionString = searchField.getType() == SearchFieldDataType.STRING
|| searchField.getType() == COLLECTION_STRING;
boolean hasAnalyzerName = !CoreUtils.isNullOrEmpty(analyzerName);
boolean hasSearchAnalyzerName = !CoreUtils.isNullOrEmpty(searchAnalyzerName);
boolean hasIndexAnalyzerName = !CoreUtils.isNullOrEmpty(indexAnalyzerName);
boolean hasNormalizerName = !CoreUtils.isNullOrEmpty(normalizerName);
if (searchable) {
if (!isStringOrCollectionString) {
errorMessage.append("SearchField can only be used on string properties. Property '")
.append(member.getName()).append("' returns a '").append(searchField.getType()).append("' value. ");
}
if (!searchableField.searchAnalyzerName().isEmpty()) {
searchField.setAnalyzerName(LexicalAnalyzerName.fromString(
searchableField.searchAnalyzerName()));
}
if (!searchableField.indexAnalyzerName().isEmpty()) {
searchField.setAnalyzerName(LexicalAnalyzerName.fromString(
searchableField.indexAnalyzerName()));
}
if (searchableField.synonymMapNames().length != 0) {
List<String> synonymMaps = Arrays.stream(searchableField.synonymMapNames())
.filter(synonym -> !synonym.trim().isEmpty()).collect(Collectors.toList());
searchField.setSynonymMapNames(synonymMaps);

// Searchable fields are allowed to have either no analyzer names configure or one of the following
// analyzerName is set and searchAnalyzerName and indexAnalyzerName are not set
// searchAnalyzerName and indexAnalyzerName are set and analyzerName is not set
if ((!hasAnalyzerName && (hasSearchAnalyzerName != hasIndexAnalyzerName))
|| (hasAnalyzerName && (hasSearchAnalyzerName || hasIndexAnalyzerName))) {
errorMessage.append("Please specify either analyzer or both searchAnalyzer and indexAnalyzer. ");
}
}

// Any field is allowed to have a normalizer but it must be either a STRING or Collection(STRING) and have one
// of filterable, sortable, or facetable set to true.
if (hasNormalizerName && (!isStringOrCollectionString || !(filterable || sortable || facetable))) {
errorMessage.append("A field with a normalizer name can only be used on string properties and must have ")
.append("one of filterable, sortable, or facetable set to true. ");
}

if (errorMessage.length() > 0) {
throw LOGGER.logExceptionAsError(new RuntimeException(errorMessage.toString()));
}

searchField.setKey(key)
.setHidden(hidden)
.setSearchable(searchable)
.setFilterable(filterable)
.setSortable(sortable)
.setFacetable(facetable);

if (hasAnalyzerName) {
searchField.setAnalyzerName(LexicalAnalyzerName.fromString(analyzerName));
} else if (hasSearchAnalyzerName || hasIndexAnalyzerName) {
searchField.setAnalyzerName(LexicalAnalyzerName.fromString(searchAnalyzerName));
searchField.setAnalyzerName(LexicalAnalyzerName.fromString(indexAnalyzerName));
}

if (hasNormalizerName) {
searchField.setNormalizerName(LexicalNormalizerName.fromString(normalizerName));
}

if (!CoreUtils.isNullOrEmpty(synonymMapNames)) {
List<String> synonymMaps = Arrays.stream(searchableField.synonymMapNames())
.filter(synonym -> !synonym.trim().isEmpty())
.collect(Collectors.toList());
searchField.setSynonymMapNames(synonymMaps);
}

return searchField;
}

Expand All @@ -318,11 +368,9 @@ private static void validateType(Type type, boolean hasArrayOrCollectionWrapped)
if (!(type instanceof ParameterizedType)) {
if (UNSUPPORTED_TYPES.contains(type)) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException(
String.format("Type '%s' is not supported. "
+ "Please use @FieldIgnore to exclude the field "
+ "and manually build SearchField to the list if the field is needed. %n"
+ "For more information, refer to link: aka.ms/azsdk/java/search/fieldbuilder",
type.getTypeName())));
"Type '" + type.getTypeName() + "' is not supported. Please use @FieldIgnore to exclude the field "
+ "and manually build SearchField to the list if the field is needed. For more information, "
+ "refer to link: aka.ms/azsdk/java/search/fieldbuilder"));
}
return;
}
Expand All @@ -333,13 +381,13 @@ private static void validateType(Type type, boolean hasArrayOrCollectionWrapped)
}

if (hasArrayOrCollectionWrapped) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException(
"Only single-dimensional array is supported."));
throw LOGGER.logExceptionAsError(
new IllegalArgumentException("Only single-dimensional array is supported."));
}

if (!List.class.isAssignableFrom((Class<?>) parameterizedType.getRawType())) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException(
String.format("Collection type %s is not supported", type.getTypeName())));
throw LOGGER.logExceptionAsError(
new IllegalArgumentException("Collection type '" + type.getTypeName() + "' is not supported"));
}
}

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

import com.azure.search.documents.indexes.models.FieldBuilderOptions;
import com.azure.search.documents.indexes.models.LexicalAnalyzerName;
import com.azure.search.documents.indexes.models.LexicalNormalizerName;
import com.azure.search.documents.indexes.models.SearchField;
import com.azure.search.documents.indexes.models.SynonymMap;

Expand Down Expand Up @@ -58,27 +59,35 @@
/**
* A {@link LexicalAnalyzerName} to associate as the search and index analyzer for the {@link SearchField field}.
*
* @return The {@link LexicalAnalyzerName} that will be associated as the search and index analyzer for the {@link
* SearchField field}.
* @return The {@link LexicalAnalyzerName} that will be associated as the search and index analyzer for the
* {@link SearchField field}.
*/
String analyzerName() default "";

/**
* A {@link LexicalAnalyzerName} to associate as the search analyzer for the {@link SearchField field}.
*
* @return The {@link LexicalAnalyzerName} that will be associated as the search analyzer for the {@link SearchField
* field}.
* @return The {@link LexicalAnalyzerName} that will be associated as the search analyzer for the
* {@link SearchField field}.
*/
String searchAnalyzerName() default "";

/**
* A {@link LexicalAnalyzerName} to associate as the index analyzer for the {@link SearchField field}.
*
* @return The {@link LexicalAnalyzerName} that will be associated as the index analyzer for the {@link SearchField
* field}.
* @return The {@link LexicalAnalyzerName} that will be associated as the index analyzer for the
* {@link SearchField field}.
*/
String indexAnalyzerName() default "";

/**
* A {@link LexicalNormalizerName} to associate as the normalizer for the {@link SearchField field}.
*
* @return The {@link LexicalNormalizerName} that will be associated as the normalizer for the
* {@link SearchField field}.
*/
String normalizerName() default "";

/**
* A list of {@link SynonymMap} names to be associated with the {@link SearchField field}.
* <p>
Expand Down
Expand Up @@ -4,6 +4,7 @@
package com.azure.search.documents.indexes;

import com.azure.search.documents.indexes.models.FieldBuilderOptions;
import com.azure.search.documents.indexes.models.LexicalNormalizerName;
import com.azure.search.documents.indexes.models.SearchField;

import java.lang.annotation.ElementType;
Expand Down Expand Up @@ -52,4 +53,12 @@
* @return A flag indicating if the field or method should generate as a filterable {@link SearchField field}.
*/
boolean isFilterable() default false;

/**
* A {@link LexicalNormalizerName} to associate as the normalizer for the {@link SearchField field}.
*
* @return The {@link LexicalNormalizerName} that will be associated as the normalizer for the
* {@link SearchField field}.
*/
String normalizerName() default "";
}
Expand Up @@ -5,6 +5,7 @@

import com.azure.core.models.GeoPoint;
import com.azure.search.documents.TestHelpers;
import com.azure.search.documents.indexes.models.LexicalNormalizerName;
import com.azure.search.documents.indexes.models.SearchField;
import com.azure.search.documents.indexes.models.SearchFieldDataType;
import com.azure.search.documents.test.environment.models.HotelAnalyzerException;
Expand All @@ -18,6 +19,8 @@
import com.azure.search.documents.test.environment.models.HotelWithIgnoredFields;
import com.azure.search.documents.test.environment.models.HotelWithUnsupportedField;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.time.OffsetDateTime;
import java.util.Arrays;
Expand Down Expand Up @@ -255,6 +258,43 @@ public void unsupportedFields() {
assertExceptionMassageAndDataType(exception, null, "is not supported");
}

@Test
public void validNormalizerField() {
List<SearchField> fields = SearchIndexClient.buildSearchFields(ValidNormalizer.class, null);

assertEquals(1, fields.size());

SearchField normalizerField = fields.get(0);
assertEquals(LexicalNormalizerName.STANDARD, normalizerField.getNormalizerName());
}

@SuppressWarnings("unused")
public static final class ValidNormalizer {
@SimpleField(normalizerName = "standard", isFilterable = true)
public String validNormalizer;
}

@ParameterizedTest
@ValueSource(classes = { NonStringNormalizer.class, MissingFunctionalityNormalizer.class })
public void invalidNormalizerField(Class<?> type) {
RuntimeException ex = assertThrows(RuntimeException.class,
() -> SearchIndexClient.buildSearchFields(type, null));

assertTrue(ex.getMessage().contains("A field with a normalizer name"));
}

@SuppressWarnings("unused")
public static final class NonStringNormalizer {
@SimpleField(normalizerName = "standard")
public int wrongTypeForNormalizer;
}

@SuppressWarnings("unused")
public static final class MissingFunctionalityNormalizer {
@SimpleField(normalizerName = "standard")
public String rightTypeWrongFunctionality;
}

private void assertListFieldEquals(List<SearchField> expected, List<SearchField> actual) {
assertEquals(expected.size(), actual.size());
for (int i = 0; i < expected.size(); i++) {
Expand Down

0 comments on commit c16508c

Please sign in to comment.