In plain words: SOSL (Salesforce Object Search Language) is a search language designed to scan text across multiple unrelated standard and custom objects in a single query, returning separate lists of matching records efficiently.
While SOQL queries individual database tables with exact field matching, SOSL indexes text fields across your entire org. Pairing SOSL with a custom Visualforce or Lightning interface allows developers to create unified global search panels tailored to custom business workflows.
Prerequisites
- Basic knowledge of Apex controllers, properties (
{ get; set; }), and Visualforce tag structures. - Salesforce Developer Edition, Sandbox, or Scratch Org.
- Understanding of text indexing and field retrieval across standard Salesforce objects.
Step 1: Build the Apex Controller
In Apex, executing a dynamic SOSL statement with Search.query() returns a multidimensional list: List<List<sObject>>. Each nested list corresponds directly to an object declared in the RETURNING clause.
public with sharing class SOSLController {
public String searchTerm { get; set; }
public List<Account> accountResults { get; set; }
public List<Contact> contactResults { get; set; }
public List<Opportunity> opportunityResults { get; set; }
public SOSLController() {
accountResults = new List<Account>();
contactResults = new List<Contact>();
opportunityResults = new List<Opportunity>();
}
public PageReference performSearch() {
// Enforce a minimum character length to prevent broad index scans
if (String.isBlank(searchTerm) || searchTerm.trim().length() < 2) {
ApexPages.addMessage(new ApexPages.Message(
ApexPages.Severity.WARNING,
'Please enter at least 2 characters to search.'
));
return null;
}
// Sanitize input to protect against SOSL injection
String sanitizedTerm = String.escapeSingleQuotes(searchTerm.trim()) + '*';
String searchQuery = 'FIND :sanitizedTerm IN ALL FIELDS RETURNING ' +
'Account(Id, Name, Industry, Phone), ' +
'Contact(Id, Name, Email, Account.Name), ' +
'Opportunity(Id, Name, StageName, Amount)';
List<List<sObject>> searchList = Search.query(searchQuery);
accountResults = (List<Account>) searchList[0];
contactResults = (List<Contact>) searchList[1];
opportunityResults = (List<Opportunity>) searchList[2];
return null;
}
}
Trap Alert: Never concatenate unsanitized user strings directly into dynamic SOSL queries without using
String.escapeSingleQuotes() or bind variables. Always handle the return type as List<List<sObject>> and cast each individual index to its concrete object list type.
Step 2: Build the Visualforce Search Interface
Create a Visualforce page named SOSLSearchPage.page. Bind the input field, search button, and tabbed/sectioned data tables to the controller lists:
<apex:page controller="SOSLController" docType="html-5.0" lightningStylesheets="true">
<apex:form>
<apex:pageMessages id="msg" />
<apex:pageBlock title="Global SOSL Multi-Object Search">
<apex:pageBlockSection columns="2">
<apex:inputText value="{!searchTerm}" label="Search Keyword" html-placeholder="e.g. Acme, John..." />
<apex:commandButton value="Run Global Search" action="{!performSearch}" reRender="searchResults,msg" status="searchStatus" />
</apex:pageBlockSection>
<apex:actionStatus id="searchStatus" startText="Scanning database..." />
</apex:pageBlock>
<apex:outputPanel id="searchResults">
<!-- Matching Accounts -->
<apex:pageBlock title="Matching Accounts ({!accountResults.size})" rendered="{!accountResults.size > 0}">
<apex:pageBlockTable value="{!accountResults}" var="acc">
<apex:column headerValue="Account Name">
<apex:outputLink value="/{!acc.Id}" target="_blank">{!acc.Name}</apex:outputLink>
</apex:column>
<apex:column value="{!acc.Industry}" />
<apex:column value="{!acc.Phone}" />
</apex:pageBlockTable>
</apex:pageBlock>
<!-- Matching Contacts -->
<apex:pageBlock title="Matching Contacts ({!contactResults.size})" rendered="{!contactResults.size > 0}">
<apex:pageBlockTable value="{!contactResults}" var="con">
<apex:column headerValue="Contact Name">
<apex:outputLink value="/{!con.Id}" target="_blank">{!con.Name}</apex:outputLink>
</apex:column>
<apex:column value="{!con.Email}" />
<apex:column value="{!con.Account.Name}" headerValue="Associated Account" />
</apex:pageBlockTable>
</apex:pageBlock>
<!-- Matching Opportunities -->
<apex:pageBlock title="Matching Opportunities ({!opportunityResults.size})" rendered="{!opportunityResults.size > 0}">
<apex:pageBlockTable value="{!opportunityResults}" var="opp">
<apex:column headerValue="Opportunity Name">
<apex:outputLink value="/{!opp.Id}" target="_blank">{!opp.Name}</apex:outputLink>
</apex:column>
<apex:column value="{!opp.StageName}" />
<apex:column value="{!opp.Amount}" />
</apex:pageBlockTable>
</apex:pageBlock>
</apex:outputPanel>
</apex:form>
</apex:page>
360 Architecture Summary:
- SOQL vs. SOSL: Use SOQL for exact records on single/related objects; use SOSL when searching for arbitrary text across multiple unrelated objects simultaneously.
- Governor Limits: A single synchronous Apex transaction can execute up to 20 SOSL statements and return up to 2,000 total records across all objects.
- Styling Note:
lightningStylesheets="true"applies the modern Salesforce Lightning Design System (SLDS) styling to legacy Visualforce tags automatically.
Step 3: Verification and Real-World Usage
Step-by-Step Testing Procedure:
- Deploy both
SOSLController.clsandSOSLSearchPage.pageto your environment. - Open the Visualforce page by navigating to
/apex/SOSLSearchPagein your browser. - Type a standard test name (such as an existing account/contact name) and click Run Global Search.
- Confirm that each object section displays only relevant matching records in its own individual table.
Core Takeaway: SOSL offers an optimized, high-throughput text search engine across multiple standard and custom Salesforce objects in one single query, eliminating the need for sequential SOQL queries.