Home · Server-Side Swift Tutorials

Elasticsearch in Vapor: Getting Started

In this tutorial, you’ll set up a Vapor server to interact with an Elasticsearch server running locally with Docker to store and retrieve recipe documents.

4.5/5 2 Ratings

Version

  • Swift 5, macOS 10.15, Xcode 11
Update note: Christian Weinberger updated this tutorial for macOS 10.15, Xcode 11.4 and Swift 5.2. He also wrote the original.

Elasticsearch is a distributed search and analytics engine for all types of structured and unstructured data. It’s built on Apache Lucene, and is known for its speed and scalability. Elasticsearch provides you with a set of REST APIs to interact with data.

Developers typically use Elasticsearch when they need to deal with a huge amount of data — for example, when storing logs. You’ll use it to index data and make that data available through a powerful search.

In this tutorial, you’ll learn the basics of Elasticsearch, and how to:

  • Set it up for the development environment.
  • Perform CRUD (Create, Read, Update, Delete) operations.
  • Leverage it to search for documents.

You’ll do this by developing a Recipe Finder Web API in Vapor.

Note: This tutorial assumes you’ve installed Docker and have some basic experience with it. See Docker on macOS: Getting Started to learn more about Docker and how to install it.

Additionally, you should have some experience using Vapor to build web apps. See Getting Started With Server-Side Swift With Vapor if you’re new to Vapor.

Getting Started

Use the Download Materials button at the top or bottom of this page to download the starter project for the Recipe Finder Web API.

Locate starter/recipe-finder-vapor4/ in Finder and double-click Package.swift to open the project in Xcode. Wait while Xcode resolves and fetches the package dependencies — don’t stop the build!

Note: To use Vapor 4, you need to install at least Xcode 11.4 with Swift 5.2 on your machine.

The starter project is a Vapor server that you’ll set up to interact with an Elasticsearch server. You’ll use this setup to store and retrieve recipe documents.

You’ll find an ElasticsearchClient.swift file in Sources/App/Services/Elasticsearch. Now, you just need an Elasticsearch server.

Running Elasticsearch in Docker

To avoid version problems and to isolate Elasticsearch from your system, you’ll run Elasticsearch in a Docker container.

In Terminal, run this command:

docker run -d --name elasticsearch -p 9200:9200 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.6.2

This downloads the Elasticsearch image, if you haven’t downloaded it already, and runs it in a Docker container named elasticsearch. The detach option -d runs the process in the background.

Elasticsearch exposes its API over HTTP on port 9200. The publish option -p 9200:9200 tells Docker to map the container’s internal port 9200 to port 9200 of your host machine.

The environment variable discovery.type starts a single-node Elasticsearch cluster for development or testing, which bypasses bootstrap checks.

Note: This command doesn’t set up any shared volumes to persist your data. Any data stored in this Elasticsearch instance won’t exist after you stop and run the container again.

Your next step is to verify your Elasticsearch instance is running properly. In a browser window, open http://localhost:9200. You should see something like this:

Localhost showing Elasticsearch is running

Congrats! You’re now ready to play around with Elasticsearch. You’ll connect your Vapor server to the Elasticsearch server, then implement a REST API for CRUD and search operations on the Vapor server. Requests sent to the Vapor server’s 8080 port will forward to the Elasticsearch server’s 9200 port.

Using Elasticsearch

Elasticsearch is a good choice for relevance-based searching, full-text search, synonym or phonetic search and, if you want to get good search results, even for misspelled search terms. Here’s a look at some of the basics of using Elasticsearch.

Understanding Indexes and Documents

Instead of storing information in rows and columns, Elasticsearch stores JSON documents in indexes. Think of an index as a schematic collection of similar documents, where documents are the basic unit of information you index.

Each index also has one mapping type, which determines how you index the documents. Here’s how you differentiate the two types:

  • Dynamic mapping: With this type, which is the default, you don’t have to define fields and mapping types beforehand. Adding a document to the index automatically adds all new fields to the mapping type.
  • Explicit mapping: Here, you need to provide the fields and mapping types when you create the index. You can add more fields later.

