SharePointVault ~/scripts/search ⌕ Search 404 pages, error messages, scripts…Ctrl K ★ Star
M↓ README.mdPS Test-SearchManagedProperty.ps1
sharepoint-vault›scripts›search›Test-SearchManagedProperty.ps1historyedit on GitHub
● SCRIPTsearch

Test-SearchManagedProperty.ps1

Tests whether a search managed property really exists, holds data and can be filtered on - before you build anything on top of it.

⬇ downloadview on GitHub ↗256 lines · 10.2 KB
<#
.SYNOPSIS
    Tests whether a search managed property really exists, holds data and can
    be filtered on - before you build anything on top of it.

.DESCRIPTION
    SharePoint Search does not fail on a managed property that does not exist.
    It answers HTTP 200 with a normal result set, and a KQL filter on a
    nonexistent property quietly matches nothing. Both failure modes look
    exactly like "there is no data", which is how a feature ships, works on
    the developer tenant, and returns zero rows at every customer.

    The obvious test does not work. Asking for a made-up property in
    selectproperties returns it as a cell like any other:

        selectproperties='Path,ZzFakeProp123'
        -> Cells: Path (Edm.String), ZzFakeProp123 (Null)

    And ValueType does not separate the cases either - a real but empty
    property (RefinableString00 on a tenant that never mapped it) reports
    Null just like the fake one. Presence of the key proves nothing.

    What DOES separate them is sorting. Sorting is resolved against the
    search schema, so the service answers differently for each case, and the
    three answers are distinguishable:

        sortlist='<real, sortable>'      -> HTTP 200
        sortlist='<real, not sortable>'  -> "An unknown error occurred."
        sortlist='<does not exist>'      -> "We didn't understand your search
                                             terms. Make sure they're using
                                             proper syntax."

    This script combines both probes and refuses to answer when its own
    control sample misbehaves.

    READ-ONLY: this script changes no search schema and no content.

    Practical note: auto-created properties for Choice columns (the
    *OWSCHCS family) usually do not exist as queryable managed properties at
    all. Making one queryable means mapping the crawled property to a
    RefinableString slot - a tenant admin action followed by a reindex. Do
    not build a customer-facing feature on a property this script reports as
    missing.

.PARAMETER SiteUrl
    Site to run the queries against.

.PARAMETER ClientId
    Client ID of your own Entra ID app registration used by PnP.PowerShell.
    https://pnp.github.io/powershell/articles/registerapplication.html

.PARAMETER Name
    One or more managed property names to test.

.PARAMETER SampleValue
    A value you expect the property to hold somewhere. When given, the script
    also runs `Name:SampleValue` to prove the property is filterable.

.PARAMETER BaseQuery
    Query used to fetch sample rows. Default: IsDocument:1

.EXAMPLE
    .\Test-SearchManagedProperty.ps1 -SiteUrl https://contoso.sharepoint.com/sites/projects -ClientId 00000000-0000-0000-0000-000000000000 -Name ViewableByExternalUsers,RefinableString00,ContentTypeId,ZzTotallyMadeUp42

    Base query: IsDocument:1
    Control OK: invented property names are rejected, so a "missing" verdict
    below is meaningful.
    Sampling 25 row(s) per property.

    Property                Exists Sortable RowsWithData Filterable Verdict
    --------                ------ -------- ------------ ---------- -------
    ViewableByExternalUsers   True     True           25       True Exists and carries data
    RefinableString00         True     True            0      False Exists in the schema but empty here
    ContentTypeId             True    False           25      False Exists and carries data
    ZzTotallyMadeUp42        False    False            0      False DOES NOT EXIST

    Four different real answers: a fabricated name, a real-but-unmapped
    RefinableString00, a real-but-unsortable ContentTypeId, and one that works.
    selectproperties alone reports the first two identically.

.EXAMPLE
    .\Test-SearchManagedProperty.ps1 -SiteUrl https://contoso.sharepoint.com/sites/projects -ClientId 00000000-0000-0000-0000-000000000000 -Name ViewableByExternalUsers -SampleValue true

.NOTES
    Requires : PnP.PowerShell 2.x or newer (Install-Module PnP.PowerShell)
    Auth     : Interactive (browser) sign-in. No admin rights needed - this
               is a read-only query as yourself.
    Related  : gotchas/search/unknown-managed-properties-fail-silently.md
    Samples  : scripts/sample-outputs.md - what this prints, from a real run
    Source   : https://github.com/EasyPortal365/sharepoint-vault
#>
#Requires -Modules PnP.PowerShell

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string]$SiteUrl,

    [Parameter(Mandatory = $true)]
    [string]$ClientId,

    [Parameter(Mandatory = $true)]
    [string[]]$Name,

    [string]$SampleValue,

    [string]$BaseQuery = 'IsDocument:1'
)

$ErrorActionPreference = 'Stop'

# Names nothing could possibly map to. If these behave like real properties,
# the whole method is broken and the script says so instead of guessing.
$controlNames = @('VaultControlPropertyCannotExist99', 'VaultControlPropertyCannotExist98')

# The service's way of saying "no such managed property".
$unknownPropertyMarker = "didn't understand your search terms"

function Invoke-SearchQuery {
    param([string]$QueryString)
    return Invoke-PnPSPRestMethod -Method Get -Url $QueryString
}

