« Posts under Python

Django: A Simple Keyword Search

Disclaimer:

This post was actually started about 9 months ago and never finished, so I’m not sure if the code will even run on the latest version of Django. I’m actually doing Ruby on Rails work nowadays at my job, and using Django solely for my personal projects. With that said, I hope some people out there will find some of these techniques somewhat useful.
—————————————————————-

Search

One feature that is commonly used on a lot of Django applications is the ability to search through content by keywords.  There are many different ways to do this, but I’d like to present a way that’s super easy to implement and uses only the Django model API.

Before I start, I’d like to first give a little disclaimer: this is absolutely NOT the most efficient way to perform a search through data.  If you’re looking for a more advanced solution, check out djangosearch on google code.

Ok, let’s say you’re making a blogging application, and you need a way to search blog entries for certain keywords. You may have a model named BlogEntry that looks like this:

class BlogEntry(models.Model):
    blog = models.ForeignKey(Blog)
    subject = models.CharField(max_length=100)
    body = models.TextField()

Now let’s say you needed a way to search the ‘body’ and ‘subject’ fields of all BlogEntries for the keywords ‘Django’ and ‘Python.’ Given a set of BlogEntries, this function will search their subject and body fields for the specified keywords. Here’s how you would build the function:

def search_keywords(blogentries, keywords):
    '''
    Searches a given QuerySet and returns a
    QuerySet that contains any word in the list of keywords
    '''
    if isinstance(keywords, str):
        keywords = [keywords]

    if not isinstance(keywords, list):
        return None

    list_body_qs = [Q(body__icontains=x) for x in keywords]
    list_subj_qs = [Q(subject__icontains=x) for x in keywords]
    final_q = reduce(operator.or_, list_body_qs + list_subj_qs)
    r_qs = blogentries.filter(final_q)
    return r_qs

The beginning of the function is pretty straightforward. The interesting part is the latter half of the function, which uses Django’s database API and Python’s reduce function to dynamically create a Django query with all of the keywords in it.

First, build a couple lists of Q objects, one for each keyword and field.

...
list_body_qs = [Q(body__icontains=x) for x in keywords]
list_subj_qs = [Q(subject__icontains=x) for x in keywords]
---

Using Python’s reduce function, we glue all these Q objects together with operator.or_.

...
final_q = reduce(operator.or_, list_body_qs + list_subj_qs)
r_qs = blogentries.filter(final_q)
return r_qs
---

Essentially, you create a query similar to this:
Q(body__icontains=”dog”) | Q(body__icontains=”cat”) | Q(body__icontains=”parrot”) …

If you wanted to perform an all-inclusive search (i.e. AND’ing the search terms together instead of OR’ing them), you’d use operator.and_ instead of operator.or_

Finish

And that’s it. Just a few lines of code and you’ve got your own search function. Leave a comment with your thoughts :) (Unless you’re a spam bot)

A Simple Django Truncate Filter

The Problem:

The built-in Django filter, truncate_words, truncates a string after a certain number of words.  This is great, but many times I find I have very tight space restrictions in certain areas of a page, and a string that is too long would push its way into another element and subsequently into my head in the form of a headache.

The built-in truncate_words filter is no help here — it does nothing to limit the width of a string.

I.e., “One two” and “ooooooooooooooooooooooooooonnnnnnnnnnnnnnneeeeeeeeeeeeee twoooooooooooooo” are both only 2 words, yet they have extremely different widths :)

The Solution:

We need a filter that truncates not only by words, but by characters too.  It’s an extremely simple filter, and often times I wonder why it’s not included in Django.

Let’s start from the very beginning.  Every filter must live in your app’s templatetags directory.  So create a file in that directory named “truncate_filters.py” or something.  If you need any more information than that, take a look at the Django documentation on how to create a custom filter.

Here is what the filter looks like:

from django import template
register = template.Library()

@register.filter("truncate_chars")
def truncate_chars(value, max_length):
    if len(value) <= max_length:
        return value

    truncd_val = value[:max_length]
    if value[max_length] != " ":
        rightmost_space = truncd_val.rfind(" ")
        if rightmost_space != -1:
            truncd_val = truncd_val[:rightmost_space]

    return truncd_val + "..."


*update* code was changed per chris and paul’s suggestions below, I haven’t tested but assume they work :)

Here’s how it works visually on this string: “This is a sample string”
This is what happens when the filter is supplied with the argument 20:

  1. Cut down the string to 20 chars if it is greater than 20 chars in length.  The string now becomes: “This is a sample str”
  2. Find the right-most space, indicating the start of the last word in the string, and truncate again:  The string is now: “This is a sample”
  3. Add “…” and return.  “This is a sample…”

You can invoke the filter from within your template like so:

{% load truncate_filters %}
<ol>
{% for some_string in a_list_of_strings %}
    <li>{{some_string|truncate_chars:50}}</li>
{% endfor %}
</ol>

Hope someone out there finds this helpful :)