Today on Planet Mozilla I came across this post by Daniel Glazman searching a tree generated from a SQLite template. This reminded me a lot of a similar trick that I play in our XUL code for searching a tree generated from an XML template.
Mirroring Daniel’s code, let’s create a tree first of all:
<treecol id="cText" label="Text" flex="1" persist="width ordinal"/>
<splitter class="tree-splitter" persist="ordinal"/>
<treecol id="cAssignee" label="Assignee" flex="1" persist="width ordinal"/>
<splitter class="tree-splitter" persist="ordinal"/>
<treecol id="cImportance" label="Importance" flex="1" persist="width ordinal"/>
<assign anonid="fulltext" var="?FULLTEXT" expr="concat(@text,@assignee,@importance)" />
<where id="filter" subject="?FULLTEXT" rel="contains" value="" ignorecase="true" />
This template assumes that the XML datasource looks something like this:
<todo text="Buy some milk" assignee="Bob" importance="High" />
<todo text="Feed the dog" assignee="Alice" importance="High" />
<todo text="Feed the fish" assignee="Alice" importance="Medium" />
<todo text="Feed the kids (halibut)" assignee="Bob" importance="Very Low" />
<todo text="Feed Bob" assignee="Alice" importance="Very Low" />
<todo text="Bob for apples" assignee="Alice" importance="High" />
Now we can add a search field, just like Daniel’s:
<textbox type="search" oncommand="SearchKeyword(this)"/>
And the SearchKeyword function looks like this:
var filter = document.getElementById("filter");
So what’s going on here? First of all, it’s worth noting that there’s a distinct difference between my code and Daniel’s – and not just in the switch from a SQL template to an XML template. His code changes the SQL query to only select results where the search term is found in one of the fields, but I can’t change the content of my XML file, so my code filters the displayed results instead. In other words his approach reduces the number of rows of data that are passed to the template, whereas mine passes the same amount of data to the template, then reduces the number of rows that are actually displayed.
This leads to a distinct difference in operation between these approaches. Daneil’s looks for the search term in each piece of data separately, whereas mine concatenates all the data for each row, then looks for the search term within this concatenated string. This can lead to some surprising results: if you search for “hal” you would expect the “Feed the kids (halibut)” entry to appear, but might be a bit put out to find “Feed the fish” also appearing. The reason for this becomes clearer if we look at the concatenated string that is searched (emphasis added for clarity):
Feed the fishAliceMedium
There are a couple of ways to deal with this issue – either by avoiding it altogether, or by turning it into a feature. To avoid the problem you could add multiple rules to the template, each with a different <where> element, effecively OR-ing them together. This is a lot of extra work, especially as the number of parameters grows.
The alternative approach is to separate the parameters by a character that’s not likely to appear in searches. For example, using the “~” as a separator, the XUL <assign> element would look something like this:
<assign anonid="fulltext" var="?FULLTEXT" expr="concat('~',@text,'~',@assignee,'~',@importance,'~')" />
The concatenated string now looks like this:
~Feed the fish~Alice~Medium~
Now the search term “hal” no longer appears. You’ll notice that I’ve added a tilde at the start and end of the concatenation as well. This isn’t strictly needed just for breaking up the search terms, but remember that I said I was going to turn this problem into a feature; now it’s possible to intentionally use a tilde to “anchor” a search. Look at these search terms:
- “bob” – Searches for “bob” anywhere in any of the parameters
- “~bob” – Searches for “bob” but only at the start of a parameter. Matches results where “Bob” is the assignee, and matches “Bob for apples”, but doesn’t match “Feed Bob”
- “bob~” – Searches for bob at the end of a parameter. Matches results where “Bob” is the assignee, and matches “Feed Bob”, but doesn’t match “Bob for apples”
- “~bob~” – Searches for a parameter that contains nothing but “bob”. Matches results where “Bob” is the assignee, but doesn’t match “Feed Bob” or “Bob for apples”
Finally let me share one more trick with you – the use of the “multiple” attribute on the <where> element:
<where id="filter" subject="?FULLTEXT" rel="contains" value="" ignorecase="true" multiple="true" />
This addition allows multiple comma-separated search strings to be combined as if using an OR operator. So searching for “~bob~,fish” would find all the tasks assigned to Bob, plus the “Feed the fish” task. Obviously this won’t work properly for search terms that contain a comma, so it’s not suited to every circumstance.
In our applications we often use multiple search boxes (usually three) where the user might want to perform some complex filtering. Each search box is tied to a separate <where> element, all of which have the “multiple” attribute set. The effect is that comma separated search terms in each box are combined as an OR operation, but the separate boxes are combined as an AND operation. This makes it easy to construct complex combinations of AND and OR whilst keeping a simple UI – just three search boxes.
For most users it’s intuitive to type a filter term into one box, then further restrict the results by typing into the second or third box. This means that we’re able to use the same UI for both novice and advanced users, without the former feeling overwhelmed.