Skip to main content

Multi-Object SOSL Search in Salesforce Aura Components: Apex & UI Guide

In plain words: Salesforce Object Search Language (SOSL) in Lightning Aura components lets you search for a text term across multiple unrelated standard and custom objects—such as Accounts, Contacts, and Opportunities—in a single backend operation. Using Apex with a custom Aura interface, you can return categorized results to users without running separate SOQL queries.

In enterprise Salesforce development, building universal search bars or quick-lookup widgets requires querying across multiple objects simultaneously. While SOQL is optimized for retrieving specific fields from a single object hierarchy, SOSL scans indexed text fields across multiple objects in one search request. Below is a complete guide to wiring a multi-object SOSL search engine into a Lightning Aura component.

1. Understanding SOSL vs. SOQL in Search Architecture

Choosing between SOSL and SOQL depends on the scope of your search requirements:

  • Multi-Object Capability: A single SOSL FIND statement can query multiple sObjects simultaneously using the RETURNING clause.
  • Full-Text Indexing: SOSL leverages Salesforce's automated search indexes across text, email, and phone fields, making it significantly faster than multiple SOQL queries running LIKE '%term%' filters.
  • Apex Return Structure: SOSL queries in Apex return a list of lists of sObjects (List<List<sObject>>), matching the order defined in the RETURNING clause.
360 SOSL in Aura Architecture Card:
  • Apex Data Type: List<List<sObject>> or strongly-typed Apex wrapper classes.
  • Sanitization Method: Always escape user inputs with String.escapeSingleQuotes().
  • Security Enforcement: Add WITH USER_MODE inside RETURNING clauses to respect Field-Level Security and sharing rules.
  • Aura Data Binding: Display categorized lists using <aura:iteration> tags.

2. Implementing the Apex Search Controller

The Apex controller receives the search term from the Aura component, validates the input length, sanitizes the query, and returns a structured wrapper class with results for Accounts, Contacts, and Opportunities.

Step 1: Apex Search Controller (SearchController.cls)
public with sharing class SearchController {

    public class SearchWrapper {
        @AuraEnabled public List<Account> accounts { get; set; }
        @AuraEnabled public List<Contact> contacts { get; set; }
        @AuraEnabled public List<Opportunity> opportunities { get; set; }

        public SearchWrapper() {
            this.accounts = new List<Account>();
            this.contacts = new List<Contact>();
            this.opportunities = new List<Opportunity>();
        }
    }

    @AuraEnabled
    public static SearchWrapper performSearch(String searchTerm) {
        SearchWrapper wrapper = new SearchWrapper();

        if (String.isBlank(searchTerm) || searchTerm.trim().length() < 2) {
            return wrapper;
        }

        // Sanitize search phrase and append wildcard
        String sanitizedTerm = String.escapeSingleQuotes(searchTerm.trim()) + '*';

        // Execute multi-object SOSL search with user security
        List<List<sObject>> rawResults = [
            FIND :sanitizedTerm 
            IN ALL FIELDS 
            RETURNING 
                Account(Id, Name, Industry WITH USER_MODE),
                Contact(Id, Name, Email WITH USER_MODE),
                Opportunity(Id, Name, StageName, Amount WITH USER_MODE)
            LIMIT 50
        ];

        if (!rawResults.isEmpty()) {
            wrapper.accounts = (List<Account>) rawResults[0];
            wrapper.contacts = (List<Contact>) rawResults[1];
            wrapper.opportunities = (List<Opportunity>) rawResults[2];
        }

        return wrapper;
    }
}

3. Building the Lightning Aura Component

The Aura component below renders a search input with structured columns for each object type returned by the Apex wrapper.

Step 2: Component Markup (SearchComponent.cmp)
<aura:component controller="SearchController" implements="flexipage:availableForAllPageTypes">
    <aura:attribute name="searchTerm" type="String" default="" />
    <aura:attribute name="searchData" type="Object" />
    <aura:attribute name="isLoading" type="Boolean" default="false" />

    <lightning:card title="Multi-Object SOSL Search" iconName="standard:search">
        <div class="slds-p-around_medium">
            
            <!-- Search Input & Action Button -->
            <div class="slds-grid slds-gutters slds-grid_vertical-align-end slds-m-bottom_medium">
                <div class="slds-col slds-size_8-of-12">
                    <lightning:input 
                        type="search" 
                        label="Search Across Objects" 
                        placeholder="Enter at least 2 characters..." 
                        value="{!v.searchTerm}" />
                </div>
                <div class="slds-col slds-size_4-of-12">
                    <lightning:button 
                        label="Search" 
                        variant="brand" 
                        iconName="utility:search" 
                        onclick="{!c.searchRecords}" 
                        disabled="{!v.isLoading}" />
                </div>
            </div>

            <!-- Loading Spinner -->
            <aura:if isTrue="{!v.isLoading}">
                <lightning:spinner alternativeText="Searching records..." size="small" />
            </aura:if>

            <!-- Search Results Grid -->
            <aura:if isTrue="{!not(empty(v.searchData))}">
                <div class="slds-grid slds-gutters slds-wrap">
                    
                    <!-- Accounts Column -->
                    <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-3">
                        <h3 class="slds-text-title_bold slds-m-bottom_small">Accounts ({!v.searchData.accounts.length})</h3>
                        <ul class="slds-has-dividers_bottom-space">
                            <aura:iteration items="{!v.searchData.accounts}" var="acc">
                                <li class="slds-item">
                                    <p><strong>{!acc.Name}</strong></p>
                                    <p class="slds-text-body_small slds-text-color_weak">Industry: {!acc.Industry}</p>
                                </li>
                            </aura:iteration>
                        </ul>
                    </div>

                    <!-- Contacts Column -->
                    <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-3">
                        <h3 class="slds-text-title_bold slds-m-bottom_small">Contacts ({!v.searchData.contacts.length})</h3>
                        <ul class="slds-has-dividers_bottom-space">
                            <aura:iteration items="{!v.searchData.contacts}" var="con">
                                <li class="slds-item">
                                    <p><strong>{!con.Name}</strong></p>
                                    <p class="slds-text-body_small slds-text-color_weak">Email: {!con.Email}</p>
                                </li>
                            </aura:iteration>
                        </ul>
                    </div>

                    <!-- Opportunities Column -->
                    <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-3">
                        <h3 class="slds-text-title_bold slds-m-bottom_small">Opportunities ({!v.searchData.opportunities.length})</h3>
                        <ul class="slds-has-dividers_bottom-space">
                            <aura:iteration items="{!v.searchData.opportunities}" var="opp">
                                <li class="slds-item">
                                    <p><strong>{!opp.Name}</strong></p>
                                    <p class="slds-text-body_small slds-text-color_weak">Stage: {!opp.StageName}</p>
                                </li>
                            </aura:iteration>
                        </ul>
                    </div>

                </div>
            </aura:if>
        </div>
    </lightning:card>
</aura:component>
Step 3: Client-Side Controller (SearchComponentController.js)
({
    searchRecords: function(component, event, helper) {
        var searchTerm = component.get("v.searchTerm");

        if (!searchTerm || searchTerm.trim().length < 2) {
            helper.showToast("Search Warning", "Please enter at least 2 characters.", "warning");
            return;
        }

        component.set("v.isLoading", true);
        var action = component.get("c.performSearch");
        action.setParams({ searchTerm: searchTerm });

        action.setCallback(this, function(response) {
            component.set("v.isLoading", false);
            var state = response.getState();

            if (state === "SUCCESS") {
                var resultData = response.getReturnValue();
                component.set("v.searchData", resultData);
            } else if (state === "ERROR") {
                var errors = response.getError();
                var message = (errors && errors[0] && errors[0].message) ? errors[0].message : "Unknown search error";
                helper.showToast("Query Error", message, "error");
            }
        });

        $A.enqueueAction(action);
    }
})
Step 4: Helper Script (SearchComponentHelper.js)
({
    showToast: function(title, message, type) {
        var toastEvent = $A.get("e.force:showToast");
        if (toastEvent) {
            toastEvent.setParams({
                title: title,
                message: message,
                type: type
            });
            toastEvent.fire();
        }
    }
})

4. Common Traps & SOSL Best Practices

SOSL Injection Trap: Unsanitized String Concatenation
Building dynamic SOSL queries using raw string concatenation like 'FIND \'' + searchTerm + '\'' without escaping user inputs creates SOQL/SOSL injection vulnerabilities and causes syntax crashes when users type special characters (like quotes or dashes). Always use static SOSL binds (FIND :sanitizedTerm) or sanitize inputs using String.escapeSingleQuotes().
Core Rule: Wrap multi-object SOSL return lists into a strongly-typed Apex wrapper class to ensure clean property binding in Aura templates, and always enforce WITH USER_MODE on queries.
  • Wildcard Handling: Append an asterisk (*) to search terms so partial keywords (e.g., Acme*) match full company names.
  • Minimum Character Threshold: Enforce a client-side minimum search length of 2 characters to avoid unselective queries and empty result payloads.
  • Respect Limits: Remember that SOSL returns a maximum of 2,000 records across all objects combined in a single transaction. Set sensible LIMIT clauses on each object inside the RETURNING statement.

Summary

Implementing SOSL inside Lightning Aura components provides a fast, full-text search experience across multiple Salesforce objects simultaneously. By wrapping multi-object results in clean Apex wrapper classes, sanitizing user inputs, and enforcing user-mode security, developers can build scalable and secure search tools across enterprise Salesforce environments.