SOSL bug (skuid.sfdc.search)

  • 1
  • Problem
  • Updated 1 year ago
  • Solved
I'm working on a skuidified replacement for the global search function on 10.0.5.  The goal is to have a simple google like search within the skuid framework that provides contextual starting points that is more refined than the salesforce search and visually better.  The starting point for this solutions is from this documentation:  https://docs.skuid.com/latest/en/skuid/api/skuid_sfdc.html

With the skuid.sfdc.search API, the SOSL string rendered does not conform to the SOSL protocol for the search string, which is generating inaccurate & unusable search results.  

This is the syntax that the API generates:

FIND 'Search Text String*' IN

This is the syntax that SOSL expects for a context search:

FIND {Search Text String} IN

The current API search syntax yields a bunch of garbage.  After playing around with a variety of syntaxes, and comparing to search results in the workbench and native salesforce UI, it became apparent that the API was wrapping whatever input with the single quote and wildcard and I have no control over it.  To top it off, if I try to wrap with {} it totally bombs because the API wraps that as well and the search chokes.

The lack of a global search replacement poses a real challenge. 
Photo of John Dahlberg

John Dahlberg, Champion

  • 2,442 Points 2k badge 2x thumb

Posted 1 year ago

  • 1
Photo of Mark DeSimone

Mark DeSimone, Official Rep

  • 11,040 Points 10k badge 2x thumb
Hi John,

Would you mind sharing the garbage results that your search is yielding, and the script snippet you're working with?

I was also curious about which declarative options you've looked into so far, and what limitations in those might have led you down the programmatic path. 
Photo of John Dahlberg

John Dahlberg, Champion

  • 2,442 Points 2k badge 2x thumb
The declarative search produces a drop-down with typeahead support and flexibility for initiating actions quickly.  I'm using this in our global header for the most common scenarios for say finding a contact and pulling a page of targeted information for a phone call.

What I'm trying to get to is a google like search that goes across a large span of salesforce data, including content natural language search and the relevancy that SOSL provides, so this typeahead / dropdown approach doesn't scale well.  I can reproduce the behavior using the sample code on the API documentation page mentioned above.  If you put in "Crazy Search" into the query text, you get a bunch of seemingly random records.  When you paste the produced SOSL into Workbench, you get a Malformed search error ("The search term must be enclosed in braces").  When you replace 'Crazy Search*' with {Crazy Search}, you get a more reasonable approach (no records).  Modifying the search term to a more reasonable business term aligns with what I would expect in results.

It appears that the search component works differently (better than the API), but it's also a lot of configuration work to setup and the UI isn't the best.

The script is an agnostic approach that evaluates models defined on a page, performs a SOSL search based on configured models, injects the returned Ids back into the model and executes the model query to pull in the fields to display on the page.  The snippet can be referenced from any page and will work on any Salesforce object, including content.  The display part is totally declarative and it seems to be a good balance between leveraging the smarts of SOSL with the creative design flexibility with SKUID.

Here's the script as it stands now:

var params = arguments[0],
    searchModel = params.model,
    searchRow = params.row,
    queryText = params.row.Search;
    searchConditionName = params.row.SearchConditionName;
    $ = skuid.$;

//  Update the display text while performing search
searchModel.updateRow(
    {
        Id : searchRow.__skuid_record__.__id
    },
    {
        SearchingText : 'Searching for ' + queryText    
    });

//  Grab the Models from the page
modlist = skuid.model.getModelList();
var searchModels = [];
var ret = [];
console.log(modlist);

$.each(modlist,function(e,sfModel){
    if(sfModel.dataSourceTypeName == 'salesforce' && sfModel.id.substring(0,searchConditionName.length) == searchConditionName ){
        sfModel.emptyData();
        searchModels.push(sfModel);
        var cond = [];

        $.each(sfModel.conditions,function(c,modelCondition){
            if(modelCondition.field !== 'Id' && modelCondition.inactive === false){
                cond.push(modelCondition);
            }
        });
        ret.push({ "objectName" : sfModel.objectName, "fields": ["Id"], "recordsLimit" : sfModel.recordsLimit, "conditions" : cond });
    } 
});

