TafitiのUIでSharepointの検索を実行する

MicrosoftSilverlight + LiveSearchで実装した検索エンジンUIの実験として
http://www.tafiti.com/
というサイトが公開されている。


このサイトの元になるソースがオープンソースとして提供されているということですこし調べてみた。
http://www.codeplex.com/WLQuickApps/Wiki/View.aspx?title=Tafiti%20Overview&referringTitle=Home


このtafitiのインタフェースで、MOSSやSearchServerの検索をおこないたいのだが、
Current Releaseの6.0 Quick Application (Beta)のソースのままでは無理のようだ。


しかし、
http://www.codeplex.com/WLQuickApps/SourceControl/ListDownloadableCommits.aspx
のページからChange Set 16061以降のソースをダウンロードすると、
WLQuickApps.Tafiti.WebSite - MOSS
というWebSiteプロジェクトが含まれているので、これを使うと比較的簡単にMOSSの検索を行えるようになる。


試してみたところ、検索結果の総件数が100件に固定されていたり、続きの検索結果の取得ができないため、
実運用としては少し厳しいように思えたので、もともとのLiveSearch版に手を加えて、総件数の表示や続きの取得ができるようにしてみた。


また、www.tafiti.comでは、検索結果を右端のShelfと呼ばれる部分に一時的に保存(LiveIDでログインすれば恒久的に)できるのだが、
codeplexからダウンロードしたソースは、この機能の実装が不完全なようでうまく動作しない。


Visual Studio2008でJavaScriptデバッグ実行し、www.tafiti.comのJavascriptと比較することで一時的な保存はできるようになった。
JavaScriptの修正点は結構あったので、気が向いたらあとでdiffをのせる)


感想
Shelfの機能は結構使いやすい。
mailやblogに送信する機能もMOSSと連動して動作できるようにすると結構便利かも。


参考サイト
Silverlight2 Beta 1 (1.0や1.1では日本語が表示できません)
http://www.microsoft.com/silverlight/resources/installationfiles.aspx?v=2.0


使ってみよう! Windows Live SDK/API 第3回 Tafiti Search Visualization
http://gihyo.jp/dev/serial/01/wl-sdk/0003?page=1


Sharepoint Search from Tafiti
http://blogs.claritycon.com/blogs/kevin_marshall/archive/2007/12/20/3555.aspx


MSの新感覚検索サイト「Tafiti」
http://www.moongift.jp/2007/12/tafiti/


マイクロソフト、新検索インターフェース「Tafiti」公開
http://japan.cnet.com/marketing/story/0,3800080523,20354899,00.htm



Search.aspx.cs

using System;
using System.Collections.Generic;
using System.Net;
using System.Web;
using System.Xml;
using System.Web.Script.Serialization;
using System.Web.Services.Protocols;

using WLQuickApps.Tafiti.WebSite;
using LiveSearch;

using WebReference to your SearchServer; //MOSSの検索サービスのWebReferenceの名前空間
using System.Text.RegularExpressions;

public partial class Search : System.Web.UI.Page
{
    /// <summary>
    /// Proxy search requests to search.live.com.
    /// 
    /// We pass thru the query string. We append the PathInfo (if any) to http://search.live.com to 
    /// support searching different scopes (e.g., images, news). Here are a few examples:
    ///   /Search.aspx?q=seattle          mimics http://search.live.com/results.aspx?q=seattle
    ///   /Search.aspx/images?q=seattle   mimics http://search.live.com/images/results.aspx?q=seattle
    /// 
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void Page_Load(object sender, EventArgs e)
    {
        Utility.VerifyIsSelfRequest();

        string scope = Request.PathInfo.Remove(0, 1); // remove beginning '/'
        if (UseSoapService(scope))
        {
            string query = Request.QueryString["q"];
            if (scope.ToLower() == "feeds")
            {
                query += " feed:";
            }

            SourceType sourceType = GetSourceType(scope);
            int offset = int.Parse(Request.QueryString["first"]);
            int count = int.Parse(Request.QueryString["count"]);
            SoapSearch(sourceType, query, offset, count, scope);
        }
        else
        {
            throw new HttpException((int)HttpStatusCode.BadRequest, "Bad Request");
        }
    }
    
