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!

4 comments:

  1. You have to express more your opinion to attract more readers, because just a video or plain text without any personal approach is not that valuable. But it is just form my point of view

    ReplyDelete
  2. Nice fill someone in on and this mail helped me alot in my college assignement. Say thank you you seeking your information.

    ReplyDelete
  3. Good fill someone in on and this post helped me alot in my college assignement. Thank you on your information.

    ReplyDelete
  4. Good article made my day. Thanks

    ReplyDelete