Recipe: real-world search patterns

Recipe: real-world search patterns

Recipe: real-world search patterns

💡

Prompt example

Find the folder's contents, look up one asset by id, list only the top-level folders, return just the display names, page through a large result set, and resolve a language_id from the Languages content definition.

search is the most-used call. These are common patterns, expressed against the search
component. All are read-only, Class A - to confine, scope on parentId.

The endpoint is POST /api/{admin|portal}/search, chosen by the session's apiType. Both SDKs
expose the full 16-parameter surface (free-text, filters, sort, paging, field-select, plus the
advanced modes below); there is no SDK ceiling - older 6-param docs were stale.

Key facts (see reference/return-shapes.md + enums.md)

  • The result is {items, totalItemCount, hasItems}. Empty results return the object with
    items: [] in both languages; guard only
    against None/null.
  • A result's name/parent live under item["identifiers"], not at the top level.
  • Filters are {fieldName, operator, values}; AND-combined by default. operator is
    Equals (the most common), Contains, Prefix, NotEquals, … (see enums.md).
  • offset is zero-based; page until len(items) < size or you cover totalItemCount.

Python

# component: search  (def search(sdk, query=None, filters=None, sort_fields=None,
#                                 size=None, offset=None, search_result_fields=None, *,
#                                 similar_asset_id=None, min_score=None, use_llm_search=None,
#                                 search_text_fields=None, filter_binder=None, ...))

def folder_contents(sdk, folder_id, size=100):
    """Direct children of a folder - the parentId-Equals pattern."""
    flt = [{"fieldName": "parentId", "operator": "Equals", "values": folder_id}]
    return (search(sdk, filters=flt, size=size) or {}).get("items", [])

def find_by_id(sdk, asset_id):
    """Fetch one record by exact id (the portal id-lookup pattern)."""
    flt = [{"fieldName": "uuidSearchField", "operator": "Equals", "values": asset_id}]
    items = (search(sdk, filters=flt) or {}).get("items", [])
    return items[0] if items else None

def folder_descendants(sdk, folder_id, size=100):
    """Everything *below* a folder, recursively (children, grandchildren, ...).

    parentId-Equals returns only direct children, so a full tree otherwise means
    walking it level by level (see iter_children below). The performant one-shot
    is uuidSearchField-Equals: it matches the folder and every asset whose ancestry
    includes it. We drop the folder itself so the result is strictly its contents."""
    flt = [{"fieldName": "uuidSearchField", "operator": "Equals", "values": folder_id}]
    items = (search(sdk, filters=flt, size=size) or {}).get("items", [])
    return [i for i in items if i.get("id") != folder_id]

def iter_children(sdk, folder_id):
    """Iterative fallback: walk the tree breadth-first via parentId-Equals.

    Acceptable for small trees, or when you must page a huge tree node-by-node;
    it issues one query per folder, so prefer folder_descendants() above. Each
    visited id is re-queried for its own children until the frontier drains."""
    stack, out = [folder_id], []
    while stack:
        flt = [{"fieldName": "parentId", "operator": "Equals", "values": stack.pop()}]
        for item in (search(sdk, filters=flt, size=100) or {}).get("items", []):
            out.append(item)
            stack.append(item.get("id"))
    return out

def top_level_folders(sdk, content_definition_id):
    """Only the roots of the tree, for one content type."""
    flt = [
        {"fieldName": "contentDefinitionId", "operator": "Equals", "values": content_definition_id},
        {"fieldName": "assetType", "operator": "Equals", "values": 1},
        {"fieldName": "topLevelFolder", "operator": "Equals", "values": "true"},
    ]
    return (search(sdk, filters=flt, size=100) or {}).get("items", [])

def names_only(sdk, folder_id):
    """Return just display names - select fields with dotted paths."""
    flt = [{"fieldName": "parentId", "operator": "Equals", "values": folder_id}]
    fields = [{"name": "identifiers.displayName"}, {"name": "identifiers.assetType"}]
    res = search(sdk, filters=flt, search_result_fields=fields, size=100) or {}
    return [i.get("identifiers", {}).get("displayName") for i in res.get("items", [])]

