How to Create a Custom Global Search Page

A year or so ago ServiceNow didn’t have any real global search capability. You could search or filter within a table list, or you could search the knowledge base like you can now. Everybody wanted global search though. In response to this demand I created a modification to the knowledge search that searched and displayed task records along with knowledge search results. Eventually, development came up with an even better solution that allows us to search globally like we do today.

Now that we can search globally in an instance people are asking us to limit the results again in certain situations. While this modification is not for everyone, it does show a very non-intrusive method for creating separate search pages to limit the search scope on that particular search page to exactly what you want.

Let’s say you have a search group named ‘Tasks’ that you would like to use independently on its own search page. Users accessing your search page should only be able to search items defined within this specific search group. Here’s what my ‘Tasks’ search group looks like…

Global Text Search Group
The basic idea behind this solution is to modify the query that returns the search groups to use. The query should only return results from the search group named ‘Tasks’.

1Create a new UI Page by navigating to ‘System UI -> UI Pages’

UI Page Settings
Name: customtextsearch
Active: true
HTML:

<!--?xml version="1.0" encoding="utf-8" ?-->

<script language="JavaScript" src="scripts/text_search.jsx?v=${gs.getProperty('glide.builddate')}"/>
    <link href="styles/text_search.cssx?v=${gs.getProperty('glide.builddate')}" type="text/css" rel="stylesheet" />

	<j2:set var="jvar_found_exact_match" value="false"/>
	<!-- are search terms? --> 	
	<g2:evaluate jelly="true" var="jvar_search">
	    var answer = false;
		if (jelly['sysparm_search'] != undefined) {
		  var s = new String(jelly['sysparm_search']);
		  s = s.trim();
	      if (s.length != 0) 
		    answer = true;
	    }
		answer;  
	</g2:evaluate>

	<j2:if test="$[jvar_search]">
		<!-- save search terms for user and set unescaped_search jvar -->
		<g2:evaluate jelly="true" var="jvar_unescaped_search">
			var term = jelly.sysparm_search;
			if (jelly.sysparm_recent_search == 'true')
				term = Packages.java.net.URLDecoder.decode(jelly.sysparm_search);
				
			var gr = new GlideRecord('ts_query');
			gr.addQuery('user',gs.getUserID());
			gr.addQuery('search_term',term);
			gr.query();
			while (gr.next()) {
				gr.recent = "false";
				gr.update();
			}
			gr.initialize();
			gr.user.setValue(gs.getUserID());
			gr.search_term = term;
			gr.recent = "true";
			gr.insert();
			term;
		</g2:evaluate>
		
		<j:set var="sysparm_view" value="${gs.getProperty('glide.ui.text_search.view')}"/>
		<j2:if test="${gs.getPreference('ts.match','true') == 'true'}">
		    <g:inline template="search_match_redirect.xml" />
		</j2:if>
	</j2:if>
	<j2:if test="$[jvar_found_exact_match != 'true']">
		<g:inline template="custom_search_form" />
		<j2:if test="$[jvar_search]">
			<j:set var="jvar_list_only" value="true"/>
			<j2:set var="sysparm_query" value="123TEXTQUERY321=$[jvar_unescaped_search]" />
			<j2:set var="sysparm_force_row_count" value="${gs.getProperty('glide.ui.text_search.rowcount',10)}" />
			<j:set var="jvar_pos_link_msg" value="${gs.getMessage('Navigate to see all results for this table or try a different filter')}" />
			<j:set var="jvar_neg_link_msg" value="${gs.getMessage('Navigate to list for this table to try a different filter')}" />
			<j2:set var="jvar_query_orig" value="$[sysparm_query]"/>
			<j:if test="${sysparm_tsgroups == '' || !sysparm_tsgroups}">
				<!-- got here from ui macro, so get groups to search -->
				<g:evaluate var="sysparm_tsgroups">
					var ans = '';
					var tsgroups = new GlideRecord('ts_group');
                                        tsgroups.addQuery('name', 'Tasks'); 
					tsgroups.addActiveQuery();
					var qc = tsgroups.addQuery('group','');
					qc.addOrCondition('group',getMyGroups());
					qc.addOrCondition('group.manager',gs.getUserID());
					tsgroups.orderBy('order');
					tsgroups.query();
					while (tsgroups.next()) {
						if (gs.hasRole(tsgroups.roles)) {
							if (ans == '')
								ans = tsgroups.sys_id + '';
							else
								ans = ans + ',' + tsgroups.sys_id;
						}
					}
					ans;
				</g:evaluate>
			</j:if>
			<!-- jvar_this_tsparm has the sys_ids of the groups to be searched -->
	        <g2:evaluate>
                       if (typeof GlideIRQuerySummary != 'undefined')
                          var summarizer = GlideIRQuerySummary.get();
                       else
	                  var summarizer = Packages.com.glide.db.ir.IRQuerySummary.get();
	        </g2:evaluate>
			<g:tokenize var="jvar_this_tsparm" delim=",">${sysparm_tsgroups}</g:tokenize>
			<j:forEach var="jvar_ts_groupid" items="${jvar_this_tsparm}">
				<g2:evaluate>
					var seeGroup = gs.getPreference('ts.group.${jvar_ts_groupid}','true') == 'true';
				</g2:evaluate>
				<j2:if test="$[seeGroup]">
	               <g2:evaluate>
	                    summarizer.addGroup('${jvar_ts_groupid}', '$[jvar_query_orig]');
	               </g2:evaluate>
				</j2:if>
			</j:forEach>
			<g:tokenize var="jvar_this_tsparm" delim=",">${sysparm_tsgroups}</g:tokenize>
			<j:forEach var="jvar_ts_groupid" items="${jvar_this_tsparm}">
				<g2:evaluate>
					var seeGroup = gs.getPreference('ts.group.${jvar_ts_groupid}','true') == 'true';
				</g2:evaluate>
				<j2:if test="$[seeGroup]">
						<g:inline template="ts_group.xml"/>
				</j2:if>
			</j:forEach>
			<g:inline template="search_summary.xml"/>
		</j2:if>
		
		<script>
			if ("$[jvar_nosearch]" != "true") 
				if ("$[jvar_quicknav_text]" == "$[jvar_quicknav_text_orig]")
					document.getElementById('quicknav2').innerHTML = "${gs.getMessage('no_match_check_spelling')}";
				else {
					document.getElementById('quicknav2').innerHTML = "$[jvar_quicknav_text]";
					document.getElementById('additional').innerHTML = "$[jvar_additional_text]";
		        }
		        
			// stop the spinner
			hideLoading();
		   
			// update recent searches dropdowns
			adjustSearch();
			parent.adjustSearch();
	   
			function adjustSearch() {
				var ajax = new GlideAjax("TextSearchAjax");
				ajax.addParam("sysparm_name", "recent");
				ajax.getXML(adjustSearchResponse);
			}
	
			function adjustSearchResponse(request) {
				var answer =  request.responseXML.documentElement.getAttribute("answer");
				if (answer != null) {
					var gcm = new GwtContextMenu('context_searchform');
					gcm.clear();
					if (answer.indexOf("xyzzyx") != 0) {
						var db = answer.split("^");
						for (var i = 0; i != db.length; i++) {
							var u = new GlideURL('customtextsearch.do');
							u.addParam('sysparm_search',escape(db[i]));
							   u.addParam('sysparm_recent_search','true');
							gcm.addHref(db[i], "executeRecentSearch('"+escape(db[i])+"','"+u.getURL()+"')");
						}
					} else gcm.addLabel(answer.substr(6));
				}
			}
		</script>


