diff --git a/core/src/main/java/com/redis/vl/schema/NumericField.java b/core/src/main/java/com/redis/vl/schema/NumericField.java index bd07900..2788f0d 100644 --- a/core/src/main/java/com/redis/vl/schema/NumericField.java +++ b/core/src/main/java/com/redis/vl/schema/NumericField.java @@ -10,18 +10,22 @@ @Getter public class NumericField extends BaseField { + /** Un-normalized form - disable normalization for sorting (only applies when sortable=true) */ + private final boolean unf; + /** * Create a NumericField with just a name. * * @param name The field name */ public NumericField(String name) { - super(name); + this(name, null, true, false, false); } /** Create a NumericField with all properties */ - private NumericField(String name, String alias, Boolean indexed, Boolean sortable) { + private NumericField(String name, String alias, Boolean indexed, Boolean sortable, Boolean unf) { super(name, alias, indexed != null ? indexed : true, sortable != null ? sortable : false); + this.unf = unf != null ? unf : false; } /** @@ -59,6 +63,9 @@ public SchemaField toJedisSchemaField() { if (sortable) { jedisField.sortable(); + // NOTE: Jedis NumericField doesn't support sortableUNF() yet + // The unf flag is stored in this wrapper but cannot be passed to Redis via Jedis + // TODO: File issue with Jedis to add sortableUNF() support for NumericField } if (!indexed) { @@ -74,6 +81,7 @@ public static class NumericFieldBuilder { private String alias; private Boolean indexed; private Boolean sortable; + private Boolean unf; private NumericFieldBuilder(String name) { this.name = name; @@ -144,6 +152,39 @@ public NumericFieldBuilder sortable() { return this; } + /** + * Set whether to use un-normalized form for sorting + * + *

UNF disables normalization when sorting, preserving original values. Only applies when + * sortable=true. + * + *

NOTE: Jedis doesn't support sortableUNF() for NumericField yet, so this flag is stored but + * not passed to Redis. + * + * @param unf True to disable normalization for sorting + * @return This builder for chaining + */ + public NumericFieldBuilder unf(boolean unf) { + this.unf = unf; + return this; + } + + /** + * Use un-normalized form for sorting (equivalent to unf(true)) + * + *

UNF disables normalization when sorting, preserving original values. Only applies when + * sortable=true. + * + *

NOTE: Jedis doesn't support sortableUNF() for NumericField yet, so this flag is stored but + * not passed to Redis. + * + * @return This builder for chaining + */ + public NumericFieldBuilder unf() { + this.unf = true; + return this; + } + /** * Build the NumericField instance. * @@ -154,7 +195,7 @@ public NumericField build() { if (name == null || name.trim().isEmpty()) { throw new IllegalArgumentException("Field name cannot be null or empty"); } - return new NumericField(name, alias, indexed, sortable); + return new NumericField(name, alias, indexed, sortable, unf); } } } diff --git a/core/src/main/java/com/redis/vl/schema/TextField.java b/core/src/main/java/com/redis/vl/schema/TextField.java index c99789b..55bca8f 100644 --- a/core/src/main/java/com/redis/vl/schema/TextField.java +++ b/core/src/main/java/com/redis/vl/schema/TextField.java @@ -23,13 +23,17 @@ public class TextField extends BaseField { @JsonProperty("phonetic") private final String phonetic; + /** Un-normalized form - disable normalization for sorting (only applies when sortable=true) */ + @JsonProperty("unf") + private final boolean unf; + /** * Create a TextField with just a name * * @param name Field name */ public TextField(String name) { - this(name, null, true, false, 1.0, false, null); + this(name, null, true, false, 1.0, false, null, false); } /** Create a TextField with all properties */ @@ -40,11 +44,13 @@ private TextField( Boolean sortable, Double weight, Boolean noStem, - String phonetic) { + String phonetic, + Boolean unf) { super(name, alias, indexed != null ? indexed : true, sortable != null ? sortable : false); this.weight = weight != null ? weight : 1.0; this.noStem = noStem != null ? noStem : false; this.phonetic = phonetic; + this.unf = unf != null ? unf : false; } /** @@ -80,7 +86,11 @@ public SchemaField toJedisSchemaField() { jedisField.as(alias); } - if (sortable) { + // Handle sortable with UNF support + // UNF only applies when sortable=true + if (sortable && unf) { + jedisField.sortableUNF(); + } else if (sortable) { jedisField.sortable(); } @@ -112,6 +122,7 @@ public static class TextFieldBuilder { private Double weight; private Boolean noStem; private String phonetic; + private Boolean unf; private TextFieldBuilder(String name) { this.name = name; @@ -247,6 +258,33 @@ public TextFieldBuilder withPhonetic(String phonetic) { return this; } + /** + * Set whether to use un-normalized form for sorting + * + *

UNF disables normalization when sorting, preserving original character case. Only applies + * when sortable=true. + * + * @param unf True to disable normalization for sorting + * @return This builder + */ + public TextFieldBuilder unf(boolean unf) { + this.unf = unf; + return this; + } + + /** + * Use un-normalized form for sorting (equivalent to unf(true)) + * + *

UNF disables normalization when sorting, preserving original character case. Only applies + * when sortable=true. + * + * @return This builder + */ + public TextFieldBuilder unf() { + this.unf = true; + return this; + } + /** * Build the TextField * @@ -256,7 +294,7 @@ public TextField build() { if (name == null || name.trim().isEmpty()) { throw new IllegalArgumentException("Field name cannot be null or empty"); } - return new TextField(name, alias, indexed, sortable, weight, noStem, phonetic); + return new TextField(name, alias, indexed, sortable, weight, noStem, phonetic, unf); } } } diff --git a/core/src/test/java/com/redis/vl/schema/UnfNoindexFieldsTest.java b/core/src/test/java/com/redis/vl/schema/UnfNoindexFieldsTest.java new file mode 100644 index 0000000..3ed6631 --- /dev/null +++ b/core/src/test/java/com/redis/vl/schema/UnfNoindexFieldsTest.java @@ -0,0 +1,288 @@ +package com.redis.vl.schema; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import redis.clients.jedis.search.schemafields.SchemaField; + +/** + * Unit tests for UNF and NOINDEX field attributes (issue #374). + * + *

Ported from Python: tests/unit/test_unf_noindex_fields.py + * + *

Python reference: PR #386 - UNF and NOINDEX attributes for field classes + * + *

Key behaviors: + * + *

+ */ +@DisplayName("UNF and NOINDEX Field Attributes Tests") +class UnfNoindexFieldsTest { + + // ========== TextField Tests ========== + + @Test + @DisplayName("TextField: Should support NOINDEX attribute") + void testTextFieldNoIndex() { + // Python: TextField(name="title", attrs={"no_index": True}) + TextField field = TextField.builder().name("title").indexed(false).build(); + + assertThat(field.isIndexed()).isFalse(); + + // Verify Jedis field is created (actual NOINDEX verification in integration tests) + SchemaField jedisField = field.toJedisSchemaField(); + assertThat(jedisField).isNotNull(); + assertThat(jedisField).isInstanceOf(redis.clients.jedis.search.schemafields.TextField.class); + } + + @Test + @DisplayName("TextField: Should support NOINDEX with sortable") + void testTextFieldNoIndexWithSortable() { + // Python: TextField(name="title", attrs={"no_index": True, "sortable": True}) + TextField field = TextField.builder().name("title").indexed(false).sortable(true).build(); + + assertThat(field.isIndexed()).isFalse(); + assertThat(field.isSortable()).isTrue(); + + SchemaField jedisField = field.toJedisSchemaField(); + assertThat(jedisField).isNotNull(); + } + + @Test + @DisplayName("TextField: Should support UNF attribute with sortable") + void testTextFieldUnfWithSortable() { + // Python: TextField(name="title", attrs={"unf": True, "sortable": True}) + TextField field = TextField.builder().name("title").unf(true).sortable(true).build(); + + assertThat(field.isUnf()).isTrue(); + assertThat(field.isSortable()).isTrue(); + + // Verify Jedis field is created with sortableUNF + SchemaField jedisField = field.toJedisSchemaField(); + assertThat(jedisField).isNotNull(); + assertThat(jedisField).isInstanceOf(redis.clients.jedis.search.schemafields.TextField.class); + } + + @Test + @DisplayName("TextField: UNF should be ignored when not sortable") + void testTextFieldUnfWithoutSortable() { + // Python: TextField(name="title", attrs={"unf": True}) + // UNF is ignored when sortable=False + TextField field = TextField.builder().name("title").unf(true).build(); + + assertThat(field.isUnf()).isTrue(); + assertThat(field.isSortable()).isFalse(); + + // Verify Jedis field is created (UNF ignored internally because not sortable) + SchemaField jedisField = field.toJedisSchemaField(); + assertThat(jedisField).isNotNull(); + } + + @Test + @DisplayName("TextField: Should support both UNF and NOINDEX with sortable") + void testTextFieldUnfAndNoIndex() { + // Python: TextField(name="title", attrs={"unf": True, "no_index": True, "sortable": True}) + // UNF and NOINDEX can coexist when sortable=True + TextField field = + TextField.builder().name("title").unf(true).indexed(false).sortable(true).build(); + + assertThat(field.isUnf()).isTrue(); + assertThat(field.isIndexed()).isFalse(); + assertThat(field.isSortable()).isTrue(); + + SchemaField jedisField = field.toJedisSchemaField(); + assertThat(jedisField).isNotNull(); + } + + @Test + @DisplayName("TextField: Builder should have fluent unf() method") + void testTextFieldBuilderFluentUnf() { + // Java convenience: unf() without parameter + TextField field = TextField.builder().name("title").sortable().unf().build(); + + assertThat(field.isUnf()).isTrue(); + assertThat(field.isSortable()).isTrue(); + } + + // ========== NumericField Tests ========== + + @Test + @DisplayName("NumericField: Should support NOINDEX attribute") + void testNumericFieldNoIndex() { + // Python: NumericField(name="price", attrs={"no_index": True}) + NumericField field = NumericField.builder().name("price").indexed(false).build(); + + assertThat(field.isIndexed()).isFalse(); + + SchemaField jedisField = field.toJedisSchemaField(); + assertThat(jedisField).isNotNull(); + assertThat(jedisField).isInstanceOf(redis.clients.jedis.search.schemafields.NumericField.class); + } + + @Test + @DisplayName("NumericField: Should support UNF attribute with sortable") + void testNumericFieldUnfWithSortable() { + // Python: NumericField(name="price", attrs={"unf": True, "sortable": True}) + // Note: Jedis NumericField doesn't have sortableUNF() yet, so we document the limitation + NumericField field = NumericField.builder().name("price").unf(true).sortable(true).build(); + + assertThat(field.isUnf()).isTrue(); + assertThat(field.isSortable()).isTrue(); + + SchemaField jedisField = field.toJedisSchemaField(); + assertThat(jedisField).isNotNull(); + } + + @Test + @DisplayName("NumericField: UNF should be stored even when not sortable") + void testNumericFieldUnfWithoutSortable() { + // Python: NumericField(name="price", attrs={"unf": True}) + NumericField field = NumericField.builder().name("price").unf(true).build(); + + assertThat(field.isUnf()).isTrue(); + assertThat(field.isSortable()).isFalse(); + + SchemaField jedisField = field.toJedisSchemaField(); + assertThat(jedisField).isNotNull(); + } + + @Test + @DisplayName("NumericField: Should support both UNF and NOINDEX with sortable") + void testNumericFieldUnfAndNoIndex() { + // Python: NumericField(name="price", attrs={"unf": True, "no_index": True, "sortable": True}) + NumericField field = + NumericField.builder().name("price").unf(true).indexed(false).sortable(true).build(); + + assertThat(field.isUnf()).isTrue(); + assertThat(field.isIndexed()).isFalse(); + assertThat(field.isSortable()).isTrue(); + + SchemaField jedisField = field.toJedisSchemaField(); + assertThat(jedisField).isNotNull(); + } + + @Test + @DisplayName("NumericField: Builder should have fluent unf() method") + void testNumericFieldBuilderFluentUnf() { + NumericField field = NumericField.builder().name("price").sortable().unf().build(); + + assertThat(field.isUnf()).isTrue(); + assertThat(field.isSortable()).isTrue(); + } + + // ========== TagField Tests ========== + + @Test + @DisplayName("TagField: Should support NOINDEX attribute") + void testTagFieldNoIndex() { + // Python: TagField(name="category", attrs={"no_index": True}) + TagField field = TagField.builder().name("category").indexed(false).build(); + + assertThat(field.isIndexed()).isFalse(); + + SchemaField jedisField = field.toJedisSchemaField(); + assertThat(jedisField).isNotNull(); + assertThat(jedisField).isInstanceOf(redis.clients.jedis.search.schemafields.TagField.class); + } + + @Test + @DisplayName("TagField: Should support NOINDEX with sortable") + void testTagFieldNoIndexWithSortable() { + // Python: TagField(name="category", attrs={"no_index": True, "sortable": True}) + TagField field = TagField.builder().name("category").indexed(false).sortable(true).build(); + + assertThat(field.isIndexed()).isFalse(); + assertThat(field.isSortable()).isTrue(); + + SchemaField jedisField = field.toJedisSchemaField(); + assertThat(jedisField).isNotNull(); + } + + @Test + @DisplayName("TagField: Should NOT support UNF attribute") + void testTagFieldNoUnf() { + // Python: TagField does not have 'unf' attribute + // Verify TagField class does not have unf field or method + TagField field = TagField.builder().name("category").sortable(true).build(); + + // TagField should not have unf() method - this is compile-time checked + // Just verify it builds successfully without unf + assertThat(field.isSortable()).isTrue(); + } + + // ========== GeoField Tests ========== + + @Test + @DisplayName("GeoField: Should support NOINDEX attribute") + void testGeoFieldNoIndex() { + // Python: GeoField(name="location", attrs={"no_index": True}) + GeoField field = GeoField.builder().name("location").indexed(false).build(); + + assertThat(field.isIndexed()).isFalse(); + + SchemaField jedisField = field.toJedisSchemaField(); + assertThat(jedisField).isNotNull(); + assertThat(jedisField).isInstanceOf(redis.clients.jedis.search.schemafields.GeoField.class); + } + + @Test + @DisplayName("GeoField: Should support NOINDEX with sortable") + void testGeoFieldNoIndexWithSortable() { + // Python: GeoField(name="location", attrs={"no_index": True, "sortable": True}) + GeoField field = GeoField.builder().name("location").indexed(false).sortable(true).build(); + + assertThat(field.isIndexed()).isFalse(); + assertThat(field.isSortable()).isTrue(); + + SchemaField jedisField = field.toJedisSchemaField(); + assertThat(jedisField).isNotNull(); + } + + @Test + @DisplayName("GeoField: Should NOT support UNF attribute") + void testGeoFieldNoUnf() { + // Python: GeoField does not have 'unf' attribute + GeoField field = GeoField.builder().name("location").sortable(true).build(); + + // GeoField should not have unf() method - this is compile-time checked + assertThat(field.isSortable()).isTrue(); + } + + // ========== Backward Compatibility Tests ========== + + @Test + @DisplayName("TextField: Default values should maintain backward compatibility") + void testTextFieldBackwardCompatibility() { + // Python: TextField(name="title") - defaults: indexed=True, sortable=False, unf=False + TextField field = TextField.of("title").build(); + + assertThat(field.isIndexed()).isTrue(); + assertThat(field.isSortable()).isFalse(); + assertThat(field.isUnf()).isFalse(); + + SchemaField jedisField = field.toJedisSchemaField(); + assertThat(jedisField).isNotNull(); + } + + @Test + @DisplayName("NumericField: Default values should maintain backward compatibility") + void testNumericFieldBackwardCompatibility() { + // Python: NumericField(name="price") - defaults: indexed=True, sortable=False, unf=False + NumericField field = NumericField.of("price").build(); + + assertThat(field.isIndexed()).isTrue(); + assertThat(field.isSortable()).isFalse(); + assertThat(field.isUnf()).isFalse(); + + SchemaField jedisField = field.toJedisSchemaField(); + assertThat(jedisField).isNotNull(); + } +}