Skip to main content

Search API

POST /search retrieves evidence from the caller's client-scoped document corpus. The content scope searches indexed text, metadata searches typed metadata values, and both runs both evidence lanes. content is the default, but callers should send an explicit scope in every request. metadata and both are opt-in. Scope values are case-insensitive.

Endpoint​

POST /search

Request​

Headers​

HeaderRequiredDescription
X-API-KeyYesValid API key from API_KEY_CLIENTS. The server resolves the caller's Client from this key.
Content-TypeYesapplication/json

Body parameters​

ParameterTypeDefaultDescription
querystringrequiredNon-empty search query text.
scopestringcontentcontent, metadata, or both (case-insensitive). Send this explicitly. Every scope requires active publication authority; metadata and both also require Discovery Readiness for the caller's Client.
alphafloat0.5Dense/semantic weight from 0.0 to 1.0; sparse/keyword weight is 1 - alpha.
limitint10Maximum Evidence Results to return (1–100).
offsetint0Page offset. offset + limit must not exceed the 200-result retrieval window. With reranking enabled, only the first 50 candidates are reranked; later results keep fused search order.
filtersobject{}Exact metadata and entity constraints. Scalar values match exactly; list values match any value in that key. See Filterable fields.
rerankboolfalseEnable cross-encoder reranking for the Content lane.
top_k_rerankint20Requested number of Content candidates to rerank (1–200 accepted). The effective prefix is max(top_k_rerank, offset + limit), capped at 50; candidates after it keep fused search order.
include_deletedboolfalseInclude soft-deleted versions and their evidence. Client, active-generation, and publication-eligibility checks still apply.
highlightboolfalseReturn literal evidence spans for matched terms in Content and Metadata evidence.

The server always resolves a Client from X-API-Key; callers do not provide client_id. Client isolation applies to every scope, filter, and lifecycle option. An empty or missing API_KEY_CLIENTS mapping fails closed with 401 rather than serving unscoped data.

content searches indexed text. metadata searches immutable, typed Metadata values, and both runs content and metadata in parallel before merging at one (document_id, version_id) identity. Publication-backed requests return the common Evidence Result contract below for all three scopes. A version must be in the active generation and marked publication_eligible to be returned. Missing, stale, changing, or unverifiable publication authority fails the whole request under the publication error contract; it never produces a partial answer. include_deleted changes lifecycle visibility only and never bypasses Client or generation checks.

metadata and both additionally require the caller's Client to have reached Discovery Readiness. A Client that is still backfilling receives 409 METADATA_DISCOVERY_NOT_READY, not an empty or content-only answer. content is unaffected by Metadata Discovery readiness. Every scope requires active publication authority.

Alpha blending​

alpha controls retrieval weighting:

  • 1.0 uses the dense semantic lane.
  • 0.0 uses the sparse lexical lane.
  • A value between them uses weighted hybrid retrieval, with dense weight alpha and sparse weight 1 - alpha.

Start with 0.5. Lower values help exact names, identifiers, and terminology; higher values help conceptual queries whose wording differs from indexed text. An explicit request value takes precedence over HYBRID_ALPHA.

Query normalization and typo correction​

Content retrieval may expand conservative abbreviations and correct known typos before embedding. Fuzzy correction requires a Client-vocabulary candidate with at least 0.6 character similarity, and preserves query words that are roots of common Malay affix forms found in that vocabulary. Metadata retrieval uses only lossless Unicode, case, and whitespace normalization; it does not expand, correct, fuzzily rewrite, or passage-rerank the query.

Metadata matching and eligibility​

Metadata matching has deterministic precedence and gates:

  1. Exact normalized whole-value equality ranks above lexical and semantic evidence.
  2. Lexical matching requires the eligible value to share the required normalized query tokens.
  3. Semantic matching embeds the rendered value alone and uses the immutable Metadata registry. The dense semantic floor for BAAI/bge-small-en-v1.5 is 0.75; callers cannot lower or override it.

Value eligibility is checked before path context, and one typed contract governs every branch: structured queries and values (complete identifiers, opaque codes, dates, numbers) admit only whole-value equality; mixed queries keep their digit-bearing components verbatim; descriptive values keep lexical and independent semantic matching, including short acronyms. A path can reject an otherwise eligible lexical value when the path supplies the shared tokens, but path context cannot rescue an ineligible value. The path is never the embedding input. A path-only match therefore never qualifies, and a candidate-supplied match kind or path cannot override the value-only gate.

