Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Support for Normalizer in SimpleField and SearchableField #29542

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 "";
}
Original file line number Diff line number Diff line change
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