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
| Header | Required | Description |
|---|---|---|
X-API-Key | Yes | Valid API key from API_KEY_CLIENTS. The server resolves the caller's Client from this key. |
Content-Type | Yes | application/json |
Body parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
query | string | required | Non-empty search query text. |
scope | string | content | content, 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. |
alpha | float | 0.5 | Dense/semantic weight from 0.0 to 1.0; sparse/keyword weight is 1 - alpha. |
limit | int | 10 | Maximum Evidence Results to return (1–100). |
offset | int | 0 | Page 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. |
filters | object | {} | Exact metadata and entity constraints. Scalar values match exactly; list values match any value in that key. See Filterable fields. |
rerank | bool | false | Enable cross-encoder reranking for the Content lane. |
top_k_rerank | int | 20 | Requested 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_deleted | bool | false | Include soft-deleted versions and their evidence. Client, active-generation, and publication-eligibility checks still apply. |
highlight | bool | false | Return 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.0uses the dense semantic lane.0.0uses the sparse lexical lane.- A value between them uses weighted hybrid retrieval, with dense weight
alphaand sparse weight1 - 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:
- Exact normalized whole-value equality ranks above lexical and semantic evidence.
- Lexical matching requires the eligible value to share the required normalized query tokens.
- Semantic matching embeds the rendered value alone and uses the immutable Metadata
registry. The dense semantic floor for
BAAI/bge-small-en-v1.5is0.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_idmay benullwhen 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 achunk_id,chunk_index, text, and page. The page is read from the canonicalmetadata.location.page_number. For pre-v2 records, the serializer also falls back tometadata.processing.page_number, flatmetadata.page_number, and rootpage_number; these are read-only compatibility fallbacks, not public v2 metadata paths. A genuinely pageless item haspage: null.metadata_evidence: a bounded set of matching metadata items. Each item may contain its path, typed value, andmatch_kind(exact,lexical, orsemantic).
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.
Basic Content search
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
}'
Metadata search
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
}'
Keyword-heavy Content search
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:
| Endpoint | During pre-activation FENCED | During ACTIVATED, epoch movement, or unavailable authority |
|---|---|---|
POST /search | A 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_id | See the Documents API lifecycle wait and retry contract. | See the Documents API response codes. |
PATCH /update | 202 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
| Code | Description |
|---|---|
200 | Success; returns Evidence Results. |
401 | Missing or invalid API key; the Client cannot be resolved. |
400 | Unsupported or invalid filter path (custom metadata requires the metadata.custom. prefix). |
409 | metadata or both requested before this Client reached Discovery Readiness. |
422 | Invalid request parameters or body shape. |
429 | Rate limit exceeded for the resolved Client. |
500 | Stored search evidence is inconsistent with its retrieval contributions; the response includes lane/point details for operators. |
503 | Publication 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
| Key | Type | Example | Description |
|---|---|---|---|
document_id | string | "doc-123" | Document identifier |
created_at | datetime | "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 key | Full path | Example |
|---|---|---|
source.filename | metadata.source.filename | "doc.pdf" |
source.mime_type | metadata.source.mime_type | "application/pdf" |
source.extension | metadata.source.extension | "pdf" |
location.page_number | metadata.location.page_number | 1 |
location.page_label | metadata.location.page_label | "1" |
location.chunk_index | metadata.location.chunk_index | 0 |
document.page_count | metadata.document.page_count | 10 |
classification.categories | metadata.classification.categories | "risk management" |
classification.status | metadata.classification.status | "completed" |
quality.text_extraction | metadata.quality.text_extraction | "native" or "ocr" |
quality.ocr_used | metadata.quality.ocr_used | true or false |
extracted.title | metadata.extracted.title | "Annual Report" |
extracted.author | metadata.extracted.author | "Jane Doe" |
extracted.created_at | metadata.extracted.created_at | "2026-04-09" |
Entity filter keys
Entity filters address extracted values and accept one string or a list:
| Key | Example | Description |
|---|---|---|
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
| Code | When it happens | Caller action |
|---|---|---|
401 | The API key is missing or not mapped to a Client. | Send a valid X-API-Key; never guess or omit Client scope. |
409 | The 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. |
422 | The body has a missing, extra, mistyped, or out-of-range parameter. | Read the stable details entries and correct the request. |
429 | The resolved Client exceeded a rate limit. | Wait for the window to reset or adjust deployment limits. |
503 | Publication 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.