Skip to content

[Kotlin CR] Add additional SearchOperator helper methods for the rest of the Atlas Search operators #208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 2, 2025
37 changes: 36 additions & 1 deletion examples/src/test/kotlin/AggregatesBuilderTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ class AggregatesBuilderTest {
val rated: String,
val plot: String,
val runtime: Int,
val imdb: IMDB
val imdb: IMDB,
val fullplot: String? = "No full plot",
){
data class IMDB(
val rating: Double
Expand Down Expand Up @@ -951,6 +952,40 @@ class AggregatesBuilderTest {
assertEquals("Back to the Future", results.first().title)
}

/* NOTE: Test is not run by default. FTS requires the creation of a text index on the collection before running
(see note at top of file for additional setup requirements for FTS).
*/
@Ignore
fun atlasSearchOperatorTest() = runBlocking {

// :snippet-start: atlas-search-pipeline
data class Results(val title: String, val year: Int, val genres: List<String>)

val searchStage = Aggregates.search(
SearchOperator.compound()
.filter(
listOf(
SearchOperator.`in`(SearchPath.fieldPath(Movie::genres.name), listOf("Comedy")),
SearchOperator.phrase(SearchPath.fieldPath(Movie::fullplot.name), "new york"),
SearchOperator.numberRange(SearchPath.fieldPath(Movie::year.name)).gtLt(1950, 2000),
SearchOperator.wildcard(SearchPath.fieldPath(Movie::title.name), "Love *")
)
)
)

val projectStage = Aggregates.project(
Projections.include(Movie::title.name, Movie::year.name, Movie::genres.name))

val pipeline = listOf(searchStage, projectStage)
val resultsFlow = ftsCollection.aggregate<Results>(pipeline)

resultsFlow.collect { println(it) }
// :snippet-end:

val result = resultsFlow.toList()
assertEquals(2, result.size)
}

/* NOTE: Test is not run by default. FTS requires the creation of a text index on the collection before running
(see note at top of file for additional setup requirements for FTS).
*/
Expand Down
126 changes: 109 additions & 17 deletions examples/src/test/kotlin/AggregationTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@ import com.mongodb.client.model.Accumulators
import com.mongodb.client.model.Aggregates
import com.mongodb.client.model.Filters
import com.mongodb.client.model.Projections
import com.mongodb.client.model.search.SearchOperator
import com.mongodb.client.model.search.SearchPath.fieldPath
import com.mongodb.kotlin.client.coroutine.MongoClient
import config.getConfig
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.bson.Document
import org.bson.codecs.pojo.annotations.BsonId
import org.bson.conversions.Bson
import org.bson.json.JsonWriterSettings
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.Ignore


class AggregationTest {
Expand Down Expand Up @@ -44,16 +48,102 @@ class AggregationTest {
fun beforeAll() {
runBlocking {
val restaurants = listOf(
Restaurant("Sun Bakery Trattoria", Restaurant.Contact("386-555-0189", "SunBakeryTrattoria@example.org", listOf(-74.0056649, 40.7452371)), 4, listOf("Pizza", "Pasta", "Italian", "Coffee", "Sandwiches")),
Restaurant("Blue Bagels Grill", Restaurant.Contact("786-555-0102", "BlueBagelsGrill@example.com", listOf(-73.92506, 40.8275556)), 3, listOf("Bagels", "Cookies", "Sandwiches")),
Restaurant("XYZ Bagels Restaurant", Restaurant.Contact("435-555-0190", "XYZBagelsRestaurant@example.net", listOf(-74.0707363, 40.59321569999999)), 4, listOf("Bagels", "Sandwiches", "Coffee")),
Restaurant("Hot Bakery Cafe", Restaurant.Contact("264-555-0171", "HotBakeryCafe@example.net", listOf(-73.96485799999999, 40.761899)), 4, listOf("Bakery", "Cafe", "Coffee", "Dessert")),
Restaurant("Green Feast Pizzeria", Restaurant.Contact("840-555-0102", "GreenFeastPizzeria@example.com", listOf(-74.1220973, 40.6129407)), 2, listOf("Pizza", "Italian")),
Restaurant("ZZZ Pasta Buffet", Restaurant.Contact("769-555-0152", "ZZZPastaBuffet@example.com", listOf(-73.9446421, 40.7253944)), 0, listOf("Pasta", "Italian", "Buffet", "Cafeteria")),
Restaurant("XYZ Coffee Bar", Restaurant.Contact("644-555-0193", "XYZCoffeeBar@example.net", listOf(-74.0166091, 40.6284767)), 5, listOf("Coffee", "Cafe", "Bakery", "Chocolates")),
Restaurant("456 Steak Restaurant", Restaurant.Contact("990-555-0165", "456SteakRestaurant@example.com", listOf(-73.9365108, 40.8497077)), 0, listOf("Steak", "Seafood")),
Restaurant("456 Cookies Shop", Restaurant.Contact("604-555-0149", "456CookiesShop@example.org", listOf(-73.8850023, 40.7494272)), 4, listOf("Bakery", "Cookies", "Cake", "Coffee")),
Restaurant("XYZ Steak Buffet", Restaurant.Contact("229-555-0197", "XYZSteakBuffet@example.org", listOf(-73.9799932, 40.7660886)), 3, listOf("Steak", "Salad", "Chinese"))
Restaurant(
"Sun Bakery Trattoria",
Restaurant.Contact(
"386-555-0189",
"SunBakeryTrattoria@example.org",
listOf(-74.0056649, 40.7452371)
),
4,
listOf("Pizza", "Pasta", "Italian", "Coffee", "Sandwiches")
),
Restaurant(
"Blue Bagels Grill",
Restaurant.Contact(
"786-555-0102",
"BlueBagelsGrill@example.com",
listOf(-73.92506, 40.8275556)
),
3,
listOf("Bagels", "Cookies", "Sandwiches")
),
Restaurant(
"XYZ Bagels Restaurant",
Restaurant.Contact(
"435-555-0190",
"XYZBagelsRestaurant@example.net",
listOf(-74.0707363, 40.59321569999999)
),
4,
listOf("Bagels", "Sandwiches", "Coffee")
),
Restaurant(
"Hot Bakery Cafe",
Restaurant.Contact(
"264-555-0171",
"HotBakeryCafe@example.net",
listOf(-73.96485799999999, 40.761899)
),
4,
listOf("Bakery", "Cafe", "Coffee", "Dessert")
),
Restaurant(
"Green Feast Pizzeria",
Restaurant.Contact(
"840-555-0102",
"GreenFeastPizzeria@example.com",
listOf(-74.1220973, 40.6129407)
),
2,
listOf("Pizza", "Italian")
),
Restaurant(
"ZZZ Pasta Buffet",
Restaurant.Contact(
"769-555-0152",
"ZZZPastaBuffet@example.com",
listOf(-73.9446421, 40.7253944)
),
0,
listOf("Pasta", "Italian", "Buffet", "Cafeteria")
),
Restaurant(
"XYZ Coffee Bar",
Restaurant.Contact("644-555-0193", "XYZCoffeeBar@example.net", listOf(-74.0166091, 40.6284767)),
5,
listOf("Coffee", "Cafe", "Bakery", "Chocolates")
),
Restaurant(
"456 Steak Restaurant",
Restaurant.Contact(
"990-555-0165",
"456SteakRestaurant@example.com",
listOf(-73.9365108, 40.8497077)
),
0,
listOf("Steak", "Seafood")
),
Restaurant(
"456 Cookies Shop",
Restaurant.Contact(
"604-555-0149",
"456CookiesShop@example.org",
listOf(-73.8850023, 40.7494272)
),
4,
listOf("Bakery", "Cookies", "Cake", "Coffee")
),
Restaurant(
"XYZ Steak Buffet",
Restaurant.Contact(
"229-555-0197",
"XYZSteakBuffet@example.org",
listOf(-73.9799932, 40.7660886)
),
3,
listOf("Steak", "Salad", "Chinese")
)
)
collection.insertMany(restaurants)
}
Expand All @@ -71,15 +161,17 @@ class AggregationTest {
}

@Test
fun basicAggregationTest() = runBlocking {
fun basicAggregationTest() = runBlocking {
// :snippet-start: basic-aggregation
data class Results(@BsonId val id: Int, val count: Int)

val resultsFlow = collection.aggregate<Results>(
listOf(
Aggregates.match(Filters.eq(Restaurant::categories.name, "Bakery")),
Aggregates.group("\$${Restaurant::stars.name}",
Accumulators.sum("count", 1))
Aggregates.group(
"\$${Restaurant::stars.name}",
Accumulators.sum("count", 1)
)
)
)

Expand Down Expand Up @@ -124,7 +216,7 @@ class AggregationTest {
@Test
fun explainAggregationTest() = runBlocking {
// :snippet-start: explain-aggregation
data class Results (val name: String, val count: Int)
data class Results(val name: String, val count: Int)

val explanation = collection.aggregate<Results>(
listOf(
Expand All @@ -143,11 +235,11 @@ class AggregationTest {
@Test
fun buildDocumentsTipTest() {
val method1 =
// :snippet-start: build-documents-tip
Document("\$arrayElemAt", listOf("\$categories", 0))
// :snippet-start: build-documents-tip
Document("\$arrayElemAt", listOf("\$categories", 0))
// is equivalent to
val method2 = // :remove:
Document.parse("{ \$arrayElemAt: ['\$categories', 0] }")
Document.parse("{ \$arrayElemAt: ['\$categories', 0] }")
// :snippet-end:
// assert to test equivalency
assertEquals(method1, method2)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
data class Results(val title: String, val year: Int, val genres: List<String>)

val searchStage = Aggregates.search(
SearchOperator.compound()
.filter(
listOf(
SearchOperator.`in`(SearchPath.fieldPath(Movie::genres.name), listOf("Comedy")),
SearchOperator.phrase(SearchPath.fieldPath(Movie::fullplot.name), "new york"),
SearchOperator.numberRange(SearchPath.fieldPath(Movie::year.name)).gtLt(1950, 2000),
SearchOperator.wildcard(SearchPath.fieldPath(Movie::title.name), "Love *")
)
)
)

val projectStage = Aggregates.project(
Projections.include(Movie::title.name, Movie::year.name, Movie::genres.name))

val pipeline = listOf(searchStage, projectStage)
val resultsFlow = ftsCollection.aggregate<Results>(pipeline)

resultsFlow.collect { println(it) }
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ data class Movie(
val rated: String,
val plot: String,
val runtime: Int,
val imdb: IMDB
val imdb: IMDB,
val fullplot: String? = "No full plot",
){
data class IMDB(
val rating: Double
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ data class Results(@BsonId val id: Int, val count: Int)
val resultsFlow = collection.aggregate<Results>(
listOf(
Aggregates.match(Filters.eq(Restaurant::categories.name, "Bakery")),
Aggregates.group("\$${Restaurant::stars.name}",
Accumulators.sum("count", 1))
Aggregates.group(
"\$${Restaurant::stars.name}",
Accumulators.sum("count", 1)
)
)
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Document("\$arrayElemAt", listOf("\$categories", 0))
Document("\$arrayElemAt", listOf("\$categories", 0))
// is equivalent to
Document.parse("{ \$arrayElemAt: ['\$categories', 0] }")
Document.parse("{ \$arrayElemAt: ['\$categories', 0] }")
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
data class Results (val name: String, val count: Int)
data class Results(val name: String, val count: Int)

val explanation = collection.aggregate<Results>(
listOf(
Expand Down
3 changes: 3 additions & 0 deletions source/fundamentals/aggregation.txt
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ first element in the ``categories`` field.
Results(name=456 Cookies Shop, firstCategory=Bakery)
Results(name=XYZ Steak Buffet, firstCategory=Steak)

API Documentation
-----------------

For more information about the methods and classes mentioned in this section,
see the following API Documentation:

Expand Down
25 changes: 25 additions & 0 deletions source/fundamentals/builders/aggregates.txt
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,31 @@ field in the ``movies`` collection for text that contains the word "Future":
Learn more about the builders from the
`search package API documentation <{+core-api+}/client/model/search/package-summary.html>`__.

.. _kotlin-cr-atlas-search-stage:

Search Operator Methods
~~~~~~~~~~~~~~~~~~~~~~~

.. sharedinclude:: dbx/jvm/atlas-search-operator-helpers.rst

.. replacement:: as-idx-link

the :ref:`kotlin-search-indexes` section of the Indexes guide

.. replacement:: atlas-query-operators-example

.. io-code-block::

.. input:: /examples/generated/AggregatesBuilderTest.snippet.atlas-search-pipeline.kt
:language: kotlin

.. output::
:language: console
:visible: false

Results(title=Love at First Bite, year=1979, genres=[Comedy, Romance])
Results(title=Love Affair, year=1994, genres=[Comedy, Drama])

Atlas Search Metadata
---------------------

Expand Down
5 changes: 5 additions & 0 deletions source/whats-new.txt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ and features:
the `SearchOperator <{+core-api+}/client/model/search/SearchOperator.html>`__
interface API documentation

.. replacement:: atlas-query-operators

the :ref:`kotlin-cr-atlas-search-stage` section of the Aggregates
Builders guide

.. _kotlin-coroutine-version-5.3:

What's New in 5.3
Expand Down
Loading