function Get-CellReport {
    <# Retrievability: does the property carry a typed value on any sampled row? #>
    param([string]$Property)

    $url = "/_api/search/query?querytext='$BaseQuery'&selectproperties='$Property'&rowlimit=25"
    $res = Invoke-SearchQuery -QueryString $url
    $rows = @($res.PrimaryQueryResult.RelevantResults.Table.Rows)

    $typed = 0
    $withData = 0
    foreach ($row in $rows) {
        $cell = $row.Cells | Where-Object { $_.Key -eq $Property }
        if (-not $cell) { continue }
        if ($cell.ValueType -and $cell.ValueType -ne 'Null') { $typed++ }
        if (-not [string]::IsNullOrEmpty([string]$cell.Value)) { $withData++ }
    }

    return @{ Rows = $rows.Count; Typed = $typed; WithData = $withData }
}

function Get-SortProbe {
    <# Existence: sorting is resolved against the schema, so it tells the truth. #>
    param([string]$Property)

    $url = "/_api/search/query?querytext='$BaseQuery'&sortlist='$($Property):descending'&rowlimit=1"
    try {
        [void](Invoke-SearchQuery -QueryString $url)
        return @{ Exists = $true;  Sortable = $true;  Detail = 'sorts fine' }
    }
    catch {
        $msg = $_.Exception.Message
        if ($msg -like "*$unknownPropertyMarker*") {
            return @{ Exists = $false; Sortable = $false; Detail = 'service does not recognise the name' }
        }
        # Any other error means the name resolved but sorting was refused.
        return @{ Exists = $true; Sortable = $false; Detail = 'exists but is not sortable' }
    }
}

Write-Host "Connecting to $SiteUrl ..." -ForegroundColor Cyan
Connect-PnPOnline -Url $SiteUrl -Interactive -ClientId $ClientId

Write-Host ("Base query: {0}" -f $BaseQuery) -ForegroundColor DarkGray

# --- Control samples first ----------------------------------------------
$controlOk = $true
foreach ($c in $controlNames) {
    $probe = Get-SortProbe -Property $c
    if ($probe.Exists) {
        Write-Warning "Control property '$c' was NOT rejected by the service. The detection method does not hold on this tenant."
        $controlOk = $false
    }
}

if ($controlOk) {
    Write-Host 'Control OK: invented property names are rejected, so a "missing" verdict below is meaningful.' -ForegroundColor DarkGray
}
else {
    Write-Host 'Control FAILED - results below are reported but must not be trusted.' -ForegroundColor Red
}

$sample = Get-CellReport -Property 'Path'
if ($sample.Rows -eq 0) {
    Write-Warning 'The base query returned no results at all. Nothing can be tested against an empty result set - widen -BaseQuery.'
    try { Disconnect-PnPOnline } catch { }
    return
}
Write-Host ("Sampling {0} row(s) per property." -f $sample.Rows) -ForegroundColor DarkGray
Write-Host ''

# --- The actual test ------------------------------------------------------
$results = New-Object System.Collections.Generic.List[object]

foreach ($prop in $Name) {
    Write-Host ("Testing {0} ..." -f $prop) -ForegroundColor Cyan

    $cells = Get-CellReport -Property $prop
    $sort  = Get-SortProbe  -Property $prop

    # A typed cell proves existence on its own, whatever sorting says.
    $exists = $sort.Exists -or ($cells.Typed -gt 0)

    $filterable = ''
    $filterHits = ''
    if ($SampleValue) {
        try {
            # Parenthesised so the filter cannot be escaped by an OR in the base query.
            $url = "/_api/search/query?querytext='($BaseQuery) AND $($prop):$SampleValue'&rowlimit=1"
            $res = Invoke-SearchQuery -QueryString $url
            $filterHits = $res.PrimaryQueryResult.RelevantResults.TotalRows
            $filterable = ($filterHits -gt 0)
        }
        catch {
            $filterable = 'query failed'
            $filterHits = $_.Exception.Message
        }
    }

    $verdict =
        if (-not $exists)            { 'DOES NOT EXIST - anything built on it returns empty forever' }
        elseif ($cells.WithData -gt 0) { 'Exists and carries data' }
        elseif ($cells.Typed -gt 0)  { 'Exists, typed, but empty on every sampled row' }
        else                         { 'Exists in the schema but empty here - likely never mapped or not crawled yet' }

    $results.Add([pscustomobject]@{
        Property     = $prop
        Exists       = $exists
        Sortable     = $sort.Sortable
        RowsSampled  = $cells.Rows
        RowsWithData = $cells.WithData
        Filterable   = $filterable
        FilterHits   = $filterHits
        Verdict      = $verdict
    })
}

Write-Host ''
$results | Format-Table -AutoSize -Wrap

Write-Host ''
Write-Host 'Reading the result:' -ForegroundColor Cyan
Write-Host '  Exists=False           -> the name is unknown to search. selectproperties still returns it as a Null cell,'
Write-Host '                            so "no data" and "no such property" are indistinguishable without this test.'
Write-Host '  Exists=True, no data   -> real property, nothing indexed into it here (a RefinableString nobody mapped).'
Write-Host '  Sortable=False         -> real property that cannot be used in sortlist; filtering may still work.'

if (-not $controlOk) {
    Write-Host ''
    Write-Warning 'Control sample failed - treat every row above as unverified.'
}

try { Disconnect-PnPOnline } catch { }
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 updated: 2026-07-29historyedit on GitHubview source