SharePointVault ~/gotchas/search ⌕ Search 404 pages, error messages, scripts…Ctrl K ★ Star
M↓ README.mdM↓ scope-search-to-a-list-of-sites-refinementfilters.md
sharepoint-vault›gotchas›search›scope-search-to-a-list-of-sites-refinementfilters.mdhistoryedit on GitHub
● GOTCHAsearch· 3 min read

Scope SharePoint Search to hundreds or thousands of sites with RefinementFilters — and or() with one value returns nothing

applies to: SharePoint Online Search REST (/_api/search/postquery)reviewed: 2026-11-03#search#kql#refiners#postquery#large-tenant#activity

Restricting a query to a list of sites with Path:"…" OR Path:"…" in a GET querytext hits the URL length limit after a few dozen sites. POST to /_api/search/postquery with RefinementFilters: ['SPSiteUrl:or("url1","url2",…)'] instead — it worked with 3,000 site URLs. For exactly one site use SPSiteUrl:equals("url"): or() with a single value silently returns 0 rows.

Omezení dotazu na seznam webů přes Path:"…" OR … v GET querytext narazí na délku URL už u desítek webů. Použij POST /_api/search/postquery s RefinementFilters: ['SPSiteUrl:or("url1","url2",…)'] – funguje i s 3 000 adresami. Pro jediný web SPSiteUrl:equals("url"): or() s jednou hodnotou tiše vrátí 0 výsledků.

Symptom

A governance tool should only evaluate the sites people actually work on (say 200 of 7,000). Every content query — anonymous links, files shared with guests, sensitive content — must be limited to that list. Building Path:"https://contoso.sharepoint.com/sites/a" OR … into querytext fails once the URL gets long, and splitting into many small queries multiplies the calls and breaks paging and totals.

After switching to refinement filters, a list that happens to contain one site returns no results at all, while the same site in a two-site list works.

Cause

RefinementFilters is applied server-side after the query, and in the POST body it has no practical length limit. SPSiteUrl is refinable, so or() over site URLs works as a scope. The FQL or() operator, however, needs at least two operands — with one it matches nothing instead of failing.

Fix

function siteRefinementFilter(urls) {
  const vals = urls.map(u => '"' + u.replace(/"/g, '') + '"');
  return vals.length === 1
    ? 'SPSiteUrl:equals(' + vals[0] + ')'
    : 'SPSiteUrl:or(' + vals.join(',') + ')';
}

const body = {
  request: {
    Querytext: 'ViewableByAnonymousUsers:1 AND IsDocument:1',
    StartRow: 0, RowLimit: 500, TrimDuplicates: false,
    SelectProperties: ['Title', 'Path', 'SPSiteURL'],
    RefinementFilters: [siteRefinementFilter(siteUrls)]
  }
};
await fetch(webUrl + '/_api/search/postquery', {
  method: 'POST',
  headers: {
    Accept: 'application/json;odata=nometadata',
    'Content-Type': 'application/json;odata=nometadata',
    'odata-version': '3.0',
    'X-RequestDigest': digest
  },
  body: JSON.stringify(body)
});

To report what lies outside the list, run the same query without the filter and subtract — one extra RowLimit: 1 call per query (TotalRows only).

Picking the active sites without calling each site

LastModifiedTime on a contentclass:STS_Site row is the root web's timestamp (often a settings change), not content activity — don't use it. Signals that come in bulk:

  • Views: ViewsRecent and ViewsLastMonths1..3 (monthly buckets) are retrievable on the site row of the inventory query (managed properties overview). Microsoft doesn't document the window of ViewsRecent (commonly described as the last 14 days); it overlaps the current month, so take the larger of the two, don't add them.
  • Changed documents per site: one query IsDocument:1 AND LastModifiedTime>=2026-08-05 with refiner SPSiteUrl(filter=5000/0/*) gives a count per site collection. The refiner can be truncated — a site missing from a truncated list is unmeasured, not zero.
  • Usage reports (Graph getSharePointSiteUsageDetail, getTeamsTeamActivityDetail, beta with $format=application/json works from the browser) add last-activity dates; with concealed names in the Microsoft 365 admin center nothing can be matched — report that as "not available", not as "inactive".

Notes

  • Not verified on a tenant with tens of thousands of sites: the maximum number of refiner entries returned, and case sensitivity of SPSiteUrl values for sites that are not in the search index exactly as you spell them (send URLs as the inventory returned them or test both).
Was this helpful?ask or add your experience on GitHub →
Saved you an hour? A star on GitHub helps other SharePoint people find the vault.★ Star on GitHub
last reviewed: 2026-11-03last updated: 2026-11-03historyedit on GitHubview source