diff --git a/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/MoreLikeThisSearchOperatorDsl.kt b/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/MoreLikeThisSearchOperatorDsl.kt
new file mode 100644
index 00000000..626e81e7
--- /dev/null
+++ b/core/src/main/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/MoreLikeThisSearchOperatorDsl.kt
@@ -0,0 +1,44 @@
+package com.github.inflab.spring.data.mongodb.core.aggregation.search
+
+import com.github.inflab.spring.data.mongodb.core.annotation.AggregationMarker
+import org.bson.Document
+
+/**
+ * A Kotlin DSL to configure text moreLikeThis operator using idiomatic Kotlin code.
+ *
+ * @author Jake Son
+ * @since 1.0
+ * @see moreLikeThis
+ */
+@AggregationMarker
+class MoreLikeThisSearchOperatorDsl {
+ private val document = Document()
+
+ /**
+ * One or more BSON documents that Atlas Search uses to extract representative terms to query for.
+ *
+ * @param bson One BSON document or an array of documents.
+ */
+ fun like(vararg bson: Document) {
+ document["like"] = bson.toList()
+ }
+
+ /**
+ * Configures the 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.
+ *
+ * When you query values in arrays, Atlas Search doesn't alter the score of the matching results based on the number of values inside the array that matched the query.
+ * The score would be the same as a single match regardless of the number of matches inside an array.
+ *
+ * @see Score the Documents in the Results
+ */
+ fun score(scoreConfiguration: ScoreSearchOptionDsl.() -> Unit) {
+ document["score"] = ScoreSearchOptionDsl().apply(scoreConfiguration).get()
+ }
+
+ internal fun build() = Document("moreLikeThis", 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 13599d5a..d2605927 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
@@ -99,4 +99,13 @@ interface SearchOperator {
* @see geoWithin
*/
fun geoWithin(configuration: GeoWithinSearchOperatorDsl.() -> Unit)
+
+ /**
+ * Returns documents similar to input documents.
+ * The moreLikeThis operator allows you to build features for your applications that display similar or alternative results based on one or more given documents.
+ *
+ * @param configuration The configuration block for the [MoreLikeThisSearchOperatorDsl].
+ * @see moreLikeThis
+ */
+ fun moreLikeThis(configuration: MoreLikeThisSearchOperatorDsl.() -> 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 5a37c434..23d01167 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
@@ -49,4 +49,8 @@ class SearchOperatorDsl : SearchOperator {
override fun geoWithin(configuration: GeoWithinSearchOperatorDsl.() -> Unit) {
operators.add(GeoWithinSearchOperatorDsl().apply(configuration).build())
}
+
+ override fun moreLikeThis(configuration: MoreLikeThisSearchOperatorDsl.() -> Unit) {
+ operators.add(MoreLikeThisSearchOperatorDsl().apply(configuration).build())
+ }
}
diff --git a/core/src/test/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/MoreLikeThisSearchOperatorDslTest.kt b/core/src/test/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/MoreLikeThisSearchOperatorDslTest.kt
new file mode 100644
index 00000000..b9c7c739
--- /dev/null
+++ b/core/src/test/kotlin/com/github/inflab/spring/data/mongodb/core/aggregation/search/MoreLikeThisSearchOperatorDslTest.kt
@@ -0,0 +1,72 @@
+package com.github.inflab.spring.data.mongodb.core.aggregation.search
+
+import com.github.inflab.spring.data.mongodb.core.util.shouldBeJson
+import io.kotest.core.spec.style.FreeSpec
+import org.bson.Document
+
+internal class MoreLikeThisSearchOperatorDslTest : FreeSpec({
+ fun moreLikeThis(block: MoreLikeThisSearchOperatorDsl.() -> Unit) =
+ MoreLikeThisSearchOperatorDsl().apply(block)
+
+ "like" - {
+ "should build a like option" {
+ // given
+ val operator = moreLikeThis {
+ like(
+ Document("foo", "bar"),
+ Document("baz", "qux"),
+ )
+ }
+
+ // when
+ val result = operator.build()
+
+ // then
+ result.shouldBeJson(
+ """
+ {
+ "moreLikeThis": {
+ "like": [
+ {
+ "foo": "bar"
+ },
+ {
+ "baz": "qux"
+ }
+ ]
+ }
+ }
+ """.trimIndent(),
+ )
+ }
+ }
+
+ "score" - {
+ "should build a score option" {
+ // given
+ val operator = moreLikeThis {
+ score {
+ constant(2.0)
+ }
+ }
+
+ // when
+ val result = operator.build()
+
+ // then
+ result.shouldBeJson(
+ """
+ {
+ "moreLikeThis": {
+ "score": {
+ "constant": {
+ "value": 2.0
+ }
+ }
+ }
+ }
+ """.trimIndent(),
+ )
+ }
+ }
+})
diff --git a/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/entity/mflix/Movies.kt b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/entity/mflix/Movies.kt
index 14a1b2b0..0a468dbc 100644
--- a/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/entity/mflix/Movies.kt
+++ b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/entity/mflix/Movies.kt
@@ -9,14 +9,14 @@ data class Movies(
@Id
val id: String,
val plot: String,
- val genres: List,
+ val genres: List?,
val runtime: Int,
val cast: List,
val poster: String,
val title: String,
val fullplot: String,
val languages: List,
- val released: LocalDateTime,
+ val released: LocalDateTime?,
val directors: List,
val rated: String,
val lastupdated: LocalDateTime,
diff --git a/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/MoreLikeThisSearchRepository.kt b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/MoreLikeThisSearchRepository.kt
new file mode 100644
index 00000000..706d5ca6
--- /dev/null
+++ b/example/spring-data-mongodb/src/main/kotlin/com/github/inflab/example/spring/data/mongodb/repository/MoreLikeThisSearchRepository.kt
@@ -0,0 +1,137 @@
+package com.github.inflab.example.spring.data.mongodb.repository
+
+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.bson.Document
+import org.bson.types.ObjectId
+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
+import java.time.LocalDateTime
+
+@Repository
+class MoreLikeThisSearchRepository(
+ private val mongoTemplate: MongoTemplate,
+) {
+
+ data class TitleAndReleasedAndGenres(val title: String, val released: LocalDateTime?, val genres: List?)
+
+ /**
+ * @see Single Document with Multiple Fields
+ */
+ fun findTitleAndGenres(): AggregationResults {
+ val aggregation = aggregation {
+ search {
+ moreLikeThis {
+ like(
+ Document("title", "The Godfather").append("genres", "action"),
+ )
+ }
+ }
+
+ // TODO: add $limit stage
+
+ project {
+ excludeId()
+ +Movies::title
+ +Movies::released
+ +Movies::genres
+ }
+ }
+
+ return mongoTemplate.aggregate(aggregation)
+ }
+
+ /**
+ * @see Input Document Excluded in Results
+ */
+ fun findByMovie(): AggregationResults {
+ val movie = Document("_id", ObjectId("573a1396f29313caabce4a9a"))
+ .append("genres", listOf("Crime", "Drama"))
+ .append("title", "The Godfather")
+ val aggregation = aggregation {
+ search {
+ compound {
+ must {
+ moreLikeThis {
+ like(movie)
+ }
+ }
+
+ mustNot {
+ equal {
+ path("_id")
+ value(ObjectId("573a1396f29313caabce4a9a"))
+ }
+ }
+ }
+ }
+
+ // TODO: add $limit stage
+
+ project {
+ excludeId()
+ +Movies::title
+ +Movies::released
+ +Movies::genres
+ }
+ }
+
+ return mongoTemplate.aggregate(aggregation)
+ }
+
+ data class IdAndTitleAndGenres(val id: String, val title: String, val genres: List?)
+
+ /**
+ * @see Multiple Analyzers
+ */
+ fun findByMovies(): AggregationResults {
+ val movies = listOf(
+ Document("_id", ObjectId("573a1394f29313caabcde9ef"))
+ .append("plot", "Alice stumbles into the world of Wonderland. Will she get home? Not if the Queen of Hearts has her way.")
+ .append("title", "Alice in Wonderland"),
+ Document("_id", ObjectId("573a1398f29313caabce963d"))
+ .append("plot", "Alice is in Looking Glass land, where she meets many Looking Glass creatures and attempts to avoid the Jabberwocky, a monster that appears due to her being afraid.")
+ .append("title", "Alice in Wonderland"),
+ Document("_id", ObjectId("573a1398f29313caabce9644"))
+ .append("plot", "Alice is in Looking Glass land, where she meets many Looking Glass creatures and attempts to avoid the Jabberwocky, a monster that appears due to her being afraid.")
+ .append("title", "Alice in Wonderland"),
+ Document("_id", ObjectId("573a139df29313caabcfb504"))
+ .append("plot", "The wizards behind The Odyssey (1997) and Merlin (1998) combine Lewis Carroll's \"Alice in Wonderland\" and \"Through the Looking Glass\" into a two-hour special that just gets curiouser and curiouser.")
+ .append("title", "Alice in Wonderland"),
+ Document("_id", ObjectId("573a13bdf29313caabd5933b"))
+ .append("plot", "Nineteen-year-old Alice returns to the magical world from her childhood adventure, where she reunites with her old friends and learns of her true destiny: to end the Red Queen's reign of terror.")
+ .append("title", "Alice in Wonderland"),
+ )
+ val aggregation = aggregation {
+ search {
+ compound {
+ must {
+ moreLikeThis {
+ // change list of movie to vararg
+ like(*movies.toTypedArray())
+ }
+ }
+
+ mustNot {
+ equal {
+ path("_id")
+ value(ObjectId("573a1394f29313caabcde9ef"))
+ }
+ }
+ }
+ }
+
+ // TODO: add $limit stage
+
+ project {
+ +Movies::title
+ +Movies::genres
+ }
+ }
+
+ return mongoTemplate.aggregate(aggregation)
+ }
+}
diff --git a/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/repository/MoreLikeThisSearchRepositoryTest.kt b/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/repository/MoreLikeThisSearchRepositoryTest.kt
new file mode 100644
index 00000000..c5a5acab
--- /dev/null
+++ b/example/spring-data-mongodb/src/test/kotlin/com/github/inflab/example/spring/data/mongodb/repository/MoreLikeThisSearchRepositoryTest.kt
@@ -0,0 +1,78 @@
+package com.github.inflab.example.spring.data.mongodb.repository
+
+import com.github.inflab.example.spring.data.mongodb.extension.AtlasTest
+import io.kotest.core.annotation.Ignored
+import io.kotest.core.spec.style.FreeSpec
+import io.kotest.matchers.shouldBe
+
+@Ignored
+@AtlasTest(database = "sample_mflix")
+internal class MoreLikeThisSearchRepositoryTest(
+ private val moreLikeThisSearchRepository: MoreLikeThisSearchRepository,
+) : FreeSpec({
+
+ "findTitleAndGenres" {
+ // when
+ val result = moreLikeThisSearchRepository.findTitleAndGenres()
+
+ // then
+ result.mappedResults.take(5).map { it.title } shouldBe listOf(
+ "Godfather",
+ "The Godfather",
+ "The Godfather: Part II",
+ "The Godfather: Part III",
+ "The Defender",
+ )
+ result.mappedResults.take(5).map { it.genres } shouldBe listOf(
+ listOf("Comedy", "Drama", "Romance"),
+ listOf("Crime", "Drama"),
+ listOf("Crime", "Drama"),
+ listOf("Crime", "Drama"),
+ listOf("Action"),
+ )
+ }
+
+ "findByMovie" {
+ // when
+ val result = moreLikeThisSearchRepository.findByMovie()
+
+ // then
+ result.mappedResults.take(5).map { it.title } shouldBe listOf(
+ "Godfather",
+ "The Godfather: Part II",
+ "The Godfather: Part III",
+ "The Testimony",
+ "The Bandit",
+
+ )
+ result.mappedResults.take(5).map { it.genres } shouldBe listOf(
+ listOf("Comedy", "Drama", "Romance"),
+ listOf("Crime", "Drama"),
+ listOf("Crime", "Drama"),
+ listOf("Crime", "Drama"),
+ listOf("Crime", "Drama"),
+ )
+ }
+
+ "findByMovies" {
+ // when
+ val result = moreLikeThisSearchRepository.findByMovies()
+
+ // then
+ result.mappedResults.take(5).map { it.title } shouldBe listOf(
+ "Alice in Wonderland",
+ "Alice in Wonderland",
+ "Alice in Wonderland",
+ "Alice in Wonderland",
+ "Alex in Wonderland",
+
+ )
+ result.mappedResults.take(5).map { it.genres } shouldBe listOf(
+ listOf("Adventure", "Family", "Fantasy"),
+ listOf("Adventure", "Family", "Fantasy"),
+ listOf("Adventure", "Comedy", "Family"),
+ listOf("Adventure", "Family", "Fantasy"),
+ listOf("Comedy", "Drama"),
+ )
+ }
+})