In the first part of this series we spent some time setting up the Yahoo BOSS Mashup Framework, and ended by putting together an extremely minimal search service. For those who didn't work through part one, you can grab a zip of what we developed here, but you'll need to follow the instructions in part one for acquiring a BOSS App ID. In this second part of the series we are going to flesh out our search service a bit:
We're going to let users search either the web, Yahoo News, or images.
We're going to let users page through search results.
Lets get moving.
First, open up my_search/yahoo_search/views.py and take a look at SearchForm. At the moment all we have is this:
Then navigate over to http://127.0.0.1:8000/, and you'll see that you can now search the web, images, or Yahoo News. Pretty nifty. Now lets get cracking on paginating our search results.
Paginating Search Results
When you are using Django and you think paginating, your train of thought should immediately about the Paginator class, which is very helpful at dealing all pagination messiness. However, we're not dealing with a normal list (or a QuerySet, in which case we could use the QuerySetPaginator), so we're going to have to massage things a little bit.
We're going to do that by creating an intermediary class, named BossResultList that will implement the subset of Python list functionality that the Paginator needs to function. Fortunately, thats only three methods: __getitem__(self,i), __getslice__1 and __len__. Create a file in my_search/yahoo_search named boss_utils.py, and in that file we're going to insert this code:
The BossResultList takes the results of ysearch_search and uses them to mimic a list. This isn't a perfect abstraction, because it will only allow access to the subset of results that it is passed in its init function, however, it will be enough to take advantage of Paginator2.
Now lets go back to my_search/yahoo_search/views.py and add two imports:
This change is necessary because BossResultList needs data contained directly
within the returned results that isn't carried over after the results are converted
into a databse using the db.create function. (Specifically, it needs access
to the totalhits field that lets us inform users how many pages of results we can
serve them for their search query.)
Now we just have two little details remaining before we finish updating our search app:
revamping the index function, and updating our index.html template. Updating the
template will be easy, but we can't do that until we write index, which happens to
involve a pretty complete rewrite. Because there are so many changes I'll post the function
first, let you read over it, and then comment on particularly salient details.
We are now using GET instead of POST. Infact, we never really
should have been using POST to begin with. Sorry about that.
Because we are using GET its harder to distinguish
between when a user first lands on the page and when they
are submitting a search. That is why our response to an
invalid form is not to display the error messages generated
by newforms, but instead to display an empty form: the only
time we'll encounter an invalid form is when a user first
comes to the page, and we don't want to greet new users
with error messages.
Paginator's page count starts at 1 instead of at 0
which is why we calculate start as (page-1)*count
instead of as page*count.
We need total_pages and page because we want to
let users know where they are in the midst of the search
results. (For example, on page 5 of 412.)
Updating the index.html template
Much like the index function, the index.html template
has recieved a substantial overhaul as well. Fortunately, the
changes to the template are largely self-explanatory. After
its remodeling its going to look like this:
The most complex part is for handling advancing and retreating between pages of results.
Here we are using the Page returned by our Paginator to handle most of the
complexity (in this template context our Page is named results), but it gets
a bit more complex because we need to keep track of the search terms and the type of the search.
Pretty neat, wouldn't ya say? Hopefully this tutorial has been helpful, and let me know if you have any questions.
The Python documentation makes it pretty clear that __getslice__ is deprecated, but how to handle a 'slice object' as the documentation suggests is entirely unclear. As such, I am doing this the quick and easy way, while acknowleding it apparently isn't the preferable way to do so.↩
Certainly implementing a fuller implementation that allows seemless access to the entire search result set would be a fun exercise, and if I have a bit of time I'll try to throw it together.↩