Skip to content
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

Implement Near Search Operator #62

Merged
merged 8 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
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.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 <a href="https://www.mongodb.com/docs/atlas/atlas-search/near">near</a>
*/
@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 <a href="https://www.mongodb.com/docs/atlas/atlas-search/path-construction/#std-label-ref-path">Path Construction</a>
*/
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 <a href="https://www.mongodb.com/docs/atlas/atlas-search/path-construction/#std-label-ref-path">Path Construction</a>
*/
@JvmName("pathNumber")
fun path(vararg path: KProperty<Number?>) {
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 <a href="https://www.mongodb.com/docs/atlas/atlas-search/path-construction/#std-label-ref-path">Path Construction</a>
*/
@JvmName("pathDate")
fun path(vararg path: KProperty<Temporal>) {
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 <a href="https://www.mongodb.com/docs/atlas/atlas-search/path-construction/#std-label-ref-path">Path Construction</a>
*/
@JvmName("pathPoint")
fun path(vararg path: KProperty<GeoJsonPoint>) {
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is missing @param docs for origin and pivot method :)

*/
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 <a href="https://www.mongodb.com/docs/upcoming/reference/glossary/#std-term-ISODate">ISODate</a>
*/
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 <a href="https://www.mongodb.com/docs/upcoming/reference/geojson/#std-label-geojson-point">GeoJson Point</a>
*/
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
Comment on lines +105 to +107
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better if these lines are wrapped in code block.

Suggested change
* pivot
* score = ------------------
* pivot + distance
*
* ```
* 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 <a href="https://www.mongodb.com/docs/atlas/atlas-search/score/modify-score">Modify the Score</a>
*/
fun score(scoreConfiguration: ScoreSearchOptionDsl.() -> Unit) {
document["score"] = ScoreSearchOptionDsl().apply(scoreConfiguration).get()
}

internal fun build() = Document("near", document)
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,14 @@ interface SearchOperator {
* @see <a href="https://www.mongodb.com/docs/atlas/atlas-search/queryString">queryString</a>
*/
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 <a href="https://www.mongodb.com/docs/atlas/atlas-search/near">near</a>
*/
fun near(configuration: NearSearchOperatorDsl.() -> Unit)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
Original file line number Diff line number Diff line change
@@ -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(),
)
}
}
})
Loading