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 d2605927..e1f014a3 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 @@ -108,4 +108,12 @@ interface SearchOperator { * @see moreLikeThis */ fun moreLikeThis(configuration: MoreLikeThisSearchOperatorDsl.() -> Unit) + + /** + * Enables queries which use special characters in the search string that can match any character. + * + * @param configuration The configuration block for the [WildcardSearchOperatorDsl]. + * @see wildcard + */ + fun wildcard(configuration: WildcardSearchOperatorDsl.() -> 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 23d01167..5bf6a2e2 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 @@ -53,4 +53,8 @@ class SearchOperatorDsl : SearchOperator { override fun moreLikeThis(configuration: MoreLikeThisSearchOperatorDsl.() -> Unit) { operators.add(MoreLikeThisSearchOperatorDsl().apply(configuration).build()) } + + override fun wildcard(configuration: WildcardSearchOperatorDsl.() -> Unit) { + operators.add(WildcardSearchOperatorDsl().apply(configuration).build()) + } } diff --git a/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/WildcardSearchOperatorDsl.kt b/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/WildcardSearchOperatorDsl.kt new file mode 100644 index 00000000..a6c92945 --- /dev/null +++ b/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/WildcardSearchOperatorDsl.kt @@ -0,0 +1,102 @@ +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 kotlin.reflect.KProperty + +/** + * A Kotlin DSL to configure wildcard search operator using idiomatic Kotlin code. + * + * @author minwoo + * @since 1.0 + * @see wildcard + */ +@AggregationMarker +class WildcardSearchOperatorDsl { + private val document = Document() + + /** + * Must be set to true if the query is run against an analyzed field. + */ + var allowAnalyzedField: Boolean = false + set(value) { + document["allowAnalyzedField"] = value + field = value + } + + /** + * The string or strings to search for. + * + * @param query The string or strings to search for. + */ + fun query(vararg query: String) { + document["query"] = query.toList().firstOrAll() + } + + /** + * The indexed field or fields to search. + * You can also specify a wildcard path to search. + * See path construction for more information. + * + * @param path The indexed field or fields to search. + * @see Path Construction + */ + fun path(vararg path: String) { + document["path"] = path.toList().firstOrAll() + } + + /** + * The indexed field or fields to search. + * You can also specify a wildcard path to search. + * See path construction for more information. + * + * @param path The indexed field or fields to search. + * @see Path Construction + */ + fun path(vararg path: KProperty) { + document["path"] = path.map { it.toDotPath() }.firstOrAll() + } + + /** + * The indexed field or fields to search. + * You can also specify a wildcard path to search. + * See path construction for more information. + * + * @param path The indexed field or fields to search. + * @see Path Construction + */ + @JvmName("pathIterable") + fun path(vararg path: KProperty?>) { + document["path"] = path.map { it.toDotPath() }.firstOrAll() + } + + /** + * The indexed field or fields to search. + * You can also specify a wildcard path to search. + * See path construction for more information. + * + * @param configuration The configuration block for the [PathSearchOptionDsl]. + * @see Path Construction + */ + fun path(configuration: PathSearchOptionDsl.() -> Unit) { + document["path"] = PathSearchOptionDsl().apply(configuration).build() + } + + /** + * Score to assign to matching search results. + * You can modify the default score using the following options: + * + * - 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. + * + * @see Score the Documents in the Results + */ + fun score(scoreConfiguration: ScoreSearchOptionDsl.() -> Unit) { + document["score"] = ScoreSearchOptionDsl().apply(scoreConfiguration).get() + } + + internal fun build() = Document("wildcard", document) +} diff --git a/core/src/test/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/WildcardSearchOperatorDslTest.kt b/core/src/test/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/WildcardSearchOperatorDslTest.kt new file mode 100644 index 00000000..03ec1c5e --- /dev/null +++ b/core/src/test/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/WildcardSearchOperatorDslTest.kt @@ -0,0 +1,235 @@ +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 + +internal class WildcardSearchOperatorDslTest : FreeSpec({ + fun wildcard(block: WildcardSearchOperatorDsl.() -> Unit) = + WildcardSearchOperatorDsl().apply(block) + + "allowAnalyzedField" - { + "should build an option with given value" { + // given + val operator = wildcard { + allowAnalyzedField = true + } + + // when + val result = operator.build() + + // then + result.shouldBeJson( + """ + { + "wildcard": { + "allowAnalyzedField": true + } + } + """.trimIndent(), + ) + } + } + + "query" - { + "should build a query by string" { + // given + val operator = wildcard { + query("query") + } + + // when + val result = operator.build() + + // then + result.shouldBeJson( + """ + { + "wildcard": { + "query": "query" + } + } + """.trimIndent(), + ) + } + + "should build a query by multiple strings" { + // given + val operator = wildcard { + query("query1", "query2") + } + + // when + val result = operator.build() + + // then + result.shouldBeJson( + """ + { + "wildcard": { + "query": [ + "query1", + "query2" + ] + } + } + """.trimIndent(), + ) + } + } + + "path" - { + "should build a path by strings" { + // given + val operator = wildcard { + path("path1", "path2") + } + + // when + val result = operator.build() + + // then + result.shouldBeJson( + """ + { + "wildcard": { + "path": [ + "path1", + "path2" + ] + } + } + """.trimIndent(), + ) + } + + "should build a path by iterable property" { + // given + data class TestCollection(val path1: List) + val operator = wildcard { + path(TestCollection::path1) + } + + // when + val result = operator.build() + + // then + result.shouldBeJson( + """ + { + "wildcard": { + "path": "path1" + } + } + """.trimIndent(), + ) + } + + "should build a path by multiple properties" { + // given + data class TestCollection(val path1: String, val path2: String) + val operator = wildcard { + path(TestCollection::path1, TestCollection::path2) + } + + // when + val result = operator.build() + + // then + result.shouldBeJson( + """ + { + "wildcard": { + "path": [ + "path1", + "path2" + ] + } + } + """.trimIndent(), + ) + } + + "should build a path by nested property" { + // given + data class Child(val path: String) + data class Parent(val child: Child) + val operator = wildcard { + path(Parent::child..Child::path) + } + + // when + val result = operator.build() + + // then + result.shouldBeJson( + """ + { + "wildcard": { + "path": "child.path" + } + } + """.trimIndent(), + ) + } + + "should build a path by option block" { + // given + data class TestCollection(val path1: String, val path2: List) + val operator = wildcard { + path { + +"path0" + +TestCollection::path1 + +TestCollection::path2 + } + } + + // when + val result = operator.build() + + // then + result.shouldBeJson( + """ + { + "wildcard": { + "path": [ + "path0", + "path1", + "path2" + ] + } + } + """.trimIndent(), + ) + } + } + + "score" - { + "should build a score" { + // given + val operator = wildcard { + score { + constant(1.0) + } + } + + // when + val result = operator.build() + + // then + result.shouldBeJson( + """ + { + "wildcard": { + "score": { + "constant": { + "value": 1.0 + } + } + } + } + """.trimIndent(), + ) + } + } +}) diff --git a/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/WildcardSearchRepository.kt b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/WildcardSearchRepository.kt new file mode 100644 index 00000000..5210da71 --- /dev/null +++ b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/WildcardSearchRepository.kt @@ -0,0 +1,58 @@ +package com.github.inflab.example.spring.data.mongodb.repository.atlas + +import com.github.inflab.example.spring.data.mongodb.entity.mflix.Movies +import com.github.inflab.spring.data.mongodb.core.aggregation.aggregation +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.stereotype.Repository + +@Repository +class WildcardSearchRepository( + private val mongoTemplate: MongoTemplate, +) { + data class TitleDto(val title: String) + + /** + * @see Example + */ + fun findTitleWithGreenD(): AggregationResults { + val aggregation = aggregation { + search { + wildcard { + path(Movies::title) + query("Green D*") + } + } + project { + excludeId() + +Movies::title + } + } + + return mongoTemplate.aggregate(aggregation) + } + + /** + * @see Escape Character Example + */ + fun findTitleWithQuestionMark(): AggregationResults { + val aggregation = aggregation { + search { + wildcard { + path(Movies::title) + query("*\\?") + } + } + + // TODO: add $limit stage + + project { + excludeId() + +Movies::title + } + } + + return mongoTemplate.aggregate(aggregation) + } +} diff --git a/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/WildcardSearchRepositoryTest.kt b/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/WildcardSearchRepositoryTest.kt new file mode 100644 index 00000000..efac91bb --- /dev/null +++ b/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/repository/atlas/WildcardSearchRepositoryTest.kt @@ -0,0 +1,36 @@ +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") +internal class WildcardSearchRepositoryTest( + private val wildcardSearchRepository: WildcardSearchRepository, +) : FreeSpec({ + + "findTitleWithGreenD" { + // when + val result = wildcardSearchRepository.findTitleWithGreenD() + + // then + result.mappedResults.map { it.title } shouldBe listOf( + "Green Dolphin Street", + "Green Dragon", + ) + } + + "findTitleWithQuestionMark" { + // when + val result = wildcardSearchRepository.findTitleWithQuestionMark() + + // then + result.mappedResults.take(5).map { it.title } shouldBe listOf( + "Where Are My Children?", + "Who Killed Cock Robin?", + "What's Opera, Doc?", + "Will Success Spoil Rock Hunter?", + "Who Was That Lady?", + ) + } +})