Tuning Search Results¶
Tuning Result Volume with pageSize¶
The pageSize parameter controls how many matches each collection may return. It defaults to 10 and is capped at 100.
Use a larger pageSize when the default isn't enough; for example, when a license has many similarly-named items and the top 10 from each endpoint isn't covering the matches you're looking for. Use a smaller pageSize when the request is purely a "did anything match" check.
curl 'https://api.trackandtrace.tools/v2/search?licenseNumber=LIC-00001&query=Blue%20Dream&pageSize=50' \
-H 'Authorization: Bearer <TOKEN>'
pageSize applies uniformly to every queried endpoint. Out-of-range values (< 1 or > 100) return 400 Bad Request.
If you're not sure whether a higher pageSize would yield more results, look at endpoints in the response: any entry with truncated: true had more matches available than your pageSize returned, and totalAvailable reports Metrc's count.
Scores¶
A result's score is in [0, 1] and comes from the field that matched best:
- Tags and numbers score higher the closer the match is to the end, where a tag is unique. Leading zeros are ignored on numbers, so
2034486scores1.0against manifest0002034486. - Names score higher the closer the match is to the start, and higher again when it starts a word:
Dreamscores more inBlue Dreamthan inDaydream. - Secondary fields count for less. A package's label and item name count fully; a match on its production batch, strain, source tags, harvests, location or category counts progressively less, so an object's own identifier outranks a coincidental match elsewhere. The same applies to a transfer's facility.
matchedFields lists every field that matched, with its weighted score, so you can highlight the match or say why a result appeared. A row in which the query matches no searched field is never returned.
for match in body["data"]:
best = match["matchedFields"][0]
print(match["t3EndpointId"], f"matched on {best['field']}", match["score"])
Filtering Noise with minScore¶
The minScore query parameter drops results whose relevance score is below the threshold, server-side. Scores are in [0, 1], where 1.0 is an exact full-string match. Defaults to 0.0 (no filtering).
curl 'https://api.trackandtrace.tools/v2/search?licenseNumber=LIC-00001&query=Blue%20Dream&minScore=0.1' \
-H 'Authorization: Bearer <TOKEN>'
Useful when:
- A short or common query string produces a long tail of low-score matches that the UI just discards anyway.
- You're looking for "is there a strong match?" and want the server to filter out the dross before serializing it.
The filter is per result and applied before sorting. In SSE mode, a result event still fires when every match was filtered out; its results array is empty, which tells the client the endpoint completed.
Streaming Responses (SSE)¶
For interactive UIs that want to render results as soon as each collection answers (instead of waiting for the slowest one), /search can stream Server-Sent Events. There are two equivalent ways to opt in:
- Add
?stream=trueto the URL (easy to test fromcurl, easy to document). - Send
Accept: text/event-stream(whatEventSourcedoes natively).
The response is a text/event-stream body with these events, in order:
event: start: first. Every endpoint the search covers, aspendingorskipped, withdetectedQueryFormatsand the resolvedrequest.event: result: one per sent endpoint, in completion order (fastest first). The endpoint's entry fromendpoints(status, reason, totals) plus itsresults, each shaped like an entry ofdata, best first.event: done: last. Every endpoint's final state inendpoints.event: error: instead ofdone, when the search cannot finish. The problem details a JSON request would get, plusendpoints.code: METRC_SESSION_EXPIREDmeans authenticate again.
When nothing new has arrived for about 250 ms, the server writes a comment line (:). Clients ignore it. Closing the connection cancels the rest of the search, so drop a stale search as soon as the query changes.
Sample stream:
event: start
data: {"endpoints":[{"t3EndpointId":"METRC_ACTIVE_PACKAGES","status":"pending",...},{"t3EndpointId":"METRC_ACTIVE_ITEMS","status":"skipped","reason":"format",...}],"detectedQueryFormats":["HEX_STRING"],"request":{"query":"1A4FF0100000022000000123","licenseNumber":"LIC-00001","pageSize":10,"minScore":0.0,"t3EndpointIds":["METRC_ACTIVE_PACKAGES","METRC_ACTIVE_ITEMS"]}}
:
event: result
data: {"t3EndpointId":"METRC_ACTIVE_PACKAGES","status":"answered","reason":null,"resultCount":1,"truncated":false,"totalAvailable":1,"results":[{"t3EndpointId":"METRC_ACTIVE_PACKAGES","score":1.0,"isExactMatch":true,"matchedFields":[{"field":"label","score":1.0}],"matchedEntry":{"...":"..."}}],...}
event: done
data: {"endpoints":[{"t3EndpointId":"METRC_ACTIVE_PACKAGES","status":"answered",...},{"t3EndpointId":"METRC_ACTIVE_ITEMS","status":"skipped","reason":"format",...}]}
Python usage:
import json
import requests
with requests.get(
"https://api.trackandtrace.tools/v2/search",
params={"licenseNumber": "LIC-00001", "query": "Blue Dream", "stream": "true"},
headers={"Authorization": "Bearer <TOKEN>"},
stream=True,
) as response:
event = None
for line in response.iter_lines(decode_unicode=True):
if line.startswith("event: "):
event = line[len("event: "):]
elif line.startswith("data: ") and event == "result":
result = json.loads(line[len("data: "):])
print(result["t3EndpointId"], result["status"], len(result["results"]))
Browser usage with EventSource. EventSource cannot send an Authorization header, so pass a secret key in the URL, and only from code you control:
const params = new URLSearchParams({
licenseNumber: 'LIC-00001',
query: 'Blue Dream',
secretKey: SECRET_KEY,
});
const source = new EventSource(`https://api.trackandtrace.tools/v2/search?${params}`);
source.addEventListener('result', (e) => {
const { t3EndpointId, status, results } = JSON.parse(e.data);
// Render incrementally
});
source.addEventListener('done', () => source.close());
source.addEventListener('error', () => source.close());
curl usage:
curl -N 'https://api.trackandtrace.tools/v2/search?licenseNumber=LIC-00001&query=Blue%20Dream&stream=true' \
-H 'Authorization: Bearer <TOKEN>'
(-N disables curl's output buffering so events appear as they arrive.)
Note
The SSE stream emits results in completion order, not score order. Each event's own results array is sorted by score, but the global ranking that JSON mode produces requires the client to merge events as they arrive.
Limits and Behavior¶
- Per-endpoint cap of
pageSizematches. Each endpoint returns at mostpageSizeresults (default 10, max 100). Watchtruncatedinendpointsto know when raisingpageSizewould yield more. - 10 second deadline. Each endpoint has 10 seconds to answer, so one slow endpoint can't hold up the rest. An endpoint that misses it is
failedwith reasontimeout. In SSE mode itsresultevent still fires. - Latency varies by endpoint. Tag, item, location, and strain searches typically return in well under a second. Package, plant, and especially transfer searches can take several seconds. SSE mode delivers fast endpoints' results first.
- Rate limit. Search has its own limit per Metrc user, 300 requests a minute by default, separate from the limit your other requests share. Over it, the response is
429. - Narrow scope for speed. If you know the kind of object you're after, pass
t3EndpointIdsto drop everything else. A query againstt3EndpointIds=METRC_AVAILABLE_TAGS,METRC_USED_TAGSwill be much faster than the default set. - Per-license. Like every collection endpoint,
/searchis scoped to thelicenseNumberquery parameter. Searching across multiple licenses requires multiple requests. - Free. No T3+ subscription required.
Next Steps¶
- Back to the endpoint catalog and response shape: Search.
- Check what your account may query first with Permissions.