В SharePoint 2013 появился REST веб-сервис, который позволяет делать поисковые запросы из JavaScript. В SharePoint 2010 есть только search.asmx, который требует генерировать и парсить большой объём XML (в лучших традициях SharePoint).

Чтобы упростить жизнь разработчику клиентских компонентов я написал REST веб-сервис для SharePoint 2010.

Реализация

За основу взял метод, который описывал ранее - Javascript-enabled SharePoint WCF services.

Контракт у сервиса очень простой:

[ServiceContract]
public interface ISearch
{
    [OperationContract]
    [WebGet(BodyStyle = WebMessageBodyStyle.Bare, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
    Stream Query(string q, int top, int skip, string select, string orderBy, bool includeRefiners, string refiners);
}

Параметры вызова:

  • q – текст запроса (обязательно).
  • top – количество результатов.
  • skip – с какой позиции в выборке отдавать результаты.
  • select – через запятую названия managed properties в результатах.
  • orderBy – через запятую названия managed properties по которым сортировать результат, после имени можно указать desc для сортировки по убыванию.
  • includeRefinerstrue или false, возвращать результаты уточнений или нет.
  • refiners - через запятую названия managed properties для формирования уточнений.

Реализация:

public System.IO.Stream Query(string q, int top, int skip, string select, string orderBy,
                    bool includeRefiners, string r) 
{
    using (new SPMonitoredScope("Execute Query Method"))
    {
        var site = SPContext.Current.Site;
        var result = GetSearchResults(site, q, top, skip, select, orderBy, includeRefiners, r);
        return ToJson(result);
    }
}

Метод GetSearchResults довольно простой, он передает параметры запроса в объект KeywordQuery и получает результат.

private static ResultTableCollection GetSearchResults(SPSite site, string q, int top, int skip, string select, string orderBy, bool includeRefiners, string r)
{
    var query = new KeywordQuery(site);
    query.QueryText = q;
    query.StartRow = skip;
    if (top > 0)
    {
        query.RowLimit = top;
    }

    FillSelectProperties(select, query);

    FillSortList(orderBy, query);

    query.ResultTypes = ResultType.RelevantResults;
    if (includeRefiners)
    {
        query.ResultTypes |= ResultType.RefinementResults;
        query.Refiners = r;
    }
    return query.Execute();
}

Методы FillSelectProperties и FillSortList парсят значения из строки запроса и заполняют свойства объекта KeywordQuery.

private static void FillSortList(string orderBy, KeywordQuery query)
{
    if (!string.IsNullOrEmpty(orderBy))
    {
        var orderByParts = orderBy.Split(new[] { ',' }, System.StringSplitOptions.RemoveEmptyEntries);
        query.SortList.Clear();
        foreach (var part in orderByParts)
        {
            var pair = part.Split(' ');
            if (pair.Length > 1 && string.Compare(pair[1], "desc", System.StringComparison.OrdinalIgnoreCase) == 0)
            {
                query.SortList.Add(pair[0], SortDirection.Descending);
            }
            else
            {
                query.SortList.Add(pair[0], SortDirection.Ascending);
            }
        }
    }
}

private static void FillSelectProperties(string select, KeywordQuery query)
{
    if (!string.IsNullOrEmpty(select))
    {
        var properties = select.Split(new[] { ',' }, System.StringSplitOptions.RemoveEmptyEntries);
        query.SelectProperties.Clear();
        query.SelectProperties.AddRange(properties);
    }
}

Теперь самая интересная часть – преобразование результатов в JSON. Для сериализации не подойдет стандартный DataContractJsonSerializer, он не умеет сериализовывать DataSet и DataTable в компактном виде. Со времен появления ASP.NET Ajax в библиотеке появился класс JavaScriptSerializer. Он не очень быстр, зато его легко расширять, чтобы получать ровно ту разметку, которая нужна и не требуется дополнительных библиотек.

Метод ToJson:

private static Stream ToJson(ResultTableCollection value)
{
    JavaScriptSerializer ser = new JavaScriptSerializer();

    List<JavaScriptConverter> converters = new List<JavaScriptConverter>();

    converters.Add(new DataRowConverter());
    converters.Add(new ResultTableCollectionConverter());
    ser.RegisterConverters(converters);

    var resultStream = new MemoryStream();
    var writer = new StreamWriter(resultStream);
    writer.Write(ser.Serialize(value));

    writer.Flush();
    resultStream.Position = 0;

    return resultStream;
}

Для сериализации используется два дополнительных конвертера.

ResultTableCollectionConverter:
internal class ResultTableCollectionConverter : JavaScriptConverter
{
    public override IEnumerable<Type> SupportedTypes
    {
        get { return new Type[] { typeof(ResultTableCollection) }; }
    }

    public override object Deserialize(IDictionary<string, object> dictionary, Type type,
                                       JavaScriptSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
    {
        var resultTableCollection = obj as ResultTableCollection;
        Dictionary<string, object> propValues = new Dictionary<string, object>();

        if (resultTableCollection != null)
        {
            if (resultTableCollection.Exists(ResultType.RelevantResults))
            {
                var resultTable = resultTableCollection[ResultType.RelevantResults];
                propValues.Add("TotalResults", resultTable.TotalRows);
                propValues.Add("Results", resultTable.Table.Rows.OfType<DataRow>());
            }
            if (resultTableCollection.Exists(ResultType.RefinementResults))
            {
                var refinersTable = resultTableCollection[ResultType.RefinementResults];
                propValues.Add("TotalRefiners", refinersTable.TotalRows);
                propValues.Add("Refiners", refinersTable.Table.Rows.OfType<DataRow>());
            }
        }

        return propValues;
    }
}

DataRowConverter:

internal class DataRowConverter : JavaScriptConverter
{
    public override IEnumerable<Type> SupportedTypes
    {
        get { return new Type[] { typeof(DataRow) }; }
    }

    public override object Deserialize(IDictionary<string, object> dictionary, Type type,
                                       JavaScriptSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
    {
        DataRow dataRow = obj as DataRow;

        return dataRow != null
               ? dataRow.Table.Columns.OfType<DataColumn>().ToDictionary(c => c.ColumnName, c => dataRow[c])
               : new Dictionary<string, object>();
    }
}

 

Применение

Возможность делать поисковые запросы на клиенте позволяет создавать чисто клиентские веб-части, которые не требуют для работы серверного кода. Для реализации этой идеи я реализовал одну базовую веб-часть, которая работает на jQuery и jsRender, и позволяет задавать параметры и настройки на уровне .webpart файла. Таким образом одни раз установив Farm Solution с веб-сервисом и базовой веб-частью появляется возможность добавлять веб-части с клиентским кодам в виде Sandbox решений.

Пример такого решения я, как обычно, выложил на codeplex:

Исходники - https://spsamples.codeplex.com/SourceControl/latest#SearchWidgetWebParts/

Релиз - https://spsamples.codeplex.com/releases/view/118068

Кстати это решение, в слегка измененном виде, уже больше года работает на крупном портале.

Заключение

Это последняя часть серии про использование поиска в SharePoint 2010. Часть 1 и Часть 2 по ссылкам.

Теги : ajax, javascript, WCF, web parts, jQuery, поиск, SharePoint