Query & Search API¶
Base path: /api/v1/query
This module handles all natural-language interaction with the knowledge base: asking questions (RAG), semantic search, query history, documentation gap tracking, and aggregate analytics.
See API Reference for auth, errors, and pagination.
POST /api/v1/query/ask¶
Submit a natural language question and receive a RAG-generated answer with source citations.
The service embeds the question, retrieves the most relevant knowledge chunks from the organisation's knowledge base, and synthesises a grounded answer using an LLM. The interaction is persisted as a QueryLog record. If no relevant knowledge is found, the question is logged as a documentation gap.
Auth: JWT required — any active user (member, manager, or admin). All plans.
Request body (application/json)
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
query_text |
string | Yes | 1–2000 chars, non-blank | Natural language question (Arabic or English) |
Response 200 OK
| Field | Type | Description |
|---|---|---|
answer |
string | LLM-generated answer grounded in knowledge base sources |
sources |
SourceCitation[] | List of knowledge entries cited in the answer |
confidence |
string | high | medium | low | none — derived from source quality |
language |
string | Detected language of the question: ar | en | mixed |
query_id |
string (UUID) | ID of the persisted QueryLog record |
SourceCitation object
| Field | Type | Description |
|---|---|---|
entry_id |
string | UUID of the cited KnowledgeEntry |
title |
string | Title of the knowledge entry |
relevance_score |
float | 0.0–1.0 cosine similarity score |
{
"answer": "Damaged goods may be returned within 30 days of purchase with original packaging. Contact support@example.com to initiate the process.",
"sources": [
{
"entry_id": "a1b2c3d4-0000-0000-0000-000000000001",
"title": "Return & Refund Policy",
"relevance_score": 0.94
},
{
"entry_id": "a1b2c3d4-0000-0000-0000-000000000002",
"title": "Customer Support Procedures",
"relevance_score": 0.78
}
],
"confidence": "high",
"language": "en",
"query_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}
Errors
| HTTP | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR |
query_text is missing, blank, or > 2000 chars |
| 401 | INVALID_TOKEN |
JWT missing or malformed |
| 401 | USER_NOT_FOUND |
Authenticated user does not exist or is inactive |
| 402 | SUBSCRIPTION_REQUIRED |
Organisation subscription is inactive |
curl -X POST https://api.knora.io/api/v1/query/ask \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"query_text": "What is the return policy for damaged goods?"}'
GET /api/v1/query/search¶
Perform a semantic (vector) search over the organisation's knowledge base and return the top-K most similar chunks — without invoking an LLM. Use this endpoint when you want raw similarity results rather than a synthesised answer, e.g. for suggestions, autocomplete, or building custom UI experiences.
Auth: JWT required — any active user. All plans.
Query parameters
| Parameter | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
q |
string | Yes | — | Non-empty | Search text |
limit |
integer | No | 20 |
1–50 | Maximum number of results to return |
department |
string | No | — | — | Filter results to a specific department name |
limit is clamped server-side to [1, 50]. The department filter is applied after visibility checks.
Response 200 OK
| Field | Type | Description |
|---|---|---|
query |
string | The search text that was submitted |
results |
SearchResult[] | Ranked list of matching knowledge chunks |
total |
integer | Number of results returned |
SearchResult object
| Field | Type | Description |
|---|---|---|
entry_id |
string | UUID of the parent KnowledgeEntry |
title |
string | Title of the entry |
similarity_score |
float | 0.0–1.0 cosine similarity |
chunk_text |
string | The matching text chunk |
department |
string | null | Department the entry belongs to, if set |
language |
string | null | Language of the chunk (ar, en, etc.) |
{
"query": "vacation request process",
"results": [
{
"entry_id": "b2c3d4e5-0000-0000-0000-000000000001",
"title": "HR Leave Management Policy",
"similarity_score": 0.91,
"chunk_text": "Annual leave requests must be submitted at least two weeks in advance via the HR portal...",
"department": "Human Resources",
"language": "en"
}
],
"total": 1
}
Errors
| HTTP | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR |
q parameter is missing or empty |
| 401 | INVALID_TOKEN |
JWT missing or malformed |
| 401 | USER_NOT_FOUND |
User does not exist or is inactive |
| 402 | SUBSCRIPTION_REQUIRED |
Organisation subscription is inactive |
curl "https://api.knora.io/api/v1/query/search?q=vacation+request+process&limit=10&department=Human+Resources" \
-H "Authorization: Bearer <token>"
GET /api/v1/query/history¶
Return the authenticated user's own recent query history. Each item represents one question the user submitted via /ask.
Auth: JWT required — any active user (own history only). All plans.
Query parameters
| Parameter | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
limit |
integer | No | 50 |
1–100 | Maximum number of history items to return |
limit is clamped server-side to [1, 100]. Results are returned in reverse chronological order.
Response 200 OK
| Field | Type | Description |
|---|---|---|
items |
QueryHistoryItem[] | Ordered list of recent queries |
total |
integer | Number of items returned |
QueryHistoryItem object
| Field | Type | Description |
|---|---|---|
id |
string | UUID of the QueryLog record |
query_text |
string | The original question text |
language |
string | Detected language: ar | en | mixed |
has_answer |
boolean | Whether the query received an LLM answer |
source_count |
integer | Number of knowledge entries cited in the answer |
created_at |
string | ISO 8601 timestamp of when the query was submitted |
{
"items": [
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"query_text": "What is the return policy for damaged goods?",
"language": "en",
"has_answer": true,
"source_count": 2,
"created_at": "2026-05-31T14:22:10Z"
},
{
"id": "c1d2e3f4-0000-0000-0000-000000000001",
"query_text": "كيف أطلب إجازة سنوية؟",
"language": "ar",
"has_answer": false,
"source_count": 0,
"created_at": "2026-05-31T09:05:44Z"
}
],
"total": 2
}
Errors
| HTTP | Code | Cause |
|---|---|---|
| 401 | INVALID_TOKEN |
JWT missing or malformed |
| 401 | USER_NOT_FOUND |
User does not exist or is inactive |
| 402 | SUBSCRIPTION_REQUIRED |
Organisation subscription is inactive |
GET /api/v1/query/gaps¶
List unresolved documentation gaps — queries that were submitted but could not be answered because no relevant knowledge was found. Gaps are automatically created by the /ask endpoint. Use this endpoint to understand what knowledge your organisation is missing and prioritise content creation.
Auth: JWT required — manager or admin only. All plans.
Query parameters
| Parameter | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
department |
string | No | — | — | Filter gaps to a specific department |
limit |
integer | No | 100 |
1–500 | Maximum number of gaps to return |
limit is clamped server-side to [1, 500]. Only unresolved gaps are returned.
Response 200 OK
| Field | Type | Description |
|---|---|---|
items |
GapResponse[] | List of unresolved gap records |
total |
integer | Number of items returned |
GapResponse object
| Field | Type | Description |
|---|---|---|
id |
string | UUID of the gap log record |
query_text |
string | The original question that could not be answered |
user_id |
string | null | UUID of the user who asked the question |
department |
string | null | Department context of the gap, if available |
resolved |
boolean | Whether the gap has been resolved |
resolved_by_entry_id |
string | null | UUID of the KnowledgeEntry that resolved this gap |
created_at |
string | ISO 8601 timestamp when the gap was first logged |
{
"items": [
{
"id": "d4e5f6a7-0000-0000-0000-000000000001",
"query_text": "كيف أطلب إجازة سنوية؟",
"user_id": "e5f6a7b8-0000-0000-0000-000000000001",
"department": "Human Resources",
"resolved": false,
"resolved_by_entry_id": null,
"created_at": "2026-05-31T09:05:44Z"
}
],
"total": 1
}
Errors
| HTTP | Code | Cause |
|---|---|---|
| 401 | INVALID_TOKEN |
JWT missing or malformed |
| 401 | USER_NOT_FOUND |
User does not exist or is inactive |
| 403 | INSUFFICIENT_ROLE |
Caller is a plain member |
| 402 | SUBSCRIPTION_REQUIRED |
Organisation subscription is inactive |
curl "https://api.knora.io/api/v1/query/gaps?department=Finance&limit=50" \
-H "Authorization: Bearer <token>"
POST /api/v1/query/gaps/:gap_id/resolve¶
Mark a documentation gap as resolved by linking it to an existing KnowledgeEntry. This signals that the missing content has been created, allowing the gap to be tracked as closed.
Auth: JWT required — manager or admin only. All plans.
Path parameters
| Parameter | Type | Description |
|---|---|---|
gap_id |
UUID | ID of the gap record to resolve |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
entry_id |
string | Yes | UUID of the KnowledgeEntry that resolves this gap |
Response 200 OK
Returns the updated GapResponse object (see /gaps above) reflecting the resolved state, with resolved: true and resolved_by_entry_id populated.
{
"id": "d4e5f6a7-0000-0000-0000-000000000001",
"query_text": "كيف أطلب إجازة سنوية؟",
"user_id": "e5f6a7b8-0000-0000-0000-000000000001",
"department": "Human Resources",
"resolved": true,
"resolved_by_entry_id": "a1b2c3d4-0000-0000-0000-000000000005",
"created_at": "2026-05-31T09:05:44Z"
}
Errors
| HTTP | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR |
entry_id is missing or is not a valid UUID |
| 401 | INVALID_TOKEN |
JWT missing or malformed |
| 401 | USER_NOT_FOUND |
User does not exist or is inactive |
| 403 | INSUFFICIENT_ROLE |
Caller is a plain member |
| 404 | GAP_NOT_FOUND |
gap_id does not exist within this organisation |
| 404 | ENTRY_NOT_FOUND |
entry_id does not exist or belongs to a different org |
| 409 | GAP_ALREADY_RESOLVED |
The gap has already been resolved |
| 402 | SUBSCRIPTION_REQUIRED |
Organisation subscription is inactive |
curl -X POST "https://api.knora.io/api/v1/query/gaps/d4e5f6a7-0000-0000-0000-000000000001/resolve" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"entry_id": "a1b2c3d4-0000-0000-0000-000000000005"}'
GET /api/v1/query/analytics¶
Return aggregate statistics about query activity and documentation gap coverage for the organisation. Useful for dashboards, health reporting, and prioritising knowledge base growth.
Auth: JWT required — admin only. Plan check fires before role check — trial and starter organisations receive 403 PLAN_UPGRADE_REQUIRED regardless of role.
No query parameters or request body.
Response 200 OK
| Field | Type | Description |
|---|---|---|
total_queries |
integer | Total questions submitted by all users in the organisation |
answered_queries |
integer | Queries that received an LLM answer with at least one source |
unanswered_queries |
integer | Queries that received no answer (became gaps) |
answer_rate |
float | Fraction 0.0–1.0 of queries that were answered (answered / total) |
total_gaps |
integer | Total number of gap records ever logged |
resolved_gaps |
integer | Gaps that have been linked to a resolving KnowledgeEntry |
unresolved_gaps |
integer | Gaps still awaiting resolution |
top_unanswered_topics |
TopicCount[] | Most frequently occurring unanswered query clusters (up to 10) |
answer_rate is 0.0 when total_queries is 0. top_unanswered_topics is empty if no unanswered queries exist.
TopicCount object
| Field | Type | Description |
|---|---|---|
topic |
string | Representative query text for the cluster |
count |
integer | Number of times this topic was asked unanswered |
{
"total_queries": 1240,
"answered_queries": 1058,
"unanswered_queries": 182,
"answer_rate": 0.853,
"total_gaps": 182,
"resolved_gaps": 74,
"unresolved_gaps": 108,
"top_unanswered_topics": [
{ "topic": "expense reimbursement process", "count": 23 },
{ "topic": "parental leave policy", "count": 17 },
{ "topic": "remote work equipment allowance", "count": 11 }
]
}
Errors
| HTTP | Code | Cause |
|---|---|---|
| 401 | INVALID_TOKEN |
JWT missing or malformed |
| 401 | USER_NOT_FOUND |
User does not exist or is inactive |
| 403 | PLAN_UPGRADE_REQUIRED |
Organisation is on trial or starter plan (checked first) |
| 403 | INSUFFICIENT_ROLE |
Caller is member or manager (admin required) |
| 402 | SUBSCRIPTION_REQUIRED |
Organisation subscription is inactive |