SfSkills Domains Docs GitHub

Home / data / sosl-search-result-limits

sosl-search-result-limits

Use when a SOSL search silently omits a record that clearly matches the search term — diagnosing the search engine's internal per-stage record caps: the 2,000-record term-matching scan (API v28.0+), the 250-record single-object default and how a WHERE or ORDER BY inside the RETURNING clause raises it to 2,000, the min(2000/n, 250) multi-object split, View All Data vs.

Open the skill folder on GitHub


SOSL Search Result Limits

This skill activates when a SOSL search returns fewer records than expected — or a specific record that plainly matches the search term never appears — and you need to explain why. The cause is almost never the developer's RETURNING field list; it is a stack of internal limits the search engine applies at each stage of query processing, most of which fail silently rather than raising an error.


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

The search engine limits records at each stage

Official docs state plainly: "The search engine limits the number of records analyzed at each stage of the search process." A record can be a genuine match on the search term and still be excluded because it fell outside one of those internal stage windows — there is no error, warning, or partial-result flag.

The first stage is term matching: "The search engine looks for matches to the search term across a maximum of 2,000 records (this limit starts with API version 28.0)." So even before any RETURNING shaping, the engine only considers up to 2,000 records for the term. If the record you want is the 2,001st the engine would have scanned, no amount of RETURNING tuning brings it back.

The returned-record caps are not 2,000 by default

After matching, the number of records actually returned is capped, and the cap depends on object count:

View All Data vs. per-user permission filtering

"Admins (users with the View All Data permission) see the full set of results returned. For all other users, SOSL applies user permission filters." This filtering happens on top of the caps above, so a standard user can receive a shorter result list than an admin running the identical search — records the engine matched are removed because the user cannot see them. When debugging a "missing record," always reproduce as the affected user, not as an admin.

Dynamic SearchQuery string-length thresholds

When you build the search string dynamically and pass it to Search.query, the string itself has thresholds that change behavior silently:


Common Patterns

Raise the single-object cap from 250 to 2,000

When to use: a single-object SOSL search needs more than 250 results (reporting, bulk reconciliation, "find everything named X").

How it works: add a WHERE filter and/or an ORDER BY inside the object's parentheses in the RETURNING clause. Per the docs, either one lifts the ceiling to 2,000 for that object.

// 250-record cap (single object, no WHERE/ORDER BY inside RETURNING)
List<List<SObject>> capped = [FIND 'Acme*' IN NAME FIELDS RETURNING Account(Id, Name)];

// Up to 2,000 — a WHERE or ORDER BY inside the parentheses lifts the cap
List<List<SObject>> raised = [
    FIND 'Acme*' IN NAME FIELDS
    RETURNING Account(Id, Name WHERE Industry = 'Technology' ORDER BY Name)
];

Why not the alternative: widening the FIND term or removing filters does not raise the cap; only the WHERE/ORDER BY inside RETURNING does. Assuming 2,000 is the default and never adding a clause is the most common cause of a silently truncated single-object search.

Narrow to one object to protect a specific record (the "Joe" pattern)

When to use: a user reports that one particular record never appears in a multi-object search, even though it matches.

How it works: the docs' own remedy — "If Joe limits his search to just one object, the limit applies to only that object, increasing the chance that the record he wants is returned." Scoping the search to the single object that holds the record removes the min(2000/n, 250) division and gives that object the full single-object budget.

Why not the alternative: adding more objects to "cast a wider net" does the opposite — each additional object shrinks every object's slice once n passes 8, making the target record less likely to survive.

When to use: the SearchQuery string is assembled from user input or a variable-length list of terms and handed to Search.query.

How it works: measure String.length() before the call and reject or truncate above a safe bound well under 4,000 characters, so logical operators are never stripped and you never cross the 10,000-character zero-result cliff.

String searchQuery = buildSoslFromInput(userTerms);
if (searchQuery.length() > 4000) {
    throw new SearchInputException(
        'Search string exceeds 4,000 characters; logical operators would be removed.'
    );
}
List<List<SObject>> results = Search.query(searchQuery);