    private bool UseSoapService(string scope)
    {
        switch (scope.ToLower())
        {
            case "web":
            case "images":
            case "news":
            case "phonebook":
            case "feeds":
                return true;

            default: return false;
        }
    }

    private SourceType GetSourceType(string scope)
    {
        switch (scope.ToLower())
        {
            case "web": return SourceType.Web;
            case "images": return SourceType.Image;
            case "news": return SourceType.News;
            case "phonebook": return SourceType.PhoneBook;
            case "feeds": return SourceType.Web;
            default: throw new ArgumentException("unsupported search scope");
        }
    }

    // For compatibility with LiveSearch's XML-format responses
    private string GetDocumentSetSource(SourceType sourceType)
    {
        switch (sourceType)
        {
            case SourceType.Web:
            case SourceType.Image:
            case SourceType.PhoneBook:
            case SourceType.InlineAnswers:
                return "FEDERATOR_MONARCH";
            case SourceType.News:
                return "FEDERATOR_BACKFILL_NEWS";
            default:
                throw new ArgumentException("unsupported source type");
        }
    }

    private ResultFieldMask GetResultFieldMask(SourceType sourceType)
    {
        switch (sourceType)
        {
            case SourceType.Web:
                return ResultFieldMask.Title | ResultFieldMask.Description | ResultFieldMask.Url;
            case SourceType.Image:
                return ResultFieldMask.Image | ResultFieldMask.Url;
            case SourceType.News:
                return ResultFieldMask.Title | ResultFieldMask.Description | ResultFieldMask.Url | ResultFieldMask.Source;
            case SourceType.PhoneBook:
                return ResultFieldMask.Title | ResultFieldMask.Description | ResultFieldMask.Url | ResultFieldMask.Location;
            default:
                throw new ArgumentException("unsupported source type");
        }
    }

    private void SoapSearch(SourceType sourceType, string query, int first, int count, string domain)
    {
        try
        {
            SourceRequest[] sr = new SourceRequest[1];
            sr[0] = new SourceRequest();
            sr[0].Source = sourceType;
            sr[0].Offset = first;
            sr[0].Count = count;
            sr[0].ResultFields = GetResultFieldMask(sourceType);
           
           //修正部分開始 修正部分開始 修正部分開始 修正部分開始 修正部分開始
            string qXMLString = "<QueryPacket xmlns='urn:Microsoft.Search.Query'>" +
                "<Query><SupportedFormats><Format revision='1'>" +
                "urn:Microsoft.Search.Response.Document:Document</Format>" +
                "</SupportedFormats><Context><QueryText language='ja-JP' type='STRING'>" +
                query +
                "</QueryText></Context><Range id='result'><StartAt>" + (first + 1).ToString() + "</StartAt>" +
                "<Count>" + count + "</Count></Range></Query></QueryPacket>";
                
            QueryService queryService = new QueryService();
            queryService.Credentials = new System.Net.NetworkCredential("MOSSにアクセスするユーザ", "パスワード", "ドメイン");

            //Queryではdescriptionが取得できず
            //QueryExでは総件数の取得ができないので、両方実行する
            string resultXml = queryService.Query(qXMLString);
            System.Data.DataSet resultDs = queryService.QueryEx(qXMLString);

            LiveXmlSearchResults result = CreateLiveXmlFromMossSearchResults(resultDs, resultXml);

           //修正部分終了 修正部分終了 修正部分終了 修正部分終了 修正部分終了
           
            JavaScriptSerializer serializer = new JavaScriptSerializer();
            string json = serializer.Serialize(result);
            byte[] jsonUtf8 = System.Text.Encoding.UTF8.GetBytes(json);

            Response.StatusCode = 200;
            Response.ContentType = "text/javascript";
            Response.OutputStream.Write(jsonUtf8, 0, jsonUtf8.Length);
        }
        catch (SoapException e)
        {
            throw new HttpException((int)HttpStatusCode.InternalServerError, "Internal Server Error", e);
        }
        catch (WebException e)
        {
            throw new HttpException((int)HttpStatusCode.InternalServerError, "Internal Server Error", e);
        }
    }

