Gå til hovedinnhold

Graph Store Library

The graph store library (core/graph/) is a backend-agnostic graph database interface for concept management. The framework ships a SQLite backend (adjacency tables) for local and CLI use, and a server deployment can add its own backend behind the same interface.

Architecture​

GraphStorebackend-agnostic interfaceimplemented bySQLite backendgraphstore.NewSQLiteGraphStorereads and writesgraph.dbgraph_nodes · graph_edges

Inside a kapi project the graph_nodes / graph_edges tables live in the workspace database beside the project registry, and their node ids carry the project they belong to (see Project store); a standalone graph.db is the library example on this page.

Two record types travel through that interface. A node carries an ID, a label, and a map of properties. An edge joins two nodes under a label and may carry a validity (a ValidFrom/ValidTo interval plus a set of tags), so a query can ask the graph a question at a point in time and within a scope. Both are defined in Key Types below.

GraphStore Interface​

type GraphStore interface {
// Node CRUD
CreateNode(ctx context.Context, node *Node) error
GetNode(ctx context.Context, id string) (*Node, error)
UpdateNode(ctx context.Context, node *Node) error
DeleteNode(ctx context.Context, id string) error

// Node queries
FindNodes(ctx context.Context, label string, properties map[string]string) ([]*Node, error)
FindNodesScoped(ctx context.Context, label string, properties map[string]string, scope Scope) ([]*Node, error)

// Edge CRUD + queries
CreateEdge(ctx context.Context, edge *Edge) error
GetEdge(ctx context.Context, id string) (*Edge, error)
UpdateEdge(ctx context.Context, edge *Edge) error
DeleteEdge(ctx context.Context, id string) error
FindEdges(ctx context.Context, label string, properties map[string]string) ([]*Edge, error)

// Traversal
Neighbors(ctx context.Context, nodeID string, direction Direction, labels ...string) ([]*Node, error)
NeighborsScoped(ctx context.Context, nodeID string, direction Direction, scope Scope, labels ...string) ([]*Node, error)
EdgesOf(ctx context.Context, nodeID string, direction Direction, labels ...string) ([]*Edge, error)
ShortestPath(ctx context.Context, fromID, toID string, maxDepth int) (*Path, error)

// Bulk operations
BulkCreateNodes(ctx context.Context, nodes []*Node) error
BulkCreateEdges(ctx context.Context, edges []*Edge) error

// Cypher escape hatch (AGE backend only; SQLite returns ErrCypherNotSupported)
CypherQuery(ctx context.Context, query string, params map[string]any) ([]*Node, error)
CypherExec(ctx context.Context, query string, params map[string]any) error

// Lifecycle
Close() error
}

Key Types​

Node​

type Node struct {
ID string `json:"id"`
Label string `json:"label"`
Properties map[string]string `json:"properties"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

Edge​

type Edge struct {
ID string `json:"id"`
Source string `json:"source"`
Target string `json:"target"`
Label string `json:"label"`
Properties map[string]string `json:"properties"`
Validity *Validity `json:"validity,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

Direction​

type Direction int
const (
Outgoing Direction = iota // source -> target
Incoming // source <- target
Both // either direction
)

Path​

type Path struct {
Nodes []Node `json:"nodes"`
Edges []Edge `json:"edges"`
}

Temporal Validity​

Edges can carry temporal bounds and tag-based scoping:

type Validity struct {
ValidFrom *time.Time `json:"valid_from,omitempty"`
ValidTo *time.Time `json:"valid_to,omitempty"`
Tags map[string]string `json:"tags,omitempty"`
}

Scope Matching​

A Scope represents an evaluation point:

type Scope struct {
At time.Time `json:"at"`
Tags map[string]string `json:"tags,omitempty"`
}

Matching rules:

  • Nil validity always matches (unbounded edge)
  • Time: half-open interval [ValidFrom, ValidTo)
  • Tags: all scope tags must be present in validity tags with matching values
  • Extra validity tags not in scope are ignored (open-world assumption)
import "github.com/neokapi/neokapi/core/graph"

// Query with current time, no tag constraints
scope := graph.Now()

// Query at a specific point in time
scope := graph.ScopeAt(time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC))