Metadata matching uses lossless Unicode, case, and whitespace normalization. It does not expand abbreviations, correct typos, fuzzily rewrite the query, or passage-rerank metadata.

Exact filters​

Filters constrain the candidate set; they do not add to, replace, or otherwise score a result. Keys are combined with AND logic. A list under one key uses OR logic within that key. For example, the following requires metadata.custom.tenant_id to equal tenant-a and metadata.custom.department to equal one of the two listed values:

{
"metadata.custom.tenant_id": "tenant-a",
"metadata.custom.department": ["finance", "operations"]
}

Response​

Every supported caller scope uses one response language: an Evidence Result page. A result row represents one Document Version, not one chunk or one Metadata Leaf. Callers decode the same Evidence Result shape for every scope and must not branch on deployment mode.

{
"results": [
{
"rank": 1,
"document_version": {
"document_id": "doc-123",
"version_id": "v2"
},
"canonical_metadata": {
"schema_version": "2",
"source": {"filename": "apollo.pdf"},
"document": {},
"location": {},
"classification": {"status": "ready", "categories": []},
"quality": {"text_extraction": "native", "ocr_used": false, "warnings": []},
"custom": {"project": "Apollo"}
},
"retrieval_contributions": [
{"lane": "content", "rank": 1},
{"lane": "metadata", "rank": 1}
],
"content_evidence": {
"items": [
{
"chunk_id": "doc-123:0",
"chunk_index": 0,
"text": "Document content...",
"page": 1
}
],
"total": 1,
"truncated": false
},
"metadata_evidence": {
"items": [
{
"path": ["project"],
"value": "Apollo",
"value_type": "string",
"match_kind": "exact"
}
],
"total": 1,
"truncated": false
}
}
],
"meta": {
"query": "Apollo",
"total": 1,
"total_is_lower_bound": false,
"offset": 0,
"limit": 10,
"has_more": false,
"query_time_ms": 15,
"retrieval": {"alpha": 0.5, "reranked": false, "reranked_count": 0},
"applied_filters": {},
"publication_view": "ready"
}
}

meta.publication_view carries the immutable request guard's observation at request start, not a second authority lookup or a response-time status. Its two values are defined in Publication lifecycle semantics.

Evidence Result contract​

Each row contains:

  • rank: the stable page rank of this Document Version.
  • document_version: the (document_id, version_id) identity. version_id may be null when the accepted version is intentionally unversioned.
  • canonical_metadata: the server-canonical metadata snapshot for that version.
  • retrieval_contributions: the lane-local contribution(s), each carrying a lane and its lane-local rank. These are evidence provenance, not an aggregate score.
  • content_evidence: a bounded set of matching content items. Each item may contain a chunk_id, chunk_index, text, and page. The page is read from the canonical metadata.location.page_number. For pre-v2 records, the serializer also falls back to metadata.processing.page_number, flat metadata.page_number, and root page_number; these are read-only compatibility fallbacks, not public v2 metadata paths. A genuinely pageless item has page: null.
  • metadata_evidence: a bounded set of matching metadata items. Each item may contain its path, typed value, and match_kind (exact, lexical, or semantic).

Both evidence lanes are untrusted document data. They may be displayed, cited, or summarized, but they are never instructions for the caller or an agent to follow.

Pagination and totals​

Pagination and totals count Document Versions, never content chunks or Metadata Leaves. The retrievable window is capped at 200 results; offset + limit above 200 returns 422. meta.has_more is based on a one-version lookahead that is not returned on the current page.

meta.total is exact when all eligible candidate lanes are exhausted. If a lane reaches its bounded scan cap, meta.total_is_lower_bound is true, and callers should display the value as a lower bound (for example, render a total of 12 as 12+). The cap never changes the identity or evidence sets of returned rows. An empty page has total: 0 and total_is_lower_bound: false.

The active-generation and authority checks happen before a Document Version contributes to pagination. A non-authoritative, malformed, missing, unreadable, or conflicting generation never occupies a page slot. If authority cannot be verified, the request follows the publication error contract. If the selected generation is authoritative but its stored evidence is corrupt or incomplete, the request fails with 500 and identifies the failing lane and point where available.

