George Durzi

in

SharePoint Search from Tafiti - Continued ...

UPDATE: This code is now in the Tafiti project in CodePlex and should be going into the main branch soon.

In this excellent post, my colleague Kevin Marshall build a prototype for integrating Tafiti search into SharePoint. I've been working with Kevin to add some functionality to the prototype and get it working as a tab on the Search Center on the Clarity intranet.

The first thing I wanted to do was to move away from having all the SharePoint search code in the search.aspx code-behind. I created a very simple provider to encapsulate the search functionality:

public abstract class SearchProvider : ProviderBase
{
    public abstract LiveXmlSearchResults ExecuteSearch();
} 

It's worth noting at this point that I didn't want to put any effort into refactoring any existing Tafiti code so it would work within the provider model. So when you see some cheesy branching logic a little further down, that's why :)

To support retrieving the specified provider from the application's web.config, I implemented SearchProviderCollection and SearchProviderSection classes to describe a collection of search providers, and a web.config section to describe the search provider.

public class SearchProviderCollection : ProviderCollection
{
    public new SearchProvider this[string name]
    {
        get { return (SearchProvider)base[name]; }
    }

    public override void Add(ProviderBase provider)
    {
        if (provider == null)
            throw new ArgumentNullException("provider");

        if (!(provider is SearchProvider))
            throw new ArgumentException
                ("Invalid provider type", "provider");

        base.Add(provider);
    }
}

public class SearchProviderSection : ConfigurationSection
{
    [ConfigurationProperty("providers")]
    public ProviderSettingsCollection Providers
    {
        get { return 
              (ProviderSettingsCollection)base["providers"];}
    }

    [StringValidator(MinLength = 1)]
    [ConfigurationProperty("defaultProvider", 
        DefaultValue = "SearchProviderSharePoint")]
    public string DefaultProvider
    {
        get { return (string)base["defaultProvider"]; }
        set { base["defaultProvider"] = value; }
    }
}

This allows me to configure the application directly in the web.config to use a specific search provider. This is supported by first defining a new sectionGroup:

    <sectionGroup name="system.web">
      <section name="searchService"
        type="SearchProviderSection"
        allowDefinition="MachineToApplication"
        restartOnExternalChanges="true" />
    </sectionGroup>

and then by specifying the search provider in a searchService element within :

    <system.web>
    <searchService defaultProvider="SearchProviderSharePoint">
      <providers>
        <add name="SearchProviderSharePoint" 
             type="SearchProviderSharePoint"/>
      </providers>
    </searchService>

Let's now take a look at the implementation of the SearchProviderSharePoint class:

    public string QueryText { get; set; }
    private string _queryPacket = string.Empty;
    private WindowsImpersonationContext 
        _impersonationContext = null;

    public override void Initialize(string name, 
     System.Collections.Specialized.NameValueCollection config)
    {
        // Left out some code, nothing fancy happening here
        base.Initialize(name, config);
    }

    public SearchProviderSharePoint() {} 

In an overloaded constructor, we format the QueryPacket that will be sent to the search service and do a simple check to see if the service is actually available. You would obviously want to do something a little more than simply throwing an exception, but you get the point:

    public SearchProviderSharePoint(string queryText)
    {
        QueryText = queryText;
        _queryPacket = String.Concat(
        "<QueryPacket xmlns='urn:Microsoft.Search.Query'>",
            "<Query>",
                "<SupportedFormats>",
                    "<Format revision='1'>
urn:Microsoft.Search.Response.Document:Document</Format>"
, "</SupportedFormats>", "<Context>", "<QueryText language='en-US' type='STRING'>", QueryText, "</QueryText>", "</Context>", "<Range>", "<StartAt>1</StartAt>", "<Count>100</Count>", "</Range>", "</Query>", "</QueryPacket>");
        if (IsSearchServiceAvailable() == false)
            throw new Exception("Service unavailable");
    }

You can add some more information to the QueryPacket, very similar to how you can tweak the search request in the object model before executing it, for example:

<EnableStemming>true</EnableStemming>
<TrimDuplicates>true</TrimDuplicates>
<IgnoreAllNoiseQuery>true</IgnoreAllNoiseQuery>
<ImplicitAndBehavior>true</ImplicitAndBehavior>
<IncludeRelevanceResults>true</IncludeRelevanceResults>
<IncludeSpecialTermResults>true</IncludeSpecialTermResults>
<IncludeHighConfidenceResults>true</>IncludeHighConfidenceResults>

