Semantic and Hybrid Search
This tutorial shows how to find the content chunks most relevant to a question with vector similarity search over the embeddings stored in the Cloud Content Repository, how to narrow the search with a document filter and a full-text match on the chunk text, and how to look up a single embedding by ID. These are the building blocks of a Retrieval-Augmented Generation (RAG) workflow.
Prerequisites
- A valid user access token for the signed-in user you are searching for, not a service user token: document ACLs are applied to the caller (see Authentication)
- The base URL of your environment
- A query vector produced by the same embedding model as the stored embeddings. The API does not compute embeddings from text
Set two variables before running the curl examples. The HTTP examples use the equivalent {{baseUrl}} and {{accessToken}} placeholders.
export BASE_URL="https://{environment_id}.content-lake.ai.experience.hyland.com"
export TOKEN="<access token of the signed-in user>"
Embeddings are vector representations of text and images generated during ingestion. The vector dimensions depend on the model used. These embeddings power semantic search and are commonly used in Retrieval-Augmented Generation (RAG) workflows.
During ingestion, documents are chunked and each chunk is converted into a vector embedding. Each embedding record contains the chunk text (sysembed_text), the vector (sysembed_vector), the location within the source document (sysembed_location — page/paragraph for text, bounding box for images, row/column for spreadsheets, timestamps for video), and a type label (sysembed_type, e.g. cohere.embed-multilingual-v3). There is no separate "chunks" API — embeddings are the chunks.
Tutorial Overview
In this tutorial, you'll learn to:
- Run a vector similarity search
- Filter the search by document and embedding type
- Combine vector and full-text matching (hybrid search)
- Look up an embedding by ID
- Use the results in a RAG workflow
Run a Vector Search
Perform a vector similarity (k-NN) search over embeddings. You supply the query vector; the API does not compute embeddings from text. Optionally combine it with a full-text match on the chunk text (hybrid search).
Send the query vector to POST /api/query/embeddings:
- HTTP
- curl
POST {{baseUrl}}/api/query/embeddings
Authorization: Bearer {{accessToken}}
Accept: application/vnd.hxcs.v1+json
Content-Type: application/json
{
"vector": [0.0316, 0.0449, -0.0221, ...],
"query": "SELECT * FROM SysContent WHERE sys_primaryType = 'SysFile'",
"limit": 2
}
curl -X POST "$BASE_URL/api/query/embeddings" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.hxcs.v1+json" \
-H "Content-Type: application/json" \
--data @- <<'JSON'
{
"vector": [0.0316, 0.0449, -0.0221, ...],
"query": "SELECT * FROM SysContent WHERE sys_primaryType = 'SysFile'",
"limit": 2
}
JSON
The vector's length must equal the index's vector dimension. In the tested environment, whose embeddings are of type cohere.embed-multilingual-v3, the dimension is 1024; the dimension in your environment depends on its embedding model. Any other length returns 400 with a message containing Query vector has invalid dimension: 3. Dimension should be: 1024 (prefixed with Error performing OpenSearch query: failed to create query:), and a request without a vector returns 400 Invalid input.
Response
Returns repositoryId, the supplied vector, query, limit, offset, count, and an embeddings array. With trackTotalCount: true the response includes totalCount (plus totalCountIsTruncated: true when the count was capped); otherwise neither field is returned. In vector search, totalCount is not the number of all matching chunks: it is capped at the number of nearest neighbours fetched, which is the larger of limit and trackTotalCountUpTo. count can be lower than limit when some matching chunks' text cannot be retrieved. Each entry is an embedding object (sysembed_* fields) as described in Embedding Object, with sysembed_score set to the similarity score.
-- Request (vector shortened; it must have 1024 values here)
{
"vector": [0.0316, 0.0449, -0.0221, ...],
"query": "SELECT * FROM SysContent WHERE sys_primaryType = 'SysFile'",
"limit": 2
}
-- Response (trimmed)
{
"query": "SELECT * FROM SysContent WHERE sys_primaryType = 'SysFile'",
"limit": 2,
"offset": 0,
"count": 2,
"embeddings": [
{
"sysembed_id": "ea20b9df-50fd-5687-acf3-ef90de020002",
"sysembed_docId": "019546d0-32c9-487c-a75d-2ae160b6a93a",
"sysembed_type": "cohere.embed-multilingual-v3",
"sysembed_text": "## Master Services Agreement",
"sysembed_location": { "text": { "page": 1, "paragraph": 0 }, "position": { "left": 448, "top": 260, "right": 712, "bottom": 319 }, "timestamp": null, "spreadsheet": null },
"sysembed_score": 1.0,
...
},
{ ..., "sysembed_score": 0.73336875 }
],
"vector": [ ... ]
}
Search Fields
| Field | Type | Required | Description |
|---|---|---|---|
vector | number[] | Yes | The query vector. Missing returns 400 Invalid input. Its length must equal the index's vector dimension, otherwise 400 with a message containing Query vector has invalid dimension: 3. Dimension should be: 1024 (prefixed with Error performing OpenSearch query: failed to create query:) (1024 is the dimension in the tested environment, whose embeddings are of type cohere.embed-multilingual-v3) |
repositoryId | string | No | Repository to search (default "default"; the response then echoes "repositoryId": null). An unknown repository returns 404 No such repository: <name>. |
embeddingType | string | No | Wildcard pattern on the embedding type (default *) |
chunkFTS | string | No | Space-separated terms to match in the chunk text. When set, the search is hybrid: full-text matches are merged into the vector ranking (rank fusion). It does not exclude chunks that lack the terms, and sysembed_score becomes a fused rank score instead of a similarity. |
query | string | No | Filter on the parent document, in the syntax given by syntax (default SELECT * FROM SysContent). This is a filter, not a natural-language query |
syntax | string | No | "hxql" (default) or "expr" |
limit | integer | No | Max embeddings to return (default 10) |
offset | integer | No | Number of embeddings to skip (default 0) |
trackTotalCount | boolean | No | Whether to compute the total count of embeddings (default false) |
trackTotalCountUpTo | integer | No | Max number of embeddings to count, up to 10000. Larger values (and limit: 0) return 400 ([knn] requires k to be in the range (0, 10000]). |
Both endpoints return JSON for Accept: application/vnd.hxcs.v1+json, application/json or */*, or when Accept is omitted. Non-JSON types such as text/plain or application/xml return 406 with an empty body.
Filter and Combine with Full-Text (Hybrid Search)
Narrow the search with two optional fields:
queryis an HXQL filter on the parent document (defaultSELECT * FROM SysContent). It is a filter, not a natural-language question. See Querying with HXQL.chunkFTSholds space-separated terms to match in the chunk text. When set, the search is hybrid: chunks that match the terms are merged into the vector results by rank fusion. Chunks without the terms can still be returned, and scores become fused rank values (around0.016) rather than similarities.
embeddingType is a wildcard pattern on the embedding type (default *).
- HTTP
- curl
POST {{baseUrl}}/api/query/embeddings
Authorization: Bearer {{accessToken}}
Accept: application/vnd.hxcs.v1+json
Content-Type: application/json
{
"repositoryId": "default",
"vector": [0.0123, -0.0456, 0.0789, ...],
"embeddingType": "*",
"chunkFTS": "agreement",
"query": "SELECT * FROM SysContent WHERE sys_primaryType = 'SysFile'",
"limit": 10
}
curl -X POST "$BASE_URL/api/query/embeddings" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.hxcs.v1+json" \
-H "Content-Type: application/json" \
--data @- <<'JSON'
{
"repositoryId": "default",
"vector": [0.0123, -0.0456, 0.0789, ...],
"embeddingType": "*",
"chunkFTS": "agreement",
"query": "SELECT * FROM SysContent WHERE sys_primaryType = 'SysFile'",
"limit": 10
}
JSON
An embeddingType pattern that matches nothing (e.g. "nomatch*") returns 200 with "count": 0.
Look Up an Embedding by ID
There are two ways to retrieve embeddings:
- Vector search (POST /api/query/embeddings) — supply a query vector and get back the most similar chunks ranked by score. This is the primary API for semantic search and RAG workflows. The vector must have the index's dimension (1024 for
cohere.embed-multilingual-v3in the tested environment); any other length returns400 "Query vector has invalid dimension: N. Dimension should be: 1024". - Direct lookup (below) — retrieve a specific embedding by its document ID and embedding ID, when you already know which chunk you want (for example, the
sysembed_docIdandsysembed_idof a vector-search hit).
- HTTP
- curl
GET {{baseUrl}}/api/documents/{docId}/embedding/{embeddingId}
Authorization: Bearer {{accessToken}}
Accept: application/vnd.hxcs.v1+json
curl -X GET "$BASE_URL/api/documents/{docId}/embedding/{embeddingId}" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.hxcs.v1+json"
Embedding Object
A real response shape, with the text replaced and the 1024-element vector shortened:
{
"sysembed_id": "7c1e2a90-1111-5aaa-8bbb-000000000001",
"sysembed_docId": "3f6d0b4e-2222-4ccc-9ddd-000000000002",
"sysembed_type": "cohere.embed-multilingual-v3",
"sysembed_vector": [0.03161621, 0.044891357, -0.022125244, ...],
"sysembed_text": "## Section heading from the source document",
"sysembed_blobref": null,
"sysembed_location": {
"text": { "page": 1, "paragraph": 0 },
"position": { "left": 448, "top": 260, "right": 712, "bottom": 319 },
"timestamp": null,
"spreadsheet": null
},
"sysembed_score": 0.0
}
Response Fields
| Field | Type | Description |
|---|---|---|
sysembed_id | string | Unique identifier for this embedding |
sysembed_docId | string | The document this embedding belongs to |
sysembed_text | string | Text label for the vector, or the original chunk text for text embeddings. Omitted when not set. |
sysembed_type | string | Label for the embedding type, usually the model name (e.g. cohere.embed-multilingual-v3). It ensures semantic queries compare vectors of the same type. |
sysembed_vector | number[] | The vector as a list of floating point numbers (1024 values for cohere.embed-multilingual-v3) |
sysembed_location | object | Where in the source document this chunk came from (see Location Types below). Omitted when not set. |
sysembed_blobref | string | Blob label for the embedding; returned as null when not set |
sysembed_score | number | Similarity score. In vector search it ranks the hit (e.g. 1.0 for an identical vector, 0.73 for a close one). With chunkFTS, it is a fused rank score instead. A direct lookup is a filter-only match, so it returns 0.0. |
Location Types
The sysembed_location object always has four keys: text, position, timestamp and spreadsheet. Keys that don't apply are returned as null. More than one key can be populated: a chunk extracted from a page image, for example, has both text (page and paragraph) and position (bounding box), as in the example above.
| Content Type | Location Fields | Example |
|---|---|---|
| Text documents | text.page (integer), text.paragraph (integer) | {"text": {"page": 2, "paragraph": 5}, "position": null, "timestamp": null, "spreadsheet": null} |
| Images / page regions | position.left, position.top, position.right, position.bottom (integers) | {"text": {"page": 1, "paragraph": 0}, "position": {"left": 448, "top": 260, "right": 712, "bottom": 319}, "timestamp": null, "spreadsheet": null} |
| Spreadsheets | spreadsheet.sheet (string), spreadsheet.row (integer), spreadsheet.column (integer) | {"spreadsheet": {"sheet": "Sheet1", "row": 3, "column": 1}, ...} |
| Video/Audio | timestamp.start, timestamp.end (decimal numbers) | Seconds within the media file |
Lookup Errors
1. Unknown embedding ID
GET /api/documents/3f6d0b4e-2222-4ccc-9ddd-000000000002/embedding/nope
# 404
{"entity-type":"exception","status":404,"message":"Could not find embedding with id nope for document 3f6d0b4e-2222-4ccc-9ddd-000000000002"}
2. Path-traversal-like ID
GET /api/documents/{docId}/embedding/..%2F..
# 403 text/html — rejected by the gateway before it reaches the API
<html><head><title>403 Forbidden</title></head>...
Error Responses
| Status | Description |
|---|---|
403 | In the tested environment, the gateway blocks IDs containing encoded traversal sequences such as ..%2F.., with an HTML 403 Forbidden page. |
401 | Not authenticated (empty body) |
404 | Document or embedding not found, e.g. "Could not find embedding with id {embeddingId} for document {docId}". Also returned when the caller has no access to the document. |
Use the Results in a RAG Workflow
For Retrieval-Augmented Generation: (1) use POST /api/query/embeddings with a query vector to find the most relevant chunks, (2) use the sysembed_text from each result as context for your LLM, and (3) use sysembed_docId + sysembed_location to cite the source document and page in the response. If you need the full document, use the Download APIs with the sysembed_docId.
Next Steps
- Querying with HXQL: build the document filters used in
query - Downloading Content: fetch the source document of a hit
- API Reference: every endpoint and schema
- Error Reference: error formats and status codes