Friday, December 12, 2008

MOSS 2007 Searching Index Backups

I've got a situation with my SharePoint Indexes.  Primarily, it takes about 15 hours to do a full crawl if something goes wrong with them.  I am working on ordering new hardware so I can rebuild the farm and unleash about 12 64-bit cores on this task, but until then I have to occasionally deal with situations where the users will upload documents to a library and for some unknown reason, they will not get indexed when the next incremental crawl goes off.  I can find no errors in the crawl log related to this, and the only way I can find to get these files to index is to either go in and manually edit the file in some way so that the modification date gets changes thus causing the next incremental crawl to collect them; or reset all crawled content and start a full crawl which as stated takes 15 hours.  I've been looking for a better alternative to this, and I want to quote Joel Oleson's blog:

The most creative but ugly scenario for making your index redundant is to create an additional farm that is a single box that has indexing services on it. That single box farm would have a copy of the index or have it’s own indexing schedule. Thus if a failure were to happen you could update the connection of the SSP to the other farm. Not sure who’d do this, but it’s possible.

Well I may be one of the people who would do this, but before I resort to this extreme measure, I have tried another alternative.  Good old fashioned backups!  I have setup my farm to perform bi-weekly backups of the Index so that if something goes missing I can restore to three days ago or such, and allow the service to index the new content from the last few days rather than a full crawl.

SharePoint Index Facts

Until recently, I believed that all the information for searching was stored in the database, however the actual index is stored on the hard drive of your indexing server, and the actual crawled metadata properties are stored in the database.  To see where these items are located:

  1. Open up Central Admin
  2. Click Shared Services Administration
  3. Click on the select menu for the Share Service Provider in question and select Edit Properties

Indexloc

On this page look at the section labeled Search Database.  This specifies the location where the crawled metadata properties are stored.  Now scroll down to the section labeled Index Server and look at the Path for index file location.  This is the location that holds the index of the metadata in the Search Database.  Together they work to provide the users with search results.  Therefore no backup of just one of these alone will constitute a full backup of your search index.  This rules out using SQL backups or file backups since the scope of the content spans across both mediums.  The only option left to us is to use SharePoint's native backup capabilities.

How to Backup the Index

Central Admin has a friendly UI that is nice for executing backups, but this is not helpful when you wish to automate the process, so our option is to use the stsadm.exe tool. It is valid to point out that these directions could be easily modified to backup the entire farm in this manner rather than just the index.  Something else interesting to note is that Microsoft will not let you backup the index by itself.  You can only collect a backup of the index by backing up the entire Shared Services Provider.  Hopefully in the future MS will allow for more granular backup options.

  1. Choose a location where you wish to store the backups, for me each backup requires about 5GB or space, however this will depend upon the amount of indexed content.  Select a location that will have enough space to hold your backups.  For this example, I will use this location:
    \\MOSSDB\IndexBak
  2. Notice I have selected a network share for the path.  This is because data will need to be written to this folder from the Indexing Server and the MOSS database.  For this reason, you will want to run the SQL service hosting the SharePoint databases under credentials that have write access to this shared location.  Likewise, the SharePoint system account will also need write access here.
  3. Create a folder to store script files on your index server, for this example I will use:
    c:\BackupCmd
  4. In this example, I am assuming the name of your Shared Service Provider is SharedServices1.  Naturally, you want to change this to your SSP's name. In the new folder make a text file named SSPBackup.bat. Then put this command into it:
    "C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\BIN\stsadm.exe" -o backup -directory "\\MOSSDB\IndexBak" -backupmethod full -item "SharedServices1"
  5. Be sure to replace the directory path and the SSP name with those used by your farm.  Now that the bat file is created, let's test it. Click Start -> Run. Type cmd and press Enter.  In the console window, type cd c:\BackupCmd and press Enter.  Now type SSPBackup and press Enter.  Make sure the command runs without error. If it errors out, make sure your permissions are set both in the \\mossdb\Indexbak file share and in the NTFS security panel.

How to Clean Up your Backups

