Skeleton of Spring Boot + Redis OM + Redis Stack
The most important thing in the begining is Redis OM 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.
- Or build a new project by yourself. Follow the steps below.
- Create a new project by using Spring Initializr.
- project: Maven
- Language: Java
- Spring Boot: 3.2.x
- Project Metadata: Depends on your project
- Java: 17
- Packaging: Jar
- Dependencies: Spring Web, Lombok, devtools
- Add else dependencies we need in pom.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>- Run Redis Stack by docker compose.
version: '3'
services:
redis-stack:
image: redis/redis-stack:latest
container_name: redis-stack
ports:
- "6379:6379"
- "8001:8001"
volumes:
- /local-data/:/datadocker-compose up -d- Open a command line and use the following command to connect to Redis Stack as a monitor.
docker exec -it redis-stack redis-cli MONITOR- Add the following code to
application.properties.
spring.redis.host=localhost
spring.redis.port=6379- Run Spring Boot application.
- Use browser to access RedisInsight.
http://localhost:8001- Use browser to access Swagger UI.
http://localhost:8080/swagger-ui.html- Then the project is ready to go.
Configuration
- Create a package named
configundercom.redis.skeletonand create a class namedSpringDocConfigin it. - Add the following code to the class.
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@EnableRedisRepositoriesin the same time.com.redis.skeleton: Is where the redis documents(model) and repository located.
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
modelundercom.redis.skeletonand create a class namedPersonin it.@Id: An autogenerated String using ULIDs@Document: The class is annotated with@Documentto marks the object as a Redis entity to be persisted as JSON document by appropiate type of repository.@Indexed: The field is annotated with@Indexedto 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.
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
Addressincom.redis.skeleton.modelpackage.
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
repositoryundercom.redis.skeletonand create a interface namedPersonRepositoryin it.RedisDocumentRepository: The RedisDocumentRepository extends PagingAndSortingRepository which extends CrudRepository to provide additional methods to retrieve entities using the pagination and sorting.
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
controllerundercom.redis.skeletonand create a class namedPersonControllerV1in it.
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-dataAPI to create some data in Redis Stack.
- In the beginning, you have to run the
Here is my code on github if you interesting.
NOTE
- If you follow the official guide to use Redis OM, you will find that the official guide is not correct.
- Redis OM
- findAll() not exists
- findById(id) not exists
- deleteAll() not exists
- If your redis om dependency version is 0.8.6, then you have to add the jadis like below
<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.
- 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>- 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