Vector Embeddings
Automatic embedding generation for semantic search.
Overview
NornicDB automatically generates vector embeddings for nodes, enabling:
- Semantic similarity search
- Hybrid search (vector + text)
- Automatic relationship inference
- Clustering and categorization
Storage model: NornicDB-managed embeddings are stored on nodes in ChunkEmbeddings (the first chunk is the main embedding). Client-managed vectors (e.g. via Qdrant gRPC) are stored in NamedEmbeddings. See docs/architecture/embedding-search.md for details.
Embedding Providers
| Provider | Latency | Cost | Quality |
|---|---|---|---|
| Ollama (local) | 50-100ms | Free | High |
| OpenAI | 100-200ms | $$$ | Highest |
| OrcaRouter | Network-dependent | Usage-based | Model-dependent |
| Voyage AI | 100-200ms | $$$ | Highest |
| Local GGUF | 30-80ms | Free | High |
Configuration
Ollama (Recommended)
# Start Ollama
ollama serve
# Pull embedding model
ollama pull mxbai-embed-large
# Configure NornicDB
export NORNICDB_EMBEDDING_ENABLED=true
export NORNICDB_EMBEDDING_PROVIDER=ollama
export NORNICDB_EMBEDDING_API_URL=http://localhost:11434
export NORNICDB_EMBEDDING_MODEL=mxbai-embed-large
export NORNICDB_EMBEDDING_DIMENSIONS=1024OpenAI
export NORNICDB_EMBEDDING_PROVIDER=openai
export NORNICDB_EMBEDDING_API_KEY=sk-...
export NORNICDB_EMBEDDING_MODEL=text-embedding-3-smallOrcaRouter
OrcaRouter uses the existing generic embedding settings; there are no Orca-specific environment variables. Selecting orca supplies the API URL, model, and dimension defaults, while explicit generic values override them.
export NORNICDB_EMBEDDING_ENABLED=true
export NORNICDB_EMBEDDING_PROVIDER=orca
export NORNICDB_EMBEDDING_API_KEY=your-orca-key
# Defaults:
# NORNICDB_EMBEDDING_API_URL=https://api.orcarouter.ai
# NORNICDB_EMBEDDING_MODEL=openai/text-embedding-3-small
# NORNICDB_EMBEDDING_DIMENSIONS=1536Voyage AI
export NORNICDB_EMBEDDING_PROVIDER=voyage
export NORNICDB_EMBEDDING_API_KEY=pa-...
export NORNICDB_EMBEDDING_MODEL=voyage-4-large
export NORNICDB_EMBEDDING_DIMENSIONS=1024For provider-managed document chunking, add NORNICDB_EMBEDDING_MODE=contextualized and use voyage-context-4. See Voyage AI.
Local GGUF
export NORNICDB_EMBEDDING_PROVIDER=local
export NORNICDB_EMBEDDING_MODEL=mxbai-embed-large # filename stem under NORNICDB_MODELS_DIR
export NORNICDB_MODELS_DIR=/models # directory containing the .gguf files
export NORNICDB_EMBEDDING_GPU_LAYERS=-1 # auto-detectThe local provider resolves the model file as ${NORNICDB_MODELS_DIR}/${NORNICDB_EMBEDDING_MODEL}.gguf.
Which properties are embedded
By default, the embedding worker builds text from all node properties and node labels. Managed embedding metadata is stored internally (EmbedMeta) to avoid property namespace pollution. You can limit this so that only specific properties are used, or exclude others.
Use cases:
- Embed only one field (e.g.
content) so you don’t re-embed stored vectors or noisy fields. - Exclude internal or large fields (e.g.
internal_id,raw_html) from the text sent to the embedder.
YAML (in your config file under embedding_worker):
embedding_worker:
properties_include: [content] # Only these keys (empty = all)
properties_exclude: [internal_id, raw_html]
include_labels: true # Prepend labels (default: true)Environment variables:
# Embed only the "content" property (and labels)
export NORNICDB_EMBEDDING_PROPERTIES_INCLUDE=content
# Embed only content and title
export NORNICDB_EMBEDDING_PROPERTIES_INCLUDE=content,title
# Exclude internal fields (all other properties still embedded)
export NORNICDB_EMBEDDING_PROPERTIES_EXCLUDE=internal_id,raw_html
# Omit labels from embedding text (e.g. when using a single field)
export NORNICDB_EMBEDDING_INCLUDE_LABELS=falseIf properties_include is set, only those keys are used (and exclude still applies). If only properties_exclude is set, all properties except those keys are used. See Configuration Guide for full details.
Which nodes are automatically embedded
Managed embedding admits all user nodes by default. To limit the pending queue by node label, configure:
export NORNICDB_EMBEDDING_LABELS_INCLUDE=Document,Chunk
export NORNICDB_EMBEDDING_LABELS_EXCLUDE=AuditLog,JobThe YAML equivalents under embedding_worker are eligible_labels: [Document, Chunk] and excluded_labels: [AuditLog, Job]. At least one eligible label must match when the include list is nonempty; any excluded label blocks the node even if another label is eligible. An empty include list allows all labels, including unlabeled nodes. Filtering applies to new writes, explicit requeues, and startup pending-index refresh. Filtered nodes do not enter the provider queue or contribute to pending/processed counts.
Per-database overrides use db.nornic.embedding.labels.include and db.nornic.embedding.labels.exclude via PUT /admin/databases/{db}/config. They override the global lists independently and require a process restart to take effect; pendingRestart reports this state. Changing the filter removes disallowed pending markers on startup refresh, but does not remove vectors already stored on nodes. Rebuild or clear those embeddings explicitly if the existing vector index must reflect the new policy. NORNICDB_EMBEDDING_INCLUDE_LABELS is unrelated: it only controls whether labels appear in the text sent to the provider.
Automatic Embedding
On Node Creation
When a node is created, embeddings are generated automatically:
node, err := db.CreateNode(ctx, []string{"Document"}, map[string]any{
"title": "Machine Learning Basics",
"content": "An introduction to ML concepts...",
})
// Embedding is generated asynchronouslyOn Node Creation
result, err := db.ExecuteCypher(ctx, `CREATE (n:KnowledgeFact {
content: "User prefers dark mode for coding",
title: "Preference"
}) RETURN n`, nil)
// Embedding is generated automatically from content + titleEmbedding Queue
Embeddings are processed asynchronously for performance:
// Check queue status
status, _ := db.EmbeddingQueueStatus(ctx)
fmt.Printf("Pending: %d\n", status.Pending)
fmt.Printf("Processing: %d\n", status.Processing)Monitor Queue
curl http://localhost:7474/status | jq .embeddings{
"enabled": true,
"provider": "ollama",
"model": "mxbai-embed-large",
"pending": 42,
"processed_total": 15234,
"errors": 0
}Trigger Regeneration
# Regenerate managed embeddings in one database
curl -X POST 'http://localhost:7474/nornicdb/embed/trigger?regenerate=true&database=mydb' \
-H "Authorization: Bearer $TOKEN"The optional database parameter limits clearing to a concrete database and its vector index. Other databases retain their embeddings and the shared worker keeps running. The request requires database read/write access and administrator permission. Unknown databases return 404; composite databases must be targeted through a constituent. The 202 response includes the selected database. Regeneration runs asynchronously; worker statistics remain server-wide. Omitting database preserves the legacy regeneration behavior.
Clearing is per node and not atomic: the reset scans the selected nodes that currently hold managed vectors, clears each one through its own node update, and reports the number of nodes it actually cleared. A stored record the scan cannot decode, or that does not belong to the key holding it, fails the reset before any node is cleared; a node the reset cannot read or write stops it and returns the number already cleared together with the error, so a partial reset is never reported as complete (re-run it to finish). Hidden (suppressed) stored nodes are cleared too, and each cleared node is queued for regeneration by its own update, so a reset never rebuilds the pending queue of another database or namespace.
Recover Terminal Failures
Transient provider failures remain in the durable pending queue and retry after a provider-wide exponential cooldown. Request-validation errors are parked instead of blocking other nodes. List and retry those terminal failures:
curl 'http://localhost:7474/nornicdb/embed/failures?limit=100' \
-H "Authorization: Bearer $TOKEN"
# Retry selected nodes. Use an empty body or empty node_ids to retry all.
curl -X POST http://localhost:7474/nornicdb/embed/retry-failures \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"node_ids":["picture-123"]}'Manual Embedding
Embed Query
// Generate embedding for search query
embedding, err := db.EmbedQuery(ctx, "What are the ML basics?")
if err != nil {
return err
}
// Use for vector search
results, err := db.HybridSearch(ctx, "", embedding, nil, 10)Pre-computed Embeddings
// Store node with pre-computed embedding
db.ExecuteCypher(ctx, `CREATE (n:KnowledgeFact {
content: $content
}) RETURN n`, map[string]any{
"content": "Important information",
})
// Pre-computed embeddings can be set via the vector index APIEmbedding Dimensions
| Model | Dimensions | Memory/Vector |
|---|---|---|
| mxbai-embed-large | 1024 | 4KB |
| text-embedding-3-small | 1536 | 6KB |
| text-embedding-3-large | 3072 | 12KB |
Configuration
export NORNICDB_EMBEDDING_DIMENSIONS=1024Caching
Embedding Cache
# Cache 10,000 embeddings in memory
export NORNICDB_EMBEDDING_CACHE_SIZE=10000Cache Behavior
- Identical text returns cached embedding
- Cache is LRU (Least Recently Used)
- Cache is not persisted across restarts
Search with Embeddings
Vector Search
// Pure vector similarity search
results, err := db.FindSimilar(ctx, nodeID, 10)Hybrid Search
// Combine vector + text search
results, err := db.HybridSearch(ctx,
"machine learning", // Text query
queryEmbedding, // Vector query
[]string{"Document"}, // Labels
10, // Limit
)RRF Fusion
Results are combined using Reciprocal Rank Fusion:
RRF_score = Σ 1/(k + rank_i)Where k is typically 60.
Indexing
Vector Index (Auto Strategy)
Embeddings are indexed using an auto-selected strategy:
- GPU brute-force (exact) when GPU is enabled and
Nis within the configured threshold - CPU brute-force (exact) only when
NORNICDB_VECTOR_CPU_BRUTE_MAX_Nopts in below a chosen threshold - HNSW (ANN) by default when GPU brute-force or clustering is not selected
// Indexing/search strategy is selected automatically at runtime.
// HNSW parameters (when used) are tuned for quality/speed balance:
// M: 16
// efConstruction: 200
// efSearch: 50Rebuild Index
curl -X POST http://localhost:7474/nornicdb/search/rebuild \
-H "Authorization: Bearer $TOKEN"Best Practices
Content Preparation
// Good: Combine relevant fields
content := fmt.Sprintf("%s\n%s", title, description)
memory := &Memory{Content: content}
// Bad: Too little context
memory := &Memory{Content: "yes"}Batch Processing
// Process in batches for efficiency
for batch := range batches(nodes, 100) {
db.CreateNodes(ctx, batch)
// Wait for embeddings
time.Sleep(time.Second)
}Monitor Quality
// Check embedding coverage
result, _ := db.ExecuteCypher(ctx, `
MATCH (n)
WHERE n.embedding IS NOT NULL
RETURN count(n) as with_embedding
`, nil)Troubleshooting
Embeddings Not Generating
Check embedding service:
bashcurl http://localhost:11434/api/embed \ -d '{"model":"mxbai-embed-large","input":"test"}'Check queue:
bashcurl http://localhost:7474/status | jq .embeddingsCheck logs:
bashdocker logs nornicdb | grep -i embed
Slow Embedding
- Use GPU acceleration
- Increase batch size
- Use embedding cache
- Consider local GGUF models
Disabling vector search per database
The per-database setting db.nornic.search.vector.enabled (default true) is the strongest memory-pressure lever: it prevents the in-memory ANN substrate from being populated at all, even when embeddings exist on disk. NORNICDB_SEARCH_VECTOR_ENABLED remains the supported global environment alternative.
Distinct from NORNICDB_EMBEDDING_ENABLED
| Flag | Stops the embed worker? | Loads existing embeddings into RAM? | Vector queries work? |
|---|---|---|---|
NORNICDB_EMBEDDING_ENABLED=true | no | yes | yes |
NORNICDB_EMBEDDING_ENABLED=false | yes | yes (user-set vectors stay live) | yes |
db.nornic.search.vector.enabled=false | no | no | no (returns no results) |
EMBEDDING_ENABLED=false only stops automatic generation. User-set vectors written via SET n.embedding = [...], WITH EMBEDDING, or external import are still durable in Badger and still iterated into the in-memory ANN substrate by today's search service. SEARCH_VECTOR_ENABLED=false is the stronger guarantee: those durable vectors are NOT iterated into RAM and no ANN strategy serves vector queries.
Exports-only pattern
A common combination for downstream systems like Qdrant or Weaviate that keep their own vector indexes:
memory:
search_vector_enabled: false # don't load embeddings into NornicDB's ANN substrate
embedding:
enabled: true # do generate themNornicDB writes the embeddings to durable Badger storage; an external pipeline (e.g. CDC into Qdrant) consumes them. NornicDB itself never builds HNSW / IVF / GPU brute-force structures and db.index.vector.queryNodes returns empty results with a WARN log.
db.index.vector.queryNodes against a vector-disabled database
Returns zero rows and emits a WARN log line:
db.index.vector.queryNodes called against vector-disabled database — returning empty result
subsystem=vector_search index_name=<index>The query does not error — composite queries that gracefully handle empty vector results continue to succeed. Operators see the misconfiguration in logs.
See Also
- Vector Search - Search guide
- Hybrid Search - RRF fusion
- GPU Acceleration - Speed up embeddings
- Per-Database Search Index Flags — control BM25 and vector indexes per database