Friday, May 1, 2009

Reversing Sort Order

‹prev | My Chain | next›

With the app able to sort couchdb-lucene results in ascending order, it is time to get descending order working. Descending order is only available after the user has previously sorted in ascending order—if the results are currently sorted by title in ascending order, then clicking the title column header again should sort in the opposite direction.

This means that the sort_link helper needs access to the current state of the search results. This gives sort_link an arity of four, requiring: the text of the link, the field on which we are sorting, the query, and, now, the current couchdb-lucene results. My preference would be to cut down on the arity—either deriving the sort field based on the link text or pulling the query from the current results. The former is probably not a good idea—too much coupling. The latter is not an option because the query represented in the results set has stemming applied. If I search on "berries", the results set will represent the search as "berri" (so that recipes containing either "berry" or "berries" can be matched).

So I will stick with an arity of 4. After updating my existing sort_link specs to supply a fourth argument, it is time to get sort_link reversing the order.

As discovered the other day, applying sort to couchdb-lucene adds a sort attribute to the search result set. For ascending order, the results set will include something like:
 "sort_order":[{"field":"sort_date","reverse":false,"type":"string"},
{"reverse":false,"type":"doc"}]
For descending order, the results set will look like:
 "sort_order":[{"field":"sort_date","reverse":true,"type":"string"},
{"reverse":false,"type":"doc"}]
The examples that use results sets with these attributes are as follows:
  it "should link in descending order if already sorted on the sort field in ascending order" do
results = {
"sort_order" => [{ "field" => "sort_foo",
"reverse" => false }]
}
sort_link("Foo", "sort_foo", "query", results).
should have_selector("a",
:href => "/recipes/search?q=query&sort=\\sort_foo")
end

it "should link in ascending order if already sorted on the sort field in descending order" do
results = {
"sort_order" => [{ "field" => "sort_foo",
"reverse" => true }]
}
sort_link("Foo", "sort_foo", "query", results).
should have_selector("a",
:href => "/recipes/search?q=query&sort=sort_foo")
end
The only differences between the two examples are the value of "reverse" (in the first, it is false/ascending, in the second, it it true/descending) and the expectation that the second will have the back-slash pre-pended on the sort field (which is how one tells couchdb-lucene to reverse sort order).

With all of the inner specs passing, it is time to move back out to the Cucumber scenario:
cstrom@jaynestown:~/repos/eee-code$ rake
(in /home/cstrom/repos/eee-code)
..........

Finished in 0.098244 seconds

10 examples, 0 failures
.....................

Finished in 0.017578 seconds

21 examples, 0 failures
................................

Finished in 0.179981 seconds

32 examples, 0 failures
Unfortunately, when I re-run the Cucumber scenario, I find this:
cstrom@jaynestown:~/repos/eee-code$ cucumber -n features -s "Sorting (name, date, preparation time, number of ingredients)"
Feature: Search for recipes

So that I can find one recipe among many
As a web user
I want to be able search recipes

Scenario: Sorting (name, date, preparation time, number of ingredients)
Given 50 "delicious" recipes with ascending names, dates, preparation times, and number of ingredients
And a 0.5 second wait to allow the search index to be updated
When I search for "delicious"
Then I should see 20 results
When I click the "Name" column header
Then the results should be ordered by name in ascending order
When I click the "Name" column header
bad URI(is not URI?): http://localhost:5984/eee-test/_fti?limit=20&q=delicious&skip=0&sort=\sort_title (URI::InvalidURIError)
/usr/lib/ruby/1.8/uri/common.rb:436:in `split'
/usr/lib/ruby/1.8/uri/common.rb:485:in `parse'
./features/support/../../eee.rb:33:in `GET /recipes/search'
(eval):7:in `get'
features/recipe_search.feature:83:in `When I click the "Name" column header'
It turns out that Webrat really dislikes back-slashes—even when URI-encoded.

I am left with a choice here: change the application to suite the testing framework or not test this particular feature. I think Cucumber / Webrat are valuable enough to warrant changing the application, so back in I go. Instead of passing the back-slash from Sinatra to couchdb-lucene, I will add an order query parameter that will cue Sinatra to supply the back-slash to couchdb-lucene.

The example for the Sinatra app is:
    it "should reverse sort when order=desc is supplied" do
RestClient.stub!(:get).
and_return('{"total_rows":30,"skip":0,"limit":20,"rows":[]}')

RestClient.should_receive(:get).with(/sort=\\/)

get "/recipes/search?q=title:egg&sort=sort_foo&order=desc"
end
To get this passing, I add the bold text to the /recipe/search action:
get '/recipes/search' do
@query = params[:q]

page = params[:page].to_i
skip = (page < 2) ? 0 : ((page - 1) * 20) + 1

couchdb_url = "#{@@db}/_fti?limit=20" +
"&q=#{@query}" +
"&skip=#{skip}"

if params[:sort] =~ /\w/
order = params[:order] =~ /desc/ ? "\\" : ""
couchdb_url += "&sort=#{order}#{params[:sort]}"
end

data = RestClient.get couchdb_url

@results = JSON.parse(data)

if @results['rows'].size == 0 && page > 1
redirect("/recipes/search?q=#{@query}")
return
end

haml :search
end
Then I need to teach the helper to produce the order parameter, which is a simple matter of replacing the double back-slashes with "order=desc".

Now, it is back out to the feature. To implement the descending sort step, I use:
Then /^the results should be ordered by name in descending order$/ do
response.should have_selector("tr:nth-child(2) a",
:content => "delicious recipe 9")
response.should have_selector("tr:nth-child(3) a",
:content => "delicious recipe 8")
end
And now I have all of the steps passing up through pagination with sorting:



(commit)

Something to start on tomorrow.

No comments:

Post a Comment