Azure / Azure/data-api-builder

[Enh]: Embedding Phase 1 & 2: Internal subsystem for embedding text.

Open
#3,103 3 comments 1 reaction 2 assignees Claimed by @JerryNixon View on GitHub
2.1 has-pr semantic
Dominant language
C#
Stars
1.5k
Forks
370
Avg merge
3d 17h
Merged PRs (30d)
8

Description

## What

Add a runtime embedding subsystem to DAB.

The subsystem converts text into embeddings through a configured provider. It can be used internally by later features and exposed through an optional public `/embed` endpoint.

Phase 1 includes embedding cache.

## Why

Several DAB features need embeddings, including stored procedure parameter embedding and future semantic operations.

Phase 1 creates the shared subsystem first so later work doesn't need to implement provider calls, caching, chunking, validation, or telemetry again.

## Scope

Phase 1 includes:

```text
runtime.embeddings configuration
AzureOpenAI provider
OpenAI provider
internal embedding service
single text embedding
batch text embedding
optional public /embed endpoint
chunking
embedding cache
startup validation
dab validate support
dab configure support
logs and OTEL traces
health check integration
```

Phase 1 excludes:

```text
stored procedure auto-embed (Phase 3)
semantic search query operators
automatic table or field embedding
offline indexing
vector schema management
agent-facing semantic search tools
managed identity
custom providers
native vector type support
```

## Runtime configuration

```json
{
"runtime": {
"embeddings": {
"enabled": true,
"provider": "AzureOpenAI",
"base-url": "@env('EMBEDDING_BASE_URL')",
"api-key": "@env('EMBEDDING_API_KEY')",
"model": "@env('EMBEDDING_MODEL')",
"api-version": "2024-02-01",
"timeout-seconds": 86400,
"chunking": {
"enabled": false,
"size-chars": 800,
"overlap-chars": 100
},
"cache": {
"enabled": false,
"ttl-seconds": 86400,
"level": "L1L2"
},
"endpoint": {
"enabled": false,
"path": "/embed",
"roles": [ "authenticated" ]
}
}
}
}
```

## Defaults

```text
runtime.embeddings.enabled: false
runtime.embeddings.timeout-seconds: 86400
runtime.embeddings.chunking.enabled: false
runtime.embeddings.chunking.size-chars: 800
runtime.embeddings.chunking.overlap-chars: 100
runtime.embeddings.cache.enabled: false
runtime.embeddings.cache.ttl-seconds: 86400
runtime.embeddings.cache.level: L1L2
runtime.embeddings.endpoint.enabled: false
runtime.embeddings.endpoint.path: /embed
```

`runtime.embeddings.provider`, `api-key`, and `model` are required when embeddings are enabled.

`endpoint.roles` is required only when the public endpoint is enabled.

## Providers

Phase 1 supports these provider enum values:

```text
AzureOpenAI
OpenAI
```

Provider enum values are case-sensitive.

Valid:

```json
"provider": "AzureOpenAI"
```

```json
"provider": "OpenAI"
```

Invalid:

```json
"provider": "azure-openai"
```

```json
"provider": "openai"
```

Future providers are out of scope for Phase 1.

## Provider settings

Use `model` for both providers.

For Azure OpenAI, `model` represents the deployment or model identifier required by the implementation.

`dimensions` is not part of Phase 1.

Provider-specific validation:

```text
AzureOpenAI requires base-url
AzureOpenAI requires api-key
AzureOpenAI requires model
AzureOpenAI requires api-version

OpenAI requires api-key
OpenAI requires model
OpenAI does not require base-url if the implementation has a safe default
OpenAI must reject api-version
```

Managed identity is out of scope for Phase 1.

## Internal embedding service

The internal embedding subsystem is available when `runtime.embeddings.enabled` is true and configuration is valid.

The internal service supports:

```text
single text embedding
batch text embedding
chunking
cache lookup
cache write
provider timeout
logs and OTEL traces
```

The internal service is not role-aware.

Calling features are responsible for authentication, authorization, and request validation before using the embedding subsystem.

## Public endpoint

Phase 1 includes an optional public `/embed` endpoint.

The endpoint exposes the same embedding subsystem used internally by later phases.

Disabling the endpoint does not disable the internal embedding subsystem.

`/embed` is enabled only when:

```json
{
"runtime": {
"embeddings": {
"enabled": true,
"endpoint": {
"enabled": true,
"roles": [ "authenticated" ]
}
}
}
}
```

`anonymous` is allowed only when explicitly configured:

```json
{
"runtime": {
"embeddings": {
"endpoint": {
"enabled": true,
"roles": [ "anonymous" ]
}
}
}
}
```

`/embed` is `POST` only.

## Endpoint request contract

The endpoint accepts an array of input objects.

Each object has:

```text
key
text
```

Example:

```json
[
{
"key": "document-1",
"text": "Long document text here that may exceed chunk size."
},
{
"key": "document-2",
"text": "Another document."
}
]
```

Rules:

```text
key is required
key must not be empty
key must be unique within the request
text is required
text must not be null
text must not be empty
text must not be whitespace-only
```

Validation trims `text` only to decide whether the value is embeddable.

When embedding runs, DAB sends the original `text` value to the provider.

```text
null text -> 400 Bad Request
"" -> 400 Bad Request
" " -> 400 Bad Request
"cat" -> embed "cat"
" cat " -> embed " cat "
```

## Endpoint response contract

The response is always an array.

The response preserves input order.

Each response item includes the original `key` and a `data` array.

`data` is always an array because chunking can produce multiple embeddings.

Each `data` item includes a zero-based chunk `index` and a numeric embedding array.

Example:

```json
[
{
"key": "document-1",
"data": [
{
"index": 0,
"embedding": [0.123, 0.456]
},
{
"index": 1,
"embedding": [0.789, 0.101]
}
]
},
{
"key": "document-2",
"data": [
{
"index": 0,
"embedding": [0.111, 0.222]
}
]
}
]
```

When chunking is disabled, `data` still contains one item:

```json
[
{
"key": "document-1",
"data": [
{
"index": 0,
"embedding": [0.123, 0.456]
}
]
}
]
```

The response does not include:

```text
cache metadata
effective chunking settings
provider metadata
input text
```

## All-or-nothing behavior

`/embed` is all or nothing.

If any input item is invalid, no embeddings are generated.

If any provider call fails, the whole request fails.

If batching is used and one item fails, the whole request fails.

No partial success response in Phase 1.

## Chunking

Chunking is available in Phase 1.

`runtime.embeddings.chunking.enabled` controls whether DAB automatically chunks input by default.

Chunking capability exists even when `chunking.enabled` is false.

Defaults:

```text
chunking.enabled: false
chunking.size-chars: 800
chunking.overlap-chars: 100
```

Config:

```json
{
"runtime": {
"embeddings": {
"chunking": {
"enabled": false,
"size-chars": 800,
"overlap-chars": 100
}
}
}
}
```

Validation:

```text
chunking.enabled must be true or false
chunking.size-chars must be positive
chunking.overlap-chars must be zero or positive
chunking.overlap-chars must be less than chunking.size-chars
```

## Endpoint query overrides

`/embed` supports query string overrides for chunking.

```text
$chunking.enabled=true
$chunking.enabled=false
$chunking.size-chars=512
$chunking.overlap-chars=50
```

Examples:

```text
/embed?$chunking.enabled=false
/embed?$chunking.enabled=true&$chunking.size-chars=512&$chunking.overlap-chars=50
```

Overrides apply only to the current `/embed` request.

Overrides do not change runtime configuration.

Any caller authorized for `/embed` can use chunking overrides.

Runtime behavior:

```text
runtime chunking disabled + no override -> no chunking
runtime chunking disabled + $chunking.enabled=true -> chunking enabled for request
runtime chunking enabled + $chunking.enabled=false -> chunking disabled for request
```

## Cache

Phase 1 includes embedding cache.

Embedding cache is opt-in.

Config:

```json
{
"runtime": {
"embeddings": {
"cache": {
"enabled": false,
"ttl-seconds": 86400,
"level": "L1L2"
}
}
}
}
```

Defaults:

```text
cache.enabled: false
cache.ttl-seconds: 86400
cache.level: L1L2
```

Allowed cache levels:

```text
L1
L1L2
```

Embedding cache should reuse existing DAB cache configuration and infrastructure when possible.

Embedding cache configuration is scoped to embeddings. It does not change REST, GraphQL, or MCP response caching.

Behavior:

```text
cache.enabled false -> no embedding cache
cache.enabled true + cache.level L1 -> L1 only
cache.enabled true + cache.level L1L2 + Redis configured -> L1 and L2
cache.enabled true + cache.level L1L2 + Redis missing -> startup validation error
```

When `cache.enabled` is false, `cache.level` is not operational. If present, the schema still validates the value.

```text
cache.enabled false + cache.level missing -> valid
cache.enabled false + cache.level L1 -> valid
cache.enabled false + cache.level L1L2 -> valid, Redis not required
cache.enabled false + cache.level Banana -> schema validation error
```

## Cache key and value

Embedding cache operates per effective text sent to the provider.

If a document is split into five chunks, DAB can cache five independent entries.

Cache key is an opaque hash.

Cache hash material:

```text
effective text
```

Do not include:

```text
provider
model
base-url
api-key
api-version
request key
request order
role
endpoint path
chunking settings
```

