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
FINDstatement can query multiple sObjects simultaneously using theRETURNINGclause. - 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 theRETURNINGclause.
- 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_MODEinsideRETURNINGclauses 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.
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.
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>
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);
}
})
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
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().
- 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
LIMITclauses on each object inside theRETURNINGstatement.
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.