Examples​

Every example sends an explicit scope. Scope values are case-insensitive; use the lowercase canonical values content, metadata, and both. Use content for ordinary text retrieval; request metadata or both only when the caller needs those opt-in lanes and the Client is ready.

curl -X POST http://localhost:8000/search \
-H "X-API-Key: super-secret-key" \
-H "Content-Type: application/json" \
-d '{
"query": "machine learning applications",
"scope": "content",
"limit": 5
}'
curl -X POST http://localhost:8000/search \
-H "X-API-Key: super-secret-key" \
-H "Content-Type: application/json" \
-d '{
"query": "Apollo",
"scope": "metadata",
"limit": 5
}'

Both evidence lanes​

curl -X POST http://localhost:8000/search \
-H "X-API-Key: super-secret-key" \
-H "Content-Type: application/json" \
-d '{
"query": "project management",
"scope": "both",
"limit": 10
}'
curl -X POST http://localhost:8000/search \
-H "X-API-Key: super-secret-key" \
-H "Content-Type: application/json" \
-d '{
"query": "Python developer",
"scope": "content",
"alpha": 0.3,
"limit": 10
}'

Exact metadata filters​

Exact filters constrain results without changing their rank. Custom metadata requires the explicit metadata.custom. prefix; documented dotted paths address system metadata.

curl -X POST http://localhost:8000/search \
-H "X-API-Key: super-secret-key" \
-H "Content-Type: application/json" \
-d '{
"query": "quarterly report",
"scope": "content",
"filters": {
"classification.categories": "financial reporting",
"metadata.custom.department": "Engineering"
}
}'

Include soft-deleted evidence​

Soft-deleted versions are hidden by default. Set include_deleted only for an intentional audit or recovery workflow. The request remains restricted to the Client from the API key, the active generation, and eligible published versions.

curl -X POST http://localhost:8000/search \
-H "X-API-Key: super-secret-key" \
-H "Content-Type: application/json" \
-d '{
"query": "project management",
"scope": "content",
"include_deleted": true
}'

Entity filters​

Use one string for an exact entity value or a list for OR semantics within that key. Different keys remain AND-constrained.

curl -X POST http://localhost:8000/search \
-H "X-API-Key: super-secret-key" \
-H "Content-Type: application/json" \
-d '{
"query": "quarterly earnings",
"scope": "content",
"filters": {
"persons": "John Doe",
"organizations": ["Bank A", "Bank B"]
}
}'

Reranking​

Reranking can change Content result order. The service considers the larger of top_k_rerank and offset + limit, capped at 50. Requests still accept top_k_rerank up to 200 and return up to 100 results. Candidates after the first 50 keep their fused search order. meta.retrieval.reranked_count reports how many Content candidates were actually reranked; model scores are not returned.

curl -X POST http://localhost:8000/search \
-H "X-API-Key: super-secret-key" \
-H "Content-Type: application/json" \
-d '{
"query": "deep learning neural networks",
"scope": "content",
"rerank": true,
"top_k_rerank": 20,
"limit": 5
}'

Pagination​

# First page
curl -X POST http://localhost:8000/search \
-H "X-API-Key: super-secret-key" \
-H "Content-Type: application/json" \
-d '{
"query": "product roadmap",
"scope": "content",
"limit": 10,
"offset": 0
}'

# Second page
curl -X POST http://localhost:8000/search \
-H "X-API-Key: super-secret-key" \
-H "Content-Type: application/json" \
-d '{
"query": "product roadmap",
"scope": "content",
"limit": 10,
"offset": 10
}'

Just command​

As an alternative to curl, use the just search command from the terminal. It always sends scope, which defaults to content; the CLI examples below retain its accepted title-case flags while the API canonicalizes them to lowercase:

export INFOCONNECT_API_KEY=super-secret-key

# Concise Content summary
just search "machine learning" -- --scope Content

# Full Evidence Result JSON, preserving nested objects and arrays
just search "machine learning" -- --scope Content --json

# Metadata opt-in
just search "Apollo" -- --scope Metadata --json

# Both lanes with exact filters
just search "project management" -- --scope Both \
--filters '{"metadata.custom.department":"Engineering"}' --json