Explicit mapping has the benefit of giving you control over which fields are relevant for the index and how you preprocess their values. This makes your life easier when doing complex searches. It also has a positive effect on storage and performance.

Comparing Elasticsearch to Relational Databases

Unlike relational databases, Elasticsearch works best without relations. While it’s possible to set some basic relations between documents, those relationships aren’t as powerful as with relational databases like MySQL or PostgreSQL.

In SQL, you try to normalize data as much as possible; the opposite is true in Elasticsearch. You can’t create relations and join indexes to perform queries, so you have to denormalize the data in your indexes. By doing this, you build a powerful cluster that can scale up quickly, as needed.

Implementing CRUD

Now, back to the Xcode project to implement some basic CRUD — Create, Read, Update, Delete — operations so you can store and read recipes from Elasticsearch. To do this, you’ll modify files in Sources/App.

First, you need to specify your model for recipes. Open Models/Recipe.swift and add these properties to Recipe:

var id: String?
var name: String
var description: String
var instructions: String
var ingredients: [String]

This is a very basic model of a recipe, but it’ll do the job. id is Optional since in Elasticsearch, a Recipe only gets an ID after you store it.

Here, ingredients is a simple array of type String. In a more sophisticated version, you’d have a struct with more fields, such as amount and unit.

Next, inspect the classes in Services/Elasticsearch:

List of several Elasticsearch classes in Xcode

Open ElasticsearchClient.swift. It’s already capable of sending requests to a URL and returning a response: EventLoopFuture<Response> or a parsed object: EventLoopFuture<D: Decodable>.

In this file, you’ll only have to worry about the Requests section, where you’ll find method signatures prepared without implementation, except for fatalError() statements, to make the project build.

Creating a Recipe

You’ll start by storing a recipe to Elasticsearch. First, look at the createDocument(_:in:) method signature:

public func createDocument<Document: Encodable>(
  _ document: Document,
  in indexName: String
) throws -> EventLoopFuture<ESCreateDocumentResponse>

It has a generic type, Document, which conforms to Encodable. This lets you store any encodable object.

Furthermore, the method expects the name of the index where you want to store the document. It’ll return a EventLoopFuture<ESCreateDocumentResponse>, which wraps the response body coming from the Elasticsearch API.

Now, it’s time to start coding!

Replace the fatalError() line in createDocument(_:in:) with the following:

// 1
let url = baseURL(path: "/\(indexName)/_doc")
// 2
var request = try ClientRequest(method: .POST, url: url, body: document)
// 3
request.headers.contentType = .json
return try sendRequest(request)

Here’s what this code does:

  1. You declare the URL, which you compose using baseURL, the indexName, which is recipes throughout this tutorial, and _doc, which is the endpoint for dealing with single documents.
  2. Next, you pass both the url and the document into a ClientRequest object with method POST.
  3. Finally, you set the content-type header to application/json and send the request.
Note: In this tutorial, you add the the initializer ClientRequest().init(method:url:headers:body) through an extension. Doing so removes the complexity of JSON encoding the object and writing it into a ByteBuffer.

Look at ClientRequest+ConvenienceInit.swift to learn how it works.

Connecting Elasticsearch to Your API

To test your implementation, you’ll have to create routes and pass everything through them. You’ll implement all your recipe-related routes in Controllers/RecipeFinderController.swift.

First, you need to tell Vapor you want to use this controller as a route collection. Open App/routes.swift and replace the TODO comment in routes(_:) with the following:

let recipeFinderController = RecipeFinderController()
try app.grouped("api", "recipes").register(collection: recipeFinderController)

This initializes your RecipeFinderController and adds it to the app as a RouteCollection.

Next, jump to the definition of register(collection:). Vapor then asks your RecipeFinderController for a list of supported routes in boot(router:):

