Routed Warehouse Search
One routed door over tenant warehouse data: each ask lands on typed filters, an NL-to-SQL pass, or vector similarity. Use when you hold a plain question OR typed predicates and want the cheapest lane picked automatically (force one via mode). Returns the lane, rows, any compiled SQL, and retry_guidance naming the exact params to re-send when part of a multi-part ask went unanswered. Act on retry_guidance instead of giving up. Lanes read different stores at different grains — read corpus.
Request
- Base URL: https://docs.amdahl.ai
- URL: https://docs.amdahl.ai/api/platform/v1/search/query
- Auth: one of:
- API key in header X-API-Key
- HTTP bearer
Request body
Free-text ask. Meaning-shaped wording ("what do customers say about…") routes to the semantic lane; other questions route to the NL-to-SQL fuzzy lane.
How many independent sub-questions a multi-part ask may fan out into. Default 3; up to 5 synchronously and 12 with async:true. RAISE THIS when a reply comes back with retry_guidance.action = "raise_max_subqueries" — it means parts of your question were cut by this cap, and retry_guidance.suggested_params carries the value to use.
Run as a background job instead of blocking. Returns {job:{job_id,status}} straight away; collect it by calling this verb again with that job_id and no query. USE THIS when a reply comes back with retry_guidance.action = "run_async" — the synchronous path is bounded at ~15s and your ask needs longer, and the async path also allows a higher max_subqueries. Fuzzy lane only; others name it in corpus.ignored_params.
Collect a job started with async:true. Send this ALONE (no query). While status is "queued"/"running", poll again; on "complete" the full envelope is on detail.
Lane override. Default auto: filters-with-no-query → filter; meaning-shaped query → semantic; else fuzzy. Lanes read DIFFERENT stores at different grains — read corpus on the response to see which one answered.
Warehouse surface for the filter lane. Default interactions. Ignored by the fuzzy and semantic lanes, which report it back in corpus.ignored_params when you pass it.
Who counts as the voice you want. Default "all". Use "customer_voice" to keep only genuine customer / prospect speech and drop investor, advisor, media, partner, vendor and competitor chatter that merely shares an account record — while STILL keeping a real buyer on such an account (someone on a live opportunity, or a scored champion / economic buyer). This applies the platform's own canonical rule server-side; it is a disjunction you cannot express with filters, which are ANDed. interactions only. Applied on the filter lane, and on the semantic lane with hydrate (turned on for you unless you send hydrate:false). A request that would land on the fuzzy lane, which writes its own SQL, is rerouted to semantic with hydrate and says so in rerouted_from and notes. Refused on the lexical lane, which matches exact terms.
Semantic lane only. Semantic matches carry a truncated preview and no labels, so they cannot be quoted or attributed. Set true to bridge each match back to its full warehouse row — full content plus company, speaker title, pushback and buyer-signal columns. One row per match, containing the matched utterance, so a hit is citable. SPEAKER comes from the ATOMIC utterance that matched, not from the turn stamp: read speaker_attribution on each row. "utterance" = verified from the atomic source. "ambiguous" = two matched utterances share this row and disagree on speaker, so the speaker fields are NULL and nobody is named — check turn_speakers for who is in play. "turn" = the atomic source had no row, so the fields are the turn's FIRST member and are NOT verified. Separately, speaker_ambiguous says the row's TEXT spans more than one speaker (13.58% of turns on the tenant measured 2026-08-15) — a row can be cleanly attributed AND span two voices, so quoting the whole content as one person is still wrong there. Both fields ship on every turn-grain row including the clean ones; corpus.speaker_ambiguous_rows / corpus.speaker_unattributable_rows are the rates. speaker_ambiguous_source says where that verdict came from: "stamped" = read from the column the pipeline writes on the turn itself, "overlay" = reconstructed at read time because the pipeline recorded none for that turn. A missing record is NOT "one speaker" — permanently-excluded interactions never get one — so an unrecorded turn falls back rather than being reported unambiguous, and a "stamped" row also carries speaker_count. corpus.speaker_stamped_rows / corpus.speaker_fallback_rows are the split. Stakeholder columns (role_level, is_champion, is_economic_buyer, champion_score, inferred_persona) are derived from the turn stamp, so they are NULLED when the row is re-attributed to a different speaker rather than left describing someone else. Slower — two reads. Check corpus.hydrated_grain: "turn" is that row-per-match answer; "call" means the turn lookup failed and the rows are arbitrary turns from the right conversations (context, not the matches; also flagged corpus.degraded). Matches whose utterance belongs to no turn have no warehouse row and are counted in corpus.unresolved_matches; matches sharing one turn are corpus.collapsed_matches. Read those two directly rather than inferring a gap from a match_count/row_count delta — corpus.match_count is how many nearest neighbours the lane pulled and is BOUNDED BY limit, not a count of how many things genuinely matched. Caps at 200 distinct ids, reported as corpus.truncated.
Opt into gap detection on corpus.coverage. Every response ALREADY carries corpus.coverage with max_timestamp / min_timestamp and an age histogram, computed free from the rows returned. This flag adds gaps: windows where the corpus HELD rows and this result set had none — the "your evidence has a hole in it" signal, as opposed to the ordinary quiet periods a plain empty-bucket scan would flag. Costs one extra bucketed read, so it is off by default. Read coverage.basis first: "matched_set" means the numbers describe your whole match set, "returned_rows" means they describe a truncated slice, and "unavailable" means coverage could not be measured — never read that as zero.
Row / match cap; max 1000. A larger value is clamped to 1000 rather than refused, and the response reports it on limit_clamped and in notes. Default is lane-dependent: 100 on the filter and semantic lanes, 50 on the fuzzy lane (where an unforced plain-language question usually lands), so pass it explicitly when the row count matters.
Filter lane: group fields (requires at least one metric).
Response
OK
Changes
No recorded changes to this operation across all 1 revision of this API.