Skip to main content

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

Updated Add a comment
💬 In plain words: Salesforce Object Search Language (SOSL) lets you search for a specific word or phrase across multiple, completely unrelated objects (like Accounts, Contacts, and Opportunities) at the exact same time. When you combine SOSL with Lightning Web Components (LWC) or Aura, you can build powerful, Google-style global search bars that instantly return categorized results to your users without bogging down the system with dozens of separate SOQL queries.

In enterprise Salesforce applications, users rarely want to search just one table at a time. They want to type a company name and instantly see the Account, the related Contacts, and any open Deals. While SOQL is great for fetching specific fields from a single object hierarchy, it is slow and inefficient for full-text searching across multiple objects. To build a true search engine widget, you must wire a SOSL query in Apex to your LWC or Aura frontend.

📌 Key Points

  • Multi-Object Power: A single SOSL FIND statement can query text, email, and phone fields across multiple objects simultaneously.
  • Apex Return Structure: SOSL returns a "List of Lists" (List<List<sObject>>). You must map this back to your frontend.
  • Sanitization is Mandatory: Always use String.escapeSingleQuotes() to prevent injection attacks when accepting search terms from a UI.
  • LWC vs. Aura: Both frameworks use the exact same Apex backend logic, differing only in how they render the HTML and handle navigation.

🏗️ 1. The Apex Backend: SOSL & Wrapper Classes

Because SOSL returns a complex List<List<sObject>>, passing raw arrays back to your JavaScript front-end gets messy fast. The best architectural pattern is to create an Apex Wrapper Class that neatly categorizes the results into named lists before sending them to the UI.

Step 1: The Universal Apex Controller (GlobalSearchController.cls)
This controller handles the SOSL query, enforces security, and works perfectly for both LWC and Aura components.
public with sharing class GlobalSearchController {

    // The Wrapper Class neatly packages results for the JavaScript frontend
    public class SearchResultWrapper {
        @AuraEnabled public List<Account> accounts { get; set; }
        @AuraEnabled public List<Contact> contacts { get; set; }
        @AuraEnabled public List<Opportunity> opportunities { get; set; }

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

    // @AuraEnabled(cacheable=true) is recommended for LWC wire adapters
    @AuraEnabled(cacheable=true)
    public static SearchResultWrapper searchEntities(String searchTerm) {
        SearchResultWrapper wrapper = new SearchResultWrapper();

        if (String.isBlank(searchTerm) || searchTerm.trim().length() < 2) {
            return wrapper; // Return empty wrapper if search is too short
        }

        // Safely sanitize the input and append the wildcard (*)
        String sanitizedTerm = String.escapeSingleQuotes(searchTerm.trim()) + '*';
        
        // Execute the SOSL query. Note the WITH USER_MODE security enforcement.
        List<List<SObject>> searchResults = [
            FIND :sanitizedTerm 
            IN ALL FIELDS 
            RETURNING 
                Account(Id, Name, Industry WITH USER_MODE),
                Contact(Id, Name, Email WITH USER_MODE),
                Opportunity(Id, Name, StageName WITH USER_MODE)
            LIMIT 50
        ];

        // Map the List of Lists back to the Wrapper properties
        if (!searchResults.isEmpty()) {
            wrapper.accounts = (List<Account>) searchResults[0];
            wrapper.contacts = (List<Contact>) searchResults[1];
            wrapper.opportunities = (List<Opportunity>) searchResults[2];
        }

        return wrapper;
    }
}

⚡ 2. The Frontend: Lightning Web Components (LWC)

LWC is the modern standard for Salesforce UI. The component below takes the user's input, calls the Apex controller imperatively, and renders the categorized results.

Step 2: LWC JavaScript (soslSearch.js)
import { LightningElement, track } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
import searchEntities from '@salesforce/apex/GlobalSearchController.searchEntities';

export default class SoslSearch extends NavigationMixin(LightningElement) {
    searchTerm = '';
    @track results = { accounts: [], contacts: [], opportunities: [] };

    // Dynamically check if we have any results to show
    get hasResults() {
        return (this.results.accounts.length > 0) || 
               (this.results.contacts.length > 0) || 
               (this.results.opportunities.length > 0);
    }

    handleInputChange(event) {
        this.searchTerm = event.target.value;
    }

    async handleSearch() {
        if (!this.searchTerm || this.searchTerm.length < 2) return;

        try {
            // Call Apex imperatively
            this.results = await searchEntities({ searchTerm: this.searchTerm });
        } catch (error) {
            console.error('Search failed:', error);
        }
    }

    // Safely route the user to the actual record page using standard navigation
    handleNavigate(event) {
        event.preventDefault();
        const recordId = event.currentTarget.dataset.id;

        this[NavigationMixin.Navigate]({
            type: 'standard__recordPage',
            attributes: {
                recordId: recordId,
                actionName: 'view'
            }
        });
    }
}

⚙️ 3. The Frontend: Legacy Aura Components

If you are maintaining an older org or building a component for a context where LWC isn't fully supported yet, you can use the exact same Apex controller with an Aura frontend.

Step 2 Alternative: Aura Controller (SearchComponentController.js)
({
    searchRecords: function(component, event, helper) {
        var searchTerm = component.get("v.searchTerm");

        if (!searchTerm || searchTerm.length < 2) return;

        component.set("v.isLoading", true);
        
        // Setup the Apex action
        var action = component.get("c.searchEntities");
        action.setParams({ searchTerm: searchTerm });

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

            if (state === "SUCCESS") {
                // Bind the wrapper directly to an Aura attribute
                component.set("v.searchData", response.getReturnValue());
            } else {
                console.error("Search failed.");
            }
        });

        $A.enqueueAction(action);
    }
})