/// Groups collections of routes together for adding to a router.
public protocol RouteCollection {
    /// Registers routes to the incoming router.
    ///
    /// - parameters:
    ///     - routes: `RoutesBuilder` to register any new routes to.
    func boot(routes: RoutesBuilder) throws
}

extension RoutesBuilder {
    /// Registers all of the routes in the group to this router.
    ///
    /// - parameters:
    ///     - collection: `RouteCollection` to register.
    public func register(collection: RouteCollection) throws {
        try collection.boot(routes: self)
    }
}

Back in RecipeFinderController.swift, add this new method inside the class definition, replacing the first TODO:

// 1
func createHandler(_ req: Request) throws -> EventLoopFuture<Recipe> {
  // 2
  let recipe = try req.content.decode(Recipe.self)
  // 3
  return try req.esClient
    // 4
    .createDocument(recipe, in: "recipes")
    .map { response in
      // 5
      recipe.id = response.id
      return recipe
  }
}

What does this do? Glad you asked! This is the handler for your create recipe requests:

  1. First, it takes a request and returns a Recipe object.
  2. Second, it decodes the Recipe from the Request‘s body
  3. By calling req.esClient, you’ll receive an instance of ElasticsearchClient. This is defined in Request+ElasticsearchClient.swift and follows the new services approach of Vapor 4.
  4. You use your newly-created createDocument(_:in:) to store recipe in Elasticsearch’s recipes index.
  5. Finally, you use the id from the response to set your Recipe object’s id property. You store the document under this id in the recipes index.

There’s just one more step before you can test this. In the same file, replace the TODO in boot(routes:) with the following:

routes.post(use: createHandler)

Here, you add the route POST /api/recipes and connect it to your createHandler(_:).

Now, select the Run scheme:

Select The Run Scheme

Build and run, then fire up the REST client of your choice to create a new recipe. For example, to use cURL, run this command in your terminal:

curl -X "POST" "http://localhost:8080/api/recipes" \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{
  "instructions": "Mix apple with cake.",
  "name": "Apple Cake",
  "ingredients": [
    "Apple",
    "Cake"
  ],
  "description": "As easy as apple pie."
}'

The Xcode console displays the Elasticsearch response:

Response:

HTTP/1.1 201 Created
Location: /recipes/_doc/25TxfXEBLVx2dJNCgFQB
content-type: application/json; charset=UTF-8
content-length: 174

{"_index":"recipes","_type":"_doc","_id":"25TxfXEBLVx2dJNCgFQB","_version":1,"result":"created","_shards":{"total":2,"successful":1,"failed":0},"_seq_no":0,"_primary_term":1}

Your Vapor server returns the recipe, now with an Elasticsearch id, so this appears in the terminal window:

{"ingredients":["Apple","Cake"],"id":"25TxfXEBLVx2dJNCgFQB","name":"Apple Cake","description":"As easy as apple pie.","instructions":"Mix apple with cake."}

If you prefer to use a request client like Insomnia, it looks like this:

A request in Insomnia

As you can see, your recipe has an id now.

Congratulations! You’ve stored your first document in Elasticsearch!

Reading a Recipe

Now that you know the id of the document, you can easily get it from Elasticsearch.

Back in ElasticsearchClient.swift, look at getDocument(id:from:):

public func getDocument<Document: Decodable>(
  id: String,
  from indexName: String
) throws -> EventLoopFuture<ESGetSingleDocumentResponse<Document>>

This method expects the id of the document you want to retrieve and the index name where you stored it. It then returns EventLoopFuture<ESGetSingleDocumentResponse<Document>>, where Document is an object that conforms to Decodable. ESGetSingleDocumentResponse wraps the response from Elasticsearch, including the actual Document.

Now, replace fatalError() in this method with the following:

let url = baseURL(path: "/\(indexName)/_doc/\(id)")
let request = ClientRequest(method: .GET, url: url)
return try sendRequest(request)

This code is similar to what you did earlier to create a document with two differences: This time, you use GET instead of POST, and the URL includes the id of the document.

