From 029f5d34dd3931a958b90d725ac277117f3efc38 Mon Sep 17 00:00:00 2001 From: username1103 Date: Mon, 2 Oct 2023 20:25:59 +0900 Subject: [PATCH 1/8] feat: add near search operator --- .../search/NearSearchOperatorDsl.kt | 137 ++++++++++ .../core/aggregation/search/SearchOperator.kt | 10 + .../aggregation/search/SearchOperatorDsl.kt | 4 + .../search/NearSearchOperatorDslTest.kt | 237 ++++++++++++++++++ 4 files changed, 388 insertions(+) create mode 100644 core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDsl.kt create mode 100644 core/src/test/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDslTest.kt diff --git a/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDsl.kt b/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDsl.kt new file mode 100644 index 00000000..1c87a3e9 --- /dev/null +++ b/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDsl.kt @@ -0,0 +1,137 @@ +package com.github.inflab.spring.data.mongodb.core.aggregation.search + +import com.github.inflab.spring.data.mongodb.core.annotation.AggregationMarker +import com.github.inflab.spring.data.mongodb.core.extension.firstOrAll +import com.github.inflab.spring.data.mongodb.core.extension.toDotPath +import org.bson.Document +import org.springframework.data.mongodb.core.geo.GeoJson +import org.springframework.data.mongodb.core.geo.GeoJsonPoint +import java.time.temporal.Temporal +import kotlin.reflect.KProperty + +/** + * A Kotlin DSL to configure near search operator using idiomatic Kotlin code. + * + * @author username1103 + * @since 1.0 + * @see near + */ +@AggregationMarker +class NearSearchOperatorDsl { + private val document = Document() + + /** + * Indexed field or fields to search. + * See Path Construction. + * + * @param path The indexed field or fields to search. + * @see Path Construction + */ + fun path(vararg path: String) { + document["path"] = path.toList().firstOrAll() + } + + /** + * Indexed number type field or fields to search. + * See Path Construction. + * + * @param path The indexed field or fields to search. + * @see Path Construction + */ + @JvmName("pathNumber") + fun path(vararg path: KProperty) { + document["path"] = path.map { it.toDotPath() }.firstOrAll() + } + + /** + * Indexed date type field or fields to search. + * See Path Construction. + * + * @param path The indexed field or fields to search. + * @see Path Construction + */ + @JvmName("pathDate") + fun path(vararg path: KProperty) { + document["path"] = path.map { it.toDotPath() }.firstOrAll() + } + + /** + * Indexed geo type field or fields to search. + * See Path Construction for more information. + * + * @param path The indexed field or fields to search. + * @see Path Construction + */ + @JvmName("pathPoint") + fun path(vararg path: KProperty) { + document["path"] = path.map { it.toDotPath() }.firstOrAll() + } + + /** + * Origin to query for Number field. + * This is the origin from which the proximity of the results is measured. + * + * For number fields, the value must be of BSON int32, int64, or double data types. + */ + fun origin(origin: Number) { + document["origin"] = origin + } + + /** + * Origin to query for Date field. + * This is the origin from which the proximity of the results is measured. + * + * For date fields, the value must be an ISODate formatted date. + * @see ISODate + */ + fun origin(origin: Temporal) { + document["origin"] = origin + } + + /** + * Origin to query for Geo field. + * This is the origin from which the proximity of the results is measured. + * + * For geo fields. the value must be a GeoJSON point. + * @see GeoJson Point + */ + fun origin(origin: GeoJsonPoint) { + document["origin"] = Document("type", "Point").append("coordinates", origin.coordinates) + } + + /** + * Value to use to calculate scores of Atlas Search result documents. Score is calculated using the following formula: + * pivot + * score = ------------------ + * pivot + distance + * + * where distance is the difference between origin and the indexed field value. + * + * Results have a score equal to 1/2 (or 0.5) when their indexed field value is pivot units away from origin. + * The value of pivot must be greater than (i.e. >) 0. + * + * If origin is a: + * - Number, pivot can be specified as an integer or floating point number. + * - Date, pivot must be specified in milliseconds and can be specified as a 32 or 64 bit integer. + * - GeoJSON point, pivot is measured in meters and must be specified as an integer or floating point number. + */ + fun pivot(pivot: Number) { + document["pivot"] = pivot + } + + /** + * The score assigned to matching search term results. Use one of the following options to modify the score: + * + * - boost: multiply the result score by the given number. + * - constant: replace the result score with the given number. + * - function: replace the result score using the given expression. + * + * @param scoreConfiguration The configuration block for [ScoreSearchOptionDsl] + * @see Modify the Score + */ + fun score(scoreConfiguration: ScoreSearchOptionDsl.() -> Unit) { + document["score"] = ScoreSearchOptionDsl().apply(scoreConfiguration).get() + } + + internal fun build() = Document("near", document) +} diff --git a/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/SearchOperator.kt b/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/SearchOperator.kt index 20f3ec8b..7232be08 100644 --- a/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/SearchOperator.kt +++ b/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/SearchOperator.kt @@ -125,4 +125,14 @@ interface SearchOperator { * @see queryString */ fun queryString(configuration: QueryStringSearchOperatorDsl.() -> Unit) + + /** + * Supports querying and scoring numeric, date, and GeoJSON point values. + * You can use the near operator to find results that are near a number or a date. + * The near operator scores the Atlas Search results by proximity to the number or date. + * + * @param configuration The Configuration block for the [NearSearchOperatorDsl]. + * @see near + */ + fun near(configuration: NearSearchOperatorDsl.() -> Unit) } diff --git a/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/SearchOperatorDsl.kt b/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/SearchOperatorDsl.kt index d022600d..99d7ec69 100644 --- a/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/SearchOperatorDsl.kt +++ b/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/SearchOperatorDsl.kt @@ -61,4 +61,8 @@ class SearchOperatorDsl : SearchOperator { override fun queryString(configuration: QueryStringSearchOperatorDsl.() -> Unit) { operators.add(QueryStringSearchOperatorDsl().apply(configuration).build()) } + + override fun near(configuration: NearSearchOperatorDsl.() -> Unit) { + operators.add(NearSearchOperatorDsl().apply(configuration).build()) + } } diff --git a/core/src/test/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDslTest.kt b/core/src/test/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDslTest.kt new file mode 100644 index 00000000..b1195db6 --- /dev/null +++ b/core/src/test/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDslTest.kt @@ -0,0 +1,237 @@ +package com.github.inflab.spring.data.mongodb.core.aggregation.search + +import com.github.inflab.spring.data.mongodb.core.mapping.rangeTo +import com.github.inflab.spring.data.mongodb.core.util.shouldBeJson +import io.kotest.core.spec.style.FreeSpec +import org.springframework.data.mongodb.core.geo.GeoJsonPoint +import java.time.LocalDate +import java.time.LocalDateTime + +internal class NearSearchOperatorDslTest : FreeSpec({ + fun near(block: NearSearchOperatorDsl.() -> Unit): NearSearchOperatorDsl = + NearSearchOperatorDsl().apply(block) + + "path" - { + "should build a path by strings" { + // given + val operator = near { + path("path1", "path2") + } + + // when + val result = operator.build() + + // then + result.shouldBeJson( + """ + { + "near": { + "path": [ + "path1", + "path2" + ] + } + } + """.trimIndent(), + ) + } + + "should build a path by multiple properties" { + // given + data class TestCollection(val path1: Number, val path2: Number) + + val operator = near { + path(TestCollection::path1, TestCollection::path2) + } + + // when + val result = operator.build() + + // then + result.shouldBeJson( + """ + { + "near": { + "path": [ + "path1", + "path2" + ] + } + } + """.trimIndent(), + ) + } + + "should build a path by nested property" { + // given + data class Child(val path: Number) + data class Parent(val child: Child) + + val operator = near { + path(Parent::child..Child::path) + } + + // when + val result = operator.build() + + // then + result.shouldBeJson( + """ + { + "near": { + "path": "child.path" + } + } + """.trimIndent(), + ) + } + } + + "origin" - { + "should build a origin by number" { + // given + val operator = near { + origin(123) + } + + // when + val result = operator.build() + + // then + result.shouldBeJson( + """ + { + "near": { + "origin": 123 + } + } + """.trimIndent(), + ) + } + + "should build a origin by LocalDateTime" { + // given + val operator = near { + origin(LocalDateTime.of(2023, 9, 18, 4, 10, 50, 1)) + } + + // when + val result = operator.build() + + // then + result.shouldBeJson( + """ + { + "near": { + "origin": { + "${'$'}date": "2023-09-18T04:10:50Z" + } + } + } + """.trimIndent(), + ) + } + + "should build a origin by LocalDate" { + // given + val operator = near { + origin(LocalDate.of(2023, 9, 18)) + } + + // when + val result = operator.build() + + // then + result.shouldBeJson( + """ + { + "near": { + "origin": { + "${'$'}date": "2023-09-18T00:00:00Z" + } + } + } + """.trimIndent(), + ) + } + + "should build a origin by GeoJson Point" { + // given + val operator = near { + origin(GeoJsonPoint(1.0, 2.0)) + } + + // when + val result = operator.build() + + // then + result.shouldBeJson( + """ + { + "near": { + "origin": { + "type": "Point", + "coordinates": [ + 1.0, + 2.0 + ] + } + } + } + """.trimIndent(), + ) + } + } + + "pivot" - { + "should build a pivot by Number" { + // given + val operator = near { + pivot(123) + } + + // when + val result = operator.build() + + // then + result.shouldBeJson( + """ + { + "near": { + "pivot": 123 + } + } + """.trimIndent(), + ) + } + } + + "score" - { + "should build a score" { + // given + val operator = near { + score { + boost(5.0) + } + } + + // when + val result = operator.build() + + // then + result.shouldBeJson( + """ + { + "near": { + "score": { + "boost": { + "value": 5.0 + } + } + } + } + """.trimIndent(), + ) + } + } +}) From 2a5b94cfcfa78ba36e03930d0cd9048ec17664f6 Mon Sep 17 00:00:00 2001 From: username1103 Date: Mon, 2 Oct 2023 22:28:11 +0900 Subject: [PATCH 2/8] feat: add near search operator example --- .../repository/atlas/NearSearchRepository.kt | 80 +++++++++++++++++++ .../atlas/NearSearchRepositoryTest.kt | 45 +++++++++++ 2 files changed, 125 insertions(+) create mode 100644 example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt create mode 100644 example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepositoryTest.kt diff --git a/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt new file mode 100644 index 00000000..dfaebd86 --- /dev/null +++ b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt @@ -0,0 +1,80 @@ +package com.github.inflab.example.spring.data.mongodb.repository.atlas + +import com.github.inflab.example.spring.data.mongodb.entity.airbnb.ListingsAndReviewsAddress +import com.github.inflab.spring.data.mongodb.core.aggregation.aggregation +import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.aggregation.AggregationResults +import org.springframework.stereotype.Repository +import java.time.LocalDateTime + +@Repository +class NearSearchRepository( + private val mongoTemplate: MongoTemplate, +) { + + data class RuntimeDto( + val title: String, + val runtime: Int, + val score: Double, + ) + + data class ReleasedDto( + val title: String, + val released: LocalDateTime, + val score: Double, + ) + + data class GeoDto( + val title: String, + val address: ListingsAndReviewsAddress, + val score: Double, + ) + + fun findByRuntime(): AggregationResults { + val aggregation = aggregation { + search { + index = "runtimes" + near { + path("runtime") + origin(279) + pivot(2) + } + } + + // TODO: add $limit stage + + project { + excludeId() + +"title" + +"runtime" + searchScore() + } + } + + return mongoTemplate.aggregate(aggregation, "movies", RuntimeDto::class.java) + } + + fun findByDate(): AggregationResults { + val aggregation = aggregation { + search { + index = "releaseddate" + near { + path("released") + origin(LocalDateTime.of(1915, 9, 13, 0, 0, 0)) + pivot(7776000000) + } + } + + // TODO: add $limit stage + + project { + excludeId() + +"title" + +"released" + searchScore() + } + } + + return mongoTemplate.aggregate(aggregation, "movies", ReleasedDto::class.java) + } +} diff --git a/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepositoryTest.kt b/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepositoryTest.kt new file mode 100644 index 00000000..6a0e6f07 --- /dev/null +++ b/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepositoryTest.kt @@ -0,0 +1,45 @@ +package com.github.inflab.example.spring.data.mongodb.repository.atlas + +import com.github.inflab.example.spring.data.mongodb.extension.AtlasTest +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe + +@AtlasTest(database = "sample_mflix") +class NearSearchRepositoryTest( + private val nearSearchRepository: NearSearchRepository, +) : FreeSpec({ + + "findByRuntime" { + // when + val result = nearSearchRepository.findByRuntime() + + // then + result.mappedResults.take(5).map { it.title } shouldBe listOf( + "The Kingdom", + "The Jinx: The Life and Deaths of Robert Durst", + "Shoah", + "Les Misèrables", + "Tokyo Trial", + ) + + result.mappedResults.take(5).map { it.runtime } shouldBe listOf( + 279, + 279, + 280, + 281, + 277, + ) + } + + "findByDate" { + // when + val result = nearSearchRepository.findByDate() + + // then + result.mappedResults.take(3).map { it.title } shouldBe listOf( + "Regeneration", + "The Cheat", + "Hell's Hinges", + ) + } +}) From bf0e62249f98e5528dc53ced5d376d2308eb3086 Mon Sep 17 00:00:00 2001 From: username1103 Date: Mon, 2 Oct 2023 22:57:47 +0900 Subject: [PATCH 3/8] feat: add NearSearchRepository example - add Database Annotation Near Example use multi database. So need multi mongo template. --- .../search/NearSearchOperatorDsl.kt | 1 - .../data/mongodb/annotation/Database.kt | 4 + .../entity/airbnb/ListingsAndReviews.kt | 3 + .../repository/atlas/NearSearchRepository.kt | 77 ++++++++++++++++++- .../AtlasTestConstructorExtension.kt | 27 +++++-- .../atlas/NearSearchRepositoryTest.kt | 31 ++++++++ 6 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/annotation/Database.kt diff --git a/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDsl.kt b/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDsl.kt index 1c87a3e9..9d9b31ef 100644 --- a/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDsl.kt +++ b/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDsl.kt @@ -4,7 +4,6 @@ import com.github.inflab.spring.data.mongodb.core.annotation.AggregationMarker import com.github.inflab.spring.data.mongodb.core.extension.firstOrAll import com.github.inflab.spring.data.mongodb.core.extension.toDotPath import org.bson.Document -import org.springframework.data.mongodb.core.geo.GeoJson import org.springframework.data.mongodb.core.geo.GeoJsonPoint import java.time.temporal.Temporal import kotlin.reflect.KProperty diff --git a/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/annotation/Database.kt b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/annotation/Database.kt new file mode 100644 index 00000000..9bc70556 --- /dev/null +++ b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/annotation/Database.kt @@ -0,0 +1,4 @@ +package com.github.inflab.example.spring.data.mongodb.annotation + +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class Database(val value: String) diff --git a/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/entity/airbnb/ListingsAndReviews.kt b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/entity/airbnb/ListingsAndReviews.kt index 9e5d3947..2eca7316 100644 --- a/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/entity/airbnb/ListingsAndReviews.kt +++ b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/entity/airbnb/ListingsAndReviews.kt @@ -2,11 +2,14 @@ package com.github.inflab.example.spring.data.mongodb.entity.airbnb import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document +import org.springframework.data.mongodb.core.mapping.Field @Document("listingsAndReviews") data class ListingsAndReviews( @Id val id: String, val name: String, + @Field("property_type") + val propertyType: String, val address: ListingsAndReviewsAddress, ) diff --git a/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt index dfaebd86..8fd3db28 100644 --- a/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt +++ b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt @@ -1,15 +1,21 @@ package com.github.inflab.example.spring.data.mongodb.repository.atlas +import com.github.inflab.example.spring.data.mongodb.annotation.Database +import com.github.inflab.example.spring.data.mongodb.entity.airbnb.ListingsAndReviews import com.github.inflab.example.spring.data.mongodb.entity.airbnb.ListingsAndReviewsAddress import com.github.inflab.spring.data.mongodb.core.aggregation.aggregation +import com.github.inflab.spring.data.mongodb.core.mapping.rangeTo import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.aggregation.AggregationResults +import org.springframework.data.mongodb.core.geo.GeoJsonPoint +import org.springframework.data.mongodb.core.mapping.Field import org.springframework.stereotype.Repository import java.time.LocalDateTime @Repository class NearSearchRepository( - private val mongoTemplate: MongoTemplate, + @Database("sample_mflix") private val mflixMongoTemplate: MongoTemplate, + @Database("sample_airbnb") private val airbnbMongoTamplate: MongoTemplate, ) { data class RuntimeDto( @@ -25,7 +31,14 @@ class NearSearchRepository( ) data class GeoDto( - val title: String, + val name: String, + val address: ListingsAndReviewsAddress, + val score: Double, + ) + + data class GeoPropertyTypeDto( + @Field("property_type") + val propertyType: String, val address: ListingsAndReviewsAddress, val score: Double, ) @@ -51,7 +64,7 @@ class NearSearchRepository( } } - return mongoTemplate.aggregate(aggregation, "movies", RuntimeDto::class.java) + return mflixMongoTemplate.aggregate(aggregation, "movies", RuntimeDto::class.java) } fun findByDate(): AggregationResults { @@ -75,6 +88,62 @@ class NearSearchRepository( } } - return mongoTemplate.aggregate(aggregation, "movies", ReleasedDto::class.java) + return mflixMongoTemplate.aggregate(aggregation, "movies", ReleasedDto::class.java) + } + + fun findByGeo(): AggregationResults { + val aggregation = aggregation { + search { + near { + path(ListingsAndReviews::address..ListingsAndReviewsAddress::location) + origin(GeoJsonPoint(-8.61308, 41.1413)) + pivot(1000) + } + } + + // TODO: add $limit stage + + project { + excludeId() + +ListingsAndReviews::name + +ListingsAndReviews::address + searchScore() + } + } + + return airbnbMongoTamplate.aggregate(aggregation, "listingsAndReviews", GeoDto::class.java) + } + + fun findByGeoWithCompound(): AggregationResults { + val aggregation = aggregation { + search { + compound { + must { + text { + query("Apartment") + path(ListingsAndReviews::propertyType) + } + } + should { + near { + origin(GeoJsonPoint(114.15027, 22.28158)) + pivot(1000) + path(ListingsAndReviews::address..ListingsAndReviewsAddress::location) + } + } + } + } + + // TODO: add $limit stage + + project { + excludeId() + +ListingsAndReviews::propertyType + +ListingsAndReviews::address + searchScore() + } + } + + return airbnbMongoTamplate.aggregate(aggregation, "listingsAndReviews", GeoPropertyTypeDto::class.java) } } diff --git a/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/extension/AtlasTestConstructorExtension.kt b/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/extension/AtlasTestConstructorExtension.kt index da030b9c..0ae57fe6 100644 --- a/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/extension/AtlasTestConstructorExtension.kt +++ b/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/extension/AtlasTestConstructorExtension.kt @@ -1,5 +1,6 @@ package com.github.inflab.example.spring.data.mongodb.extension +import com.github.inflab.example.spring.data.mongodb.annotation.Database import io.kotest.core.annotation.AutoScan import io.kotest.core.extensions.ConstructorExtension import io.kotest.core.spec.Spec @@ -14,9 +15,12 @@ import kotlin.reflect.full.primaryConstructor internal object AtlasTestConstructorExtension : ConstructorExtension { private val ATLAS_DOMAIN by lazy { val property = YamlPropertySourceLoader().load("env", ClassPathResource("application.yml")).first() - val username = property.getProperty("spring.data.mongodb.username") ?: throw IllegalStateException("spring.data.mongodb.username is not set") - val password = property.getProperty("spring.data.mongodb.password") ?: throw IllegalStateException("spring.data.mongodb.password is not set") - val host = property.getProperty("spring.data.mongodb.host") ?: throw IllegalStateException("spring.data.mongodb.host is not set") + val username = property.getProperty("spring.data.mongodb.username") + ?: throw IllegalStateException("spring.data.mongodb.username is not set") + val password = property.getProperty("spring.data.mongodb.password") + ?: throw IllegalStateException("spring.data.mongodb.password is not set") + val host = property.getProperty("spring.data.mongodb.host") + ?: throw IllegalStateException("spring.data.mongodb.host is not set") "$username:$password@$host" } @@ -29,8 +33,8 @@ internal object AtlasTestConstructorExtension : ConstructorExtension { return null } - val connectionString = "mongodb+srv://$ATLAS_DOMAIN/${atlasTest.database}?retryWrites=true&w=majority" - val mongoTemplate = MongoTemplate(SimpleMongoClientDatabaseFactory(connectionString)) + val connectionString = getConnectionString(atlasTest.database) + val mongoTemplate = getMongoTemplate(connectionString) val parameters = testConstructor.parameters.associateWith { parameter -> val parameterClass = parameter.type.classifier as KClass<*> @@ -47,6 +51,11 @@ internal object AtlasTestConstructorExtension : ConstructorExtension { "The parameter type of repository constructor must be MongoTemplate but ${repositoryParameterClass.qualifiedName} from ${parameterClass.qualifiedName}" } + val database = repositoryParameter.annotations.find { it is Database } as Database? + if (database != null) { + return@associateWith getMongoTemplate(getConnectionString(database.value)) + } + mongoTemplate } @@ -55,4 +64,12 @@ internal object AtlasTestConstructorExtension : ConstructorExtension { return testConstructor.callBy(parameters) } + + private fun getConnectionString(databaseName: String): String { + return "mongodb+srv://$ATLAS_DOMAIN/$databaseName?retryWrites=true&w=majority" + } + + private fun getMongoTemplate(connectionString: String): MongoTemplate { + return MongoTemplate(SimpleMongoClientDatabaseFactory(connectionString)) + } } diff --git a/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepositoryTest.kt b/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepositoryTest.kt index 6a0e6f07..793b9004 100644 --- a/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepositoryTest.kt +++ b/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepositoryTest.kt @@ -3,6 +3,7 @@ package com.github.inflab.example.spring.data.mongodb.repository.atlas import com.github.inflab.example.spring.data.mongodb.extension.AtlasTest import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.shouldBe +import org.springframework.data.mongodb.core.geo.GeoJsonPoint @AtlasTest(database = "sample_mflix") class NearSearchRepositoryTest( @@ -42,4 +43,34 @@ class NearSearchRepositoryTest( "Hell's Hinges", ) } + + "findByGeo" { + // when + val result = nearSearchRepository.findByGeo() + + // then + result.mappedResults.take(3).map { it.name } shouldBe listOf( + "Ribeira Charming Duplex", + "DB RIBEIRA - Grey Apartment", + "Ribeira 24 (4)", + ) + } + + "findByGeoWithCompound" { + // when + val result = nearSearchRepository.findByGeoWithCompound() + + // then + result.mappedResults.take(3).map { it.propertyType } shouldBe listOf( + "Apartment", + "Apartment", + "Apartment", + ) + + result.mappedResults.take(3).map { it.address.location } shouldBe listOf( + GeoJsonPoint(114.15027, 22.28158), + GeoJsonPoint(114.15082, 22.28161), + GeoJsonPoint(114.15007, 22.28215), + ) + } }) From 4e1ad4465c1e87d082a24774deb19b5ed881c5e3 Mon Sep 17 00:00:00 2001 From: username1103 Date: Tue, 3 Oct 2023 17:05:47 +0900 Subject: [PATCH 4/8] chore: fix comment in NearOperatorDsl --- .../mongodb/core/aggregation/search/NearSearchOperatorDsl.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDsl.kt b/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDsl.kt index 9d9b31ef..0a72a08e 100644 --- a/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDsl.kt +++ b/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDsl.kt @@ -100,9 +100,12 @@ class NearSearchOperatorDsl { /** * Value to use to calculate scores of Atlas Search result documents. Score is calculated using the following formula: + * + * ``` * pivot * score = ------------------ * pivot + distance + * ``` * * where distance is the difference between origin and the indexed field value. * @@ -113,6 +116,8 @@ class NearSearchOperatorDsl { * - Number, pivot can be specified as an integer or floating point number. * - Date, pivot must be specified in milliseconds and can be specified as a 32 or 64 bit integer. * - GeoJSON point, pivot is measured in meters and must be specified as an integer or floating point number. + * + * @param pivot The value to use to calculate scores of Atlas Search result documents. */ fun pivot(pivot: Number) { document["pivot"] = pivot From a97e0b184de0c5d2459233c2e102341a66fbf29b Mon Sep 17 00:00:00 2001 From: username1103 Date: Tue, 3 Oct 2023 17:06:22 +0900 Subject: [PATCH 5/8] fix: typo --- .../data/mongodb/repository/atlas/NearSearchRepository.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt index 8fd3db28..73dd80b8 100644 --- a/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt +++ b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt @@ -15,7 +15,7 @@ import java.time.LocalDateTime @Repository class NearSearchRepository( @Database("sample_mflix") private val mflixMongoTemplate: MongoTemplate, - @Database("sample_airbnb") private val airbnbMongoTamplate: MongoTemplate, + @Database("sample_airbnb") private val airbnbMongoTemplate: MongoTemplate, ) { data class RuntimeDto( @@ -111,7 +111,7 @@ class NearSearchRepository( } } - return airbnbMongoTamplate.aggregate(aggregation, "listingsAndReviews", GeoDto::class.java) + return airbnbMongoTemplate.aggregate(aggregation, "listingsAndReviews", GeoDto::class.java) } fun findByGeoWithCompound(): AggregationResults { @@ -144,6 +144,6 @@ class NearSearchRepository( } } - return airbnbMongoTamplate.aggregate(aggregation, "listingsAndReviews", GeoPropertyTypeDto::class.java) + return airbnbMongoTemplate.aggregate(aggregation, "listingsAndReviews", GeoPropertyTypeDto::class.java) } } From 6234cf76f9d425ec7dedadefd717993be5612fe3 Mon Sep 17 00:00:00 2001 From: username1103 Date: Tue, 3 Oct 2023 17:09:26 +0900 Subject: [PATCH 6/8] chore: add link in comment --- .../mongodb/repository/atlas/NearSearchRepository.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt index 73dd80b8..eb3cfeca 100644 --- a/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt +++ b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt @@ -43,6 +43,9 @@ class NearSearchRepository( val score: Double, ) + /** + * @see Number Example + */ fun findByRuntime(): AggregationResults { val aggregation = aggregation { search { @@ -67,6 +70,9 @@ class NearSearchRepository( return mflixMongoTemplate.aggregate(aggregation, "movies", RuntimeDto::class.java) } + /** + * @see Date Example + */ fun findByDate(): AggregationResults { val aggregation = aggregation { search { @@ -91,6 +97,9 @@ class NearSearchRepository( return mflixMongoTemplate.aggregate(aggregation, "movies", ReleasedDto::class.java) } + /** + * @see GeoJSON Point Basic Example + */ fun findByGeo(): AggregationResults { val aggregation = aggregation { search { @@ -114,6 +123,9 @@ class NearSearchRepository( return airbnbMongoTemplate.aggregate(aggregation, "listingsAndReviews", GeoDto::class.java) } + /** + * @see GeoJSON Point Compound Example + */ fun findByGeoWithCompound(): AggregationResults { val aggregation = aggregation { search { From aed90c99fe6b6813a5bc6d3e12513d26ed2edf52 Mon Sep 17 00:00:00 2001 From: username1103 Date: Tue, 3 Oct 2023 17:12:34 +0900 Subject: [PATCH 7/8] refactor: use aggregate extension function --- .../search/NearSearchOperatorDsl.kt | 4 ++-- .../repository/atlas/NearSearchRepository.kt | 22 ++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDsl.kt b/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDsl.kt index 0a72a08e..d9d99f66 100644 --- a/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDsl.kt +++ b/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/NearSearchOperatorDsl.kt @@ -50,7 +50,7 @@ class NearSearchOperatorDsl { * @see Path Construction */ @JvmName("pathDate") - fun path(vararg path: KProperty) { + fun path(vararg path: KProperty) { document["path"] = path.map { it.toDotPath() }.firstOrAll() } @@ -62,7 +62,7 @@ class NearSearchOperatorDsl { * @see Path Construction */ @JvmName("pathPoint") - fun path(vararg path: KProperty) { + fun path(vararg path: KProperty) { document["path"] = path.map { it.toDotPath() }.firstOrAll() } diff --git a/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt index eb3cfeca..47e17c21 100644 --- a/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt +++ b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/NearSearchRepository.kt @@ -3,9 +3,11 @@ package com.github.inflab.example.spring.data.mongodb.repository.atlas import com.github.inflab.example.spring.data.mongodb.annotation.Database import com.github.inflab.example.spring.data.mongodb.entity.airbnb.ListingsAndReviews import com.github.inflab.example.spring.data.mongodb.entity.airbnb.ListingsAndReviewsAddress +import com.github.inflab.example.spring.data.mongodb.entity.mflix.Movies import com.github.inflab.spring.data.mongodb.core.aggregation.aggregation import com.github.inflab.spring.data.mongodb.core.mapping.rangeTo import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.aggregate import org.springframework.data.mongodb.core.aggregation.AggregationResults import org.springframework.data.mongodb.core.geo.GeoJsonPoint import org.springframework.data.mongodb.core.mapping.Field @@ -51,7 +53,7 @@ class NearSearchRepository( search { index = "runtimes" near { - path("runtime") + path(Movies::runtime) origin(279) pivot(2) } @@ -61,13 +63,13 @@ class NearSearchRepository( project { excludeId() - +"title" - +"runtime" + +Movies::title + +Movies::runtime searchScore() } } - return mflixMongoTemplate.aggregate(aggregation, "movies", RuntimeDto::class.java) + return mflixMongoTemplate.aggregate(aggregation) } /** @@ -78,7 +80,7 @@ class NearSearchRepository( search { index = "releaseddate" near { - path("released") + path(Movies::released) origin(LocalDateTime.of(1915, 9, 13, 0, 0, 0)) pivot(7776000000) } @@ -88,13 +90,13 @@ class NearSearchRepository( project { excludeId() - +"title" - +"released" + +Movies::title + +Movies::released searchScore() } } - return mflixMongoTemplate.aggregate(aggregation, "movies", ReleasedDto::class.java) + return mflixMongoTemplate.aggregate(aggregation) } /** @@ -120,7 +122,7 @@ class NearSearchRepository( } } - return airbnbMongoTemplate.aggregate(aggregation, "listingsAndReviews", GeoDto::class.java) + return airbnbMongoTemplate.aggregate(aggregation) } /** @@ -156,6 +158,6 @@ class NearSearchRepository( } } - return airbnbMongoTemplate.aggregate(aggregation, "listingsAndReviews", GeoPropertyTypeDto::class.java) + return airbnbMongoTemplate.aggregate(aggregation) } } From 2f32aab76e3b02ae6f1d7e00b631e8ef71ee9059 Mon Sep 17 00:00:00 2001 From: username1103 Date: Tue, 3 Oct 2023 17:18:56 +0900 Subject: [PATCH 8/8] refactor: use findAnnotation extension function --- .../extension/AtlasTestConstructorExtension.kt | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/extension/AtlasTestConstructorExtension.kt b/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/extension/AtlasTestConstructorExtension.kt index 0ae57fe6..9af89755 100644 --- a/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/extension/AtlasTestConstructorExtension.kt +++ b/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/extension/AtlasTestConstructorExtension.kt @@ -8,7 +8,9 @@ import org.springframework.boot.env.YamlPropertySourceLoader import org.springframework.core.io.ClassPathResource import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory +import org.springframework.stereotype.Repository import kotlin.reflect.KClass +import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.primaryConstructor @AutoScan @@ -26,7 +28,7 @@ internal object AtlasTestConstructorExtension : ConstructorExtension { } override fun instantiate(clazz: KClass): Spec? { - val atlasTest = clazz.annotations.find { it is AtlasTest } as AtlasTest? ?: return null + val atlasTest = clazz.findAnnotation() ?: return null val testConstructor = clazz.primaryConstructor if (testConstructor == null || testConstructor.parameters.isEmpty()) { @@ -38,9 +40,11 @@ internal object AtlasTestConstructorExtension : ConstructorExtension { val parameters = testConstructor.parameters.associateWith { parameter -> val parameterClass = parameter.type.classifier as KClass<*> - check(parameterClass.annotations.any { it is org.springframework.stereotype.Repository }) { + + checkNotNull(parameterClass.findAnnotation()) { "The parameter type of constructor must be annotated with @Repository but ${parameterClass.qualifiedName}" } + val repositoryConstructor = checkNotNull(parameterClass.primaryConstructor) { "The parameter type of constructor must have primary constructor but ${parameterClass.qualifiedName}" } @@ -51,12 +55,9 @@ internal object AtlasTestConstructorExtension : ConstructorExtension { "The parameter type of repository constructor must be MongoTemplate but ${repositoryParameterClass.qualifiedName} from ${parameterClass.qualifiedName}" } - val database = repositoryParameter.annotations.find { it is Database } as Database? - if (database != null) { - return@associateWith getMongoTemplate(getConnectionString(database.value)) - } - - mongoTemplate + repositoryParameter.findAnnotation()?.let { + getMongoTemplate(getConnectionString(it.value)) + } ?: mongoTemplate } repositoryConstructor.callBy(repositoryParameters)