def iter_all(sdk, filters, page_size=100):
    """Yield every item across pages (offset is zero-based)."""
    offset = 0
    while True:
        page = search(sdk, filters=filters, size=page_size, offset=offset) or {}
        items = page.get("items", [])
        for item in items:
            yield item
        if len(items) < page_size:
            return
        offset += 1  # offset is in PAGES (page index), not records

def sorted_by_name(sdk, folder_id):
    """Folder contents sorted A→Z by display name (sortType, not sortOrder)."""
    flt = [{"fieldName": "parentId", "operator": "Equals", "values": folder_id}]
    srt = [{"fieldName": "displayName", "sortType": "Ascending"}]
    return (search(sdk, filters=flt, sort_fields=srt, size=100) or {}).get("items", [])

def find_language_id(sdk, languages_content_definition_id, display_name):
    """Resolve a language_id for update_asset_language via the Languages content def.

    There is no list-languages SDK verb; search the Languages content definition
    and read the matching record's id. languages_content_definition_id is the
    stable per-deployment content definition for languages (discover once, reuse).
    """
    flt = [{"fieldName": "contentDefinitionId", "operator": "Equals",
            "values": languages_content_definition_id}]
    items = (search(sdk, filters=flt, size=200) or {}).get("items", [])
    for item in items:
        if (item.get("identifiers") or {}).get("displayName") == display_name:
            return item.get("id")
    return None

JavaScript

// component: search(sdk, query, filters, sortFields, size, offset, searchResultFields, opts)
//   opts = { similarAssetId, minScore, useLlmSearch, searchTextFields, filterBinder, ... }

export async function folderContents(sdk, folderId, size = 100) {
    const flt = [{ fieldName: "parentId", operator: "Equals", values: folderId }];
    const res = await search(sdk, null, flt, null, size);
    return res ? res.items : []; // res is the object (items: []) when empty; null only on failure
}

export async function findById(sdk, assetId) {
    const flt = [{ fieldName: "uuidSearchField", operator: "Equals", values: assetId }];
    const res = await search(sdk, null, flt);
    return res && res.items.length ? res.items[0] : null;
}

export async function folderDescendants(sdk, folderId, size = 100) {
    // One-shot recursive load: the folder and everything beneath it. Preferred over
    // walking parentId level by level (see iterChildren). Drop the folder itself.
    const flt = [{ fieldName: "uuidSearchField", operator: "Equals", values: folderId }];
    const res = await search(sdk, null, flt, null, size);
    const items = res ? res.items : [];
    return items.filter((i) => i.id !== folderId);
}

export async function iterChildren(sdk, folderId) {
    // Iterative fallback: breadth-first over parentId. Acceptable for small trees;
    // one query per folder, so prefer folderDescendants() when you can.
    const stack = [folderId], out = [];
    while (stack.length) {
        const flt = [{ fieldName: "parentId", operator: "Equals", values: stack.pop() }];
        const res = await search(sdk, null, flt, null, 100);
        for (const item of res ? res.items : []) {
            out.push(item);
            stack.push(item.id);
        }
    }
    return out;
}

export async function topLevelFolders(sdk, contentDefinitionId) {
    const flt = [
        { fieldName: "contentDefinitionId", operator: "Equals", values: contentDefinitionId },
        { fieldName: "assetType", operator: "Equals", values: 1 },
        { fieldName: "topLevelFolder", operator: "Equals", values: "true" },
    ];
    const res = await search(sdk, null, flt, null, 100);
    return res ? res.items : [];
}

export async function namesOnly(sdk, folderId) {
    const flt = [{ fieldName: "parentId", operator: "Equals", values: folderId }];
    const fields = [{ name: "identifiers.displayName" }, { name: "identifiers.assetType" }];
    const res = await search(sdk, null, flt, null, 100, null, fields);
    return res ? res.items.map((i) => i.identifiers?.displayName) : [];
}

export async function findLanguageId(sdk, languagesContentDefinitionId, displayName) {
    // Resolve a languageId for updateAssetLanguage via the Languages content def.
    // No list-languages SDK verb; search the Languages content definition instead.
    const flt = [{ fieldName: "contentDefinitionId", operator: "Equals",
                   values: languagesContentDefinitionId }];
    const res = await search(sdk, null, flt, null, 200);
    const items = res ? res.items : [];
    const match = items.find((i) => i.identifiers?.displayName === displayName);
    return match ? match.id : null;
}