// Query with tag constraints
scope := graph.ScopeWithTags(map[string]string{"market": "us", "product": "enterprise"})

// Check if validity is currently active
edge.Validity.IsActive()

// Check if validity has expired
edge.Validity.IsExpired()

Edge Labels​

Labels are aligned with W3C SKOS vocabulary for terminology interoperability:

// Hierarchical (SKOS)
graph.LabelBroader // "BROADER": parent concept
graph.LabelNarrower // "NARROWER": child concept

// Associative (SKOS)
graph.LabelRelated // "RELATED": associative link

// Compositional
graph.LabelPartOf // "PART_OF": component of
graph.LabelHasPart // "HAS_PART": contains component

// Terminological
graph.LabelHasTerm // "HAS_TERM": concept → term
graph.LabelUseInstead // "USE_INSTEAD": deprecated → preferred
graph.LabelReplacedBy // "REPLACED_BY": superseded → replacement

// Equivalence (SKOS)
graph.LabelExactMatch // "EXACT_MATCH": cross-scheme equivalence
graph.LabelCloseMatch // "CLOSE_MATCH": approximate equivalence

// Voice profile
graph.LabelForbidden // "FORBIDDEN": voice → forbidden term
graph.LabelPreferred // "PREFERRED": voice → preferred term
graph.LabelCompetitor // "COMPETITOR": voice → competitor term

InverseLabel() returns the inverse of directional labels (e.g., BROADER -> NARROWER).

SQLite Backend​

import (
"github.com/neokapi/neokapi/core/storage"
graphstore "github.com/neokapi/neokapi/host/storage/graph"
)

db, _ := storage.Open("graph.db")
store, _ := graphstore.NewSQLiteGraphStore(db)
defer store.Close()

The backend stores adjacency tables (graph_nodes, graph_edges) with JSON properties. Shortest path is a recursive CTE with BFS. Scoped queries filter edges in Go after retrieval.

The SQLite backend has no native Cypher support, so CypherQuery and CypherExec return the sentinel graph.ErrCypherNotSupported. A server-side deployment can supply a backend with native Cypher support behind the same interface.

Usage Examples​

Building a Concept Hierarchy​

store, _ := graphstore.NewSQLiteGraphStore(db)

// Create concept nodes
store.CreateNode(ctx, &graph.Node{ID: "animal", Label: "Concept", Properties: map[string]string{"name": "Animal"}})
store.CreateNode(ctx, &graph.Node{ID: "mammal", Label: "Concept", Properties: map[string]string{"name": "Mammal"}})
store.CreateNode(ctx, &graph.Node{ID: "dog", Label: "Concept", Properties: map[string]string{"name": "Dog"}})

// Create hierarchy edges
store.CreateEdge(ctx, &graph.Edge{ID: "e1", Source: "mammal", Target: "animal", Label: graph.LabelBroader})
store.CreateEdge(ctx, &graph.Edge{ID: "e2", Source: "dog", Target: "mammal", Label: graph.LabelBroader})

// Navigate: what is broader than "dog"?
parents, _ := store.Neighbors(ctx, "dog", graph.Outgoing, graph.LabelBroader)
// parents = [mammal]

// Navigate: what is narrower than "animal"?
children, _ := store.Neighbors(ctx, "animal", graph.Incoming, graph.LabelBroader)
// children = [mammal]

// Find path from dog to animal
path, _ := store.ShortestPath(ctx, "dog", "animal", 10)
// path.Nodes = [dog, mammal, animal]
// path.Edges = [e2, e1]

Temporal Edges​

start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)

store.CreateEdge(ctx, &graph.Edge{
ID: "e3", Source: "old-term", Target: "new-term", Label: graph.LabelReplacedBy,
Validity: &graph.Validity{
ValidFrom: &start,
ValidTo: &end,
Tags: map[string]string{"market": "us"},
},
})

// Query with scope: only returns edges active at the given time with matching tags
scope := graph.Scope{At: time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC), Tags: map[string]string{"market": "us"}}
neighbors, _ := store.NeighborsScoped(ctx, "old-term", graph.Outgoing, scope, graph.LabelReplacedBy)

Use FindNodes, Neighbors, and ShortestPath for portable queries that work across all backends.