The code to check if the search service is online is:

    private bool IsSearchServiceAvailable()
    {
        _impersonationContext = ((WindowsIdentity)
            HttpContext.Current.User.Identity).Impersonate();
        SPSearch.QueryService queryService = 
            new SPSearch.QueryService();
        queryService.Credentials = 
            CredentialCache.DefaultNetworkCredentials;
        string serviceStatus = queryService.Status();
        _impersonationContext.Undo();

        return serviceStatus.ToUpper() == "ONLINE" ? true : false;
    }

The search is then executed and formatted into LiveXml format before being displayed in the browser:

   public override LiveXmlSearchResults ExecuteSearch()
    {
        _impersonationContext = ((WindowsIdentity)
            HttpContext.Current.User.Identity).Impersonate();
        SPSearch.QueryService queryService = 
new SPSearch.QueryService(); queryService.Credentials =
CredentialCache.DefaultNetworkCredentials; DataSet queryResults =
queryService.QueryEx(_queryPacket); _impersonationContext.Undo(); return CreateLiveXmlSearchResults(queryResults); }
    private LiveXmlSearchResults CreateLiveXmlSearchResults
        (DataSet queryResults)
    {
        DataTable relevantResults = 
            queryResults.Tables["RelevantResults"];

        LiveXmlSearchResults returnResults = 
            new LiveXmlSearchResults();
        returnResults.searchresult.documentset._source = 
            "FEDERATOR_MONARCH";
        returnResults.searchresult.documentset._count = 
            relevantResults.Rows.Count.ToString();
        returnResults.searchresult.documentset._start = "0";
        returnResults.searchresult.documentset._total = 
            relevantResults.Rows.Count.ToString();

        LiveXmlWebResult[] results = 
            new LiveXmlWebResult[relevantResults.Rows.Count];

        for (int i = 0; i < relevantResults.Rows.Count; i++)
        {
            LiveXmlWebResult webResult = 
                new LiveXmlWebResult();
            webResult.title = 
                relevantResults.Rows[ i]["Title"].ToString();
            webResult.desc = GetDescription(
              relevantResults.Rows[ i]["Description"],
              relevantResults.Rows[ i]["HitHighlightedSummary"]);
            webResult.url = 
                relevantResults.Rows[ i]["Path"].ToString();
            results[ i] = webResult;
        }

        if (results != null)
            returnResults.searchresult.documentset.document =
              (results.Length > 1) 
              ? (object)results : (object)results[0];
        
        return returnResults;  
    }

The GetDescription method is the exact same in Kevin's original post, so for the sake of brevity, I'll leave it out.

As promised, here's the cheesy part where I avoided refactoring the original code to fit the search logic into the new search provider. In the SoapSearch method in search.aspx.cs, we first load the SearchProviderSection from web.config. I basically leave everything intact if there is no provider specified in web.config. Otherwise, we just call the ExecuteSearch method of the provider to search against SharePoint.

    private void SoapSearch(...
    {
        LiveXmlSearchResults result = null;

        if (_provider == null)
        {
            SearchProviderSection section = (SearchProviderSection)
             WebConfigurationManager.
GetSection("system.web/searchService"); _providers = new SearchProviderCollection(); ProvidersHelper.InstantiateProviders (section.Providers, _providers, typeof(SearchProvider)); _provider = _providers[section.DefaultProvider]; } try { if (_provider == null) { // Original LiveSearch prep code goes here - left out } else if (_provider is SearchProviderSharePoint) { SearchProviderSharePoint sharePointProvider = new SearchProviderSharePoint(query); result = sharePointProvider.ExecuteSearch(); } // Continue ...
     }

And finally, we want to get Tafiti set up as a tab on our portal's Search Center. I chose to simply create a new web site on my SharePoint front-end, I had to install .Net 3.5 first though for Tafiti to run.

You can create a new Search Page in your Search Center, stick a good old Page Viewer Web Part on it and point it to your Tafiti site.

The only thing that's acting strange now is actually signing in to Tafiti in order to be able to save your search results. When you register an application with Windows Live, you have to specify a url for it. In this case, I want this to be the address of the search page in SharePoint, which contains a Page Viewer web part to display the actual Tafiti web application. I'm prompted to sign in, but it doesn't look I really ever do get signed in - my shelved results aren't being saved to the database.

Comments

No Comments