Google Books API & Search Filters In Rails: How I learned to Love REGEX and “.send”

David Ryan Morphew
6 min readFeb 27, 2021
Library—The Analog Version

I didn’t plan to build search features for my Books & Banter Rails project, especially with the Google Books API , but…laziness.

Laziness is a mother of invention—along with her sister, necessity, of course.

Why conjure up a bunch of book data to build a library, when you can just harvest it from Google Books?

In a previous blog, I went over the basic setup for using the Google Books API . This time, I added a check to see if the data was valid before instantiating a new book instance:

...
books_array.map do |book_hash|
book_assignment_hash = {}
volume_info = book_hash["volumeInfo"]
book_assignment_hash[:authors] = volume_info["authors"].join(", ")
...
if Book.new(book_assignment_hash).valid?
Book.new(book_assignment_hash)
end
...

I also added validations in my Book model to weed out entries missing important data I wanted:

validates :authors, :title, :description, :publisher, :publication_date, :categories, :isbn, presence: true

The ISBN Issue

I decided to use the ISBN for a search feature later in my project (you can find a particular book with the Google Books API query using “isbn:<isbn-10 or -13>”). So, that data needed to be easily accessible and usable.

The problem: if you harvest books using a different search term, like “subject:fiction,” you can get many entries with alternative identifiers, like this: “OSU:32435022414775,” which is not something you can easily use to find that book with the Google Books API. So, for my project, I decided only to validate books with a proper ISBN-10 or -13. (Go here for other Google Books query options.)

The Fix

In the Google Books API, all non-ISBN identifiers prepend their alternative type before the number (the “OSU” in “OSU:32435022414775”). You might think that just looking for non-digit characters would weed that out, but some ISBN-10s have an “X” as the last character. So, I rejected any identifier that included non-digit characters other than an “X” at the end using this REGEX evaluation (which I tested with Rubular):

.match(/\D[^\d][^X]/)

That solved that problem. And, to make sure that my user didn’t add unconventional ISBN formats of their own, I ran a custom validation to make sure that the ISBN was 13-digits, 10-digits, or 9-digits-followed-by-X (for ISBN-10s that end in X):

 isbn.match(/(\A\d{9}[X]\z|\A\d{10}\z|\A\d{13}\z)/)

Creating A Search Feature with Non-Persisted Data

For my project, I wanted the library admin to be able to add new books to the application’s library. Again, for the admin’s convenience (i.e. for their laziness), I wanted to use Google Books to allow them to just search for a book and add it to the library without any need to manually enter data.

So, I had the admin submit any combination of “author,” “title,” and/or “subject” to search the Google Books API for results.

Book New Form for Admin Google Books API Search

(You have to format the queries for the API, but then you should get results if they match.)

Currently, only 1 result is returned, but I built the search feature for future functionality with multiple results.

Search Results for an admin’s search for author: Dostoevsky, title: brothers karamazov. Add Book to Library? Or Start Over?

This search result data is not persisted yet, since I did not want to add every search result to the database and then delete unwanted results.

So, if the admin likes a result and wants to add it to the library, the books#create controller will grab the ISBN of the selected search result, and run a Google Books API query with that ISBN number and save it.

(This refetching of the book with the ISBN number is built for when the feature returns multiple results and the admin only wants one or two of the books out of those results. Only the desired books are saved.)

Search and Order Filters with “.send”

I also wanted to allow users to search through the current list of persisted books (the library) with wildcard queries for “author,” “title,” and “description,” and I wanted them to be able to filter the results ordered by “most recently added,” “most recently published,” OR “highest rated” (with a radio-button).

Library Books Search and Order Filters Form: Search author, title, description; Order by Most Recently Added, Most Recently Published, Highest Rated

The Filters Issue

To filter and order the results, I wanted to chain together all of the methods that a user uses (any combination of search terms with or without 1 ordering filter). But, how do you chain together different combinations of search queries with an ordering method in one go?

The Solution

I decided to use the Ruby “.send” method.

