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?",
+ )
+ }
+})