Empty results
Search returns `found: 0` for queries that should match — diagnose missing documents, narrow filters, scoped-token filters, and `queryBy` mismatches.
Empty results almost always come from one of four causes. Run the checks in order.
Symptom
{
"hits": [],
"found": 0,
"page": 1,
"perPage": 10,
"facetCounts": []
}A 200 with found: 0 is not an error. It means "request was valid, no documents matched."
Decision tree
1. Is the index actually populated?
- Search → Indexes → look at "Documents" count
- If 0 → see [Ingest failures](/troubleshooting/ingest-failures)
2. Does the query field exist on the documents?
- queryBy lists fields to search
- If you queryBy a field that does not exist, no document can match
3. Are filters too narrow?
- Drop filterBy entirely and retry
- If results return now, the filter was the cause
4. Is a scoped token AND-combining a hidden filter?
- Scoped tokens always AND-combine their scopedFilter
- Decode the token to see what is being addedCheck 1: index has documents
curl -X GET https://app.aacsearch.com/api/v1/projects/<projectId>/indexes \
-H "Authorization: Bearer aa_admin_..."Look for the documentCount field on the matching index. If 0, ingestion has not happened or has failed.
You can also check from the dashboard: Search → Indexes → "Documents" column.
→ If 0, jump to Ingest failures.
Check 2: queryBy matches indexed fields
queryBy is a comma-separated list of the fields the search engine scans. A field must exist in the index schema and be of a string-like type.
// ❌ This never matches anything if the field is `name`, not `title`
client.search({ q: "shoes", queryBy: "title" });
// ✅ Match the actual field
client.search({ q: "shoes", queryBy: "name,description" });Inspect the schema:
const indexes = await admin.listIndexes();
const products = indexes.find((i) => i.slug === "products");
console.log(products.fields);
// → look for which fields have type "string" or "string[]"Check 3: filters narrow it to nothing
Strip filterBy and re-run:
// Original — returns 0
await client.search({
q: "shoes",
queryBy: "name",
filterBy: "in_stock:=true && price:<50 && brand:=Nike",
});
// Remove filters
await client.search({ q: "shoes", queryBy: "name" });
// → 247 results
// Add filters back one at a time to find the offenderA common cause is comparing a string facet with a numeric operator (:<, :>) — the engine returns 0 because the comparison cannot be evaluated.
Check 4: scoped-token AND-combined filter
If you are using a scoped token, the token's scopedFilter is silently AND-ed with whatever the caller passes. From the request side it is invisible.
Decode the token to see what is being added:
const [, payloadB64] = scopedToken.replace("ss_scoped_", "").split(".");
const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
console.log(payload.scopedFilter);
// → "organization_id:=org_abc && availability:=in_stock"If the token's filter excludes the documents you expect, mint a new token with the right filter (server-side; see Scoped search tokens).
Scoped tokens can only narrow access. The AND-combination is enforced by combineFilters() in packages/api/modules/search/lib/scoped-token.ts — Hard Invariant 4 in the codebase. There is no way to widen a scoped token from the client.
Other causes
Typo tolerance is set to 0
By default, AACsearch allows minor typos. If numTypos: 0 is set on the index or per-request, "shose" will not match "shoes".
client.search({ q: "shose", queryBy: "name", numTypos: 2 });Stop words filtered the entire query
Common words ("the", "and") are dropped before search. A query of only stop words returns nothing. Add a more specific term.
Synonyms not loaded
If you expect "sneaker" to match "shoe", verify the synonym set is published:
const synonyms = await admin.listSynonyms(indexId);
console.log(synonyms);→ See Search API → multi-search and querying for the full synonym configuration.
Recently ingested but not yet visible
Documents go through SearchIngestBuffer before they reach the live index. Typical lag is < 30 seconds; cold-start can be longer. If you just pushed, wait and retry.
# Check the buffer queue depth (admin API)
curl -X GET https://app.aacsearch.com/api/v1/projects/<projectId>/usage \
-H "Authorization: Bearer aa_admin_..."Fix matrix
| Diagnosis | Fix |
|---|---|
| Index empty | Push documents — see Ingest failures |
queryBy field does not exist | Use a real field; check schema |
| Filter too narrow | Loosen filterBy; remove and add back one term at a time |
| Scoped token excludes target docs | Mint a new scoped token server-side with the right filter |
| Typo or unusual spelling | Set numTypos to 1 or 2 |
| Recent ingest not visible | Wait 30 seconds and retry; check buffer queue depth |
Diagnostics packet
| Field | Notes |
|---|---|
| Organization ID | required |
| Index slug | required |
| Full request body | including q, queryBy, filterBy, sortBy |
| Expected match | one document ID or external_id you expected to see |
| Document count on index | from listIndexes() or dashboard |
| Scoped token in use | yes/no — if yes, decoded payload (without signature) |
Related
Rate limits & quota
429 responses from AACsearch — diagnose per-key rate limits, monthly quota exhaustion, and the headers that tell you when to retry.
Slow search
Search latency above expected — diagnose cold starts, oversized `perPage`, complex `filterBy`, missing facets, and upstream search engine pressure.