Next, back in RecipeFinderController.swift, you need to add the handler for your new request and connect it to a route.

Start by adding the following method below createHandler(_:):

func getSingleHandler(_ req: Request) throws -> EventLoopFuture<Recipe> {
  // 1
  guard let id: String = req.parameters.get("id") else { throw Abort(.notFound) }
  return try req.esClient
    // 2
    .getDocument(id: id, from: "recipes")
    .map { (response: ESGetSingleDocumentResponse<Recipe>) in
      // 3
      let recipe = response.source
      recipe.id = response.id
      return recipe
  }
}

In this code, you:

  1. Grab the parameter id from the URL. This will be the recipe id.
  2. Call your new method to get a document from Elasticsearch with this id.
  3. Return the source property of the Elasticsearch response, which is your recipe object, and add the Elasticsearch id.

Now, add this route to your controller’s boot(routes:):

routes.get(":id", use: getSingleHandler)

You’re defining a route GET /api/recipes/:id with a named parameter :id that refers to the id of the recipe you want to fetch.

Build and run, then enter this command in the terminal:

curl "http://localhost:8080/api/recipes/oOnTKm4Bcnc4_Sk4gx4E"

Replace id oOnTKm4Bcnc4_Sk4gx4E with the id of the recipe you created earlier.

And there’s your recipe again!

{"ingredients":["Apple","Cake"],"id":"25TxfXEBLVx2dJNCgFQB","name":"Apple Cake","description":"As easy as apple pie.","instructions":"Mix apple with cake."}
Note: If you’ve restarted Elasticsearch since you created the recipe, the database will be empty. Remember, your Docker configuration doesn’t persist anything. In that case, create a new recipe using your POST /api/recipes endpoint, then work with the id from the response.

In your next step, you’ll learn how to make changes to the recipes you’ve retrieved.

Updating a Recipe

Updating a recipe requires a mixture of what you’ve learned so far. You have to specify both the id of the document you want to update in the URL and the updated recipe in the request body. To update a resource, use the PUT HTTP method.

Note: If a document with the specified id doesn’t exist, Elasticsearch will create the document instead.

Now, in ElasticsearchClient.swift, replace the fatalError() line in updateDocument(_:id:in:) with the following:

let url = baseURL(path: "/\(indexName)/_doc/\(id)")
var request = try ClientRequest(method: .PUT, url: url, body: document)
request.headers.contentType = .json
return try sendRequest(request)

Next, in RecipeFinderController.swift, add this new handler below the previous one:

func updateHandler(_ req: Request) throws -> EventLoopFuture<Recipe> {
  let recipe = try req.content.decode(Recipe.self)
  guard let id: String = req.parameters.get("id") else { throw Abort(.notFound) }
  return try req.esClient.updateDocument(recipe, id: id, in: "recipes").map { response in
    return recipe
  }
}

Finally, add the new route to boot(routes:):

routes.put(":id", use: updateHandler)

You’re now ready to update a recipe with the id. Replace the id 25TxfXEBLVx2dJNCgFQB with your recipe’s id value, then run this command in the terminal:

curl -X "PUT" "http://localhost:8080/api/recipes/25TxfXEBLVx2dJNCgFQB" \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{
  "instructions": "Mix the two apples with one cake.",
  "name": "Apple Cake",
  "ingredients": [
    "Apple",
    "Cake"
  ],
  "description": "As easy as apple pie."
}'

Here, you’re changing the instructions to use two apples. Vapor displays the updated recipe:

{"ingredients":["Apple","Cake"],"name":"Apple Cake","instructions":"Mix the two apples with one cake.","description":"As easy as apple pie."}

Next, you’ll find out how to see all of the documents you keep in an Elasticsearch index.

Getting All the Recipes

To get all the documents of an index in Elasticsearch, use the _search endpoint with no search query parameters. You’ll try this next.

In ElasticsearchClient.swift, look at the getAllDocuments(from:) method signature:

public func getAllDocuments<Document: Decodable>(
  from indexName: String
) throws -> EventLoopFuture<ESGetMultipleDocumentsResponse<Document>>

This time, the response is type ESGetMultipleDocumentsResponse, which contains a list of hits, each representing a ESGetSingleDocumentResponse.

First, replace the fatalError() line in getAllDocuments(from:) with the following:

let url = baseURL(path: "/\(indexName)/_search")
let request = ClientRequest(method: .GET, url: url)
return try sendRequest(request)

This code is similar to what you did earlier for create except you use GET for searching the documents.

You know the drill by now: Add the handler below your previous one in RecipeFinderController.swift:

func getAllHandler(_ req: Request) throws -> EventLoopFuture<[Recipe]> {
  return try req.esClient
    .getAllDocuments(from: "recipes")
    .map { (response: ESGetMultipleDocumentsResponse<Recipe>) in
      return response.hits.hits.map { doc in
        let recipe = doc.source
        recipe.id = doc.id
        return recipe
      }
  }
}

Finally, update boot(routes:) with the new route:

routes.get(use: getAllHandler)

Build and run, then call GET /api/recipes/ via cURL:

curl "http://localhost:8080/api/recipes"

As a result, you’ll receive an array of all the recipes you’ve stored in Elasticsearch:

[{"ingredients":["Apple","Cake"],"id":"3JT0fXEBLVx2dJNCr1QE","name":"Apple Cake","description":"As easy as apple pie.","instructions":"Mix apple with cake."},
{"ingredients":["Apple","Cake"],"id":"25TxfXEBLVx2dJNCgFQB","name":"Apple Cake","description":"As easy as apple pie.","instructions":"Mix the two apples with one cake."}]

These results include one recipe created with Insomnia and one created and updated with cURL.

So, what if you want to get rid of a recipe? You’ll find out how to delete unnecessary entries next.

Deleting a Recipe

To delete a recipe, use DELETE with the same URL you used to update and read a resource.

First, in ElasticsearchClient.swift, replace fatalError() in deleteDocument(id:from:) with the following:

let url = baseURL(path: "/\(indexName)/_doc/\(id)")
let request = ClientRequest(method: .DELETE, url: url)
return try sendRequest(request)

This code is similar to what you did earlier to create a document with two differences: This time, you use DELETE instead of POST, and the URL includes the id of the document to delete.

Next, in RecipeFinderController.swift, add the handler:

func deleteHandler(_ req: Request) throws -> EventLoopFuture<HTTPStatus> {
  guard let id: String = req.parameters.get("id") else { throw Abort(.notFound) }
  return try req.esClient.deleteDocument(id: id, from: "recipes").map { response in
    return .ok
  }
}

Add the route to boot(routes:):

routes.delete(":id", use: deleteHandler)

Now, you can delete a recipe with DELETE:

curl -X "DELETE" "http://localhost:8080/api/recipes/25TxfXEBLVx2dJNCgFQB"

Remember to replace the ID with the ID of your recipe. To be sure it worked, use your get all command to check that the recipe is gone:

curl "http://localhost:8080/api/recipes"

Sure enough, there’s only one recipe now:

[{"ingredients":["Apple","Cake"],"id":"3JT0fXEBLVx2dJNCr1QE","name":"Apple Cake","description":"As easy as apple pie.","instructions":"Mix apple with cake."}]

Congrats! You’ve made it through CRUD. Now, you can focus on the most interesting part of Elasticsearch: search.

Searching Recipes

Elasticsearch provides you with two options to search documents:

  1. URI Search: A simple search using URI query parameters. You’ll focus on this option for this tutorial.
  2. Query DSL: This option allows for complex and powerful searches using Elasticsearch’s Query DSL syntax. This tutorial doesn’t cover Query DSL because it could easily fill its own tutorial.
  3. In this section, you’ll add a simple search to the recipe finder. It will look for the search term in name, description and ingredients. It will also allow partial searches. For example, a search for app or pple will return apple recipes.

    Start by going to ElasticsearchClient.swift and replacing fatalError() in searchDocuments(from:searchTerm:) with the following:

    let url = baseURL(
      path: "/\(indexName)/_search",
      queryItems: [URLQueryItem(name: "q", value: searchTerm)]
    )
    let request = ClientRequest(method: .GET, url: url)
    return try sendRequest(request)
    

    The endpoint for searching documents is under {indexName}/_search.

    You’ll need to provide a query string to the parameter q. In the next section, you’ll see how to construct that query string.

    Creating a Query String

    The query string parameter has its own syntax, which lets you customize the search and filter field names. It allows you to use wildcards and regular expressions.

    Note: If you don’t provide any query parameters, you’ll receive all documents in an index, as you saw when you created getAllDocuments(from:).

    Here are some relevant examples and operators:

  • Single term: name:cake finds results that have cake in the name field.
  • Multiple terms: name:(cake OR pie) finds results that have cake or pie in name.
  • Exact match: name:"apple cake" finds results that have apple cake in name.
  • Fuzziness: name:apple~ finds results that have apple in name. It works even for misspelled queries, like appel.
  • Wildcard: name:app* finds results that have words starting with app in name.
  • Multiple fields: (name:cake AND ingredients:apple) finds results that have cake in name and apple in ingredients.
  • Boosting: (name:cake^2 OR ingredients:apple) does the same as Multiple terms, but hits in name are boosted and are more relevant.
Note: The search ignores case, treating uppercase and lowercase letters the same.

A full description of the query string syntax is available at Elasticsearch’s query string syntax reference page.

Now that you understand your options, you’re ready to construct your searchTerm. In RecipeFinderController.swift, add this method below deleteHandler(_:):

func searchHandler(_ req: Request) throws -> EventLoopFuture<[Recipe]> {
  // 1
  guard let term = req.query[String.self, at: "term"] else {
    throw Abort(.badRequest, reason: "`term` is mandatory")
  }
  // 2
  let searchTerm = "(name:*\(term)* OR description:*\(term)* OR ingredients:*\(term)*)"
  // 3
  return try req.esClient
    .searchDocuments(from: "recipes", searchTerm: searchTerm)
    .map { (response: ESGetMultipleDocumentsResponse<Recipe>) in
      return response.hits.hits.map { doc in
        let recipe = doc.source
        recipe.id = doc.id
        return recipe
      }
  }
}

Here, you:

  1. Make sure the user provided a term.
  2. Compose searchTerm using the query string syntax to find searchTerm in name, description or ingredients, and allowing for partial matches.
  3. Use the same implementation as in getAllHandler(_:).

Next, add the new route to boot(routes:):

routes.get("search", use: searchHandler)

That’s it! Your search should be ready now, but to be sure, you’ll test it in the next section.

Testing Search

Build and run. Before testing search, run this command in Terminal to add another recipe:

curl -X "POST" "http://localhost:8080/api/recipes" \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{
  "name": "Banana Pie",
  "ingredients": [
    "Banana",
    "Sugar",
    "Milk",
    "Vanilla",
    "Egg yolks",
    "Baked pastry shell"
  ],
  "description": "If you like bananas, this is for you",
  "instructions": "Mix all the ingredients. Enjoy!"
}'

Your Vapor server returns your new recipe, along with its Elasticsearch id:

{"ingredients":["Banana","Sugar","Milk","Vanilla","Egg yolks","Baked pastry shell"],"id":"3ZQafnEBLVx2dJNClFTE","name":"Banana Pie","description":"If you like bananas, this is for you","instructions":"Mix all the ingredients. Enjoy!"}

Now that you have two recipes in your database, run this command to test if a partial search results in a tasty apple cake:

curl "http://localhost:8080/api/recipes/search?term=app"

Here’s what this search finds:

[{"ingredients":["Apple","Cake"],"id":"3JT0fXEBLVx2dJNCr1QE","name":"Apple Cake","description":"As easy as apple pie.","instructions":"Mix apple with cake."}]

Great, it found your apple cake! But there’s a reason why you added a banana pie recipe.

Run this command to search for pies:

curl "http://localhost:8080/api/recipes/search?term=pie"

The search returns both recipes:

[{"ingredients":["Apple","Cake"],"id":"3JT0fXEBLVx2dJNCr1QE","name":"Apple Cake","description":"As easy as apple pie.","instructions":"Mix apple with cake."},
{"ingredients":["Banana","Sugar","Milk","Vanilla","Egg yolks","Baked pastry shell"],"id":"3ZQafnEBLVx2dJNClFTE","name":"Banana Pie","description":"If you like bananas, this is for you","instructions":"Mix all the ingredients. Enjoy!"}]

As you can see, it returns two results: the apple cake in the first position and the banana pie in the second. This happens because both recipes contain the term pie in their data. It’s in the apple cake’s description and in the banana pie’s name and description.

As you see, it’s a good idea to prioritize hits in the name and ingredients fields over matching terms in the description field.

Adding Weighted Search

To make results in name and ingredients more relevant, your next step is to add the boost operator, ^, to your searchTerm.

In RecipeFinderController.swift, replace let searchTerm = "(name:*\(term)* OR description:*\(term)* OR ingredients:*\(term)*)" in searchHandler(_:) with the following:

let searchTerm = "(name:*\(term)*^5 OR description:*\(term)* OR ingredients:*\(term)*^2)"

This boosts any matches in name by five and in ingredients by two.

Now build and run again, then retry the search for pie:

curl "http://localhost:8080/api/recipes/search?term=pie"

This time, the results put the banana pie recipe first, because the search term in the name banana pie weighs more than the search term in the description, “As easy as apple pie“.

[{"ingredients":["Banana","Sugar","Milk","Vanilla","Egg yolks","Baked pastry shell"],"id":"3ZQafnEBLVx2dJNClFTE","name":"Banana Pie","description":"If you like bananas, this is for you","instructions":"Mix all the ingredients. Enjoy!"},
{"ingredients":["Apple","Cake"],"id":"3JT0fXEBLVx2dJNCr1QE","name":"Apple Cake","description":"As easy as apple pie.","instructions":"Mix apple with cake."}]

Nice work — you did it! But before you go off to enjoy some delicious recipes, take a moment to clean up.

Cleaning Up

Stop your Xcode project to stop the Vapor server.

To stop the Elasticsearch container and remove it from your system, run these commands in Terminal:

docker stop elasticsearch
docker rm $(docker ps -a -q -f status=exited)

You named the container when you started it, which makes it easier to stop it — you don’t have to search for its ID. The second command finds all exited container processes and removes them.

To remove the Elasticsearch image, first find its IMAGE ID:

docker images

Your output will look similar to this:

docker.elastic.co/elasticsearch/elasticsearch   7.6.2   f29a1ee41030  ...

Then use its IMAGE ID to remove it. Be sure to replace the IMAGE ID with your own:

docker rmi f29a1ee41030

Congratulations! You’ve learned how to set up a search with Elasicsearch in Vapor and how to clean it up when you’re done.

Where to Go From Here?

You can download the completed Recipe Finder Web API project using the Download Materials button at the top or bottom of this page.

In the completed project, you’ll find some implementations that this tutorial doesn’t cover. Instructions are in the project’s README.md file:

  • DeleteIndexCommand: Useful to delete your index.
  • ImportRecipesCommand: An example of how to import data into Elasticsearch via a command. If you deal with a lot of data in production, you should have a look at the Elasticsearch Bulk API instead.

Learn more about Elasticsearch by reviewing its comprehensive documentation at Elasticsearch Reference.

I hope you enjoyed this tutorial. If you have any questions or feedback, please join the forum discussion below.

Average Rating

4.5/5

Add a rating for this content

2 ratings

More like this

Contributors

Comments