Rust: Adding Search functionality to website using Yew

Pudding Entertainment
7 min readJan 14, 2024
Generated by Bing Image Creator

In the ever-evolving landscape of web development, choosing the right tools can make all the difference. Rust, renowned for its performance, safety, and concurrency, emerges as a powerhouse in this domain. Its unique blend of low-level control and modern language features has made it a favourite among developers seeking efficiency without compromising on safety.

In this tutorial, with the help of Rust, Yew, Actix Web, and Sqlx we’ll add an essential piece any website will need at some point — search functionality.

Prerequisites

I’m using Rust version 1.75 and Yew version 0.21. This tutorial assumes prior knowledge of Rust and Yew and focuses more on practical usage of the language and the framework.

I’ll be using the project I’m actively working on at the moment — illuvi-analytics.com. The relevant parts of it will be stripped into self-sufficient code snippets, and you are welcome to check the entire project out.

If you are not yet familiar with Yew, take a look at one of my previous articles — Rust: Building Web Frontend with Yew

There are 3 parts in this tutorial: Problem statement, Backend and Frontend

Background

If you’re intrigued by the project and the broader scope of web3 technologies, I recommend exploring the Background section in one of my previous articles — Rust: How to consume REST API. This will give you a solid foundation in the concepts we’re building upon.

While I employ a domain-specific data model in this tutorial, the concepts and techniques discussed are universal. Therefore, you can seamlessly adapt and apply this approach to your projects, regardless of your specific interest or expertise in the finer details of the domain in question.

Part 1. Problem statement

At first glance, the task seems simple: add a search box to the website, ideally in the header, allowing users to input their queries. However, the simplicity is deceptive, as deeper exploration reveals numerous edge cases that require careful consideration.

Frontend considerations
There are several critical design decisions to make that will define the overall user experience of the search functionality, making it user-friendly and visually appealing.

First, consider the interaction style of the search box. Will it trigger a search automatically as the user types, or will it require pressing “Enter”? This decision impacts the immediacy of the user’s search experience.

Next, decide how to present the search results. Options include displaying them in a popup window, on a dedicated results page, or a combination of both. This choice significantly influences the user’s navigation flow and ease of accessing the information.

Lastly, determining the number of results to fetch and display is crucial. This involves balancing the need to provide comprehensive information with maintaining a clean and uncluttered interface.

In my specific use case, I opted for a dynamic approach. The search box activates as the user begins typing, displaying results in a pop-up. This method offers immediate feedback and a seamless user experience, keeping the user engaged without navigating away from their current page.

Backend considerations
The backend receives an arbitrary search string, which must be safely used for querying multiple database fields. This scenario raises immediate concerns about SQL injection attacks and the need for robust database protection. My experience with SQLX in Rust has shown its effectiveness in guarding against such vulnerabilities, primarily through its support for prepared statements. For more details, refer to the SQLX documentation.

Speed and efficiency are paramount in search functionality. Initially, optimizing the search query is crucial. In databases like Postgres and MySQL, utilizing EXPLAIN ANALYZE with your query helps identify execution bottlenecks and areas for improvement. Strategies might include adding indexes to search fields, separating certain fields into distinct columns (particularly with JSON data), or creating views or materialized views. Depending on your specific requirements, you might also consider parallelizing queries, introducing a caching layer or even reevaluating your choice of database technology to better suit your needs.

Having outlined the key considerations for both the backend and frontend, we now have a solid foundation to begin the development phase. Let’s dive into the development process, starting with the backend part.

Part 2. Backend

We begin by defining a controller for our search functionality using actix-web. As we discussed previously, we will have a single search value sent from the frontend, which will be defined in a struct:

An important aspect of our backend is the use of a database pool, rather than opening and closing connections for each request. You can see it being passed as a parameter in the get_search_results method. This approach, implemented in the main method, significantly enhances query performance and reduces database overhead:

The create_pool function configures the database connection with essential parameters like host, port, and credentials. You might tweak many other parameters here as well:

You might recognize that env_utils from my previous tutorials:

Next, let’s create a database querying service:

In my case here the search works on two fields — integer token_id and json metadata with a field called name in it.

A noteworthy aspect of our query implementation is the concatenation technique used in wildcard searches (‘%’ || $1 || ‘%’). This approach is required due to the way parameter binding works.

A small but significant optimization is applied to the token_id field. Recognizing that token_id is a numeric field, we first verify the search input as a number before initiating a more resource-intensive wildcard search.

There were a few other improvements considered, but eventually discarded:

  • Creating an index on the metadata.name field did not result in notable performance improvements and was therefore set aside.
  • Extracting metadata.name into its own column and using a database trigger for updates. While this approach did reduce the response time for negative searches from around 3 seconds to under 2 seconds, it was not implemented due to its limited necessity for my website.
  • Implementing a caching read-through layer was also explored. However, given its complexity it was an excessive measure at this point. It remains an area of interest for future enhancements though.

Finally, the data models SearchData and AssetContentData are defined. These models structure the data sent from the backend to the frontend, ensuring a consistent and predictable format for the search results. The file itself will be defined in a shared module used by both frontend and backend:

With that the backend part is now complete and we go ahead and implement the search box on the frontend.

Part 3. Frontend

In this section we’re combining the capabilities of Yew and Bootstrap 5 to build the search interface. While Yew is a robust framework, it’s important to note that unfortunately it often requires verbose code, particularly in complex scenarios like this one where there are multiple states to manage.

The frontend implementation can be logically divided into three main tasks:

  1. Handling user input in the search box
    Our first challenge is to efficiently handle user input in the search box. Unlike the straightforward approach in JavaScript, Yew requires a more nuanced method. To prevent sending each keystroke to the backend immediately, we’ll utilize the gloo-timers crate for timeout functionality.
  2. Querying the backend and receiving results
    After capturing the user’s input, it’s sent to the backend. This process involves using the reqwest crate to manage the data transfer.
  3. Displaying the search results
    The final step involves presenting the results to the user. For an optimal user experience, we’ll display the results in a dropdown view. During the data retrieval process, a spinner will indicate loading to the user, maintaining engagement. While the creation of the spinner is outside the scope of this tutorial, its presence is crucial for a seamless user experience.

The end result Search function component will look like this:

As you can see in the code above there are 4 states defined:

  • search: Captures the user’s input.
  • search_data: Holds data retrieved from the backend.
  • typed_state: Indicates whether the user has entered any input.
  • timeout_state: Manages the timing to send a backend request, ensuring it’s triggered only after the user stops typing.

To enhance user experience, five distinct actions (callbacks) were implemented:

  • oninput: Manages user input. This action uses the gloo-timers crate’s Timeout struct to delay updating the search state which is used as a dependency in use_effect_with dependency therefore delaying the backend request
  • onsubmit: Prevents page reload on form submission (Enter key press) within an HTML form context.
  • onfocusout: Automatically closes the popup when the user clicks outside the search.
  • onfocusin: Reopens the popup when the user clicks back into the search box.
  • onclick: Resets the state upon clicking a search result, typically leading to a redirect to the relevant page.

The verbosity in our Yew implementation arises primarily from the need for each callback to clone the state, resulting in numerous cloned objects. The rest of the code follows standard Yew practices, enhanced with Bootstrap 5 classes for aesthetic appeal.

For those interested in exploring the complete project, the code is available on GitHub. While some parts are specific to my project, the general principles and techniques are applicable across various Yew applications.

The final result will look like this:

Afterwards

Yew, despite being a newer player in the web framework arena, is fully capable of powering dynamic and functional websites with Rust’s robustness. This journey through creating a search functionality showcases Yew’s potential in real-world applications. It demonstrates that this framework can be used to build complex features with the efficiency and safety that Rust offers. I believe that with time it will mature and become the next big thing in web development!

Do check out my Yew-powered website https://illuvi-analytics.com

Support

If you like the content you read and want to support the author — thank you very much!
Here is my Ethereum wallet for tips:
0xB34C2BcE674104a7ca1ECEbF76d21fE1099132F0

--

--

Pudding Entertainment

Serious software engineer with everlasting passion for GameDev. Dreaming of next big project. https://pudding.pro