TafitiのUIでSharepointの検索を実行する
MicrosoftがSilverlight + 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; } }