Now let's discuss how to delete the old backups after they age.  Someone at Microsoft has written a nice script to handle this for us.  To take advantage of this:

  1. Create a new text file in the BackupCmd folder named DeleteOldBackups.vbs.  Copy this code into it, or you can get the code direct from Microsoft in the link above:
    '       Title: BackupCleanUp
    ' Description: Deletes SharePoint 2007 backups that are older than a specified 
    '              number of days and then removes the backups from the backup history. 
    
    Setlocale(1033)
    Dim nNumberOfDays
    Dim strTOCFile
    Dim dtDeleteDate
    
    Dim sTemp
    
    Set objXML = CreateObject("Microsoft.XMLDOM")
    Set objFS  = CreateObject("Scripting.FileSystemObject")
    Set objLog = objFS.OpenTextFile("BackupCleanUp.log",8,true)
    
    ' Validate command line arguments and initialize data.
    If WScript.Arguments.Count = 2 Then
        If IsNumeric(WScript.Arguments(0)) Then   
            nNumberOfDays = CInt(WScript.Arguments(0))
            dtDeleteDate = DateAdd("d",nNumberOfDays*-1,Now)
        Else
            WScript.Echo "<NumberOfDays> must be an integer value."
        End If
            strTOCFile = WScript.Arguments(1)
    Else
        WScript.Echo "Usage: BackupCleanUp <NumberOfDays> <PathToTOC>"
        WScript.Quit
    End If
    
    objLog.WriteLine(Now() &vbTab& "Start: Clean up backups older than " &nNumberOfDays& " days from " &strTOCFile& ".")
    
    ' Load the SharePoint backup and restore the TOC file.
    objXML.Async = false
    objXML.Load(strTOCFile)
    
    If objXML.ParseError.ErrorCode <> 0 Then
        objLog.WriteLine(Now() &vbTab& "Error: Could not load the SharePoint Backup / Restore History." &vbCrLf&_
                         Now() &vbTab& "Reason: " &objXML.ParseError.Reason& ".") 
        WScript.Quit
    End If
    
    ' Delete backup nodes that are older than the deletion date.
    For Each objNode in objXML.DocumentElement.ChildNodes
        If CDate(objNode.SelectSingleNode("SPFinishTime").Text) < dtDeleteDate Then
            If objNode.SelectSingleNode("SPIsBackup").Text = "True" Then
                
    sTemp = mid(objNode.SelectSingleNode("SPBackupDirectory").Text,1,len(objNode.SelectSingleNode("SPBackupDirectory").Text)-1)
    
    'objFS.DeleteFolder(mid(objNode.SelectSingleNode("SPBackupDirectory").Text),1,len(objNode.SelectSingleNode("SPBackupDirectory").Text)-1)
    objFS.DeleteFolder sTemp
    
    
    
    
                objLog.WriteLine(Now() &vbTab& "Deleted: " &objNode.SelectSingleNode("SPBackupDirectory").Text& ".")
                objXML.DocumentElement.RemoveChild(objNode)
            End If     
        End If
    Next
    
    ' Save the XML file with the old nodes removed.
    objXML.Save(strTOCFile)
    objLog.WriteLine(Now() &vbTab& "Finish: Completed backup clean up.")
  2. Now we need to write another bat file that will take advantage of this script.  Create a file in the BackupCmd folder named SSPCleanUp.bat.  Into this file, paste this command:
    cscript DeleteOldBackups.vbs 13 "\\MOSSDB\IndexBak\spbrtoc.xml"
  3. This command will delete all backups in the file share that are older than 13 days.  It is easy to change this to any retention period you prefer.
  4. Be sure to test this bat file to make sure it is functional.  If necessary, you can edit the dates in the spbrtoc.xml file so that some backups appear older.

How to Automate the Batch Files