    private LiveXmlSearchResults CreateXmlSearchResults(SourceType sourceType, SourceResponse sourceResponse, string domain)
    {
        LiveXmlSearchResults searchResults = new LiveXmlSearchResults();
        searchResults.searchresult.documentset._source = GetDocumentSetSource(sourceType);
        searchResults.searchresult.documentset._count = sourceResponse.Results.Length.ToString();
        searchResults.searchresult.documentset._start = sourceResponse.Offset.ToString();
        searchResults.searchresult.documentset._total = sourceResponse.Total.ToString();

        if (sourceResponse.Results.Length > 0)
        {
            LiveXmlResult[] results;
            switch (sourceType)
            {
                case SourceType.Web:
                    results = ConvertWebResults(sourceResponse, domain);
                    break;

                case SourceType.Image:
                    results = ConvertImageResults(sourceResponse, domain);
                    break;

                case SourceType.News:
                    results = ConvertNewsResults(sourceResponse, domain);
                    break;

                case SourceType.PhoneBook:
                    results = ConvertPhoneBookResults(sourceResponse, domain);
                    break;

                default:
                    results = null;
                    break;
            }

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

        return searchResults;
    }

    private static LiveXmlResult[] ConvertPhoneBookResults(SourceResponse sourceResponse, string domain)
    {
        return Search.ConvertWebResults(sourceResponse, domain);
    }

    private static LiveXmlResult[] ConvertNewsResults(SourceResponse sourceResponse, string domain)
    {
        LiveXmlResult[] results = new LiveXmlResult[sourceResponse.Results.Length];
        for (int i = 0; i < sourceResponse.Results.Length; i++)
        {
            Result sourceResult = sourceResponse.Results[i];
            LiveXmlResult result = new LiveXmlResult();
            result.domain = domain;
            result.title = sourceResult.Title;
            result.source = sourceResult.Source;
            result.description = sourceResult.Description;
            result.url = sourceResult.Url;
            results[i] = result;
        }
        return results;
    }

    private static LiveXmlResult[] ConvertImageResults(SourceResponse sourceResponse, string domain)
    {
        LiveXmlResult[] results = new LiveXmlResult[sourceResponse.Results.Length];
        for (int i = 0; i < sourceResponse.Results.Length; i++)
        {
            Result sourceResult = sourceResponse.Results[i];
            LiveXmlResult result = new LiveXmlResult();
            if (sourceResult.Image != null)
            {
                result.domain = domain;
                result.width = sourceResult.Image.ThumbnailWidthSpecified ? sourceResult.Image.ThumbnailWidth.ToString() : "100"; // make up a value!
                result.height = sourceResult.Image.ThumbnailHeightSpecified ? sourceResult.Image.ThumbnailHeight.ToString() : "100";
                result.url = sourceResult.Image.ThumbnailURL;
                result.imageUrl = sourceResult.Image.ThumbnailURL;
            }
            result.url = sourceResult.Url;
            results[i] = result;
        }
        return results;
    }

    private static LiveXmlResult[] ConvertWebResults(SourceResponse sourceResponse, string domain)
    {
        LiveXmlResult[] results = new LiveXmlResult[sourceResponse.Results.Length];
        for (int i = 0; i < sourceResponse.Results.Length; i++)
        {
            Result sourceResult = sourceResponse.Results[i];
            LiveXmlResult result = new LiveXmlResult();
            result.domain = domain;
            result.title = sourceResult.Title;
            result.description = sourceResult.Description;
            result.url = sourceResult.Url;
            results[i] = result;
        }
        return results;
    }

    //修正部分開始 修正部分開始 修正部分開始 修正部分開始 修正部分開始
    private LiveXmlSearchResults CreateLiveXmlFromMossSearchResults(System.Data.DataSet mossSearchResults, string resultXml)
    {
        XmlDocument xmldoc = new XmlDocument();
        xmldoc.LoadXml(resultXml);

        int count = 0;
        int start = 0;
        int total = 0;

        XmlNamespaceManager nsmgr = new XmlNamespaceManager(xmldoc.NameTable);
        nsmgr.AddNamespace("a", "urn:Microsoft.Search.Response");
        nsmgr.AddNamespace("b", "urn:Microsoft.Search.Response.Document");

        XmlNode startAtNode = xmldoc.SelectSingleNode("//a:StartAt", nsmgr);
        if (startAtNode != null)
        {
            if (!string.IsNullOrEmpty(startAtNode.InnerText))
            {
                start = int.Parse(startAtNode.InnerText);
            }
        }

        XmlNode countNode = xmldoc.SelectSingleNode("//a:Count", nsmgr);
        if (countNode != null)
        {
            if (!string.IsNullOrEmpty(countNode.InnerText))
            {
                count = int.Parse(countNode.InnerText);
            }
        }

        XmlNode totalAvailableNode = xmldoc.SelectSingleNode("//a:TotalAvailable", nsmgr);
        if (totalAvailableNode != null)
        {
            if (!string.IsNullOrEmpty(totalAvailableNode.InnerText))
            {
                total = int.Parse(totalAvailableNode.InnerText);
            }
        }

        LiveXmlSearchResults searchResults = new LiveXmlSearchResults();
        searchResults.searchresult.documentset._source = "FEDERATOR_MONARCH";
        searchResults.searchresult.documentset._count = count.ToString();
        searchResults.searchresult.documentset._start = start.ToString();
        searchResults.searchresult.documentset._total = total.ToString();


        LiveXmlResult[] results = ConvertMOSSWebResults(mossSearchResults.Tables[0]);
        if (results != null && results.Length > 0)
        {
            searchResults.searchresult.documentset.document = results;
        }
        else
        {
            searchResults.searchresult.documentset.document = new LiveXmlResult();
        }
        return searchResults;
    }

    private static LiveXmlResult[] ConvertMOSSWebResults(System.Data.DataTable mossSearchResultsTable)
    {
        LiveXmlResult[] results = new LiveXmlResult[mossSearchResultsTable.Rows.Count];
        for (int i = 0; i < mossSearchResultsTable.Rows.Count; i++)
        {
            LiveXmlResult result = new LiveXmlResult();

            string description = string.Empty;

            description =
                GetDescription(mossSearchResultsTable.Rows[i]["Description"].ToString(), mossSearchResultsTable.Rows[i]["HitHighlightedSummary"].ToString());
            result.title = mossSearchResultsTable.Rows[i]["Title"].ToString();
            result.description = description;
            result.url = mossSearchResultsTable.Rows[i]["Path"].ToString();
            result.domain = "web";

            results[i] = result;
        }
        return results;
    }

    private static string GetDescription(string description, string hitHighlightedSummary)
    {
        string strippedText = string.Empty;
        if (string.IsNullOrEmpty(description))
        {

            Regex regEx = new Regex("<[^>]*>", RegexOptions.IgnoreCase);

            strippedText = regEx.Replace(hitHighlightedSummary, "...");
            return strippedText;
        }
        else
            return description;
    }
   //修正部分終了 修正部分終了 修正部分終了 修正部分終了 修正部分終了
   
    class LiveXmlSearchResults
    {
        public LiveXmlSearchResult searchresult = new LiveXmlSearchResult();
    }

    class LiveXmlSearchResult
    {
        public LiveXmlDocumentSet documentset = new LiveXmlDocumentSet();
    }

    class LiveXmlDocumentSet
    {
        public string _source;
        public string _start;
        public string _total;
        public string _count;
        public object document; // if _count is 1, this is an instance of LiveXml...Result, otherwise it is an array
    }

    class LiveXmlResult
    {
        public string domain;
        public string title;
        public string description;
        public string source;
        public string imageUrl;
        public string height;
        public string width;
        public string url;
    }

}