-If you want to modify this to use a different search group you just need to modify this line above and change ‘Tasks’ to the name of your search group.
‘tsgroups.addQuery(‘name’, ‘Tasks’);’-If you want to create more than one custom search page you’ll need to make sure that the above UI Page (along with its corresponding UI Macro) is unique. You can do this by making the following modifications to the UI Page name and HTML above…

  • Find and replace every instance of ‘customtextsearch’ with the name of your new search page.
  • Find and replace the single occurrence of ‘custom_search_form’ with the corresponding name of the UI Macro you create in step 2.

2Create a new UI Macro by navigating to ‘System UI -> UI Macros’

UI Macro Settings
Name: custom_search_form
Active: true
XML:

<!--?xml version="1.0" encoding="utf-8" ?-->
<!-- called from customtextsearch UI page -->
<div><form id="customtextsearch" style="display: inline;" action="customtextsearch.do"><input id="sysparm_tsgroups" name="sysparm_tsgroups" type="hidden" value="" />
<div id="searchgroups" style="display: $[jvar_search_groups_display];">
<table>
<tbody>
<tr>
<td id="searchgroup_checkboxes"><!-- "select all" checkbox -->
<span id="ts_selectall" style="display: inline;"><input id="ts_all" name="ts_all" type="checkbox" /><label for="ts_all"><em>${gs.getMessage('Select all')}</em></label></span>
<!-- search group checkboxes -->