Now that the batch files are working, it's time to automate the process.  Windows has a tool to do this that is pretty easy to use called Scheduled Tasks.

  1. Click Start -> Control Panel -> Scheduled Tasks -> Add Scheduled Task
  2. Click Next -> Browse and drill down and select the SSPBackup.bat file we created in the c:\BackupCmd folder.
  3. Give the task a name like SSP Full Backup, select Weekly and click Next.
  4. Much of these options are up to you. For this example, select 7:00pm and Monday and Thursday and click Next.
  5. On this page you will be prompted for credentials to run this command under.  Use your SharePoint system account here. Click Next and click Finish.
  6. In your scheduled tasks window, you can now test your task by right clicking on it and selecting run.

Use this same technique to automate the SSPCleanUp.bat task as well, you will likely want to run it about an hour after the backup process or in this example at 8:00pm.

This blog post is starting to run long so I will stop here and I will post again soon on how to perform a restore from this backup.  Until then best of luck to you.

-Mark Gabriel

Friday, November 28, 2008

MOSS Barcode Image Timing Out in Large Document Libraries

I'm working with a few large libraries in our SharePoint farm, one of which is well over 200,000 documents and climbing.  In these libraries, we have the barcode policy turned on, however on the DispForm.aspx page for viewing a document's properties, the barcode image never loads.  Eventually after about 5 minutes, it will timeout and show a broken link like this.

brokenbarcode

In the web front end event log, this error is recorded:

0x0234  Windows SharePoint Services  Database  8sli  Monitorable    
List item query elapsed time: 371854 milliseconds, Additional data 
(if available): Query HRESULT: 80131530 List internal name, flags, 
and URL: {GUID}, flags=0x0000000004c01088, 
URL=https://[hostname]/_layouts/barcodeimagefromitem.aspx?
doc=[DocGuid]&list=[ListGuid]

After troubleshooting, searching the web, and dealing with Microsoft support all to no gain, it was decided to break a best practice and overwrite one of Microsoft's layout pages, namely the troublesome barcodeimagefromitem.aspx. To this I want to give many props to my colleague, Jamie Burkholder, who did much of the work on this fix. This is the solution we arrived at.

If you view the source for this layout page, it's not much to look at.  It primarily links to a code behind page, that we don't have the code for, so the only option is to rename the page to barcodeimagefromitem.old.aspx so we can create our own page.

Here is the code we used to replace the original.

<%@ Page language="c#" %>

<%@ Assembly Name="Microsoft.Office.Policy, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>

<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<%@ Import Namespace="System.Diagnostics" %>
<%@ Import Namespace="System.Drawing" %>
<%@ Import Namespace="System.Drawing.Imaging" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Import Namespace="Microsoft.Office.RecordsManagement" %>
<%@ Import Namespace="Microsoft.Office.RecordsManagement.PolicyFeatures" %>


<script runat="server">
     
        
        #region "Event Handlers"
     
        //----------------------------------------------
        //  Event Handlers
        //----------------------------------------------
     
        void Page_Load(object sender, System.EventArgs e)
       {
           string listId = Request["List"].ToString();         // collect the list guid from URL
           string docId = Request["Doc"].ToString();                // collect the doc guid from URL
           string barCodeValue = "";
    
           Guid uidListId = new Guid(listId);
           SPList targetList = null;
           SPListItem item;
           System.Drawing.Image    barCodeImage = null;
    
           
           // get list from the guid
           targetList = SPContext.Current.Web.Lists[uidListId];
                   
           // Lookup the document
           if (targetList != null)
           {
               item = targetList.GetItemByUniqueId(new Guid(docId)); 
               
               if (item != null)
               {
                   barCodeValue = Barcode.GetBarcodeValue(item);
                   barCodeImage = Barcode.GetBarcodeImage(item);  
               }
    
               Response.ContentType = "image/jpeg";
               barCodeImage.Save(Response.OutputStream, ImageFormat.Jpeg);
           }
       }    
       
       #endregion
       
</script> <html xmlns="http://www.w3.org/1999/xhtml"> <head id="Head1" runat="server"> <title></title> </head> <body> <form id="form1" runat="server"> </form> </body> </html>

