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:
+ *
+ *
+ * - NOINDEX: Prevents field from being indexed (saves memory), field still sortable/retrievable
+ *
- UNF: Un-normalized form - preserves original character case for sorting
+ *
- UNF only applies when sortable=True
+ *
- TextField and NumericField support both NOINDEX and UNF
+ *
- TagField and GeoField support NOINDEX only
+ *
- Redis command order: UNF must come before NOINDEX
+ *
+ */
+@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();
+ }
+}