// Run the SOSL Search
$.when(skuid.sfdc.search({
    query: queryText,
    searching: "ALL FIELDS",
    returning: ret 
    
})).done(function(searchResult){
    $.each(searchResult.results,function(i,sobjectResult){
        var objectName = sobjectResult.objectName;
        var records = sobjectResult.records;
        var ConditionRecords = [];
//        console.log('* Found ' + records.length + ' ' + objectName + ' records');
        $.each(records,function(j,record){
            ConditionRecords.push(record.Id);
        });
        
// Update the model conditions for the Salesforce Models
        $.each(searchModels,function(s,searchModels){
            if(searchModels.objectName == objectName){
                var SearchCond = searchModels.getConditionByName(searchConditionName);
                searchModels.setCondition(SearchCond,ConditionRecords);
                searchModels.updateData();
            }
        });
    });
}).fail(function(searchResult){
//  Update Message to indicate search failed
        searchModel.updateRow(
        {
            Id : searchRow.__skuid_record__.__id
        },
        {
            SearchingText : 'Search Incomplete'    
        }
    );
}).always(function(searchResult){
   console.log('Raw SOSL generated: ' + searchResult.sosl);

// Update Message to show completion
    searchModel.updateRow(
        {
            Id : searchRow.__skuid_record__.__id
        },
        {
            SearchingText : 'Search Complete'    
        }
    );
});
Photo of Mark DeSimone

Mark DeSimone, Official Rep

  • 11,040 Points 10k badge 2x thumb
Hi John,

I'm wondering if you've looked at the detail pages for any of the seemingly random records. The query could be matching one of your search terms in any SOSL-accessible field on that record, if you're specifying ALL FIELDS. 

I've been able to use the snippet given as an example in the documentation you shared, and gotten results that seem predictable in my small test environment. I see that the query string has '' instead of {}, but Salesforce isn't returning an error for me, so I wonder if there's more going on behind the scenes before the SOSL query is submitted.
Photo of John Dahlberg

John Dahlberg, Champion

  • 2,442 Points 2k badge 2x thumb
I don't have visibility to it, so this is somewhat of a guess, but it looks like the SKUID search API has some error handling logic in place behind the scenes that works around the brace syntax issue but does something funky with the search paramaters.   Try running the same query in workbench and you should get this type of error:



In the meantime, I finally got around to figuring out how to do a direct REST connection to our Salesforce environment and did a quick Proof of Concept this morning using the Salesforce REST SOSL API.  This produced the results we're after and unless there's a quick identification on the skuid API, I'm pretty confident I can refactor the approach using a REST model to perform the SOSL.
Photo of John Dahlberg

John Dahlberg, Champion

  • 2,442 Points 2k badge 2x thumb
So I've got this figured out.  Looks like the skuid API figures out the syntax adjustments behind the scenes, but displays the search criteria in the incorrect format in the SOSL displayed in the result, so this issue turned out to be a rabbit hole. 

Turns out that what was happening is we had a set of models setup to receive the array of Ids returned in the query result and when these objects came back null, the model filter didn't have any values in it.  I would expect a model with a null value in the filter for the Id would simply not return any records, but it ends up returning the first set of random records it finds.  Adding a criteria to bypass the model updates for null result sets corrects this issue.

So now I've got a working design for both the skuid.sfdc.search API and the native Salesforce REST SOSL API.  I'll post more detail on these when I have some time to write up the final solution.
Photo of Mark DeSimone

Mark DeSimone, Official Rep

  • 11,040 Points 10k badge 2x thumb
Hi John, I'm glad to hear that you have a way forward, and I appreciate the time you've taken to reach out and share your progress.  I agree that there must be more going on behind the scenes to prep the search term before its query is submitted to Salesforce, otherwise we would see same malformed search error when trying it from our Skuid pages. 

Of course Skuid will always advocate for using a declarative approach when possible, since programmatic solutions are so much trickier to troubleshoot and maintain. In case it's useful to anyone else who's looking for information on setting up a SOSL query via API, here is XML for the page I used for testing. Please note that it's not a full solution; you would need to write additional script to handle the search results as your needs would dictate. 
<skuidpage unsavedchangeswarning="" personalizationmode="server" showsidebar="true" useviewportmeta="true" showheader="true">
    <models>
        <model id="NewModel" query="true" createrowifnonefound="true" datasource="Ui-Only" processonclient="true">
            <fields>
                <field id="SearchTerm" displaytype="TEXT" label="SearchTerm"/>
            </fields>
            <conditions/>
            <actions/>
        </model>
    </models>
    <components>
        <grid uniqueid="sk-rd0tC-107">
            <divisions>
                <division behavior="flex" verticalalign="top" minwidth="100px" ratio="1">
                    <components/>
                </division>
                <division behavior="specified" verticalalign="top" width="50%">
                    <components>
                        <template multiple="false" uniqueid="sk-rdELT-129" allowhtml="true">
                            <contents>&lt;br/&gt;
