Skip to main content

How to Build a Custom Multi-Object Search in Salesforce Using SOSL and Visualforce

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.cls and SOSLSearchPage.page to your environment.
  • Open the Visualforce page by navigating to /apex/SOSLSearchPage in 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.