Skip to content

Commit 678f955

Browse files
committed
feat: add lexicographic string comparison support for Redis OM Spring (#526)
Implements lexicographic string comparison operations (>, <, >=, <=, between) for string fields in Redis OM Spring. This feature enables efficient range queries on string data using Redis sorted sets with ZRANGEBYLEX commands. Key changes: - Add lexicographic parameter to @indexed and @searchable annotations - Implement sorted set indexing for fields marked with lexicographic=true - Add gt(), lt(), and between() methods to TextTagField and TextField metamodel classes - Support repository query methods like findByFieldGreaterThan for lexicographic fields - Integrate lexicographic predicates with EntityStream API - Create LexicographicQueryExecutor to handle repository method queries - Implement marker/implementation predicate pattern for clean separation of concerns Usage: ```java @indexed(lexicographic = true) private String comId; // Repository method List<Entity> findByComIdGreaterThan(String value); // EntityStream API entityStream.of(Entity.class) .filter(Entity$.COM_ID.gt("100000")) .collect(Collectors.toList()); ```
1 parent f8ded2f commit 678f955

37 files changed

+3097
-29
lines changed

docs/content/modules/ROOT/pages/entity-streams.adoc

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,54 @@ List<Company> prefixMatches = entityStream.of(Company.class)
130130
.collect(Collectors.toList());
131131
----
132132

133+
=== Lexicographic String Comparisons
134+
135+
For fields marked with `@Indexed(lexicographic = true)` or `@Searchable(lexicographic = true)`, you can perform string range queries:
136+
137+
[source,java]
138+
----
139+
@Document
140+
public class Product {
141+
@Id
142+
private String id;
143+
144+
@Indexed(lexicographic = true)
145+
private String sku;
146+
147+
@Searchable(lexicographic = true)
148+
private String name;
149+
}
150+
151+
// Find products with SKU greater than a value
152+
List<Product> products = entityStream.of(Product.class)
153+
.filter(Product$.SKU.gt("PROD-1000"))
154+
.collect(Collectors.toList());
155+
156+
// Find products with SKU less than a value
157+
List<Product> earlyProducts = entityStream.of(Product.class)
158+
.filter(Product$.SKU.lt("PROD-0500"))
159+
.collect(Collectors.toList());
160+
161+
// Find products with SKU between two values
162+
List<Product> rangeProducts = entityStream.of(Product.class)
163+
.filter(Product$.SKU.between("PROD-1000", "PROD-2000"))
164+
.collect(Collectors.toList());
165+
166+
// Combine with other predicates
167+
List<Product> filteredProducts = entityStream.of(Product.class)
168+
.filter(Product$.SKU.gt("PROD-1000")
169+
.and(Product$.NAME.containing("Premium")))
170+
.sorted(Product$.SKU)
171+
.collect(Collectors.toList());
172+
173+
// Works with TextFields too (when lexicographic = true)
174+
List<Product> alphabeticalRange = entityStream.of(Product.class)
175+
.filter(Product$.NAME.between("A", "M"))
176+
.collect(Collectors.toList());
177+
----
178+
179+
NOTE: Lexicographic comparisons use Redis sorted sets for efficient range queries. They're ideal for ID ranges, SKU comparisons, version strings, and alphabetical filtering.
180+
133181
=== Boolean Predicates
134182

135183
[source,java]

docs/content/modules/ROOT/pages/index-annotations.adoc

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,42 @@ public class Company {
3737
}
3838
----
3939

40+
==== Configuration Options:
41+
42+
* `sortable` - Whether the field can be used for sorting (default: false)
43+
* `fieldName` - Custom field name in the index
44+
* `alias` - Field alias for queries
45+
* `indexMissing` - Index missing/null values (default: false)
46+
* `indexEmpty` - Index empty string values (default: false)
47+
* `lexicographic` - Enable lexicographic string comparisons for TAG fields (default: false)
48+
49+
==== Lexicographic String Comparisons
50+
51+
The `lexicographic` parameter enables string range queries (>, <, >=, <=, between) on TAG-indexed string fields by creating an additional Redis sorted set index:
52+
53+
[source,java]
54+
----
55+
@Document
56+
public class Product {
57+
@Id
58+
private String id;
59+
60+
@Indexed(lexicographic = true)
61+
private String sku; // Enables findBySkuGreaterThan("ABC123")
62+
63+
@Indexed(lexicographic = true)
64+
private String productCode; // Enables range queries on product codes
65+
}
66+
----
67+
68+
When `lexicographic = true`:
69+
- An additional Redis sorted set is created for the field (e.g., `Product:sku:lex`)
70+
- Repository methods like `findBySkuGreaterThan`, `findBySkuLessThan`, and `findBySkuBetween` are supported
71+
- EntityStream operations like `Product$.SKU.gt("ABC")` and `Product$.SKU.between("A", "Z")` work
72+
- Useful for ID ranges, SKU comparisons, alphabetical ordering, and version strings
73+
74+
NOTE: Lexicographic indexing requires additional storage for the sorted set. Only enable it for fields where you need string range queries.
75+
4076
=== @Searchable
4177

4278
For full-text search on string fields, use `@Searchable` which provides rich text search capabilities:
@@ -67,6 +103,37 @@ public class Game {
67103
* `phonetic` - Enable phonetic matching using Double Metaphone algorithm (default: "")
68104
* `indexMissing` - Index missing/null values (default: false)
69105
* `indexEmpty` - Index empty string values (default: false)
106+
* `lexicographic` - Enable lexicographic string comparisons (default: false)
107+
108+
==== Lexicographic String Comparisons with @Searchable
109+
110+
The `lexicographic` parameter also works with `@Searchable` fields to enable string range queries:
111+
112+
[source,java]
113+
----
114+
@Document
115+
public class Article {
116+
@Id
117+
private String id;
118+
119+
@Searchable(lexicographic = true)
120+
private String title; // Enables alphabetical range queries
121+
122+
@Searchable(lexicographic = true, sortable = true)
123+
private String category; // Range queries with sorting
124+
}
125+
126+
// Repository methods
127+
public interface ArticleRepository extends RedisDocumentRepository<Article, String> {
128+
// Find articles with titles alphabetically after "M"
129+
List<Article> findByTitleGreaterThan(String title);
130+
131+
// Find articles in category range
132+
List<Article> findByCategoryBetween(String start, String end);
133+
}
134+
----
135+
136+
NOTE: When using `lexicographic = true` on `@Searchable` fields, Redis OM Spring creates both a full-text index and a sorted set for range queries. This allows you to use both text search methods (like `findByTitleContaining`) and range queries (like `findByTitleGreaterThan`) on the same field.
70137

71138
== Specialized Indexing Annotations
72139

docs/content/modules/ROOT/pages/quickstart.adoc

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,257 @@ It is important to understand that as opposed to a regular Java stream, most ope
961961
be executed server-side. On the query above the query is executed during and only during the terminal `collect` operation. At that point you
962962
will have a `List<Company>` objects now loaded in memory.
963963

964+
== Step 21: Lexicographic String Comparisons
965+
966+
Redis OM Spring supports lexicographic (alphabetical) string comparisons for range queries on string fields. This is useful for finding entities within ID ranges, SKU comparisons, or alphabetical filtering.
967+
968+
Let's add a Product entity to demonstrate this feature:
969+
970+
[source,java]
971+
----
972+
package com.example.demo.domain;
973+
974+
import org.springframework.data.annotation.Id;
975+
976+
import com.redis.om.spring.annotations.Document;
977+
import com.redis.om.spring.annotations.Indexed;
978+
import com.redis.om.spring.annotations.Searchable;
979+
980+
import lombok.*;
981+
982+
@Data
983+
@NoArgsConstructor
984+
@RequiredArgsConstructor(staticName = "of")
985+
@Document
986+
public class Product {
987+
@Id
988+
private String id;
989+
990+
@NonNull
991+
@Searchable
992+
private String name;
993+
994+
@NonNull
995+
@Indexed(lexicographic = true) // Enable lexicographic comparisons
996+
private String sku;
997+
998+
@NonNull
999+
@Indexed
1000+
private Double price;
1001+
1002+
@Indexed(lexicographic = true) // Version strings can be compared
1003+
private String version;
1004+
}
1005+
----
1006+
1007+
Notice the `lexicographic = true` parameter on the `@Indexed` annotation. This tells Redis OM Spring to create an additional sorted set index for string range queries.
1008+
1009+
== Step 22: Create the Product Repository
1010+
1011+
[source,java]
1012+
----
1013+
package com.example.demo.repositories;
1014+
1015+
import java.util.List;
1016+
1017+
import com.example.demo.domain.Product;
1018+
import com.redis.om.spring.repository.RedisDocumentRepository;
1019+
1020+
public interface ProductRepository extends RedisDocumentRepository<Product, String> {
1021+
// Lexicographic string comparisons
1022+
List<Product> findBySkuGreaterThan(String sku);
1023+
List<Product> findBySkuLessThan(String sku);
1024+
List<Product> findBySkuBetween(String startSku, String endSku);
1025+
1026+
// Combine with other conditions
1027+
List<Product> findBySkuGreaterThanAndPriceGreaterThan(String sku, Double price);
1028+
1029+
// Order by lexicographic field
1030+
List<Product> findByNameContainingOrderBySkuAsc(String keyword);
1031+
}
1032+
----
1033+
1034+
These repository methods leverage the lexicographic index to perform efficient string range queries.
1035+
1036+
== Step 23: Product Service with Entity Streams
1037+
1038+
[source,java]
1039+
----
1040+
package com.example.demo.service;
1041+
1042+
import java.util.List;
1043+
1044+
import com.example.demo.domain.Product;
1045+
1046+
public interface ProductService {
1047+
List<Product> findProductsInSkuRange(String startSku, String endSku);
1048+
List<Product> findNewerVersions(String version);
1049+
}
1050+
----
1051+
1052+
[source,java]
1053+
----
1054+
package com.example.demo.service.impl;
1055+
1056+
import java.util.List;
1057+
import java.util.stream.Collectors;
1058+
1059+
import org.springframework.stereotype.Service;
1060+
1061+
import com.example.demo.domain.Product;
1062+
import com.example.demo.domain.Product$;
1063+
import com.example.demo.service.ProductService;
1064+
import com.redis.om.spring.search.stream.EntityStream;
1065+
1066+
import lombok.RequiredArgsConstructor;
1067+
1068+
@Service
1069+
@RequiredArgsConstructor
1070+
public class ProductServiceImpl implements ProductService {
1071+
1072+
private final EntityStream entityStream;
1073+
1074+
@Override
1075+
public List<Product> findProductsInSkuRange(String startSku, String endSku) {
1076+
return entityStream
1077+
.of(Product.class)
1078+
.filter(Product$.SKU.between(startSku, endSku))
1079+
.sorted(Product$.SKU) // Sort by SKU alphabetically
1080+
.collect(Collectors.toList());
1081+
}
1082+
1083+
@Override
1084+
public List<Product> findNewerVersions(String version) {
1085+
return entityStream
1086+
.of(Product.class)
1087+
.filter(Product$.VERSION.gt(version))
1088+
.sorted(Product$.VERSION.desc())
1089+
.collect(Collectors.toList());
1090+
}
1091+
}
1092+
----
1093+
1094+
The Entity Streams API provides type-safe methods for lexicographic comparisons:
1095+
- `.gt(value)` - Greater than
1096+
- `.lt(value)` - Less than
1097+
- `.between(start, end)` - Between two values (inclusive)
1098+
1099+
== Step 24: Update Application to Load Product Data
1100+
1101+
[source,java]
1102+
----
1103+
// Add to the Application class imports
1104+
import com.example.demo.domain.Product;
1105+
import com.example.demo.repositories.ProductRepository;
1106+
1107+
// Add to the Application class fields
1108+
@Autowired
1109+
ProductRepository productRepo;
1110+
1111+
// Add to the loadTestData() method
1112+
// Clear and load Product data
1113+
productRepo.deleteAll();
1114+
1115+
// Create products with sequential SKUs
1116+
productRepo.save(Product.of("Laptop Pro", "PROD-1001", 1299.99));
1117+
productRepo.save(Product.of("Wireless Mouse", "PROD-1002", 29.99));
1118+
productRepo.save(Product.of("USB-C Hub", "PROD-1003", 49.99));
1119+
productRepo.save(Product.of("Monitor 4K", "PROD-2001", 599.99));
1120+
productRepo.save(Product.of("Keyboard Mechanical", "PROD-2002", 149.99));
1121+
1122+
// Products with versions
1123+
Product software1 = Product.of("Analytics Suite", "SOFT-001", 499.99);
1124+
software1.setVersion("2.1.0");
1125+
productRepo.save(software1);
1126+
1127+
Product software2 = Product.of("Database Manager", "SOFT-002", 799.99);
1128+
software2.setVersion("3.0.1");
1129+
productRepo.save(software2);
1130+
1131+
Product software3 = Product.of("Cloud Platform", "SOFT-003", 999.99);
1132+
software3.setVersion("1.9.5");
1133+
productRepo.save(software3);
1134+
----
1135+
1136+
== Step 25: Create the Product Controller
1137+
1138+
[source,java]
1139+
----
1140+
package com.example.demo.controllers;
1141+
1142+
import java.util.List;
1143+
1144+
import org.springframework.web.bind.annotation.*;
1145+
1146+
import com.example.demo.domain.Product;
1147+
import com.example.demo.repositories.ProductRepository;
1148+
import com.example.demo.service.ProductService;
1149+
1150+
import lombok.RequiredArgsConstructor;
1151+
1152+
@RestController
1153+
@RequestMapping("/api/products")
1154+
@RequiredArgsConstructor
1155+
public class ProductController {
1156+
1157+
private final ProductRepository repository;
1158+
private final ProductService productService;
1159+
1160+
@GetMapping
1161+
public Iterable<Product> getAllProducts() {
1162+
return repository.findAll();
1163+
}
1164+
1165+
@GetMapping("sku/gt/{sku}")
1166+
public List<Product> bySkuGreaterThan(@PathVariable("sku") String sku) {
1167+
return repository.findBySkuGreaterThan(sku);
1168+
}
1169+
1170+
@GetMapping("sku/range/{start}/{end}")
1171+
public List<Product> bySkuRange(
1172+
@PathVariable("start") String start,
1173+
@PathVariable("end") String end) {
1174+
return productService.findProductsInSkuRange(start, end);
1175+
}
1176+
1177+
@GetMapping("version/newer/{version}")
1178+
public List<Product> newerVersions(@PathVariable("version") String version) {
1179+
return productService.findNewerVersions(version);
1180+
}
1181+
}
1182+
----
1183+
1184+
== Testing Lexicographic Queries
1185+
1186+
After adding the Product functionality, you can test the lexicographic string comparisons:
1187+
1188+
Find products with SKU greater than a value:
1189+
[source,bash]
1190+
----
1191+
curl http://localhost:8080/api/products/sku/gt/PROD-1002
1192+
# Returns products with SKUs: PROD-1003, PROD-2001, PROD-2002, SOFT-001, etc.
1193+
----
1194+
1195+
Find products in a SKU range:
1196+
[source,bash]
1197+
----
1198+
curl http://localhost:8080/api/products/sku/range/PROD-1001/PROD-2000
1199+
# Returns products with SKUs between PROD-1001 and PROD-2000
1200+
----
1201+
1202+
Find software with version newer than 2.0.0:
1203+
[source,bash]
1204+
----
1205+
curl http://localhost:8080/api/products/version/newer/2.0.0
1206+
# Returns software with versions > 2.0.0 (e.g., 2.1.0, 3.0.1)
1207+
----
1208+
1209+
The lexicographic feature is particularly useful for:
1210+
- **Product SKUs**: Finding products in specific code ranges
1211+
- **Sequential IDs**: Querying entities with IDs in a certain range
1212+
- **Version strings**: Comparing semantic versions
1213+
- **Alphabetical filtering**: Finding names within alphabetical ranges
1214+
9641215
== Testing the REST API
9651216

9661217
After starting your application with `./mvnw spring-boot:run`, you can test the various endpoints:

0 commit comments

Comments
 (0)