While developing this code, we were able to recreate the same timeout issue as the Microsoft version.  Here is what happened.  Originally, when we were collecting the SPListItem from the SharePoint API we used this line:

item = targetList.Items[new Guid(docId)];        /* Never use this!!!!! */

This works fine until you start putting thousands of documents in the same library.  When this happens, this line will timeout and you will never get your list item.  However, Microsoft knew what they were doing and they gave us an alternative to this that performs like a champ even when there are hundreds of thousands of list items.  Here is the example:

item = targetList.GetItemByUniqueId(new Guid(docId));

This is a solid work around until Microsoft patches the bug, however remember when modifying these layout pages that Microsoft will likely come along and overwrite your work with the next patch.

Best Regards,

-Mark Gabriel

Saturday, November 8, 2008

MOSS 2007 Document Properties Link in Search Results

For over a year I was frustrated with the lack of a link to a document's properties in SharePoint search results. Currently, the out of the box results provides only a link to the document itself, not the metadata or properties. In fact, it is quit difficult for an average user to go from the search results to the document properties. An other blog had an excellent solution to this that provided a link to the containing folder of the document. From here you can simply drop down the menu for the document and select View Properties. While this works for small libraries, in the situation when you have hundreds of other files in that parent folder, this solution becomes less attractive. After a couple frustrating attempts and having to sit down and learn about XSLT and XPath functions, now I have found a solution to this. As in the example pictured above, you can create a link in the search results to get to the properties of a document. The trick is to be able to build the URL using XPath functions in XSL. Here is an example of such a URL.

http://gabrieldev1/Docs/Documents/Forms/DispForm.aspx?ID=2

Create a Metadata Property Mapping

The first thing we need is the ID number which refers to the unique ID of the document for that library. We therefore must create a Metadata Property Mapping for this.

  • Open up the Shared Services Administration.
  • Click Search Settings -> Metadata property mappings -> New Managed Property Fill out the form like this:
  • Click OK and perform a Full Index on all relevant content sources.

You have now successfully created and populated the needed metadata property mapping, now we need to pull that value into our search results.

Add The New Metadata Property to the Core Search Results Web Part

  • In your Search Center, go to the results.aspx page.
  • Click Site Actions -> Edit Page
  • On the Core Results Webpart, click Edit -> Modify Shared Webpart
  • In the Core Results Webpart properties, click the + next to Results Query Option
  • Look for the section labeled Selected Columns, Click the "..." button next to this field to open up the XML editor
  • You will see a window that looks like this
  • Add this Column to the XML:
    <Column Name="ListItemID"/>
  • This is an example of what your XML should look like when you are done:
    <root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
      <Columns>
        <Column Name="WorkId"/>
        <Column Name="Rank"/>
        <Column Name="Title"/>
        <Column Name="Author"/>
        <Column Name="Size"/>
        <Column Name="Path"/>
        <Column Name="Description"/>
        <Column Name="Write"/>
        <Column Name="SiteName"/>
        <Column Name="CollapsingStatus"/>
        <Column Name="HitHighlightedSummary"/>
        <Column Name="HitHighlightedProperties"/>
        <Column Name="ContentClass"/>
        <Column Name="IsDocument"/>
        <Column Name="PictureThumbnailURL"/>
        <Column Name="ListItemID"/>
      </Columns>
    </root>

 

Add the Link to the Core Search Results XSL Template

