Search with the API
Query the Nexus index programmatically and get ranked documents back
This reference describes the underlying platform. Use a Nexus school release with its school membership, class policy, and cost controls. Installing a base engine alone does not add those controls.
Nexus exposes two search endpoints, and which one you want depends on what you are building.
| Endpoint | Use it when |
|---|---|
POST /search | You want Nexus's search results inside your own application: RAG pipelines, agents, integrations, anything that needs ranked passages rather than an answer. Runs the same retrieval pipeline as the Search action in chat. |
POST /search/send-search-message | You are building a search interface. This is the endpoint behind the Nexus Search UI, and it adds keyword expansion, LLM document selection, streaming, and per-user search history. |
Neither endpoint generates an answer. To have a model read the results and write a response,
use POST /chat/send-chat-message instead.
Both endpoints search only the documents the calling user is allowed to see, so the same query run by two users can return different results. Both also need a vector database: on deployments running with
DISABLE_VECTOR_DBset (Nexus Lite), they answer with501.
POST /search#
The request needs nothing but a query. Everything else narrows the search or changes how the query is interpreted.
| Parameter | Description |
|---|---|
query | The query to search for. Between 1 and 2048 characters. |
sources | Restrict results to these connector source types, e.g. ["slack", "google_drive"]. |
document_sets | Restrict results to documents in these document sets, by name. |
tags | Restrict results to documents carrying all of these metadata tags, as a list of {"tag_key": ..., "tag_value": ...} objects. |
time_cutoff | ISO 8601 timestamp. Only documents updated on or after this moment are returned. Timestamps without a timezone are treated as UTC. |
persona_id | Search as an Agent. The Agent's document sets, attached documents and search start date apply on top of the other filters, and its LLM is used for query expansion. |
provider / model | The LLM used for query expansion and section selection. Both must be sent together, and the caller must have access to the provider. Defaults to the Agent's LLM, or the deployment default. |
skip_query_expansion | Run the query as written instead of rewriting and expanding it first. Useful when the query is already precise, or when you have your own expansion step. |
message_history | Preceding conversation turns, so a query like "what about last quarter?" can be interpreted in context. Defaults to query on its own. |
Results come back most relevant first:
{
"results": [
{
"citation_id": 1,
"title": "Q3 Planning",
"content": "Full text of the matched section...",
"link": "https://...",
"source_type": "google_drive",
"updated_at": "2026-08-14T09:31:00Z"
}
]
}citation_id identifies the source document, not the result:
several results share one citation_id when the search returned multiple non-overlapping sections of the same document.
import requests
API_BASE_URL = "https://school.narb.cc/api" # or your own domain
API_KEY = "YOUR_KEY_HERE"
response = requests.post(
f"{API_BASE_URL}/search",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
json={
"query": "What is our parental leave policy?",
"sources": ["confluence", "google_drive"],
},
)
for result in response.json()["results"]:
print(f"[{result['citation_id']}] {result['title']} - {result['link']}")#!/bin/bash
API_BASE_URL="https://school.narb.cc/api" # or your own domain
API_KEY="YOUR_KEY_HERE"
curl -s -X POST "${API_BASE_URL}/search" \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"query": "What is our parental leave policy?",
"sources": ["confluence", "google_drive"]
}' | jq '.results[] | {citation_id, title, link}'POST /search/send-search-message#
This endpoint takes a different set of parameters, aimed at a search interface rather than a retrieval pipeline.
| Parameter | Description |
|---|---|
search_query | The query to search for. |
filters | Restrict which documents are searched: source_type, document_set, created_at_range / updated_at_range, and tags. |
num_hits | Maximum number of merged sections to return. Defaults to 30. |
hybrid_alpha | Balance between vector and keyword matching, from 0.0 (pure keyword) to 1.0 (pure vector). Leave unset to use the deployment's HYBRID_ALPHA, which defaults to 0.5. |
include_content | When true, each result carries the full text of the matched section in content. When false, content is null and only blurb is populated. |
run_query_expansion | Have an LLM generate extra keyword queries. Every query runs in parallel and the results are merged with weighted reciprocal-rank fusion, the original query counting twice as much as each expansion. |
num_docs_fed_to_llm_selection | Hand the top N sections to an LLM that picks the most relevant ones. Omit it to skip the extra LLM call. |
stream | Whether to stream packets as they are produced. Defaults to false, unlike the chat API. |
Both LLM-backed options are optional and each costs an LLM call, so leave them off for a plain lexical/semantic search.
Query expansion widens recall on short keyword queries;
document selection narrows a long result list down to what actually answers the query,
reporting its picks in llm_selected_doc_ids without dropping the other results.
Non-streaming response#
{
"all_executed_queries": ["parental leave policy"],
"search_docs": [
{
"document_id": "...",
"semantic_identifier": "Parental Leave",
"link": "https://...",
"blurb": "...",
"content": null,
"source_type": "confluence",
"score": 0.82
}
],
"llm_selected_doc_ids": null,
"error": null
}all_executed_queries holds more than one entry only when run_query_expansion was set.
llm_selected_doc_ids is null when LLM selection was not requested or failed,
and an empty list when it ran and chose nothing. If the search fails partway through,
error is set and the other fields hold whatever was gathered before the failure.
Streaming response#
With stream: true the response is text/event-stream, one JSON object per line, in this order:
Packet type | Contents |
|---|---|
search_queries | all_executed_queries. the original query plus any expansions. |
search_docs | search_docs. the ranked results. |
llm_selected_docs | llm_selected_doc_ids. Sent only when num_docs_fed_to_llm_selection was set. |
search_error | error. Sent in place of the remaining packets if the search fails. |
import json
import requests
API_BASE_URL = "https://school.narb.cc/api" # or your own domain
API_KEY = "YOUR_KEY_HERE"
with requests.post(
f"{API_BASE_URL}/search/send-search-message",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
json={
"search_query": "What is our parental leave policy?",
"num_hits": 10,
"include_content": True,
"stream": True,
},
stream=True,
) as response:
for line in response.iter_lines():
if not line:
continue
packet = json.loads(line)
if packet["type"] == "search_docs":
for doc in packet["search_docs"]:
print(doc["semantic_identifier"], doc["link"])
elif packet["type"] == "search_error":
print("Search failed:", packet["error"])#!/bin/bash
API_BASE_URL="https://school.narb.cc/api" # or your own domain
API_KEY="YOUR_KEY_HERE"
curl -s -N -X POST "${API_BASE_URL}/search/send-search-message" \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"search_query": "What is our parental leave policy?",
"num_hits": 10,
"include_content": true,
"stream": true
}' | jq -c 'select(.type == "search_docs") | .search_docs[] | {semantic_identifier, link}'Search history#
Every query sent through POST /search/send-search-message by a signed-in user is recorded,
and GET /search/search-history
reads back that user's own queries, most recent first. Pass limit (1-1000, default 100)
and filter_days to narrow the window. Queries sent to POST /search and to the chat API are not recorded there.
Next Steps#
Guide: Send a Message to Nexus#
Have an Agent read the results and answer, instead of ranking documents
Guide: Use the Ingestion API#
Index your own documents so they show up in search