AI embeddings & configuration
Native vector embeddings for semantic search and similar-record panels — provider setup, scheduling, cost controls, privacy, and graceful degradation.
Overview
Signals can generate vector embeddings for core records so global search can blend semantic matches with full-text ranking, and so detail pages can surface similar rentals, accounts, and catalogue_items.
Embeddings are optional. If no API key is configured, pgvector is unavailable, or sync is paused, every consumer behaves exactly as it did before embeddings existed.
What gets embedded
Core: Rentals, Accounts, Catalogue items
Tier 1: Activities, Conversations (+ comments), Invoices, Credit notes, Purchase orders, Equipment test results, Repairs, Attachments
Each model implements the Embeddable contract via toEmbeddableText(). Content is hashed so status/total-only updates do not cost tokens.
Configuration
Supported labs
Lab (SIGNALS_AI_PROVIDER) |
Models | Default model | Dimensions |
|---|---|---|---|
openai |
text-embedding-3-small, text-embedding-3-large |
text-embedding-3-small |
1536 / 3072 |
gemini |
gemini-embedding-001 |
gemini-embedding-001 |
3072 |
mistral |
mistral-embed |
mistral-embed |
1024 |
openai_compatible |
any (you supply the id) | — | any (you supply the width) |
Anthropic and Moonshot are not offered — those labs expose no embeddings API. If stored settings still reference a removed lab, Signals falls back to the default provider/model and logs a warning.
Bring your own endpoint (openai_compatible)
The three hosted labs above all mean your record text crosses the network to a company you do not run. openai_compatible is the way out of that: point Signals at any server that speaks the OpenAI embeddings contract — POST {base_url}/embeddings taking {model, input, dimensions} and returning {data: [{embedding: [...]}], usage: {prompt_tokens}} — and it becomes the provider.
Run that server yourself and no record text leaves your infrastructure at all. This is the same posture the MCP server documents from the other direction: core Signals ships no model and phones nothing home, and embeddings are the one place it calls out — so this setting is what decides whether "out" means the public internet or a box on your own network.
Known-good hosts:
| Host | Base URL | Example model |
|---|---|---|
| Ollama | http://127.0.0.1:11434/v1 |
nomic-embed-text, mxbai-embed-large |
| vLLM | http://127.0.0.1:8000/v1 |
intfloat/e5-mistral-7b-instruct |
| LM Studio | http://127.0.0.1:1234/v1 |
text-embedding-nomic-embed-text-v1.5 |
| LocalAI | http://127.0.0.1:8080/v1 |
bert-embeddings |
| HF text-embeddings-inference | http://127.0.0.1:8080/v1 |
whatever the container serves |
| Any OpenAI-compatible gateway (LiteLLM, OpenRouter, Azure-fronted proxies) | your gateway URL | your gateway's model id |
Three things you must get right, because there is no catalogue to fall back on:
- Base URL — the root the
/embeddingspath is appended to. Include the version segment (/v1) if your server uses one. Required whenever this lab is selected. - Model — the id exactly as your endpoint names it. Never coerced to a default.
- Dimensions — the vector width your model actually returns. A mismatch does not error at write time; it silently poisons similarity ranking and the HNSW expression index, so check it against your model card.
The API key is optional for this lab. Most local servers ignore auth, so an empty key is treated as configured as long as the base URL is set (Signals sends a placeholder bearer token so the header stays well-formed). Set a key when your gateway needs one.
Changing the base URL or dimensions queues a full re-embed, exactly as changing lab or model does — all three define the vector space.
Signals talks to your endpoint over plain HTTP(S) with no certificate pinning. Keep local endpoints bound to loopback or a private network; if the endpoint is remote, terminate TLS.
Environment variables
| Variable | Purpose |
|---|---|
SIGNALS_AI_API_KEY |
Provider API key (Cloud: required; self-hosted: optional override path is settings). Optional for openai_compatible |
SIGNALS_AI_PROVIDER |
Lab key: openai, gemini, mistral, or openai_compatible |
SIGNALS_AI_EMBEDDING_MODEL |
Model id for that lab |
SIGNALS_AI_EMBEDDING_DIMENSIONS |
Optional dimension override (otherwise catalogue default); effectively required for openai_compatible |
SIGNALS_AI_BASE_URL |
Endpoint root for openai_compatible only — ignored by the hosted labs, whose URLs are fixed |
Resolution order for provider, model, dimensions, and base URL: env (when set) → admin settings store (self-hosted only) → config / catalogue default.
A fully self-contained local setup:
SIGNALS_AI_PROVIDER=openai_compatible
SIGNALS_AI_BASE_URL=http://127.0.0.1:11434/v1
SIGNALS_AI_EMBEDDING_MODEL=nomic-embed-text
SIGNALS_AI_EMBEDDING_DIMENSIONS=768
Self-hosted
Admin → Integrations → AI embeddings
- Encrypted API key (optional for
openai_compatible) - Lab / provider and embedding model (table above)
- Endpoint base URL and vector dimensions (shown only for
openai_compatible) - Full refresh cadence (weekly or monthly)
- Optional monthly token budget
- Usage log retention
Changing lab or model queues a full re-embed. Similarity queries always filter to the active provider/model.
SIGNALS_AI_PROVIDER, SIGNALS_AI_EMBEDDING_MODEL, and SIGNALS_AI_EMBEDDING_DIMENSIONS override the admin store when set (useful for ops / staging without changing DB settings).
Signals Cloud
Provider credentials and model selection are env-only (SIGNALS_AI_*). The AI settings form and usage totals are hidden in the admin UI.
TODO (cloud privacy): per-tenant opt-out flag and DPA line before cloud rollout — see Solo todo #960 privacy section.
Scheduling
| Job | When | Purpose |
|---|---|---|
| Daily sync | 01:30 UTC | Embed new/changed records since last run |
| Full refresh | Weekly or monthly (setting) | Re-validate hashes and backfill gaps |
Jobs run on the embeddings queue, batched via Bus::batch(), with rate-limit middleware for provider API calls.
Cost logging & budgets (self-hosted)
Every AI call through the shared client is logged to ai_usage_logs (function, provider, model, tokens). The AI tab shows totals for the last 7 days, last 30 days, this month, last month, and year-to-date.
An optional monthly token budget pauses sync and notifies admins when exceeded. Logs are pruned like action logs (ai-usage:prune).
PostgreSQL / pgvector
Production similarity ranking requires PostgreSQL with the pgvector extension. The migration enables the extension when available and creates a dimensionless vector column so provider/model swaps do not need a schema change. SQLite (and pgsql without the extension) keep a text column so the schema contract is identical. Without pgvector, similarity features stay off.
The HNSW cosine index is not created by the migration (HNSW requires a fixed dimension). When an AI model with known dimensions is activated — or a full re-embed runs — Signals creates an expression index (embedding::vector(N)) and similarity queries use the same cast so the index is used.
Privacy
Embeddable text is sent to the selected lab. Self-hosted admins see an informed-choice notice on the AI tab before enabling the feature, and that notice changes wording for openai_compatible because the destination is one you chose rather than one Signals picked. With a self-run endpoint the text never leaves your network; with a hosted lab or a third-party gateway, that operator receives it.
Graceful degradation
| Condition | Behaviour |
|---|---|
| No API key / disabled | Sync no-ops (one log line); search is full-text only; similar panels hidden |
| No pgvector | Same as above for similarity |
| Budget paused | Sync skipped until cleared |
| Record not yet embedded | Full-text / existing UI unchanged for that record |
API
Semantic search mirrors global search:
| Method | URL | Summary | Ability | Weight |
|---|---|---|---|---|
GET | /api/v1/search | Search across every registered searchable entity type | search:read | 2 |
GET | /api/v1/search/semantic | Semantic (vector) search across embeddable entity types | search:read | 4 |
Entity index endpoints accept q[similar_to_eq]=… (text, numeric id, or Class:id). Response DTOs for rentals, accounts, and catalogue_items include embedded_at and embedding_model (never the raw vector by default).
Adding a new embeddable model
- Implement
App\Contracts\Embeddableand useApp\Models\Traits\Embeddable - Override
toEmbeddableText() - Register the class in
App\Services\Embeddings\EmbeddableRegistry