Search
Find moments, pages and boxes across every video, photo and document in your workspace, by meaning, not just keywords.
Semantic search
POST /api/v1/search runs a multimodal embedding search over frames, transcripts, and scene descriptions, fused into one ranked list.
curl https://api.niravi.io/api/v1/search \
-H "X-API-Key: $NIRAVI_API_KEY" -H "Content-Type: application/json" \
-d '{"query": "two people arguing in a kitchen", "limit": 10, "moments_per_video": 3}'
| Field | Type | Notes |
|---|---|---|
query | string | required — natural-language description |
limit | int | results to return (server default applies if omitted); counts moments |
moments_per_video | int | 1 to 10, default 1: up to this many moments per video, each a separate hit with its own timestamp. Moments of one video that sit close together merge into one. Ignored with video_id (a search inside one video already returns every matching moment) |
video_id | string | restrict to a single video |
search_options | object | advanced tuning (fusion weights, modalities) |
rerank | bool | apply a second-pass re-ranker |
engine | string | arrow (current) or classic (NiQu, deprecated). Omit for your workspace’s default: Arrow for every new workspace, and for any workspace that is empty or holds only Arrow assets. A library indexed by NiNo still defaults to classic, so pass arrow there to search what you upload |
workspace_ids | string[] | search several workspaces at once (federated) |
Each hit carries video_id, timestamp, frame_url (the JPEG of that moment) and two scores:
| Field | Meaning |
|---|---|
score | 0..1, the best hit of the search is 1.0. A whole-library Arrow search normalises against the whole candidate pool, so scores compare across the videos of one result list (not across searches) |
raw_score | the hit’s fused score before pool normalisation. The weakest candidate of the pool reads 0, so rank and compare with score |
With engine: arrow a hit also says what it landed on:
| Field | Meaning |
|---|---|
asset_kind | video, image or document |
page | documents: the 1-based page |
passage | documents: the passage of the page that best matches the query |
region | photos and pages: the best-matching box, {x, y, w, h, label, source} normalized top-left |
The response’s engine and recipe say which engine answered; engine_fallback explains when another one had to. See Images & documents.
Only finished assets. An asset that is still processing (or failed) never appears in results, even when part of it is already indexed.
Speed. The first search of a library takes about 4 seconds while its index loads; after that, searches answer in under a second. A finished upload is searchable within seconds of completing (3 to 7 s measured).
import requests # the Python SDK is not on PyPI yet: plain HTTP works
BASE = "https://api.niravi.io/api/v1"
H = {"X-API-Key": "nv_..."}
r = requests.post(f"{BASE}/search", headers=H,
json={"query": "two people arguing in a kitchen", "limit": 10, "moments_per_video": 3})
for hit in r.json()["data"]["results"]:
print(hit["video_id"], hit["timestamp"], hit["score"], hit["raw_score"])
const hits = await niravi.search("two people arguing in a kitchen", { limit: 10, momentsPerVideo: 3 });
const full = await niravi.searchResponse("two people arguing in a kitchen", { engine: "arrow" }); // + engine, engine_fallback
Magic search
Typed search narrows to a single dimension: POST /api/v1/search/{type} where type is character, location, music, or emotion. The body takes query, limit and video_id; a character search can instead take an example face, cluster_id with its source_video_id.
curl https://api.niravi.io/api/v1/search/emotion \
-H "X-API-Key: $NIRAVI_API_KEY" -H "Content-Type: application/json" \
-d '{"query": "tense and suspenseful"}'
On an Arrow workspace each type searches these signals:
| Type | Searches | Not available |
|---|---|---|
location | shot captions + scene labels | |
emotion | shot captions + sound events + mood music | facial expression, scene mood |
music | sound events + audio segments (genre, instrument, music segments) | song title and artist identification |
character | a query that names a workspace Person (part of the name, or the exact person id) returns that person’s faces; cluster_id + source_video_id returns the faces of that cluster; shot captions answer a description |
meta.search says which legs ran, how many hits each gave, and which facets are unavailable and why:
"meta": { "search": {
"type": "emotion", "engine": "arrow",
"legs": { "dense": { "hits": 1 }, "sound_events": { "hits": 0 }, "mood_music": { "hits": 0 } },
"unavailable": { "facial_expression": "facial expression is not indexed by Arrow", "scene_mood": "scene mood is not written by Arrow" }
} }
requests.post(f"{BASE}/search/character", headers=H, json={"query": "a man in a red jacket"})
requests.post(f"{BASE}/search/character", headers=H, json={"cluster_id": 1, "source_video_id": video_id})
requests.post(f"{BASE}/search/location", headers=H, json={"query": "a rainy city street at night"})
await niravi.magicSearch("music", "upbeat synth");
await niravi.magicSearch("emotion", "joyful");
Search a single video instead of the whole corpus by passing
video_id(semantic) or scoping a guide’s per-video endpoints — see Videos & processing.
The science behind this → Niravi’s retrieval engine (frame + transcript + scene fusion) is documented in the Engineering Notes at blog.niravi.io/blog — the pipeline generation by generation, with the head-to-head retrieval benchmarks vs the commercial leader.