Advanced search

Operator and field-bucket tables are in reference/enums.md.

Python

def find_similar(sdk, asset_id, min_score=0.65, size=20):
    """Vector find-similar: items most like `asset_id`. Ignores query/offset;
    needs vector search enabled (else an empty result with a 'not enabled' message)."""
    return (search(sdk, similar_asset_id=asset_id, min_score=min_score, size=size)
            or {}).get("items", [])

def llm_search(sdk, question, size=20):
    """LLM/deep semantic search over a natural-language query (segment-level for video)."""
    return (search(sdk, query=question, use_llm_search=True, size=size)
            or {}).get("items", [])

def search_transcripts(sdk, phrase, size=50):
    """Scope free-text to subtitles/transcripts only (see SearchTextFields in enums.md)."""
    return (search(sdk, query=phrase, search_text_fields=["Transcripts"], size=size)
            or {}).get("items", [])

def created_in_range(sdk, folder_id, start_utc, end_utc):
    """Range operators on a date field (UTC YYYY-MM-DDTHH:MM:SS.SSSZ); AND-combined."""
    flt = [
        {"fieldName": "parentId", "operator": "Equals", "values": folder_id},
        {"fieldName": "createdDate", "operator": "GreaterThanEquals", "values": start_utc},
        {"fieldName": "createdDate", "operator": "LessThan", "values": end_utc},
    ]
    return (search(sdk, filters=flt, size=100) or {}).get("items", [])

def name_prefix_or_substring(sdk, folder_id, term):
    """Non-Equals string ops: Prefix (starts-with) + Contains (token match), OR-combined.
    Use filter_binder=1 so the two name predicates are ORed instead of ANDed."""
    flt = [
        {"fieldName": "displayName", "operator": "Prefix", "values": term},
        {"fieldName": "displayName", "operator": "Contains", "values": term},
    ]
    return (search(sdk, filters=flt, filter_binder=1, size=100) or {}).get("items", [])

def exact_name_or_missing(sdk, name):
    """Exact (case-sensitive) match via the .keyword sub-field; also keep records that
    have no displayName at all (includeNull)."""
    flt = [{"fieldName": "displayName.keyword", "operator": "Equals",
            "values": name, "includeNull": True}]
    return (search(sdk, filters=flt, size=100) or {}).get("items", [])

def distinct_videos_with_clips(sdk, query, size=50):
    """Collapse duplicates on masterId, return clip/segment rows, skip the (costly) total,
    and compute signed URLs only for the preview field."""
    return (search(sdk, query=query, size=size,
                   distinct_on_field_name="masterId",
                   include_video_clips=True,
                   full_url_field_names=["previewImageUrl"],
                   exclude_total_record_count=True) or {}).get("items", [])

JavaScript

// Advanced options ride on the trailing opts object; the core positional args are unchanged.

export async function findSimilar(sdk, assetId, minScore = 0.65, size = 20) {
    const res = await search(sdk, null, null, null, size, null, null,
                             { similarAssetId: assetId, minScore });
    return res ? res.items : [];
}

export async function llmSearch(sdk, question, size = 20) {
    const res = await search(sdk, question, null, null, size, null, null,
                             { useLlmSearch: true });
    return res ? res.items : [];
}

export async function searchTranscripts(sdk, phrase, size = 50) {
    const res = await search(sdk, phrase, null, null, size, null, null,
                             { searchTextFields: ["Transcripts"] });
    return res ? res.items : [];
}

export async function namePrefixOrSubstring(sdk, term) {
    // OR the two name predicates with filterBinder = 1 (0/unset = AND).
    const flt = [
        { fieldName: "displayName", operator: "Prefix", values: term },
        { fieldName: "displayName", operator: "Contains", values: term },
    ];
    const res = await search(sdk, null, flt, null, 100, null, null, { filterBinder: 1 });
    return res ? res.items : [];
}