You now have the ID variable available to you in the XSL Template for the Search Results. Now we need to edit this template to display the link.

  • In the Core Search Results Properties, click the XSL Editor button.
  • Another window with a bunch of poorly indented XML will appear. I recommend you take the time to copy this into visual studio or other XML text editor.
  • Scroll down until you find the remark
    <!-- This template is called for each result -->
  • Below this remark is a list of variable declarations, add sitename, listitemid, and isdocument declarations to this list so that the code looks like this:
    <!-- This template is called for each result -->
    <xsl:template match="Result">
      <xsl:variable name="id" select="id"/>
      <xsl:variable name="url" select="url"/>
      <xsl:variable name="sitename" select="sitename"/>
      <xsl:variable name="listitemid" select="listitemid"/>
      <xsl:variable name="isdocument" select="isdocument"/>
  • Now that the variables are initialized for the Template, we must construct the URL to the document properties page.
          <!-- BEGIN Add Link to document properties -->
          <xsl:choose>
            <xsl:when test="isdocument[. != 0]">
              <span class="srch-Description">
                |
              </span>
              <span class="srch-URL">
                <a href="{$sitename}/{substring-before(substring-after(substring($url, string-length($sitename)), '/'), '/')}/Forms/DispForm.aspx?ID={$listitemid}">
                  Properties
                </a>
              </span>
            </xsl:when>
          </xsl:choose>
          <!-- END Add Link to document properties -->

Here is the entire XSL template that I used in this example:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
<xsl:param name="ResultsBy" />
<xsl:param name="ViewByUrl" />
<xsl:param name="ViewByValue" />
<xsl:param name="IsNoKeyword" />
<xsl:param name="IsFixedQuery" />
<xsl:param name="ShowActionLinks" />
<xsl:param name="MoreResultsText" />
<xsl:param name="MoreResultsLink" />
<xsl:param name="CollapsingStatusLink" />
<xsl:param name="CollapseDuplicatesText" />
<xsl:param name="AlertMeLink" />
<xsl:param name="AlertMeText" />
<xsl:param name="SrchRSSText" />
<xsl:param name="SrchRSSLink" />
<xsl:param name="ShowMessage" />
<xsl:param name="IsThisListScope" />
<xsl:param name="DisplayDiscoveredDefinition" select="True" />
<xsl:param name="NoFixedQuery" />
<xsl:param name="NoKeyword" />
<xsl:param name="NoResults" />
<xsl:param name="NoResults1" />
<xsl:param name="NoResults2" />
<xsl:param name="NoResults3" />
<xsl:param name="NoResults4" />
<xsl:param name="DefinitionIntro" />
 
<!-- When there is keywory to issue the search -->
<xsl:template name="dvt_1.noKeyword">
  <span class="srch-description">
  <xsl:choose>
  <xsl:when test="$IsFixedQuery">
      <xsl:value-of select="$NoFixedQuery" />
  </xsl:when>
   <xsl:otherwise>
      <xsl:value-of select="$NoKeyword" />
   </xsl:otherwise>
  </xsl:choose>
  </span>       
</xsl:template>
 
 
<!-- When empty result set is returned from search -->
<xsl:template name="dvt_1.empty">
 <div class="srch-sort">
  <xsl:if test="$AlertMeLink and $ShowActionLinks">  
    <span class="srch-alertme" > <a href ="{$AlertMeLink}" id="CSR_AM1" title="{$AlertMeText}"><img style="vertical-align: middle;" src="/_layouts/images/bell.gif" alt="" border="0"/><xsl:text disable-output-escaping="yes">&amp;nbsp;</xsl:text><xsl:value-of select="$AlertMeText" /></a>
    </span>
  </xsl:if>
 
  <xsl:if test="string-length($SrchRSSLink) &gt; 0 and $ShowActionLinks">     
   <xsl:if test="$AlertMeLink">  
    |
   </xsl:if>
   <a type="application/rss+xml" href ="{$SrchRSSLink}" title="{$SrchRSSText}" id="SRCHRSSL"><img style="vertical-align: middle;" border="0" src="/_layouts/images/rss.gif" alt=""/><xsl:text disable-output-escaping="yes">&amp;nbsp;</xsl:text><xsl:value-of select="$SrchRSSText"/></a>
  </xsl:if>
 </div>
 <br/> <br/>
 
  <span class="srch-description" id="CSR_NO_RESULTS">
   <xsl:value-of select="$NoResults" />
 
    <ol>
    <li><xsl:value-of select="$NoResults1" /></li>
    <li><xsl:value-of select="$NoResults2" /></li>
    <li><xsl:value-of select="$NoResults3" /></li>
    <li><xsl:value-of select="$NoResults4" /></li>
    </ol>
  </span>