var tsgroups = new GlideRecord('ts_group');
tsgroups.addQuery('name', 'Tasks');
tsgroups.addActiveQuery();
var qc = tsgroups.addQuery('group','');
qc.addOrCondition('group',getMyGroups());
qc.addOrCondition('group.manager',gs.getUserID());
tsgroups.orderBy('order');
tsgroups.query();












<span style="margin-left: 4px;"><input id="ts_group_$[jvar_tsgroup]" title="$[jvar_desc]" checked="checked" name="ts_group_$[jvar_tsgroup]" type="checkbox" /></span>


<span style="margin-left: 4px;"><input id="ts_group_$[jvar_tsgroup]" title="$[jvar_desc]" name="ts_group_$[jvar_tsgroup]" type="checkbox" /></span>

<a class="searchgrouplink" title="$[jvar_edit_group_msg]">$[tsgroups.name]</a></td>
</tr>
</tbody>
</table>
</div>
<div id="searchBoxAndPrefs">
<table border="0" cellspacing="0" cellpadding="0">
<tbody>
<tr><!-- search box -->
<td style="background-color: white;" valign="top" nowrap="nowrap">
<div style="border: 1px solid #d5d5d5;"><input id="sysparm_tsgroups" name="sysparm_tsgroups" type="hidden" value="" />

var term1 = jelly.sysparm_search;
if (jelly.sysparm_recent_search == 'true')
term1 = Packages.java.net.URLDecoder.decode(term1);

<input id="sysparm_search" style="padding-left: 3px; border: solid 0px;" title="${gs.getMessage('Search')}" autocomplete="off" name="sysparm_search" size="45" type="text" value="$[term1]" />
<input class="searchGlass" style="vertical-align: middle;" title="${gs.getMessage('Search')}" alt="${gs.getMessage('Search')}" height="18" src="images/search_glass.gifx" type="image" width="14" />
<a style="margin-left: 4px;" title="${gs.getMessage('Recent searches')}">
<img id="imgText2" style="vertical-align: middle;" src="images/drop_down.gifx" alt="${gs.getMessage('Recent searches')}" width="9" height="18" border="0" /></a></div></td>
<td width="99%"></td>
<td align="right" valign="top" nowrap="nowrap"><a class="ts_adminlink" title="${gs.getMessage('Navigate to list of search groups for administration')}" href="ts_group_list.do">${gs.getMessage('Edit Search Groups')}</a></td>
</tr>
<tr>
<td id="quicknav2" colspan="2" width="99%"></td>
</tr>
</tbody>
</table>
</div>
<!-- focus in search box -->
<script>
            var l = gel('sysparm_search'); if (l) l.focus();
        </script>

