December 27, 2009

SharePoint People Search Autocomplete

Back in July, Jan Tielens demonstrated how to write an autocomplete mechanism for SharePoint search. Muhimbi then proposed a greatly enhanced version 2.

In our version, we have enhanced the code just a little to enable the following:

  • paging: you can display results n at a time, and offer a link to go to the next page

Next

  • sorting: allow users to refresh results by alphabetical order, by department, etc. (you can add your own sorting criteria)

SortBy

  • search across all people metadata:  let people either search on name, skills, office number, phone number, or any other indexed metadata you include in the search query

Terms

  • Rich formatted results: display picture, presence awareness (Office Communicator) and links to internal or external systems (Yammer, Skype, Facebook, etc.)

RichResult

  • Refine search results: allow users to click on any metadata on the autocomplete search to drill down into specific criteria

drilldown

Example of the final result:

PeopleAutocomplete

To implement this version of the people search autocomplete, copy the code below, modify it as you need (mandatory: modify the server name), and paste it on a Content Editor web part.

December 12, 2009

Using Photoshop to Design your own Theme for ICE

In a previous post, I’ve introduced a new open source, Silverlight-based visualization framework called ICE (Information Connections Engine). Since some people had difficulties creating their own style, I created this short video which not only details how to modify the icon library, but shows how to do so using Photoshop and Microsoft Blend 3.

December 10, 2009

Using SharePoint Search Web Service to Surface Blog Posts or other Content Type

One of the most powerful though untapped features of SharePoint search is its web service. SharePoint search web service url looks like this: http://Sp_Server/_vti_bin/search.asmx. JavaScript aficionadi like myself get excited when they hear about web services, because we immediately start thinking of the rich functionalities we can then easily add to any HTML page, or to SharePoint with a Content Editor Web Part (by far my favorite web part). Why the excitement? Mainly for 2 reasons:
  • Contrary to other data interaction mechanisms like ODBC, BDC, etc., web services are self-describing. All you need to know about methods, inputs and outputs is in the WSDL file (check http://Sp_Server/_vti_bin/search.asmx?wsdl). No need to contact the developer or database administrator for a password or parameters type.
  • Armed with Ajax (remember, Ajax is just JavaScript, nothing to brag about or to fear), and, say, a Content Editor Web Part (or just a basic HTML page), you don’t even need to have access rights to any server to get this functionality on your site. Everything is done on the client side.
So let’s see how we can use SharePoint search web service to search only from a certain content type on your site, like blog posts, wikis, threaded discussions, documents, pictures, etc. (see basic list of available content types here). Since the web service returns some XML, it’s up to you to decide how and where to display the results. In this example, the result of the code below looks like this:
capture
 Generate the query
In theory, you’d need to study a little bit how to build a QueryPacket, or learn about the SQL Search language. In reality, you don’t have to study anything at all, thanks to query generators like SharePoint Search Service Tool or Search Coder. I often use both, since they have complementary functionalities. So it shouldn’t take you too long to come up with the following query. Notice that “AND (CONTAINS (ContentType,'"post"'))” is the trick to filter by content type.
<QueryPacket xmlns="urn:Microsoft.Search.Query" Revision="1000">
<Query domain="QDomain">
<SupportedFormats>
<Format>urn:Microsoft.Search.Response.Document.Document</Format>
</SupportedFormats>
<Context>
<QueryText language="en-US" type="MSSQLFT">
<![CDATA[ 
SELECT Title, Rank, Size, Description, Write, Path, 
PersonalSpace, Author, Title, Path, Created, 
CreatedBy, PictureURL, Account, EmployeeID 
FROM portal..scope()  
WHERE FREETEXT(DefaultProperties, 'My search terms') 
AND  ( ("SCOPE" = 'All Sites') )  AND (CONTAINS (ContentType,'"post"')) 
ORDER BY "Rank" Desc, "Created" Desc" 
]]>
</QueryText>
</Context>

<Range><StartAt>1</StartAt><Count>10</Count></Range>
<EnableStemming>true</EnableStemming>
<TrimDuplicates>true</TrimDuplicates>
<IgnoreAllNoiseQuery>true</IgnoreAllNoiseQuery>
<ImplicitAndBehavior>true</ImplicitAndBehavior>
<IncludeRelevanceResults>true</IncludeRelevanceResults>
<IncludeSpecialTermResults>true</IncludeSpecialTermResults>
<IncludeHighConfidenceResults>true</IncludeHighConfidenceResults>
</Query>
</QueryPacket>





Call the Web Service


In a previous blog, I’ve showed how to use Darren’s JavaScript library to interact with SharePoint web services. I could have done the same here, especially since he developed a library specifically for search, but I chose to use JQuery instead. Why? No reason, I just like to explore different technologies ;)


 Final Result


So now all you have to do is copy the  code below, change the server name and paste the code into a Content Editor Web Part (or any other HTML page). Notice that while this example is about filtering by content type, the same technique can be used to filter by scope and any metadata you wish. In fact, you’ll quickly realize that this search web service is way more powerful than SharePoint Content Query Web Part, as search doesn’t care about site collection boundaries and has access to a wide variety of filters.



<script language="javascript">
// _spBodyOnLoadFunctionNames.push is a SharePoint OOTB function 
// that ensures the function is called only after the DOM has been loaded
_spBodyOnLoadFunctionNames.push("DisplayBlogSearchResults");

// Change these parameters as needed
var maxResultsToDisplayBlogs = 5;
var webSite = “http://url.of.site/;

function DisplayBlogSearchResults() {

// the search terms is passed in the query string (e.g., blogsearch?k=tax+reform)
var query = unescape(querySt("k"));    

var queryXML = 
"<QueryPacket xmlns=\"urn:Microsoft.Search.Query\" Revision=\"1000\">"+
"    <Query domain=\"QDomain\">"+
"        <SupportedFormats><Format>urn:Microsoft.Search.Response.Document.Document"+
" </Format></SupportedFormats>"+
"        <Context>"+
"     <QueryText language=\"en-US\" type=\"MSSQLFT\"><![CDATA[ "+
"SELECT Title, Rank, Size, Description, Write, Path, PersonalSpace, "+
"Author, Title, Path, Created, CreatedBy, "+
" PictureURL, Account, EmployeeID FROM "+
" portal..scope() " + 
//" WHERE CONTAINS ('\"" + query + "\"') " +
" WHERE FREETEXT(DefaultProperties, '" + query + "') " +
" AND  ( (\"SCOPE\" = 'All Sites') )  AND (CONTAINS (ContentType,'\"post\"'))" +
" ORDER BY \"Rank\" Desc, \"Created\" Desc"  +
" ]]>" +
"      </QueryText>" +
"        </Context>"+
"        <Range><StartAt>1</StartAt><Count>" + 
maxResultsToDisplayBlogs + "</Count></Range>"+
"        <EnableStemming>true</EnableStemming>"+
"<TrimDuplicates>true</TrimDuplicates>"+
"<IgnoreAllNoiseQuery>true</IgnoreAllNoiseQuery>"+
"<ImplicitAndBehavior>true</ImplicitAndBehavior>"+
"<IncludeRelevanceResults>true</IncludeRelevanceResults>"+
"<IncludeSpecialTermResults>true</IncludeSpecialTermResults>"+
"<IncludeHighConfidenceResults>true</IncludeHighConfidenceResults>"+
"</Query></QueryPacket>";

var soapEnv =
"<soap:Envelope xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' " +
"xmlns:xsd='http://www.w3.org/2001/XMLSchema' " +
"xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'>"+
" <soap:Body>"+
"<QueryEx xmlns='http://microsoft.com/webservices/OfficeServer/QueryService'>"+
"     <queryXml>" + escapeHTML(queryXML) + "</queryXml>"+
"   </QueryEx>"+
" </soap:Body>"+
"</soap:Envelope>";

$.ajax({
url: webSite +"/_vti_bin/search.asmx",
type: "POST",
dataType: "xml",
data: soapEnv,
complete: processResult,
contentType: "text/xml; charset=\"utf-8\""
});          


}

// processResult is called async. when the web service returns something
function processResult(xData, status) {

var TotalResults = 0;                
var xmlDoc = xData.responseXML;
var docs = xmlDoc.selectNodes("//RelevantResults");
// Total Results returned 
// (should be equal or less than <Count> parameter of search query)
TotalResults = docs.length;

// Total available results (while the query only returns
// the first <Count> results, there might be more available)
var TotalAvailable = 0;
var docTotalAvailable  = xmlDoc.selectNodes("//xs:element[@name='RelevantResults']");
if (docTotalAvailable.length != 0) {
TotalAvailable = docTotalAvailable[0].getAttribute("msprop:TotalRows"); 
}

var strDisplay="";
if (TotalResults >0) {
strDisplay = "<table width='100%' style='BORDER: #8ebbf5 1px solid'><tr>";
strDisplay += "<td><img src='http://atgdev-intranet.imf.org/_layouts/images/buddychat.jpg' ";
strDisplay += "style='float:left;vertical-align:middle'/>"; 
strDisplay += "<span style='font-size:12px'>"; 
strDisplay += "<strong>You may also be interested by these " + TotalResults 
strDisplay += " blogs:</strong></span></td>";
strDisplay += "</tr>";
}

for(var i = 0; i < TotalResults ; i++){
var title = docs[i].selectSingleNode("TITLE") != null ? docs[i].selectSingleNode("TITLE").text : "TITLE not found";
var path = docs[i].selectSingleNode("PATH") != null ? docs[i].selectSingleNode("PATH").text : "PATH not found";
var creationDate = docs[i].selectSingleNode("CREATED") != null ? docs[i].selectSingleNode("CREATED").text : "CREATED not found";
var author = docs[i].selectSingleNode("AUTHOR") != null ? docs[i].selectSingleNode("AUTHOR").text : "PATH not found";

strDisplay += "<tr><td>";
strDisplay += "<img src='/_layouts/images/bullet.gif' style='vertical-align:middle' />&nbsp;";
strDisplay += "<a href='" + path + "'>" + title + "</a>";
strDisplay += "<div style='color:#dbdbdb;text-align:right'>written on " + formatDateString(creationDate)
strDisplay += " by <span style='color:#545454'>" + author + "</span></div>";
strDisplay += "</td></tr>";

}
// Verify if we have displayed the total available or not
if (TotalAvailable > TotalResults) {
strDisplay += "<tr style='text-align:right'><td><a href=''><br />See all " + TotalAvailable + " results...</a></td></tr>"
} 
if (TotalResults >0) {
strDisplay += "</table>";
}

// Display result is specific DIV (id=idBlogSearchResults). Could be located anywhere in your page.
$("#idBlogSearchResults").html(strDisplay);

}

function querySt(ji) {
hu = window.location.search.substring(1);
gy = hu.split("&");
for (i=0;i<gy.length;i++) {
ft = gy[i].split("=");
if (ft[0] == ji)  return ft[1];
}
}

function escapeHTML (str) {
return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}

function unescapeHTML (str) {
return str.replace(/&lt;/g,'<').replace(/&gt;/g,'>');
}

function formatDateString(strDate) {
var yearStr = strDate.substr(0, 4);     
var monthStr = strDate.substr(5, 2);     
var dayStr = strDate.substr(8, 2);           
return monthStr + "/" + dayStr + "/" + yearStr; 
} 

</script>





Have fun!

November 21, 2009

Visualization On ICE

3882464062_fdfd24ec43_m_d[1]Our little “ICE” open source project (“Information Connections Engine”) is growing in popularity! ICE is a visualization framework based on Silverlight, which allows any .net developer to visualize any sort of data and their connections in just a few hours of work. It’s free, it’s fun, it’s easy.

Check out this 3 min teaser to have an idea of what ICE is capable of:

There’s a complete step by step tutorial that should get you started real fast with your first “application on ICE”.

What’s most amazing about such a flexible and easy to use tool is to watch what developers are doing with it. The mathematical models and countless configuration options, along with the possibility to use XAML to visualize and animate nodes and links according to your own creativity, result in applications we had never anticipated. I’m pretty sure someone will create a little game using ICE soon…. anyone?

November 20, 2009

Getting Started…

HerveSo here we are, I’m finally giving in to my friends' and also my inner voice request to start my own blog. I know I won't have the time to write too often. I know that my interests in technology are so broad and so diverse that I might have difficulties capturing a specific audience (who else is equally interested by the impact on productivity of social networking in the enterprise and by how optimizing an AJAX call to a SOAP Web Service?). But it doesn't matter. This blog will at least satisfy my old frustration of having so much of my work or thoughts undocumented or even unchallenged.

So there you have it. Off the top of my head, I think you're most likely to read about SharePoint, web services, .Net, Silverlight, and Ajax technologies. And also high level views on social computing, enterprise mashup ecosystems, mobility, visualization technologies and cloud computing. And whatever else crosses my mind, after all it's MY blog!
Ah, one last point: don't fool yourself, you will never read this blog again unless you subscribe to the RSS feed, or unless you use the email subscription on the right column. There's simply no other way to stay connected to a blog.