# Pagination and reranking for Content
just search "neural networks" -- --scope Content --limit 10 --offset 10 --rerank

The concise terminal view renders each Document Version's rank, identity, retrieval lane/rank contributions, and bounded Content/Metadata evidence sets. --json prints the complete API body as JSON, including nested document_version, canonical_metadata, both evidence sets, total, truncated, and pagination metadata without flattening or dropping values. See the CLI Tools guide for the complete option list.

Publication lifecycle semantics​

Search, document deletion, and metadata updates intentionally have different publication contracts:

EndpointDuring pre-activation FENCEDDuring ACTIVATED, epoch movement, or unavailable authority
POST /searchA request that observes FENCED at start and has its Client's last READY snapshot pinned is served as 200. meta.publication_view is "pinned_ready", including for a genuine no-match. There is no Retry-After on this success. A normal READY read reports "ready".Whole-request 503 with Retry-After: 1; no partial or fabricated empty result is returned.
DELETE /document/:document_idSee the Documents API lifecycle wait and retry contract.See the Documents API response codes.
PATCH /update202 Accepted means the client-bound update was queued; it does not mean the new generation is visible yet. The worker retries publication-busy conditions.The same accepted-not-applied contract applies when enqueue succeeds; poll GET /status/{job_id} for the eventual result.

The search signal exposes only the public view kind. It does not expose publication epochs, operation IDs, or other state-machine internals. A pinned FENCED response is the last committed view before activation, not a mixed-generation response. See the Update API for the asynchronous job contract and the Documents API for synchronous delete and restore semantics.

Response codes​

CodeDescription
200Success; returns Evidence Results.
401Missing or invalid API key; the Client cannot be resolved.
400Unsupported or invalid filter path (custom metadata requires the metadata.custom. prefix).
409metadata or both requested before this Client reached Discovery Readiness.
422Invalid request parameters or body shape.
429Rate limit exceeded for the resolved Client.
500Stored search evidence is inconsistent with its retrieval contributions; the response includes lane/point details for operators.
503Publication reads fail closed under the publication error contract. See Publication lifecycle semantics for successful pinned reads.

A 409 is a client capability/readiness conflict. A 503 fails closed; do not treat it as an empty result or silently fall back to another scope. Honor Retry-After for retryable publication codes and investigate repeated internal_error responses.

Rate limiting​

Search requests are rate-limited per resolved Client using a sliding-window algorithm. The default is 30 requests per minute for protected routes, with an additional search-specific counter for POST /search. Multiple API keys mapped to the same Client share that budget. See the Rate Limiting guide for deployment recommendations.

Filterable fields​

Use filters to constrain the candidate set. All keys use AND logic, while a list under one key uses OR logic. Filters are exact constraints and do not affect retrieval rank.

Top-level paths​

KeyTypeExampleDescription
document_idstring"doc-123"Document identifier
created_atdatetime"2026-04-09T10:30:00Z"Exact creation timestamp

client_id and lifecycle markers (deleted, deleted_at, and deleted_by) are reserved system state, not user filters. Use include_deleted to change deletion visibility. A caller cannot use filters to cross Client boundaries.

Custom metadata paths​

Any custom metadata uploaded through /embed or /update can be filtered using the explicit metadata.custom. prefix:

{
"query": "python developer",
"scope": "content",
"filters": {
"metadata.custom.Field A": "test"
}
}

Keys with spaces remain ordinary JSON keys under the metadata.custom. prefix. The resolver quotes them for the underlying store.

System metadata paths​

Filter keyFull pathExample
source.filenamemetadata.source.filename"doc.pdf"
source.mime_typemetadata.source.mime_type"application/pdf"
source.extensionmetadata.source.extension"pdf"
location.page_numbermetadata.location.page_number1
location.page_labelmetadata.location.page_label"1"
location.chunk_indexmetadata.location.chunk_index0
document.page_countmetadata.document.page_count10
classification.categoriesmetadata.classification.categories"risk management"
classification.statusmetadata.classification.status"completed"
quality.text_extractionmetadata.quality.text_extraction"native" or "ocr"
quality.ocr_usedmetadata.quality.ocr_usedtrue or false
extracted.titlemetadata.extracted.title"Annual Report"
extracted.authormetadata.extracted.author"Jane Doe"
extracted.created_atmetadata.extracted.created_at"2026-04-09"

