Skip to content

Commit b279fdd

Browse files
authored
feature: enable indexed enumeration to be serialized as an ordinal and indexed as numeric (resolves gh-280) (#335)
* #280 added the ability to save an enumeration as a number * #280 added test * #280 added "ORDINAL" value to SchemaFieldType. removed annotation @Enumerated
1 parent c3bc5d3 commit b279fdd

File tree

9 files changed

+209
-34
lines changed

9 files changed

+209
-34
lines changed

redis-om-spring/src/main/java/com/redis/om/spring/RediSearchIndexer.java

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package com.redis.om.spring;
22

3+
import com.google.gson.GsonBuilder;
34
import com.google.gson.annotations.JsonAdapter;
45
import com.redis.om.spring.annotations.*;
56
import com.redis.om.spring.ops.RedisModulesOperations;
67
import com.redis.om.spring.ops.search.SearchOperations;
78
import com.redis.om.spring.repository.query.QueryUtils;
9+
import com.redis.om.spring.serialization.gson.EnumTypeAdapter;
810
import org.apache.commons.lang3.ObjectUtils;
911
import org.apache.commons.logging.Log;
1012
import org.apache.commons.logging.LogFactory;
@@ -50,14 +52,16 @@ public class RediSearchIndexer {
5052
private final ApplicationContext ac;
5153
private final RedisModulesOperations<String> rmo;
5254
private final RedisMappingContext mappingContext;
55+
private final GsonBuilder gsonBuilder;
5356

5457
private static final String SKIPPING_INDEX_CREATION = "Skipping index creation for %s because %s";
5558

5659
@SuppressWarnings("unchecked")
57-
public RediSearchIndexer(ApplicationContext ac) {
60+
public RediSearchIndexer(ApplicationContext ac, GsonBuilder gsonBuilder) {
5861
this.ac = ac;
5962
rmo = (RedisModulesOperations<String>) ac.getBean("redisModulesOperations");
6063
mappingContext = (RedisMappingContext) ac.getBean("keyValueMappingContext");
64+
this.gsonBuilder = gsonBuilder;
6165
}
6266

6367
public void createIndicesFor(Class<?> cls) {
@@ -207,9 +211,10 @@ private List<Field> findIndexFields(java.lang.reflect.Field field, String prefix
207211
createIndexedFieldForReferenceIdField(field, isDocument).ifPresent(fields::add);
208212
} else if (indexed.schemaFieldType() == SchemaFieldType.AUTODETECT) {
209213
//
210-
// Any Character class, Enums or Boolean -> Tag Search Field
214+
// Any Character class, Boolean or Enum with AUTODETECT -> Tag Search Field
211215
//
212-
if (CharSequence.class.isAssignableFrom(fieldType) || (fieldType == Boolean.class) || (fieldType.isEnum())) {
216+
if (CharSequence.class.isAssignableFrom(fieldType) || (fieldType == Boolean.class) ||
217+
(fieldType.isEnum())) {
213218
fields.add(indexAsTagFieldFor(field, isDocument, prefix, indexed.sortable(), indexed.separator(),
214219
indexed.arrayIndex(), indexed.alias()));
215220
}
@@ -288,6 +293,11 @@ else if (fieldType == Point.class) {
288293
fields.addAll(findIndexFields(subfield, subfieldPrefix, isDocument));
289294
}
290295
}
296+
case ORDINAL -> {
297+
fields.add(indexAsNumericFieldFor(field, isDocument, prefix, indexed.sortable(),
298+
indexed.noindex(), indexed.alias()));
299+
gsonBuilder.registerTypeAdapter(fieldType, EnumTypeAdapter.of(fieldType));
300+
}
291301
}
292302
}
293303
}
@@ -444,67 +454,49 @@ private Field indexAsTagFieldFor(java.lang.reflect.Field field, boolean isDocume
444454
}
445455

446456
private Field indexAsTextFieldFor(java.lang.reflect.Field field, boolean isDocument, String prefix, TextIndexed ti) {
447-
String fieldPrefix = getFieldPrefix(prefix, isDocument);
448-
String name = fieldPrefix + field.getName();
449-
String alias = ObjectUtils.isEmpty(ti.alias()) ? QueryUtils.searchIndexFieldAliasFor(field, prefix) : ti.alias();
450-
FieldName fieldName = FieldName.of(name);
451-
fieldName = fieldName.as(alias);
457+
var fieldName = getFieldName(field, isDocument, prefix, ObjectUtils.isEmpty(ti.alias())
458+
? QueryUtils.searchIndexFieldAliasFor(field, prefix) : ti.alias());
452459

453460
String phonetic = ObjectUtils.isEmpty(ti.phonetic()) ? null : ti.phonetic();
454461

455462
return new TextField(fieldName, ti.weight(), ti.sortable(), ti.nostem(), ti.noindex(), phonetic);
456463
}
457464

458465
private Field indexAsTextFieldFor(java.lang.reflect.Field field, boolean isDocument, String prefix, Searchable ti) {
459-
String fieldPrefix = getFieldPrefix(prefix, isDocument);
460-
String name = fieldPrefix + field.getName();
461-
String alias = ObjectUtils.isEmpty(ti.alias()) ? QueryUtils.searchIndexFieldAliasFor(field, prefix) : ti.alias();
462-
FieldName fieldName = FieldName.of(name);
463-
fieldName = fieldName.as(alias);
466+
var fieldName = getFieldName(field, isDocument, prefix, ObjectUtils.isEmpty(ti.alias())
467+
? QueryUtils.searchIndexFieldAliasFor(field, prefix) : ti.alias());
464468

465469
String phonetic = ObjectUtils.isEmpty(ti.phonetic()) ? null : ti.phonetic();
466470

467471
return new TextField(fieldName, ti.weight(), ti.sortable(), ti.nostem(), ti.noindex(), phonetic);
468472
}
469473

470474
private Field indexAsGeoFieldFor(java.lang.reflect.Field field, boolean isDocument, String prefix, GeoIndexed gi) {
471-
String fieldPrefix = getFieldPrefix(prefix, isDocument);
472-
String name = fieldPrefix + field.getName();
473-
String alias = ObjectUtils.isEmpty(gi.alias()) ? QueryUtils.searchIndexFieldAliasFor(field, prefix) : gi.alias();
474-
FieldName fieldName = FieldName.of(name);
475-
fieldName = fieldName.as(alias);
475+
var fieldName = getFieldName(field, isDocument, prefix, ObjectUtils.isEmpty(gi.alias())
476+
? QueryUtils.searchIndexFieldAliasFor(field, prefix) : gi.alias());
476477

477478
return new Field(fieldName, FieldType.GEO);
478479
}
479480

480481
private Field indexAsNumericFieldFor(java.lang.reflect.Field field, boolean isDocument, String prefix,
481482
NumericIndexed ni) {
482-
String fieldPrefix = getFieldPrefix(prefix, isDocument);
483-
String name = fieldPrefix + field.getName();
484-
String alias = ObjectUtils.isEmpty(ni.alias()) ? QueryUtils.searchIndexFieldAliasFor(field, prefix) : ni.alias();
485-
FieldName fieldName = FieldName.of(name);
486-
fieldName = fieldName.as(alias);
483+
var fieldName = getFieldName(field, isDocument, prefix, ObjectUtils.isEmpty(ni.alias())
484+
? QueryUtils.searchIndexFieldAliasFor(field, prefix) : ni.alias());
487485

488486
return new Field(fieldName, FieldType.NUMERIC);
489487
}
490488

491489
private Field indexAsNumericFieldFor(java.lang.reflect.Field field, boolean isDocument, String prefix,
492490
boolean sortable, boolean noIndex, String annotationAlias) {
493-
String fieldPrefix = getFieldPrefix(prefix, isDocument);
494-
String name = fieldPrefix + field.getName();
495491
String alias = (annotationAlias == null || annotationAlias.isBlank()) ? QueryUtils.searchIndexFieldAliasFor(field, prefix) : annotationAlias;
496-
FieldName fieldName = FieldName.of(name);
497-
fieldName = fieldName.as(alias);
492+
var fieldName = getFieldName(field, isDocument, prefix, alias);
498493

499494
return new Field(fieldName, FieldType.NUMERIC, sortable, noIndex);
500495
}
501496

502497
private Field indexAsGeoFieldFor(java.lang.reflect.Field field, boolean isDocument, String prefix, String annotationAlias) {
503-
String fieldPrefix = getFieldPrefix(prefix, isDocument);
504-
String name = fieldPrefix + field.getName();
505498
String alias = (annotationAlias == null || annotationAlias.isBlank()) ? QueryUtils.searchIndexFieldAliasFor(field, prefix) : annotationAlias;
506-
FieldName fieldName = FieldName.of(name);
507-
fieldName = fieldName.as(alias);
499+
var fieldName = getFieldName(field, isDocument, prefix, alias);
508500

509501
return new Field(fieldName, FieldType.GEO);
510502
}
@@ -514,6 +506,15 @@ private List<Field> indexAsNestedFieldFor(java.lang.reflect.Field field, String
514506
return getNestedField(fieldPrefix, field, prefix, null);
515507
}
516508

509+
private FieldName getFieldName(java.lang.reflect.Field field, boolean isDocument, String prefix,
510+
String alias) {
511+
String fieldPrefix = getFieldPrefix(prefix, isDocument);
512+
String name = fieldPrefix + field.getName();
513+
FieldName fieldName = FieldName.of(name);
514+
fieldName = fieldName.as(alias);
515+
return fieldName;
516+
}
517+
517518
private List<Field> getNestedField(String fieldPrefix, java.lang.reflect.Field field, String prefix,
518519
List<Field> fieldList) {
519520
if (fieldList == null) {

redis-om-spring/src/main/java/com/redis/om/spring/RedisModulesConfiguration.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,9 @@ CuckooFilterOperations<?> redisCuckooFilterOperations(RedisModulesOperations<?>
147147
}
148148

149149
@Bean(name = "rediSearchIndexer")
150-
public RediSearchIndexer redisearchIndexer(ApplicationContext ac) {
151-
return new RediSearchIndexer(ac);
150+
public RediSearchIndexer redisearchIndexer(ApplicationContext ac,
151+
@Qualifier("omGsonBuilder") GsonBuilder gsonBuilder) {
152+
return new RediSearchIndexer(ac, gsonBuilder);
152153
}
153154

154155
@Bean(name = "djlImageFactory")

redis-om-spring/src/main/java/com/redis/om/spring/annotations/SchemaFieldType.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ public enum SchemaFieldType {
66
NUMERIC,
77
GEO,
88
VECTOR,
9-
NESTED
9+
NESTED,
10+
ORDINAL
1011
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.redis.om.spring.serialization.gson;
2+
3+
import com.google.gson.JsonDeserializationContext;
4+
import com.google.gson.JsonDeserializer;
5+
import com.google.gson.JsonElement;
6+
import com.google.gson.JsonParseException;
7+
import com.google.gson.JsonPrimitive;
8+
import com.google.gson.JsonSerializationContext;
9+
import com.google.gson.JsonSerializer;
10+
import java.lang.reflect.Type;
11+
12+
public class EnumTypeAdapter<T extends Enum<?>> implements JsonSerializer<T>,
13+
JsonDeserializer<T> {
14+
15+
private final T[] values;
16+
17+
public EnumTypeAdapter(Class<?> enumType) {
18+
this.values = (T[]) enumType.getEnumConstants();
19+
}
20+
21+
public static <T extends Enum<?>> EnumTypeAdapter<T> of(Class<?> enumType) {
22+
return new EnumTypeAdapter<>(enumType);
23+
}
24+
25+
@Override
26+
public JsonElement serialize(T o, Type type,
27+
JsonSerializationContext jsonSerializationContext) {
28+
return new JsonPrimitive(o.ordinal());
29+
}
30+
31+
@Override
32+
public T deserialize(JsonElement json, Type type, JsonDeserializationContext context)
33+
throws JsonParseException {
34+
return values[json.getAsInt()];
35+
}
36+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.redis.om.spring.annotations.document;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import com.google.gson.Gson;
6+
import com.google.gson.annotations.SerializedName;
7+
import com.redis.om.spring.AbstractBaseDocumentTest;
8+
import com.redis.om.spring.annotations.document.fixtures.Developer;
9+
import com.redis.om.spring.annotations.document.fixtures.DeveloperRepository;
10+
import com.redis.om.spring.annotations.document.fixtures.DeveloperState;
11+
import com.redis.om.spring.annotations.document.fixtures.DeveloperType;
12+
import com.redis.om.spring.client.RedisModulesClient;
13+
import java.util.List;
14+
import java.util.function.Function;
15+
import java.util.stream.Collectors;
16+
import lombok.AccessLevel;
17+
import lombok.AllArgsConstructor;
18+
import lombok.Builder;
19+
import lombok.Getter;
20+
import lombok.Setter;
21+
import org.junit.jupiter.api.BeforeEach;
22+
import org.junit.jupiter.api.Test;
23+
import org.springframework.beans.factory.annotation.Autowired;
24+
25+
class EnumeratedTest extends AbstractBaseDocumentTest {
26+
27+
@Autowired
28+
DeveloperRepository developerRepository;
29+
30+
@Autowired
31+
RedisModulesClient redisModulesClient;
32+
33+
@BeforeEach
34+
void loadTestData() {
35+
var java = Developer.builder()
36+
.id("1")
37+
.typeOrdinal(DeveloperType.JAVA)
38+
.state(DeveloperState.REST)
39+
.build();
40+
var cpp = Developer.builder()
41+
.id("2")
42+
.typeOrdinal(DeveloperType.CPP)
43+
.state(DeveloperState.WORK)
44+
.build();
45+
var python = Developer.builder()
46+
.id("3")
47+
.typeOrdinal(DeveloperType.PYTHON)
48+
.state(DeveloperState.WORK)
49+
.build();
50+
51+
developerRepository.saveAll(List.of(java, cpp, python));
52+
}
53+
54+
@Test
55+
void testSaveEnumAsNumber() {
56+
57+
var search = redisModulesClient.clientForSearch();
58+
Gson gson = new Gson();
59+
60+
var data = search.ftSearch("com.redis.om.spring.annotations.document.fixtures.DeveloperIdx")
61+
.getDocuments().stream()
62+
.map(el -> gson.fromJson((String) el.get("$"), DeveloperNative.class))
63+
.collect(Collectors.toMap(DeveloperNative::getId, Function.identity()));
64+
65+
assertThat(data.get("1").getOrdinal()).isEqualTo(DeveloperType.JAVA.ordinal());
66+
assertThat(data.get("2").getOrdinal()).isEqualTo(DeveloperType.CPP.ordinal());
67+
assertThat(data.get("3").getOrdinal()).isEqualTo(DeveloperType.PYTHON.ordinal());
68+
69+
assertThat(data.get("1").getState()).isEqualTo(DeveloperState.REST.toString());
70+
assertThat(data.get("2").getState()).isEqualTo(DeveloperState.WORK.toString());
71+
assertThat(data.get("3").getState()).isEqualTo(DeveloperState.WORK.toString());
72+
73+
}
74+
75+
@Getter
76+
@Setter
77+
@Builder
78+
@AllArgsConstructor(access = AccessLevel.PROTECTED)
79+
private static class DeveloperNative {
80+
81+
private String id;
82+
@SerializedName("typeOrdinal")
83+
private int ordinal;
84+
85+
private String state;
86+
}
87+
88+
89+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.redis.om.spring.annotations.document.fixtures;
2+
3+
import com.redis.om.spring.annotations.Document;
4+
import com.redis.om.spring.annotations.Indexed;
5+
import com.redis.om.spring.annotations.SchemaFieldType;
6+
import lombok.AccessLevel;
7+
import lombok.AllArgsConstructor;
8+
import lombok.Builder;
9+
import lombok.Getter;
10+
import lombok.NoArgsConstructor;
11+
import lombok.Setter;
12+
import org.springframework.data.annotation.Id;
13+
14+
@Getter
15+
@Setter
16+
@Builder
17+
@NoArgsConstructor
18+
@AllArgsConstructor(access = AccessLevel.PROTECTED)
19+
@Document
20+
public class Developer {
21+
22+
@Id
23+
private String id;
24+
25+
@Indexed(schemaFieldType = SchemaFieldType.ORDINAL)
26+
private DeveloperType typeOrdinal;
27+
28+
private DeveloperState state;
29+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.redis.om.spring.annotations.document.fixtures;
2+
3+
import com.redis.om.spring.repository.RedisDocumentRepository;
4+
5+
@SuppressWarnings("unused")
6+
public interface DeveloperRepository extends RedisDocumentRepository<Developer, String> {
7+
8+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.redis.om.spring.annotations.document.fixtures;
2+
3+
public enum DeveloperState {
4+
WORK, REST
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.redis.om.spring.annotations.document.fixtures;
2+
3+
public enum DeveloperType {
4+
JAVA, CPP, PYTHON
5+
}

0 commit comments

Comments
 (0)