Friday, August 7, 2009

Ramaze, Sequel, and Search

If you've been following my emails on the Ramaze and Sequel lists, you've probably realized that I've been working on a Ramaze application for a library. This project came about when our company decided that we needed a library and we started looking for software. There wasn't anything that really met our needs so I decided to take a stab at writing it myself. It's a pretty good sized application now and still not "finished", but I thought in these next few posts, I'd lay out some of the things that I've learned while working on it in smaller pieces than the whole application.

The first thing I'd like to show is the search function. This is much more simple than I thought it would be. What we'll do is create some books with titles and descriptions. We'll add a page that allows the user to input some search terms and then another page that displays these results. The results will be based on whether any of the search terms are found in either the title or the description.

First let's create the database with our migration. Here's the code for this. Note that we're adding the data as well as creating the database so we don't have to create the data using another page.

# dbMigration/001_SearchMigration.rb
#
# This is the "new" way to do migrations by using the Class.new form.
# It means that a new class name is not required and so there is no chance
# of creating a second class in the migration files that is the same as the
# first causing problems.
Class.new(Sequel::Migration) do
def up

create_table(:books) do
primary_key :id
String :title
String :description
end

from(:books).insert(:title => 'Programming Ruby', :description => 'A great Ruby book')
from(:books).insert(:title => 'Agile Web Developement with Rails', :description => 'A book about Ruby on Rails')
from(:books).insert(:title => 'Cryptonomicon', :description => 'A book about a Unix sys admin')
from(:books).insert(:title => 'The C Programming Language', :description => 'A book about programming in C')

end

def down
drop_table(:books)
end
end


We've seen this before. We create the table called "books" (note the plural) with text columns for the title and the description. Our down method, just drops the table. In the middle we add four books with their titles and descriptions.

Next let's look at our models. Jeremy Evans, the Sequel maintainer/guru, recommends having a single models.rb in our models directory to make it easier to use irb to test things. I've taken to doing this and it works quite well. Here our model is very simple (empty). We name the model Book (singular of the table books) and derive it from Sequel::Model. This will give us access to books table. Here's the actual code:

#
# This is the model for the book and is backed by the :book table in the
# database.
#
# Create the Book model.
class Book < Sequel::Model
end


The controller, controllers/main_controller.rb is also quite simple. It has an index method that only sets the title and a second method search_results that "calculate" the results and save them to the @search_response variable for use in the view/search.xhtml view. We also go ahead and save the @search_string so we can display that we can display that also. The last line, that calculates the search_response, probably needs a bit of explanation.

We're going use the "grep" method on the Book dataset. We will pass an array with :title and :description to let the method know which columns of Book we're interested in. The next piece, we take the search_string and split it into an array. We then "map" the array generating something that will look like:

%ruby % %rails %

for a search string of "ruby rails". You can read about the grep function in a Sequel dataset here.

Here's the controller code:

# controllers/main_controller.rb
#
# The mainController has a two methods index and search_results. First map to /
# for the view. Next, set the layout to page so that we use the
# layout/page.xhtml for our layout.
class MainController < Ramaze::Controller

# The Admin controller will be accessed using "admin" as in:
# http://localhost:7000/admin.
map '/'

# Use page.xhtml in the layout directory for layout
layout :page

# You can access it now with http://localhost:7000/
def index
@title = "Search Example"
end

# Calculate and display the search results.
def search_results
@title = "Search Example - Results"

# Grab the search_string from the request hash. This is generated when the user inputs
# something into the Search box in the view/index.xhtml file.
@search_string = request[:search]
@search_response = Book.grep([:title, :description], @search_string.split.map{|x| "%#{x} %"}).all
end
end


The search.xhtml contains the form to type in the search term(s) and submit it. On the form we use the action attribute to send the results to the search_results method in the controller. We use a "get" method so the results are passed on the URL (allowing bookmarking as Gavin pointed out when I asked how to do this incorrectly in a Ramaze thread. I had he and Clive steer me in the right direction though). The URL will look something like http://localhost:7000/search_results?search=ruby+rails when you're searching type "ruby rails" in the text box. Here's the view/index.xhtml:

<!-- view/index.xhtml -->
<!-- Create the form for the search. We're going to set the action to
search_results so that method will get called when the form is
submitted and we'll use a "get" method to pass the parameters in
the URL.
-->

<form id="search" action="search_results" method="get">
<fieldset>
<legend> Search </legend>
<div>
<!-- for= goes with id=, the name= is placed in the request variable. -->

<!-- Input for the title. -->
<label for="search">Search:</label>
<input id="search" name="search" type="text" />
<br/>

<!-- Submit the edited book values. -->
<input type="submit" value="Search" />
</div>
</fieldset>
</form>


The page to display the search response, view/search_results.xhtml, is also pretty simple. It takes the results saved in @search_response by the search_results method in the controller, loops through them and displays each of the books and their descriptions. Here's the code:

#{flashbox}
<br/>
<?r if @search_response.each && @search_response.size > 0 ?>
Results found for "#{@search_string}." <br/><br/>
<?r @search_response.each do | book | ?>
Title: #{book.title}
<br/>
Description: #{book.description}
<br/><br/>
<?r end ?>
<?r else ?>
No results found for "#{@search_string}".
<?r end ?>
<br/>


Finally, here's the layout (layout/page.xhtml):

<!-- view/page.xhtml -->

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<!-- Use the page.css in the public directory and set title based on
what's set in the associated method.
-->

<link rel="stylesheet" type="text/css" href="/page.css"/>

<title>#@title</title>
</head>
<body>
<div id="header">
<h1>Search Example</h1>
</div>

<div id="content">
<!-- Display the actual content. This will come from the method or the
associated view/*.xhtml file
-->

#@content
</div>

<!-- Set the footer in the center of the screen. -->
<div id="footer" style="text-align: center;">
<h5> Powered by Ramaze </h5>
</div>
</body>
</html>


and the CSS file (not formatted):

/* Header CSS */
#header {
background:#9DA9EE;
color: white;
margin-bottom: 0;
padding: 0.25em;
}

/* Footer CSS */
#footer {
background:#9DA9EE;
color: black;
}

All told, pretty simple and easy after someone points you in the right direction anyway.

As always, let me know if you have questions in the comments and I'll do my best to answer them.

1 comment: