Eric60
Tera Contributor

When making a custom app, you may want to also make a custom portal for it and have Search.

Scripting a Search Source is... not well documented, so here is how I did it. (I am doing this in Paris, but it should all be applicable across many versions.)

Step 1 Go to Service Portal > Search Sources and create a new Search Source

There are three sections that we are going to care about on the form, the top one is the Search Page template, we are going to ignore that one for now.

Step 2 Check the box for a scripted source. You will get some template code in the Data Fetch Script to get you started.

(function(query) {
  var results = [];
  /* Calculate your results here. */
  
  return results;
})(query);

query is a placeholder for your search term. results is what we are going to build all of our results into to be passed to the template.

 

Step 3 Add a GlideRecord and build the results array

(function(query) {
    var results = [];


    var gr= new GlideRecord("<your table>");
    gr.addQuery('IR_AND_OR_QUERY', query);
    gr.orderBy('ir_query_score');
    gr.query();
    while (gr.next()) {
        var item = {};
        item.primary = gr.getDisplayValue('name');
        item.number = gr.getDisplayValue('number');
        item.sys_id = gr.getValue('sys_id');
        item.score = gr.getValue('ir_query_score');
        results.push(item);
    }

    return results;
})(query);

I'm doing a few things here.

The IR_AND_OR_QUERY is equivalent to GOTO123TEXTQUERY321 to do a freeform keyword search. The advantage that it has is that it uses the global search engine to not only get all results based on the keywords, but will also give us the ir_query_score, which is a measure of the relevancy of the result.

Inside the while loop we are building an Object called item. and putting the various aspects of each result into it. You do not need to define them ahead of time, so push whatever will be useful to you into it.

 

Step 4 Search page template

By default we get this template

<div>
  <a href="?id=form&sys_id={{item.sys_id}}&table={{item.table}}" class="h4 text-primary m-b-sm block">
    <span ng-bind-html="highlight(item.primary, data.q)"></span>
  </a>
  <span class="text-muted" ng-repeat="f in item.fields">
    <span class="m-l-xs m-r-xs" ng-if="!$first"> &middot; </span>
    {{f.label}}: <span ng-bind-html="highlight(f.display_value, data.q)"></span>
  </span>
</div>

What you don't see is that in the portal search page it is wrapped in another ng-repeat that is looking for the results array we returned.

I made a few tweaks to this. I added {{item.score}} mainly for testing to see how good my results were. I also changed:

<a href="?id=form&sys_id={{item.sys_id}}&table={{item.table}}"

to:

<a href="?id=<mypage>&number={{item.number}}"