Entity filter keys​

Entity filters address extracted values and accept one string or a list:

KeyExampleDescription
persons"John Doe"People mentioned in the document
organizations"Bank of America"Companies, banks, and institutions
dates"2024-01-15"Dates and time expressions
locations"New York"Geographic locations
monetary_amounts"$1,000,000"Currency and monetary values
account_numbers"1234567890"Account numbers
transaction_refs"TXN-12345"Transaction references
account_types"savings"Account types

Error responses​

CodeWhen it happensCaller action
401The API key is missing or not mapped to a Client.Send a valid X-API-Key; never guess or omit Client scope.
409The Client's Metadata Discovery convergence is pending, blocked, or stale.Read /discovery/readiness; while backfilling, retry after Retry-After and use content meanwhile. For historical corpora, signature changes, or repeated failures, use the manual backfill repair path.
422The body has a missing, extra, mistyped, or out-of-range parameter.Read the stable details entries and correct the request.
429The resolved Client exceeded a rate limit.Wait for the window to reset or adjust deployment limits.
503Publication authority cannot prove the active eligible generation, or a publication lane fails.Follow the publication error contract; do not use a partial or unverified response.

409 — Metadata Discovery Not Ready​

A metadata or both request never silently degrades to content and never returns an empty success merely because Discovery is incomplete:

{
"detail": "Metadata Discovery is not ready for this Client",
"error_code": "METADATA_DISCOVERY_NOT_READY",
"request_id": "5d7f4c1a2b9e4b3e8f6f0d2a4c9b8a1e",
"details": {
"code": "METADATA_DISCOVERY_NOT_READY",
"state": "not_started",
"readiness": "/discovery/readiness",
"scopes_available": ["Content"]
}
}

Read Discovery Readiness for this Client's state and remaining work. A 503 from that endpoint or from search means the publication registry could not be read or verified at all; it is not a readiness conflict.

503 — Publication authority inconsistent​

A read that encounters eligible lane points whose required Manifest evidence is missing fails closed with 503, Retry-After: 1, and error_code: "publication_authority_inconsistent". Unsealed uploads stay invisible and do not trigger this error. Pre-existing eligible orphan points or corrupted publication state can still trigger it:

{
"detail": "Internal server error",
"error_code": "publication_authority_inconsistent",
"request_id": "5d7f4c1a2b9e4b3e8f6f0d2a4c9b8a1e",
"details": {
"code": "publication_authority_inconsistent",
"message": "publication identity has no authoritative manifest"
}
}

Retry after Retry-After. If the failure persists, an operator should investigate the Manifest-less generation using the orphan reconciler guidance. Read failures emit the existing info-level publication_unavailable audit event, with a query fingerprint for failed searches and never the query text. They do not emit a dedicated alert or alert metric; durable-orphan alerting belongs to the separate report-only orphan sweep, which waits out the upload window.

An explicit authority-not-ready refusal returns 503 publication_busy; a snapshot move or a request that begins or ends during ACTIVATE returns 503 publication_snapshot_changed. Both carry Retry-After: 1. Search adds no wait for publication readiness. Unexpected failures, including unreadable or malformed Manifest evidence and unexpected publication lane failures, remain internal_error. Delete and restore use the distinct Documents API response codes.

Search automatically retries an idempotent Qdrant read once when the HTTP connection is dropped (httpx.RemoteProtocolError, including its qdrant-client transport wrapper). If the retry succeeds, the request returns its normal results. A second consecutive drop still fails closed with 503, Retry-After: 1, and error_code: "internal_error". Other transport errors are not retried by this mechanism, and it never replays writes.

422 — Validation error​

The request requires a non-empty query and rejects unknown body keys:

curl -X POST http://localhost:8000/search \
-H "X-API-Key: super-secret-key" \
-H "Content-Type: application/json" \
-d '{"scope": "content"}'
{
"detail": "Request validation failed",
"error_code": "validation_error",
"request_id": "5d7f4c1a2b9e4b3e8f6f0d2a4c9b8a1e",
"details": [
{
"type": "missing",
"loc": ["body", "query"],
"msg": "Field required",
"input": {"scope": "content"}
}
]
}

The loc array identifies the failing request property. Check details for the exact constraint before retrying.