Search ignores unknown managed properties — silently
A misspelled or non-existent managed property does not raise an error: selectproperties hands it back as a cell with ValueType: Null, and in querytext it matches nothing and returns TotalRows: 0. HTTP 200 proves nothing, and neither does the cell being present. The one probe that does tell the truth is sortlist, which is resolved against the schema and answers differently for "no such property" than for "exists but not sortable".
Překlep nebo neexistující managed property nevyvolá chybu: selectproperties ji vrátí jako buňku s ValueType: Null a v querytext nic nenajde a vrátí TotalRows: 0. HTTP 200 nedokazuje nic – a přítomnost buňky taky ne. Jediná sonda, která říká pravdu, je sortlist: řeší se proti schématu a na „taková property neexistuje“ odpovídá jinak než na „existuje, ale nejde podle ní řadit“.
Symptom
You add a site/list column (say ContosoClassification, a Choice field), wait for the crawl, and check whether Search can see it:
/_api/search/query?querytext='IsDocument:1'&selectproperties='ContosoClassificationOWSCHCS'&rowlimit=1
→ HTTP 200, TotalRows: 93 ✅ "great, the property exists"
It doesn't. The exact same query with a name you invented on the spot returns the same thing:
/_api/search/query?querytext='IsDocument:1'&selectproperties='ContosoDoesNotExistXyz123'&rowlimit=1
→ HTTP 200, TotalRows: 93 ❌ the fake property is just as "successful"
And filtering behaves the same way — no error, just nothing:
querytext='ContosoClassificationOWSCHCS:Internal' → TotalRows: 0
querytext='ContosoDoesNotExistXyz123:whatever' → TotalRows: 0
querytext='ContentTypeOWSCHCS:Document' → TotalRows: 0 ← real data, still nothing
querytext='ContentType:Document' → TotalRows: 147 ← built-in property works
The last two lines are the giveaway: an auto-generated managed property (…OWSCHCS, ows_*) is not usable in KQL even when the underlying data definitely exists.
Cause
Two separate behaviours combine into a very convincing lie:
selectpropertiesechoes back whatever you asked for. The name you invented is not dropped from the projection — it comes back as a perfectly ordinary cell whoseValueTypeisNull. Measured on a live tenant:selectproperties='Path,RefinableString00,ZzFakeProp123,ViewableByExternalUsers' Path ValueType=Edm.String Value='https://…/Report.docx' RefinableString00 ValueType=Null Value='' ← real, never mapped ZzFakeProp123 ValueType=Null Value='' ← does not exist ViewableByExternalUsers ValueType=Edm.Boolean Value='false'Note rows 2 and 3: a real but unmapped property and a fabricated one are byte-for-byte identical in the response. Testing for the key's presence — or even for its
ValueType— cannot separate them.Auto-created managed properties are not queryable. When a new column is crawled, SharePoint creates a crawled property and an automatic managed property (
ows_<Name>, or a typed variant like<Name>OWSCHCSfor Choice). These are, by default, retrievable at best — not queryable, not refinable, not sortable. A KQL predicate against them therefore matches nothing rather than failing.
So both the "does it exist" check and the "does it have data" check return the same friendly HTTP 200 no matter what — and a feature built on top ships broken, reporting zeroes that look like a clean tenant.
Fix
Ask sortlist, not selectproperties. Sorting is resolved against the search schema, so the service has to admit whether the name exists — and it distinguishes three cases with three different answers:
sortlist='ViewableByExternalUsers:descending' → HTTP 200 exists, sortable
sortlist='ContentTypeId:descending' → "An unknown error occurred." exists, NOT sortable
sortlist='ZzFakeProp123:descending' → "We didn't understand your does not exist
search terms. Make sure
they're using proper syntax."
All three verified on a live tenant. The wording is the signal: "didn't understand your search terms" means the schema has never heard of the name, while any other error means it resolved and sorting was refused for some other reason.
# Exists? One request, and the answer is unambiguous.
try {
Invoke-PnPSPRestMethod -Method Get -Url "/_api/search/query?querytext='IsDocument:1'&sortlist='$prop`:descending'&rowlimit=1"
'exists (sortable)'
}
catch {
if ($_.Exception.Message -like "*didn't understand your search terms*") { 'DOES NOT EXIST' }
else { 'exists, but not sortable' }
}
Still run a control name through the same probe — if your fabricated name is not rejected, the method does not hold on that tenant and no verdict should be reported. Ready-made: Test-SearchManagedProperty.ps1.
The older control-probe comparison still works for the narrower question "is it queryable?". Never conclude "the property works" from a successful call alone. Run the same query twice — once with your property, once with a name you invented — and compare:
const probe = async (prop: string): Promise<number> => {
const r = await fetch(
`${web}/_api/search/query?querytext='${prop}:*'&rowlimit=1`,
{ headers: { Accept: 'application/json;odata=nometadata', 'odata-version': '3.0' } }
);
const d = await r.json();
return d.PrimaryQueryResult?.RelevantResults?.TotalRows ?? -1;
};
const mine = await probe('ContosoClassificationOWSCHCS');
const fake = await probe('ContosoDefinitelyNotAProperty123');
// mine === fake → your property is NOT queryable (regardless of HTTP status)
Make it actually queryable. Map the crawled property to one of the pre-built refinable managed properties, which are queryable/refinable/sortable:
- SharePoint admin center → More features → Search → Manage Search Schema.
- Find
ows_<YourColumn>under Crawled Properties (it appears only after the first crawl that saw the column). - Edit one of the pre-built
RefinableStringNN managed properties → Add a Mapping → pick the crawled property. (Microsoft documents the pool asRefinableString00–RefinableString99; check what your tenant actually offers in the schema list rather than trusting a number.) - Trigger a reindex of the site/library (Site settings → Search and offline availability → Reindex site) and wait for the crawl.
- Query
RefinableString07:Internalinstead of the auto-generated name.
Notes
- This is a product decision you have to design around, not a bug to work around. Requiring every customer to hand-map a schema entry is a real deployment cost — if your feature must work out of the box, get the data from the list REST API per site/library instead, and accept that it doesn't scale to a whole tenant in one query.
- The mapping is tenant-wide and the pool of
Refinable*slots is finite (strings, plus dates, integers and decimals in their own smaller pools). Treat them as a shared resource and write down which slot you took; a second product silently reusing your slot produces mixed results. RefinableString00:anythingreturning 0 is not proof of anything either — an unmapped refinable exists but is empty. Thesortlistprobe tells these apart: it answers HTTP 200 for the unmapped refinable (it exists) and rejects the fabricated name.- Wrapper libraries can hide the distinction entirely. PnP's
Submit-PnPSearchQuerymaterialises every requested property as a key on each result row, so a "does the key come back?" test passes for every name including invented ones. Go to/_api/search/querydirectly when the answer matters. - The failure mode is worst for counting features ("how much content is classified?"), because zero is a plausible answer. Whenever a number could legitimately be zero, prove your query works on data you know exists before trusting the zero.