Friday, August 31, 2012

What’s New in SharePoint 2013 Search (A Developer’s Perspective) Part Two

Technorati Tags: ,,

In my first post of this series Part One I talked about how SharePoint 13 preview search has made changes to the API to accommodate the ability to store search objects at different levels. These levels are the Search Service Application, Site Collection and the Site (Tennant). One of these objects is the new result source. It is a combination of SP2010 Federated Locations and Scopes. The result source is a sophisticated tool to allow administrators to construct fine tuned sources for user queries. The sources can be SharePoint (local and remote), file shares, Microsoft Exchange or even custom sources. In this post I will show you how to use the new SearchExecutor class and search using  FQL (FAST Query Language) with it. I will also show you how to leverage the result sources to execute multiple queries and bring back results from different sources in one method call. Please note this posting is based on features in SharePoint 2013 Preview and are subject to change.

The new SearchExecutor class

In SP2010 you used either the KeywordQuery or FullTextSQLQuery classes to execute searches. Since the FAST search technology has now been assimilated into SharePoint 2013  the FullTextSQL syntax is no longer supported. Strangely, the FullTextSQLQuery class still exists and is not marked as deprecated, but it does not work. The KeywordQuery class appears to be the only class to execute queries with. This class uses the new KQL (Keyword Query Language) which is a mixture of SP2010 keyword and FQL syntax. Building Search Queries in SharePoint 2013.

Below is an example of doing a keyword search in SP2010. Notice that the search was invoked by calling the KeywordQuery’s  Execute method

SharePoint 2010 Keyword Search

public static DataTable ExecuteKeywordSearch(string queryText)
{
    ResultTableCollection rtc = null;
    DataTable retResults = new DataTable();

    using (SPSite site = new SPSite("http://basesmc2008"))
    {
        using (KeywordQuery query = new KeywordQuery(site))
        {

            query.QueryText = queryText;
            query.ResultTypes |= ResultType.RelevantResults;
            query.KeywordInclusion = KeywordInclusion.AllKeywords;
            query.RowLimit = 500;
            query.ResultsProvider = SearchProvider.SharepointSearch;
            query.SelectProperties.Add("Path");
            query.SelectProperties.Add("IsDocument");

            rtc = query.Execute();

            if (rtc.Count > 0)
            {

                using (ResultTable relevantResults = rtc[ResultType.RelevantResults])
                    retResults.Load(relevantResults, LoadOption.OverwriteChanges);

            }

        }
    }
    return retResults;
}

The next example is a SharePoint 2013 keyword search. It uses the new SearchExecutor class and the ExecuteQuery method taking the KeywordQuery object as an argument. You no longer need to set the ResultsProvider property since there is just one provider. Also you do not need to set the ResultTypes property to tell the search engine what type of results you want. Finally, you need to use the new Filter method on the returned ResultTableCollection object to retrieve the results you want. In SP2010 you used an enumeration and the ResultTableCollection’s indexer. The Filter method is much more flexible but you need to hard code string property names rather than using an enumeration. The string argument represents property name of the ResultTableColection, and the second argument is an object value for that property.

SharePoint 2013 Keyword Search

public static DataTable ExecuteKeyWordSearch(string queryText)
{
    ResultTableCollection rtc = null;
    DataTable retResults = new DataTable();

    using (SPSite site = new SPSite("http://basesmc15"))
    {
        using (KeywordQuery query = new KeywordQuery(site))
        {
            query.QueryText = queryText;
            query.KeywordInclusion = KeywordInclusion.AllKeywords;
            query.RowLimit = 500;                
            query.SelectProperties.Add("Path");
            query.SelectProperties.Add("IsDocument");


            SearchExecutor se = new SearchExecutor();
            rtc = se.ExecuteQuery(query);


            if (rtc.Count > 0)
            {
                var results = rtc.Filter("TableType", KnownTableTypes.RelevantResults);
                if (results != null && results.Count() == 1)
                    retResults.Load(results.First(), LoadOption.OverwriteChanges);

            }

        }

    }

    return retResults;

}

What about FQL?

SharePoint allegedly supports FQL queries. So if you have legacy solutions that use FQL then these should be supported. You must set the EnableFQL property of the KeywordQuery class to true. Unfortunately, results are inconsistent. The results are returned every 4 searches, the other 3 no results are returned. There is additional set up needed in order for FQL to work.  A bigger issue with using FQL is that FQL searches will not work with any result sources that have Query Transformations. The reason for this is that Query Transformation are additional conditions that are appended to the query text. Many of the out of the box result sources including the default have transformations that use KQL (Keyword Query Language). Therefore when you execute a FQL search with a result source with a Query Transformation you will receive a QueryMalformedException. You must set up a result source without a transformation and use that when executing FQL searches. So FQL support is theoretical at this point of the beta.

The new SearchExecutor is built for executing multiple queries at once

So why the switch to the new SearchExecutor class? The big reason is to give you more flexibility to execute multiple queries either on the server or remotely using the new client object model for search. The code below demonstrates how to create and issue multiple queries to the server at once. The first query uses the default result source, the second query uses a different result source by setting the KeywordQuery’s SourceID property. It uses a method for getting a search result source from part one of this series. The SearchExecutor’s ExecuteQueries method takes a dictionary of strings and KewywordQuery objects. The strings are friendly names you can give the queries so you can refer to them in the returned dictionary of strings and ResultTableCollections. Also notice that you can check the QueryErrors collection on each returned ResultTableCollection. This is useful if you set the handleExceptions argument of the ExecuteQueries method to true. If you set this to false the first error that occurs on any of the queries will halt execution and throw the error to the caller. You should be aware that if you are sending a query to a remote source you must set up a trusted connection.

The code also illustrates the inefficiencies of creating multiple KeywordQuery objects to submit to multiple sources. Typically you would submit the same terms, returned properties, row limits, and other properties. It would be nice if there was a method to clone KeywordQuery objects and then just set the SourceID properties. A good opportunity for an extension method.

 

public static Dictionary<string,DataTable> ExecuteMultipleKeyWordQueries()
{

    DataTable tableResults = null;
    Dictionary<string, DataTable> retResults = new Dictionary<string, DataTable>();

    using (SPSite site = new SPSite("http://basesmc15"))
    {

        Dictionary<string, Query> queries = new Dictionary<string, Query>();
        Dictionary<string, ResultTableCollection> results;

        using (KeywordQuery query1 = new KeywordQuery(site))
        {
            query1.QueryText = "title:\"Viewer Testing with PDF\"";
            query1.RowLimit = 500;
            query1.SelectProperties.Clear();
            query1.SelectProperties.Add("Path");
            query1.SelectProperties.Add("IsDocument");
            query1.SelectProperties.Add("Title");
            SearchExecutor se = new SearchExecutor();

            queries.Add("local", query1);
            using (KeywordQuery query2 = new KeywordQuery(site))
            {

                query2.QueryText = "title:\"Viewer Testing with PDF\"";
                query2.RowLimit = 500;
                query2.SelectProperties.Clear();
                query2.SelectProperties.Add("Path");
                query2.SelectProperties.Add("IsDocument");
                query2.SelectProperties.Add("Title");
                Source remoteSource = GetSearchSource("remoteFarm");
                query2.SourceId = remoteSource.Id;
                queries.Add("remote", query2);

                results = se.ExecuteQueries(queries, true);

                foreach (KeyValuePair<string,ResultTableCollection> result in results)
                {
                    if (result.Value.Count > 0 && result.Value.QueryErrors.Count() == 0)
                    {
                        var resultTable = result.Value.Filter("TableType", KnownTableTypes.RelevantResults);
                        if (resultTable != null && resultTable.Count() == 1)
                        {
                            tableResults = new DataTable();
                            tableResults.Load(resultTable.First(), LoadOption.OverwriteChanges);
                            retResults.Add(result.Key, tableResults);
                        }
                    }
                }

            }
        }

    }

    return retResults;

}

 

It is all about Federation

The SearchExecutor is built for Federation of different search sources. Federation is key piece in making SharePoint search more scalable by eliminating the need to crawl and index other searchable sources. Designing the SearchExecutor  to easily execute and process the results of multiple queries, even FQL queries, helps you leverage Federation in your solutions. Even the CSOM has exposed this feature in the Microsoft.SharePoint.Client.Search assembly and namespace. Retrieving results is different in the CSOM since there is no Filter method exposed on the ResultsTableCollection due to the nature of the execution. You just use basic LINQ to get the appropriate DataTable. You must call the SearchExecutor’s ExecuteQuery method and then call the Context’s ExecuteQuery method. This looks strange but if you have used the CSOM in the past it makes sense.

Client Object Model Keyword Search

public static DataTable ExecuteKeywordQuery(string query)
{
    DataTable retResults = new DataTable();
    ClientContext con = new ClientContext("http://basesmc15");
    KeywordQuery kq = new KeywordQuery(con);

    kq.QueryText = query;
    kq.SelectProperties.Add("testcol");

    SearchExecutor se = new SearchExecutor(con);
    var results = se.ExecuteQuery(kq);

    con.Load(kq);
    con.Load(se);
    con.ExecuteQuery();

    var result = from r in results.Value where r.TableType == KnownTableTypes.RelevantResults select r;

    if (result != null)
    {
        //get column names
        foreach (var col in result.First().ResultRows.First())
        {
            retResults.Columns.Add(col.Key.ToString(), col.Value != null ? col.Value.GetType() : typeof(object));
        }

        foreach (var row in result.First().ResultRows)
        {
            retResults.LoadDataRow(row.Values.ToArray(), LoadOption.Upsert);

        }

        retResults.AcceptChanges();

    }

    return retResults;

}

SharePoint 2013 search has many new features. I will be posting more about these soon.

5 comments:

Mikael Svenson said...

Very nice write-up and overview of how to execute searches!

Anonymous said...

thanks, was looking for this

Anonymous said...

Excelent post, Thanks

Muneeb said...

Hi,
How I can pass a document library column name in the query text.
I have a document library and each document is loaded with its Job_Id. I want to search on the basis of Result source and Job_Id. This job_Id is dynamic field.

Swati Sri said...

how can I restrict the result scope to return result only from one subsite i.e. http://spportal/sites/subsite

Currently even if I apply the site restriction under KeywordQuery(subsite) it returns from entire web application site content matches.

Post a Comment