Tutorial Part Zen - Axiom

Welcome to the final Axiom tutorial. In the previous installments, we've covered how Axiom handles tasks common to most web frameworks. Now, it's time to start exploring Axiom's unique features. What we're going to go over here is pretty different from anything else out there, so don't worry about getting the big picture at first.

Search

Any good blog should have full-text search of its entries available. In most other frameworks, you have to use external libraries to accomplish this (acts_as_ferret in Rails, for example). With Axiom's Lucene-based datastore, search is baked right in.

We saw earlier an example of using the Axiom query API to retrieve our blog entries. The same API can be used to handle readers' text searches. Create a file called search.properties in your application directory:

site_search
site_search.fields = {title: 2, body: 1}
site_search.analyzer = StandardAnalyzer

What we're doing here is creating something called a search profile. Search profiles are a convenient way of bundling a set of query behavior together into a single abstraction. For site_search, we define a pair of properties. The first, fields, is a javascript object that contains information about what properties to search across and how to weight the terms. In site_search, we will apply the entered query terms to the title and body fields of objects in the database. Further, the title field query will be be weighted twice as much as the body field query in determining the relevancy of the results. The last property tells Lucene what analyzer class to use when creating the query.

Let's put this to use. First, let's give our blog readers a form to enter searches into- add the following after the h1 element in HomePage's main.tal:

<form action="search">
   <input name="query"/>
   <input type="submit" value="Search"/>
</form>

This simple form just takes a user-entered query and submits it to a method called search. Now, we'll create that method to handle the query. Add the following to methods.js in HomePage:

/**
 * Display the first ten results of a user-entered query.
 */
function search(){
    return this.search_results({ 
	   results: app.getHits("Entry", new NativeFilter(req.data.query, 'site_search')).objects(0,10)
	});
}

As before, we're creating a named function to handle an http request. When the user enters a search and submits the search form, this function will be invoked. What's going on inside? We're returning the result of calling another function- search_results is a TALE file we'll create next. We're passing a JSON object with single property into the TALE call. The results property of this object contains an array of objects. To get this array, we first use the query API to get a Hits object of our search results. We tell it we're interested in searching Entry prototypes and to use a filter that takes the user-entered query and combines it with the behavior packaged up above in our site_search profile. Once we've got that query, we grab the first ten objects off of it. Not bad for five lines of code.

Now, let's create that search_results TALE file. Create search_results.tal under HomePage:

<html xmlns:tal="http://axiomstack.com/tale">
    <head><title tal:text="$">${this.title}: Search Results</title></head>
    <body>
		<h1>Search Results</h1>
        <ol>
			<li tal:repeat="obj: results">
				<a tal:attr="href: obj.getURI()" tal:content="obj.title"/>
        	</li>
		</ol>
	</body>
 </html>

Not a great deal here we haven't seen before. We repeat over the results variable we passed into the TALE call, creating a <li> element for each member of the array. For each repeated element, we generate an <a> element, with the href attribute set to the object's URI and the content of the link set to the object's title.

References

Most blogs these days have some sort of tagging or categories system. All the cool kids are doing it, so let's do it too. While we're at it, we'll take a look at Axiom's Reference model, one of its most powerful features.

References in Axiom work conceptually like pointers in C/C++, only smarter and with less of a chance of blowing your foot off. A Reference is an object that forms a link between two other objects:

-----------------                  -------------                   -----------------
| Source Object |  == contains =>  | Reference |  == points to =>  | Target Object |
-----------------                  -------------                   -----------------

The Reference object is stored as a property on the source object and 'points' to the target object. Let's take a look at how this is used.

First, make a Tag prototype. As before, create a directory called 'Tag' in the application directory. We'll give our Tags a title property, in the prototype.properties:

title
title.type = String
title.index = UNTOKENIZED

The index property controls how Lucene behaves when it stores this property and indexes it for easy searching. We're set it to UNTOKENIZED for easier exact-text matching. Author's Note: we probably need a link here to a more through discussion of (un)?tokenized. Next, we need to create a property on our Entries to store the References to our Tag objects. Add the following to Entry's prototype.properties:

tags
tags.type = MultiValue(Reference)

tags is a MultiValue property that can contain multiple Reference objects.

Now, we need a way for users to actually tag entries! Modify entry_content.tal under Entry to look like this:

<div xmlns:tal="http://axiomstack.com/tale">
      <h2 tal:content="this.title"/>
      <p tal:text="$">Published on: ${this.date}</p>
      <p tal:content="this.body"/>
      <a tal:attr="href: this.getURI('edit_entry')">Edit This Entry</a> 
      <a tal:attr="href: this.getURI('delete_entry')">Delete This Entry</a>
	  <!-- Display the tags -->
      <p>Tags: <ul>
	  		<li tal:repeat="ref: this.tags" tal:var="tag: ref.getTarget()">
                <a tal:attr="href: tag.getURI()" tal:content="tag.title"/>
            </li>
      </ul> </p>
	  <!-- Form for tagging entries -->
      <form tal:attr="action: this.getURI('add_tag')" method="post">
		<label for="tag">Add A Tag:</label>
        <input name="tag"/>
        <input type="submit" value="Save"/>
     </form>
</div>

Now, each of our Entry objects will display their tags and contain a form for adding new ones. Let's call out a few things in the TALE we've added. In the section where we're display this entry's Tags, we're iterating over the References in the tags properties of our Entry with tal:repeat, using the ref variable. We want to get a couple of properties off of the Tag object that each Reference is pointing to, so we grab it with the getTarget method and store it in a local variable tag, using tal:var. We then use that tag variable to access the actual Tag object we're interested in.

Another piece to note is how we're generating the action URI that our tag-generation form will submit to. We know the name of the method we want to hit is add_tag, and it'll be a method defined on the Entry prototype. Since this TALE fragment will be called in multiple contexts (from the HomePage and from an Entry's own main method), we need the absolute URI for the object and its action. By calling getURI on our Tag instance and passing in the desired action as a string, we get back the web-addressable location of the object with the action tacked on.

The last ingredient is the aforemented action, the Javascript that recieves our form post and tags our object. Add the following to any .js file under Entry:

/**
 * Add a single Tag reference to this Entry.  If no Tag with the given title exists already, create it.
 */
function add_tag(){
    var tag_title = req.data.tag;
    var hits = app.getHits("Tag", {title: tag_title});
	var tag;
	if(hits.length > 0){
		tag = hits.objects(0,1)[0];
    } else {
		tag = new Tag();
		tag.title = tag_title;
		tag.id = tag_title.replace(/\s+/g, '-').replace(/[^\w-]/g, '');
		app.getHits("HomePage", {}).objects(0,1)[0].add(tag); // for clarity, we assume the HomePage exists
	}
	if(!this.tags)
		this.tags = new MultiValue();
	this.tags = this.tags.concat(new Reference(tag));
	return "Added tag "+tag_title;
}

We look for the Tag with the given title using the query API. If it's found in the database, we use that; otherwise, we create a new one and add as a child of the home page. We then create a new Reference to that Tag (by simply feeding it into a Reference constructor) and then adding it to the tags MultiValue. Note that manner in which this is done; the concat method creates a new MultiValue with the appended object, which we then assign back to the property on this, our Entry. MultiValues are immutable, like Strings.

Time to try out our new tagging system! Start Axiom if it's not already started and point your browser to your homepage to http://localhost/simple-blog/home . Start adding tags to your entries!

Wouldn't it be nice we could see all the entries with a certain tag? With the simple system we set up above, this is disgustingly easy. Add the following methods.js to the Tag prototype:

/**
 * Display all the entries with this tag.
 */
 function main(){
	 return app.getHits("HomePage").objects(0,1)[0].main({
		 entries: app.getSources(this, "Entry")
	 })
}

Two interesting bits here: One, check out how we're getting all the entries for this Tag. The app.getSources function returns everything that has a Reference to the first argument. You can also narrow it down by prototype- we're specifying that we're only interested in Entry objects that have References to our Tag. Two, we're not even going to bother writing more TALE to display our Tag's entries. After all, we've already written something that display a bunch of entries in the HomePage's prototype- why repeat ourselves?

And we'll make one quick change to HomePage's main.tal to make this possible. Modify that line we added to display all our entries (back in the first Tutorial) to look like:

<div tal:repeat="entry: (data.entries || this.get_entries())" tal:replace="entry.entry_content()"/>		   

What we're doing here is allowing ourselves to pass a custom set of objects into be displayed inside the HomePage's main TALE method. If a variable named entries is present in when the TALE is processed, that will be used; otherwise, it'll call this.get_entries() and use the result of that.

Now, click on one of those tag links on your tagged entries. You should see every entry with that tag!