Explore Developer Center's New Chatbot! MongoDB AI Chatbot can be accessed at the top of your navigation to answer all your MongoDB questions.

Join us at AWS re:Invent 2024! Learn how to use MongoDB for AI use cases.
MongoDB Developer
Atlas
plus
Sign in to follow topics
MongoDB Developer Centerchevron-right
Developer Topicschevron-right
Productschevron-right
Atlaschevron-right

Discover Your Ideal Airbnb: Implementing a Spring Boot & Atlas Search With Kotlin Sync Driver

Ricardo Mello8 min read • Published Aug 06, 2024 • Updated Oct 02, 2024
SpringKotlinAtlas
Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
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.

Video Version

Here is a video version of this article if you prefer to watch.

Demonstration

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.
GIF A Postman demonstrating a search request
Fig: A Postman demonstrating a search request.

Pre-requisites

  • MongoDB Atlas account
    • 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

What is Atlas Search?

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 the gte (greater than or equals) condition for the number_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 the text fuzzy and range 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.

Load sample dataset

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:
Screenshot demonstrating how to load sample dataset
Fig: Loading Dataset in MongoDB Atlas.
If everything goes smoothly, after the import, you will see our databases and collections displayed as shown in the image.
Screenshot demonstrating databases and collections imported
Fig: MongoDB Atlas with imported databases and collections.

Creating the Atlas Search index

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:
Screenshot demonstrating how to create an Atlas Search Index
Fig: MongoDB Atlas indicating to create an Atlas Search index.
In the next step, select the sample_airbnb database and the listingsAndReviews collection (the Airbnb collection). Then, name your index "searchPlaces":
Screenshot demonstrating how to define Index Properties
Fig: MongoDB Atlas indicating to create an Atlas Search index Properties
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:
Screenshot demonstrating Atlas Search Index created
Fig: Screen showing the created Atlas Search Index

Testing our index in MongoDB Compass

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:
  1. Filter the summary field by text
  2. 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:
  1. $search: The $search stage uses the MongoDB Atlas Search capabilities to perform a full-text search with additional filtering.
    1. index: "searchPlaces" specifies the search index to use. If the index name were "default," we would not need to specify it here.
    2. 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.
    3. filter: This contains an array of filter criteria applied to the search results.
    4. range: This filters documents where the number_of_reviews field is greater than or equal to 50.
    5. 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).
  2. $limit: This limits the number of documents returned by the query to 5. Using a limit is essential to maintain performance.
  3. $project: This specifies which fields to include or exclude in the final result.
Simply run this pipeline to obtain the results. See:
Screenshot demonstrating MongoDB Compass with an executed aggregation pipeline
Fig: MongoDB Compass with an executed aggregation pipeline

Building a Kotlin application

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.

Creating the project

To do this, we'll use the Spring Initializer official page to create our project:
Screenshot demonstrating Spring Initializr web page
Fig: Spring Initializr website for project creation
As you can see, I have only added the Spring Web dependency.

Adding MongoDB driver dependency

The first thing we’ll do is open the build.gradle.kts file and add the mongodb-driver-kotlin-sync dependency.
1dependencies {
2 implementation("org.mongodb:mongodb-driver-kotlin-sync:5.1.1")
3}
Screenshot demonstrating build.gradle.kts file
Fig: build.gradle.kts with the mongodb-driver-kotlin-sync dependency added.

Establishing a connection

To establish our connection, we need to follow these steps. First, update the application.properties file with the required values.
1spring.application.name=Airbnb Searcher
2spring.data.mongodb.uri=mongodb+srv://user:pass@cluster0.cluster.mongodb.net/
3spring.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.
1package com.mongodb.searcher.application.config
2
3import com.mongodb.kotlin.client.MongoClient
4import com.mongodb.kotlin.client.MongoDatabase
5import org.springframework.beans.factory.annotation.Value
6import org.springframework.context.annotation.Bean
7import org.springframework.context.annotation.Configuration
8
9@Configuration
10class MongoConfig {
11
12 @Value("\${spring.data.mongodb.uri}")
13 lateinit var uri: String
14
15 @Value("\${spring.data.mongodb.database}")
16 lateinit var databaseName: String
17
18 @Bean
19 fun getMongoClient(): MongoClient {
20 return MongoClient.create(uri)
21 }
22
23 @Bean
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:
1package com.mongodb.searcher.resources
2
3import com.mongodb.searcher.domain.Airbnb
4import org.bson.codecs.pojo.annotations.BsonId
5import org.bson.codecs.pojo.annotations.BsonProperty
6import org.bson.types.Decimal128
7
8data class AirbnbEntity(
9 @BsonId val id: String,
10 val name: String,
11 val summary: String,
12 val price: Decimal128,
13 @BsonProperty("number_of_reviews")
14 val numbersOfReviews: Int,
15 val address: Address
16) {
17 data class Address(
18 val street: String,
19 val country: String,
20 @BsonProperty("country_code")
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}

Creating the repository

Now, let’s create our class that will utilize the Atlas Search index. To do this, create the AirbnbRepository class within the resources package.
1package com.mongodb.searcher.resources
2
3import com.mongodb.client.model.Aggregates
4import com.mongodb.client.model.Projections
5import com.mongodb.client.model.search.FuzzySearchOptions
6import com.mongodb.client.model.search.SearchOperator
7import com.mongodb.client.model.search.SearchOptions
8import com.mongodb.client.model.search.SearchPath
9import com.mongodb.kotlin.client.MongoDatabase
10import org.slf4j.LoggerFactory
11import org.springframework.stereotype.Repository
12
13@Repository
14class 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:
  1. Search stage: Utilizes the $search operator to filter documents based on the query and the minimum number of reviews.
  2. Limit stage: Restricts the result set to a maximum number of documents.
  3. 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.

Creating a service

To continue with our project, let's create a domain package with two classes. The first will be our Airbnb.
1package com.mongodb.searcher.domain
2
3import org.bson.codecs.pojo.annotations.BsonId
4import org.bson.codecs.pojo.annotations.BsonProperty
5import org.bson.types.Decimal128
6
7data class Airbnb(
8 @BsonId val id: String,
9 val name: String,
10 val summary: String,
11 val price: Decimal128,
12 @BsonProperty("number_of_reviews")
13 val numbersOfReviews: Int,
14 val street: String
15)
Next, our AirbnbService:
1package com.mongodb.searcher.domain
2
3import com.mongodb.searcher.resources.AirbnbRepository
4import org.springframework.stereotype.Service
5
6@Service
7class 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.

Creating a controller

To enable REST communication, create the AirbnbController class within the application.web package:
1package com.mongodb.searcher.application.web
2
3import com.mongodb.searcher.domain.Airbnb
4import com.mongodb.searcher.domain.AirbnbService
5import org.springframework.web.bind.annotation.GetMapping
6import org.springframework.web.bind.annotation.RequestParam
7import org.springframework.web.bind.annotation.RestController
8
9@RestController
10class AirbnbController(
11 private val airbnbService: AirbnbService
12) {
13
14 @GetMapping("/airbnb/search")
15 fun find(
16 @RequestParam("query") query: String,
17 @RequestParam("minNumberReviews") minNumberReviews: Int
18 ): List<Airbnb> {
19 return airbnbService.find(query, minNumberReviews)
20 }
21}

Final application structure

The final folder structure should look similar to the one in the image:
Screenshot demonstrating Application structure.
Fig: Application structure

Running the application

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:
1curl --location 'http://localhost:8080/airbnb/search?query=Istambun&minNumberReviews=50'
Screenshot demonstrating Postman Search Request
Fig: Postman Search Request

Conclusion

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.
Start the Conversation

Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Related
Tutorial

Semantic search with Jina Embeddings v2 and MongoDB Atlas


Dec 05, 2023 | 12 min read
Code Example

EHRS-Peru


Sep 11, 2024 | 3 min read
Tutorial

Boosting AI: Build Your Chatbot Over Your Data With MongoDB Atlas Vector Search and LangChain Templates Using the RAG Pattern


Sep 18, 2024 | 7 min read
Tutorial

Beyond Basics: Enhancing Kotlin Ktor API With Vector Search


Sep 18, 2024 | 9 min read
Table of Contents