Skip to content

index4.ai Retrieval ​

POST /search is index4.ai's main retrieval endpoint. It runs hybrid (lexical + semantic) search across the project namespace and returns a RetrievalContext — a prompt-ready XML + Markdown block, not a list of ranked hits.

This page covers the request shape, how the four tuning parameters interact, and when to use searchInDoc instead.

Request ​

http
POST /v1/search HTTP/1.1
Authorization: Bearer <project-api-key>
Content-Type: application/json

{
  "query": "How did operating margin change year over year?",
  "doc_filter": null,
  "exclude_doc_ids": [],
  "return_assets": false,
  "return_page_images": false
}

Only query is required. The tuning knobs (min_relevance, target_tokens, max_tokens) are omitted above on purpose — their defaults are sized for the common case, so set them only when you want to deviate.

FieldTypeDefaultPurpose
query (required)string—Natural-language query. Hybrid retrieval splits this into a lexical term set and a vector embedding internally.
min_relevancefloat 0..10.3Relevance floor: hits scoring below it are dropped. Higher = stricter (return less, drop borderline hits); lower = more permissive. 0.3 is the balanced default.
target_tokensinteger5000Soft token budget — the typical answer size. Hits are admitted best-first (sandwich-aware) up to this. This is the knob to size a response. Can never exceed max_tokens (clamped down if it would).
max_tokensinteger15000Hard ceiling. index4.ai only expands past target_tokens toward it for tight multi-document clusters. Set it below target_tokens and the soft target is clamped down to match — the hard ceiling always wins; it never inflates a response on its own.
doc_filterstringunsetOptional document id. When set, restricts retrieval to a single doc. (Use /searchInDoc if you want this required.)
doc_idsarray of stringunsetRestrict retrieval to this set of document ids.
exclude_doc_idsarray of stringunsetDoc ids to exclude from results (max 100). Useful in agent loops to avoid re-citing docs already shown.
return_assetsbooleanfalseWhen true, the cited documents' figures (and tables, where available) are returned in an assets field keyed by doc_id. Images are base64 data URIs; resolved per document.
return_page_imagesbooleanfalseDeprecated / not available — no-op today; page screenshots aren't stored, so nothing is returned regardless of this flag (a dedicated endpoint is planned). Included in the sample request above because the API accepts it, not because it does anything yet.

Metadata filters ​

Filter on the metadata you attached at ingest (see Ingestion → Attributes & metadata) — typed attributes with attribute_filters, or plaintext strings with unencrypted_strings_match. If you still send meta_tags_any / meta_tags_all or meta_int_*_gte / _lte, see Migrating from labels / meta_int for the replacements.

FieldTypePurpose
attribute_filtersarray of {attr, op, value}Filter on typed attributes. Clauses AND together; operators depend on the attribute's type — see below.
unencrypted_strings_matchobject (string→string)Match on the plaintext X-Unencrypted-Strings you set at ingest: a key→glob-pattern map. Each entry is a case-insensitive glob (* = any run, ? = one char; a pattern with no wildcard is an exact match). Multiple keys AND together.

All filters (attribute_filters, unencrypted_strings_match, doc_ids / exclude_doc_ids) combine via AND.

Filtering on typed attributes ​

attribute_filters is a list of {attr, op, value} clauses, combined with AND. To see which attribute names exist in the project and what type each has (and so which operators apply), call GET /attributes (MCP: index4ai_attributes) — see the API reference. A 503 from it means "retry", not "no attributes".

python
g.search(
    "q3 revenue",
    attribute_filters=[
        {"attr": "region",       "op": "eq",             "value": "emea"},
        {"attr": "published_at", "op": "gte",            "value": "2026-01-01T00:00:00Z"},
        {"attr": "case_notes",   "op": "has_all_tokens", "value": "chest pain"},
    ],
)

Which operators you can use depends on the attribute's type:

TypeOperators
stringeq ne in nin glob iglob
int, float, datetimeeq ne lt lte gt gte (ints also in / nin)
booleq ne
[]stringcontains contains_any eq in
[]int, []floatcontains contains_any any_lt any_gt
[]datetimecontains any_lt any_gt
[]boolcontains contains_any
encrypted_text, []encrypted_texthas_all_tokens has_any_tokens

An operator the type does not offer returns a 400 naming the ones it does.

Two behaviours worth knowing, because both are deliberate:

  • A filter on an attribute your project has never used matches nothing. It is not ignored — a filter that silently matched everything would be worse than an empty result.
  • encrypted_text matching is exact: the same tokenization runs on both sides, with no stemming and no stop-word removal, so "pains" does not find "pain". For Chinese, Japanese and Thai the text is split into character n-grams, so a whole word matches but an arbitrary mid-word fragment may not.

