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.
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.
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).
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.
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).
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 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:
Two of the points above may need more explanation:
Point 1. The purpose of the AssemblyResourceVirtualPathProvider is to be able to request the URL
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:
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);
}; } );
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:
<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>
and place view on your page with this code, for example:
<!– ko with: queryBox –>
<!– ko template: ‘querybox-control’ –>
<!– /ko –>
<!– /ko –>
Let us now have a closer look at how the QueryBox is implemented. The control consists of a view model:
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);
}); }; } );
and a view template:
<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>
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.
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:
<span data-bind=”i18n: { key: ‘Search’ }”></span>
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:
i18n.translate({ key: ‘No categories selected’ })
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.
<span data-bind=”i18n: { key: ‘Found %d result’, plural: ‘Found %d results’, number: numberOfMatches, arguments: [numberOfMatches] }”></span>
In translation to Polish it would expand to:
“Znaleziono 1 wynik”
or
“Znaleziono 2 wyniki”
or
“Znaleziono 5 wyników”
<span data-bind=”i18n: { key: ‘Search’, context: ‘QueryBox’ }”></span>
<span data-bind=”i18n: { key: ‘Search’, context: ‘Settings’ }”></span>
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:
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:
“IntelliSearch.AjaxClient.PATH/IntelliSearch.AjaxClient.i18n_data”: ajaxClientPrefix + “IntelliSearch.AjaxClient.i18n_data_pl”
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.
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.
November 8, 2024 / 3 min read
Sustainability is now a key focus for tech companies. In a recent survey at our company, we learned more about how our employees view sustainability and how they approach it. These...
July 26, 2024 / 10 min read
This guide covers how Optimizely Data Platform (ODP) can improve your business with real-time customer insights, personalised campaigns, and simple data integration and will give you...