RockOn pt 2: Searching with Ransack
Tags: ruby, rails
Published: 2020-07-03 17:48:33

*This is the second post in my series about building [RockOn](https://rockonapp.com). For more background and to read about how I seeded my database head [here](https://dev.to/dianakw8591/rockon-pt-1-using-nokogiri-to-seed-my-database-2h05).*

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](https://github.com/activerecord-hackery/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:
```ruby
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):
![Ransack documentation for case insensitive search](https://dev-to-uploads.s3.amazonaws.com/i/2o6fwzn8ykm3ffw4lp8h.png)
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:
```ruby
  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
```ruby
@q = Climb.ransack({name_i_cont: "nose"})
@q.result
```
generates this SQL:
```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:
```js
const url = new URL(`${API_ROOT}/climbs`);
url.searchParams.append("q", route);
```
This is equivalent to writing 
```js
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.

![GIF of user searching for 'nose'](https://dev-to-uploads.s3.amazonaws.com/i/i0k00lkev3zbtygm50be.gif)
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!