export async function distinctVideosWithClips(sdk, query, size = 50) {
    const res = await search(sdk, query, null, null, size, null, null, {
        distinctOnFieldName: "masterId",
        includeVideoClips: true,
        fullUrlFieldNames: ["previewImageUrl"],
        excludeTotalRecordCount: true,
    });
    return res ? res.items : [];
}

Operator gotcha: range operators (LessThan/GreaterThan/…) and Prefix are not
valid against an array of values - the API returns 400. Use a single value per range
predicate (combine with multiple filters as above).

Notes

  • Direct sdk.search calls are fully positional. The search(sdk,...) wrappers shown
    above accept keyword args, but the underlying sdk.search method has 15 positional
    parameters with no keyword support. When writing standalone scripts that call the SDK
    directly, pass all arguments positionally and use None for unused slots:
  sdk.search(query, offset, size, filters, sort_fields, search_result_fields,
             similar_asset_id, min_score, exclude_total_record_count, filter_binder,
             full_url_field_names, distinct_on_field_name, include_video_clips,
             use_llm_search, include_internal_fields_in_results)
  • Equals on displayName is fuzzy, not exact. It matches any record whose display
    name contains the search term -- not an exact string equality. To find a folder by its
    precise name, search by parentId to get the candidate list, then filter client-side:
  # Find root-level "Content" folder exactly
  ROOT_ID = "00000000-0000-0000-0000-000000000000"
  flt = [{"fieldName": "parentId", "operator": "Equals", "values": ROOT_ID}]
  items = sdk.search(None, 0, 200, flt, None, None, None, None, None, None, None, None, None, None, None)
  content = next((i for i in (items or {}).get("items", [])
                  if (i.get("identifiers") or {}).get("displayName","").strip() == "Content"), None)
  • parentId Equals is the workhorse for "what's directly in this folder?"; uuidSearchField Equals is the workhorse for "give me this id and everything under it" - a leaf id matches
    just that record, a folder id matches its whole subtree.
  • Direct children vs. whole subtree: parentId Equals lists only immediate children. To
    load everything below a folder you can either walk the tree level by level (the iterative
    iter_children / iterChildren above - acceptable for small trees, but one query per folder)
    or, preferably, issue a single uuidSearchField Equals <folder-id> query that returns the
    folder and all of its descendants at once. The recursive one-shot is the performant choice;
    the iterative walk is the fallback when you must page a huge tree or the field is unavailable.
  • url Equals is a prefix match, not an exact match. Searching url Equals "Content/Photo/Events/" returns the folder and every asset inside it. To locate a specific folder, navigate via parentId Equals on the parent's id instead.
  • filter_binder controls AND vs OR. Default (null/omitted) is AND - all filters must match. filter_binder: 1 (Python: filter_binder=1) switches to OR - any filter match returns the record. The namePrefixOrSubstring / name_prefix_or_substring patterns above use OR deliberately; most other patterns should leave it at the default AND.
  • Pagination uses a keyset cursor. nextPageOffset / next_page_offset is a sort-key token returned by the server - pass it back verbatim. When calling the raw API or JS SDK directly, use nextPageOffset from the response rather than incrementing a counter manually.
  • Search index has a short consistency lag. Newly created/moved/deleted assets may not appear in (or disappear from) search results for several seconds to minutes. When you already hold an id (e.g. from a create response), verify existence with a direct get or url-filter lookup rather than a full paginated scan - direct lookups bypass the index lag.
  • moveAsset / move_asset is asynchronous. The call returns a success response immediately, but the move is queued and executed in the background. Searching for the asset at its new URL right after a successful move will fail - it still appears at the old URL until the operation completes. To confirm a move finished, poll url Equals <new-path> (or uuidSearchField Equals <id> and check the returned URL) until the asset appears at the destination.
  • Newly created/uploaded assets are searchable only after indexing - there is a short lag
    before they appear in results, so poll (or read directly via get / list_children when
    you already hold the id).
  • Use sortType (Ascending/Descending), not sortOrder.
  • Resolving a language_id: there is no list-languages SDK verb. Filter search on the
    Languages content definition (contentDefinitionId Equals <languages-content-def-id>) and
    read the matching item's id - this is the id update_asset_language expects. The
    Languages content definition id is stable per deployment; discover it once and reuse it.

Did this page help you?