Friday, September 17, 2010

Google Powered Site Search with jQuery

Google Powered Site Search with jQuery

By far one of the most requested features by Tutorialzine’s readers, is building a site-wide search. One way to do it, is to build it yourself from the ground up. That is, to use a server-side language like PHP and run search queries on your database, displaying the results to the user.

Another way is to use the services of the one search engine that already knows everything about everyone. Yep, you guessed it. In this tutorial we are using Google’s AJAX Search API, to create a custom search engine, with which you can search for web results, images, video and news items on your site.

The HTML

Lets start with the HTML markup. From the new HTML5 doctype, we move on to defining the title of the document and including the stylesheet to the head section of the page.

search.html

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Google Powered Site Search | Tutorialzine Demo</title>

<link rel="stylesheet" type="text/css" href="styles.css" />

</head>
<body>

<div id="page">

  <h1>Google Powered Site Search</h1>

  <form id="searchForm" method="post">
      <fieldset>

          <input id="s" type="text" />

          <input type="submit" value="Submit" id="submitButton" />

          <div id="searchInContainer">
              <input type="radio" name="check" value="site" id="searchSite" checked />
              <label for="searchSite" id="siteNameLabel">Search</label>

              <input type="radio" name="check" value="web" id="searchWeb" />
              <label for="searchWeb">Search The Web</label>
          </div>

          <ul class="icons">
              <li class="web" title="Web Search" data-searchType="web">Web</li>
              <li class="images" title="Image Search" data-searchType="images">Images</li>
              <li class="news" title="News Search" data-searchType="news">News</li>
              <li class="videos" title="Video Search" data-searchType="video">Videos</li>
          </ul>

      </fieldset>
  </form>

  <div id="resultsDiv"></div>

</div>

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script src="script.js"></script>
</body>
</html>

In the body section, we have the main container element – the #page div. The form inside it, acts not only as a search form, but as a container as well. It has CSS3 rounded corners and a darker background color applied to it, which makes it more easily distinguishable from the rest of the page.

Inside the form is the text input box, after which comes the radio group for searching on the current site / the web, and the four search type icons, organized as an unordered list. Lastly we include jQuery and our scripts.js, which is discussed in the last step of this tutorial.

Google Powered Site Search

Google Powered Site Search

The CSS

The CSS styles reside in styles.css. Only the more interesting parts are included here.

styles.css – Part 1

#searchForm{
/* The search form. */
background-color:#4C5A65;
padding:50px 50px 30px;
margin:80px 0;
position:relative;

-moz-border-radius:16px;
-webkit-border-radius:16px;
border-radius:16px;
}

fieldset{
border:none;
}

#s{
/* The search text box. */

border:none;
color:#888888;
background:url('img/searchBox.png') no-repeat;

float:left;
font-family:Arial,Helvetica,sans-serif;
font-size:15px;
height:36px;
line-height:36px;
margin-right:12px;
outline:medium none;
padding:0 0 0 35px;
text-shadow:1px 1px 0 white;
width:385px;
}

As mentioned above, the form’s functions are not limited to only submitting data, but also to act as a regular container element. This keeps the markup on the page to a minimum, while still providing rich functionality.

The text input box, #s, is styled with a background image and padding, so that the text does not cover the magnifying glass.

styles.css – Part 2

.icons{
list-style:none;
margin:10px 0 0 335px;
height:19px;
position:relative;
}

.icons li{
background:url('img/icons.png') no-repeat;
float:left;
height:19px;
text-indent:-999px;
cursor:pointer;
margin-right:5px;
}

/* Styling each icon */

li.web{ width:15px;}
li.web.active,
li.web:hover{ background-position:left bottom;}

li.images{ width:22px; background-position:-18px 0;}
li.images.active,
li.images:hover{ background-position:-18px bottom;}

li.news{ width:14px; background-position:-44px 0;}
li.news.active,
li.news:hover{ background-position:-44px bottom;}

li.videos{ width:17px; background-position:right 0;}
li.videos.active,
li.videos:hover{ background-position:right bottom;}

span.arrow{
/* The little arrow that moves below the icons */

width:11px;
height:6px;
margin:21px 0 0 5px;
position:absolute;
background:url('img/arrow.png') no-repeat;
left:0;
}

/* The submit button */

#submitButton{
background:url('img/buttons.png') no-repeat;
width:83px;
height:36px;
text-indent:-9999px;
overflow:hidden;
text-transform:uppercase;
border:none;
cursor:pointer;
}

#submitButton:hover{
background-position:left bottom;
}

In the fragment above, you can see that the search type icons all share a single background image. It is offset with background position so the appropriate part of it is shown, for both the default and the hover state.

The same technique is used for the submit button. Its text is hidden with a negative text-indent, and buttons.png is shown as its background, with the top part of the image visible by default and the bottom on hover.

styles.css – Part 3

/* Web & news results */