</xsl:template>
 
 
<!-- Main body template. Sets the Results view (Relevance or date) options -->
<xsl:template name="dvt_1.body">
  <div class="srch-results">
  <xsl:if test="$ShowActionLinks">
  <div class="srch-sort"> <xsl:value-of select="$ResultsBy" /> 
   <xsl:if test="$ViewByUrl">  
     | 
    <a href ="{$ViewByUrl}" id="CSR_RV" title="{$ViewByValue}">              
     <xsl:value-of select="$ViewByValue" />
    </a>  
   </xsl:if>
   <xsl:if test="$AlertMeLink">  
     | 
    <span class="srch-alertme" > <a href ="{$AlertMeLink}" id="CSR_AM2" title="{$AlertMeText}"><img style="vertical-align: middle;" src="/_layouts/images/bell.gif" alt="" border="0"/><xsl:text disable-output-escaping="yes">&amp;nbsp;</xsl:text><xsl:value-of select="$AlertMeText" /></a>
    </span>
   </xsl:if>
   <xsl:if test="string-length($SrchRSSLink) &gt; 0">  
     | 
     <a type="application/rss+xml" href ="{$SrchRSSLink}" title="{$SrchRSSText}" id="SRCHRSSL"><img style="vertical-align: middle;" border="0" src="/_layouts/images/rss.gif" alt=""/><xsl:text disable-output-escaping="yes">&amp;nbsp;</xsl:text><xsl:value-of select="$SrchRSSText"/></a>
   </xsl:if>
  </div>
  <br /><br />
  </xsl:if>
  <xsl:apply-templates />
 
  </div>  
  <xsl:call-template name="DisplayMoreResultsAnchor" />
</xsl:template><!-- This template is called for each result -->
<xsl:template match="Result"> 
  <xsl:variable name="id" select="id"/>
  <xsl:variable name="url" select="url"/>
  <xsl:variable name="sitename" select="sitename"/>
  <xsl:variable name="listitemid" select="listitemid"/>
  <xsl:variable name="isdocument" select="isdocument"/>
  <span class="srch-Icon"> 
   <a href="{$url}" id="{concat('CSR_IMG_',$id)}" title="{$url}">
   <img align="absmiddle" src="{imageurl}" border="0" alt="{imageurl/@imageurldescription}" />
   </a>
  </span>
  <span class="srch-Title">
   <a href="{$url}" id="{concat('CSR_',$id)}" title="{$url}">
    <xsl:choose>
     <xsl:when test="hithighlightedproperties/HHTitle[. != '']">
         <xsl:call-template name="HitHighlighting">
          <xsl:with-param name="hh" select="hithighlightedproperties/HHTitle" /> 
         </xsl:call-template>   
     </xsl:when>
     <xsl:otherwise><xsl:value-of select="title"/></xsl:otherwise> 
    </xsl:choose>
   </a>
    <br/> 
   </span>
 
   <xsl:choose>
     <xsl:when test="$IsThisListScope = 'True' and contentclass[. = 'STS_ListItem_PictureLibrary'] and picturethumbnailurl[. != '']">
       <div style="padding-top: 2px; padding-bottom: 2px;">
        <a href="{$url}" id="{concat('CSR_P',$id)}" title="{title}">
          <img src="{picturethumbnailurl}" alt="" />
        </a>
       </div>
     </xsl:when>
   </xsl:choose>
   <div class="srch-Description">
    <xsl:choose>
    <xsl:when test="hithighlightedsummary[. != '']">
       <xsl:call-template name="HitHighlighting">
          <xsl:with-param name="hh" select="hithighlightedsummary" /> 
       </xsl:call-template> 
    </xsl:when>   
     <xsl:when test="description[. != '']">
        <xsl:value-of select="description"/>     
     </xsl:when>     
    </xsl:choose>
    </div >
    <p class="srch-Metadata">
    <span class="srch-URL">
      <!-- BEGIN Add Link to document properties -->
      <xsl:choose>
        <xsl:when test="isdocument[. != 0]">
          <span class="srch-URL">
            <a href="{$sitename}/{substring-before(substring-after(substring($url, string-length($sitename)), '/'), '/')}/Forms/DispForm.aspx?ID={$listitemid}">
              Properties
            </a>
          </span>
          <span class="srch-Description">
            |
          </span>
        </xsl:when>
      </xsl:choose>
      <!-- END Add Link to document properties -->
     <a href="{$url}" id="{concat('CSR_U_',$id)}" title="{$url}" dir="ltr">      
      <xsl:choose>
        <xsl:when test="hithighlightedproperties/HHUrl[. != '']">
           <xsl:call-template name="HitHighlighting">
              <xsl:with-param name="hh" select="hithighlightedproperties/HHUrl" /> 
           </xsl:call-template> 
        </xsl:when>
       <xsl:otherwise><xsl:value-of select="url"/></xsl:otherwise> 
      </xsl:choose>
     </a>
    </span>           
     <xsl:call-template name="DisplaySize">
      <xsl:with-param name="size" select="size" />
     </xsl:call-template>     
     <xsl:call-template name="DisplayString">
      <xsl:with-param name="str" select="author" /> 
     </xsl:call-template>  
     <xsl:call-template name="DisplayString">
      <xsl:with-param name="str" select="write" />
     </xsl:call-template>     
     <xsl:call-template name="DisplayCollapsingStatusLink">
        <xsl:with-param name="status" select="collapsingstatus"/>
        <xsl:with-param name="urlEncoded" select="urlEncoded"/>
        <xsl:with-param name="id" select="concat('CSR_CS_',$id)"/>
     </xsl:call-template>        
    </p>
