REQUEST A DEMO

Exposing Knowledge Using Dovetail Seeker

Dovetail Seeker is our new search offering for Dovetail CRM. One of the primary design goals from a developer’s point of view is to make it easy to integrate with Seeker. I know I am biased, but think we did pretty well. In this post I’ll I share my experience integrating Seeker with our public web site exposing search access to Dovetail’s Knowledge Base.

 

Why Is This A Good Idea?

For a long time we’ve exposed to customers the ability to browse our knowledge base. Here it is:

Dovetail's Browe KB Page

This is a pretty good solution for Dovetail administrators looking for common solutions to issues that may pop-up. A better solution would be to let users search the knowledge base for the exact issue they are running into and see what pops up. Being able to search for an problematic error message is pretty handy:

Searching Dovetail's KB

Raise your hand if you’d rather search than browsing around for a solution! If your hand happens to be up right now… Read on.

What’ve Got To Work With Here

The Dovetail public knowledge base is part of an ASP.Net 2.0 web application.

Our Seeker index resides in on a separate machine exposed by the Seeker search web service. Here is a look at a possible Seeker network topology ripped from the documentation:

Dovetail Seeker Topology

Disclaimer, I am not an ASP.Net expert. Likely there is a better way to get this done. Also, I leaned heavily on Chad Myers, for his ASP.Net experience. Thank you Chad for putting up with my silly questions, ASP.Net ignorance, and general complaints about ASP.Net Web Forms.

 

What Needs to Happen

To create a basic web search interface to Dovetail CRM we need to do the following:

  • Create a web page with a text box for search queries and a go button.
  • Write ASP.Net “code behind” for the page that relays the search query to Seeker.
  • Display the search results returned by Seeker to the web user as Html.
    • Show which search results are currently being displayed.
    • Show the total number of search results available for the query.
    • The interface needs to support paging. There can be a lot of search results.

Let’s take this one step at a time working our way from an html form to search results.

Search User Input

Search input is pretty straight forward. You give the user a text box and a Go button. Here is the Html and some JavaScript I used to gather the search query from the user.

<script language="Javascript" type="text/javascript">
	function searchSeeker() {
		window.location.href= 'knowledgeBaseSearch.aspx?query=' + document.getElementById('searchQuery').value;
	}
</script>            

<div id="searchUi">
	<label for="searchQuery">Search :</label>
	<input id="searchQuery" type="text" name="query" size="57" value="<%=QueryString%>" onkeypress="if(event.keyCode == 13) {searchSeeker(); return false; }"/>
	<input id="goSearch" type="button" value="Go" onclick="searchSeeker(); return false;"/>
</div>

I am using JavaScript to “submit” the search results due the nature of the ASP.Net application my application is living in. I was having trouble getting the page to behave using standard ASP.Net Web Forms techniques so I decided to trick ASP.Net into behaving like classic ASP. Sorry, I have no love for ASP.Net Web Forms I’d much rather have ASP.Net MVC next time around. You may also notice the query text box has an onkeypress event handler to capture the Enter key being pressed. Another crappy ASP.Net work around. In the end all that is happening is a GET request is returning to the same page with the query string as a parameter.

GET http://dovetailsoftware.com/…/knowledgeBaseSearch.aspx?query=ORA-065502

ASP.Net Code Behind

For searching Dovetail Seeker the knowledge base search code behind basically does the following:

  1. Get the search query and any paging information from the WebRequest
  2. Make a search WebRequest to the Dovetail Seeker web service
  3. Use a ASP.Net repeater control to data bind to the array of search results returned by Seeker.

I just want to hit the highlights if you are interested in the nitty gritty details. I have posted all the relevant html and code behind up on Pastie.

At a high level the code below is what’s going on. When the page is loading: 1) Get the search query and paging information  2) execute a search against Seeker, and 3) bind the results to a repeater control on the page:

protected override void OnLoad(EventArgs e)
{
	base.OnLoad(e);
	
	_queryString = Request["query"];
	_resultCount = String.IsNullOrEmpty(Request["resultCount"]) ? "10" : Request["resultCount"];
	_startResultIndex = String.IsNullOrEmpty(Request["startResultIndex"]) ? "0" : Request["startResultIndex"];
	
	Search();
}

