diff --git a/src/main/java/com/yelp/nrtsearch/server/field/AtomFieldDef.java b/src/main/java/com/yelp/nrtsearch/server/field/AtomFieldDef.java index df21a5289..aefb926b5 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/AtomFieldDef.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/AtomFieldDef.java @@ -43,7 +43,24 @@ public class AtomFieldDef extends TextBaseFieldDef public AtomFieldDef( String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { - super(name, requestField, context); + this(name, requestField, context, null); + } + + /** + * Constructor for creating an instance of this field based on a previous instance. This is used + * when updating field properties. + * + * @param name name of the field + * @param requestField the field definition from the request + * @param context context for creating the field definition + * @param previousField the previous instance of this field definition, or null if there is none + */ + protected AtomFieldDef( + String name, + Field requestField, + FieldDefCreator.FieldDefCreatorContext context, + AtomFieldDef previousField) { + super(name, requestField, context, previousField); } @Override @@ -59,6 +76,12 @@ public String getType() { return "ATOM"; } + @Override + public FieldDef createUpdatedFieldDef( + String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { + return new AtomFieldDef(name, requestField, context, this); + } + @Override public Object parseLastValue(String value) { return new BytesRef(value); diff --git a/src/main/java/com/yelp/nrtsearch/server/field/BooleanFieldDef.java b/src/main/java/com/yelp/nrtsearch/server/field/BooleanFieldDef.java index 333b5af0a..9a0b99fbb 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/BooleanFieldDef.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/BooleanFieldDef.java @@ -39,7 +39,24 @@ public class BooleanFieldDef extends IndexableFieldDef implements TermQueryable { protected BooleanFieldDef( String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { - super(name, requestField, context, Boolean.class); + this(name, requestField, context, null); + } + + /** + * Constructor for creating an instance of this field based on a previous instance. This is used + * when updating field properties. + * + * @param name name of the field + * @param requestField the field definition from the request + * @param context context for creating the field definition + * @param previousField the previous instance of this field definition, or null if there is none + */ + protected BooleanFieldDef( + String name, + Field requestField, + FieldDefCreator.FieldDefCreatorContext context, + BooleanFieldDef previousField) { + super(name, requestField, context, Boolean.class, previousField); } @Override @@ -138,6 +155,12 @@ public String getType() { return "BOOLEAN"; } + @Override + public FieldDef createUpdatedFieldDef( + String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { + return new BooleanFieldDef(name, requestField, context, this); + } + @Override public Query getTermQueryFromBooleanValue(boolean booleanValue) { verifySearchable("Term query"); diff --git a/src/main/java/com/yelp/nrtsearch/server/field/ContextSuggestFieldDef.java b/src/main/java/com/yelp/nrtsearch/server/field/ContextSuggestFieldDef.java index 74c32c8a0..dbf5e830d 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/ContextSuggestFieldDef.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/ContextSuggestFieldDef.java @@ -41,7 +41,24 @@ public class ContextSuggestFieldDef extends IndexableFieldDef { */ protected ContextSuggestFieldDef( String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { - super(name, requestField, context, Void.class); + this(name, requestField, context, null); + } + + /** + * Constructor for creating an instance of this field based on a previous instance. This is used + * when updating field properties. + * + * @param name name of the field + * @param requestField the field definition from the request + * @param context context for creating the field definition + * @param previousField the previous instance of this field definition, or null if there is none + */ + protected ContextSuggestFieldDef( + String name, + Field requestField, + FieldDefCreator.FieldDefCreatorContext context, + ContextSuggestFieldDef previousField) { + super(name, requestField, context, Void.class, previousField); this.indexAnalyzer = this.parseIndexAnalyzer(requestField); this.searchAnalyzer = this.parseSearchAnalyzer(requestField); this.postingsFormat = @@ -64,6 +81,12 @@ public String getType() { return "CONTEXT_SUGGEST"; } + @Override + public FieldDef createUpdatedFieldDef( + String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { + return new ContextSuggestFieldDef(name, requestField, context, this); + } + @Override public void parseDocumentField( Document document, List fieldValues, List> facetHierarchyPaths) { diff --git a/src/main/java/com/yelp/nrtsearch/server/field/DateTimeFieldDef.java b/src/main/java/com/yelp/nrtsearch/server/field/DateTimeFieldDef.java index 2469768f4..eae947b24 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/DateTimeFieldDef.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/DateTimeFieldDef.java @@ -82,7 +82,24 @@ private static DateTimeFormatter createDateTimeFormatter(String dateTimeFormat) public DateTimeFieldDef( String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { - super(name, requestField, context, Instant.class); + this(name, requestField, context, null); + } + + /** + * Constructor for creating an instance of this field based on a previous instance. This is used + * when updating field properties. + * + * @param name name of the field + * @param requestField the field definition from the request + * @param context context for creating the field definition + * @param previousField the previous instance of this field definition, or null if there is none + */ + protected DateTimeFieldDef( + String name, + Field requestField, + FieldDefCreator.FieldDefCreatorContext context, + DateTimeFieldDef previousField) { + super(name, requestField, context, Instant.class, previousField); dateTimeFormat = requestField.getDateTimeFormat(); dateTimeFormatter = createDateTimeFormatter(dateTimeFormat); } @@ -369,6 +386,12 @@ public String getType() { return "DATE_TIME"; } + @Override + public FieldDef createUpdatedFieldDef( + String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { + return new DateTimeFieldDef(name, requestField, context, this); + } + /** * Get the format used to parse date time string. * diff --git a/src/main/java/com/yelp/nrtsearch/server/field/DoubleFieldDef.java b/src/main/java/com/yelp/nrtsearch/server/field/DoubleFieldDef.java index ca7ccfc8d..ccf34cfe9 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/DoubleFieldDef.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/DoubleFieldDef.java @@ -39,7 +39,24 @@ public class DoubleFieldDef extends NumberFieldDef { public DoubleFieldDef( String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { - super(name, requestField, DOUBLE_PARSER, context, Double.class); + this(name, requestField, context, null); + } + + /** + * Constructor for creating an instance of this field based on a previous instance. This is used + * when updating field properties. + * + * @param name name of the field + * @param requestField the field definition from the request + * @param context context for creating the field definition + * @param previousField the previous instance of this field definition, or null if there is none + */ + protected DoubleFieldDef( + String name, + Field requestField, + FieldDefCreator.FieldDefCreatorContext context, + DoubleFieldDef previousField) { + super(name, requestField, DOUBLE_PARSER, context, Double.class, previousField); } @Override @@ -101,6 +118,12 @@ public String getType() { return "DOUBLE"; } + @Override + public FieldDef createUpdatedFieldDef( + String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { + return new DoubleFieldDef(name, requestField, context, this); + } + @Override public Query getRangeQuery(RangeQuery rangeQuery) { verifySearchableOrDocValues("Range query"); diff --git a/src/main/java/com/yelp/nrtsearch/server/field/FieldDef.java b/src/main/java/com/yelp/nrtsearch/server/field/FieldDef.java index 066b0ef66..307894eab 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/FieldDef.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/FieldDef.java @@ -15,6 +15,7 @@ */ package com.yelp.nrtsearch.server.field; +import com.yelp.nrtsearch.server.grpc.Field; import java.io.Closeable; /** Base class for all field definition types. */ @@ -35,6 +36,22 @@ public String getName() { return name; } + /** + * Create a new instance of the current {@link FieldDef} that is updated to use the provided + * request field definition. This method must not modify the current instance, but instead return + * a new instance with the updated properties. + * + * @param name name of the field + * @param requestField the field definition from the request + * @param context context for creating the field definition + * @return a new instance of the field definition with updated properties + */ + public FieldDef createUpdatedFieldDef( + String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { + throw new UnsupportedOperationException( + String.format("Field %s does not support update", this.getName())); + } + /** Get String representation of the field type. */ public abstract String getType(); diff --git a/src/main/java/com/yelp/nrtsearch/server/field/FieldDefCreator.java b/src/main/java/com/yelp/nrtsearch/server/field/FieldDefCreator.java index 5e5828a41..a6a47890f 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/FieldDefCreator.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/FieldDefCreator.java @@ -97,6 +97,29 @@ public FieldDef createFieldDef(String name, Field field, FieldDefCreatorContext return provider.get(name, field, context); } + /** + * Create a new {@link FieldDef} instance based on a previous {@link FieldDef}. This is typically + * used when updating a field definition in the index. + * + * @param name name of the field + * @param field grpc request field definition + * @param previousFieldDef previous field definition, or null if there is none + * @param context context for creating the field definition + * @return new field definition instance + */ + public FieldDef createFieldDefFromPrevious( + String name, Field field, FieldDef previousFieldDef, FieldDefCreatorContext context) { + if (previousFieldDef == null) { + return createFieldDef(name, field, context); + } + FieldDef updatedFieldDef = previousFieldDef.createUpdatedFieldDef(name, field, context); + if (updatedFieldDef == null) { + throw new IllegalArgumentException( + "FieldDef " + previousFieldDef.getName() + " cannot be updated"); + } + return updatedFieldDef; + } + /** * Create a new {@link FieldDefCreatorContext} instance. * diff --git a/src/main/java/com/yelp/nrtsearch/server/field/FloatFieldDef.java b/src/main/java/com/yelp/nrtsearch/server/field/FloatFieldDef.java index 91835b223..0de48cc3a 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/FloatFieldDef.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/FloatFieldDef.java @@ -39,7 +39,24 @@ public class FloatFieldDef extends NumberFieldDef { public FloatFieldDef( String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { - super(name, requestField, FLOAT_PARSER, context, Float.class); + this(name, requestField, context, null); + } + + /** + * Constructor for creating an instance of this field based on a previous instance. This is used + * when updating field properties. + * + * @param name name of the field + * @param requestField the field definition from the request + * @param context context for creating the field definition + * @param previousField the previous instance of this field definition, or null if there is none + */ + protected FloatFieldDef( + String name, + Field requestField, + FieldDefCreator.FieldDefCreatorContext context, + FloatFieldDef previousField) { + super(name, requestField, FLOAT_PARSER, context, Float.class, previousField); } @Override @@ -99,6 +116,12 @@ public String getType() { return "FLOAT"; } + @Override + public FieldDef createUpdatedFieldDef( + String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { + return new FloatFieldDef(name, requestField, context, this); + } + @Override public Query getRangeQuery(RangeQuery rangeQuery) { verifySearchableOrDocValues("Range query"); diff --git a/src/main/java/com/yelp/nrtsearch/server/field/IdFieldDef.java b/src/main/java/com/yelp/nrtsearch/server/field/IdFieldDef.java index 2f78dfa31..307c8704a 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/IdFieldDef.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/IdFieldDef.java @@ -43,7 +43,24 @@ public class IdFieldDef extends IndexableFieldDef implements TermQueryab protected IdFieldDef( String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { - super(name, requestField, context, String.class); + this(name, requestField, context, null); + } + + /** + * Constructor for creating an instance of this field based on a previous instance. This is used + * when updating field properties. + * + * @param name name of the field + * @param requestField the field definition from the request + * @param context context for creating the field definition + * @param previousField the previous instance of this field definition, or null if there is none + */ + protected IdFieldDef( + String name, + Field requestField, + FieldDefCreator.FieldDefCreatorContext context, + IdFieldDef previousField) { + super(name, requestField, context, String.class, previousField); } /** @@ -142,6 +159,12 @@ public String getType() { return "_ID"; } + @Override + public FieldDef createUpdatedFieldDef( + String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { + return new IdFieldDef(name, requestField, context, this); + } + /** * Construct a Term with the given field and value to identify the document to be added or updated * diff --git a/src/main/java/com/yelp/nrtsearch/server/field/IndexableFieldDef.java b/src/main/java/com/yelp/nrtsearch/server/field/IndexableFieldDef.java index 6f7c9a98f..1cfc173bb 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/IndexableFieldDef.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/IndexableFieldDef.java @@ -75,12 +75,14 @@ public enum FacetValueType { * @param requestField field definition from grpc request * @param context creation context * @param docValuesObjectClass class of doc values object + * @param previousField the previous instance of this field definition, or null if there is none */ protected IndexableFieldDef( String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context, - Class docValuesObjectClass) { + Class docValuesObjectClass, + IndexableFieldDef previousField) { super(name); validateRequest(requestField); @@ -120,7 +122,12 @@ protected IndexableFieldDef( for (Field field : requestField.getChildFieldsList()) { checkChildName(field.getName()); String childName = getName() + IndexState.CHILD_FIELD_SEPARATOR + field.getName(); - FieldDef fieldDef = FieldDefCreator.getInstance().createFieldDef(childName, field, context); + Map> previousChildFields = + previousField == null ? Map.of() : previousField.getChildFields(); + FieldDef fieldDef = + FieldDefCreator.getInstance() + .createFieldDefFromPrevious( + childName, field, previousChildFields.get(childName), context); if (!(fieldDef instanceof IndexableFieldDef)) { throw new IllegalArgumentException("Child field is not indexable: " + childName); } @@ -146,10 +153,10 @@ private void checkChildName(String name) { /** * Method called by {@link #IndexableFieldDef(String, Field, - * FieldDefCreator.FieldDefCreatorContext, Class)} to validate the provided {@link Field}. Field - * definitions should define a version that checks for incompatible parameters and any other - * potential issues. It is recommended to also call the super version of this method, so that - * general checks do not need to be repeated everywhere. + * FieldDefCreator.FieldDefCreatorContext, Class, IndexableFieldDef)} to validate the provided + * {@link Field}. Field definitions should define a version that checks for incompatible + * parameters and any other potential issues. It is recommended to also call the super version of + * this method, so that general checks do not need to be repeated everywhere. * * @param requestField field properties to validate */ @@ -157,9 +164,9 @@ protected void validateRequest(Field requestField) {} /** * Method called by {@link #IndexableFieldDef(String, Field, - * FieldDefCreator.FieldDefCreatorContext, Class)} to determine the doc value type used by this - * field. Fields are not necessarily limited to one doc value, but this should represent the - * primary value that will be accessible to scripts and search through {@link + * FieldDefCreator.FieldDefCreatorContext, Class, IndexableFieldDef)} to determine the doc value + * type used by this field. Fields are not necessarily limited to one doc value, but this should + * represent the primary value that will be accessible to scripts and search through {@link * #getDocValues(LeafReaderContext)}. A value of NONE implies that the field does not support doc * values. * @@ -172,9 +179,9 @@ protected DocValuesType parseDocValuesType(Field requestField) { /** * Method called by {@link #IndexableFieldDef(String, Field, - * FieldDefCreator.FieldDefCreatorContext, Class)} to determine the facet value type for this - * field. The result of this method is exposed externally through {@link #getFacetValueType()}. A - * value of NO_FACETS implies that the field does not support facets. + * FieldDefCreator.FieldDefCreatorContext, Class, IndexableFieldDef)} to determine the facet value + * type for this field. The result of this method is exposed externally through {@link + * #getFacetValueType()}. A value of NO_FACETS implies that the field does not support facets. * * @param requestField field from request * @return field facet value type @@ -185,12 +192,12 @@ protected FacetValueType parseFacetValueType(Field requestField) { /** * Method called by {@link #IndexableFieldDef(String, Field, - * FieldDefCreator.FieldDefCreatorContext, Class)} to set the search properties on the given - * {@link FieldType}. The {@link FieldType#setStored(boolean)} has already been set to the value - * from {@link Field#getStore()}. This method should set any other needed properties, such as - * index options, tokenization, term vectors, etc. It likely should not set a doc value type, as - * those are usually added separately. The common use of this {@link FieldType} is to add a {@link - * FieldWithData} during indexing. This method should not freeze the field type. + * FieldDefCreator.FieldDefCreatorContext, Class, IndexableFieldDef)} to set the search properties + * on the given {@link FieldType}. The {@link FieldType#setStored(boolean)} has already been set + * to the value from {@link Field#getStore()}. This method should set any other needed properties, + * such as index options, tokenization, term vectors, etc. It likely should not set a doc value + * type, as those are usually added separately. The common use of this {@link FieldType} is to add + * a {@link FieldWithData} during indexing. This method should not freeze the field type. * * @param fieldType type that needs search properties set * @param requestField field from request diff --git a/src/main/java/com/yelp/nrtsearch/server/field/IntFieldDef.java b/src/main/java/com/yelp/nrtsearch/server/field/IntFieldDef.java index 498268802..b2f7b8dbb 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/IntFieldDef.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/IntFieldDef.java @@ -38,7 +38,24 @@ public class IntFieldDef extends NumberFieldDef { public IntFieldDef( String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { - super(name, requestField, INT_PARSER, context, Integer.class); + this(name, requestField, context, null); + } + + /** + * Constructor for creating an instance of this field based on a previous instance. This is used + * when updating field properties. + * + * @param name name of the field + * @param requestField the field definition from the request + * @param context context for creating the field definition + * @param previousField the previous instance of this field definition, or null if there is none + */ + protected IntFieldDef( + String name, + Field requestField, + FieldDefCreator.FieldDefCreatorContext context, + IntFieldDef previousField) { + super(name, requestField, INT_PARSER, context, Integer.class, previousField); } @Override @@ -97,6 +114,12 @@ public String getType() { return "INT"; } + @Override + public FieldDef createUpdatedFieldDef( + String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { + return new IntFieldDef(name, requestField, context, this); + } + @Override public Query getRangeQuery(RangeQuery rangeQuery) { verifySearchableOrDocValues("Range query"); diff --git a/src/main/java/com/yelp/nrtsearch/server/field/LatLonFieldDef.java b/src/main/java/com/yelp/nrtsearch/server/field/LatLonFieldDef.java index 023f4e571..30db312ad 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/LatLonFieldDef.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/LatLonFieldDef.java @@ -49,7 +49,24 @@ public class LatLonFieldDef extends IndexableFieldDef implements Sortable, GeoQueryable { public LatLonFieldDef( String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { - super(name, requestField, context, GeoPoint.class); + this(name, requestField, context, null); + } + + /** + * Constructor for creating an instance of this field based on a previous instance. This is used + * when updating field properties. + * + * @param name name of the field + * @param requestField the field definition from the request + * @param context context for creating the field definition + * @param previousField the previous instance of this field definition, or null if there is none + */ + protected LatLonFieldDef( + String name, + Field requestField, + FieldDefCreator.FieldDefCreatorContext context, + LatLonFieldDef previousField) { + super(name, requestField, context, GeoPoint.class, previousField); } @Override @@ -119,6 +136,12 @@ public String getType() { return "LAT_LON"; } + @Override + public FieldDef createUpdatedFieldDef( + String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { + return new LatLonFieldDef(name, requestField, context, this); + } + @Override public SortField getSortField(SortType type) { verifyDocValues("Sort field"); diff --git a/src/main/java/com/yelp/nrtsearch/server/field/LongFieldDef.java b/src/main/java/com/yelp/nrtsearch/server/field/LongFieldDef.java index c22173ab5..0d973f547 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/LongFieldDef.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/LongFieldDef.java @@ -38,7 +38,24 @@ public class LongFieldDef extends NumberFieldDef { public LongFieldDef( String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { - super(name, requestField, LONG_PARSER, context, Long.class); + this(name, requestField, context, null); + } + + /** + * Constructor for creating an instance of this field based on a previous instance. This is used + * when updating field properties. + * + * @param name name of the field + * @param requestField the field definition from the request + * @param context context for creating the field definition + * @param previousField the previous instance of this field definition, or null if there is none + */ + protected LongFieldDef( + String name, + Field requestField, + FieldDefCreator.FieldDefCreatorContext context, + LongFieldDef previousField) { + super(name, requestField, LONG_PARSER, context, Long.class, previousField); } @Override @@ -97,6 +114,12 @@ public String getType() { return "LONG"; } + @Override + public FieldDef createUpdatedFieldDef( + String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { + return new LongFieldDef(name, requestField, context, this); + } + @Override public Query getRangeQuery(RangeQuery rangeQuery) { verifySearchableOrDocValues("Range query"); diff --git a/src/main/java/com/yelp/nrtsearch/server/field/NumberFieldDef.java b/src/main/java/com/yelp/nrtsearch/server/field/NumberFieldDef.java index 160f17b43..2c7641eb9 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/NumberFieldDef.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/NumberFieldDef.java @@ -71,8 +71,9 @@ protected NumberFieldDef( Field requestField, Function fieldParser, FieldDefCreator.FieldDefCreatorContext context, - Class docValuesClass) { - super(name, requestField, context, docValuesClass); + Class docValuesClass, + NumberFieldDef previousField) { + super(name, requestField, context, docValuesClass, previousField); this.fieldParser = fieldParser; } diff --git a/src/main/java/com/yelp/nrtsearch/server/field/ObjectFieldDef.java b/src/main/java/com/yelp/nrtsearch/server/field/ObjectFieldDef.java index 6834549a1..17b729070 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/ObjectFieldDef.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/ObjectFieldDef.java @@ -44,14 +44,30 @@ public class ObjectFieldDef extends IndexableFieldDef { - private final Gson gson; + private static final Gson GSON = new GsonBuilder().serializeNulls().create(); private final boolean isNestedDoc; protected ObjectFieldDef( String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { - super(name, requestField, context, Struct.class); + this(name, requestField, context, null); + } + + /** + * Constructor for creating an instance of this field based on a previous instance. This is used + * when updating field properties. + * + * @param name name of the field + * @param requestField the field definition from the request + * @param context context for creating the field definition + * @param previousField the previous instance of this field definition, or null if there is none + */ + protected ObjectFieldDef( + String name, + Field requestField, + FieldDefCreator.FieldDefCreatorContext context, + ObjectFieldDef previousField) { + super(name, requestField, context, Struct.class, previousField); this.isNestedDoc = requestField.getNestedDoc(); - gson = new GsonBuilder().serializeNulls().create(); } @Override @@ -59,6 +75,12 @@ public String getType() { return "OBJECT"; } + @Override + public FieldDef createUpdatedFieldDef( + String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { + return new ObjectFieldDef(name, requestField, context, this); + } + @Override public void parseDocumentField( Document document, List fieldValues, List> facetHierarchyPaths) {} @@ -72,7 +94,7 @@ public void parseFieldWithChildren( parseFieldWithChildren(documentsContext.getRootDocument(), fieldValues, facetHierarchyPaths); } else { List> fieldValueMaps = new ArrayList<>(); - fieldValues.stream().map(e -> gson.fromJson(e, Map.class)).forEach(fieldValueMaps::add); + fieldValues.stream().map(e -> GSON.fromJson(e, Map.class)).forEach(fieldValueMaps::add); List childDocuments = fieldValueMaps.stream() @@ -102,7 +124,7 @@ private Document createChildDocument( public void parseFieldWithChildren( Document document, List fieldValues, List> facetHierarchyPaths) { List> fieldValueMaps = new ArrayList<>(); - fieldValues.stream().map(e -> gson.fromJson(e, Map.class)).forEach(fieldValueMaps::add); + fieldValues.stream().map(e -> GSON.fromJson(e, Map.class)).forEach(fieldValueMaps::add); if (isStored()) { for (String fieldValue : fieldValues) { document.add(new StoredField(this.getName(), jsonToStruct(fieldValue).toByteArray())); @@ -147,7 +169,7 @@ public void parseFieldWithChildrenObject( if (childValue instanceof List) { for (Object e : (List) childValue) { if (e instanceof List || e instanceof Map) { - childrenValues.add(gson.toJson(e)); + childrenValues.add(GSON.toJson(e)); } else { childrenValues.add(String.valueOf(e)); } diff --git a/src/main/java/com/yelp/nrtsearch/server/field/PolygonfieldDef.java b/src/main/java/com/yelp/nrtsearch/server/field/PolygonfieldDef.java index 8a0963ab2..8f5a25369 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/PolygonfieldDef.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/PolygonfieldDef.java @@ -40,7 +40,24 @@ public class PolygonfieldDef extends IndexableFieldDef implements Polygo protected PolygonfieldDef( String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { - super(name, requestField, context, Struct.class); + this(name, requestField, context, null); + } + + /** + * Constructor for creating an instance of this field based on a previous instance. This is used + * when updating field properties. + * + * @param name name of the field + * @param requestField the field definition from the request + * @param context context for creating the field definition + * @param previousField the previous instance of this field definition, or null if there is none + */ + protected PolygonfieldDef( + String name, + Field requestField, + FieldDefCreator.FieldDefCreatorContext context, + PolygonfieldDef previousField) { + super(name, requestField, context, Struct.class, previousField); } @Override @@ -97,6 +114,12 @@ public String getType() { return "POLYGON"; } + @Override + public FieldDef createUpdatedFieldDef( + String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { + return new PolygonfieldDef(name, requestField, context, this); + } + @Override public LoadedDocValues getDocValues(LeafReaderContext context) throws IOException { if (docValuesType == DocValuesType.BINARY) { diff --git a/src/main/java/com/yelp/nrtsearch/server/field/PrefixFieldDef.java b/src/main/java/com/yelp/nrtsearch/server/field/PrefixFieldDef.java index 19b5b529e..c95d4905e 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/PrefixFieldDef.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/PrefixFieldDef.java @@ -38,7 +38,24 @@ public class PrefixFieldDef extends TextBaseFieldDef { public PrefixFieldDef( String parentName, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { - super(parentName + INDEX_PREFIX, requestField, context); + this(parentName, requestField, context, null); + } + + /** + * Constructor for creating an instance of this field based on a previous instance. This is used + * when updating field properties. + * + * @param parentName name of the parent text field + * @param requestField the field definition from the request + * @param context context for creating the field definition + * @param previousField the previous instance of this field definition, or null if there is none + */ + protected PrefixFieldDef( + String parentName, + Field requestField, + FieldDefCreator.FieldDefCreatorContext context, + PrefixFieldDef previousField) { + super(parentName + INDEX_PREFIX, requestField, context, previousField); this.minChars = requestField.getIndexPrefixes().getMinChars(); this.maxChars = requestField.getIndexPrefixes().getMaxChars(); this.parentField = parentName; @@ -91,6 +108,12 @@ public String getType() { return "PREFIX"; } + @Override + public FieldDef createUpdatedFieldDef( + String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { + return new PrefixFieldDef(name, requestField, context, this); + } + public int getMinChars() { return minChars; } diff --git a/src/main/java/com/yelp/nrtsearch/server/field/TextBaseFieldDef.java b/src/main/java/com/yelp/nrtsearch/server/field/TextBaseFieldDef.java index 6d52b583e..c1caecea9 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/TextBaseFieldDef.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/TextBaseFieldDef.java @@ -15,6 +15,7 @@ */ package com.yelp.nrtsearch.server.field; +import com.google.common.annotations.VisibleForTesting; import com.yelp.nrtsearch.server.analysis.AnalyzerCreator; import com.yelp.nrtsearch.server.analysis.PosIncGapAnalyzerWrapper; import com.yelp.nrtsearch.server.doc.LoadedDocValues; @@ -59,24 +60,40 @@ public abstract class TextBaseFieldDef extends IndexableFieldDef private final Analyzer indexAnalyzer; private final Analyzer searchAnalyzer; private final boolean eagerFieldGlobalOrdinals; - - public final Map ordinalLookupCache = new HashMap<>(); - private final Object ordinalBuilderLock = new Object(); private final int ignoreAbove; + // These members are shared by all instances of the FieldDefs for the same field name + public final Map ordinalLookupCache; + @VisibleForTesting final Object ordinalBuilderLock; + /** * Field constructor. Uses {@link IndexableFieldDef#IndexableFieldDef(String, Field, - * FieldDefCreator.FieldDefCreatorContext, Class)} to do common initialization, then sets up - * analyzers. Analyzers are parsed through calls to the protected methods {@link + * FieldDefCreator.FieldDefCreatorContext, Class, IndexableFieldDef)} to do common initialization, + * then sets up analyzers. Analyzers are parsed through calls to the protected methods {@link * #parseIndexAnalyzer(Field)} and {@link #parseSearchAnalyzer(Field)}. * * @param name field name * @param requestField field definition from grpc request * @param context creation context + * @param previousField previous instance of this field definition, or null if there is none */ protected TextBaseFieldDef( - String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { - super(name, requestField, context, String.class); + String name, + Field requestField, + FieldDefCreator.FieldDefCreatorContext context, + TextBaseFieldDef previousField) { + super(name, requestField, context, String.class, previousField); + + // If the previous field exists, we need to copy the ordinal lookup cache and lock from it + // since it is a shared resource. + if (previousField != null) { + ordinalLookupCache = previousField.ordinalLookupCache; + ordinalBuilderLock = previousField.ordinalBuilderLock; + } else { + ordinalLookupCache = new HashMap<>(); + ordinalBuilderLock = new Object(); + } + indexAnalyzer = parseIndexAnalyzer(requestField); searchAnalyzer = parseSearchAnalyzer(requestField); eagerFieldGlobalOrdinals = requestField.getEagerFieldGlobalOrdinals(); diff --git a/src/main/java/com/yelp/nrtsearch/server/field/TextFieldDef.java b/src/main/java/com/yelp/nrtsearch/server/field/TextFieldDef.java index 02b91811f..85333a604 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/TextFieldDef.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/TextFieldDef.java @@ -40,7 +40,25 @@ public class TextFieldDef extends TextBaseFieldDef implements PrefixQueryable { public TextFieldDef( String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { - super(name, requestField, context); + this(name, requestField, context, null); + } + + /** + * Constructor for creating an instance of this field based on a previous instance. This is used + * when updating field properties. + * + * @param name name of the field + * @param requestField the field definition from the request + * @param context context for creating the field definition + * @param previousField the previous instance of this field definition, or null if there is none + */ + protected TextFieldDef( + String name, + Field requestField, + FieldDefCreator.FieldDefCreatorContext context, + TextFieldDef previousField) { + super(name, requestField, context, previousField); + if (requestField.hasIndexPrefixes()) { verifySearchable("Prefix query"); int minChars = @@ -65,7 +83,10 @@ public TextFieldDef( prefixFieldBuilder.setIndexAnalyzer(requestField.getIndexAnalyzer()); } - this.prefixFieldDef = new PrefixFieldDef(getName(), prefixFieldBuilder.build(), context); + PrefixFieldDef previousPrefixField = + previousField != null ? previousField.getPrefixFieldDef() : null; + this.prefixFieldDef = + new PrefixFieldDef(getName(), prefixFieldBuilder.build(), context, previousPrefixField); Map> childFieldsMap = new HashMap<>(super.getChildFields()); childFieldsMap.put(prefixFieldDef.getName(), prefixFieldDef); @@ -86,6 +107,12 @@ public String getType() { return "TEXT"; } + @Override + public FieldDef createUpdatedFieldDef( + String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { + return new TextFieldDef(name, requestField, context, this); + } + @Override protected void setSearchProperties(FieldType fieldType, Field requestField) { if (requestField.getSearch()) { diff --git a/src/main/java/com/yelp/nrtsearch/server/field/VectorFieldDef.java b/src/main/java/com/yelp/nrtsearch/server/field/VectorFieldDef.java index 5b7c0292a..48eac842f 100644 --- a/src/main/java/com/yelp/nrtsearch/server/field/VectorFieldDef.java +++ b/src/main/java/com/yelp/nrtsearch/server/field/VectorFieldDef.java @@ -254,18 +254,20 @@ static VectorFieldDef createField( * @param requestField field definition from grpc request * @param context creation context * @param docValuesClass class of doc values object + * @param previousField previous instance of this field definition, or null if there is none */ protected VectorFieldDef( String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context, - Class docValuesClass) { - super(name, requestField, context, docValuesClass); + Class docValuesClass, + VectorFieldDef previousField) { + super(name, requestField, context, docValuesClass, previousField); this.vectorDimensions = requestField.getVectorDimensions(); if (isSearchable()) { VectorSearchType vectorSearchType = getSearchType(requestField.getVectorIndexingOptions()); this.similarityFunction = getSimilarityFunction(requestField.getVectorSimilarity()); - setupNormalizedVectorField(requestField.getVectorSimilarity(), context); + setupNormalizedVectorField(requestField.getVectorSimilarity(), context, previousField); this.vectorsFormat = createVectorsFormat(vectorSearchType, requestField.getVectorIndexingOptions()); } else { @@ -276,9 +278,13 @@ protected VectorFieldDef( } private void setupNormalizedVectorField( - String similarity, FieldDefCreator.FieldDefCreatorContext context) { + String similarity, + FieldDefCreator.FieldDefCreatorContext context, + VectorFieldDef previousField) { if (NORMALIZED_COSINE.equals(similarity)) { // add field to store magnitude before normalization + FloatFieldDef previousMagnitudeField = + previousField != null ? previousField.magnitudeField : null; magnitudeField = new FloatFieldDef( getName() + MAGNITUDE_FIELD_SUFFIX, @@ -287,7 +293,8 @@ private void setupNormalizedVectorField( .setType(FieldType.FLOAT) .setStoreDocValues(true) .build(), - context); + context, + previousMagnitudeField); Map> childFieldsMap = new HashMap<>(super.getChildFields()); childFieldsMap.put(magnitudeField.getName(), magnitudeField); childFieldsWithMagnitude = Collections.unmodifiableMap(childFieldsMap); @@ -396,7 +403,24 @@ public Query getExactQuery(ExactVectorQuery exactVectorQuery) { public static class FloatVectorFieldDef extends VectorFieldDef { public FloatVectorFieldDef( String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { - super(name, requestField, context, FloatVectorType.class); + this(name, requestField, context, null); + } + + /** + * Constructor for creating an instance of this field based on a previous instance. This is used + * when updating field properties. + * + * @param name name of the field + * @param requestField the field definition from the request + * @param context context for creating the field definition + * @param previousField the previous instance of this field definition, or null if there is none + */ + protected FloatVectorFieldDef( + String name, + Field requestField, + FieldDefCreator.FieldDefCreatorContext context, + FloatVectorFieldDef previousField) { + super(name, requestField, context, FloatVectorType.class, previousField); } @Override @@ -579,13 +603,36 @@ private void normalizeVector(float[] vector, float magnitude) { vector[i] /= magnitude; } } + + @Override + public FieldDef createUpdatedFieldDef( + String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { + return new FloatVectorFieldDef(name, requestField, context, this); + } } /** Field class for 'BYTE' vector field type. */ public static class ByteVectorFieldDef extends VectorFieldDef { public ByteVectorFieldDef( String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { - super(name, requestField, context, ByteVectorType.class); + this(name, requestField, context, null); + } + + /** + * Constructor for creating an instance of this field based on a previous instance. This is used + * when updating field properties. + * + * @param name name of the field + * @param requestField the field definition from the request + * @param context context for creating the field definition + * @param previousField the previous instance of this field definition, or null if there is none + */ + protected ByteVectorFieldDef( + String name, + Field requestField, + FieldDefCreator.FieldDefCreatorContext context, + ByteVectorFieldDef previousField) { + super(name, requestField, context, ByteVectorType.class, previousField); if (NORMALIZED_COSINE.equals(requestField.getVectorSimilarity())) { throw new IllegalArgumentException( "Normalized cosine similarity is not supported for byte vectors"); @@ -731,5 +778,11 @@ public void validateVectorForSearch(byte[] vector) { } } } + + @Override + public FieldDef createUpdatedFieldDef( + String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { + return new ByteVectorFieldDef(name, requestField, context, this); + } } } diff --git a/src/main/java/com/yelp/nrtsearch/server/index/FieldUpdateUtils.java b/src/main/java/com/yelp/nrtsearch/server/index/FieldUpdateUtils.java index 1d26fa1a7..c41ede260 100644 --- a/src/main/java/com/yelp/nrtsearch/server/index/FieldUpdateUtils.java +++ b/src/main/java/com/yelp/nrtsearch/server/index/FieldUpdateUtils.java @@ -15,6 +15,7 @@ */ package com.yelp.nrtsearch.server.index; +import com.google.protobuf.Descriptors; import com.yelp.nrtsearch.server.doc.DocLookup; import com.yelp.nrtsearch.server.field.FieldDef; import com.yelp.nrtsearch.server.field.FieldDefCreator; @@ -29,6 +30,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import org.apache.lucene.search.DoubleValuesSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,6 +41,9 @@ /** Static helper class to handle a request to add/update fields. */ public class FieldUpdateUtils { private static final Logger logger = LoggerFactory.getLogger(FieldUpdateUtils.class); + // The name is not actually changeable, but it is allowed to be present to identify the field to + // update + private static final Set ALLOWED_UPDATABLE_FIELDS = Set.of("name", "childFields"); private FieldUpdateUtils() {} @@ -70,7 +78,8 @@ public static UpdatedFieldInfo updateFields( FieldDefCreator.FieldDefCreatorContext context) { Map newFields = new HashMap<>(currentFields); - FieldAndFacetState.Builder fieldStateBuilder = currentState.toBuilder(); + FieldAndFacetState.Builder fieldStateBuilder = + initializeFieldStateBuilder(currentState, currentFields, updateFields); List nonVirtualFields = new ArrayList<>(); List virtualFields = new ArrayList<>(); @@ -84,11 +93,10 @@ public static UpdatedFieldInfo updateFields( } for (Field field : nonVirtualFields) { - if (newFields.containsKey(field.getName())) { - throw new IllegalArgumentException("Duplicate field registration: " + field.getName()); - } - parseField(field, fieldStateBuilder, context); - newFields.put(field.getName(), field); + Field updatedField = getUpdatedField(field, newFields.get(field.getName())); + parseField( + updatedField, currentState.getFields().get(field.getName()), fieldStateBuilder, context); + newFields.put(updatedField.getName(), updatedField); } // Process the virtual fields after non-virtual fields, since they may depend on other @@ -127,15 +135,23 @@ public static void checkFieldName(String fieldName) { * FieldAndFacetState.Builder}. * * @param field field to process + * @param previousFieldDef current field definition, or null if this is a new field * @param fieldStateBuilder builder for new field state * @param context creation context */ public static void parseField( Field field, + FieldDef previousFieldDef, FieldAndFacetState.Builder fieldStateBuilder, FieldDefCreator.FieldDefCreatorContext context) { - FieldDef fieldDef = - FieldDefCreator.getInstance().createFieldDef(field.getName(), field, context); + FieldDef fieldDef; + if (previousFieldDef != null) { + fieldDef = + FieldDefCreator.getInstance() + .createFieldDefFromPrevious(field.getName(), field, previousFieldDef, context); + } else { + fieldDef = FieldDefCreator.getInstance().createFieldDef(field.getName(), field, context); + } fieldStateBuilder.addField(fieldDef, field); if (fieldDef instanceof IndexableFieldDef) { addChildFields((IndexableFieldDef) fieldDef, fieldStateBuilder); @@ -143,6 +159,112 @@ public static void parseField( logger.info("REGISTER: " + fieldDef.getName() + " -> " + fieldDef); } + /** + * Initialize a {@link FieldAndFacetState.Builder} with the current fields, excluding any fields + * that are being updated. + * + * @param currentState current state of the fields + * @param currentFields current fields + * @param updateFields fields to update + * @return a new {@link FieldAndFacetState.Builder} initialized with non-updated fields + */ + private static FieldAndFacetState.Builder initializeFieldStateBuilder( + FieldAndFacetState currentState, + Map currentFields, + Iterable updateFields) { + Set updateFieldNames = + StreamSupport.stream(updateFields.spliterator(), false) + .map(Field::getName) + .collect(Collectors.toSet()); + + FieldAndFacetState.Builder fieldStateBuilder = new FieldAndFacetState().toBuilder(); + for (Map.Entry entry : currentFields.entrySet()) { + String fieldName = entry.getKey(); + Field field = entry.getValue(); + if (!updateFieldNames.contains(fieldName)) { + FieldDef fieldDef = currentState.getFields().get(fieldName); + fieldStateBuilder.addField(fieldDef, field); + if (fieldDef instanceof IndexableFieldDef) { + addChildFields((IndexableFieldDef) fieldDef, fieldStateBuilder); + } + } + } + return fieldStateBuilder; + } + + /** + * Get the updated {@link Field} based on the new field and the existing field. If the existing + * field is null, it returns the new field. If the new field has only updatable properties, it + * updates the existing field with the new properties and returns it. + * + * @param newField the new field or update + * @param oldField the existing field, or null if this is a new field + * @return fully materialized {@link Field} with updated properties + */ + private static Field getUpdatedField(Field newField, Field oldField) { + Objects.requireNonNull(newField); + if (oldField == null) { + return newField; + } + + if (hasOnlyUpdatableProperties(newField)) { + Map updatedChildFields = new HashMap<>(); + for (Field childField : oldField.getChildFieldsList()) { + updatedChildFields.put(childField.getName(), childField); + } + List newChildFields = new ArrayList<>(); + + for (Field childField : newField.getChildFieldsList()) { + Field updatedChildField = + getUpdatedField(childField, updatedChildFields.get(childField.getName())); + if (updatedChildFields.containsKey(updatedChildField.getName())) { + updatedChildFields.put(updatedChildField.getName(), updatedChildField); + } else { + newChildFields.add(updatedChildField); + } + } + + // Add old fields first to maintain order in rebuilt Field + List finalChildFields = new ArrayList<>(); + for (Field childField : oldField.getChildFieldsList()) { + finalChildFields.add(updatedChildFields.get(childField.getName())); + } + finalChildFields.addAll(newChildFields); + + Field.Builder fieldBuilder = oldField.toBuilder(); + fieldBuilder.clearChildFields(); + fieldBuilder.addAllChildFields(finalChildFields); + + return fieldBuilder.build(); + } else { + throw new IllegalArgumentException("Duplicate field registration: " + newField.getName()); + } + } + + /** + * Check if the field has only updatable properties. + * + * @param field the field to check + * @return true if the field has only updatable properties, false otherwise + */ + static boolean hasOnlyUpdatableProperties(Field field) { + if (field.getChildFieldsCount() == 0) { + return false; + } + Set fieldKeys = + field.getAllFields().keySet().stream() + .map(Descriptors.FieldDescriptor::getName) + .collect(Collectors.toSet()); + fieldKeys.removeAll(ALLOWED_UPDATABLE_FIELDS); + if (fieldKeys.isEmpty()) { + return true; + } else { + logger.warn( + "Unable to update field {}: properties {} are not updatable", field.getName(), fieldKeys); + return false; + } + } + // recursively add all children to pendingFieldDefs private static void addChildFields( IndexableFieldDef indexableFieldDef, FieldAndFacetState.Builder fieldStateBuilder) { diff --git a/src/test/java/com/yelp/nrtsearch/server/field/AtomFieldDefTest.java b/src/test/java/com/yelp/nrtsearch/server/field/AtomFieldDefTest.java index de5d542b3..acd425d2d 100644 --- a/src/test/java/com/yelp/nrtsearch/server/field/AtomFieldDefTest.java +++ b/src/test/java/com/yelp/nrtsearch/server/field/AtomFieldDefTest.java @@ -16,6 +16,10 @@ package com.yelp.nrtsearch.server.field; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import com.yelp.nrtsearch.server.config.NrtsearchConfig; @@ -142,4 +146,24 @@ public void testDocValueType_binary() { .build()); assertEquals(DocValuesType.BINARY, fieldDef.getDocValuesType()); } + + @Test + public void testCreateUpdatedFieldDef() { + AtomFieldDef fieldDef = + createFieldDef(Field.newBuilder().setName("field").setStoreDocValues(true).build()); + FieldDef updatedField = + fieldDef.createUpdatedFieldDef( + "field", + Field.newBuilder().setStoreDocValues(false).build(), + mock(FieldDefCreator.FieldDefCreatorContext.class)); + assertTrue(updatedField instanceof AtomFieldDef); + AtomFieldDef updatedFieldDef = (AtomFieldDef) updatedField; + + assertNotSame(fieldDef, updatedFieldDef); + assertEquals("field", updatedFieldDef.getName()); + assertTrue(fieldDef.hasDocValues()); + assertFalse(updatedFieldDef.hasDocValues()); + assertSame(fieldDef.ordinalLookupCache, updatedFieldDef.ordinalLookupCache); + assertSame(fieldDef.ordinalBuilderLock, updatedFieldDef.ordinalBuilderLock); + } } diff --git a/src/test/java/com/yelp/nrtsearch/server/field/BooleanFieldDefTest.java b/src/test/java/com/yelp/nrtsearch/server/field/BooleanFieldDefTest.java index f3d7f7a63..fbf78375c 100644 --- a/src/test/java/com/yelp/nrtsearch/server/field/BooleanFieldDefTest.java +++ b/src/test/java/com/yelp/nrtsearch/server/field/BooleanFieldDefTest.java @@ -15,14 +15,23 @@ */ package com.yelp.nrtsearch.server.field; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import com.yelp.nrtsearch.server.grpc.Field; import org.junit.Assert; import org.junit.Test; public class BooleanFieldDefTest { + private BooleanFieldDef createFieldDef(Field field) { + return new BooleanFieldDef( + "test_field", field, mock(FieldDefCreator.FieldDefCreatorContext.class)); + } + @Test public void testParseTrue() { assertTrue(BooleanFieldDef.parseBooleanOrThrow("true")); @@ -58,4 +67,22 @@ private void assertMalformedString(String booleanStr) { } } + + @Test + public void testCreateUpdatedFieldDef() { + BooleanFieldDef fieldDef = + createFieldDef(Field.newBuilder().setName("field").setStoreDocValues(true).build()); + FieldDef updatedField = + fieldDef.createUpdatedFieldDef( + "field", + Field.newBuilder().setStoreDocValues(false).build(), + mock(FieldDefCreator.FieldDefCreatorContext.class)); + assertTrue(updatedField instanceof BooleanFieldDef); + BooleanFieldDef updatedFieldDef = (BooleanFieldDef) updatedField; + + assertNotSame(fieldDef, updatedFieldDef); + assertEquals("field", updatedFieldDef.getName()); + assertTrue(fieldDef.hasDocValues()); + assertFalse(updatedFieldDef.hasDocValues()); + } } diff --git a/src/test/java/com/yelp/nrtsearch/server/field/ContextSuggestFieldDefTest.java b/src/test/java/com/yelp/nrtsearch/server/field/ContextSuggestFieldDefTest.java index 7b5b57090..30bc08c64 100644 --- a/src/test/java/com/yelp/nrtsearch/server/field/ContextSuggestFieldDefTest.java +++ b/src/test/java/com/yelp/nrtsearch/server/field/ContextSuggestFieldDefTest.java @@ -17,6 +17,8 @@ import static com.yelp.nrtsearch.server.search.collectors.MyTopSuggestDocsCollector.SUGGEST_KEY_FIELD_NAME; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -49,6 +51,19 @@ public class ContextSuggestFieldDefTest extends ServerTestCase { private static final String MULTI_VALUED_FIELD_NAME = "context_suggest_name_multi_valued"; private static final String FIELD_TYPE = "CONTEXT_SUGGEST"; + private ContextSuggestFieldDef createFieldDef(Field field) { + return new ContextSuggestFieldDef("test_field", field, createFieldDefCreatorContext()); + } + + private FieldDefCreator.FieldDefCreatorContext createFieldDefCreatorContext() { + FieldDefCreator.FieldDefCreatorContext mockContext = + mock(FieldDefCreator.FieldDefCreatorContext.class); + NrtsearchConfig config = + new NrtsearchConfig(new ByteArrayInputStream("nodeName: node1".getBytes())); + when(mockContext.config()).thenReturn(config); + return mockContext; + } + public FieldDef getFieldDef(String testIndex, String fieldName) throws IOException { return getGrpcServer().getGlobalState().getIndexOrThrow(testIndex).getFieldOrThrow(fieldName); } @@ -246,4 +261,20 @@ private SearchRequest getRequestWithQuery(Query query) { .setQuery(query) .build(); } + + @Test + public void testCreateUpdatedFieldDef() { + ContextSuggestFieldDef fieldDef = + createFieldDef(Field.newBuilder().setName("field").setStore(true).build()); + FieldDef updatedField = + fieldDef.createUpdatedFieldDef( + "field", Field.newBuilder().setStore(false).build(), createFieldDefCreatorContext()); + assertTrue(updatedField instanceof ContextSuggestFieldDef); + ContextSuggestFieldDef updatedFieldDef = (ContextSuggestFieldDef) updatedField; + + assertNotSame(fieldDef, updatedFieldDef); + assertEquals("field", updatedFieldDef.getName()); + assertTrue(fieldDef.isStored()); + assertFalse(updatedFieldDef.isStored()); + } } diff --git a/src/test/java/com/yelp/nrtsearch/server/field/DateTimeFieldDefTest.java b/src/test/java/com/yelp/nrtsearch/server/field/DateTimeFieldDefTest.java index af6dda9dc..c5baf4a11 100644 --- a/src/test/java/com/yelp/nrtsearch/server/field/DateTimeFieldDefTest.java +++ b/src/test/java/com/yelp/nrtsearch/server/field/DateTimeFieldDefTest.java @@ -16,12 +16,16 @@ package com.yelp.nrtsearch.server.field; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; import com.yelp.nrtsearch.server.ServerTestCase; import com.yelp.nrtsearch.server.grpc.AddDocumentRequest; import com.yelp.nrtsearch.server.grpc.AddDocumentRequest.MultiValuedField; +import com.yelp.nrtsearch.server.grpc.Field; import com.yelp.nrtsearch.server.grpc.FieldDefRequest; import com.yelp.nrtsearch.server.grpc.Query; import com.yelp.nrtsearch.server.grpc.RangeQuery; @@ -48,6 +52,11 @@ public class DateTimeFieldDefTest extends ServerTestCase { @ClassRule public static final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + private DateTimeFieldDef createFieldDef(Field field) { + return new DateTimeFieldDef( + "test_field", field, mock(FieldDefCreator.FieldDefCreatorContext.class)); + } + protected List getIndices() { return Collections.singletonList(DEFAULT_TEST_INDEX); } @@ -920,4 +929,22 @@ private String formatAddDocumentsExceptionMessage( + "%s could not parse %s as date_time with format %s", dateTimeField, dateTimeValue, dateTimeFormat); } + + @Test + public void testCreateUpdatedFieldDef() { + DateTimeFieldDef fieldDef = + createFieldDef(Field.newBuilder().setName("field").setStoreDocValues(true).build()); + FieldDef updatedField = + fieldDef.createUpdatedFieldDef( + "field", + Field.newBuilder().setStoreDocValues(false).build(), + mock(FieldDefCreator.FieldDefCreatorContext.class)); + assertTrue(updatedField instanceof DateTimeFieldDef); + DateTimeFieldDef updatedFieldDef = (DateTimeFieldDef) updatedField; + + assertNotSame(fieldDef, updatedFieldDef); + assertEquals("field", updatedFieldDef.getName()); + assertTrue(fieldDef.hasDocValues()); + assertFalse(updatedFieldDef.hasDocValues()); + } } diff --git a/src/test/java/com/yelp/nrtsearch/server/field/DoubleFieldDefTest.java b/src/test/java/com/yelp/nrtsearch/server/field/DoubleFieldDefTest.java index 70af69e63..a23564de6 100644 --- a/src/test/java/com/yelp/nrtsearch/server/field/DoubleFieldDefTest.java +++ b/src/test/java/com/yelp/nrtsearch/server/field/DoubleFieldDefTest.java @@ -16,11 +16,15 @@ package com.yelp.nrtsearch.server.field; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; import com.yelp.nrtsearch.server.ServerTestCase; import com.yelp.nrtsearch.server.grpc.AddDocumentRequest; +import com.yelp.nrtsearch.server.grpc.Field; import com.yelp.nrtsearch.server.grpc.FieldDefRequest; import com.yelp.nrtsearch.server.grpc.Query; import com.yelp.nrtsearch.server.grpc.RangeQuery; @@ -42,6 +46,11 @@ public class DoubleFieldDefTest extends ServerTestCase { @ClassRule public static final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + private DoubleFieldDef createFieldDef(Field field) { + return new DoubleFieldDef( + "test_field", field, mock(FieldDefCreator.FieldDefCreatorContext.class)); + } + private static final String fieldName = "double_field"; private static final List values = Arrays.asList( @@ -324,4 +333,22 @@ private void assertRangeQuery(RangeQuery rangeQuery, Double... expectedValues) { .collect(Collectors.toList()); assertEquals(Arrays.asList(expectedValues), actualValues); } + + @Test + public void testCreateUpdatedFieldDef() { + DoubleFieldDef fieldDef = + createFieldDef(Field.newBuilder().setName("field").setStoreDocValues(true).build()); + FieldDef updatedField = + fieldDef.createUpdatedFieldDef( + "field", + Field.newBuilder().setStoreDocValues(false).build(), + mock(FieldDefCreator.FieldDefCreatorContext.class)); + assertTrue(updatedField instanceof DoubleFieldDef); + DoubleFieldDef updatedFieldDef = (DoubleFieldDef) updatedField; + + assertNotSame(fieldDef, updatedFieldDef); + assertEquals("field", updatedFieldDef.getName()); + assertTrue(fieldDef.hasDocValues()); + assertFalse(updatedFieldDef.hasDocValues()); + } } diff --git a/src/test/java/com/yelp/nrtsearch/server/field/FieldDefCreatorTest.java b/src/test/java/com/yelp/nrtsearch/server/field/FieldDefCreatorTest.java index 87cff7233..af6e3411b 100644 --- a/src/test/java/com/yelp/nrtsearch/server/field/FieldDefCreatorTest.java +++ b/src/test/java/com/yelp/nrtsearch/server/field/FieldDefCreatorTest.java @@ -15,8 +15,10 @@ */ package com.yelp.nrtsearch.server.field; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -149,4 +151,21 @@ public void testCreateContext() { assertSame(config, context.config()); } + + @Test + public void testCreateFieldDefFromPreviousNullUpdatedField() { + FieldDef mockFieldDef = mock(FieldDef.class); + when(mockFieldDef.getName()).thenReturn("test_field"); + try { + FieldDefCreator.getInstance() + .createFieldDefFromPrevious( + "field", + Field.newBuilder().build(), + mockFieldDef, + mock(FieldDefCreator.FieldDefCreatorContext.class)); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("FieldDef test_field cannot be updated", e.getMessage()); + } + } } diff --git a/src/test/java/com/yelp/nrtsearch/server/field/FloatFieldDefTest.java b/src/test/java/com/yelp/nrtsearch/server/field/FloatFieldDefTest.java index b58d3dccf..1309c1206 100644 --- a/src/test/java/com/yelp/nrtsearch/server/field/FloatFieldDefTest.java +++ b/src/test/java/com/yelp/nrtsearch/server/field/FloatFieldDefTest.java @@ -16,11 +16,15 @@ package com.yelp.nrtsearch.server.field; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; import com.yelp.nrtsearch.server.ServerTestCase; import com.yelp.nrtsearch.server.grpc.AddDocumentRequest; +import com.yelp.nrtsearch.server.grpc.Field; import com.yelp.nrtsearch.server.grpc.FieldDefRequest; import com.yelp.nrtsearch.server.grpc.Query; import com.yelp.nrtsearch.server.grpc.RangeQuery; @@ -42,6 +46,11 @@ public class FloatFieldDefTest extends ServerTestCase { @ClassRule public static final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + private FloatFieldDef createFieldDef(Field field) { + return new FloatFieldDef( + "test_field", field, mock(FieldDefCreator.FieldDefCreatorContext.class)); + } + private static final String fieldName = "float_field"; private static final List values = Arrays.asList( @@ -323,4 +332,22 @@ private void assertRangeQuery(RangeQuery rangeQuery, Float... expectedValues) { .collect(Collectors.toList()); assertEquals(Arrays.asList(expectedValues), actualValues); } + + @Test + public void testCreateUpdatedFieldDef() { + FloatFieldDef fieldDef = + createFieldDef(Field.newBuilder().setName("field").setStoreDocValues(true).build()); + FieldDef updatedField = + fieldDef.createUpdatedFieldDef( + "field", + Field.newBuilder().setStoreDocValues(false).build(), + mock(FieldDefCreator.FieldDefCreatorContext.class)); + assertTrue(updatedField instanceof FloatFieldDef); + FloatFieldDef updatedFieldDef = (FloatFieldDef) updatedField; + + assertNotSame(fieldDef, updatedFieldDef); + assertEquals("field", updatedFieldDef.getName()); + assertTrue(fieldDef.hasDocValues()); + assertFalse(updatedFieldDef.hasDocValues()); + } } diff --git a/src/test/java/com/yelp/nrtsearch/server/field/IdFieldTest.java b/src/test/java/com/yelp/nrtsearch/server/field/IdFieldTest.java index e53eda13f..591bddaf4 100644 --- a/src/test/java/com/yelp/nrtsearch/server/field/IdFieldTest.java +++ b/src/test/java/com/yelp/nrtsearch/server/field/IdFieldTest.java @@ -16,6 +16,10 @@ package com.yelp.nrtsearch.server.field; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; import com.yelp.nrtsearch.server.ServerTestCase; import com.yelp.nrtsearch.server.grpc.*; @@ -31,6 +35,10 @@ public class IdFieldTest extends ServerTestCase { @ClassRule public static final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + private IdFieldDef createFieldDef(Field field) { + return new IdFieldDef("test_field", field, mock(FieldDefCreator.FieldDefCreatorContext.class)); + } + @Override public FieldDefRequest getIndexDef(String name) throws IOException { return getFieldsFromResourceFile("/field/registerFieldsIdStored.json"); @@ -205,4 +213,22 @@ private void assertRangeQuery(RangeQuery rangeQuery, String... expectedValues) { .collect(Collectors.toList()); assertEquals(Arrays.asList(expectedValues), actualValues); } + + @Test + public void testCreateUpdatedFieldDef() { + IdFieldDef fieldDef = + createFieldDef(Field.newBuilder().setName("field").setStoreDocValues(true).build()); + FieldDef updatedField = + fieldDef.createUpdatedFieldDef( + "field", + Field.newBuilder().setStoreDocValues(false).setStore(true).build(), + mock(FieldDefCreator.FieldDefCreatorContext.class)); + assertTrue(updatedField instanceof IdFieldDef); + IdFieldDef updatedFieldDef = (IdFieldDef) updatedField; + + assertNotSame(fieldDef, updatedFieldDef); + assertEquals("field", updatedFieldDef.getName()); + assertTrue(fieldDef.hasDocValues()); + assertFalse(updatedFieldDef.hasDocValues()); + } } diff --git a/src/test/java/com/yelp/nrtsearch/server/field/IntFieldDefTest.java b/src/test/java/com/yelp/nrtsearch/server/field/IntFieldDefTest.java index ded6e0449..593a8f636 100644 --- a/src/test/java/com/yelp/nrtsearch/server/field/IntFieldDefTest.java +++ b/src/test/java/com/yelp/nrtsearch/server/field/IntFieldDefTest.java @@ -16,11 +16,15 @@ package com.yelp.nrtsearch.server.field; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; import com.yelp.nrtsearch.server.ServerTestCase; import com.yelp.nrtsearch.server.grpc.AddDocumentRequest; +import com.yelp.nrtsearch.server.grpc.Field; import com.yelp.nrtsearch.server.grpc.FieldDefRequest; import com.yelp.nrtsearch.server.grpc.Query; import com.yelp.nrtsearch.server.grpc.RangeQuery; @@ -42,6 +46,10 @@ public class IntFieldDefTest extends ServerTestCase { @ClassRule public static final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + private IntFieldDef createFieldDef(Field field) { + return new IntFieldDef("test_field", field, mock(FieldDefCreator.FieldDefCreatorContext.class)); + } + private static final String fieldName = "int_field"; private static final List values = Arrays.asList( @@ -314,4 +322,22 @@ private void assertRangeQuery(RangeQuery rangeQuery, Integer... expectedValues) .collect(Collectors.toList()); assertEquals(Arrays.asList(expectedValues), actualValues); } + + @Test + public void testCreateUpdatedFieldDef() { + IntFieldDef fieldDef = + createFieldDef(Field.newBuilder().setName("field").setStoreDocValues(true).build()); + FieldDef updatedField = + fieldDef.createUpdatedFieldDef( + "field", + Field.newBuilder().setStoreDocValues(false).build(), + mock(FieldDefCreator.FieldDefCreatorContext.class)); + assertTrue(updatedField instanceof IntFieldDef); + IntFieldDef updatedFieldDef = (IntFieldDef) updatedField; + + assertNotSame(fieldDef, updatedFieldDef); + assertEquals("field", updatedFieldDef.getName()); + assertTrue(fieldDef.hasDocValues()); + assertFalse(updatedFieldDef.hasDocValues()); + } } diff --git a/src/test/java/com/yelp/nrtsearch/server/field/LatLonFieldDefTest.java b/src/test/java/com/yelp/nrtsearch/server/field/LatLonFieldDefTest.java index 8553fb71d..53566ea17 100644 --- a/src/test/java/com/yelp/nrtsearch/server/field/LatLonFieldDefTest.java +++ b/src/test/java/com/yelp/nrtsearch/server/field/LatLonFieldDefTest.java @@ -16,8 +16,11 @@ package com.yelp.nrtsearch.server.field; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; import com.google.type.LatLng; import com.yelp.nrtsearch.server.ServerTestCase; @@ -38,6 +41,11 @@ public class LatLonFieldDefTest extends ServerTestCase { @ClassRule public static final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + private LatLonFieldDef createFieldDef(Field field) { + return new LatLonFieldDef( + "test_field", field, mock(FieldDefCreator.FieldDefCreatorContext.class)); + } + protected List getIndices() { return Collections.singletonList(DEFAULT_TEST_INDEX); } @@ -514,4 +522,22 @@ private void queryAndVerifyIds(Query query, String... expectedIds) { assertTrue(idList.contains(hit.getFieldsOrThrow("doc_id").getFieldValue(0).getTextValue())); } } + + @Test + public void testCreateUpdatedFieldDef() { + LatLonFieldDef fieldDef = + createFieldDef(Field.newBuilder().setName("field").setStoreDocValues(true).build()); + FieldDef updatedField = + fieldDef.createUpdatedFieldDef( + "field", + Field.newBuilder().setStoreDocValues(false).build(), + mock(FieldDefCreator.FieldDefCreatorContext.class)); + assertTrue(updatedField instanceof LatLonFieldDef); + LatLonFieldDef updatedFieldDef = (LatLonFieldDef) updatedField; + + assertNotSame(fieldDef, updatedFieldDef); + assertEquals("field", updatedFieldDef.getName()); + assertTrue(fieldDef.hasDocValues()); + assertFalse(updatedFieldDef.hasDocValues()); + } } diff --git a/src/test/java/com/yelp/nrtsearch/server/field/LongFieldDefTest.java b/src/test/java/com/yelp/nrtsearch/server/field/LongFieldDefTest.java index c1f1b95c3..ffb903576 100644 --- a/src/test/java/com/yelp/nrtsearch/server/field/LongFieldDefTest.java +++ b/src/test/java/com/yelp/nrtsearch/server/field/LongFieldDefTest.java @@ -16,11 +16,15 @@ package com.yelp.nrtsearch.server.field; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; import com.yelp.nrtsearch.server.ServerTestCase; import com.yelp.nrtsearch.server.grpc.AddDocumentRequest; +import com.yelp.nrtsearch.server.grpc.Field; import com.yelp.nrtsearch.server.grpc.FieldDefRequest; import com.yelp.nrtsearch.server.grpc.Query; import com.yelp.nrtsearch.server.grpc.RangeQuery; @@ -42,6 +46,11 @@ public class LongFieldDefTest extends ServerTestCase { @ClassRule public static final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + private LongFieldDef createFieldDef(Field field) { + return new LongFieldDef( + "test_field", field, mock(FieldDefCreator.FieldDefCreatorContext.class)); + } + private static final String fieldName = "long_field"; private static final List values = Arrays.asList( @@ -309,4 +318,22 @@ private void assertRangeQuery(RangeQuery rangeQuery, Long... expectedValues) { .collect(Collectors.toList()); assertEquals(Arrays.asList(expectedValues), actualValues); } + + @Test + public void testCreateUpdatedFieldDef() { + LongFieldDef fieldDef = + createFieldDef(Field.newBuilder().setName("field").setStoreDocValues(true).build()); + FieldDef updatedField = + fieldDef.createUpdatedFieldDef( + "field", + Field.newBuilder().setStoreDocValues(false).build(), + mock(FieldDefCreator.FieldDefCreatorContext.class)); + assertTrue(updatedField instanceof LongFieldDef); + LongFieldDef updatedFieldDef = (LongFieldDef) updatedField; + + assertNotSame(fieldDef, updatedFieldDef); + assertEquals("field", updatedFieldDef.getName()); + assertTrue(fieldDef.hasDocValues()); + assertFalse(updatedFieldDef.hasDocValues()); + } } diff --git a/src/test/java/com/yelp/nrtsearch/server/field/ObjectFieldDefTest.java b/src/test/java/com/yelp/nrtsearch/server/field/ObjectFieldDefTest.java index c8fb77779..393e2aade 100644 --- a/src/test/java/com/yelp/nrtsearch/server/field/ObjectFieldDefTest.java +++ b/src/test/java/com/yelp/nrtsearch/server/field/ObjectFieldDefTest.java @@ -17,6 +17,10 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -44,6 +48,11 @@ public class ObjectFieldDefTest extends ServerTestCase { @ClassRule public static final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); private static final String STORED_TEST_INDEX = "stored_test_index"; + private ObjectFieldDef createFieldDef(Field field) { + return new ObjectFieldDef( + "test_field", field, mock(FieldDefCreator.FieldDefCreatorContext.class)); + } + protected List getIndices() { return List.of(DEFAULT_TEST_INDEX, STORED_TEST_INDEX); } @@ -686,4 +695,22 @@ private void assertDataFields( Set expectedSet = new HashSet<>(Arrays.asList(expectedValues)); assertEquals(seenSet, expectedSet); } + + @Test + public void testCreateUpdatedFieldDef() { + ObjectFieldDef fieldDef = + createFieldDef(Field.newBuilder().setName("field").setStoreDocValues(true).build()); + FieldDef updatedField = + fieldDef.createUpdatedFieldDef( + "field", + Field.newBuilder().setStoreDocValues(false).build(), + mock(FieldDefCreator.FieldDefCreatorContext.class)); + assertTrue(updatedField instanceof ObjectFieldDef); + ObjectFieldDef updatedFieldDef = (ObjectFieldDef) updatedField; + + assertNotSame(fieldDef, updatedFieldDef); + assertEquals("field", updatedFieldDef.getName()); + assertTrue(fieldDef.hasDocValues()); + assertFalse(updatedFieldDef.hasDocValues()); + } } diff --git a/src/test/java/com/yelp/nrtsearch/server/field/PolygonFieldDefTest.java b/src/test/java/com/yelp/nrtsearch/server/field/PolygonFieldDefTest.java index f392963e2..1be761005 100644 --- a/src/test/java/com/yelp/nrtsearch/server/field/PolygonFieldDefTest.java +++ b/src/test/java/com/yelp/nrtsearch/server/field/PolygonFieldDefTest.java @@ -16,6 +16,8 @@ package com.yelp.nrtsearch.server.field; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -38,6 +40,11 @@ public class PolygonFieldDefTest extends ServerTestCase { @ClassRule public static final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + private PolygonfieldDef createFieldDef(Field field) { + return new PolygonfieldDef( + "test_field", field, mock(FieldDefCreator.FieldDefCreatorContext.class)); + } + protected List getIndices() { return Collections.singletonList(DEFAULT_TEST_INDEX); } @@ -331,4 +338,22 @@ private void queryAndVerifyIds(GeoPointQuery geoPolygonQuery, String... expected assertTrue(idList.contains(hit.getFieldsOrThrow("doc_id").getFieldValue(0).getTextValue())); } } + + @Test + public void testCreateUpdatedFieldDef() { + PolygonfieldDef fieldDef = + createFieldDef(Field.newBuilder().setName("field").setStoreDocValues(true).build()); + FieldDef updatedField = + fieldDef.createUpdatedFieldDef( + "field", + Field.newBuilder().setStoreDocValues(false).build(), + mock(FieldDefCreator.FieldDefCreatorContext.class)); + assertTrue(updatedField instanceof PolygonfieldDef); + PolygonfieldDef updatedFieldDef = (PolygonfieldDef) updatedField; + + assertNotSame(fieldDef, updatedFieldDef); + assertEquals("field", updatedFieldDef.getName()); + assertTrue(fieldDef.hasDocValues()); + assertFalse(updatedFieldDef.hasDocValues()); + } } diff --git a/src/test/java/com/yelp/nrtsearch/server/field/PrefixFieldDefTest.java b/src/test/java/com/yelp/nrtsearch/server/field/PrefixFieldDefTest.java index 993284549..ca4521639 100644 --- a/src/test/java/com/yelp/nrtsearch/server/field/PrefixFieldDefTest.java +++ b/src/test/java/com/yelp/nrtsearch/server/field/PrefixFieldDefTest.java @@ -35,6 +35,11 @@ public class PrefixFieldDefTest { + private PrefixFieldDef createPrefixFieldDef(Field field) { + return new PrefixFieldDef( + "test_field", field, mock(FieldDefCreator.FieldDefCreatorContext.class)); + } + @BeforeClass public static void init() { String configStr = "node: node1"; @@ -266,4 +271,23 @@ public void testPrefixQuery_NoPrefixField() { noPrefixFieldDef.getPrefixQuery(prefixQuery, MultiTermQuery.CONSTANT_SCORE_REWRITE, false); assertTrue(query instanceof org.apache.lucene.search.PrefixQuery); } + + @Test + public void testCreateUpdatedFieldDef() { + PrefixFieldDef fieldDef = + createPrefixFieldDef( + Field.newBuilder().setName("field").setSearch(true).setStoreDocValues(true).build()); + FieldDef updatedField = + fieldDef.createUpdatedFieldDef( + "field", + Field.newBuilder().setStoreDocValues(false).setSearch(true).build(), + mock(FieldDefCreator.FieldDefCreatorContext.class)); + assertTrue(updatedField instanceof PrefixFieldDef); + PrefixFieldDef updatedFieldDef = (PrefixFieldDef) updatedField; + + assertNotSame(fieldDef, updatedFieldDef); + assertEquals("field._index_prefix", updatedFieldDef.getName()); + assertTrue(fieldDef.hasDocValues()); + assertFalse(updatedFieldDef.hasDocValues()); + } } diff --git a/src/test/java/com/yelp/nrtsearch/server/field/TextFieldDefTest.java b/src/test/java/com/yelp/nrtsearch/server/field/TextFieldDefTest.java index 463a05a9f..a9c79163d 100644 --- a/src/test/java/com/yelp/nrtsearch/server/field/TextFieldDefTest.java +++ b/src/test/java/com/yelp/nrtsearch/server/field/TextFieldDefTest.java @@ -124,4 +124,24 @@ public void testPositionIncrementGap_invalid() { assertEquals("posIncGap must be >= 0", e.getMessage()); } } + + @Test + public void testCreateUpdatedFieldDef() { + TextFieldDef fieldDef = + createFieldDef(Field.newBuilder().setName("field").setStoreDocValues(true).build()); + FieldDef updatedField = + fieldDef.createUpdatedFieldDef( + "field", + Field.newBuilder().setStoreDocValues(false).build(), + mock(FieldDefCreator.FieldDefCreatorContext.class)); + assertTrue(updatedField instanceof TextFieldDef); + TextFieldDef updatedFieldDef = (TextFieldDef) updatedField; + + assertNotSame(fieldDef, updatedFieldDef); + assertEquals("field", updatedFieldDef.getName()); + assertTrue(fieldDef.hasDocValues()); + assertFalse(updatedFieldDef.hasDocValues()); + assertSame(fieldDef.ordinalLookupCache, updatedFieldDef.ordinalLookupCache); + assertSame(fieldDef.ordinalBuilderLock, updatedFieldDef.ordinalBuilderLock); + } } diff --git a/src/test/java/com/yelp/nrtsearch/server/field/VectorFieldDefTest.java b/src/test/java/com/yelp/nrtsearch/server/field/VectorFieldDefTest.java index f269176ee..0dbeef0de 100644 --- a/src/test/java/com/yelp/nrtsearch/server/field/VectorFieldDefTest.java +++ b/src/test/java/com/yelp/nrtsearch/server/field/VectorFieldDefTest.java @@ -17,7 +17,9 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; @@ -64,6 +66,16 @@ public class VectorFieldDefTest extends ServerTestCase { @ClassRule public static final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + private VectorFieldDef.FloatVectorFieldDef createFloatFieldDef(Field field) { + return new VectorFieldDef.FloatVectorFieldDef( + "test_field", field, mock(FieldDefCreator.FieldDefCreatorContext.class)); + } + + private VectorFieldDef.ByteVectorFieldDef createByteFieldDef(Field field) { + return new VectorFieldDef.ByteVectorFieldDef( + "test_field", field, mock(FieldDefCreator.FieldDefCreatorContext.class)); + } + public static final String VECTOR_SEARCH_INDEX_NAME = "vector_search_index"; public static final String NESTED_VECTOR_SEARCH_INDEX_NAME = "nested_vector_search_index"; private static final String FIELD_NAME = "vector_field"; @@ -2229,4 +2241,52 @@ private List getTrueByteTopHits( .release(searcherAndTaxonomy); } } + + @Test + public void testCreateUpdatedFieldDef_float() { + VectorFieldDef fieldDef = + createFloatFieldDef( + Field.newBuilder() + .setName("field") + .setStoreDocValues(true) + .setVectorDimensions(3) + .build()); + FieldDef updatedField = + fieldDef.createUpdatedFieldDef( + "field", + Field.newBuilder().setStoreDocValues(false).setVectorDimensions(3).build(), + mock(FieldDefCreator.FieldDefCreatorContext.class)); + assertTrue(updatedField instanceof VectorFieldDef.FloatVectorFieldDef); + VectorFieldDef.FloatVectorFieldDef updatedFieldDef = + (VectorFieldDef.FloatVectorFieldDef) updatedField; + + assertNotSame(fieldDef, updatedFieldDef); + assertEquals("field", updatedFieldDef.getName()); + assertTrue(fieldDef.hasDocValues()); + assertFalse(updatedFieldDef.hasDocValues()); + } + + @Test + public void testCreateUpdatedFieldDef_byte() { + VectorFieldDef fieldDef = + createByteFieldDef( + Field.newBuilder() + .setName("field") + .setStoreDocValues(true) + .setVectorDimensions(3) + .build()); + FieldDef updatedField = + fieldDef.createUpdatedFieldDef( + "field", + Field.newBuilder().setStoreDocValues(false).setVectorDimensions(3).build(), + mock(FieldDefCreator.FieldDefCreatorContext.class)); + assertTrue(updatedField instanceof VectorFieldDef.ByteVectorFieldDef); + VectorFieldDef.ByteVectorFieldDef updatedFieldDef = + (VectorFieldDef.ByteVectorFieldDef) updatedField; + + assertNotSame(fieldDef, updatedFieldDef); + assertEquals("field", updatedFieldDef.getName()); + assertTrue(fieldDef.hasDocValues()); + assertFalse(updatedFieldDef.hasDocValues()); + } } diff --git a/src/test/java/com/yelp/nrtsearch/server/grpc/CustomFieldTypeTest.java b/src/test/java/com/yelp/nrtsearch/server/grpc/CustomFieldTypeTest.java index 60068223e..e4a5ae91e 100644 --- a/src/test/java/com/yelp/nrtsearch/server/grpc/CustomFieldTypeTest.java +++ b/src/test/java/com/yelp/nrtsearch/server/grpc/CustomFieldTypeTest.java @@ -101,7 +101,7 @@ static class TestFieldDef extends IndexableFieldDef { public TestFieldDef( String name, Field requestField, FieldDefCreator.FieldDefCreatorContext context) { - super(name, requestField, context, Integer.class); + super(name, requestField, context, Integer.class, null); } @Override diff --git a/src/test/java/com/yelp/nrtsearch/server/grpc/UpdateFieldsTest.java b/src/test/java/com/yelp/nrtsearch/server/grpc/UpdateFieldsTest.java new file mode 100644 index 000000000..bebba9b7d --- /dev/null +++ b/src/test/java/com/yelp/nrtsearch/server/grpc/UpdateFieldsTest.java @@ -0,0 +1,504 @@ +/* + * Copyright 2025 Yelp Inc. + * + * 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.yelp.nrtsearch.server.grpc; + +import static com.yelp.nrtsearch.server.ServerTestCase.getFieldsFromResourceFile; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.yelp.nrtsearch.server.config.IndexStartConfig; +import com.yelp.nrtsearch.server.field.AtomFieldDef; +import com.yelp.nrtsearch.server.field.FieldDef; +import com.yelp.nrtsearch.server.field.LongFieldDef; +import com.yelp.nrtsearch.server.field.TextFieldDef; +import com.yelp.nrtsearch.server.index.ImmutableIndexState; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class UpdateFieldsTest { + @Rule public final TemporaryFolder folder = new TemporaryFolder(); + + private static final Set expectedFieldNames = + Set.of("doc_id", "text_field", "object_field", "int_field"); + + private static final Field idField = + Field.newBuilder() + .setName("doc_id") + .setType(FieldType._ID) + .setSearch(true) + .setStore(true) + .build(); + + private static final Field textField = + Field.newBuilder().setName("text_field").setType(FieldType.TEXT).setSearch(true).build(); + + private static final Field objectField = + Field.newBuilder() + .setName("object_field") + .setType(FieldType.OBJECT) + .addChildFields( + Field.newBuilder() + .setName("child_field") + .setType(FieldType.ATOM) + .setSearch(true) + .setStoreDocValues(true) + .build()) + .build(); + + private static final Field intField = + Field.newBuilder() + .setName("int_field") + .setType(FieldType.INT) + .setSearch(true) + .setStoreDocValues(true) + .build(); + + @After + public void cleanup() { + TestServer.cleanupAll(); + } + + private TestServer createPrimaryServer() throws Exception { + TestServer server = + TestServer.builder(folder) + .withAutoStartConfig( + true, Mode.PRIMARY, 0, IndexStartConfig.IndexDataLocationType.LOCAL) + .build(); + server.createIndex("test_index"); + server.startPrimaryIndex("test_index", -1, null); + server.registerFields( + "test_index", getFieldsFromResourceFile("/registerFieldsUpdateFields.json").getFieldList()); + assertEquals( + createExpectedFieldMap(), getIndexState(server).getIndexStateInfo().getFieldsMap()); + return server; + } + + private Map createExpectedFieldMap() { + Map expectedFieldMap = new HashMap<>(); + expectedFieldMap.put("doc_id", idField); + expectedFieldMap.put("text_field", textField); + expectedFieldMap.put("object_field", objectField); + expectedFieldMap.put("int_field", intField); + return expectedFieldMap; + } + + private ImmutableIndexState getIndexState(TestServer server) throws Exception { + return (ImmutableIndexState) server.getGlobalState().getIndex("test_index"); + } + + @Test + public void testAddChildField() throws Exception { + TestServer primaryServer = createPrimaryServer(); + primaryServer.registerFields( + "test_index", + List.of( + Field.newBuilder() + .setName("text_field") + .addChildFields( + Field.newBuilder() + .setName("new_child_field") + .setSearch(true) + .setStoreDocValues(true) + .build()) + .build())); + ImmutableIndexState indexState = getIndexState(primaryServer); + Map fieldMap = indexState.getIndexStateInfo().getFieldsMap(); + assertEquals(expectedFieldNames, fieldMap.keySet()); + + Map expectedFieldMap = createExpectedFieldMap(); + Field textFieldWithChild = + textField.toBuilder() + .addChildFields( + Field.newBuilder() + .setName("new_child_field") + .setSearch(true) + .setStoreDocValues(true) + .build()) + .build(); + expectedFieldMap.put("text_field", textFieldWithChild); + assertEquals(expectedFieldMap, indexState.getIndexStateInfo().getFieldsMap()); + + FieldDef newFieldDef = indexState.getFieldOrThrow("text_field.new_child_field"); + assertTrue(newFieldDef instanceof AtomFieldDef); + } + + @Test + public void testAddMultipleChildField() throws Exception { + TestServer primaryServer = createPrimaryServer(); + primaryServer.registerFields( + "test_index", + List.of( + Field.newBuilder() + .setName("text_field") + .addChildFields( + Field.newBuilder() + .setName("new_child_field_1") + .setSearch(true) + .setStoreDocValues(true) + .build()) + .addChildFields( + Field.newBuilder() + .setName("new_child_field_2") + .setType(FieldType.TEXT) + .setAnalyzer(Analyzer.newBuilder().setPredefined("classic").build()) + .setSearch(true) + .build()) + .build())); + ImmutableIndexState indexState = getIndexState(primaryServer); + Map fieldMap = indexState.getIndexStateInfo().getFieldsMap(); + assertEquals(expectedFieldNames, fieldMap.keySet()); + + Map expectedFieldMap = createExpectedFieldMap(); + Field textFieldWithChild = + textField.toBuilder() + .addChildFields( + Field.newBuilder() + .setName("new_child_field_1") + .setSearch(true) + .setStoreDocValues(true) + .build()) + .addChildFields( + Field.newBuilder() + .setName("new_child_field_2") + .setType(FieldType.TEXT) + .setAnalyzer(Analyzer.newBuilder().setPredefined("classic").build()) + .setSearch(true) + .build()) + .build(); + expectedFieldMap.put("text_field", textFieldWithChild); + assertEquals(expectedFieldMap, indexState.getIndexStateInfo().getFieldsMap()); + + FieldDef newFieldDef = indexState.getFieldOrThrow("text_field.new_child_field_1"); + assertTrue(newFieldDef instanceof AtomFieldDef); + newFieldDef = indexState.getFieldOrThrow("text_field.new_child_field_2"); + assertTrue(newFieldDef instanceof TextFieldDef); + } + + @Test + public void testAddWithExistingChildren() throws Exception { + TestServer primaryServer = createPrimaryServer(); + primaryServer.registerFields( + "test_index", + List.of( + Field.newBuilder() + .setName("object_field") + .addChildFields( + Field.newBuilder() + .setName("new_child_field_1") + .setType(FieldType.LONG) + .setSearch(true) + .setStoreDocValues(true) + .build()) + .addChildFields( + Field.newBuilder() + .setName("new_child_field_2") + .setType(FieldType.TEXT) + .setAnalyzer(Analyzer.newBuilder().setPredefined("classic").build()) + .setSearch(true) + .build()) + .build())); + ImmutableIndexState indexState = getIndexState(primaryServer); + Map fieldMap = indexState.getIndexStateInfo().getFieldsMap(); + assertEquals(expectedFieldNames, fieldMap.keySet()); + + Map expectedFieldMap = createExpectedFieldMap(); + Field objectFieldWithChild = + objectField.toBuilder() + .addChildFields( + Field.newBuilder() + .setName("new_child_field_1") + .setType(FieldType.LONG) + .setSearch(true) + .setStoreDocValues(true) + .build()) + .addChildFields( + Field.newBuilder() + .setName("new_child_field_2") + .setType(FieldType.TEXT) + .setAnalyzer(Analyzer.newBuilder().setPredefined("classic").build()) + .setSearch(true) + .build()) + .build(); + expectedFieldMap.put("object_field", objectFieldWithChild); + assertEquals(expectedFieldMap, indexState.getIndexStateInfo().getFieldsMap()); + + FieldDef newFieldDef = indexState.getFieldOrThrow("object_field.child_field"); + assertTrue(newFieldDef instanceof AtomFieldDef); + newFieldDef = indexState.getFieldOrThrow("object_field.new_child_field_1"); + assertTrue(newFieldDef instanceof LongFieldDef); + newFieldDef = indexState.getFieldOrThrow("object_field.new_child_field_2"); + assertTrue(newFieldDef instanceof TextFieldDef); + } + + @Test + public void testAddChildrenToMultipleFields() throws Exception { + TestServer primaryServer = createPrimaryServer(); + primaryServer.registerFields( + "test_index", + List.of( + Field.newBuilder() + .setName("text_field") + .addChildFields( + Field.newBuilder() + .setName("new_child_field_1") + .setSearch(true) + .setStoreDocValues(true) + .build()) + .addChildFields( + Field.newBuilder() + .setName("new_child_field_2") + .setType(FieldType.TEXT) + .setAnalyzer(Analyzer.newBuilder().setPredefined("classic").build()) + .setSearch(true) + .build()) + .build(), + Field.newBuilder() + .setName("object_field") + .addChildFields( + Field.newBuilder() + .setName("new_child_field_1") + .setType(FieldType.LONG) + .setSearch(true) + .setStoreDocValues(true) + .build()) + .addChildFields( + Field.newBuilder() + .setName("new_child_field_2") + .setType(FieldType.TEXT) + .setAnalyzer(Analyzer.newBuilder().setPredefined("classic").build()) + .setSearch(true) + .build()) + .build())); + ImmutableIndexState indexState = getIndexState(primaryServer); + Map fieldMap = indexState.getIndexStateInfo().getFieldsMap(); + assertEquals(expectedFieldNames, fieldMap.keySet()); + + Map expectedFieldMap = createExpectedFieldMap(); + Field textFieldWithChild = + textField.toBuilder() + .addChildFields( + Field.newBuilder() + .setName("new_child_field_1") + .setSearch(true) + .setStoreDocValues(true) + .build()) + .addChildFields( + Field.newBuilder() + .setName("new_child_field_2") + .setType(FieldType.TEXT) + .setAnalyzer(Analyzer.newBuilder().setPredefined("classic").build()) + .setSearch(true) + .build()) + .build(); + expectedFieldMap.put("text_field", textFieldWithChild); + + Field objectFieldWithChild = + objectField.toBuilder() + .addChildFields( + Field.newBuilder() + .setName("new_child_field_1") + .setType(FieldType.LONG) + .setSearch(true) + .setStoreDocValues(true) + .build()) + .addChildFields( + Field.newBuilder() + .setName("new_child_field_2") + .setType(FieldType.TEXT) + .setAnalyzer(Analyzer.newBuilder().setPredefined("classic").build()) + .setSearch(true) + .build()) + .build(); + expectedFieldMap.put("object_field", objectFieldWithChild); + assertEquals(expectedFieldMap, indexState.getIndexStateInfo().getFieldsMap()); + + FieldDef newFieldDef = indexState.getFieldOrThrow("text_field.new_child_field_1"); + assertTrue(newFieldDef instanceof AtomFieldDef); + newFieldDef = indexState.getFieldOrThrow("text_field.new_child_field_2"); + assertTrue(newFieldDef instanceof TextFieldDef); + + newFieldDef = indexState.getFieldOrThrow("object_field.child_field"); + assertTrue(newFieldDef instanceof AtomFieldDef); + newFieldDef = indexState.getFieldOrThrow("object_field.new_child_field_1"); + assertTrue(newFieldDef instanceof LongFieldDef); + newFieldDef = indexState.getFieldOrThrow("object_field.new_child_field_2"); + assertTrue(newFieldDef instanceof TextFieldDef); + } + + @Test + public void testAddChildAndTopLevelField() throws Exception { + TestServer primaryServer = createPrimaryServer(); + primaryServer.registerFields( + "test_index", + List.of( + Field.newBuilder() + .setName("text_field") + .addChildFields( + Field.newBuilder() + .setName("new_child_field") + .setSearch(true) + .setStoreDocValues(true) + .build()) + .build(), + Field.newBuilder() + .setName("new_top_level_field") + .setType(FieldType.TEXT) + .setSearch(true) + .build())); + ImmutableIndexState indexState = getIndexState(primaryServer); + + Map expectedFieldMap = createExpectedFieldMap(); + Field textFieldWithChild = + textField.toBuilder() + .addChildFields( + Field.newBuilder() + .setName("new_child_field") + .setSearch(true) + .setStoreDocValues(true) + .build()) + .build(); + expectedFieldMap.put("text_field", textFieldWithChild); + expectedFieldMap.put( + "new_top_level_field", + Field.newBuilder() + .setName("new_top_level_field") + .setType(FieldType.TEXT) + .setSearch(true) + .build()); + assertEquals(expectedFieldMap, indexState.getIndexStateInfo().getFieldsMap()); + + FieldDef newFieldDef = indexState.getFieldOrThrow("text_field.new_child_field"); + assertTrue(newFieldDef instanceof AtomFieldDef); + newFieldDef = indexState.getFieldOrThrow("new_top_level_field"); + assertTrue(newFieldDef instanceof TextFieldDef); + } + + @Test + public void testAddNestedChildField() throws Exception { + TestServer primaryServer = createPrimaryServer(); + primaryServer.registerFields( + "test_index", + List.of( + Field.newBuilder() + .setName("object_field") + .addChildFields( + Field.newBuilder() + .setName("child_field") + .addChildFields( + Field.newBuilder() + .setName("nested_child_field") + .setType(FieldType.TEXT) + .setSearch(true) + .setStoreDocValues(true) + .build())) + .build())); + ImmutableIndexState indexState = getIndexState(primaryServer); + Map fieldMap = indexState.getIndexStateInfo().getFieldsMap(); + assertEquals(expectedFieldNames, fieldMap.keySet()); + + Map expectedFieldMap = createExpectedFieldMap(); + + Field nestedChildField = + objectField.getChildFields(0).toBuilder() + .addChildFields( + Field.newBuilder() + .setName("nested_child_field") + .setType(FieldType.TEXT) + .setSearch(true) + .setStoreDocValues(true) + .build()) + .build(); + + Field objectFieldWithChild = + objectField.toBuilder().setChildFields(0, nestedChildField).build(); + expectedFieldMap.put("object_field", objectFieldWithChild); + assertEquals(expectedFieldMap, indexState.getIndexStateInfo().getFieldsMap()); + + FieldDef newFieldDef = + indexState.getFieldOrThrow("object_field.child_field.nested_child_field"); + assertTrue(newFieldDef instanceof TextFieldDef); + } + + @Test + public void testAddNestedChildFieldToMultipleLevels() throws Exception { + TestServer primaryServer = createPrimaryServer(); + primaryServer.registerFields( + "test_index", + List.of( + Field.newBuilder() + .setName("object_field") + .addChildFields( + Field.newBuilder() + .setName("child_field") + .addChildFields( + Field.newBuilder() + .setName("nested_child_field") + .setType(FieldType.TEXT) + .setSearch(true) + .setStoreDocValues(true) + .build())) + .addChildFields( + Field.newBuilder() + .setName("new_child_field") + .setType(FieldType.TEXT) + .setSearch(true) + .setStore(true) + .build()) + .build())); + ImmutableIndexState indexState = getIndexState(primaryServer); + Map fieldMap = indexState.getIndexStateInfo().getFieldsMap(); + assertEquals(expectedFieldNames, fieldMap.keySet()); + + Map expectedFieldMap = createExpectedFieldMap(); + + Field nestedChildField = + objectField.getChildFields(0).toBuilder() + .addChildFields( + Field.newBuilder() + .setName("nested_child_field") + .setType(FieldType.TEXT) + .setSearch(true) + .setStoreDocValues(true) + .build()) + .build(); + + Field objectFieldWithChild = + objectField.toBuilder() + .setChildFields(0, nestedChildField) + .addChildFields( + Field.newBuilder() + .setName("new_child_field") + .setType(FieldType.TEXT) + .setSearch(true) + .setStore(true) + .build()) + .build(); + expectedFieldMap.put("object_field", objectFieldWithChild); + assertEquals(expectedFieldMap, indexState.getIndexStateInfo().getFieldsMap()); + + FieldDef newFieldDef = + indexState.getFieldOrThrow("object_field.child_field.nested_child_field"); + assertTrue(newFieldDef instanceof TextFieldDef); + newFieldDef = indexState.getFieldOrThrow("object_field.new_child_field"); + assertTrue(newFieldDef instanceof TextFieldDef); + } +} diff --git a/src/test/resources/registerFieldsUpdateFields.json b/src/test/resources/registerFieldsUpdateFields.json new file mode 100644 index 000000000..36d59ee39 --- /dev/null +++ b/src/test/resources/registerFieldsUpdateFields.json @@ -0,0 +1,34 @@ +{ + "indexName": "test_index", + "field": [ + { + "name": "doc_id", + "type": "_ID", + "search": true, + "store": true + }, + { + "name": "text_field", + "type": "TEXT", + "search": true + }, + { + "name": "object_field", + "type": "OBJECT", + "childFields": [ + { + "name": "child_field", + "type": "ATOM", + "search": true, + "storeDocValues": true + } + ] + }, + { + "name": "int_field", + "type": "INT", + "search": true, + "storeDocValues": true + } + ] +}