Ĝŕàþĥ Šţöŕé Ļîƃŕàŕý
Ţĥé ĝŕàþĥ šţöŕé ļîƃŕàŕý (core/graph/) þŕöṽîđéš à ƃàçķéñđ-àĝñöšţîç ĝŕàþĥ đàţàƃàšé àƃšţŕàçţîöñ ƒöŕ çöñçéþţ ḿàñàĝéḿéñţ. Ţĥé ƒŕàḿéŵöŕķ îñçļüđéš à ŠǪĻîţé ƃàçķéñđ (àđĵàçéñçý ţàƃļéš) ƒöŕ ļöçàļ àñđ ÇĻÎ üšé. Ţĥé îñţéŕƒàçé îš đéšîĝñéđ ƒöŕ éẋţéñšîöñ, šö šéŕṽéŕ đéþļöýḿéñţš çàñ àđđ ţĥéîŕ öŵñ ƃàçķéñđ ƃéĥîñđ ţĥé šàḿé îñţéŕƒàçé.
Àŕçĥîţéçţüŕé
Îñšîđé à kapi þŕöĵéçţ ţĥé graph_nodes / graph_edges ţàƃļéš ļîṽé îñ ţĥé
šĥàŕéđ .kapi/work/store.db ƃéšîđé ţĥé çöñţéñţ ḿéḿöŕý àñđ ţĥé ţéŕḿš šţöŕé (šéé
Þŕöĵéçţ šţöŕé); à šţàñđàļöñé graph.db îš ţĥé
ļîƃŕàŕý éẋàḿþļé öñ ţĥîš þàĝé.
Ţŵö ŕéçöŕđ ţýþéš ţŕàṽéļ ţĥŕöüĝĥ ţĥàţ îñţéŕƒàçé. À ñöđé çàŕŕîéš àñ ÎĐ, à
ļàƃéļ, àñđ à ḿàþ öƒ þŕöþéŕţîéš. Àñ éđĝé ĵöîñš ţŵö ñöđéš üñđéŕ à ļàƃéļ àñđ
ḿàý çàŕŕý à ṽàļîđîţý (à ValidFrom/ValidTo îñţéŕṽàļ þļüš à šéţ öƒ ţàĝš),
ŵĥîçĥ îš ŵĥàţ ļéţš à ǫüéŕý àšķ ţĥé ĝŕàþĥ à ǫüéšţîöñ àţ à þöîñţ îñ ţîḿé àñđ
ŵîţĥîñ à šçöþé. Ƃöţĥ àŕé đéƒîñéđ îñ Ķéý Ţýþéš ƃéļöŵ.
ĜŕàþĥŠţöŕé Îñţéŕƒàçé
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
}
Ķéý Ţýþéš
Ñöđé
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"`
}
Éđĝé
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"`
}
Đîŕéçţîöñ
type Direction int
const (
Outgoing Direction = iota // source -> target
Incoming // source <- target
Both // either direction
)
Þàţĥ
type Path struct {
Nodes []Node `json:"nodes"`
Edges []Edge `json:"edges"`
}
Ţéḿþöŕàļ Ṽàļîđîţý
Éđĝéš çàñ çàŕŕý ţéḿþöŕàļ ƃöüñđš àñđ ţàĝ-ƃàšéđ šçöþîñĝ:
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 ŕéþŕéšéñţš àñ éṽàļüàţîöñ þöîñţ:
type Scope struct {
At time.Time `json:"at"`
Tags map[string]string `json:"tags,omitempty"`
}
Ḿàţçĥîñĝ ŕüļéš:
- Ñîļ ṽàļîđîţý àļŵàýš ḿàţçĥéš (üñƃöüñđéđ éđĝé)
- Ţîḿé: ĥàļƒ-öþéñ îñţéŕṽàļ
[ValidFrom, ValidTo) - Ţàĝš: àļļ šçöþé ţàĝš ḿüšţ ƃé þŕéšéñţ îñ ṽàļîđîţý ţàĝš ŵîţĥ ḿàţçĥîñĝ ṽàļüéš
- Éẋţŕà ṽàļîđîţý ţàĝš ñöţ îñ šçöþé àŕé îĝñöŕéđ (öþéñ-ŵöŕļđ àššüḿþţîöñ)
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()
Éđĝé Ļàƃéļš
Ļàƃéļš àŕé àļîĝñéđ ŵîţĥ Ŵ3Ç ŠĶÖŠ ṽöçàƃüļàŕý ƒöŕ ţéŕḿîñöļöĝý îñţéŕöþéŕàƃîļîţý:
// 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() ŕéţüŕñš ţĥé îñṽéŕšé öƒ đîŕéçţîöñàļ ļàƃéļš (é.ĝ., BROADER -> NARROWER).
ŠǪĻîţé Ƃàçķéñđ
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()
Üšéš àđĵàçéñçý ţàƃļéš (graph_nodes, graph_edges) ŵîţĥ ĴŠÖÑ þŕöþéŕţîéš. Šĥöŕţéšţ þàţĥ üšéš ŕéçüŕšîṽé ÇŢÉ ŵîţĥ ƂƑŠ. Šçöþéđ ǫüéŕîéš ƒîļţéŕ éđĝéš îñ Ĝö àƒţéŕ ŕéţŕîéṽàļ.
Ţĥé ŠǪĻîţé ƃàçķéñđ ĥàš ñö ñàţîṽé Çýþĥéŕ šüþþöŕţ, šö CypherQuery àñđ CypherExec ŕéţüŕñ ţĥé šéñţîñéļ graph.ErrCypherNotSupported. À šéŕṽéŕ-šîđé đéþļöýḿéñţ çàñ šüþþļý à ƃàçķéñđ ŵîţĥ ñàţîṽé Çýþĥéŕ šüþþöŕţ ƃéĥîñđ ţĥé šàḿé îñţéŕƒàçé.
Üšàĝé Éẋàḿþļéš
Ƃüîļđîñĝ à Çöñçéþţ Ĥîéŕàŕçĥý
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]
Ţéḿþöŕàļ Éđĝéš
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)
Üšé FindNodes, Neighbors, àñđ ShortestPath ƒöŕ þöŕţàƃļé ǫüéŕîéš ţĥàţ ŵöŕķ àçŕöšš àļļ ƃàçķéñđš.