APOC Functions
NornicDB has apoc.* functions and procedures from two sources:
- Built into the executor, always available: inspect
SHOW FUNCTIONSandSHOW PROCEDURESfor the current registered surface. - The optional APOC plugin (
apoc.so, built fromapoc/plugin-src/apoc) has an inventory of 982 names. Registered names preserve their exact case. Its supported Go handler bindings are described below; registration alone does not establish that a name implements Neo4j APOC semantics or can accept Cypher values.
Some of these names aren't APOC names at all: NornicDB adds its own under the apoc.* and gds.* namespaces. They are listed under NornicDB extensions; a query that uses them won't run on Neo4j with APOC or GDS.
Native graph and temporal values
The native executor implements apoc.node.id, apoc.node.labels, apoc.node.degree, apoc.rel.id, apoc.rel.type, apoc.rel.startNode, apoc.rel.endNode, apoc.nodes.connected, apoc.path.elements, apoc.path.slice, apoc.path.combine, and apoc.temporal.format for query values. These functions are available without the optional plugin. Compatible custom plugin handlers keep precedence; standalone APOC Go graph structs use a separate store and cannot replace the current query's graph reader.
MATCH (a:Person {name: 'Alice'})
RETURN apoc.node.labels(a), apoc.node.degree(a, 'KNOWS>');
MATCH p=(:Person {name: 'Alice'})-[:KNOWS*1..3]->()
RETURN apoc.path.elements(apoc.path.slice(p, 0, 1));
RETURN apoc.temporal.format(datetime('2020-01-02T03:04:05Z'), 'yyyy-MM-dd');Degree and connectivity read the current database and transaction. Relationship filters use APOC direction notation such as KNOWS>, <KNOWS, and KNOWS|LIKES. An undirected degree counts a self-relationship twice. Path slicing takes an offset and a relationship count, with defaults 0 and -1 (remaining path). Offsets past the end return the terminal node's zero-hop path. Combining paths requires the first path's end to equal the second path's start; genuine remote path carriers retain their source identities.
Native apoc.node.id and apoc.rel.id preserve NornicDB's accepted string ID contract; they do not implement Neo4j's numeric internal IDs. Remote values use the existing remote identity owner. Temporal formatting uses Java-style patterns and APOC's null/error and scalar text rules, rather than the standalone Go helper's time.Format layout. APOC duration fraction formatting is intentionally different from Cypher format(duration, pattern) and follows the pinned APOC 5.26.30 behavior.
apoc.nodes.isDense is not implemented for native graph values. Neo4j's answer reflects retained storage representation, including after relationships are deleted; a current degree threshold would give incorrect results. This and missing procedure capabilities remain tracked in #1318.
Plugin handler bindings
Both expression evaluators use the same checked invocation owner. Common Go signatures retain direct calls; other supported signatures use a reflection fallback. For example, plugin Functions() entries can contain:
// Cypher Integer arguments bind to declared int64 values.
Handler: func(a, b int64) int64 { return a & b },
// A Cypher numeric list binds element by element to []float64.
Handler: func(xs []float64) float64 {
var sum float64
for _, x := range xs { sum += x }
return sum
},
// Map results remain maps, including property/subscript access in queries.
Handler: func() map[string]interface{} {
return map[string]interface{}{"status": "ready"}
},Handlers may take fixed or variadic arguments and return no value, one value, error, or (value, error). An optional first context.Context argument receives the calling statement context and is not a Cypher argument. Cancellation is checked before invocation; panics and returned errors become query errors. The Go callback remains responsible for observing cancellation during its work.
Arity must match. Integer conversion rejects fractions, overflow, NaN, infinity, and negative values for unsigned arguments. Lists and maps convert recursively to their declared element types. Null binds to nullable Go interfaces, pointers, slices, or maps; it does not silently become an integer or other scalar zero.
APOC I/O loaders: Go API migration
Every bundled apoc.load handler that performs real network or file I/O is context-first and takes the caller's statement context as its first Go argument: Json, JSONItems, JsonParams, Csv, Xml, Html, Rest, GraphQL, Binary and Stream. JSONItems(shape) is the canonical row loader: JSONItems(DocumentItems) publishes an array's items or one item for any other document, and JSONItems(RequireArray) requires an array. There is no separate JsonStream or JsonArray Go function: the apoc.load.jsonStream and apoc.load.jsonArray Cypher names bind those two shapes. CsvStream and XmlSimple are likewise no longer separate Go functions: their Cypher names are registered against the canonical Csv and Xml handlers, so there is no forwarding alias to call. The remaining apoc.load.* plugin exports are placeholders that return fixed data and perform no I/O (see apoc/apoc_load_inventory_test.go).
Admission belongs to one owner, pkg/apocio. The query executor derives one immutable apocio.Policy from its configuration and binds the matching apocio.Capability in the statement context, so native procedures and bundled plugin handlers read the same capability instead of rebuilding a policy per call. Denied-by-default import file access, export file access and remote URL access, the remote URL allowlist, the address checks, the import root, the HTTP client defaults remain the shared policy. The default transport also judges the socket's resolved address before connecting, so a later DNS answer cannot bypass the prohibited-address rule. Native JSON procedures consume the JSONItems(DocumentItems) handler rather than maintaining a second decoder or row-shape branch.
Source admission and readers observe the caller context. A canceled read delivers no further bytes; an in-flight OS read may finish, and its primary I/O error is preserved. This does not promise preemption inside an OS call or the standard JSON decoder. Binary is the canonical byte loader, and both apoc.load.binary and apoc.load.stream register it: it reads the source, closes it inside the statement lifetime and returns the bytes, because a statement retires its context when Execute returns, so a reader returned as a query value could not be read afterwards. Both Cypher names accept the source and an optional config map; the map is accepted as before and has no other effect, and more than one is rejected before any I/O. Stream remains the Go reader API for callers that read the returned reader while their own context is live, and those callers close it. The API migration does not add XML or HTML decoding: those Go handlers retain their existing placeholder results.
A Go caller that is not a query executor must bind a capability explicitly:
policy := apocio.NewPolicy(apocio.Config{
AllowImportFileAccess: true,
FileAccessRoot: "/var/lib/nornicdb/import",
})
ctx := apocio.WithCapability(context.Background(), policy.Capability())
value, err := apocload.Json(ctx, "payload.json")There is no unrestricted fallback: a context without a bound capability receives apocio.ErrCapabilityMissing and performs no I/O, and binding the zero apocio.Capability over an already-privileged context shadows that context's access rather than inheriting it.
A Go caller that previously called JsonStream or JsonArray binds the factory once and keeps the returned handler:
document := apocload.JSONItems(apocload.DocumentItems)
items, err := document(ctx, "payload.json")There is no forwarding alias and no test-only compatibility wrapper. A shape other than DocumentItems or RequireArray is rejected before any source is opened.
This removes the previous 18-signature dispatcher ceiling, not every capability limitation in the 982-name inventory. Cypher does not supply Go callbacks, io.Reader instances, or private APOC entity structs. Multiple non-error return values and function/channel return types are unsupported handler contracts. Transporting an arbitrary Go object is not guaranteed by accepting its Go signature. Such entries require explicit adapters and semantic tests before they can be described as supported Cypher functions. Procedure names require a real procedure declaration/implementation; scalar functions are not automatically promoted to CALL.
APOC whole-graph helpers: Go API migration
apoc/storage.Storage.AllNodes and apoc/storage.Storage.AllRelationships are context-first bulk reads: each takes the caller's context as its first argument and stops, without a partial collection, when that context is canceled or past its deadline. Every bundled APOC helper that reaches either read from a statement -- the label, meta, schema, search, warmup and refactor helpers the plugin exports as scalar functions, together with the helpers those exports call -- takes the calling statement's context as its first Go argument and hands it to each whole-graph read it makes, node and relationship alike. The query executor already binds a context-first handler's first parameter (above), so a canceled statement stops those reads. apoc/apoc_bulk_read_context_test.go extracts and pins that population and drives every member through a real cancellation and an expired deadline, requiring each node and relationship read to receive that same lifetime. There is no apoc/storage.LifetimeContext forwarding helper: a reader either takes the caller's context or names its own lifetime at the read.
Six procedure-classified names (apoc.meta.graph, apoc.meta.graphSample, apoc.meta.stats, apoc.schema.nodes, apoc.schema.relationships and apoc.stats.degrees) have no statement path: apoc/plugin_exports.go classifies them as procedures and the capability audit records that the bundled runtime and plugin implement none of those procedures. Their Go entries (apoc.Call or a direct call) carry no caller context, so their whole-graph reads name an explicit context.Background() lifetime where they read, and their Go signatures stay unchanged. apoc.refactor.RenameRelProperty is the one whole-graph helper with no registered APOC name at all: apoc/apoc.go registers nothing for it, so no statement and no apoc.Call reaches it, and its relationship read names the same explicit background lifetime. Go callers of the remaining whole-graph helpers pass their own context first and rebuild against the new signatures; (*warmup.Functions).GetNodesByLabel reads only its instance's warmup cache, so it keeps its one-argument Go signature.
apoc.search.nodeReduced is the exception among the procedure-classified names: it is still procedure-classified, but its Go entry no longer runs on an unstated background lifetime. (*search.Functions).NodeReduced(ctx, label, criteria, limit) takes the caller's context first like every other apoc.search helper, so a Go caller supplies the lifetime its read runs under. apoc.Call(name, args) binds no context, so it cannot reach a context-first helper: a Go caller that wants the registered name looks it up with apoc.GetInfo and binds its own context as the handler's first argument. apoc/apoc_bulk_read_context_test.go pins the whole population -- including the package's own scan owners, which no APOC name reaches directly -- and asserts that a caller that stopped before admission makes no whole-graph read at all, while an in-flight cancel stops the helper without publishing a partial result.
Backend-bound instances: Go API migration
APOC helpers do not read a package-level storage default. Every module whose helper actually reaches storage -- label, meta, schema, search, warmup, refactor, stats, neighbors, rel, path, paths, nodes and node -- exposes one instance owner:
// Functions is one module's helpers bound to one backend.
type Functions struct { backend storage.Storage } // immutable, unexported
func New(backend storage.Storage) *FunctionsHelpers that reach storage, directly or through another helper of the package, are methods of that instance; helpers with no backend and no instance-state dependency stay free functions with unchanged signatures. warmup also owns its warmup cache in the instance, so a backend swap cannot read another backend's cached graph. Go callers build the instance for the backend they serve and keep it:
search := apocsearch.New(backend)
nodes := search.Prefix(ctx, "Person", "name", "Ali")apoc.Initialize(backend, cfg) builds every instance for backend and publishes one complete function registry. Its once-only and configuration ownership are unchanged. A later apoc.SetStorage(backend) publishes a complete replacement registry for future lookups and returns an error instead of panicking; a handler that was already admitted -- a *registry.Callable an executor, plugin or test retained -- keeps the backend it was built for, because nothing rebinds it. A rejected rebuild (a registration that fails to compile) publishes nothing and leaves the previous registry authoritative. apoc.GetStorage() returns the published default registry's own backend. There is no compatibility wrapper, no rebindable module global and no per-lookup lock inside a helper.
schema.Properties and meta.PropertyKeys were byte-identical Go duplicates of the same property-key union. (*meta.BoundFunctions).PropertyKeys is now the one owner, registered directly under both Cypher names (apoc.meta.propertyKeys and apoc.schema.properties); the schema duplicate is removed rather than aliased. apoc/apoc_backend_binding_test.go drives the registered handlers of a supplied backend, of a second backend, of a retained handler and of two independently warmed instances, and apoc/apoc_bulk_read_context_test.go keeps the whole-graph population pinned with the bound-helper evidence.
Ten further Go helpers were one-line forwards of another helper in the same package. Each Cypher name keeps its exact spelling, category, description and example metadata and registers the canonical owner directly; the forwarding Go declaration is removed rather than kept as a wrapper, so several Cypher names answer from one implementation:
| Cypher name | retired Go alias | canonical owner |
|---|---|---|
apoc.neighbors.bfs | (*neighbors.Functions).BFS | (*neighbors.Functions).ToHop |
apoc.meta.export | (*meta.BoundFunctions).Export | (*meta.BoundFunctions).Schema |
apoc.meta.snapshot | (*meta.BoundFunctions).Snapshot | (*meta.BoundFunctions).Schema |
apoc.schema.export | (*schema.Functions).Export | (*schema.Functions).Info |
apoc.schema.snapshot | (*schema.Functions).Snapshot | (*schema.Functions).Info |
apoc.meta.cypherType | meta.CypherType | meta.Type |
apoc.meta.cypherTypes | meta.CypherTypes | meta.Types |
apoc.meta.diff | meta.Diff | meta.Compare |
apoc.meta.restore | meta.Restore | meta.Import |
apoc.schema.restore | schema.Restore | schema.Import |
The canonical signatures are unchanged, so a caller sees the same arguments, results, arity and conversion errors. apoc.neighbors.bfs keeps the three-argument ToHop signature (its retained metadata example now shows all three arguments), and meta.Import/schema.Import keep their metadata-only admission. apoc/apoc_alias_binding_test.go pins each name to the canonical owner's symbol and signature, its unchanged registration metadata, its results and argument errors, and a retained handler's own backend lifetime.
Those ten declarations are exported Go API removals -- Functions.Export, Functions.Snapshot, BoundFunctions.Export, BoundFunctions.Snapshot, meta.CypherType, meta.CypherTypes, meta.Diff, meta.Restore, schema.Restore and neighbors.Functions.BFS. Go code that called one of them must be rebuilt against this source, and so must a binary or plugin built against the previous export set, including apoc.so from apoc/plugin-src/apoc: a Go plugin is loadable only when host and plugin are built together, and there is no compatibility wrapper.
The metadata instance is *meta.BoundFunctions. The pure list helper keeps its original Go name, meta.Functions(), because it reads no storage and has no instance: only the storage-dependent helper type needed a new name, so no Go caller of the value function has to change. The census resolves a registered name's owner from the handler's own runtime symbol and then from the parsed source, so (*Functions) receivers, (*BoundFunctions) receivers and free functions all resolve without assuming that every instance type shares one name.
Converged apoc.search scan
Storage-backed search methods share newResultScan for admission and one whole-graph read, resultScan.each for ordered label filtering and bounded cancellation, and resultScan.result for publication. Each public method states its own matching rule while ranging over that one traversal. Go 1.26.9 fully inlines both the iterator and each matching yield; there is no indirect callback or operation dispatch per node. The intermediate typed-request switch and cursor experiments are not retained.
multiSearchAll, multiSearchAny and parallel use selectLabels, which calls their canonical single-label method once per label. It preserves input label order and the existing duplicate when a node belongs to several requested labels. There is no second graph traversal implementation for these methods.
What the helpers agree on, they now share:
- Equality.
equalValuesis the one comparison. Two scalars compare with Go's==, so equality stays strict by dynamic type (int64(7),int(7)andfloat64(7)are three values, and NaN equals nothing); a list or a map compares by contents, exactly ascoll.Containsdoes. A graph that stores list or map properties can therefore be searched instead of aborting the statement withcomparing uncomparable type []interface {}. Membership (in,notIn) is the same owner applied to a list of values: one by one for a short list, through a table of listed scalars for a longer one, and by contents for a value the table cannot hold. - Any versus all.
multiSearchAnyselects a node that satisfies at least one criterion andmultiSearchAllone that satisfies every criterion;multiSearchAnyis not an alias ofmultiSearchAll. - Suggestions.
(*search.Functions).Suggestis the one suggestion owner and takes an optional limit:apoc.search.suggestpasses the limit it was given andapoc.search.autocompletepasses none and keeps its default of ten. The GoAutocompletewrapper is retired rather than kept as a forwarding alias.matchstill transforms a glob into a regex,parallelstill scans many labels, anddidYouMeanstill maps fuzzy matches -- none of them is a pure alias, so nothing else was folded away. - Caller lifetime. The scan observes the caller before the read, at every 256-node boundary and once more before it publishes, so a caller that has already stopped reads nothing and a caller that stops mid-scan receives the documented empty result instead of a partial collection.
selectLabelsobserves the caller before each label, so a cancel after the first label returns nothing, and the string and score collections observe the caller before they publish.
apoc/search/search_convergence_test.go and apoc/search/search_registered_handler_test.go pin these behaviours, the populations and the ordering. apoc/search/search_scan_bench_test.go and apoc/search/search_postprocess_bench_test.go carry the workloads the convergence is measured on, small (256 nodes) and large (16384 nodes).
The performance comparison distinguishes convergence cost from the cost of the correctness fixes. The original per-helper bodies omitted the new cancellation checks and could panic when comparing list/map properties. On 256-node cheap predicates, the corrected iterator remains about 6–13% slower than those incomplete bodies in the six-pair series; this is disclosed rather than called neutral. Several comparisons also require a small fixed per-call allocation that the unsafe comparison avoided. No per-node allocation was added.
Against separate loops with the same cancellation and equality rules, six paired runs of 256/16,384-node presence, absence, equality and four-value membership improve median time by about 1–9%, with equal allocation counts. A broader three-pair comparison covers all 52 labeled-scan/post-processing cases: allocation counts are equal throughout; one short Exists result is +5.2%, with a range crossing parity, while its longer six-pair run is −1.6%. The remaining cases stay within 5% or improve. These are matched local measurements on Apple M2 Max / Go 1.26.9, with ordinary machine activity and recorded variation, not a statistical or whole-system guarantee. CPU profiles and compiler diagnostics confirm the fused traversal and show property-map access as a major remaining cost. The slower local-accumulator and cursor variants were rejected.
Cancellation tests retain real caller cancellation and the complete value/order populations. The deterministic in-block bound is proven on the iterator owner; registered-handler tests prove admission/post-read cancellation and actual owner reachability. They do not claim to inject an arbitrary mid-block public cancellation without a production hook or establish a wall-clock bound from a record-count assertion. No test is excluded from the integrated suite.
Retired Go PageRank stub (v1.4.4)
The exported Go apoc/algo.PageRank function and its bundled scalar registration are removed. The function accepted no graph relationships and used empty incoming link/outdegree stubs, so it could not compute graph-dependent rankings. Rebuild Go consumers/plugins after removing that call; there is no compatibility wrapper.
Use the supported native procedure through the query API:
CALL apoc.algo.pageRank(['Person'], {iterations: 20, dampingFactor: 0.85})
YIELD node, score
RETURN node, scoreThe native procedure retains its existing graph, ranking and cancellation owner. The optional plugin catalog now contains 982 registrations: 118 procedure-only names, 864 scalar names, and 814 exported functions after 50 native-owner exclusions. The query engine's native APOC function catalog remains 52 names.
Quick Start
Using APOC in Cypher
// Collection functions
MATCH (n:Person)
RETURN apoc.coll.sum(n.scores) AS total
// Text processing
RETURN apoc.text.join(['Hello', 'World'], ' ') AS greeting
// Graph algorithms (a NornicDB extension procedure)
MATCH (n:Person)
WITH collect(n) AS people
CALL apoc.algo.pageRank(people) YIELD node, score
RETURN node.name, scoreConfiguration via Environment Variables
# Docker example - disable expensive algorithms, enable custom plugins
docker run \
-e NORNICDB_APOC_ALGO_ENABLED=false \
-e NORNICDB_PLUGINS_DIR=/plugins \
-v ./my-plugins:/plugins \
nornicdb/nornicdbAvailable Function Categories
| Category | Functions | Description |
|---|---|---|
apoc.coll.* | 60+ | Collection operations (sum, avg, sort, filter, etc.) |
apoc.text.* | 50+ | Text processing (join, split, regex, Levenshtein, etc.) |
apoc.math.* | 50+ | Math operations (trig, stats, number theory) |
apoc.convert.* | 30+ | Type conversions (toInteger, toJson, etc.) |
apoc.map.* | 35+ | Map operations (merge, keys, values, flatten) |
apoc.date.* | 20+ | Date/time functions (parse, format, add) |
apoc.temporal.* | 40+ | Advanced date/time (timezone, duration, age) |
apoc.json.* | 25+ | JSON operations (path, validate, merge) |
apoc.util.* | 50+ | Utilities (MD5, SHA, UUID, compress) |
apoc.agg.* | 20+ | Aggregations (median, percentile, histogram) |
apoc.node.* | 40+ | Node operations (degree, labels, neighbors) |
apoc.nodes.* | 30+ | Batch node operations (link, group, filter) |
apoc.rel.* | 30+ | Relationship operations (properties, clone) |
apoc.path.* | 15+ | Path finding (shortestPath, allPaths) |
apoc.paths.* | 25+ | Advanced paths (k-shortest, disjoint, cycles) |
apoc.neighbors.* | 10+ | Neighbor traversal (BFS, DFS, atHop) |
apoc.algo.* | 15+ | Graph algorithms (PageRank, centrality) |
apoc.create.* | 25+ | Dynamic creation (virtual nodes, clone) |
apoc.atomic.* | 20+ | Atomic operations (add, subtract, locks) |
apoc.bitwise.* | 15+ | Bitwise operations (and, or, xor, shift) |
apoc.cypher.* | 20+ | Dynamic Cypher (run, parallel, parse) |
apoc.diff.* | 10+ | Diff operations (nodes, relationships, maps) |
apoc.export.* | 15+ | Export data (JSON, CSV, Cypher, GraphML) |
apoc.import.* | 15+ | Import data (JSON, CSV, GraphML, batch) |
apoc.graph.* | 15+ | Virtual graphs (from, merge, validate) |
apoc.hashing.* | 20+ | Hashing (MD5, SHA*, MurmurHash, xxHash) |
apoc.label.* | 15+ | Label operations (add, remove, merge) |
apoc.load.* | 30+ | Data loading (JSON, CSV, XML, JDBC, S3) |
apoc.lock.* | 15+ | Locking (nodes, relationships, deadlock) |
apoc.log.* | 25+ | Logging (info, debug, metrics, audit) |
apoc.merge.* | 20+ | Merge operations (nodes, rels, properties) |
apoc.meta.* | 30+ | Metadata (schema, stats, constraints) |
apoc.number.* | 40+ | Number formatting (roman, hex, base conversion) |
apoc.periodic.* | 10+ | Periodic execution (iterate, schedule) |
apoc.refactor.* | 25+ | Graph refactoring (merge, clone, normalize) |
apoc.schema.* | 25+ | Schema management (indexes, constraints) |
apoc.scoring.* | 25+ | Scoring/ranking (cosine, jaccard, TF-IDF) |
apoc.search.* | 30+ | Full-text search (fuzzy, regex, autocomplete) |
apoc.spatial.* | 25+ | Geographic functions (distance, bearing) |
apoc.stats.* | 30+ | Statistics (mean, median, correlation) |
apoc.trigger.* | 20+ | Trigger management (onCreate, onUpdate) |
apoc.warmup.* | 15+ | Database warmup (cache, indexes) |
apoc.xml.* | 25+ | XML processing (parse, query, transform) |
Configuration
Environment Variables
All APOC settings can be configured via environment variables (Docker/K8s friendly):
# Plugin directory for custom .so plugins
NORNICDB_PLUGINS_DIR=/opt/nornicdb/plugins
# Enable/disable function categories
NORNICDB_APOC_COLL_ENABLED=true # Collection functions
NORNICDB_APOC_TEXT_ENABLED=true # Text processing
NORNICDB_APOC_MATH_ENABLED=true # Math operations
NORNICDB_APOC_ALGO_ENABLED=false # Graph algorithms (disable if expensive)
NORNICDB_APOC_CREATE_ENABLED=false # Dynamic creation (disable for read-only)
# Security settings
NORNICDB_APOC_SECURITY_ALLOW_IMPORT_FILE_ACCESS=false
NORNICDB_APOC_SECURITY_ALLOW_EXPORT_FILE_ACCESS=false
NORNICDB_APOC_SECURITY_ALLOW_REMOTE_URL_ACCESS=false
NORNICDB_APOC_SECURITY_FILE_ACCESS_ROOT=/var/lib/nornicdb/import
NORNICDB_APOC_SECURITY_MAX_COLLECTION_SIZE=100000YAML Configuration
Alternatively, use a configuration file:
# /etc/nornicdb/apoc.yaml
# Plugin directory for custom .so plugins
plugins_dir: /opt/nornicdb/plugins
# Enable/disable categories
categories:
coll: true
text: false # Enable selected text functions below
math: true
algo: false # Disable expensive algorithms
create: false # Disable write operations
# Fine-grained function control (overrides categories)
functions:
"apoc.export.*": false # Disable all export
"apoc.import.*": false # Disable all import
"apoc.text.camelCase": true # Enable one text function
# Security
security:
allow_dynamic_creation: false
allow_import_file_access: false
allow_export_file_access: false
allow_remote_url_access: false
file_access_root: /var/lib/nornicdb/import
max_collection_size: 10000allow_file_access remains supported as a legacy shorthand that enables both import and export file access, but prefer the split import/export settings for new configurations.
Remote APOC HTTP(S) loads are denied by default even when local file access is enabled. Set allow_remote_url_access: true only when callers must fetch trusted public URLs.
Docker Compose Example
version: "3.8"
services:
nornicdb:
image: nornicdb/nornicdb:latest
environment:
- NORNICDB_APOC_ALGO_ENABLED=false
- NORNICDB_APOC_CREATE_ENABLED=false
- NORNICDB_PLUGINS_DIR=/plugins
volumes:
- ./custom-plugins:/plugins
- ./data:/var/lib/nornicdb
ports:
- "7687:7687"Custom Plugins
NornicDB supports loading custom functions from Go plugin files (.so).
Plugin Interface
Custom plugins must implement this interface:
type PluginInterface interface {
Name() string // Plugin name (e.g., "ml")
Version() string // Version (e.g., "1.0.0")
Functions() map[string]PluginFunction // Function definitions
}
type PluginFunction struct {
Handler interface{} // The function implementation
Description string // Documentation
Examples []string // Usage examples
}Creating a Custom Plugin
Step 1: Create the plugin
// plugin_ml.go
package main
import (
"math"
"github.com/orneryd/nornicdb/apoc"
)
// Plugin must be exported
var Plugin MLPlugin
type MLPlugin struct{}
func (p MLPlugin) Name() string { return "ml" }
func (p MLPlugin) Version() string { return "1.0.0" }
func (p MLPlugin) Type() string { return "function" }
func (p MLPlugin) Functions() map[string]apoc.PluginFunction {
return map[string]apoc.PluginFunction{
"sigmoid": {
Handler: Sigmoid,
Description: "Sigmoid activation function",
Examples: []string{"apoc.ml.sigmoid(0) => 0.5"},
},
"relu": {
Handler: ReLU,
Description: "ReLU activation function",
Examples: []string{"apoc.ml.relu(-5) => 0"},
},
}
}
func Sigmoid(x float64) float64 {
return 1.0 / (1.0 + math.Exp(-x))
}
func ReLU(x float64) float64 {
if x < 0 {
return 0
}
return x
}Step 2: Build as plugin
go build -buildmode=plugin -o apoc-ml.so plugin_ml.goStep 3: Deploy
# Copy to plugins directory
cp apoc-ml.so /opt/nornicdb/plugins/
# Or mount in Docker
docker run -v ./apoc-ml.so:/plugins/apoc-ml.so \
-e NORNICDB_PLUGINS_DIR=/plugins \
nornicdb/nornicdbStep 4: Use in Cypher
RETURN apoc.ml.sigmoid(0.5) AS activation
// Returns: 0.6224593312018546Auto-Detection
When NornicDB starts with NORNICDB_PLUGINS_DIR set, the unified plugin loader:
- Scans the directory for
*.sofiles - Validates each file has a
Pluginexport - Calls
Plugin.Type()to determine plugin type:"function"or"apoc"or""→ Function plugin (extends Cypher)"heimdall"→ Heimdall plugin (extends SLM subsystem)
- Routes function plugins to Cypher executor
- Registers valid functions as
<plugin-name>.<function>(e.g.,apoc.ml.sigmoid) - Logs warnings for invalid plugins (doesn't prevent startup)
plugins/
├── apoc-ml.so ✅ Loaded → apoc.ml.sigmoid, apoc.ml.relu (function plugin)
├── apoc-kafka.so ✅ Loaded → apoc.kafka.produce, apoc.kafka.consume (function plugin)
├── random-lib.so ⚠️ Skipped (no Plugin export)
└── broken.so ⚠️ Skipped (invalid interface)Note: For Heimdall subsystem plugins (SLM management), use NORNICDB_HEIMDALL_PLUGINS_DIR instead. See Plugin System for details.
Function Reference
Collection Functions (apoc.coll.*)
// Aggregation
apoc.coll.sum([1,2,3]) // → 6
apoc.coll.avg([1,2,3]) // → 2.0
apoc.coll.min([3,1,2]) // → 1
apoc.coll.max([3,1,2]) // → 3
// Transformation
apoc.coll.sort([3,1,2]) // → [1,2,3]
apoc.coll.reverse([1,2,3]) // → [3,2,1]
apoc.coll.flatten([[1,2],[3]]) // → [1,2,3]
// Set operations
apoc.coll.union([1,2], [2,3]) // → [1,2,3]
apoc.coll.intersection([1,2], [2,3]) // → [2]
apoc.coll.subtract([1,2,3], [2]) // → [1,3]
// Filtering
apoc.coll.contains([1,2,3], 2) // → true
apoc.coll.duplicates([1,2,2,3]) // → [2]
apoc.coll.frequencies([1,2,2,3]) // → [{item:1, count:1}, {item:2, count:2}, {item:3, count:1}]Text Functions (apoc.text.*)
// Basic
apoc.text.join(['a','b'], '-') // → "a-b"
apoc.text.split('a-b', '-') // → ["a", "b"]
apoc.text.replace('hello', 'l', 'L') // → "heLLo"
// Case conversion
apoc.text.capitalize('hello') // → "Hello"
apoc.text.camelCase('hello_world') // → "helloWorld"
apoc.text.snakeCase('helloWorld') // → "hello_world"
// String similarity (full algorithms, not placeholders)
apoc.text.distance('hello', 'helo') // → 1 (Levenshtein)
apoc.text.jaroWinklerDistance('hello', 'helo') // → 0.96
apoc.text.hammingDistance('hello', 'hallo') // → 1
// Phonetic
apoc.text.phonetic('Robert') // → "R163" (Soundex)Math Functions (apoc.math.*)
// Basic
apoc.math.round(3.7) // → 4
apoc.math.ceil(3.2) // → 4
apoc.math.floor(3.8) // → 3
apoc.math.abs(-5) // → 5
// Statistics
apoc.math.mean([1,2,3,4,5]) // → 3.0
apoc.math.median([1,2,3,4,5]) // → 3.0
apoc.math.stdDev([1,2,3,4,5]) // → 1.414...
apoc.math.percentile([1,2,3,4,5], 0.5) // → 3.0
// Number theory
apoc.math.gcd(12, 8) // → 4
apoc.math.lcm(12, 8) // → 24
apoc.math.factorial(5) // → 120
apoc.math.isPrime(17) // → trueGraph Algorithm Procedures (apoc.algo.*)
These are procedures, called with CALL. pageRank, betweenness, closeness, wcc, louvain and labelPropagation are NornicDB extensions; dijkstra, aStar and allSimplePaths are APOC names.
// Centrality (NornicDB extensions)
MATCH (n:Person)
WITH collect(n) AS people
CALL apoc.algo.pageRank(people) YIELD node, score
RETURN node.name, score
ORDER BY score DESC
MATCH (n:Person)
WITH collect(n) AS people
CALL apoc.algo.betweenness(people) YIELD node, score
RETURN node.name, score
// Pathfinding
MATCH (start:Person {name:'Alice'}), (end:Person {name:'Bob'})
CALL apoc.algo.dijkstra(start, end, 'KNOWS', 'weight') YIELD path, weight
RETURN path, weight
// Explicit cost for relationships without the weight property
MATCH (start:Person {name:'Alice'}), (end:Person {name:'Bob'})
CALL apoc.algo.dijkstra(start, end, 'KNOWS', 'weight', 10.0) YIELD path, weight
RETURN path, weight
// Community detection
MATCH (n:Person)
RETURN apoc.algo.community(n) AS communityIdAtomic Operations (apoc.atomic.*)
// Atomic updates
MATCH (n:Counter {id: 'visits'})
CALL apoc.atomic.add(n, 'count', 1)
RETURN n.count
// Atomic list operations
MATCH (n:User {id: 123})
CALL apoc.atomic.insert(n, 'tags', 0, 'featured')
// Compare and swap
MATCH (n:Lock {resource: 'db'})
CALL apoc.atomic.compareAndSwap(n, 'owner', null, 'process-123')Bitwise Operations (apoc.bitwise.*)
// Basic operations
RETURN apoc.bitwise.op(12, '&', 10) AS result // → 8
RETURN apoc.bitwise.and(12, 10, 8) AS result // → 8
RETURN apoc.bitwise.or(4, 2, 1) AS result // → 7
// Bit manipulation
RETURN apoc.bitwise.setBit(0, 3) AS result // → 8
RETURN apoc.bitwise.testBit(8, 3) AS result // → true
RETURN apoc.bitwise.countBits(15) AS result // → 4Dynamic Cypher (apoc.cypher.*)
// Run dynamic queries
CALL apoc.cypher.run('MATCH (n:Person) WHERE n.age > $age RETURN n', {age: 30})
YIELD value
RETURN value.n
// Run multiple queries
CALL apoc.cypher.runMany('
CREATE (n:Person {name: $name});
MATCH (n:Person) RETURN count(n);
', {name: 'Alice'})
// Parallel execution
CALL apoc.cypher.parallel(['query1', 'query2'], {})Diff Operations (apoc.diff.*)
// Compare nodes
MATCH (n1:Person {id: 1}), (n2:Person {id: 2})
RETURN apoc.diff.nodes(n1, n2) AS differences
// Compare maps
RETURN apoc.diff.maps(
{name: 'Alice', age: 30},
{name: 'Alice', age: 31}
) AS changes
// → {changed: {age: {old: 30, new: 31}}}Export Functions (apoc.export.*)
// Export to JSON
MATCH (n:Person)
CALL apoc.export.json.query('MATCH (n:Person) RETURN n', '/tmp/people.json', {})
// Export to CSV
MATCH (n:Person)
CALL apoc.export.csv.all('/tmp/graph.csv', {})
// Export to Cypher
CALL apoc.export.cypher.all('/tmp/backup.cypher', {format: 'cypher-shell'})Import Functions (apoc.import.*)
// Import JSON
CALL apoc.import.json('/data/people.json')
YIELD node
RETURN node
// Import CSV
CALL apoc.import.csv('/data/data.csv', {delimiter: ',', header: true})
// Batch import
CALL apoc.import.batch([
{labels: ['Person'], props: {name: 'Alice'}},
{labels: ['Person'], props: {name: 'Bob'}}
], 1000)Hashing Functions (apoc.hashing.*)
// Cryptographic hashes
RETURN apoc.hashing.md5('hello') AS hash
RETURN apoc.hashing.sha256('password') AS hash
RETURN apoc.hashing.sha512('data') AS hash
// Fast hashes
RETURN apoc.hashing.murmur3('key') AS hash
RETURN apoc.hashing.xxhash('data') AS hash
// Consistent hashing
RETURN apoc.hashing.consistentHash('user-123', 10) AS bucketLabel Operations (apoc.label.*)
// Add labels
MATCH (n:Person {id: 123})
CALL apoc.label.add(n, ['Employee', 'Manager'])
// Remove labels
MATCH (n:Person)
CALL apoc.label.remove(n, ['Temporary'])
// Check labels
MATCH (n)
WHERE apoc.label.has(n, 'Person')
RETURN nLoad Functions (apoc.load.*)
// Load JSON from URL
CALL apoc.load.json('https://api.example.com/data')
YIELD value
RETURN value
// Load CSV
CALL apoc.load.csv('/data/file.csv', {header: true})
YIELD map
CREATE (n:Person) SET n = map
// Load from S3
CALL apoc.load.s3('s3://bucket/data.json', {region: 'us-east-1'})
// Load from database
CALL apoc.load.jdbc('jdbc:postgresql://localhost/db', 'SELECT * FROM users')Lock Functions (apoc.lock.*)
// Lock nodes
MATCH (n:Resource {id: 'db'})
CALL apoc.lock.nodes([n])
// ... perform operations ...
CALL apoc.lock.unlock([n])
// Read/write locks
CALL apoc.lock.read([node1, node2])
CALL apoc.lock.write([node3])
// Detect deadlocks
CALL apoc.lock.detectDeadlock()
YIELD deadlock
RETURN deadlockLogging Functions (apoc.log.*)
// Basic logging
CALL apoc.log.info('Processing started', {count: 100})
CALL apoc.log.debug('Variable value', {var: value})
CALL apoc.log.warn('Deprecated function', {function: 'old'})
CALL apoc.log.error('Operation failed', {error: message})
// Performance logging
WITH apoc.log.timer('query') AS stopTimer
MATCH (n:Person) WHERE n.age > 30
WITH collect(n) AS results, stopTimer
CALL stopTimer()
RETURN results
// Metrics
CALL apoc.log.metrics('query_time', 150, 'ms')Merge Operations (apoc.merge.*)
// Merge nodes
CALL apoc.merge.node(['Person'], {email: 'alice@example.com'},
{created: timestamp()}, {updated: timestamp()})
YIELD node
RETURN node
// Merge relationships
MATCH (a:Person {id: 1}), (b:Person {id: 2})
CALL apoc.merge.relationship(a, 'KNOWS', {}, {since: 2020}, {}, b)
YIELD rel
RETURN rel
// Deep merge properties
MATCH (n:Person {id: 123})
CALL apoc.merge.deepMerge(n, {address: {city: 'NYC', zip: '10001'}})Metadata Functions (apoc.meta.*)
// Get schema
CALL apoc.meta.schema()
YIELD value
RETURN value
// Get statistics
CALL apoc.meta.stats()
YIELD labelCount, relTypeCount
RETURN labelCount, relTypeCount
// Get node type properties
CALL apoc.meta.nodeTypeProperties('Person')
YIELD propertyName, propertyType
RETURN propertyName, propertyTypeNeighbor Traversal (apoc.neighbors.*)
// Get neighbors at specific distance
MATCH (n:Person {name: 'Alice'})
RETURN apoc.neighbors.atHop(n, 'KNOWS', 2) AS secondDegree
// Get all neighbors up to distance
MATCH (n:Person {name: 'Alice'})
RETURN apoc.neighbors.toHop(n, 'KNOWS', 3) AS network
// BFS traversal
MATCH (n:Person {name: 'Alice'})
CALL apoc.neighbors.bfs(n, 'KNOWS', 5)
YIELD node
RETURN nodeBatch Node Operations (apoc.nodes.*)
// Link nodes in sequence
MATCH (n:Step)
WITH collect(n) AS steps
CALL apoc.nodes.link(steps, 'NEXT')
YIELD rel
RETURN rel
// Group nodes by property
MATCH (n:Person)
WITH collect(n) AS people
RETURN apoc.nodes.group(people, 'department') AS grouped
// Filter nodes
MATCH (n:Person)
WITH collect(n) AS people
RETURN apoc.nodes.filter(people, function(n) {
RETURN n.age > 18
}) AS adultsNumber Functions (apoc.number.*)
// Format numbers
RETURN apoc.number.format(1234.5678, '#,##0.00') AS formatted
// → "1,234.57"
// Roman numerals
RETURN apoc.number.romanize(14) AS roman // → "XIV"
RETURN apoc.number.arabize('XIV') AS number // → 14
// Base conversion
RETURN apoc.number.toHex(255) AS hex // → "FF"
RETURN apoc.number.fromHex('FF') AS decimal // → 255
RETURN apoc.number.toBinary(10) AS binary // → "1010"Advanced Path Operations (apoc.paths.*)
// Find all paths
MATCH (start:Person {name: 'Alice'}), (end:Person {name: 'Bob'})
CALL apoc.paths.all(start, end, 'KNOWS', 5)
YIELD path
RETURN path
// K-shortest paths
MATCH (start:Person), (end:Person)
CALL apoc.paths.kShortest(start, end, 'KNOWS', 10, 3)
YIELD path
RETURN path
// Node-disjoint paths
CALL apoc.paths.disjoint(start, end, 'KNOWS', 10, 2)
YIELD path
RETURN pathPeriodic Execution (apoc.periodic.*)
// Batch processing
CALL apoc.periodic.iterate(
'MATCH (n:Person) RETURN n',
'SET n.processed = true',
{batchSize: 1000, parallel: true}
)
// Scheduled execution
CALL apoc.periodic.schedule('cleanup',
'MATCH (n:Temp) DELETE n',
60) // Every 60 seconds
// Commit in batches
CALL apoc.periodic.commit(
'MATCH (n:Person) WHERE n.migrated IS NULL
WITH n LIMIT $limit
SET n.migrated = true
RETURN count(*)',
{limit: 1000}
)Graph Refactoring (apoc.refactor.*)
// Merge nodes
MATCH (n1:Person {id: 1}), (n2:Person {id: 2})
CALL apoc.refactor.mergeNodes([n1, n2], {properties: 'combine'})
YIELD node
RETURN node
// Clone subgraph
MATCH path = (n:Person)-[r:KNOWS]->(m:Person)
WITH collect(n) + collect(m) AS nodes, collect(r) AS rels
CALL apoc.refactor.cloneSubgraph(nodes, rels)
YIELD nodes AS newNodes, relationships AS newRels
RETURN newNodes, newRels
// Normalize data
MATCH (n:Person)
CALL apoc.refactor.normalize(n, 'city', 'City', 'LIVES_IN')Relationship Operations (apoc.rel.*)
// Get relationship properties
MATCH ()-[r:KNOWS]->()
RETURN apoc.rel.type(r), apoc.rel.properties(r)
// Clone relationship
MATCH ()-[r:KNOWS]->()
WITH r LIMIT 1
CALL apoc.rel.clone(r)
YIELD rel
RETURN rel
// Get relationship weight
MATCH ()-[r:KNOWS]->()
RETURN apoc.rel.weight(r, 'strength', 1.0) AS weightSchema Management (apoc.schema.*)
// Create index
CALL apoc.schema.index.create('Person', ['name'])
// Create constraint
CALL apoc.schema.constraint.create('Person', ['email'], 'UNIQUE')
// List all indexes
CALL apoc.schema.node.indexes()
YIELD name, label, properties
RETURN name, label, properties
// Validate schema
CALL apoc.schema.validate()
YIELD valid, errors
RETURN valid, errorsScoring Functions (apoc.scoring.*)
// Cosine similarity
RETURN apoc.scoring.cosine([1,2,3], [4,5,6]) AS similarity
// Jaccard similarity
RETURN apoc.scoring.jaccard([1,2,3], [2,3,4]) AS similarity
// TF-IDF
RETURN apoc.scoring.tfidf('hello', 'hello world hello', 100, 30) AS score
// Pearson correlation
RETURN apoc.scoring.pearson([1,2,3,4], [2,4,6,8]) AS correlationSearch Functions (apoc.search.*)
// Full-text search
CALL apoc.search.fullText('Person', 'name', 'Alice Bob')
YIELD node
RETURN node
// Fuzzy search
CALL apoc.search.fuzzy('Person', 'name', 'Alise', 2)
YIELD node
RETURN node
// Regex search
CALL apoc.search.regex('Person', 'email', '.*@example\\.com')
YIELD node
RETURN node
// Suggestions (up to ten); apoc.search.suggest takes an explicit limit
CALL apoc.search.autocomplete('Person', 'name', 'Al')
YIELD suggestion
RETURN suggestion
// Any-criteria search across labels
CALL apoc.search.multiSearchAny(['Person', 'Company'], {name: 'Alice', status: 'active'})
YIELD node
RETURN nodeSpatial Functions (apoc.spatial.*)
// Calculate distance
WITH {latitude: 40.7128, longitude: -74.0060} AS nyc,
{latitude: 51.5074, longitude: -0.1278} AS london
RETURN apoc.spatial.distance(nyc, london) AS distanceKm
// Find nearest points
MATCH (p:Place)
WITH {latitude: 40.7128, longitude: -74.0060} AS target,
collect(p) AS places
RETURN apoc.spatial.kNearest(target, places, 5) AS nearest
// Check if within bounding box
RETURN apoc.spatial.within(
{latitude: 40.7128, longitude: -74.0060},
{minLat: 40, maxLat: 41, minLon: -75, maxLon: -73}
) AS isWithinStatistics Functions (apoc.stats.*)
// Basic statistics
WITH [1,2,3,4,5,6,7,8,9,10] AS values
RETURN apoc.stats.mean(values) AS mean,
apoc.stats.median(values) AS median,
apoc.stats.stdDev(values) AS stdDev
// Correlation
WITH [1,2,3,4,5] AS x, [2,4,6,8,10] AS y
RETURN apoc.stats.correlation(x, y) AS correlation
// Percentiles
WITH [1,2,3,4,5,6,7,8,9,10] AS values
RETURN apoc.stats.percentile(values, 0.95) AS p95Temporal Functions (apoc.temporal.*)
// Format dates
RETURN apoc.temporal.format(datetime(), 'yyyy-MM-dd HH:mm:ss') AS formatted
// Parse dates
RETURN apoc.temporal.parse('2024-01-15', 'yyyy-MM-dd') AS date
// Date arithmetic
RETURN apoc.temporal.add(datetime(), 7, 'days') AS nextWeek
RETURN apoc.temporal.subtract(datetime(), 1, 'month') AS lastMonth
// Calculate age
WITH date('1990-01-15') AS birthdate
RETURN apoc.temporal.age(birthdate) AS age
// Timezone conversion
RETURN apoc.temporal.timezone(datetime(), 'America/New_York') AS nyTimeTrigger Management (apoc.trigger.*)
// Add trigger
CALL apoc.trigger.add('onPersonCreate',
'MATCH (n:Person) SET n.created = timestamp()',
{phase: 'after'}
)
// List triggers
CALL apoc.trigger.list()
YIELD name, statement, enabled
RETURN name, statement, enabled
// Pause/resume triggers
CALL apoc.trigger.pause('onPersonCreate')
CALL apoc.trigger.resume('onPersonCreate')
// Remove trigger
CALL apoc.trigger.remove('onPersonCreate')Warmup Functions (apoc.warmup.*)
// Warm up entire database
CALL apoc.warmup.run()
YIELD nodesLoaded, relationshipsLoaded, timeTaken
RETURN nodesLoaded, relationshipsLoaded, timeTaken
// Warm up specific labels
CALL apoc.warmup.nodes(['Person', 'Company'])
// Warm up indexes
CALL apoc.warmup.indexes()
// Warm up subgraph
MATCH (n:Person {id: 123})
CALL apoc.warmup.subgraph(n, 3)XML Functions (apoc.xml.*)
// Parse XML
WITH '<root><item>value</item></root>' AS xml
RETURN apoc.xml.parse(xml) AS parsed
// Query XML
WITH apoc.xml.parse('<root><item id="1">A</item></root>') AS doc
RETURN apoc.xml.query(doc, '//item[@id="1"]') AS items
// Convert to JSON
WITH '<root><item>value</item></root>' AS xml
RETURN apoc.xml.toJson(xml) AS jsonSecurity Considerations
Production Recommendations
# Disable write operations
NORNICDB_APOC_CREATE_ENABLED=false
# Disable file access
NORNICDB_APOC_SECURITY_ALLOW_FILE_ACCESS=false
# Limit collection sizes to prevent OOM
NORNICDB_APOC_SECURITY_MAX_COLLECTION_SIZE=10000
# Disable expensive algorithms if not needed
NORNICDB_APOC_ALGO_ENABLED=falsePlugin Security
- Only load plugins from trusted sources
- Plugins run with full NornicDB permissions
- Use file permissions to restrict plugin directory:
chmod 755 /opt/nornicdb/plugins
chown root:root /opt/nornicdb/plugins/*.soTroubleshooting
Plugin Not Loading
Warning: failed to load plugins from /plugins: plugin does not export 'Plugin'Cause: The .so file doesn't have a var Plugin export.
Fix: Ensure your plugin exports var Plugin YourPluginType.
Function Not Found
RETURN apoc.custom.myFunc('test')
// Error: function apoc.custom.myFunc not foundPossible causes:
- Plugin not loaded (check
NORNICDB_PLUGINS_DIR) - Function category disabled (check
NORNICDB_APOC_<CATEGORY>_ENABLED) - Function specifically disabled in config
Platform Compatibility
Go plugins are platform-specific. A .so built on Linux x86_64 won't work on:
- macOS (use
.dylib) - Windows (use
.dll) - Linux ARM
Build plugins on the same platform/architecture as your NornicDB deployment.
Architecture
┌─────────────────────────────────────────────────────────┐
│ NornicDB Binary │
├─────────────────────────────────────────────────────────┤
│ Core APOC Functions (450+ compiled in) │
│ ├── apoc.coll.* ├── apoc.text.* ├── apoc.math.* │
│ ├── apoc.convert.* ├── apoc.map.* ├── apoc.date.* │
│ ├── apoc.json.* ├── apoc.util.* ├── apoc.agg.* │
│ ├── apoc.node.* ├── apoc.path.* └── apoc.algo.* │
├─────────────────────────────────────────────────────────┤
│ Plugin Loader (auto-loads from NORNICDB_PLUGINS_DIR)│
│ └── Scans *.so → Validates interface → Registers funcs │
├─────────────────────────────────────────────────────────┤
│ Configuration (env vars or YAML) │
│ └── Controls which functions are enabled │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Optional Plugins Directory (/opt/nornicdb/plugins) │
│ ├── apoc-ml.so → apoc.ml.* │
│ ├── apoc-kafka.so → apoc.kafka.* │
│ └── custom-*.so → apoc.custom.* │
└─────────────────────────────────────────────────────────┘Migration from Neo4j APOC
What carries over from Neo4j with APOC, and what doesn't:
- Built-in functions:
SHOW FUNCTIONSlists 48 built-inapoc.*functions (49 beside a loaded plugin's case-distinctapoc.coll.SUM). Of the original 36, 31 are real APOC functions, four are NornicDB extensions, andapoc.coll.splitis an APOC procedure offered here as a function. The twelve native graph, path and temporal functions —apoc.node.id,apoc.node.labels,apoc.node.degree,apoc.rel.id,apoc.rel.type,apoc.rel.startNode,apoc.rel.endNode,apoc.nodes.connected,apoc.path.elements,apoc.path.slice,apoc.path.combine, andapoc.temporal.format— bring the total to 48.apoc.node.idandapoc.rel.idkeep NornicDB's accepted string-ID design exception; they do not implement Neo4j's numeric internal IDs. - Built-in procedures:
SHOW PROCEDURESlists the current registered surface. The GDS-named graph catalog —gds.graph.list,gds.graph.drop,gds.graph.exists, andgds.graph.project— now takes GDS's parameters and returns GDS's columns, as dogds.fastRP.stream/gds.fastRP.stats(#1161 closed);gds.versionanswers as a function. Thegds.linkPrediction.*.streamnames and theapoc.algo.*family remain NornicDB extensions. The remaining procedure capability gaps — the 102 of 118 historical procedure-only catalog entries with no bundledCALLcontract — are tracked in the versioned procedure capability audit and #1318. - The APOC plugin: the Go library catalog has 982 names, including genuine functions, procedure-only entries and NornicDB extensions. The plugin excludes the 118 confirmed procedure-only entries and 50 scalar names owned by native implementations, exporting 814 functions. See the versioned procedure capability audit for every entry, available native contracts, and remaining gaps; the typed handler dispatcher does not certify full APOC semantics. Loaded function metadata is available through
SHOW FUNCTIONS. - File operations respect security settings, and the plugin system allows custom extensions. Performance characteristics may differ.
NornicDB extensions
These names are NornicDB's own: they don't exist in APOC Core, APOC Extended or GDS 2.13.12 (on Neo4j 5.26.30) as the same kind of name. They are kept as NornicDB extensions; a query that uses them runs only on NornicDB.
Built-in procedures (14):
| name | nearest real equivalent |
|---|---|
apoc.algo.pageRank, apoc.algo.betweenness, apoc.algo.closeness, apoc.algo.wcc, apoc.algo.louvain, apoc.algo.labelPropagation | GDS's gds.pageRank.stream, gds.betweenness.stream, gds.closeness.stream, gds.wcc.stream, gds.louvain.stream, gds.labelPropagation.stream |
gds.linkPrediction.adamicAdar.stream, .commonNeighbors.stream, .resourceAllocation.stream, .preferentialAttachment.stream | GDS's gds.alpha.linkprediction.adamicAdar / commonNeighbors / resourceAllocation / preferentialAttachment: functions over one pair of nodes, not procedures |
gds.linkPrediction.jaccard.stream, gds.linkPrediction.predict.stream | none (predict combines topology and embeddings) |
apoc.cypher.doitall | APOC's apoc.cypher.doIt |
apoc.periodic.rock_n_roll | APOC's apoc.periodic.iterate |
Built-in functions (4, and one procedure offered as a function): apoc.coll.containsAny, apoc.coll.reverse, apoc.meta.isType and apoc.meta.type (APOC 5's are apoc.meta.cypher.isType / apoc.meta.cypher.type); apoc.coll.split is an APOC procedure.
The APOC plugin: the historical catalog includes NornicDB extensions and 118 procedure-only entries, now excluded from function exports. The following historical inventory is not a declaration that every listed capability is implemented:
The plugin's names that aren't APOC names: 689 (historical inventory; the original audit's 55 measured-callable names are in bold and are not a current capability claim)
apoc.math(37): abs, acos, asin, atan, atan2, ceil, clamp, cos, exp, factorial, fibonacci, floor, gcd, isPrime, lcm, lerp, log, log10, logit, mean, median, mode, nextPrime, normalize, percentile, pow, product, random, randomInt, range, round, sin, sqrt, stdev, sum, tan, varianceapoc.number(35): abs, arabize, ceil, clamp, exact, exp, factorial, fibonacci, floor, fromBase, fromBinary, fromHex, fromOctal, gcd, isEven, isOdd, isPrime, lcm, lerp, log, log10, map, normalize, parse, power, random, randomInt, romanize, round, sign, sqrt, toBase, toBinary, toHex, toOctalapoc.node(31): addLabel, addLabels, clone, connected, degreeIn, degreeOut, diff, equals, fromMap, hasLabel, hasLabels, isDense, neighbors, neighborsIn, neighborsOut, properties, property, relationshipExists, relationships, relationshipsIn, relationshipsOut, relationshipTypes, relationshipTypesIn, relationshipTypesOut, removeLabel, removeLabels, removeProperties, removeProperty, setProperties, setProperty, toMapapoc.util(31): case, coalesce, compressWithAlgorithm, decodeBase64, decodeURL, decompressWithAlgorithm, encodeBase64, encodeURL, formatTimestamp, isNode, isPath, isRelationship, md5Base64, md5Hex, merge, now, nowInSeconds, parseTimestamp, partition, randomUUID, range, repeat, sha1Base64, sha1Hex, sha256Base64, sha256Hex, timestamp, typeOf, uuid, validatePattern, whenapoc.meta(30): analyze, cardinality, compare, config, constraints, cypherType, cypherTypes, diff, export, fromString, functions, import, indexes, isNode, isPath, isRelationship, isType, nodeLabels, pattern, procedures, propertyKeys, relTypes, restore, snapshot, toString, type, typeOf, types, validate, versionapoc.schema(27): analyze, compare, createConstraint, createExistsConstraint, createIndex, createNodeKeyConstraint, createUniqueConstraint, dropConstraint, dropIndex, export, import, info, labels, nodeConstraintExists, nodeConstraints, nodeIndexes, nodeIndexExists, optimize, properties, propertiesDistinct, relationshipConstraints, relationshipIndexes, restore, snapshot, stats, types, validateapoc.label(26): add, clear, compare, count, diff, format, fromPattern, fromString, get, has, hasAll, hasAny, intersection, list, merge, nodes, normalize, pattern, remove, replace, search, set, stats, toString, union, validateapoc.search(26): autocomplete, contains, didYouMean, dropIndex, exists, fullText, fuzzy, highlight, in, index, match, missing, multiSearchAll, multiSearchAny, nodeAny, notIn, notNull, null, parallel, prefix, range, regex, reindex, score, suffix, suggestapoc.rel(25): clone, compare, delete, direction, equals, exists, fromMap, hasProperties, hasProperty, isAnyType, isBetween, isDirectedBetween, isLoop, isType, nodes, otherNode, properties, property, removeProperties, removeProperty, reverse, setProperties, setProperty, toMap, weightapoc.temporal(24): add, age, dayOfWeek, dayOfYear, daysInMonth, difference, duration, endOf, fromEpochMillis, isBetween, isLeapYear, isWeekday, isWeekend, parse, quarter, round, startOf, subtract, timezone, toEpochMillis, toLocal, toUTC, truncate, weekOfYearapoc.stats(22): correlation, count, covariance, histogram, iqr, kurtosis, max, mean, median, min, mode, normalize, outliers, percentile, quartiles, range, skewness, stdDev, sum, summary, variance, zScoreapoc.xml(22): addChild, clone, create, escape, fromJson, fromMap, getAttribute, getNamespace, getText, minify, namespace, prettify, query, removeChild, setAttribute, setText, toJson, toMap, toString, transform, unescape, validateapoc.paths(21): all, common, count, cycles, disjoint, distance, edgeDisjoint, elementary, eulerian, exists, hamiltonian, kShortest, longest, merge, reverse, shortest, simple, slice, unique, withinLength, withLengthapoc.log(20): audit, clear, custom, format, getLevel, memory, metrics, performance, progress, query, result, rotate, search, security, setLevel, stats, tail, timer, toFile, traceapoc.scoring(20): bm25, cosine, dice, euclidean, idf, jaccard, manhattan, minMax, normalize, overlap, pageRank, pearson, percentile, rank, sigmoid, softmax, tf, tfidf, topK, zScoreapoc.community(19): averageClusteringCoefficient, clusteringCoefficient, conductance, connectedComponents, coreNumber, density, fastGreedy, infoMap, kCore, labelPropagation, louvain, modularity, numComponents, spinGlass, stronglyConnectedComponents, totalTriangles, triangleCount, walkTrap, weaklyConnectedComponentsapoc.spatial(19): area, bearing, boundingBox, centroid, contains, decodeGeohash, destination, distance, encodeGeohash, fromGeoJSON, haversineDistance, intersects, kNearest, midpoint, nearest, toGeoJSON, vincentyDistance, within, withinDistanceapoc.json(17): compact, delete, filter, flatten, keys, map, merge, parse, pretty, reduce, set, size, stringify, type, unflatten, validate, valuesapoc.load(17): arrow, avro, azure, binary, csvStream, directoryTree, elasticsearch, gcs, graphQL, jsonSchema, jsonStream, kafka, redis, rest, s3, stream, xmlSimpleapoc.lock(17): batch, clear, detectDeadlock, isLocked, priority, readNodes, readRelationships, relationships, stats, tryLock, unlockAll, unlockBatch, unlockNodes, unlockRelationships, waitFor, withLock, withReadLockapoc.merge(17): batch, conditional, conflict, deepMerge, labels, mergeNode, mergeRelationship, nodeEager, nodes, pattern, preview, properties, relationshipEager, rollback, snapshot, strategy, validateapoc.hashing(16): cityHash64, consistentHash, fnv1, fnv164, fnv1a, fnv1a64, jumpHash, md5, murmurHash3, rendezvousHash, sha1, sha256, sha384, sha512, xxHash32, xxHash64apoc.import(16): batch, convertType, csvData, cypher, cypherData, file, filter, graphMLData, jsonData, merge, parseCsvLine, parseJsonLine, stream, transform, url, validateSchemaapoc.trigger(15): after, afterAsync, before, count, disable, enable, export, import, isEnabled, nodeByLabel, onCreate, onDelete, onUpdate, relationshipByType, statsapoc.bitwise(14): and, clearBit, countBits, leftShift, not, or, reverseBits, rightShift, rotateLeft, rotateRight, setBit, testBit, toggleBit, xorapoc.export(14): csv, csvAll, csvData, cypher, cypherAll, cypherData, graphML, graphMLAll, graphMLData, json, jsonAll, jsonData, toFile, toStringapoc.nodes(14): batch, difference, distinct, distinctRels, filter, fromMap, intersect, map, partition, reduce, relationships, sort, toMap, unionapoc.warmup(14): cache, clear, indexes, nodes, optimize, path, progress, properties, relationships, runWithParams, schedule, stats, status, subgraphapoc.convert(9): fromJsonNode, toBoolean, toBooleanList, toFloat, toFloatList, toInteger, toIntList, toString, toStringListapoc.graph(9): clone, fromMap, merge, nodes, relationships, stats, subgraph, toMap, validateapoc.refactor(9): categorizeProperty, changeType, denormalize, invertRelationship, normalize, redirectRelationship, renameLabel, renameProperty, renameTypeapoc.cypher(8): explain, parse, profile, runFirstColumn, toJson, toList, toMap, validateapoc.diff(7): deep, lists, maps, merge, patch, strings, summaryapoc.algo(5): allPairs, betweennessCentrality, closenessCentrality, community, degreeCentralityapoc.text(6): bytesToString, fromCodePoint, ltrim, reverse, rtrim, trimapoc.agg(5): frequencies, histogram, mode, percentile, stdevapoc.coll(5): containsAny, isEmpty, isNotEmpty, reverse, sliceapoc.neighbors(4): bfs, count, dfs, existsapoc.atomic(3): compareAndSwap, decrement, incrementapoc.create(3): clone, cloneSubgraph, vPatternapoc.date(3): fromUnixTime, parseAsZonedDateTime, toUnixTimeapoc.map(2): dropNullValues, keysapoc.path(2): allShortestPaths, shortestPathapoc.periodic(2): rock, schedule
Historical procedure-only catalog entries: 118 (16 have registered native contracts; 102 have no bundled CALL contract — see the capability audit)
apoc.create(13): addLabels, node, nodes, relationship, removeLabels, removeProperties, removeRelProperties, setProperties, setProperty, setRelProperties, setRelProperty, uuids, vNodesapoc.load(12): csv, directory, driver, html, jdbc, jdbcUpdate, json, jsonArray, jsonParams, ldap, parquet, xmlapoc.refactor(11): cloneNodes, cloneSubgraph, cloneSubgraphFromPaths, collapseNode, deleteAndReconnect, extractNode, from, mergeNodes, mergeRelationships, normalizeAsBoolean, setTypeapoc.trigger(9): add, drop, install, list, pause, remove, removeAll, resume, showapoc.meta(8): data, graph, graphSample, nodeTypeProperties, relTypeProperties, schema, stats, subGraphapoc.periodic(8): cancel, commit, countdown, iterate, list, repeat, submit, truncateapoc.atomic(6): add, concat, insert, remove, subtract, updateapoc.cypher(6): doIt, mapParallel, parallel, run, runFile, runManyapoc.graph(6): from, fromCypher, fromData, fromDocument, fromPath, fromPathsapoc.nodes(6): collapse, cycles, delete, get, group, linkapoc.log(5): debug, error, info, stream, warnapoc.path(4): expandConfig, spanningTree, subgraphAll, subgraphNodesapoc.algo(3): aStar, cover, dijkstraapoc.import(3): csv, graphML, jsonapoc.schema(3): assert, nodes, relationshipsapoc.search(3): node, nodeAll, nodeReducedapoc.convert(2): setJsonProperty, toTreeapoc.lock(2): all, nodesapoc.neighbors(2): atHop, toHopapoc.util(2): sleep, validateapoc.coll(1): splitapoc.stats(1): degreesapoc.text(1): phoneticDeltaapoc.warmup(1): run
Test your specific APOC usage when migrating, particularly:
- File I/O operations (check security settings)
- Custom procedures (may need to be rewritten as plugins)
- Performance-sensitive queries (benchmark in your environment)
External reference
The Neo4j APOC procedures and functions reference describes Neo4j APOC. Check the support details above before relying on the same behavior in NornicDB.