Overview
Search tends to be one of those mysterious areas that everyone wants, but not everyone has. The goal of any site should be to keep your visitors around. Search provides a mechanism to keep your patrons from jumping ship. This tutorial should give you a good overview of how search works and how to implement it in your site.
What You'll Need
- A site built on Axiom Stack technology
- A bunch of content
- A bit of time
Dive In
To keep this manageable we'll start small and grow this example in another tutorial. We're going to assume that the search is found off of the homepage...'/Search'.
Making Functions
First we need to provide the search. While we don't have access to it yet, we want to provide that access.
/*
* Homepage/search.js
*/
function Search() {
var searchable_types = ['HomePage','Section','Page'];
var SIZE = 10;
var results = null;
var query = req.get('query');
var page = req.get('page_num');
results = app.getHits(searchable_types, {searchable_content: query});
return this.main(
{
content: this.search_results(
{
results: results.objects((page || 0) * SIZE, SIZE),
query: query,
pages: Math.ceil(results.length/SIZE)
}
)
}
);
};
Break It Down
Lets take this code apart and examine shall we?
var searchable_types = ['HomePage','Section','Page'];
This defines the types of objects we'll search on. These are very common types in most Axiom Stack implementations, but if you don't use them, don't sweat it, just change them out for prototypes that you do use.
var SIZE = 10;
We are defining the size of our result set. We do this for a few reasons. If we do not limit the result set you can run into major problems. Assume you're possible result set is 100,000 objects. Pulling that many objects into memory at once is bound to put some strain on your hardware, and the display of that many objects would be a nightmare for users to scan. The reason for search is manageable bites. So, we paginate the result set for both of these reasons.
var results = null;
We want to instantiate the variable, but not set it to anything. Since the return from getHits is an object, it makes sense to set it to null rather than an empty array.
var query = req.get('query');
var page = req.get('page_num'); Here we grab data from the request. If you take a look at our API you'll notice that req.get() will return to you the value that was sent with either "POST" or "GET". So, whichever mechanism you prefer, you can use it here and keep this code the same.
results = app.getHits(searchable_types, {searchable_content: query}); We are getting results here. Remember above where we defined the types? Here is where they are used. As this is not an explanation of the Query API, I'll leave it at this, searchable_content is a property, that we'll see shortly, on the prototypes you specified to search.
return this.main(
{
content: this.search_results(
{
results: results.objects((page || 0) * SIZE, SIZE),
query: query,
pages: Math.ceil(results.length/SIZE)
}
)
}
);
This last bit here is to return the results. What we are doing is assuming you have a single template that acts as your wrapper tal file, "main". One of the parameters that main takes is content, please change this if you use something different.
"search_results" is a tal file that handles the actual results from the search. It's parameters are results, query, and pages.
- results - The set of results based on the page that you are on.
- query - The original query.
- pages - The number of pages of results available.
Providing Types and Data
We need to make our prototypes searchable.
# AxiomObject/prototype.properties searchable_content searchable_content.type = String searchable_content.compute = this.generateSearchableContent(); searchable_content.depends = title,body searchable_content.index = TOKENIZED title title.type = String body body.type = XHTML
We told it to use a function this.generateSearchableContent(), so we need to create it.
/*
* AxiomObject/functions.js
*/
function _generateSearchableContent() {
return (this.title || '') + ' ' +
(this.body?this.body.toXMLString().replace(/<[a-zA-Z\/][^>]*>/g, ''):'');
}
function generateSearchableContent() {
return this._generateSearchableContent();
}
Break It Down
AxiomObject/prototype.properties
The first two lines should be fairly self explanatory, but the basic idea is that we define our property, and specify that we want it persisted as a String.
searchable_content.compute = this.generateSearchableContent();
We are defining a computed property. This means that the property changes and is dependent on other properties for it's data. There are all sorts of ways to use this, one being to set a modified date on an object, and another being to aggregate content. We're using the latter case.
searchable_content.depends = title,body
For the sake of this tutorial I have to assume that you have some properties on your objects. That is why I've specified title and body below. But, the reason for this is that, I don't need the function specified in line 3 to fire when all properties are changed. It adds unnecessary overhead. Instead I can just have it run on the properties that I care to aggregate.
searchable_content.index = TOKENIZED
We need to tokenize the content when it is persisted. This means that the analyzer being used will parse out information such as stop words, punctuation, and so on. Keep in mind that the removal of those parts is highly dependent on the analyzer.
AxiomObject/functions.js
What we are accomplishing here is a bit overkill, but I'll explain the point. First let's go through the code that does stuff.
return (this.title || "") + ' ' + (this.body?this.body.toXMLString().replace(/<[a-zA-Z\/][^>]*>/g, ""));
Here we are concatenating two strings and returning them. This is the data that is put into the property, searchable_content. Because the body property is an XHTML type, we want to grab it's string representation. Also, since we can likely have a great deal of html in there, it is appropriate to remove that junk from our search property. Nobody needs to be searching for <div>.
Now, that is the code that is really doing something, so why all the bubble wrap? I'm setting it up so that you can expand this a bit on your own. What I mean by this is that the function generateSearchableContent is overwritten on a per prototype basis. Because title and body will be used everywhere now, it makes sense to extract those pieces out into a common function. Then, when I have another property I need indexed on another prototype, for instance a News prototype, I can.
/*
* News/functions.js
*/
function generateSearchableContent() {
var content = this._generateSearchableContent();
content += (this.slug || '');
return content;
}
See, now it's as easy as that to add it in, rather than having to modify all of my search content generation functions when I add properties at the higher levels.
Security
Search is pretty secure. Especially with Axiom Stack, since functions other than main aren't accessible by default, so we need to add that exception.
#Homepage/security.properties Search=@Anyone
Showing The Results
Above I made mention of the tal file search_results. Let's actually create that tal file.
<!-- HomePage/search_results.tal -->
<div xmlns:tal="http://axiomstack.com/tale">
<h3>Search Results</h3>
<ul class="results">
<li tal:repeat="r: results">
<a tal:attr="href: r.getURI()" tal:content="(r.title || r.id)"></a>
</li>
</ul>
<ul class="pagination">
<li tal:repeat="page: range(pages)">
<a tal:omit="page == (req.get('page_num') || 0)" tal:attr="href:'/Search'+'?query='+query+'&page_num='+page" tal:content="page+1"></a>
</li>
</ul>
</div>
Fin
That is it. Now, try to go to http://localhost/Search. You should also be able to pass in query parameters for by adding ?query=<insert_text> to your URL.
Enjoy.