</xsl:template>
 
<xsl:template name="HitHighlighting"> 
 <xsl:param name="hh" /> 
 <xsl:apply-templates select="$hh"/> 
</xsl:template>
 
<xsl:template match="ddd"> 
  &#8230;  
</xsl:template> 
<xsl:template match="c0"> 
    <b><xsl:value-of select="."/></b>
</xsl:template> 
<xsl:template match="c1"> 
    <b><xsl:value-of select="."/></b>
</xsl:template> 
<xsl:template match="c2"> 
    <b><xsl:value-of select="."/></b>
</xsl:template> 
<xsl:template match="c3"> 
    <b><xsl:value-of select="."/></b>
</xsl:template> 
<xsl:template match="c4"> 
    <b><xsl:value-of select="."/></b>
</xsl:template> 
<xsl:template match="c5"> 
    <b><xsl:value-of select="."/></b>
</xsl:template> 
<xsl:template match="c6"> 
    <b><xsl:value-of select="."/></b>
</xsl:template> 
<xsl:template match="c7"> 
    <b><xsl:value-of select="."/></b>
</xsl:template> 
<xsl:template match="c8"> 
    <b><xsl:value-of select="."/></b>
</xsl:template> 
<xsl:template match="c9"> 
    <b><xsl:value-of select="."/></b>
</xsl:template> 
 
 
<!-- The size attribute for each result is prepared here -->
<xsl:template name="DisplaySize">
  <xsl:param name="size" /> 
  <xsl:if test='string-length($size) &gt; 0'>       
   <xsl:if test="number($size) &gt; 0">
   -
    <xsl:choose>
     <xsl:when test="round($size div 1024) &lt; 1"><xsl:value-of select="$size" /> Bytes</xsl:when>
     <xsl:when test="round($size div (1024 *1024)) &lt; 1"><xsl:value-of select="round($size div 1024)" />KB</xsl:when>
     <xsl:otherwise><xsl:value-of select="round($size div (1024 * 1024))"/>MB</xsl:otherwise>
    </xsl:choose>    
   </xsl:if>
  </xsl:if>
</xsl:template>
 
 
 
