AjaxClient in Intellisearch ESP: A query box on steroids

Following my previous article on enterprise search, it is time to dive deeper into the more technical side of IntelliSearch ESP. This time, we will go through the pumped up search page called AjaxClient in some detail.

The search page is usually the only interface of a search engine that is used by the end-user. It should be as simple as possible so the user is not overwhelmed, but it still needs to provide good functionality. Let’s start by looking at the desired features of a search box.

Query box

The most obvious part on a search page is the text box where users type their queries. An additional feature of the query box is the autocomplete functionality that suggests words from a dictionary.

Results view

When users enter a query, they expect a list of results. It is more than a simple list of links though. Each result typically has a title, a link, an extract with words highlighted from the search query and optionally a thumbnail of the document. With grouping functionality one may also need to display the parent item of the element found. The approximate number of results found should also be shown, and a way to navigate through them. In some cases, we might want to display so called “best bets” (hand crafted results to notify users of important information) or suggest how the query could be improved (“Did you mean…?”-kind of hints).

Filtering

Users should have the possibility to see how many documents there are in each category for a given query. Based on this information, they should be able to define filters to narrow down the results. Another way to limit the number of results is to specify a date range.

Other features

Non-functional features include the possibility to mix and match components depending on specific needs (for example change conventional Pager to InfiniteScroll), ability to translate the user interface, responsive design, encoding the query in a URL string (so that users may share the search queries easily and the state is preserved when the page is refreshed) and the possibility to integrate with both ASP.NET WebForms and MVC based solutions (plus other non .NET systems in the future).

The search page in Intellisearch ESP

IntelliSearch ESP comes with two different search pages: the original one based on ASP.NET WebForms (called WebClient) and a modern one based on JavaScript and KnockoutJS (called AjaxClient). In this article we will focus on the second one, in particular on its design.

The AjaxClient