Search Terms with “.send”

I set the search <method_names> as the names of params keys that get sent through the search request so that I could use “.send” to use them.

Form for Setting Params hash:

<%= form_tag books_path, method: :get do %>
by Author: <%= text_field_tag :search_author_name %><br>
by Title: <%= text_field_tag :search_title %><br>
by Description: <%= text_field_tag :search_description %><br>
...

Search Methods:

Book.rbdef self.search_author_name(author_query)
Book.where("authors LIKE ?", "%#{author_query}%")
end
def self.search_title(title_query)
Book.where("title LIKE ?", "%#{title_query}%")
end
def self.search_description(description_query)
Book.where("description LIKE ?", "%#{description_query}%")
end

EXAMPLE: If someone submits the name “asimov” in “Search: by author,” then the params hash will have (among other things) a hash with the <method_name> (“search_author”) as the key and the <search term> (“asimov”) as its value:

{"search_author_name"=>"asimov", ...}

To get the method working, you call the method on the Book class and use the “.send” method to turn the params key into a usable method, with the value of the params key passed in as the argument to that method:

search_params = params.keys.select { |k| k == "search_author_name" }search_params.each do |k, v|
Book.send("#{k}=", v)
end

When “asimov” is entered into the search field, this will run the method:

Book.search_author_name("asimov")

Ordering with “.send”

Next, for the radio-button of ordering filters, I set “ordering_filter” as the name of the params key and the <method name> of the ordering method as the value that gets sent through the params hash.

Form for Setting Params hash (cont’d):

<%= form_tag books_path, method: :get do %>
...
<%= radio_button_tag :ordering_filter, :most_recently_added %>
<%= radio_button_tag :ordering_filter, :most_recently_published %>
<%= radio_button_tag :ordering_filter, :ordered_by_aggregate_ratings %>

Search Methods:

Book.rbdef self.most_recently_added
self.order(created_at: :desc)
end
def self.most_recently_published
self.order(publication_date: :desc)
end
def self.ordered_by_aggregate_ratings
self.has_reviews.sort_by {|book|
book.aggregate_book_rating}.reverse
end

To run one of these methods with “.send,” you have to use the value as the method_name:

order_params = params.keys.select { |k| k == "ordering_filter" }order_params.each do |k, v|
Book.send("#{v}")
end

When “Order: by Most Recently Published” is selected, this will run the method:

Book.most_recently_published

Chaining all of the Methods Together

To chain optional searches and an optional ordering filter together, I then created a search_keys_filter method to select any search terms and filters that were submitted in the form:

def search_keys_selection

search_keys = ["search_author_name", "search_title",
"search_description", "ordering_filter"]
selected_keys = search_keys.select do |key|
!params["#{key}"].blank?
end
search_filters_hash = {}

selected_keys.each do |key|
search_filters_hash[key] = params["#{key}"]
end

search_filters_hash
end

Here, we collect all of the params key-value pairs where a search term was submitted and an ordering filter selected.

Next, we combine the “.send” approaches for the search terms and ordering filter:

def search_filter_chaining_method(search_filters_hash)

result = Book

search_filters_hash.each do |key, value|
key == "ordering_filter" ? result = result.send("#{value}") :
result = result.send("#{key}", value)
end

result
end

Here, we take each selected search or filter and evaluate whether it is (a) a search term, which sends the key as the <method_name> and the value as the argument, or (b) an ordering filter, which just sends the value as the <method_name>. This is accomplished with the ternary operator.

The chaining then occurs as the “.each” iteration adds methods to the result, which started out as “Book.”

So, if you submit a search like this:

Search: author: “asimov”, title: “robot”; Order: by Most Recently Published

It will run the equivalent of:

Book.search_author_name("asimov").search_title("robot").most_recently_published

Click here to see a short demo of the Books and Banter app.

--

--

David Ryan Morphew

I’m very excited to start a new career in Software Engineering. I love the languages, frameworks, and libraries I’ve already learned / worked with (Ruby, Rails,