springspring-bootredisredis-om-spring

Spring Data Redis OM: How to fetch selective fields? Instead of entire JSON


I’m using Spring Data Redis OM with Kotlin and want to fetch only specific fields of a document instead of the entire object. I have a Flight document and a repository set up, and I’m trying to use projections or partial field selection, but it doesn’t seem to work as expected.

Despite Redis OM generating the correct FT.SEARCH query under the hood, the repository call in Kotlin returns null in Postman, even though the data exists in Redis.

I want to know: Does Redis OM Spring support projections (i.e., fetching selected fields) in Kotlin? I think this issue has been resolved; now it supports 380.

libs.versions.toml -

[versions]

spring = "3.5.4"
kotlin = "2.2.10"

ktor = "3.2.0"
exposed = "1.0.0-beta-5"

serialization = "1.9.0"
dateTime = "0.7.1"

coroutines = "1.10.2"

[libraries]

#kotlin
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }

# Ktor Client
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-client-json = { module = "io.ktor:ktor-client-json", version.ref = "ktor" }
ktor-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }

# Coroutines
coroutines-reactor = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactor", version.ref = "coroutines" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }

# kotlinX
kotlinX-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
kotlinX-dateTime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "dateTime" }

# Database - Exposed ORM
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
exposed-r2dbc = { module = "org.jetbrains.exposed:exposed-r2dbc", version.ref = "exposed" }

exposed-kotlin-datetime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" }

# Database drivers and connection pool
postgresql-r2dbc = { module = "org.postgresql:r2dbc-postgresql", version = "1.0.7.RELEASE" }

# Spring Core
spring-webflux = { module = "org.springframework.boot:spring-boot-starter-webflux", version.ref = "spring" }
spring-r2dbc = { module = "org.springframework.boot:spring-boot-starter-data-r2dbc", version.ref = "spring" }


# Spring Security (Reactive)
spring-security = { module = "org.springframework.boot:spring-boot-starter-security", version.ref = "spring" }
spring-oauth2-client = { module = "org.springframework.boot:spring-boot-starter-oauth2-client", version.ref = "spring" }

[bundles]

spring-core = ["spring-webflux", "spring-r2dbc"]

spring-security-bundle = ["spring-security", "spring-oauth2-client"]

exposed-database = ["exposed-core", "exposed-kotlin-datetime", "exposed-r2dbc"]

ktor-client = [
    "ktor-client-core",
    "ktor-client-cio",
    "ktor-client-content-negotiation",
    "ktor-client-json",
    "ktor-json"
]

kotlinX = ["kotlinX-serialization", "kotlinX-dateTime", ]

coroutines = ["coroutines-reactor", "coroutines-core"]

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

spring-boot = { id = "org.springframework.boot", version.ref = "spring" }
spring-dependency-management = { id = "io.spring.dependency-management", version = "1.1.7" }

Dependency -

    implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.19.2")
        implementation("com.redis.om:redis-om-spring:1.0.0")
 implementation(libs.postgresql.r2dbc)
    implementation(libs.kotlin.reflect)
 implementation(libs.bundles.ktor.client)
    implementation(libs.bundles.spring.core)
    implementation(libs.bundles.exposed.database)
    implementation(libs.bundles.coroutines)
    implementation(libs.bundles.kotlinX)

    implementation(libs.bundles.spring.security.bundle)


@Configuration
class JacksonConfig {

    @Bean
    fun objectMapper(): ObjectMapper {
        return jacksonObjectMapper()
            .registerModule(KotlinModule.Builder().build())
    }
}

I have a Flight document like this:

@Document("Flight", indexName = "FlightIdx")
data class Flight(
    @Id
    @Indexed
    var flightNumber: String,   // unique ID

    @Searchable
    var name: String,

    var departure: Int,
    var arrival: Int,

    @Indexed
    var type: Int,   // e.g. 1 = Domestic, 2 = International, 3 = Cargo

    var eta:Int = 0,
    var etd:Int = 0,
    var src:String = "ABC",
    var dest:String = "XYZ"
)

And my repository, tried function data class, same redis query is being generated :

    interface Numbers {
        val flightNumber: String
    }

    // tried function 
   interface Numbers {
         fun getFlightNumber(): String
    }

    // tried data class
   data class Numbers(val flightNumber: String)
    
    interface FlightRepo : RedisDocumentRepository<Flight, String> {
        fun findByFlightNumber(flightNumber: String): Numbers?
    }

Data saved -

    val flights = listOf(
            Flight("AI101", "Air India Express", 600, 900, 1, eta = 905, etd = 555, src = "DEL", dest = "BOM"),
            Flight("BA202", "British Airways Dreamliner", 1400, 2200, 2, eta = 2215, etd = 1350, src = "LHR", dest = "DEL"),
            Flight("EK303", "Emirates SkyCargo", 230, 530, 3, eta = 545, etd = 215, src = "DXB", dest = "BOM"),
            Flight("SQ404", "Singapore Airlines", 1130, 1930, 2, eta = 1940, etd = 1120, src = "SIN", dest = "DEL"),
            Flight("AI505", "Air India Connect", 700, 930, 1, eta = 940, etd = 655, src = "BLR", dest = "HYD"),
            Flight("CX606", "Cathay Pacific Cargo", 300, 845, 3, eta = 900, etd = 245, src = "HKG", dest = "DEL"),
            Flight("UA707", "United Airlines Polaris", 2230, 830, 2, eta = 845, etd = 2215, src = "EWR", dest = "DEL"),
            Flight("AI808", "Air India Shuttle", 1630, 1830, 1, eta = 1840, etd = 1625, src = "DEL", dest = "JAI"),
            Flight("QR909", "Qatar Airways Cargo", 130, 630, 3, eta = 645, etd = 120, src = "DOH", dest = "BOM"),
            Flight("LH010", "Lufthansa CityLine", 930, 1630, 2, eta = 1645, etd = 920, src = "FRA", dest = "DEL")
        )
flightRepo.saveAll(flights)

Calling in the controller -

 @GetMapping("/get")
    suspend fun getDetails()= flightRepo.findByFlightNumber("AI101")

Query generated by Redis profiler -

"FT.SEARCH" "FlightIdx" "@flightNumber:{AI101}" "LIMIT" "0" "10000" "RETURN" "3" "$.flightNumber" "AS" "flightNumber" "DIALECT" "2"

When put in Redis CLI -

> "FT.SEARCH" "FlightIdx" "@flightNumber:{AI101}" "LIMIT" "0" "10000" "RETURN" "3" "$.flightNumber" "AS" "flightNumber" "DIALECT" "2"
1) "1"
2) "Flight:AI101"
3) 1) "flightNumber"
   2) "AI101"

In Postman, it returns nothing. It should return AI101

Question

Is there a way in Redis OM Spring to fetch selective fields of documents that match a query?


Solution

  • I answered this in a different thread as well, but you found a weird bug that has been reported.

    In the meantime, a workaround is annotating fields with the Value annotation in your projections:

    You found a weird bug. I was able to reproduce it.

    interface Stat  {
        val number: String
    
        @Value("#{target.location}")
        val location: Point
    }