I didnt want to use the oob form page for this and I wanted a nicer looking URL so am using the number instead of the sys_id (all my widgets were custom so I could use the number and didn't need the table name).

 

This is a good time to add the search source to a portal from the related list at the bottom of the page and give this all a test.

 

Step 5 Secondary fields

Non-scripted sources all have secondary fields, and as you have noticed we don't have any yet. if you look at the HTML template you will see that there is a ng-repeat in there for the secondary fields. So lets expand our script to add those in.

 

(function(query) {
    var results = [];


    var gr= new GlideRecord("<your table>");
    gr.addQuery('IR_AND_OR_QUERY', query);
    gr.orderBy('ir_query_score');
    gr.query();
    while (gr.next()) {
        var item = {};
        item.primary = gr.getDisplayValue('name');
        item.number = gr.getDisplayValue('number');
        item.sys_id = gr.getValue('sys_id');
        item.score = gr.getValue('ir_query_score');
        item.fields = getSecondary(gr);
        results.push(item);
    }

    function getSecondary(grPerson) {
        var fields = [];
        var secondary = ["field1", "field2", "field3", "etc"];
        for (var i = 0; i < secondary.length; i++) {
            var fieldObj = {};
            fieldObj.label = gr.getElement(secondary[i]).getED().getLabel();
            fieldObj.display_value = gr.getDisplayValue(secondary[i]);
            fields.push(fieldObj);
        }
        return fields;
    }

    return results;
})(query);

 

Everything starts the same, but we added a line in the while loop,  item.fields = getSecondary(gr); This sets the value of item.fields to the result of our new getSecondary() function that we are passing the glideRecord object to.

This function takes an array of field names, loops through them, and builds and array of objects with their labels and display values.

 

Here is another really good time to go and test things out.

 

Step 6 Facets

Once you have a scripted data source you will also need to script your facets.

By default you get this script. 

(function(query, facetService, searchResults) {
	/* Calculate your facets here using facetService */
	/* var stateFacet = facetService.createFacet('State', 'state'); */
	/* stateFacet.addFacetItem('Facet Item Label', '123'); */

})(query, facetService, searchResults);

 

Lets turn it into something useful...

(function(query, facetService, searchResults) {
    /* Gender Facet*/
    var genderFacet = facetService.createFacet("Gender", "gender");
    sexFacet.addFacetItem('Male', 'male');
    sexFacet.addFacetItem('Female', 'female');
    sexFacet.addFacetItem('Other', 'other');

    /* Ethnic Group */
    var ethnicMap = {};
    getEthnicChoice().forEach(function(item) {
        var ethnicLabel = item.label;
        var ethnicValue = item.value;
        if (!ethnicMap[ethnicLabel]) {
            ethnicMap[ethnicLabel] = ethnicValue;
        } else if ((ethnicMap[ethnicLabel] + "").indexOf(ethnicValue) < 0) {
            ethnicMap[ethnicLabel] += "," + ethnicValue;
        }
    });
    var ethnicFacet = facetService.createMultiChoiceFacet('Ethnic Group', 'ethnic_group');
    for (var label in ethnicMap)
        ethnicFacet.addFacetItem(label, ethnicMap[label]);

    function getEthnicChoice() {
        var results = [];
        var grSysChoice = new GlideRecord('sys_choice');
        grSysChoice.addEncodedQuery("name=<table>^element=ethnic_group^inactive=false");
        grSysChoice.orderBy('sequence');
        grSysChoice.query();
        while (grSysChoice.next()) {
            var item = {};
            item.value = grSysChoice.getValue('value');
            item.label = grSysChoice.getValue('label');
            results.push(item);
        }
        return results;
    }

 

 

 I have 2 examples here. The first is super simple, the second is much more complex.

The Gender facet is the simplest kind. It will generate a single select facet with a hard coded list of options.

The Ethnic Group facet is (obviously) way more complicated. It will generate a multiselect facet based on the values looked up from the choice list (sys_choice). When I need to create another facet like it I do a find/replace all in a text editor to change the variable names. I'm not going to break it all down as to how it works here.

 

 Now we have a Data Fetch Script, a Facet generation Script, and the Search Page template. Seems like it should all be good and we are done, right?

 

 

 

Nope, it only looks that way, the facets are not functional yet.

 

Step 7 Using the facets

In order to actually do something with the facets we need to go back to the Data Fetch script and add logic for each facet.

 

(function (query) {
    var results = [];

    var gr = new GlideRecord("<your table>");
    gr.addQuery('IR_AND_OR_QUERY', query);
    if (facets.gender) {
        grPerson.addQuery("gender", facets.gender);
    }
    if (facets.ethnic_group) {
        var ethnicSearch = grPerson.addQuery("ethnic_group", "IN", facets.ethnic_group.join(","));
        if (facets.ethnic_group.indexOf('NULL_OVERRIDE') > -1) {
            ethnicSearch.addOrCondition("ethnic_group", "");
        }
    }
    gr.orderBy('ir_query_score');
    gr.query();
    while (gr.next()) {
        var item = {};
        item.primary = gr.getDisplayValue('name');
        item.number = gr.getDisplayValue('number');
        item.sys_id = gr.getValue('sys_id');
        item.score = gr.getValue('ir_query_score');
        item.fields = getSecondary(gr);
        results.push(item);
    }

    function getSecondary(grPerson) {
        var fields = [];
        var secondary = ["field1", "field2", "field3", "etc"];
        for (var i = 0; i < secondary.length; i++) {
            var fieldObj = {};
            fieldObj.label = gr.getElement(secondary[i]).getED().getLabel();
            fieldObj.display_value = gr.getDisplayValue(secondary[i]);
            fields.push(fieldObj);
        }
        return fields;
    }
    return results;
})(query);

Again the Gender facet is easy to add. We check to see if there is a value set, if there is add it to the query.

The ethnic group facet is not actually as complicated as it looks. If the facet is set add the list of all selected groups to the query. However it does it by Value, and we are using a NULL_OVERRIDE to change the -none- value, so we need to add one more if to check for that.

 

Step 8 Happy searching!

 

That's it. You did it. 

Comments
Vineet Yadav1
Tera Expert

Hi @Eric60 

Great informative article.

Can we create facets for scripted search sources which brings data using REST API ?

AnilkumarA
Tera Contributor


We have a Country list Facet with Check Box.
When a user logs into the portal, 'Global' and users current 'country' must be auto checked and the respective kb articles must be displayed when searched.


Version history
Last update:
‎12-22-2020 12:13 PM
Updated by: