Skip to main content

Multi-Object SOSL Search in Salesforce LWC: Apex & Navigation Guide

In plain words: Salesforce Object Search Language (SOSL) in Lightning Web Components (LWC) allows you to search across multiple unrelated standard and custom objects (like Accounts, Contacts, and Leads) in a single query. When paired with an Apex backend, SOSL scans text, email, and phone fields across your org and returns categorized results directly to your front-end component.

Standard SOQL queries are designed to fetch specific records from a single object or related parent-child hierarchies. When building global search bars or multi-entity lookup utilities, executing multiple separate SOQL queries exhausts transaction limits and creates performance lag. Combining SOSL with Lightning Web Components delivers an optimized search engine that queries multiple objects simultaneously in a single roundtrip.

1. SOSL vs. SOQL Architecture in Salesforce

Understanding when to choose SOSL over SOQL ensures optimal resource utilization:

  • Multi-Object Capability: A single SOSL FIND statement can query up to 2,000 total records across multiple sObjects simultaneously using the RETURNING clause.
  • Full-Text Indexing: SOSL evaluates text, phone, email, and picklist fields using Salesforce's automated search indexes, making it significantly faster for free-text search than SOQL LIKE '%term%' operations.
  • Apex Search Return Types: In Apex, SOSL queries executed via Search.query() return a list of lists of sObjects (List<List<SObject>>), where each inner list matches the corresponding object specified in the RETURNING clause.
360 SOSL Architecture Card:
  • Query Syntax: FIND 'searchTerm*' IN ALL FIELDS RETURNING Account(Id, Name), Contact(Id, Name, Email)
  • Apex Data Type: List<List<SObject>>
  • Sanitization Method: Always sanitize free text with String.escapeSingleQuotes().
  • LWC Navigation: Standard NavigationMixin.Navigate for lightning record page routing.

2. Implementing the Apex Search Controller

The Apex controller below executes a parameterized SOSL search across Accounts and Contacts, returning a structured wrapper map that is easily consumed by Lightning Web Components.

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

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

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

    @AuraEnabled(cacheable=true)
    public static SearchResultWrapper searchEntities(String searchTerm) {
        SearchResultWrapper wrapper = new SearchResultWrapper();

        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>> searchResults = [
            FIND :sanitizedTerm 
            IN ALL FIELDS 
            RETURNING 
                Account(Id, Name, Industry, Phone WITH USER_MODE),
                Contact(Id, Name, Email, Title WITH USER_MODE)
            LIMIT 50
        ];

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

        return wrapper;
    }
}

3. Building the LWC Search Interface

The front-end component handles search input, invokes the Apex controller imperatively, and routes users to clicked records using NavigationMixin.

Step 2: HTML Template (soslSearch.html)
<template>
    <lightning-card title="Enterprise SOSL Search" icon-name="standard:search">
        <div class="slds-p-around_medium">
            <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="Type at least 2 characters..."
                        value={searchTerm}
                        onchange={handleInputChange}>
                    </lightning-input>
                </div>
                <div class="slds-col slds-size_4-of-12">
                    <lightning-button
                        label="Search"
                        variant="brand"
                        icon-name="utility:search"
                        onclick={handleSearch}>
                    </lightning-button>
                </div>
            </div>

            <!-- Search Results Display -->
            <template lwc:if={hasResults}>
                <div class="slds-grid slds-gutters slds-wrap">
                    
                    <!-- Accounts Column -->
                    <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-2">
                        <h3 class="slds-text-title_bold slds-m-bottom_small">
                            Accounts ({results.accounts.length})
                        </h3>
                        <ul class="slds-has-dividers_bottom-space">
                            <template for:each={results.accounts} for:item="acc">
                                <li key={acc.Id} class="slds-item">
                                    <a href="javascript:void(0);" data-id={acc.Id} onclick={handleNavigateToRecord}>
                                        <strong>{acc.Name}</strong>
                                    </a>
                                    <p class="slds-text-body_small slds-text-color_weak">Industry: {acc.Industry}</p>
                                </li>
                            </template>
                        </ul>
                    </div>

                    <!-- Contacts Column -->
                    <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-2">
                        <h3 class="slds-text-title_bold slds-m-bottom_small">
                            Contacts ({results.contacts.length})
                        </h3>
                        <ul class="slds-has-dividers_bottom-space">
                            <template for:each={results.contacts} for:item="con">
                                <li key={con.Id} class="slds-item">
                                    <a href="javascript:void(0);" data-id={con.Id} onclick={handleNavigateToRecord}>
                                        <strong>{con.Name}</strong>
                                    </a>
                                    <p class="slds-text-body_small slds-text-color_weak">Email: {con.Email}</p>
                                </li>
                            </template>
                        </ul>
                    </div>

                </div>
            </template>
        </div>
    </lightning-card>
</template>
Step 3: JavaScript Controller (soslSearch.js)
import { LightningElement, track } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import searchEntities from '@salesforce/apex/GlobalSearchController.searchEntities';

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

    get hasResults() {
        return (this.results.accounts && this.results.accounts.length > 0) || 
               (this.results.contacts && this.results.contacts.length > 0);
    }

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

    async handleSearch() {
        if (!this.searchTerm || this.searchTerm.trim().length < 2) {
            this.showToast('Search Error', 'Please enter at least 2 characters to search.', 'warning');
            return;
        }

        try {
            const data = await searchEntities({ searchTerm: this.searchTerm });
            this.results = data;

            if (!this.hasResults) {
                this.showToast('No Records Found', 'No matches found for your search criteria.', 'info');
            }
        } catch (error) {
            this.showToast('Query Error', error.body ? error.body.message : error.message, 'error');
        }
    }

    handleNavigateToRecord(event) {
        event.preventDefault();
        const recordId = event.currentTarget.dataset.id;

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

    showToast(title, message, variant) {
        this.dispatchEvent(new ShowToastEvent({ title, message, variant }));
    }
}

4. Common Traps & Platform Limitations

Navigation & URL Trap: Hardcoding Record Slugs
Writing raw anchor links like <a href={'/' + result.Id}> breaks deep linking in Experience Cloud communities, Salesforce Mobile, and Lightning Console tabs. Always import NavigationMixin from lightning/navigation and trigger standard standard__recordPage navigation events to preserve platform routing.
Core Rule: Use static inline SOSL binds (:sanitizedTerm) with WITH USER_MODE to prevent injection vulnerabilities, and wrap multi-object return payloads in strongly-typed Apex wrappers for clean LWC consumption.
  • Minimum Search Length: Salesforce search indexes typically require at least 2 characters to execute wildcard queries effectively.
  • Wildcard Handling: Append the asterisk wildcard (*) to search terms so partial matches (e.g., "Uni*" matching "United Oil") return reliably.
  • Enforce User Mode Security: Add WITH USER_MODE inside each object's RETURNING clause to respect Field-Level Security and object sharing rules automatically.

Summary

Combining SOSL with Lightning Web Components creates a fast, multi-object search experience for Salesforce users. By structuring clean Apex wrapper controllers, sanitizing input parameters, and using standard NavigationMixin events, developers can build scalable search interfaces across desktop and mobile Lightning environments.