Why not the alternative: relying on Salesforce to error out is unsafe — over 4,000 chars it silently drops operators, and over 10,000 it silently returns nothing. Neither throws.


Decision Guidance

SituationRecommended ApproachReason
Single-object search returns exactly 250 rowsAdd a WHERE or ORDER BY inside the RETURNING object's parenthesesEither clause raises the single-object cap to 2,000
A specific record is missing from a multi-object searchScope the search to just that objectRemoves the min(2000/n, 250) split; gives the object its full budget
Search targets 9+ objects and each returns few rowsReduce object count, or split into per-object searchesOnce n > 8, 2000/n drops below 250 and each object shrinks
Admin sees the record, standard user does notReproduce as the affected user; check sharing/FLSNon-admins have per-user permission filters applied to results
Dynamic Search.query returns zero rows unexpectedlyCheck SearchQuery length against 10,000 charsOver 10,000 characters, no result rows are returned
AND/NOT in a dynamic search behaves like a broad matchCheck SearchQuery length against 4,000 charsOver 4,000 characters, logical operators are removed
Need more than 2,000 total results from one statementRedesign — paginate, filter, or use SOQL/reporting2,000 total is the statement ceiling (incl. child objects) unless custom limits are set

  1. Reproduce as the affected user. Run the exact SOSL as the reporting user, not as an admin — View All Data hides the permission-filter shrink that a standard user experiences.
  2. Count the objects and read the RETURNING clause. Determine whether the search is single-object (250 default) or multi-object (min(2000/n, 250) per object), and whether any object's parentheses already contain a WHERE/ORDER BY/LIMIT.
  3. Map the symptom to a stage. Zero rows from a dynamic query → check SearchQuery length (10,000 / 4,000 thresholds). Exactly 250 → single-object cap. Shrinking per-object counts → object-count division. Missing only for some users → permission filtering. Missing beyond ~2,000 scanned → the term-matching scan window.
  4. Apply the matching remedy. Add WHERE/ORDER BY inside RETURNING to lift a single-object cap; narrow to fewer objects to protect a specific record; bound the SearchQuery string length; or set explicit custom per-object limits.
  5. Confirm you are not asking SOSL for a job SOQL should do. If the requirement is exhaustive, ordered, relational retrieval rather than best-match search, route to SOQL/reporting instead of fighting the caps.
  6. Run the checker and re-verify counts. Run scripts/check_sosl_search_result_limits.py over the Apex, then re-run the search as the affected user and confirm the target record now returns.

Review Checklist

Run through these before marking work in this area complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. 250, not 2,000, is the single-object default — a single-object SOSL with no WHERE/ORDER BY inside RETURNING caps at 250 records, so a search that "works" in dev against small data silently truncates in production.
  2. Adding objects shrinks every object's slice — because each object returns min(2000/n, 250), a search over 10 objects returns at most 200 per object, not 250; "search more objects to find it" backfires.
  3. Admins and standard users get different results — permission filtering is applied to search output for everyone without View All Data, so a bug reproduced as an admin can look non-existent while a standard user still cannot see the record.
  4. Dynamic SearchQuery length changes behavior with no error — over 4,000 characters logical operators are dropped (an AND search widens), and over 10,000 characters zero rows return; both fail silently.

Output Artifacts

ArtifactDescription
Stage-limit diagnosisWhich internal limit (2,000 scan, 250 single-object, min(2000/n, 250) split, permission filter, or SearchQuery length) is dropping the record
Remediated SOSLThe RETURNING clause with a WHERE/ORDER BY/custom limit, a narrowed object list, or a length-bounded dynamic string
scripts/check_sosl_search_result_limits.py outputFindings for single-object 250-caps, high object counts, and unguarded dynamic Search.query length risk
templates/sosl-search-result-limits-template.mdA worksheet mapping each observed symptom to the stage limit and its remedy

Package files