&lt;br/&gt;
&lt;br/&gt;
Enter a search term below, and click the SOSL Search button to submit it. This is being done with a snippet using the SOSL API, which is currently set up to target the Accounts and Contacts objects. Open your browser console to see the search results logged there. 
&lt;br/&gt;
&lt;br/&gt;
&lt;a href="https://docs.skuid.com/latest/en/skuid/api/skuid_sfdc.html target="_blank"&gt;Learn more here&lt;/a&gt;&lt;br/&gt;
&lt;br/&gt;</contents>
                        </template>
                        <basicfieldeditor showheader="true" showsavecancel="false" showerrorsinline="true" model="NewModel" buttonposition="" uniqueid="sk-hn8dg-123" mode="edit" layout="above">
                            <columns>
                                <column width="100%">
                                    <sections>
                                        <section title="Section A" collapsible="no" showheader="false">
                                            <fields>
                                                <field uniqueid="sk-hnD_m-149" id="SearchTerm" valuehalign="" type=""/>
                                            </fields>
                                        </section>
                                    </sections>
                                </column>
                            </columns>
                        </basicfieldeditor>
                        <buttonset model="NewModel" uniqueid="sk-hmT-t-104" position="center">
                            <buttons>
                                <button type="multi" label="SOSL Search" uniqueid="sk-hmUDl-107">
                                    <actions>
                                        <action type="custom" snippet="newSnippet">
                                            <onerroractions>
                                                <action type="blockUI" message="There was an error" timeout="3000"/>
                                            </onerroractions>
                                        </action>
                                    </actions>
                                </button>
                            </buttons>
                        </buttonset>
                    </components>
                </division>
                <division behavior="flex" verticalalign="top" minwidth="100px" ratio="1">
                    <components/>
                </division>
            </divisions>
            <styles>
                <styleitem type="background" bgtype="none"/>
            </styles>
        </grid>
    </components>
    <resources>
        <labels/>
        <javascript>
            <jsitem location="inlinesnippet" name="SOSL_Snippet" cachelocation="false">var params = arguments[0],
$ = skuid.$,
searchModel = skuid.model.getModel('NewModel'),
    row = searchModel.getFirstRow();
    queryText = searchModel.getFieldValue(row, 'SearchTerm');
    console.log (queryText);
// Perform our SOSL Search
$.when(skuid.sfdc.search({
    query: queryText,
    searching: "ALL FIELDS",
    returning: [
       { "objectName": "Account", "fields": ["Name","CreatedDate"] },
       { "objectName": "Contact", "fields": ["FirstName","LastName","CreatedDate"] }
    ]
})).done(function(searchResult){
    $.each(searchResult.results,function(i,sobjectResult){
        var objectName = sobjectResult.objectName;
        var records = sobjectResult.records;
        console.log('* Found ' + records.length + ' ' + objectName + ' records');
        $.each(records,function(j,record){
            if (objectName==='Contact') {
                console.log('Found Contact: ' + record.FirstName + ' ' + record.LastName);
            }
            else console.log('Found Account: ' + record.Name);
        });
    });
}).fail(function(searchResult){
    console.error('Search failed: ' + searchResult.error);
}).always(function(searchResult){
   console.log('Raw SOSL generated: ' + searchResult.sosl);
    console.log('Original search request');
    console.log(searchResult.request);
});</jsitem>
        </javascript>
        <css/>
    </resources>
    <styles>
        <styleitem type="background" bgtype="none"/>
    </styles>
</skuidpage>
Photo of John Dahlberg

John Dahlberg, Champion

  • 2,442 Points 2k badge 2x thumb
Thanks Mark,

Totally agree on keeping declarative as much as possible and lesson learned on the model behavior.  In cases where script is necessary to supplement functionality, I'm a firm believer that a script should be agnostic to business rules and be driven by parameters, which Skuid is really good about providing into javascript.  The script above is designed to dynamically build the SOQL statement off of a list of models on the page to allow different search scoping in different parts of our application along with some filtering capabilities to do some fine tuning on the query by end users.  Then all the layout options tie back to skuid's native declarative methods.  In our case we're using Decks to display the results and can fine tune the cards based on the object type, but we have flexibility on the UI components.  The line that I had to tweak in order to get this corrected follows:

        $.each(searchModels,function(s,searchModels){
            if(searchModels.objectName == objectName){
                var SearchCond = searchModels.getConditionByName(searchConditionName);
                searchModels.setCondition(SearchCond,ConditionRecords);
                searchModels.updateData();
            }
        });
});

is changed to:

$.each(searchModels,function(s,searchModels){
            if(searchModels.objectName == objectName && ConditionRecords.length > 0){
                var SearchCond = searchModels.getConditionByName(searchConditionName);
                searchModels.setCondition(SearchCond,ConditionRecords);
                searchModels.updateData();
            }
        });
});


Here's a screenshot of an early prototype:

Photo of Mark DeSimone

Mark DeSimone, Official Rep

  • 11,040 Points 10k badge 2x thumb
Wow, that's pretty slick. Thanks again for your contributions to our community, John.