This is the second post in my series about building RockOn. For more background and to read about how I seeded my database head here.
A key part of RockOn's user experience (and why I went through the effort of seeding my own database) is the ability for a user to easily find a rock climb. Ideally, this means that a user can type in part of a route name, forgetting the articles and misspelling the main content, and still be returned the route they were looking for. I also wanted a user to be able to search by route name or area name. The area search presented a lot more challenges from a UX perspective, so I began by just building the search by route name.
Building the full text search myself was outside the scope of this project, but luckily other people have solved this problem already and made their solutions freely available on the internet! After a bit of research, I chose to use the gem Ransack for its popularity and ease of implementation.
Using Ransack
As usual it's easy to get started with the Ransack gem. In your gemfile, add gem 'ransack'
and run bundle install
. The search I wanted to implement was very straightforward: search the 'name' column in the 'climbs' table with the query parameter and return the results. This is very similar to the first example in the Ransack documentation:
def index
@q = Person.ransack(params[:q])
@people = @q.result(distinct: true)
end
where :q
is the search term. To search just the 'name' column, I used one of Ransack's search matcher predicates (also outlined in the docs):
I called another method on the returned search object to limit the total returned climbs, and then returned a JSON object as usual. Here's the whole five lines of code:
def index
@q = Climb.ransack({name_i_cont: params[:q]})
climbs = @q.result(distinct: true).limit(8)
render json: {climbs: climbs}
end
Overall very easy to implement, and Ransack provides great documentation for more complex searching as well.
Ransack Behind the Scenes
What is Ransack actually doing when I call it on my model? More specifically, what are the SQL statements that are executed?
By enabling the ActiveRecord logger in the Rails console, I can see the SQL statements that are run by Ransack. In this case, it's pretty simple. Running these Ransack methods
@q = Climb.ransack({name_i_cont: "nose"})
@q.result
generates this SQL:
SELECT "climbs".* FROM "climbs" WHERE "climbs"."name" ILIKE '%nose%' LIMIT $1
The key to Ransack's search here is using SQL's ILIKE
operator which matches on similar values rather than exact ones. ILIKE
versus LIKE
is just case insensitive. The %
surrounding my search query "nose"
is a wildcard and represents any character or set of characters.
The Query Parameter
Now that my back end search functionality was taken care of, I needed to write the front end API call that passed along my search query. In this case I was making a GET request, so my query parameter would be in the url. For this method it was useful to use Javascript's URL object that gave me access to methods that help build and validate urls. Here's the code I used to build my url, where route
is the name of route entered by the user:
const url = new URL(`${API_ROOT}/climbs`);
url.searchParams.append("q", route);
This is equivalent to writing
const url = `${API_ROOT}/climbs?q=${route}`;
but using the URL methods helps avoid syntax errors and validates the url. With that, I just made a simple fetch request.
Putting it All Together
With one gem and a couple of functions, I was able to give my users a pretty easy search experience.
There are certainly improvements that can be made, such as handling misspelled words and providing a 'See More' option if the climb of choice isn't one of the first eight results. But so far I've found that armed with the general idea of the route name, my search will usually return exactly what I want.
Next week I'll go through the challenges of searching by area and some of the methods I built for handling my hierarchical area structure.
Thanks for reading!
Top comments (0)