Because chunking changes the effective text chunks, each chunk is cached by the chunk text sent to the provider.

Cache value:

```text
embedding numeric array
```

Do not store provider metadata in cache values.

Never expose or log the cache key or hash.

## Cache endpoint override

`/embed` supports a request-level cache override:

```text
$cache.enabled=false
```

This bypasses all embedding cache behavior for that request.

It skips cache reads.

It skips cache writes.

It does not change runtime configuration.

Behavior:

```text
runtime cache disabled + $cache.enabled=true -> cache remains disabled
runtime cache enabled + no override -> cache enabled
runtime cache enabled + $cache.enabled=false -> no cache read, no cache write
```

Any caller authorized for `/embed` can use cache bypass.

No separate role check.

No development-only restriction.

The `/embed` response does not include cache metadata.

Cache status can appear in logs and OTEL traces.

## Timeout behavior

`runtime.embeddings.timeout-seconds` controls provider timeout behavior.

Default:

```text
timeout-seconds: 86400
```

Validation:

```text
minimum: 1
maximum: 86400
```

`/embed` does not add a max item count setting.

Limits come from:

```text
existing DAB request-size limits
host limits
provider limits
timeout-seconds
```

## Error behavior

Invalid request:

```text
400 Bad Request
```

Provider failure:

```text
502 Bad Gateway
```

Embedding subsystem unavailable:

```text
503 Service Unavailable
```

Unexpected pipeline failure:

```text
500 Internal Server Error
```

Error responses should be useful, but must not expose secrets or payloads.

Responses may include:

```text
error category
provider status code
provider error code
sanitized provider message
correlation ID
```

Responses must not include:

```text
input text
embedding values
api key
authorization headers
full raw provider response body
cache key
cache key hash
```

## Validation

`dab validate` and runtime startup must validate embedding configuration.

Validation rules:

```text
embeddings disabled -> provider settings not required
embeddings enabled -> provider required
embeddings enabled -> api-key required
embeddings enabled -> model required
provider AzureOpenAI -> base-url required
provider AzureOpenAI -> api-version required
provider OpenAI -> api-version invalid
provider enum is case-sensitive
endpoint.enabled true -> endpoint.roles required
endpoint.enabled true -> endpoint.roles must not be empty
endpoint.path defaults to /embed
chunking.size-chars must be positive
chunking.overlap-chars must be zero or positive
chunking.overlap-chars must be less than chunking.size-chars
cache.enabled true + cache.level L1L2 -> Redis must be configured
timeout-seconds must be between 1 and 86400
```

## Health

Phase 1 integrates with DAB health checks.

Health should verify that the embedding subsystem is configured and provider access is functional when embeddings are enabled.

The public `/embed` endpoint does not need to be enabled for embedding health to run.

Health should not log or expose sample text, embeddings, secrets, or raw provider responses.

## Telemetry

Phase 1 must emit logs and OpenTelemetry traces.

For `/embed`, telemetry should include:

```text
one request-level span
one child span per input item
one child span per embedded chunk when chunking is used
```

Include in logs and traces:

```text
request item count
chunk count
embedding attempted
embedding succeeded or failed
duration
cache enabled
cache bypassed
cache level
cache hit
cache miss
cache write succeeded
cache write failed
provider
model
endpoint enabled
effective chunking enabled
effective chunk size
effective chunk overlap
provider status code when available
provider error code when available
sanitized provider message when available
correlation ID
provider request ID when available
```

Never include:

```text
input text
embedding values
cache key
cache key hash
api key
authorization headers
full raw provider response body
```

## CLI

Phase 1 includes `dab configure` support for every embedding setting.

Required commands:

```sh
dab configure --runtime.embeddings.enabled true
dab configure --runtime.embeddings.provider AzureOpenAI
dab configure --runtime.embeddings.base-url "@env('EMBEDDING_BASE_URL')"
dab configure --runtime.embeddings.api-key "@env('EMBEDDING_API_KEY')"
dab configure --runtime.embeddings.model "@env('EMBEDDING_MODEL')"
dab configure --runtime.embeddings.api-version "2024-02-01"
dab configure --runtime.embeddings.timeout-seconds 86400
dab configure --runtime.embeddings.chunking.enabled false
dab configure --runtime.embeddings.chunking.size-chars 800
dab configure --runtime.embeddings.chunking.overlap-chars 100
dab configure --runtime.embeddings.cache.enabled false
dab configure --runtime.embeddings.cache.ttl-seconds 86400
dab configure --runtime.embeddings.cache.level L1L2
dab configure --runtime.embeddings.endpoint.enabled false
dab configure --runtime.embeddings.endpoint.path "/embed"
dab configure --runtime.embeddings.endpoint.roles "authenticated"
```