.webResult{ text-shadow:1px 1px 0 #586a75;margin-bottom:50px;}
.webResult h2{
background-color:#5D6F7B;
font-size:18px;
font-weight:normal;
padding:8px 20px;

/* Applying CSS3 rounded corners */
-moz-border-radius:18px;
-webkit-border-radius:18px;
border-radius:18px;
}
.webResult h2 b{ color:#fff; }
.webResult h2 a{ color:#eee;border:none;}
.webResult p{ line-height:1.5;padding:15px 20px;}
.webResult p b{ color:white;}
.webResult > a{ margin-left:20px;}

/* Image & video search results */

.imageResult{
float:left;
height:170px;
margin:0 0 20px 40px;
text-align:center;
width:150px;
}
.imageResult img{ display:block;border:none;}
.imageResult a.pic{
border:1px solid #fff;
outline:1px solid #777;
display:block;
margin:0 auto 15px;
}

/* The show more button */

#more{
width:83px;
height:24px;
background:url('img/more.png') no-repeat;
cursor:pointer;
margin:40px auto;
}

#more:hover{
background-position:left bottom;
}

In the last fragment, we style the results. Although we show four types of search results – web, news, images and video, these are only styled by the two classes above – .webResult and .imageResult. Lastly, we style the #more button, which is dynamically added to the page by jQuery depending on the results returned by Google.

jQuery & JSONp Google API Site Search

jQuery & JSONp Google API Site Search

The jQuery

As mentioned in the beginning, this app uses Google’s AJAX Search API. Google do provide their own JavaScript library, but if you choose to use it, you are constrained with their UI. While functional, it may not be what you want to offer your visitors. This is why, in this tutorial, we are using the “naked version” by issuing JSONp calls with jQuery directly to their API.

Before we start discussing the jQuery code, lets have a glimpse at what data Google makes available to us, after we run a search with the API.

Sample Result From Google’s API

{
  "GsearchResultClass": "GwebSearch",
  "unescapedUrl": "http://tutorialzine.com/2010/02/html5-css3-website-template/",
  "url": "http://tutorialzine.com/2010/02/html5-css3-website-template/",
  "visibleUrl": "tutorialzine.com",
  "cacheUrl": "http://www.google.com/search?q=cache:_NSLxH-cQMAJ:tutorialzine.com",
  "title": "Coding a <b>CSS3</b> & <b>HTML5</b> One-Page Website Template | Tutorialzine",
  "titleNoFormatting": "Coding a CSS3 & HTML5 One-Page Website Template | Tutorialzine",
  "content": "Feb 16, 2010 <b>...</b> Here we are using the new version of HTML.."
}

A search run through their API would return the same set of result that you’d normally get directly from their site. The difference is that here we get a JavaScript array populated with objects like the one above. Each of these objects holds the type of search, a title, a URL, and text from the page that contains the terms we are searching for.

Using the GsearchResultClass property, we can determine how to display the information, as you will see in a moment. This search app supports only web, image, news and video searches but you can see a complete list of the available types of searches in Google’s AJAX search documentation.

script.js – Part 1

$(document).ready(function(){

var config = {
 siteURL  : 'tutorialzine.com', // Change this to your site
 searchSite : true,
 type  : 'web',
 append  : false,
 perPage  : 8,   // A maximum of 8 is allowed by Google
 page  : 0    // The start page
}

// The small arrow that marks the active search icon:
var arrow = $('<span>',{className:'arrow'}).appendTo('ul.icons');

$('ul.icons li').click(function(){
 var el = $(this);

 if(el.hasClass('active')){
  // The icon is already active, exit
  return false;
 }

 el.siblings().removeClass('active');
 el.addClass('active');

 // Move the arrow below this icon
 arrow.stop().animate({
  left  : el.position().left,
  marginLeft : (el.width()/2)-4
 });

 // Set the search type
 config.type = el.attr('data-searchType');
 $('#more').fadeOut();
});

// Adding the site domain as a label for the first radio button:
$('#siteNameLabel').append(' '+config.siteURL);

// Marking the Search tutorialzine.com radio as active:
$('#searchSite').click();

// Marking the web search icon as active:
$('li.web').click();

// Focusing the input text box:
$('#s').focus();

$('#searchForm').submit(function(){
 googleSearch();
 return false;
});

$('#searchSite,#searchWeb').change(function(){
 // Listening for a click on one of the radio buttons.
 // config.searchSite is either true or false.

 config.searchSite = this.id == 'searchSite';
});

The config object holds general configuration options, such as the site URL, a start page (used in the pagination), and the default type of search (a web search). Google only allows us to select 8 results at a time, which is enough for web searches, but not so for images. Lets hope that Google will raise this limit in the future.

When the form is submitted, jQuery calls our googleSearch() function, which you can see below.

To integrate the search into your website, just replace the siteURL property of the config object, with that of your own site URL.

script.js – Part 2

 function googleSearch(settings){

 // If no parameters are supplied to the function,
 // it takes its defaults from the config object above:

 settings = $.extend({},config,settings);
 settings.term = settings.term || $('#s').val();

 if(settings.searchSite){
  // Using the Google site:example.com to limit the search to a
  // specific domain:
  settings.term = 'site:'+settings.siteURL+' '+settings.term;
 }

 // URL of Google's AJAX search API
 var apiURL = 'http://ajax.googleapis.com/ajax/services/search/'+settings.type+
     '?v=1.0&callback=?';
 var resultsDiv = $('#resultsDiv');

 $.getJSON(apiURL,{
  q : settings.term,
  rsz : settings.perPage,
  start : settings.page*settings.perPage
 },function(r){

  var results = r.responseData.results;
  $('#more').remove();

  if(results.length){

   // If results were returned, add them to a pageContainer div,
   // after which append them to the #resultsDiv:

   var pageContainer = $('<div>',{className:'pageContainer'});

   for(var i=0;i<results.length;i++){
    // Creating a new result object and firing its toString method:
    pageContainer.append(new result(results[i]) + '');
   }

   if(!settings.append){
    // This is executed when running a new search,
    // instead of clicking on the More button:
    resultsDiv.empty();
   }

   pageContainer.append('<div class="clear"></div>')
       .hide().appendTo(resultsDiv)
       .fadeIn('slow');

   var cursor = r.responseData.cursor;

   // Checking if there are more pages with results,
   // and deciding whether to show the More button:

   if( +cursor.estimatedResultCount > (settings.page+1)*settings.perPage){
    $('<div>',{id:'more'}).appendTo(resultsDiv).click(function(){
     googleSearch({append:true,page:settings.page+1});
     $(this).fadeOut();
    });
   }
  }
  else {

   // No results were found for this search.

   resultsDiv.empty();
   $('<p>',{
    className : 'notFound',
    html  : 'No Results Were Found!'
   }).hide().appendTo(resultsDiv).fadeIn();
  }
 });
}

The googleSearch() function sends a JSONp request to Google’s API, generates the markup of the results, and inserts it into the #resultsDiv div. It can either empty that div beforehand (if we are making a fresh search) or append the results (this happens when we click the “More” button).

Both paths follow the same logic – a new .pageContainer div is created for each set of results (this div has a bottom border, so it is easier to distinguish one page of results from the next) and an object of the result class (you can see this class below), is initialized and its markup is appended to the pageContainer.

Web Search Results

Web Search Results

script.js – Part 3

 function result(r){

 // This is class definition. Object of this class are created for
 // each result. The markup is generated by the .toString() method.

 var arr = [];

 // GsearchResultClass is passed by the google API
 switch(r.GsearchResultClass){

  case 'GwebSearch':
   arr = [
    '<div class="webResult">',
    '<h2><a href="',r.url,'">',r.title,'</a></h2>',
    '<p>',r.content,'</p>',
    '<a href="',r.url,'">',r.visibleUrl,'</a>',
    '</div>'
   ];
  break;
  case 'GimageSearch':
   arr = [
    '<div class="imageResult">',
    '<a href="',r.url,'" title="',r.titleNoFormatting,
    '" class="pic" style="width:',r.tbWidth,'px;height:',r.tbHeight,'px;">',
    '<img src="',r.tbUrl,'" width="',r.tbWidth,'" height="',
    r.tbHeight,'" /></a>','<div class="clear"></div>',
    '<a href="',r.originalContextUrl,'">',r.visibleUrl,'</a>',
    '</div>'
   ];
  break;
  case 'GvideoSearch':
   arr = [
    '<div class="imageResult">',
    '<a href="',r.url,'" title="',r.titleNoFormatting,'
    " class="pic" style="width:150px;height:auto;">',
    '<img src="',r.tbUrl,'" width="100%" /></a>',
    '<div class="clear"></div>','<a href="',
    r.originalContextUrl,'">',r.publisher,'</a>',
    '</div>'
   ];
  break;
  case 'GnewsSearch':
   arr = [
    '<div class="webResult">',
    '<h2><a href="',r.unescapedUrl,'">',r.title,'</a></h2>',
    '<p>',r.content,'</p>',
    '<a href="',r.unescapedUrl,'">',r.publisher,'</a>',
    '</div>'
   ];
  break;
 }

 // The toString method.
 this.toString = function(){
  return arr.join('');
 }
}
});

This function acts as the constructor of the result class. It takes the object which was returned from Google’s API (which you saw at the beginning of the jQuery step) and initializes arr according to the value of GsearchResultClass. Notice that arr is assigned an array instead of a string. This is a bit faster than multiple concatenations of a string together.

At the bottom of the class, we have the toString() method. It basically calls the array’s internal join method, turning it into a string. toString() is a magical method, which is implicitly called on line 38 of script.js – Part 2.

With this our own Google Powered Search Engine is complete!

Conclusion

Configuring this app to search your site is really simple. Just change the siteURL property of the config object in script.js. There are many ways how you can improve this example. Not all the data that comes from Google is currently displayed. You could also use the filetype: search modifier to look for specific types of files.