<!-- A generic template to display string with non 0 string length (used for author and lastmodified time -->
<xsl:template name="DisplayString">
  <xsl:param name="str" />
  <xsl:if test='string-length($str) &gt; 0'>   
   - 
   <xsl:value-of select="$str" /> 
  </xsl:if>
</xsl:template>
 
<!-- document collapsing link setup -->
<xsl:template name="DisplayCollapsingStatusLink">
    <xsl:param name="status"/>
    <xsl:param name="urlEncoded"/>
    <xsl:param name="id"/>
    <xsl:if test="$CollapsingStatusLink">
      <xsl:choose>
          <xsl:when test="$status=1">
              <br/>
              <xsl:variable name="CollapsingStatusHref" select="concat(substring-before($CollapsingStatusLink, '$$COLLAPSE_PARAM$$'), 'duplicates:&quot;', $urlEncoded, '&quot;', substring-after($CollapsingStatusLink, '$$COLLAPSE_PARAM$$'))"/>
              <span class="srch-dup">
              [<a href="{$CollapsingStatusHref}" id="$id" title="{$CollapseDuplicatesText}">
              <xsl:value-of select="$CollapseDuplicatesText"/>
              </a>]
              </span>
          </xsl:when>
      </xsl:choose>
    </xsl:if>
</xsl:template><!-- The "view more results" for fixed query -->
<xsl:template name="DisplayMoreResultsAnchor">
  <xsl:if test="$MoreResultsLink">
   <a href="{$MoreResultsLink}" id="CSR_MRL">
    <xsl:value-of select="$MoreResultsText"/> 
    </a>
   </xsl:if>
</xsl:template>
 
<xsl:template match="All_Results/DiscoveredDefinitions">
  <xsl:variable name="FoundIn" select="DDFoundIn" />
  <xsl:variable name="DDSearchTerm" select="DDSearchTerm" />
  <xsl:if test="$DisplayDiscoveredDefinition = 'True' and string-length($DDSearchTerm) &gt; 0">
    <script language="javascript">
     
          function ToggleDefinitionSelection()
          {
            var selection = document.getElementById("definitionSelection");
            if (selection.style.display == "none")
            {
              selection.style.display = "inline";
            }
            else
            {
             selection.style.display = "none";
            }
         }
       
</script>
    <div>
      <a href="#" onclick="ToggleDefinitionSelection(); return false;">
        <xsl:value-of select="$DefinitionIntro" /><b><xsl:value-of select="$DDSearchTerm"/></b></a>
      <div id="definitionSelection" class="srch-Description" style="display:none;">
        <xsl:for-each select="DDefinitions/DDefinition">
          <br/>
          <xsl:variable name="DDUrl" select="DDUrl" />
          <xsl:value-of select="DDStart"/>
          <b>
            <xsl:value-of select="DDBold"/>
          </b>
          <xsl:value-of select="DDEnd"/>
          <br/>
          <xsl:value-of select="$FoundIn"/>
          <a href="{$DDUrl}">
          <xsl:value-of select="DDTitle"/> 
          </a>
        </xsl:for-each>
      </div>
    </div>
  </xsl:if>   
</xsl:template>
 
<!-- XSL transformation starts here -->
<xsl:template match="/">
  <xsl:if test="$AlertMeLink">  
   <input type="hidden" name="P_Query" />
   <input type="hidden" name="P_LastNotificationTime" />
  </xsl:if>
  <xsl:choose>
   <xsl:when test="$IsNoKeyword = 'True'" >
    <xsl:call-template name="dvt_1.noKeyword" />
   </xsl:when>
   <xsl:when test="$ShowMessage = 'True'">
     <xsl:call-template name="dvt_1.empty" />
   </xsl:when>
   <xsl:otherwise>
    <xsl:call-template name="dvt_1.body"/>      
   </xsl:otherwise>
  </xsl:choose>
</xsl:template> 
 
<!-- End of Stylesheet -->
</xsl:stylesheet>

Hopefully someone else out there will find this as useful as I have, or someone will laugh and comment on how it would have been so much easier to do it a different way! If this is the case, please post it!!

Until next time.

-Mark Gabriel