Compare commits

...
5 Commits
Author SHA1 Message Date
apairon 21472bdc72 Schema: ngram.sizes auf 2–10 begrenzen, combinedCache ergänzen
continuous-integration/drone/push Build is passing
Die Gesamtabnahme fand zwei Lücken im Config-Vertrag:

- ngram.sizes erlaubte minimum 1 und hatte kein maximum; der Server akzeptiert
  nur 2–10 (collection_setup.go) — das Schema ließ also Configs durch, die beim
  Projektload scheitern.
- combinedCache fehlte komplett, obwohl Server (models.CombinedCacheConfig) und
  Doku den Block als Projekt-YAML führen; additionalProperties:false lehnte
  gültige, dokumentierte Configs ab.

Ergänzt in collection.schema.json und index.d.ts (CombinedCacheConfig mit
enabled/ttl/maxEntries). Die Demo-Collection ngram-stopwords.yml enthält jetzt
eine Combined-Config mit combinedCache, sodass validate:schemas den Block
abdeckt. Gegenprobe per AJV: sizes [1] und [11] werden abgelehnt, [2,3] und
combinedCache akzeptiert.
2026-10-19 22:34:56 +00:00
apairon 9e12a8e9c3 Schema: ngram.stopwords, queryFilter und inMemory ergänzen
continuous-integration/drone/push Build is passing
Der Config-Vertrag kannte nur sizes und normalize. Damit lehnte
`validate:schemas` jedes Projekt ab, das die bereits im Server vorhandenen
ngram-Optionen nutzt (additionalProperties: false), und die generierte
Schema-Referenz konnte sie nie zeigen.

Ergänzt in collection.schema.json und index.d.ts:

- stopwords: { presets?: string[], words?: string[] }
- queryFilter: { minMatchRatio?: 0..1 }
- inMemory: { enabled?, maxDocs?, maxMemoryMB? }

plus demo-api/collections/ngram-stopwords.yml als Validierungs-Fixture; ohne
Schema-Erweiterung schlägt validate:schemas dort mit "must NOT have additional
properties" fehl.
2026-10-19 20:48:10 +00:00
apairon 3bf8055539 docs(example): Mehrfach-Duration (1m30s) als echter Wert im embedding-Beispiel
continuous-integration/drone/push Build is passing
Damit übt npm test den (...)+-Zweig des duration-Musters aus statt ihn nur zu
kommentieren (Review-Nachbefund, low).
2026-10-17 14:08:39 +00:00
apairon 1233bd393d fix(schema): latencyWindow ist ein String, duration-Muster folgt der Go-Grammatik
Review-Nachbefunde am neuen embedding-Schema:
- latencyWindow war als integer/number deklariert, ist in Go aber ein String
  (ConfigEmbeddingGroup.OnSingleQuery.LatencyWindow, per Atoi geprüft) — Schema
  und index.d.ts auf digits-String (>0) umgestellt.
- duration-Pattern erlaubte nur eine Einheit und akzeptierte Unsinn wie "1ss":
  jetzt (Zahl+Einheit)+, damit auch 1m30s/1h1m/1.5h gelten und 1ss/8/abc fallen.
- Beispiel examples/server/embedding.yml übt latencyWindow und Mehrfach-Einheiten
  aus (npm test: 5 YAML gegen 17 Schemas, tsc --noEmit --strict grün).
2026-10-17 14:07:30 +00:00
apairon 5a45c4dd3f feat(types): Embedding-Abschnitt der Server-Config (Provider, Failover-Gruppen, goldener Pfad) + Status-Payloads
index.d.ts: EmbeddingProviderConfig/EmbeddingGroupConfig (onSingleQuery, onBulkWrite,
health), EmbeddingConfig für config.server().embedding (Roh-JSON von config.Server.
Embedding) sowie die Status-Payloads von GET …/admin/embedding/status (Projekt- und
Server-Scope, Zustände live|degraded|quarantined|offline, Breaker closed|open|half-open).
VectorConfig.provider dokumentiert jetzt, dass dort auch ein Gruppenname stehen darf.

schemas/config/embedding.schema.json: validierbares Schema des Server-Abschnitts
(Enums für Policies/Distributionen, Go-Duration-Muster, Pflichtfelder, keine
erfundenen Properties); Beispiel examples/server/embedding.yml, per
scripts/validate-schemas.mjs mitgeprüft (npm test: 5 YAML gegen 17 Schemas).

