# Skeleton of Spring Boot + Redis OM + Redis Stack
The most important thing in the begining is [Redis OM](https://redis.io/docs/connect/clients/om-clients/) is still on Beta.
This project is a skeleton of Spring Boot + Redis OM + Redis stack.
The purpose of this project is to provide some demo code for the Redis OM and the Redis stack.
## Make your hands dirty
Follow the steps below to make a local copy of this project.
### Environment
- Java 17
- Spring boot 3.2.1
- Redis stack latest
- Docker desktop 4.26.1
- Redis OM 0.8.8
- Springdoc 2.0.2
### Installation
- You can just clone this [repo](https://github.com/mister33221/spring-boot-redis-om-redis-stack-example).
- Or build a new project by yourself. Follow the steps below.
1. Create a new project by using [Spring Initializr](https://start.spring.io/).
* project: Maven
* Language: Java
* Spring Boot: 3.2.x
* Project Metadata: Depends on your project
* Java: 17
* Packaging: Jar
* Dependencies: Spring Web, Lombok, devtools
2. Add else dependencies we need in pom.xml
```xml
<!--redis om-->
<dependency>
<groupId>com.redis.om</groupId>
<artifactId>redis-om-spring</artifactId>
<version>0.8.8</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.0.2</version>
</dependency>
```
3. Run Redis Stack by docker compose.
```yaml
version: '3'
services:
redis-stack:
image: redis/redis-stack:latest
container_name: redis-stack
ports:
- "6379:6379"
- "8001:8001"
volumes:
- /local-data/:/data
```
```sh
docker-compose up -d
```
4. Open a command line and use the following command to connect to Redis Stack as a monitor.
```sh
docker exec -it redis-stack redis-cli MONITOR
```
5. Add the following code to `application.properties`.
```properties
spring.redis.host=localhost
spring.redis.port=6379
```
6. Run Spring Boot application.
7. Use browser to access RedisInsight.
```url
http://localhost:8001
```
8. Use browser to access Swagger UI.
```url
http://localhost:8080/swagger-ui.html
```
9. Then the project is ready to go.
### Configuration
- Create a package named `config` under `com.redis.skeleton` and create a class named `SpringDocConfig` in it.
- Add the following code to the class.
```java
package com.redis.skeleton.config;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import org.springframework.context.annotation.Configuration;
@OpenAPIDefinition(
info = @Info(
title = "This is a Redis OM Skeleton practice project",
version = "1.0",
description = "版本: \n\n " +
"Java : 17\n\n" +
"spring boot : 3.2.1\n\n" +
"Radis OM : 0.8.8\n\n" +
"springdoc-openapi-core : 2.0.2\n\n")
)
@Configuration
public class SpringDocConfig {
}
```
- `@EnableRedisDocumentRepositories`: This only for Redis OM, scan the redis documents(model) and repository. Do not use `@EnableRedisRepositories` in the same time.
- `com.redis.skeleton`: Is where the redis documents(model) and repository located.
```java
package com.redis.skeleton;
import com.redis.om.spring.annotations.EnableRedisDocumentRepositories;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
// This annotation will specify the package to scan for redis-om documents (including the repositories)
@EnableRedisDocumentRepositories(basePackages = "com.redis.skeleton")
@SpringBootApplication
public class SkeletonApplication {
public static void main(String[] args) {
SpringApplication.run(SkeletonApplication.class, args);
}
}
```
### Start to code
- Create a package named `model` under `com.redis.skeleton` and create a class named `Person` in it.
- `@Id`: An autogenerated String using ULIDs
- `@Document`: The class is annotated with `@Document` to marks the object as a Redis entity to be persisted as JSON document by appropiate type of repository.
- `@Indexed`: The field is annotated with `@Indexed` to marks the field as an indexable field.
In this case, for the Person class an index named com.redis.om.skeleton.models.PersonIdx will be created on application startup.
In the index schema, a search field will be added for each @Indexed annotated property.
RediSearch, the underlying search engine powering searches, supports Text (full-text searches), Tag (exact-match searches), Numeric (range queries), Geo (geographic range queries), and Vector (vector queries) fields.
- `@Searchable`: Fields marked as @Searchable such as personalStatement in Person are reflected as Full-Text search fields in the search index schema.
```java
package com.redis.skeleton.model;
import java.util.Set;
import com.redis.skeleton.model.Address;
import org.springframework.data.annotation.Id;
import org.springframework.data.geo.Point;
import com.redis.om.spring.annotations.Document;
import com.redis.om.spring.annotations.Indexed;
import com.redis.om.spring.annotations.Searchable;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor(staticName = "of")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Data
@Document
public class Person {
// Id Field, also indexed
@Id
@Indexed
private String id;
// Indexed for exact text matching
@Indexed
@NonNull
private String firstName;
@Indexed
@NonNull
private String lastName;
//Indexed for numeric matches
@Indexed
@NonNull
private Integer age;
//Indexed for Full Text matches
@Searchable
@NonNull
private String personalStatement;
//Indexed for Geo Filtering
@Indexed
@NonNull
private Point homeLoc;
// Nest indexed object
@Indexed
@NonNull
private Address address;
@Indexed
@NonNull
private Set<String> skills;
}
```
- Create a class named `Address` in `com.redis.skeleton.model` package.
```java
package com.redis.skeleton.model;
import com.redis.om.spring.annotations.Indexed;
import com.redis.om.spring.annotations.Searchable;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
@Data
@RequiredArgsConstructor(staticName = "of")
public class Address {
@NonNull
@Indexed
private String houseNumber;
@NonNull
@Searchable(nostem = true)
private String street;
@NonNull
@Indexed
private String city;
@NonNull
@Indexed
private String state;
@NonNull
@Indexed
private String postalCode;
@NonNull
@Indexed
private String country;
}
```
- Create a package named `repository` under `com.redis.skeleton` and create a interface named `PersonRepository` in it.
- `RedisDocumentRepository`: The RedisDocumentRepository extends PagingAndSortingRepository which extends CrudRepository to provide additional methods to retrieve entities using the pagination and sorting.
```java
package com.redis.skeleton.repository;
import com.redis.om.spring.repository.RedisDocumentRepository;
import com.redis.skeleton.model.Person;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
import java.util.Set;
public interface PeopleRepository extends RedisDocumentRepository<Person, String>{
Iterable<Person> findByFirstNameAndLastName(String firstName, String lastName);
Iterable<Person> findByAgeBetween(int minAge, int maxAge);
// Use this method to find people who live within a certain distance of a given point
Iterable<Person> findByHomeLocNear(Point point, Distance distance);
Iterable<Person> searchByPersonalStatement(String text);
Iterable<Person> findByAddress_City(String city);
Iterable<Person> findBySkills(Set<String> skills);
}
```
- Create a package named `controller` under `com.redis.skeleton` and create a class named `PersonControllerV1` in it.
```java
package com.redis.skeleton.contoller;
import com.redis.skeleton.model.Address;
import com.redis.skeleton.model.Person;
import com.redis.skeleton.repository.PeopleRepository;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@RestController
@RequestMapping("/api/v1/people")
public class PeopleControllerV1 {
private final PeopleRepository repo;
public PeopleControllerV1(PeopleRepository repo) {
this.repo = repo;
}
@PostMapping("init-data")
@Operation(summary = "Initialize the data", tags = {"Create"})
void initData() {
repo.deleteAll();
String thorSays = "The Rabbit Is Correct, And Clearly The Smartest One Among You.";
String ironmanSays = "Doth mother know you weareth her drapes?";
String blackWidowSays = "Hey, fellas. Either one of you know where the Smithsonian is? I’m here to pick up a fossil.";
String wandaMaximoffSays = "You Guys Know I Can Move Things With My Mind, Right?";
String gamoraSays = "I Am Going To Die Surrounded By The Biggest Idiots In The Galaxy.";
String nickFurySays = "Sir, I’m Gonna Have To Ask You To Exit The Donut";
// Serendipity, 248 Seven Mile Beach Rd, Broken Head NSW 2481, Australia
Address thorsAddress = Address.of("248", "Seven Mile Beach Rd", "Broken Head", "NSW", "2481", "Australia");
// 11 Commerce Dr, Riverhead, NY 11901
Address ironmansAddress = Address.of("11", "Commerce Dr", "Riverhead", "NY", "11901", "US");
// 605 W 48th St, New York, NY 10019
Address blackWidowAddress = Address.of("605", "48th St", "New York", "NY", "10019", "US");
// 20 W 34th St, New York, NY 10001
Address wandaMaximoffsAddress = Address.of("20", "W 34th St", "New York", "NY", "10001", "US");
// 107 S Beverly Glen Blvd, Los Angeles, CA 90024
Address gamorasAddress = Address.of("107", "S Beverly Glen Blvd", "Los Angeles", "CA", "90024", "US");
// 11461 Sunset Blvd, Los Angeles, CA 90049
Address nickFuryAddress = Address.of("11461", "Sunset Blvd", "Los Angeles", "CA", "90049", "US");
Person thor = Person.of("Chris", "Hemsworth", 38, thorSays, new Point(153.616667, -28.716667), thorsAddress, Set.of("hammer", "biceps", "hair", "heart"));
Person ironman = Person.of("Robert", "Downey", 56, ironmanSays, new Point(40.9190747, -72.5371874), ironmansAddress, Set.of("tech", "money", "one-liners", "intelligence", "resources"));
Person blackWidow = Person.of("Scarlett", "Johansson", 37, blackWidowSays, new Point(40.7215259, -74.0129994), blackWidowAddress, Set.of("deception", "martial_arts"));
Person wandaMaximoff = Person.of("Elizabeth", "Olsen", 32, wandaMaximoffSays, new Point(40.6976701, -74.2598641), wandaMaximoffsAddress, Set.of("magic", "loyalty"));
Person gamora = Person.of("Zoe", "Saldana", 43, gamoraSays, new Point(-118.399968, 34.073087), gamorasAddress, Set.of("skills", "martial_arts"));
Person nickFury = Person.of("Samuel L.", "Jackson", 73, nickFurySays, new Point(-118.4345534, 34.082615), nickFuryAddress, Set.of("planning", "deception", "resources"));
repo.saveAll(List.of(thor, ironman, blackWidow, wandaMaximoff, gamora, nickFury));
}
@GetMapping("all")
@Operation(summary = "Query all the data", tags = {"Read"})
Iterable<Person> all() {
return repo.findAll();
}
@GetMapping("{id}")
@Operation(summary = "Query the data by id", tags = {"Read"})
Optional<Person> byId(@PathVariable String id) {
return repo.findById(id);
}
@GetMapping("age_between")
@Operation(summary = "Query the data by age between", tags = {"Read"})
Iterable<Person> byAgeBetween(
@Parameter(description = "min age", example = "30")
@RequestParam("min") int min,
@Parameter(description = "max age", example = "40")
@RequestParam("max") int max) {
return repo.findByAgeBetween(min, max);
}
@GetMapping("name")
@Operation(summary = "Query the data by first name and last name", tags = {"Read"})
Iterable<Person> byFirstNameAndLastName(
@Parameter(description = "first name", example = "Chris")
@RequestParam("first") String firstName,
@Parameter(description = "last name", example = "Hemsworth")
@RequestParam("last") String lastName) {
return repo.findByFirstNameAndLastName(firstName, lastName);
}
// Not working. Don't know why.
@GetMapping("homeloc")
@Operation(summary = "Query the data by home location ( not working, Redis Inc. has announced the end-of-life of RedisGraph. Check here https://redis.com/blog/redisgraph-eol/ ", tags = {"Read"})
Iterable<Person> byHomeLoc(
@Parameter(description = "latitude", example = "-28.716667")
@RequestParam("lat") double lat,
@Parameter(description = "longitude", example = "153.616667")
@RequestParam("lon") double lon,
@Parameter(description = "distance", example = "100")
@RequestParam("d") double distance) {
return repo.findByHomeLocNear(new Point(lon, lat), new Distance(distance, Metrics.MILES));
}
@GetMapping("statement")
@Operation(summary = "Query the data by personal statement", tags = {"Read"})
Iterable<Person> byPersonalStatement(
@Parameter(description = "personal statement", example = "The Rabbit Is Correct, And Clearly The Smartest One Among You.")
@RequestParam("q") String q) {
return repo.searchByPersonalStatement(q);
}
@GetMapping("city")
@Operation(summary = "Query the data by city", tags = {"Read"})
Iterable<Person> byCity(
@Parameter(description = "city", example = "New York")
@RequestParam("city") String city) {
return repo.findByAddress_City(city);
}
@GetMapping("skills")
@Operation(summary = "Query the data by skills", tags = {"Read"})
Iterable<Person> byAnySkills(
@Parameter(description = "skills", example = "hammer")
@RequestParam("skills") Set<String> skills) {
return repo.findBySkills(skills);
}
@DeleteMapping("all")
@Operation(summary = "Delete all the data", tags = {"Delete"})
void deleteAll() {
repo.deleteAll();
}
@DeleteMapping("{id}")
@Operation(summary = "Delete the data by id", tags = {"Delete"})
void deleteById(@PathVariable String id) {
repo.deleteById(id);
}
@PostMapping("{id}/{age}")
@Operation(summary = "Update the age by id", tags = {"Update"})
Person updateById(@PathVariable String id, @PathVariable int age) {
Optional<Person> person = repo.findById(id);
if (person.isPresent()) {
Person p = person.get();
p.setAge(age);
return repo.save(p);
}
return null;
}
}
```
- Then everything is ready to go. You can use the Swagger UI to test the API.
- In the beginning, you have to run the `init-data` API to create some data in Redis Stack.
- [Here](https://github.com/mister33221/spring-boot-redis-om-redis-stack-example.git) is my code on github if you interesting.
## NOTE
1. If you follow the official guide to use Redis OM, you will find that the official guide is not correct.
- [Redis OM](https://redis.io/docs/connect/clients/om-clients/stack-spring/#nested-field-search-features)
- findAll() not exists
- findById(id) not exists
- deleteAll() not exists
2. If your redis om dependency version is 0.8.6, then you have to add the jadis like below
```xml
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.1</version>
</dependency>
```
- If your redis om dependency version is 0.8.8, like our setting in pom.xml, then it's ok. It's include the jadis dependency.
3. Your maven should specity the compiler target as java17
```
<properties>
<!--java: Supported source version 'RELEASE_17' from annotation processor 'com.redis.om.spring.metamodel.MetamodelGenerator' less than -source '21'-->
<java.version>17</java.version>
<maven.compiler.target>17</maven.compiler.target>
</properties>
```
4. When you run your application, we can inspect the console, redis om will scan the @Document and @Indexed to create the index for your json model. Open redis cli and use command below to check indexes
```
docker exec -it redis-stack redis-cli
ft._list
```
## Reference
- [Redis doc](https://redis.io/doc)
- [Redis OM](https://redis.io/docs/connect/clients/om-clients/stack-spring/#nested-field-search-features)
- [Redisdeveloper](https://developer.redis.com/develop/java/spring/redis-om/redis-om-spring-json/)
- [Redis commands](https://redis.io/commands)
- [MVNrepository](https://mvnrepository.com/artifact/com.redis.om/redis-om-spring/0.8.8)
- [redis-om-spring on github](https://github.com/redis/redis-om-spring)