Ayear 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…

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’
Name: customtextsearch
Active: true
HTML:
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<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>
</j2:if>
</j:jelly>
‘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’
Name: custom_search_form
Active: true
XML:
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<!-- called from customtextsearch UI page -->
<g:inline template="javascript_includes.xml" includes_file="scripts/text_search.js" />
<g:inline template="search_load_preferences.xml" />
<div>
<form action="customtextsearch.do" style="DISPLAY: inline" id="customtextsearch"
onSubmit="parent.document.getElementById('sysparm_search').value = document.getElementById('sysparm_search').value;
showLoading();
getCheckedSearchGroups();">
<input type="hidden" id="sysparm_tsgroups" name="sysparm_tsgroups" value="" />
<div id="searchgroups" style="display:$[jvar_search_groups_display];">
<table>
<tr>
<td id="searchgroup_checkboxes">
<!-- "select all" checkbox -->
<span id="ts_selectall" style="display:inline;"><input type="checkbox" id="ts_all" onclick="tsAllCheckbox()" name="ts_all" /><label for="ts_all"><em>${gs.getMessage('Select all')}</em></label></span>
<!-- search group checkboxes -->
<g2:evaluate>
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();
</g2:evaluate>
<j2:set var="jvar_nochecked" value="true"/>
<j2:set var="jvar_allchecked" value="true"/>
<j2:set var="jvar_numvisible" value="0"/>
<j2:set var="jvar_edit_group_msg" value="$[gs.getMessage('Select tables in this search group')]"/>
<j2:while test="$[tsgroups.next()]">
<j2:set var="jvar_tsgroup_visible" value="$[gs.hasRole(tsgroups.roles)]"/>
<j2:if test="$[jvar_tsgroup_visible]">
<g2:evaluate var="jvar_numvisible" expression="$[jvar_numvisible] + 1"/>
<j2:set var="jvar_tsgroup" value="$[tsgroups.sys_id]"/>
<g2:evaluate var="jvar_tsgroup_checked" expression="gs.getPreference('ts.group.' + '$[jvar_tsgroup]','true')"/>
<j2:set var="jvar_desc" value="$[tsgroups.description]"/>
<j2:if test="$[jvar_tsgroup_checked]">
<span style="margin-left: 4px;"><input type="checkbox" checked="true" id="ts_group_$[jvar_tsgroup]"
name="ts_group_$[jvar_tsgroup]" onclick="tsGroupCheckbox(this)" title="$[jvar_desc]"/></span>
</j2:if>
<j2:if test="$[!jvar_tsgroup_checked]">
<span style="margin-left: 4px;"><input type="checkbox" id="ts_group_$[jvar_tsgroup]"
name="ts_group_$[jvar_tsgroup]" onclick="tsGroupCheckbox(this)" title="$[jvar_desc]"/></span>
</j2:if>
<a onClick="tablePrefs('$[jvar_tsgroup]','$[tsgroups.name]')" class="searchgrouplink" title="$[jvar_edit_group_msg]">$[tsgroups.name]</a>
<j2:if test="$[jvar_tsgroup_checked == 'true']">
<j2:set var="jvar_nochecked" value="false"/>
</j2:if>
<j2:if test="$[jvar_tsgroup_checked == 'false' || jvar_tsgroup_checked == null]">
<j2:set var="jvar_allchecked" value="false"/>
</j2:if>
</j2:if>
</j2:while>
</td>
</tr>
</table>
</div>
<div id="searchBoxAndPrefs">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<!-- search box -->
<td valign="top" nowrap="true" style="background-color:white;">
<div style="border: 1px solid #d5d5d5;">
<input type="hidden" id="sysparm_tsgroups" name="sysparm_tsgroups" value="" />
<g2:evaluate jelly="true">
var term1 = jelly.sysparm_search;
if (jelly.sysparm_recent_search == 'true')
term1 = Packages.java.net.URLDecoder.decode(term1);
</g2:evaluate>
<input size="45" id="sysparm_search" name="sysparm_search" autocomplete="off" title="${gs.getMessage('Search')}"
value="$[term1]" style="padding-left:3px;border:solid 0px;" onfocus="this.select()"/>
<input style="vertical-align:middle;" type="image" class="searchGlass" src="images/search_glass.gifx" title="${gs.getMessage('Search')}" alt="${gs.getMessage('Search')}" width="14" height="18" />
<a style="margin-left:4px" onclick="contextShow(event, 'searchform', 200, grabOffsetTop(this) + 20, 0, grabOffsetLeft(this) + 11);event.cancelBubble=true;" title="${gs.getMessage('Recent searches')}">
<img src="images/drop_down.gifx" alt="${gs.getMessage('Recent searches')}" style="vertical-align:middle;" border="0" id="imgText2" width="9" height="18" /></a>
</div>
</td>
<td width="99%"></td>
<j2:if test="$[gs.hasRole('text_search_admin')]">
<td valign="top" align="right" nowrap="true"><a class="ts_adminlink" href="ts_group_list.do" title="${gs.getMessage('Navigate to list of search groups for administration')}">${gs.getMessage('Edit Search Groups')}</a></td>
</j2:if>
</tr>
<tr>
<j2:set var="jvar_quicknav_text_orig" value="$[gs.getMessage('Found: ')]"/>
<j2:set var="jvar_quicknav_title" value="$[gs.getMessage('Scroll page to results for this table')]"/>
<j2:set var="jvar_quicknav_text" value="$[jvar_quicknav_text_orig]"/>
<td colspan="2" width="99%" id="quicknav2"></td>
<g:inline template="search_tips_and_preferences.xml" />
</tr>
</table>
</div>
<!-- focus in search box -->
<script>
var l = gel('sysparm_search'); if (l) l.focus();
</script>
</form>
<j2:if test="$[jvar_allchecked]">
<script>document.getElementById("ts_all").checked = true;</script>
</j2:if>
<j2:if test="$[jvar_nochecked && jvar_numvisible != 0]">
<j2:set var="jvar_nosearch" value="true"/>
<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>
</j2:if>
<j2:if test="$[jvar_numvisible == '1']">
<script>document.getElementById("ts_selectall").style.display = "none";</script>
</j2:if>
<j2:if test="$[jvar_numvisible == '0']">
<j2:set var="jvar_nosearch" value="true"/>
<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>
</j2:if>
</div>
</j:jelly>
‘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.

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.
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.
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!
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.
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?
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.
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.
Not that I’m aware of. The CMS-based search is really quite a bit more limiting.
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.
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.
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 < 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
I’ve changed ‘<’ 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.
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
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.
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!
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).
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;
}
}
}
@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.
Thanks Mark – If I stumble accross something I’ll let you know
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.
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
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.
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.
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.
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.
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.
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
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.
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.
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?