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:
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' /> ";
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,'&').replace(/</g,'<').replace(/>/g,'>');
}
function unescapeHTML (str) {
return str.replace(/</g,'<').replace(/>/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!