🚨 4. Common Traps & SOSL Best Practices

⚠ SOSL INJECTION TRAP: Unsanitized String Concatenation
Never build dynamic SOSL queries using raw string concatenation like 'FIND \'' + searchTerm + '\''. If a user types a quote or a dash, your code will crash, or worse, open you up to SOSL injection attacks. Always use static SOSL binds (FIND :sanitizedTerm) and manually clean the input using String.escapeSingleQuotes().
⚠ NAVIGATION TRAP: Hardcoding Record Slugs
Never write raw anchor links like <a href={'/' + result.Id}> in your UI. Hardcoded URLs break deep linking in Experience Cloud communities, Salesforce Mobile, and Lightning Console tabs. Always import NavigationMixin in LWC or use force:navigateToSObject in Aura.
🧠 CORE RULE: Always enforce WITH USER_MODE inside each object's RETURNING clause. This ensures your custom search bar respects the active user's Field-Level Security (FLS) and sharing rules automatically.

🎤 5. Core Interview Q&A

Q: What is the maximum number of records a SOSL query can return?

🎯 Say this: A single SOSL query can return a maximum of 2,000 records total, spread across all objects. To prevent one object from hogging all the results, you should set sensible LIMIT clauses on each individual object inside the RETURNING statement (e.g., Account(Id LIMIT 50)).

Q: Why did you add an asterisk (*) to the search term in Apex?

Answer: The asterisk acts as a wildcard. If a user searches for "Uni", adding the asterisk turns it into "Uni*", which ensures the SOSL engine returns partial matches like "United Oil" or "Universal Containers".

🔗 6. Follow-Up Question

Q: How do you handle search performance if the user types too quickly?

Answer: In a real-world LWC, you should implement "Debouncing" in your JavaScript controller. Instead of firing an Apex callout on every single keystroke, you set a brief timer (e.g., 300ms). The timer resets on every keystroke, ensuring the Apex callout only fires after the user pauses typing. This saves massive amounts of server resources and prevents UI stuttering.

Was this article helpful?

Have a question or spotted a mistake? Ask here about this article

Found this guide useful? Buy me a coffee (opens in a new tab)

Comments

Questions, corrections and your own experience are all welcome.