The easiest way to look at AjaxClient is to install the IntelliSearch.AjaxClient.VsTools.vsix package for Visual Studio. When this is done, create a new project from the “IntelliSearch AjaxWebClient” template (Visual C#->Web category) and name it EspSearchClient. You can start exploring the project by having a look at the files included in the project.

As you will see, the EspSearchClient contains no AjaxClient specific files and the project contains only bootstrapping code in default.aspx and global.asax.cs files. The bootstrapping code does the following:

  1. Registers AssemblyResourceVirtualPathProvider to enable access to resources embedded in the IntelliSearch.AjaxClient.dll assembly.
  2. Configures RequireJS by specifying the location of basic libraries (RequireJS, jQuery, KnockoutJS, JED) and importing IntelliSearch.AjaxClient. This is the main module, which in turn makes other modules available.
  3. Imports CSS files and provides a basic HTML structure.
  4. Uses various controls from the IntelliSearch.AjaxClient.Controls namespace (“is:” prefix) to “import” components into the search page.

Two of the points above may need more explanation:

Point 1. The purpose of the AssemblyResourceVirtualPathProvider is to be able to request the URL

http://hostname/App_Resource/IntelliSearch.AjaxClient//IntelliSearch.AjaxClient.ViewModels.QueryBox.js

and get the file QueryBox.js that is embedded inside the IntelliSearch.AjaxClient.dll assembly. This allows us to pack all required .js/.html files into one .dll file for easy deployment and upgrades. It also suggests to the users that they should probably not modify those files, but rather write their own version of the whole module or use other extensibility mechanisms (such as postProcessingFunction).

Point 4. It is worth noting that the controls used here are just wrappers that emit client side code – there is no back-end functionality in these controls. The code each control expands to can be viewed in a browser. It may seem complicated, but the basic purpose is simply to create the class QueryBox that implements a view model for QueryBox control, passing the reference to core search objects (SearchParameters, SearchResults and SearchActions) and import a querybox-control view from the QueryBox.html file, associating it with the view model. It also contains provisions for multiple search scopes on a single page and the ability to provide a custom postProcessingFunction that may change the view model that comes out of the box.

If you don’t want to use ASP.NET WebForms, you can create the view model yourself. In the simplest form it would look like this:

{code}

define([“knockout”, “IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.ViewModels.SearchParameters”, “IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.ViewModels.SearchActions”, “IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.ViewModels.Pager”, “IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.ViewModels.CategoryTree”, “IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.ViewModels.Results”, “IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.ViewModels.ResultStatistics”, “IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.ViewModels.SortBy”, “IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.ViewModels.QueryBox”, “IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.ViewModels.Filters”, “IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.ViewModels.PreloadedCategories”],

function (ko, SearchParameters, SearchActions, Pager, CategoryTree, Results, ResultStatistics, SortBy, QueryBox, Filters, PreloadedCategories) {

return function () {

var self = this;

self.searchParameters = new SearchParameters();

self.searchResults = ko.observable();

self.searchActions = new SearchActions(self.searchParameters, self.searchResults);

//controls

self.results = new Results(self.searchParameters, self.searchResults, self.searchActions);

self.resultStatistics = new ResultStatistics(self.searchParameters, self.searchResults, self.searchActions);

self.pager = new Pager(self.searchParameters, self.searchResults, self.searchActions, { contextSize: 1 });

self.filters = new Filters(self.searchParameters, self.searchResults, self.searchActions);

self.categoryTree = new CategoryTree(self.searchParameters, self.searchResults, self.searchActions);

self.sortBy = new SortBy(self.searchParameters, self.searchResults, self.searchActions);

self.queryBox = new QueryBox(self.searchParameters, self.searchResults, self.searchActions);

}; } );

{/code}

As you can see, first we create three standard objects (searchParameters, searchResults and searchActions) and then we construct controls passing references to those three objects, along with additional parameters if necessary.

Next you just need to fetch templates:

{code}

<script type=”text/javascript”>

require([“jquery”, “knockout”, “IntelliSearch.AjaxClient.Main”, “../Scripts/SearchPageViewModel”],

function ($, ko, IntelliSearch, ViewModel) {

var viewModel = new ViewModel();

var templates = [];

templates.push(“IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.Views.QueryBox.html”);

templates.push(“IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.Views.CategoryTree.html”);

templates.push(“IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.Views.Results.html”);

templates.push(“IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.Views.ResultStatistics.html”);

templates.push(“IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.Views.Pager.html”);

$.when.apply($, IntelliSearch.Helper.downloadTemplates(templates)).done(function () {

ko.applyBindings(viewModel);

}); } );

</script>

{/code}

and place view on your page with this code, for example:

{code}

<!– ko with: queryBox –>

<!– ko template: ‘querybox-control’ –>

<!– /ko –>

<!– /ko –>

{/code}

Let us now have a closer look at how the QueryBox is implemented. The control consists of a view model:

{code}

define([“knockout”, “IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.ViewModels.BaseControl”, “IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.i18n”],

function (ko, BaseControl, i18n) {

return function (searchParameters, searchResults, searchActions) {

BaseControl.apply(this, arguments);

var self = this;

self.queryText = ko.observable(“”);

self.searchInProgress = searchActions.searchInProgress;

self.doSearch = function () {

searchParameters.reset();

searchParameters.QueryText = self.queryText();

searchActions.executeSearch();

};

self.executeSearchIfNeeded = function (sender, e) {

var key = e.charCode ? e.charCode : e.keyCode ? e.keyCode : 0;

if (key === 13) {

self.doSearch();

return false;

}

return true;

};

self.categoriesAvailable = ko.computed(function () {

if (!searchResults()) {

return false;

}

return searchResults().NumberOfMatches > 0;

}, this);

searchResults.subscribe(function () {

self.queryText(searchParameters.QueryText);

}); }; } );

{/code}

and a view template:

{code}

<script type=”text/html” id=”querybox-control”>

<div class=”is-querybox-control-container” data-bind=”css: { ‘is-querybox-control-container-with-margin’: categoriesAvailable }”>

<input type=”text” data-bind=”value: queryText, event: { keydown: executeSearchIfNeeded }, queryAutoComplete: { limit: 10 }, valueUpdate: ‘afterkeydown'” class=”is-querybox-control” />

</div>

<div class=”is-querybox-buttons”>

<button data-bind=”click: doSearch” class=”is-button is-button-search”><span data-bind=”i18n: { key: ‘Search’, context: ‘QueryBox’ }”></span></button>

</div>

</script>

{/code}

An interesting thing to note in the above view template is the i18n KnockoutJS binding that we will explore with the rest of the translation system below. You can explore the implementation of the other controls by viewing their source in a browser.

Translation system

AjaxClient uses an interesting internal method for making translations. The system is based on gettext – an established standard in Unix systems. The key benefits are:

  • No need to create a mapping table manually between a string “id” and its value. All the developer needs to do is to write (for example):

{code}

<span data-bind=”i18n: { key: ‘Search’ }”></span>

{/code}

to create a span element with the default value ‘Search’ and the ability to translate the string later. When you want to use the translation system from JavaScript code, you can use the following expression:

{code}

i18n.translate({ key: ‘No categories selected’ })

{/code}

This approach streamlines creating new strings for translation as when writing a new translatable string there is no need to switch between documents nor to devise new string identifiers.

  • Support for multiple plural forms. In some languages (for example Polish) an expression may have a singular form and multiple plural forms that depend on the number in the expression. Consider the expression “Found %d result(s)”. To properly support it you would write this code:

{code}

<span data-bind=”i18n: { key: ‘Found %d result’, plural: ‘Found %d results’, number: numberOfMatches, arguments: [numberOfMatches] }”></span>

{/code}

In translation to Polish it would expand to:

“Znaleziono 1 wynik”

or

“Znaleziono 2 wyniki”

or

“Znaleziono 5 wyników”

  • Ability to define context. Some strings are so short and generic that they require different translations depending on the context. In such cases you can use the context parameter to differentiate one use from another. The following two usages:

{code}

<span data-bind=”i18n: { key: ‘Search’, context: ‘QueryBox’ }”></span>

<span data-bind=”i18n: { key: ‘Search’, context: ‘Settings’ }”></span>

{/code}

will result in the string “Search” used in the English (untranslated) version of the application, but may result in different strings in for example Polish.

After writing the code it is time to generate the list of strings to be translated. In the AjaxClient solution all you need to do is to switch the solution configuration to GenerateTranslations, which executes the following steps:

  1. Finds all .html and .js files and creates a .potf file for each one, which contains all strings to be translated from the respective source file. This step is done using custom made scripts executed using node.js.
  2. Merges all .potf files into one all.pot file.
  3. Merges the all.pot file into existing translations to various languages stored in .po files.

When this is done, the pl.po file (which contains the Polish translation) can be sent to the translator. The translator can use a tool like Poedit to do the translations and send them back to the developer, or commit it to the repository directly.

During a normal build (Debug/Release configuration), all .po files are converted to JSON format. For example pl.po will be converted to i18n_data_pl.js and embedded inside IntelliSearch.AjaxClient.dll. To make the application use it, you need to redirect i18n_data to the specific language version but add the following line to the RequireJS path definitions:

{code}

“IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.i18n_data”: ajaxClientPrefix + “IntelliSearch.AjaxClient.i18n_data_pl”,

{/code}

This way, whenever a module requests translation data by the generic name of “IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.i18n_data”, it will actually get our language specific version.

Next this data is applied to DOM using the i18n KnockoutJS binding or a JavaScript translate function that we have seen above. These methods use jed.js library internally.

About AjaxClient and IntelliSearch ESP

The development of IntelliSearch ESP is supported by services delivered by Making Waves for IntelliSearch Software AS, an Oslo based company specializing in enterprise search solutions for both the private and public sectors.

AjaxClient is a modern, component-based user interface for IntelliSearch ESP, designed and created by Tomasz Grobelny and Jacek Madej in 2013.

Published