private void Search()
{
	if (String.IsNullOrEmpty(QueryString))
		return;
	
	_searchResultInformation = SeekerClient.Query(QueryString, _resultCount, _startResultIndex);
	
	searchResultsRepeater.ItemDataBound += searchResultsRepeater_ItemDataBound;
	searchResultsRepeater.DataSource = _searchResultInformation.SearchResults;
	searchResultsRepeater.DataBind();
}

You may have noticed the resultCount and startResultIndex parameters. They are optional Seeker parameters to support paging. To render “page 5” when there are 10 results per page you would say. Give me 10 search results starting at the 50th result.

An important task is 2) executing a search against Dovetail Seeker. The following code is a helper class that executes a search against Dovetail Seeker and returns a .Net object containing the search result:

public static class SeekerClient
{
	public static readonly string SeekerUrlAppConfigKey = "seekerSearchWebServiceUrl";
	public static readonly string SeekerUrl = System.Configuration.ConfigurationManager.AppSettings[SeekerUrlAppConfigKey];

	public static SearchResultInformation Query(string searchQuery, string resultCount, string startResultIndex)
	{
		string queryWithPrivateSolutionFilter = HttpContext.Current.Server.UrlEncode("(" + searchQuery + ") AND public:1 AND domain:solution");
		string json = RunSearchQuery(queryWithPrivateSolutionFilter, resultCount, startResultIndex);
		return AssembleSearchResultInformationFromJson(json);
	}

	private static string RunSearchQuery(string searchQuery, string resultCount, string startResultIndex)
	{
		string searchQueryUrl = String.Format(SeekerUrl + "search/search.castle?query={0}&resultCount={1}&startResultIndex={2}", searchQuery, resultCount, startResultIndex);

		try
		{
			WebRequest webRequest = WebRequest.Create(searchQueryUrl);
			WebResponse webResponse = webRequest.GetResponse();
			StreamReader reader = new StreamReader(webResponse.GetResponseStream());
			return reader.ReadToEnd();
		}
		catch(Exception ex)
		{
			string errorMessage = String.Format("Error using the Dovetail Seeker WebService. Please make sure the Dovetail Seeker web service application URL is set correctly in your Web.Config in the appSettings node using the key '{0}'.", SeekerUrlAppConfigKey);
			throw new ApplicationException(errorMessage, ex);
		}
	}

	private static SearchResultInformation AssembleSearchResultInformationFromJson(string json)
	{
		return JavaScriptConvert.DeserializeObject<SearchResultInformation>(json);
	}
}

This code creates a web request to Dovetail Seeker and converts the JSON returned to a .Net object using the excellent Json.NET library. Dovetail Seeker includes an assembly with SearchResultInformation and SearchResult parameter classes (code) which are compatible with the JSON results returned by Dovetail Seeker.

The URI of the Dovetail Seeker web application is configured in the application configuration settings file (web.config)

Constraining the query on the server side

I only want to expose public solutions to the Dovetail knowledge base search interface. The trouble is our seeker index also contains other types of search results. You might have noticed that I added some additional magic to search query we are sending to Seeker:

 

This constraint to the query allows only Solution search results that are public to be returned from Dovetail Seeker.

string queryWithPrivateSolutionFilter = HttpContext.Current.Server.UrlEncode("(" + searchQuery + ") AND public:1 AND domain:solution");

Pagination

For a paging interface we blatantly copied Digg’s pagination. I’ll be the first to admit that the code we wrote to accomplish this complicated and very much over the top. That said, I think it looks pretty cool.

Paging UI

Search Results – Exposed

With apologies to Sun Tzu. Know your Search Results and know your search integration.

The following information returned for every search:

  • The search query executed.
  • An array of search results.
  • The total number of search results available for this query. (paging)
  • The index of the first search result returned out of all potential search results. (paging)
  • When the search index was last updated.
  • Error result: If something goes wrong an error message is returned.

Each search result returned looks like this:

  • Score – how relevant is the result to the query.
  • Title – the title of the search result.
  • Summary – details about the search result.
  • Domain – what search domain does the result belong to. This allows you to mix results of different kind of data such as Cases and Solutions that match the search term.
  • Id – the unique Dovetail CRM identifier of the result

Conculsion / Wrap Up / Summary / El Fin

I appreciate that you are reading this conclusion. It means you are a diehard. It means you really want to know what pearls of wisdom that I may still have up my sleeve but sadly I’m out. Ok maybe a couple things. My best advice is to keep it simple. Borrow from the best (*cough* Google) . The code is out there if you want to use it as guidance. Comment below if you are confused about anything so that everyone can learn.