Discover Your Ideal Airbnb: Implementing a Spring Boot & Atlas Search With Kotlin Sync Driver
Rate this tutorial
One of my favorite activities is traveling and exploring the world. You know that feeling of discovering a new place and thinking, "How have I not been here before?" It's with that sensation that I'm always motivated to seek out new places to discover. Often, when searching for a place to stay, we're not entirely sure what we're looking for or what experiences we'd like to have. For example, we might want to rent a room in a city with a view of a castle. Finding something like that can seem difficult, right? However, there is a way to search for information accurately using Atlas Search.
In this tutorial, we will learn to build an application in Kotlin that utilizes full-text search in a database containing thousands of Airbnb listings. We'll explore how we can find the perfect accommodation that meets our specific needs.
Here is a video version of this article if you prefer to watch.
To achieve our goal, we will create a Kotlin Spring Boot application that communicates with MongoDB Atlas using the Kotlin Sync Driver.
The application will use a pre-imported database in Atlas called
sample_airbnb
, utilizing the listingsAndReviews
collection, which contains information about various Airbnbs.To identify the best Airbnb listings, we will create an endpoint that returns information about these listings. This endpoint will use the
summary
field from the collection to perform a full-text search with the fuzzy parameter in text operator. Additionally, we will filter the documents based on a minimum number of reviews, utilizing the search functionalities provided by Atlas Search.- Get started with MongoDB Atlas for free! If you don’t already have an account, MongoDB offers a free-forever Atlas cluster.
- IDE of your choice
Atlas Search is a feature in MongoDB Atlas that provides powerful and flexible search capabilities for your data. It integrates with Apache Lucene, enabling advanced text analysis, custom scoring, and result highlighting. This allows you to build sophisticated search functionality directly within your MongoDB applications.
To utilize Atlas Search effectively, we will focus on three key operators:
text
, range
, and compound
. Although there are various operators available, our exploration will concentrate on these to illustrate their practical applications.- Text: This operator will be used to perform text searches within our endpoint, allowing for approximate matching and handling variations in the search terms.
- Range: We will explore the
range
operator specifically with thegte
(greater than or equals) condition for thenumber_of_reviews
field. This will enable us to query and filter based on review counts effectively. - Compound: The
compound
operator will be used to combine thetext fuzzy
andrange
queries into a more complex and refined search. This will demonstrate how to merge multiple criteria for more sophisticated search functionality.
While this article will not delve deeply into all available operators, those interested in a more comprehensive exploration can refer to the MongoDB Atlas Search documentation for further details.
Before starting, you'll need to import the sample dataset, which includes several databases and collections, like the Airbnb list. After setting up your cluster, just click on "Database" in the left menu and choose "Load sample dataset," as shown in the image:
If everything goes smoothly, after the import, you will see our
databases and collections displayed as shown in the image.
After importing the collections, the next step is to create an index for the Airbnb collection. To do this, select "Database" from the side menu under “Deployment,” go to the "Atlas Search" tab, and click on "JSON Editor," as shown in the image:
In the next step, select the
sample_airbnb
database and the listingsAndReviews
collection (the Airbnb collection). Then, name your index "searchPlaces":Note that we are using Dynamic Mappings for simplicity, which allows
Atlas Search to automatically index the fields of supported types in
each document. For more details, I suggest checking out Define Field
Mappings
If everything goes well, the "searchPlaces" index will be created successfully, and you can view it as shown in the image below:
To test our index, we need to create an aggregation pipeline. While there are various methods to test this, we will use MongoDB Compass for convenience. MongoDB Compass is a powerful GUI tool that facilitates managing and analyzing MongoDB data. It provides features to visualize schemas, build queries, and manage data through an intuitive interface.
We need to set up an aggregation pipeline to meet the following requirements:
- Filter the
summary
field by text - Ensure a minimum
number_of_reviews
Here’s the aggregation pipeline we will use for testing:
1 [ 2 { 3 $search: { 4 index: "searchPlaces", 5 compound: { 6 filter: [ 7 { 8 range: { 9 path: "number_of_reviews", 10 gte: 50 11 } 12 }, 13 { 14 text: { 15 path: "summary", 16 query: "Istambun", 17 fuzzy: { 18 maxEdits: 2 19 } 20 } 21 } 22 ] 23 } 24 } 25 }, 26 { 27 $limit: 5 28 }, 29 { 30 $project: { 31 _id: 0, 32 name: 1, 33 summary: 1, 34 number_of_reviews: 1, 35 price: 1, 36 street: "$address.street", 37 } 38 } 39 ]
Let’s break down each stage:
- $search: The
$search
stage uses the MongoDB Atlas Search capabilities to perform a full-text search with additional filtering.- index:
"searchPlaces"
specifies the search index to use. If the index name were "default," we would not need to specify it here. - compound: This allows you to combine multiple search criteria. The compound query here is used to filter the search results based on both text and range criteria.
- filter: This contains an array of filter criteria applied to the search results.
- range: This filters documents where the
number_of_reviews
field is greater than or equal to 50. - text: Text performs a full-text search on the
summary
field with the query"Istambun".
The fuzzy option with maxEdits: 2 allows for fuzzy matching, meaning it can match terms that are similar to "Istambun" with up to two character edits (insertions, deletions, or substitutions).
- $limit: This limits the number of documents returned by the query to 5. Using a limit is essential to maintain performance.
- $project: This specifies which fields to include or exclude in the final result.
Simply run this pipeline to obtain the results. See:
Our application will be developed in Kotlin with Spring. It’s important to note that we will not be using Spring Data. Instead, we will use the Kotlin Sync Driver, which is specialized for communication between the application and MongoDB. The goal of our application is simple:
- Provide an endpoint that allows us to make requests and communicate with MongoDB Atlas.
As you can see, I have only added the Spring Web dependency.
The first thing we’ll do is open the
build.gradle.kts
file and add the mongodb-driver-kotlin-sync
dependency.1 dependencies { 2 implementation("org.mongodb:mongodb-driver-kotlin-sync:5.1.1") 3 }
To establish our connection, we need to follow these steps. First, update the
application.properties
file with the required values.1 spring.application.name=Airbnb Searcher 2 spring.data.mongodb.uri=mongodb+srv://user:pass@cluster0.cluster.mongodb.net/ 3 spring.data.mongodb.database=sample_airbnb
Notice: Don't forget to update the URI to match your MongoDB connection.
Next, we will create a
MongoConfig
class within the application.config
directory to set up the connection when our application starts.1 package com.mongodb.searcher.application.config 2 3 import com.mongodb.kotlin.client.MongoClient 4 import com.mongodb.kotlin.client.MongoDatabase 5 import org.springframework.beans.factory.annotation.Value 6 import org.springframework.context.annotation.Bean 7 import org.springframework.context.annotation.Configuration 8 9 10 class MongoConfig { 11 12 13 lateinit var uri: String 14 15 16 lateinit var databaseName: String 17 18 19 fun getMongoClient(): MongoClient { 20 return MongoClient.create(uri) 21 } 22 23 24 fun mongoDatabase(mongoClient: MongoClient): MongoDatabase { 25 return mongoClient.getDatabase(databaseName) 26 } 27 }
Great, we have defined our
MongoConfig
class, which will use the values from application.properties
. Now, create the class AirbnbEntity
within the resources
package:1 package com.mongodb.searcher.resources 2 3 import com.mongodb.searcher.domain.Airbnb 4 import org.bson.codecs.pojo.annotations.BsonId 5 import org.bson.codecs.pojo.annotations.BsonProperty 6 import org.bson.types.Decimal128 7 8 data class AirbnbEntity( 9 val id: String, 10 val name: String, 11 val summary: String, 12 val price: Decimal128, 13 14 val numbersOfReviews: Int, 15 val address: Address 16 ) { 17 data class Address( 18 val street: String, 19 val country: String, 20 21 val countryCode: String 22 ) 23 24 fun toDomain(): Airbnb { 25 return Airbnb( 26 id = id, 27 name = name, 28 summary = summary, 29 price = price, 30 numbersOfReviews = numbersOfReviews, 31 street = address.street 32 ) 33 } 34 }
Now, let’s create our class that will utilize the Atlas Search index. To do this, create the
AirbnbRepository
class within the resources
package.1 package com.mongodb.searcher.resources 2 3 import com.mongodb.client.model.Aggregates 4 import com.mongodb.client.model.Projections 5 import com.mongodb.client.model.search.FuzzySearchOptions 6 import com.mongodb.client.model.search.SearchOperator 7 import com.mongodb.client.model.search.SearchOptions 8 import com.mongodb.client.model.search.SearchPath 9 import com.mongodb.kotlin.client.MongoDatabase 10 import org.slf4j.LoggerFactory 11 import org.springframework.stereotype.Repository 12 13 14 class AirbnbRepository( 15 private val mongoDatabase: MongoDatabase 16 ) { 17 companion object { 18 private val logger = LoggerFactory.getLogger(AirbnbRepository::class.java) 19 private const val COLLECTION = "listingsAndReviews" 20 } 21 fun find(query: String, minNumberReviews: Int): List<AirbnbEntity> { 22 val collection = mongoDatabase.getCollection<AirbnbEntity>(COLLECTION) 23 24 return try { 25 collection.aggregate( 26 listOf( 27 createSearchStage(query, minNumberReviews), 28 createLimitStage(), 29 createProjectionStage() 30 ) 31 ).toList() 32 } catch (e: Exception) { 33 logger.error("An exception occurred when trying to aggregate the collection: ${e.message}") 34 emptyList() 35 } 36 } 37 38 private fun createSearchStage(query: String, minNumberReviews: Int) = 39 Aggregates.search( 40 SearchOperator.compound().filter( 41 listOf( 42 SearchOperator.numberRange(SearchPath.fieldPath("number_of_reviews")) 43 .gte(minNumberReviews), 44 SearchOperator.text(SearchPath.fieldPath(AirbnbEntity::summary.name), query) 45 .fuzzy(FuzzySearchOptions.fuzzySearchOptions().maxEdits(2)) 46 ) 47 ), 48 SearchOptions.searchOptions().index("searchPlaces") 49 ) 50 private fun createLimitStage() = 51 Aggregates.limit(5) 52 53 54 private fun createProjectionStage() = 55 Aggregates.project( 56 Projections.fields( 57 Projections.include( 58 listOf( 59 AirbnbEntity::name.name, 60 AirbnbEntity::id.name, 61 AirbnbEntity::summary.name, 62 AirbnbEntity::price.name, 63 "number_of_reviews", 64 AirbnbEntity::address.name 65 ) 66 ) 67 ) 68 ) 69 }
Let's analyze the find method.
As you can see, the method expects a query string and an int
minNumberReviews
and returns a list of AirbnbEntity
. This list is generated through an aggregation pipeline, which consists of three stages:- Search stage: Utilizes the
$search
operator to filter documents based on the query and the minimum number of reviews. - Limit stage: Restricts the result set to a maximum number of documents.
- Projection stage: Specifies which fields to include in the returned documents (this stage is optional and included here just to illustrate how to use it).
Notice: Depending on the scenario, adding stages after the
$search
stage can drastically impact the application's performance. For more
details, refer to our docs on performance
considerations.To continue with our project, let's create a
domain
package with two classes. The first will be our Airbnb
.1 package com.mongodb.searcher.domain 2 3 import org.bson.codecs.pojo.annotations.BsonId 4 import org.bson.codecs.pojo.annotations.BsonProperty 5 import org.bson.types.Decimal128 6 7 data class Airbnb( 8 val id: String, 9 val name: String, 10 val summary: String, 11 val price: Decimal128, 12 13 val numbersOfReviews: Int, 14 val street: String 15 )
Next, our
AirbnbService
:1 package com.mongodb.searcher.domain 2 3 import com.mongodb.searcher.resources.AirbnbRepository 4 import org.springframework.stereotype.Service 5 6 7 class AirbnbService( 8 private val airbnbRepository: AirbnbRepository 9 ) { 10 11 fun find(query: String, minNumberReviews: Int): List<Airbnb> { 12 require(query.isNotEmpty()) { "Query must not be empty" } 13 require(minNumberReviews > 0) { "Minimum number of reviews must not be negative" } 14 15 return airbnbRepository.find(query, minNumberReviews).map { it.toDomain() } 16 } 17 }
This class is responsible for validating our inputs and accessing the repository.
To enable REST communication, create the
AirbnbController
class within the application.web
package:1 package com.mongodb.searcher.application.web 2 3 import com.mongodb.searcher.domain.Airbnb 4 import com.mongodb.searcher.domain.AirbnbService 5 import org.springframework.web.bind.annotation.GetMapping 6 import org.springframework.web.bind.annotation.RequestParam 7 import org.springframework.web.bind.annotation.RestController 8 9 10 class AirbnbController( 11 private val airbnbService: AirbnbService 12 ) { 13 14 15 fun find( 16 query: String, 17 minNumberReviews: Int 18 ): List<Airbnb> { 19 return airbnbService.find(query, minNumberReviews) 20 } 21 }
The final folder structure should look similar to the one in the image:
Simply navigate to the project directory and execute:
1 ./gradlew bootRun
Then, simply access the endpoint by providing the required arguments. Below is an example:
1 curl --location 'http://localhost:8080/airbnb/search?query=Istambun&minNumberReviews=50'
In this tutorial, we built a Kotlin-based Spring Boot application that uses MongoDB Atlas Search to find Airbnb listings efficiently. We demonstrated how to create a search index and implement an aggregation pipeline for filtering and searching data.
While we focused on fuzzy matching and review count filtering, MongoDB Atlas Search offers many other powerful features, such as custom scoring and advanced text analysis.
Exploring these additional capabilities can further enhance your search functionality and provide even more refined results. The example source code used in this series is available on GitHub.
For more details on Atlas Search, you can refer to the Exploring Search Capabilities With Atlas Search article.
Top Comments in Forums
There are no comments on this article yet.
Related
Tutorial
Enhancing LLM Accuracy Using MongoDB Vector Search and Unstructured.io Metadata
Dec 04, 2023 | 12 min read
Tutorial
Launch a Fully Managed RAG Workflow With MongoDB Atlas and Amazon Bedrock
May 08, 2024 | 6 min read