For wildcard strings, if you stored {"path": "legal/contracts/acme"} at ingest, then {"unencrypted_strings_match": {"path": "legal/contracts/*"}} keeps only documents whose path begins with legal/contracts/. Unlike encrypted_text attributes (HMAC'd, exact-token matching), unencrypted_strings values are stored unencrypted so they can be glob-matched — don't put secrets or PII in them.

Other controls ​

FieldTypePurpose
min_top_relevancefloat 0..1Floor for the top hit only. If the best hit scores below this, the whole result set is empty — a guard against returning anything when nothing is a strong match.
expand_tightnessfloat 0..1How aggressively the engine expands context around each hit.
retrieval_tierstringstandard (fusion only) or advanced (adds a reranker). See Retrieval tiers. REST only: the Python SDK and the MCP server don't expose it.
premiumbooleanLegacy flag — true equals retrieval_tier: "advanced". Prefer retrieval_tier.
formatstringprompt_ready (default) returns the XML+Markdown block; json returns structured ranked hits instead.

No top_k. Result count is bounded server-side by relevance + token budget. The recall stage isn't a tunable parameter; tune precision via min_relevance and answer size via target_tokens.

Response ​

json
{
  "context": "<doc id=\"...\">…</doc>",
  "result_count": 4,
  "tokens_estimated": 5820,
  "results_dropped": 2,
  "detected_lang": "english",
  "mode": "advanced",
  "search_units": 1
}

context is the field you actually use — index4.ai's contract is "ready to drop into a prompt", not "here is JSON for you to format". The siblings are metadata: result_count / results_dropped, tokens_estimated (rendered size), detected_lang, mode (the retrieval tier used), and search_units (billing). For the wrapper grammar (<doc>, inline [pN] / […] markers, sandwich order, citation attributes), see RetrievalContext format. To get structured per-hit JSON instead of the rendered block, set format: "json".

How the parameters interact ​

Think of retrieval as a two-stage pipeline:

text
                ┌──── filter ────┐  ┌──────── render ────────┐
query ─▶ hybrid search ─▶ score ≥ min_relevance ─▶ fill to target_tokens (sandwich, up to max_tokens) ─▶ context
  • min_relevance controls precision. Anything below the threshold is discarded outright, even if it would have fit the budget. Useful for "I'd rather return less than return junk" queries (e.g. legal Q&A).
  • target_tokens controls the answer size. After thresholding, hits are placed in sandwich order and admitted best-first up to this soft budget; if it overflows, the lowest-ranked hits are evicted first. Hits at the very top and very bottom of the order survive longest.
  • max_tokens is a hard ceiling that only matters for tight multi-document clusters (several near-equally-relevant hits expand past the target toward it). Leave it at the default unless you want to allow — or forbid — that expansion.

A typical chat-with-knowledge-base setup needs no tuning at all — the defaults are sized for exactly this:

json
{ "query": "..." }

A "strict citations" assistant is where deviating pays off — raise the floor to drop borderline hits, and shrink the budget to keep answers short:

json
{ "query": "...", "min_relevance": 0.5, "target_tokens": 2500 }

Searching a single document ​

When you already know which document the answer must come from — "chat with this PDF", a per-document help bot, a contract Q&A flow — use POST /searchInDoc. It is the same shape as /search but doc_filter is required and rejected as 400 if missing.

bash
curl -sS -X POST "https://api.index4.ai/v1/searchInDoc" \
  -H "authorization: Bearer $INDEX4AI_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "query": "What is the cancellation policy?",
    "doc_filter": "terms_of_service_v3"
  }'

Why a separate endpoint instead of just setting doc_filter on /search? Two reasons:

  • Safety. searchInDoc makes the per-document scope a contract, not an option — you cannot accidentally drop the filter and silently retrieve from the entire namespace.
  • Validation. The server rejects an empty doc_filter up front, so a typo that would otherwise become a "search the world" bug fails fast.

The response is the same RetrievalContext shape.

From the Python SDK ​

The SDK mirrors the endpoint split — g.search(...) for namespace-wide retrieval, g.search_in_doc(...) for per-document retrieval. The doc_filter argument is required positionally on search_in_doc so the safety guarantee carries through to Python.

python
from index4ai import Index4AI

g = Index4AI()

# Namespace-wide retrieval — defaults are sized for the common case
ctx = g.search("How did operating margin change year over year?")
print(ctx.context)

# Scoped to one document — doc_filter is positional and required
ctx = g.search_in_doc("What is the cancellation policy?", "terms_of_service_v3")

SearchContext.context is the same prompt-ready XML + Markdown block you'd get from curl. Full signature: Index4AI.search / Index4AI.search_in_doc.

Errors ​

StatusWhenNotes
400Missing query, missing doc_filter (on searchInDoc), or upstream validation errorBody is plain text.
401Missing or invalid tokenUse a project API key, or an account key / login token with X-Project-ID.
404doc_filter references an unknown documentConfirm the doc exists with GET /docs.
502Other upstream / proxy errorsRetry with backoff.
503Search backend unreachableRetry; if persistent, check status page.

Upstream errors are mapped to these statuses based on the upstream message, so you'll see a consistent contract regardless of which internal subsystem failed.

Tuning checklist ​

When retrieval feels off, work down this list:

  1. No matches at all. Check the doc is in GET /docs. Lower min_relevance (e.g. 0.3 → 0.1).
  2. Junk in the context. Raise min_relevance (e.g. 0.3 → 0.5).
  3. Truncated context. Raise target_tokens, or narrow the query so fewer hits are needed.
  4. Slow latency / heavy payload. Drop return_assets when you don't need it; lower target_tokens so less context is rendered. (return_page_images never adds payload today — see the parameter table above.)
  5. Wrong document picked. If you know the answer is in one doc, switch to /searchInDoc — that removes the cross-document ambiguity entirely. To exclude already-cited docs in an agent loop, pass exclude_doc_ids.

Next ​