`endpoint.roles` uses comma-separated CLI values.

```sh
dab configure --runtime.embeddings.endpoint.roles "anonymous,authenticated"
```

Config result:

```json
{
"runtime": {
"embeddings": {
"endpoint": {
"roles": [ "anonymous", "authenticated" ]
}
}
}
}
```

## Schema shape

Suggested schema shape:

```json
"embeddings": {
"type": "object",
"description": "Runtime embedding configuration.",
"additionalProperties": false,
"properties": {
"enabled": {
"$ref": "#/$defs/boolean-or-string",
"description": "Enable the embedding subsystem.",
"default": false
},
"provider": {
"type": "string",
"description": "Embedding provider.",
"enum": [ "AzureOpenAI", "OpenAI" ]
},
"base-url": {
"type": "string",
"description": "Provider base URL. Required for AzureOpenAI."
},
"api-key": {
"type": "string",
"description": "Provider API key."
},
"model": {
"type": "string",
"description": "Embedding model or deployment name."
},
"api-version": {
"type": "string",
"description": "Provider API version. Applies only to AzureOpenAI."
},
"timeout-seconds": {
"type": "integer",
"description": "Embedding provider timeout in seconds.",
"default": 86400,
"minimum": 1,
"maximum": 86400
},
"chunking": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"$ref": "#/$defs/boolean-or-string",
"description": "Enable automatic chunking by default.",
"default": false
},
"size-chars": {
"type": "integer",
"description": "Chunk size in characters.",
"default": 800,
"minimum": 1
},
"overlap-chars": {
"type": "integer",
"description": "Chunk overlap in characters.",
"default": 100,
"minimum": 0
}
}
},
"cache": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"$ref": "#/$defs/boolean-or-string",
"description": "Enable embedding cache.",
"default": false
},
"ttl-seconds": {
"type": "integer",
"description": "Embedding cache TTL in seconds.",
"default": 86400,
"minimum": 1
},
"level": {
"type": "string",
"description": "Embedding cache level.",
"enum": [ "L1", "L1L2" ],
"default": "L1L2"
}
}
},
"endpoint": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"$ref": "#/$defs/boolean-or-string",
"description": "Enable the public /embed endpoint.",
"default": false
},
"path": {
"type": "string",
"description": "Embedding endpoint path.",
"default": "/embed"
},
"roles": {
"type": "array",
"description": "Roles allowed to call the public embedding endpoint.",
"items": {
"type": "string"
}
}
}
}
}
}
```

## Acceptance criteria

`runtime.embeddings.enabled` defaults to `false`.

`runtime.embeddings.endpoint.enabled` defaults to `false`.

`runtime.embeddings.chunking.enabled` defaults to `false`.

`runtime.embeddings.cache.enabled` defaults to `false`.

`provider` supports only `AzureOpenAI` and `OpenAI`.

Provider enum values are case-sensitive.

`dimensions` is not supported.

AzureOpenAI requires `base-url`, `api-key`, `model`, and `api-version`.

OpenAI requires `api-key` and `model`.

OpenAI rejects `api-version`.

Managed identity is not supported in Phase 1.

The internal embedding subsystem works even when the public endpoint is disabled.

The public `/embed` endpoint is `POST` only.

The public `/embed` endpoint requires configured roles when enabled.

`anonymous` is allowed only when explicitly configured.

The `/embed` request is an array of objects with required unique `key` and required non-empty `text`.

Validation trims `text`, but embedding uses the original text.

Invalid input fails the whole `/embed` request.

Provider failure fails the whole `/embed` request.

No partial success response is returned.

Response order matches request order.

Response `data` is always an array.

Chunk indexes are zero-based.

`/embed` returns numeric embedding arrays.

`/embed` does not return cache metadata.

`/embed` does not return effective chunking settings.

Chunking query overrides are supported.

Cache bypass query override is supported.

`$cache.enabled=false` skips both cache reads and cache writes.

Embedding cache stores one entry per effective text sent to the provider.

Embedding cache keys are opaque hashes of effective text only.

Embedding cache values store only numeric embedding arrays.

`cache.level` supports `L1` and `L1L2`.

`cache.level` defaults to `L1L2`.

`L1L2` requires Redis when cache is enabled.

No fallback to L1 occurs when `L1L2` is selected and Redis is missing.

`timeout-seconds` defaults to `86400`.

`dab validate` validates embedding configuration.

Runtime startup validates embedding configuration.

Health checks integrate with the embedding subsystem.

Logs and OTEL traces include embedding, chunking, cache, duration, provider, and error metadata.

Logs and OTEL traces never include input text, embedding values, cache keys, API keys, raw provider responses, or authorization headers.

`dab configure` supports every embedding setting.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.