Quelle der Wahrheit: internal/config/server.go + internal/config/embedding_group.go
(tibi-server). Typecheck: tsc --noEmit --strict über index.d.ts grün.
2026-10-17 14:00:53 +00:00
6 changed files with 564 additions and 1 deletions

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
name: ngram-stopwords
uploadPath: ../media/ngram-stopwords
permissions: !include global/test-permissions.yml
fields:
- name: title
type: string
meta:
label:
de: Titel
en: Title
widget: "text"
search:
- name: fuzzy
mode: ngram
fields: [title]
autoRegenerate: true
ngram:
sizes: [2, 3]
normalize:
use: [german-umlauts]
stopwords:
presets: [de, en]
words: [zum, zur]
queryFilter:
minMatchRatio: 0.4
inMemory:
enabled: true
maxDocs: 150000
maxMemoryMB: 512
- name: combined
mode: combined
rrf:
k: 60
topK: 100
weights:
fuzzy: 1.0
combinedCache:
enabled: true
ttl: 45s
maxEntries: 1000
+49
View File
@@ -0,0 +1,49 @@
# Beispiel für den embedding-Abschnitt der Server-config.yml
# (tibi-server, server.embedding) — validiert gegen schemas/config/embedding.schema.json.
providers:
- name: bge-m3-external
type: flagembedding
model: BAAI/bge-m3
dimensions: 1024
baseURL: https://embed.intern/embed
timeoutPerAttempt: 8s
slowThreshold: 1500ms
- name: bge-m3-local
type: native
model: BAAI/bge-m3
dimensions: 1024
modelPath: /models/bge-m3
timeoutPerAttempt: 60s
groups:
- name: bge-m3-main
description: Bevorzugt extern, sonst lokal, kehrt automatisch zurück.
members:
- bge-m3-external
- bge-m3-local
onSingleQuery:
policy: prefer-in-order
preferredOrder:
- bge-m3-external
- bge-m3-local
timeoutPerAttempt: 8s
timeoutTotal: 1m30s # Mehrfach-Einheiten sind erlaubt (Go time.ParseDuration)
autoFailback: true
retryAttempts: 1
retryBackoff: 200ms
retryBackoffMax: 2s
latencyWindow: "100"
onBulkWrite:
distribution: lowest-latency
maxConcurrentTotal: 6
maxConcurrentPerEndpoint: 2
timeoutPerAttempt: 15s
timeoutTotal: 120s
health:
checkInterval: 30s
checkTimeout: 3s
unhealthyAfterFailures: 3
healthyAfterSuccesses: 2
cooldownAfterUnhealthy: 60s
goldenPath: testdata/embedding-golden
Vendored
+150
View File
@@ -81,9 +81,33 @@ declare global {
eval?: string
}
export interface NgramStopwordsConfig {
presets?: string[]
words?: string[]
}
export interface NgramQueryFilterConfig {
minMatchRatio?: number
}
export interface CombinedCacheConfig {
enabled?: boolean
ttl?: string
maxEntries?: number
}
export interface NgramInMemoryConfig {
enabled?: boolean
maxDocs?: number
maxMemoryMB?: number
}
export interface NgramConfig {
sizes?: number[]
normalize?: NormalizeConfig
stopwords?: NgramStopwordsConfig
queryFilter?: NgramQueryFilterConfig
inMemory?: NgramInMemoryConfig
}
export interface RrfConfig {
@@ -109,6 +133,12 @@ declare global {
}
export interface VectorConfig {
/**
* Name of the embedding provider (server config: `embedding.providers`) or
* of an embedding failover group (`embedding.groups`). Groups route across
* their members (policy, breaker, bulk sharding) and look like one provider
* to the collection.
*/
provider: string
documentPrefix?: string
queryPrefix?: string
@@ -169,6 +199,8 @@ declare global {
filter?: { [key: string]: any }
/** Ngram search configuration */
ngram?: NgramConfig
/** Result cache for combined searches */
combinedCache?: CombinedCacheConfig
/** Regex search configuration */
regex?: RegexConfig
/** Custom scoring for eval/filter search */
@@ -735,6 +767,122 @@ declare global {
job?: JobConfig
}
/**
* Embedding provider as exposed by `config.server()` — mirror of
* `ConfigEmbeddingProvider` (secrets and modelPath are stripped from the JSON).
*/
export interface EmbeddingProviderConfig {
name: string
/** "native" (ONNX in-process) or "flagembedding" (external HTTP endpoint) */
type: string
model?: string
dimensions?: number
baseURL?: string
documentPrefix?: string
queryPrefix?: string
/** flagembedding: HTTP bound; native: per-call bound (entry-only) */
timeoutPerAttempt?: string
/** Group-only metric knob (feeds lowest-latency routing) */
slowThreshold?: string
/** Weights/build tag; part of the model identity used by groups */
revision?: string
/** false = endpoint/model has no sparse vectors (native skips the sparse session) */
sparse?: boolean
}
/** Single-query routing of an embedding group (`onSingleQuery`). */
export interface EmbeddingOnSingleQueryConfig {
policy?:
| "prefer-in-order"
| "fastest-wins"
| "round-robin"
| "lowest-latency"
| "weighted"
/** Comma-separated names (YAML list is joined with commas before marshalling) */
preferredOrder?: string
timeoutPerAttempt?: string
timeoutTotal?: string
autoFailback?: boolean
raceDelay?: string
raceMaxFanout?: number
weights?: number[]
/** Digits as string (Go field is a string, validated with Atoi; must be > 0) */
latencyWindow?: string
retryAttempts?: number
retryBackoff?: string
retryBackoffMax?: string
hedgeAfterSlow?: string
}
/** Bulk-write sharding of an embedding group (`onBulkWrite`). */
export interface EmbeddingOnBulkWriteConfig {
distribution?: "round-robin" | "lowest-latency" | "weighted"
maxConcurrentTotal?: number
maxConcurrentPerEndpoint?: number
timeoutPerAttempt?: string
timeoutTotal?: string
}
/** Health/breaker timing of an embedding group (`health`). */
export interface EmbeddingHealthConfig {
/** > 0 also enables late join/recovery of quarantined members */
checkInterval?: string
checkTimeout?: string
unhealthyAfterFailures?: number
healthyAfterSuccesses?: number
cooldownAfterUnhealthy?: string
}
/** Embedding failover group as exposed by `config.server()`. */
export interface EmbeddingGroupConfig {
name: string
description?: string
members: string[]
onSingleQuery?: EmbeddingOnSingleQueryConfig
onBulkWrite?: EmbeddingOnBulkWriteConfig
health?: EmbeddingHealthConfig
}
/** Embedding section as exposed by `config.server()`. */
export interface EmbeddingConfig {
providers?: EmbeddingProviderConfig[]
groups?: EmbeddingGroupConfig[]
/** Directory of golden verification files (default testdata/embedding-golden) */
goldenPath?: string
}
/**
* Member state from `GET …/admin/embedding/status` (server scope keeps
* `lastError`, project scope redacts it).
*/
export interface EmbeddingMemberHealth {
state: "live" | "degraded" | "quarantined" | "offline"
breaker: "closed" | "open" | "half-open"
lastError?: string
latencyMs?: number
successes?: number
failures?: number
slowCount?: number
}
/** Group state from `GET …/admin/embedding/status`. */
export interface EmbeddingGroupHealth {
/** null while no member is live */
referenceMember: string | null
members: { [member: string]: EmbeddingMemberHealth }
}
/** Project-scope payload: `{ namespace, health: { groups } }`. */
export interface EmbeddingHealthProjectResponse {
namespace: string
health: { groups: { [group: string]: EmbeddingGroupHealth } }
}
/** Server-scope payload: `{ groups }`. */
export interface EmbeddingHealthServerResponse {
groups: { [group: string]: EmbeddingGroupHealth }
}
interface ConfigPackage {
/**
* get current namespace string
@@ -773,6 +921,8 @@ declare global {
maxTokensPerRequest?: number
}[]
}
/** Raw JSON of config.Server.Embedding (apiKey/modelPath stripped, JSON tags decide) */
embedding?: EmbeddingConfig
audit?: {
enabled: boolean
defaultTTL: string
+64 -1
View File
@@ -321,7 +321,8 @@
"type": "array",
"items": {
"type": "integer",
"minimum": 1
"minimum": 2,
"maximum": 10
}
},
"normalize": {
@@ -338,6 +339,68 @@
"type": "string"
}
}
},
"stopwords": {
"type": "object",
"additionalProperties": false,
"properties": {
"presets": {
"type": "array",
"items": {
"type": "string"
}
},
"words": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"queryFilter": {
"type": "object",
"additionalProperties": false,
"properties": {
"minMatchRatio": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
},
"inMemory": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"maxDocs": {
"type": "integer",
"minimum": 0
},
"maxMemoryMB": {
"type": "integer",
"minimum": 0
}
}
}
}
},
"combinedCache": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"ttl": {
"type": "string"
},
"maxEntries": {
"type": "integer",
"minimum": 0
}
}
},
+255
View File
@@ -0,0 +1,255 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "embedding.schema.json",
"title": "Tibi server config — embedding section (server-level, not project config)",
"description": "Der Abschnitt `embedding` aus der Server-config.yml (server.embedding): Provider + Failover-Gruppen + Gold-Datei-Pfad. Quelle der Wahrheit: internal/config/server.go und internal/config/embedding_group.go im tibi-server.",
"type": "object",
"additionalProperties": false,
"properties": {
"providers": {
"type": "array",
"items": {
"$ref": "#/definitions/provider"
}
},
"groups": {
"type": "array",
"items": {
"$ref": "#/definitions/group"
}
},
"goldenPath": {
"type": "string",
"description": "Verzeichnis der Gold-Dateien für die Mitglieds-Prüfung (Vorgabe: testdata/embedding-golden relativ zum Arbeitsverzeichnis). Erzeugen mit cmd/embed-probe-golden. Ohne verankerte Gold-Datei prüft die Gruppe nur gegen ihre Referenz."
}
},
"definitions": {
"duration": {
"type": "string",
"description": "Go-Duration (time.ParseDuration), z. B. 120s, 800ms, 25s, 1m30s, 1.5h",
"pattern": "^([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$"
},
"provider": {
"type": "object",
"additionalProperties": false,
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "Provider-Name; Sammlungen referenzieren ihn (oder eine Gruppe) über vector.provider."
},
"type": {
"type": "string",
"enum": [
"native",
"flagembedding"
],
"description": "native = ONNX im Prozess (braucht -tags cgo_embedding), flagembedding = externer HTTP-Endpunkt. Ohne Angabe: native."
},
"model": {
"type": "string"
},
"modelPath": {
"type": "string",
"description": "ONNX-Modellverzeichnis (type: native)."
},
"dimensions": {
"type": "integer",
"minimum": 1
},
"baseURL": {
"type": "string",
"description": "Embedding-Endpunkt (type: flagembedding)."
},
"apiKey": {
"type": "string"
},
"documentPrefix": {
"type": "string"
},
"queryPrefix": {
"type": "string"
},
"timeoutPerAttempt": {
"$ref": "#/definitions/duration",
"description": "flagembedding: HTTP-Timeout (Vorgabe 120s); native: Schranke je Embed-Aufruf (nur am Aufruf-/Stufen-Eintritt wirksam, ONNX-Runs sind nicht unterbrechbar). Ungültige oder nicht-positive Werte werden ignoriert (Warnung im Log)."
},
"slowThreshold": {
"$ref": "#/definitions/duration",
"description": "Nur gruppenwirksam: langsamere Aufrufe zählen für lowest-latency als langsam."
},
"revision": {
"type": "string",
"description": "Gewichts-/Build-Tag (Datei-Hash, Quantisierung); fließt in die Modell-Identität ein (Gruppen-Prüfung, Query-Cache)."
},
"sparse": {
"type": "boolean",
"description": "false = Endpunkt/Modell liefert keine Sparse-Vektoren: native lädt die Sparse-Session nicht, flagembedding schweigt die einmalige Dense-only-Warnung; in einer Gruppe lehnt die Prüfung abweichende Sparse-Formen ab."
}
}
},
"group": {
"type": "object",
"additionalProperties": false,
"required": [
"name",
"members"
],
"properties": {
"name": {
"type": "string",
"description": "Gruppenname; darf kein Provider-Name sein und darf nur einmal vorkommen."
},
"description": {
"type": "string"
},
"members": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {
"type": "string"
},
"description": "Provider-Namen der Gruppe. Alle Mitglieder müssen dieselbe Modell-Identität liefern (Dimensionen, Sparse-Unterstützung, revision)."
},
"onSingleQuery": {
"type": "object",
"additionalProperties": false,
"properties": {
"policy": {
"type": "string",
"enum": [
"prefer-in-order",
"fastest-wins",
"round-robin",
"lowest-latency",
"weighted"
]
},
"preferredOrder": {
"description": "Bevorzugte Reihenfolge; als Komma-String oder YAML-Liste. Mitglieder, die fehlen, hängen in Config-Reihenfolge hinten an.",
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"timeoutPerAttempt": {
"$ref": "#/definitions/duration",
"description": "Schranke je Versuch. Der bevorzugte Member läuft genau darin; Ausweich-Member bekommen max(Provider-Timeout, dieser Wert)."
},
"timeoutTotal": {
"$ref": "#/definitions/duration",
"description": "Gesamt-Deadline je Aufruf; muss >= timeoutPerAttempt sein."
},
"autoFailback": {
"type": "boolean",
"description": "true (Vorgabe) = jeder Aufruf beginnt wieder beim bevorzugten Mitglied; false = bleibt beim letzten Gewinner."
},
"raceDelay": {
"$ref": "#/definitions/duration",
"description": "Nur policy fastest-wins: Staffelung zwischen den Teilnehmern."
},
"raceMaxFanout": {
"type": "integer",
"minimum": 0,
"description": "Nur policy fastest-wins: maximale parallele Teilnehmer (0 = alle)."
},
"weights": {
"type": "array",
"items": {
"type": "integer",
"minimum": 0
},
"description": "Nur policy weighted: relative Gewichte je Mitglied, positional zur members-Liste."
},
"latencyWindow": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "Fenster für die EWMA-Latenz (lowest-latency) als Ziffern-String (> 0); Go-Feld ist ein String, der per Atoi geprüft wird."
},
"retryAttempts": {
"type": "integer",
"minimum": 0
},
"retryBackoff": {
"$ref": "#/definitions/duration"
},
"retryBackoffMax": {
"$ref": "#/definitions/duration"
},
"hedgeAfterSlow": {
"$ref": "#/definitions/duration",
"description": "prefer-in-order: nach dieser Dauer starten die übrigen Mitglieder parallel (Hedge)."
}
}
},
"onBulkWrite": {
"type": "object",
"additionalProperties": false,
"properties": {
"distribution": {
"type": "string",
"enum": [
"round-robin",
"lowest-latency",
"weighted"
],
"description": "Verteilung der Batch-Ranges über die Mitglieder; ohne Angabe kein Sharding (serieller Schreibpfad). Jeder Range bleibt ganz auf einem Mitglied."
},
"maxConcurrentTotal": {
"type": "integer",
"minimum": 0
},
"maxConcurrentPerEndpoint": {
"type": "integer",
"minimum": 0,
"description": "Pro Mitglied; lokale (native) Mitglieder sind auf 1 begrenzt."
},
"timeoutPerAttempt": {
"$ref": "#/definitions/duration",
"description": "Bulk-Schranke je Versuch; pro Mitglied gilt max(Provider-Timeout, dieser Wert)."
},
"timeoutTotal": {
"$ref": "#/definitions/duration"
}
}
},
"health": {
"type": "object",
"additionalProperties": false,
"properties": {
"checkInterval": {
"$ref": "#/definitions/duration",
"description": "Abstand der Health-Probes. > 0 aktiviert zusätzlich den Late-Join: quarantänisierte Mitglieder (und offline Gruppen) werden gegen die aktuelle Referenz neu geprüft und kommen zurück, sobald sie passen. 0 = nur Neustart hilft."
},
"checkTimeout": {
"$ref": "#/definitions/duration",
"description": "Schranke je Probe-Embed; die Rundenschranke beim Start skaliert mit der Zahl der Probe-Texte."
},
"unhealthyAfterFailures": {
"type": "integer",
"minimum": 0
},
"healthyAfterSuccesses": {
"type": "integer",
"minimum": 0
},
"cooldownAfterUnhealthy": {
"$ref": "#/definitions/duration",
"description": "Pause, bevor ein offenes Mitglied wieder halb geöffnet wird (danach genügt Verkehr, kein Probe-Lauf)."
}
}
}
}
}
}
}
+3
View File
@@ -64,6 +64,9 @@ const validations = [
{ schema: "job.schema.json", pattern: "demo-api/jobs/*.y*ml" },
{ schema: "action.schema.json", pattern: "demo-api/actions/*.y*ml" },
{ schema: "asset.schema.json", pattern: "demo-api/assets/*.y*ml" },
// Server-level Abschnitt (config.yml des tibi-servers), Beispiel getrennt
// vom Projekt-Demo unter examples/server/:
{ schema: "embedding.schema.json", pattern: "examples/server/*.y*ml" },
]
let hasErrors = false