</form>
<script>document.getElementById("ts_all").checked = true;</script>



<script>
            document.getElementById('quicknav2').innerHTML ="<em>${gs.getMessage('No search groups selected - please check at least one')}</em>";
            var e = document.getElementById("searchgroup_checkboxes");
            e.style.backgroundColor = "#ffffcc";
            e.style.borderWidth = "1px";
            e.style.borderStyle = "inset";
            document.getElementById("searchgroups").style.display = "block";
        </script>


<script>document.getElementById("ts_selectall").style.display = "none";</script>



<script>document.getElementById("ts_selectall").style.display = "none";
                document.getElementById("quicknav2").innerHTML = "<em>${gs.getMessage('No search groups available to you, contact your admin for details')}</em>";
        </script>

</div>
-If you want to modify this to use a different search group you just need to modify this line above and change ‘Tasks’ to the name of your search group.
‘tsgroups.addQuery(‘name’, ‘Tasks’);’-If you want to create more than one custom search page you’ll need to make sure that the above UI Macro (along with its corresponding UI Page) is unique. You can do this by making the following modifications to the UI Macro name and XML above…

  • Find and replace every instance of ‘customtextsearch’ with the name of your custom UI Page in step 1.
  • Find and replace every instance of ‘custom_search_form’ in your UI Macro above with the name of your new UI Macro.

Now you can access your new search page ‘customtextsearch’ by navigating to ‘https://yourinstancename/customtextsearch.do’ (or creating a module or gauge that points to that location). Searching from this form will yield search results only from the ‘Tasks’ search group defined previously.

Global Text Search Results

Date Posted:

February 18, 2010

Share This:

30 Comments

  1. Luuk Vonk September 30, 2010 at 2:12 am

    Is it also posible to change the layout of the searchresults? We need a total different layout, but I can’t find a good example for it.

    • Mark Stanger September 30, 2010 at 3:53 am

      You could potentially modify the UI page in this article (or any UI page) to display search results like this in any format you want. It’s not a trivial modification though and I’m not aware of any example for it either. Unless it’s extremely critical (and you’ve got an HTML/JellyScript expert handy) I wouldn’t do it.

  2. Luuk Vonk October 21, 2010 at 12:46 am

    We are now working with it and we are very happy with it. After the installation of IE8 we get an error from the adjustSearchResponse(request) function.

    The error is about: for (var i = 0; i < db.length; i++) { and the full error is: Webpage error details User Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; InfoPath.2; .NET4.0C; .NET4.0E) Timestamp: Thu, 21 Oct 2010 11:42:44 UTC Message: Expected ')' Line: 141 Char: 46 Code: 0 URI: instancename/catalogsearch.do Do you know how to solve this? Thnx already!

    • Mark Stanger October 21, 2010 at 2:33 am

      Thanks for pointing this out,

      It looks like there was a bug in the original source that I based this solution off of. I’ve fixed that and updated the article above. This should work for you if you reapply the solution based on the new information.

  3. Luuk Vonk January 17, 2011 at 11:44 pm

    Mark,

    We have had a lot of compliments for this feature, thnx for it.

    Now we want to install the Fall 2010 Stable 3 Release. At the first test we notice that the custom search page showed no results.

    We couldn’t find a solution, can you please help?

    • Mark Stanger January 18, 2011 at 2:56 am

      Sure. It looks like the backend ‘textsearch.xml’ file that this solution is based off of was modified in Fall 2010 Stable 3. I’ve added the new code in the solution above.

  4. Cesar June 22, 2011 at 4:55 am

    Is there wa way to apply this same procedure from the CMS, I tried to add a search box, and it only searches within the KB based on the example themes that are included in it.

    • Mark Stanger June 23, 2011 at 4:26 pm

      Not that I’m aware of. The CMS-based search is really quite a bit more limiting.

  5. PS September 21, 2011 at 8:45 am

    Hi Mark,
    Thanks a for a wonderful Custom Search Page. It’s working perfectly fine for us. Just couple questions:
    1. I still get the Javascript error on, for (var i = 0; i < db.length; i++) { 2. Is it possible to display the search results Collapsed ? Please let me know if you have a solution for the items mentioned above. Thanks again.

    • Mark Stanger September 21, 2011 at 9:07 am

      I’m not sure what error you’re getting. If you want to send me your instance name and steps to reproduce the error via the contact form I can take a look. I don’t know of a way to display the search results collapsed off the top of my head either.

  6. PS September 21, 2011 at 11:57 am

    Hi Mark,
    Thanks for the prompt response.
    For item # 1, I do get a javascript errror in IE -8, it says “Expected ‘)'” and the line # corresponds to the below line in the code.

    for (var i = 0; i &lt; db.length; i++) {

    For item # 2, are you using the SN – search preferences? How is the Expand/Collapse functionality achieved? I’m not an expert in jelly/glide, hence asking this question. My team wants the results displayed in a Collapsed style based on the categories in the Search Groups. Let me know if you have any ideas/suggestions.

    Thanks again.
    PS

    • Mark Stanger September 21, 2011 at 3:03 pm

      I’ve changed ‘&lt;’ to ‘!=’ in the UI page code above. You can do the same or copy the UI page code again to fix the error. It looks like the expand/collapse of categories is controlled by a user preference. Look for user preference records starting with ‘ts.’. You should be able to use one of those as a template to create a default system preference.

  7. Jamie Douglas March 13, 2012 at 5:15 pm

    Hey,

    This worked awesome for me.. just what I wanted.. There is just one thing that I cannot figure out…
    I want to have the “did you mean” functionality on this search… I have limited my customsearch to only search knowledge but it i type in “eail” it will not ask the question “Did you mean: Email”. If I use the search box in the top right hand corner of my instance it will ask me this question..

    Im stumped.. any help would be greatly appreciated.

    Thanks in advance

    Jamie

    • Mark Stanger March 13, 2012 at 5:50 pm

      Hey Jamie, thanks for the comment! I don’t know the answer to this question off the top of my head. It seems like the issue really isn’t with this solution though, it’s with the way that search pages work in general. I’m not sure if there’s an easy answer to your question but you might want to give this a shot on the forums.

  8. Abhiram Bharadwaj July 31, 2012 at 2:31 am

    Hello Mark,

    Could you please help me with what UI Page and UI macro do here? and are we calling the UI macro in the UI Page? If so where?
    I see that we are doing a customsearchform.do? in a form on UI Macro,redirecting the UI page, Where exactly are we invoking the Macro?

    Thanks!

    • Mark Stanger July 31, 2012 at 6:05 am

      The UI page contains the search form/controls (by including the UI macro) as well as the results display output. The UI macro is called from the UI page via the ‘inline template’ line that references the name of the UI macro (custom_search_form).

  9. Will November 8, 2012 at 9:19 am

    I see some of the code references the exact search functionality – I’m assuming this has to do with the task number? Is there a way through a custom page to change the field on a table that the exact search references? For example, a text search that looks for exact matches on the cmdb_ci_hardware table and will navigate to a specific form instead of a list if there is a match on Name, Serial Number, or Asset Tag, but will navigate to a list in the event that there are multiple matches?

    Right now, I’m rigging a form using a client script like this:
    function onChange(control, oldValue, newValue, isLoading, isTemplate) {
    if (isLoading || newValue == ”) {
    return;
    }
    if(newValue){
    var current = g_form.getValue(‘u_asset_search_and_verify’);
    var gr = new GlideRecord(‘cmdb_ci_hardware’);
    gr.addQuery (‘serial_number’, current);
    gr.query();
    while (gr.next()){
    var sys = gr.sys_id;
    var url=’https://amdsandbox.service-now.com/nav_to.do?uri=cmdb_ci.do?sys_id=’ + sys;
    window.top.location.href=url;
    }
    }
    }

    • Mark Stanger November 8, 2012 at 12:16 pm

      @Will. I really haven’t had to do anything to customize the page itself so I’m not sure. I’ve never seen exact match work for anything but task numbers.

      • Will November 8, 2012 at 12:19 pm

        Thanks Mark – If I stumble accross something I’ll let you know

  10. Chandana December 13, 2012 at 3:33 am

    HI Mark,

    Thanks for this solution, and we have successfully implemented it and it works as expected. How ever now we have upgraded from June 2011 patch 3 to Berlin patch 4 release and this does not work anymore. I suspect the issue to be with the line in the ui page : It does not show the search results as expected.

    Can you please provide some light here,

    Regards,
    Chandana.

    • Mark Stanger December 13, 2012 at 5:53 am

      I just tested this on the ServiceNow demo instance here and it seems to run correctly on Berlin. Try updating your code with the code above to see if that helps.

      https://demo020.service-now.com/customtextsearch.do

      • Chandana December 14, 2012 at 1:48 am

        Thanks Mark for the response,

        However even after replacing the pages again, I see the problem if I have to add multiple search groups,
        like tsgroups.addQuery(‘name’, ‘Tasks’);’. We have 4 search groups in total and we are using addOrCondition to add the remaining search groups which is causing the trouble.

        • Mark Stanger December 14, 2012 at 5:11 am

          Seems like the problem isn’t with this solution, but with your query. Make sure you’re following the correct query syntax and that your search group names don’t have any special characters in them. The ‘&’ symbol can cause issues in UI pages.

          • Chandana December 18, 2012 at 1:00 am

            HI Mark,

            I did check the condition and it works as expected, I tried to print the sys_ids of the ts groups in the addorCondition and it works fine, but the trouble I see is that, the template ts_group.xml which is displaying the search results, shows only one search group 4 times (the first order search group) (I am not able to paste the screenshot here).

            Regards,
            Chandana.

  11. Jacob April 5, 2013 at 11:28 am

    Hello,

    I’ve been using this for the last year with no problems, thank you! After updating to Berlin (it took a while) and it appears we no longer have access to the “search_summary.xml” template. Do you have any suggested workarounds?

    Thanks.

    • Mark Stanger April 5, 2013 at 11:43 am

      Unfortunately, the only workaround is to change your module to use the regular global search. Without access to the code, it’s not something I can update anymore.

  12. Steve May 23, 2013 at 2:11 pm

    Hi Mark,
    Is it possible to be able to narrow the search to a specific field on a table? Example: Just do the search on “Short Description” field.

    Thanks

  13. Pushkal Sharma October 20, 2014 at 6:45 am

    Hi Mark,

    Thank you very much for this functionality. It is very helpful!!
    Is it possible to include the pagination( same as kb_list UI page) in this UI page?
    As a workaround we used jquery for the pagination but the customer is not happy with that and wants to see the ServiceNow Pagination (Please see ‘kb_list’ UI Page for example)

    Please let us know if you have any suggestions.

    Thanks in Advance.

    • Mark Stanger October 20, 2014 at 7:22 am

      You’re stuck with what ServiceNow gives you using this approach. I have seen paging implemented in search results like this but it has always been a completely custom setup.

  14. Patrick Bishop February 2, 2017 at 6:31 am

    I was just attempting to build this inside of a scoped application and am receiving the following error: Function getPreference is not allowed in scope XXX. I was looking at the Scoped Functions but I’m not seeing an obvious workaround. Does anyone have suggestions?

Comments are closed.

Categories

Tags

Loading

Fresh Content
Direct to Your Inbox

Just add your email and hit subscribe to stay informed.