Documentation ¶
Overview ¶
Package rdf implements decoding the RDF 1.1 N-Quads line-based plain text format for encoding an RDF dataset. N-Quad parsing is performed as defined by http://www.w3.org/TR/n-quads/
Example (Graph) ¶
package main import ( "fmt" "log" "os" "strings" "text/tabwriter" "gonum.org/v1/gonum/graph" "gonum.org/v1/gonum/graph/encoding" "gonum.org/v1/gonum/graph/encoding/dot" "gonum.org/v1/gonum/graph/formats/rdf" "gonum.org/v1/gonum/graph/multi" ) // dotNode implements graph.Node and dot.Node to allow the // RDF term value to be given to the DOT encoder. type dotNode struct { rdf.Term } func (n dotNode) DOTID() string { return n.Term.Value } // dotLine implements graph.Line and encoding.Attributer to // allow the line's RDF term value to be given to the DOT // encoder and for the nodes to be shimmed to the dotNode // type. // // Because the graph here is directed and we are not performing // any line reversals, it is safe not to implement the // ReversedLine method on dotLine; it will never be called. type dotLine struct { *rdf.Statement } func (l dotLine) From() graph.Node { return dotNode{l.Subject} } func (l dotLine) To() graph.Node { return dotNode{l.Object} } func (l dotLine) Attributes() []encoding.Attribute { return []encoding.Attribute{{Key: "label", Value: l.Predicate.Value}} } func main() { const statements = ` _:alice <http://xmlns.com/foaf/0.1/knows> _:bob . _:alice <http://xmlns.com/foaf/0.1/givenName> "Alice" . _:alice <http://xmlns.com/foaf/0.1/familyName> "Smith" . _:bob <http://xmlns.com/foaf/0.1/knows> _:alice . _:bob <http://xmlns.com/foaf/0.1/givenName> "Bob" . _:bob <http://xmlns.com/foaf/0.1/familyName> "Smith" . ` // Decode the statement stream and insert the lines into a multigraph. g := multi.NewDirectedGraph() dec := rdf.NewDecoder(strings.NewReader(statements)) for { l, err := dec.Unmarshal() if err != nil { break } // Wrap the line with a shim type to allow the RDF values // to be passed to the DOT marshaling routine. g.SetLine(dotLine{l}) } // Marshal the graph into DOT. b, err := dot.MarshalMulti(g, "smiths", "", "\t") if err != nil { log.Fatal(err) } fmt.Printf("%s\n\n", b) // Get the ID look-up table. w := tabwriter.NewWriter(os.Stdout, 0, 4, 1, ' ', 0) fmt.Fprintln(w, "Term\tID") for t, id := range dec.Terms() { fmt.Fprintf(w, "%s\t%d\n", t, id) } w.Flush() }
Output: digraph smiths { // Node definitions. "_:alice"; "_:bob"; "Alice"; "Smith"; "Bob"; // Edge definitions. "_:alice" -> "_:bob" [label=<http://xmlns.com/foaf/0.1/knows>]; "_:alice" -> "Alice" [label=<http://xmlns.com/foaf/0.1/givenName>]; "_:alice" -> "Smith" [label=<http://xmlns.com/foaf/0.1/familyName>]; "_:bob" -> "_:alice" [label=<http://xmlns.com/foaf/0.1/knows>]; "_:bob" -> "Smith" [label=<http://xmlns.com/foaf/0.1/familyName>]; "_:bob" -> "Bob" [label=<http://xmlns.com/foaf/0.1/givenName>]; } Term ID _:alice 1 _:bob 2 <http://xmlns.com/foaf/0.1/knows> 3 "Alice" 4 <http://xmlns.com/foaf/0.1/givenName> 5 "Smith" 6 <http://xmlns.com/foaf/0.1/familyName> 7 "Bob" 8
Index ¶
- Variables
- func ConnectedByAny(e graph.Edge, with func(*Statement) bool) bool
- func IsoCanonicalHashes(statements []*Statement, decomp, dist bool, h hash.Hash, zero []byte) (hashes map[string][]byte, terms map[string]map[string]bool)
- func Isomorphic(a, b []*Statement, decomp bool, h hash.Hash) bool
- type Decoder
- type Graph
- func (g *Graph) AddStatement(s *Statement)
- func (g *Graph) AllStatements() *Statements
- func (g *Graph) Edge(uid, vid int64) graph.Edge
- func (g *Graph) Edges() graph.Edges
- func (g *Graph) From(id int64) graph.Nodes
- func (g *Graph) FromSubject(t Term) graph.Nodes
- func (g *Graph) HasEdgeBetween(xid, yid int64) bool
- func (g *Graph) HasEdgeFromTo(uid, vid int64) bool
- func (g *Graph) Lines(uid, vid int64) graph.Lines
- func (g *Graph) Node(id int64) graph.Node
- func (g *Graph) Nodes() graph.Nodes
- func (g *Graph) Predicates() []Term
- func (g *Graph) Query(from ...Term) Query
- func (g *Graph) RemoveStatement(s *Statement)
- func (g *Graph) RemoveTerm(t Term)
- func (g *Graph) Statements(uid, vid int64) *Statements
- func (g *Graph) TermFor(text string) (term Term, ok bool)
- func (g *Graph) To(id int64) graph.Nodes
- func (g *Graph) ToObject(t Term) graph.Nodes
- type Kind
- type Query
- func (q Query) And(p Query) Query
- func (q Query) HasAllIn(fn func(s *Statement) bool) Query
- func (q Query) HasAllOut(fn func(s *Statement) bool) Query
- func (q Query) HasAnyIn(fn func(s *Statement) bool) Query
- func (q Query) HasAnyOut(fn func(s *Statement) bool) Query
- func (q Query) In(fn func(s *Statement) bool) Query
- func (q Query) Len() int
- func (q Query) Not(p Query) Query
- func (q Query) Or(p Query) Query
- func (q Query) Out(fn func(s *Statement) bool) Query
- func (q Query) Repeat(fn func(Query) (q Query, ok bool)) Query
- func (q Query) Result() []Term
- func (q Query) Unique() Query
- type Statement
- func C14n(dst, src []*Statement, terms map[string]map[string]bool) ([]*Statement, error)
- func Deduplicate(s []*Statement) []*Statement
- func Lean(g []*Statement) ([]*Statement, error)
- func ParseNQuad(statement string) (*Statement, error)
- func URDNA2015(dst, src []*Statement) ([]*Statement, error)
- func URGNA2012(dst, src []*Statement) ([]*Statement, error)
- type Statements
- type Term
- Bugs
Examples ¶
Constants ¶
This section is empty.
Variables ¶
Functions ¶
func ConnectedByAny ¶
ConnectedByAny is a helper function to for simplifying graph traversal conditions.
func IsoCanonicalHashes ¶
func IsoCanonicalHashes(statements []*Statement, decomp, dist bool, h hash.Hash, zero []byte) (hashes map[string][]byte, terms map[string]map[string]bool)
IsoCanonicalHashes returns a mapping between the nodes of the RDF graph dataset described by the given statements using the provided hash function. If decomp is true, the graphs are decomposed before hashing. If dist is true the input graph is decomposed into identical splits, the entire graph will be hashed to distinguish nodes. If decomp is false, dist has no effect. Blank node hashes are initially set to the value of zero. Hash values are provided for literal and IRI nodes as well as for blank node. The hash input for literal nodes includes the quotes and the input for IRI nodes first removes the angle quotes around the IRI, although these are included in the map keys.
Note that hashes returned by IsoCanonicalHashes with decomp=true are not comparable with hashes returned by IsoCanonicalHashes with decomp=false.
See http://aidanhogan.com/docs/rdf-canonicalisation.pdf for details of the hashing algorithm.
Example (IsomorphicParts) ¶
package main import ( "crypto/md5" "fmt" "os" "slices" "strings" "text/tabwriter" "gonum.org/v1/gonum/graph/formats/rdf" ) func main() { for _, statements := range []string{ ` # Part 1 _:a <ex:q> <ex:p> . _:b <ex:q> <ex:p> . _:c <ex:p> _:a . _:d <ex:p> _:b . _:c <ex:r> _:d . # Part 2 _:a1 <ex:q> <ex:p> . _:b1 <ex:q> <ex:p> . _:c1 <ex:p> _:a1 . _:d1 <ex:p> _:b1 . _:c1 <ex:r> _:d1 . `, } { // Decode the statement stream. dec := rdf.NewDecoder(strings.NewReader(statements)) var s []*rdf.Statement for { l, err := dec.Unmarshal() if err != nil { break } s = append(s, l) } // Get the node label to hash look-up table. This time // we will decompose the dataset into splits and not // distinguish nodes. This will then group nodes from // the two isomorphic parts. Otherwise each node in // the complete dataset would get a unique hash. hashes, _ := rdf.IsoCanonicalHashes(s, true, false, md5.New(), make([]byte, 16)) // Get all the blank nodes. var blanks []string for k := range hashes { if strings.HasPrefix(k, "_:") { blanks = append(blanks, k) } } slices.Sort(blanks) if len(blanks) == 0 { fmt.Println("No blank nodes.") } else { w := tabwriter.NewWriter(os.Stdout, 0, 4, 1, ' ', 0) fmt.Fprintln(w, "Node\tHash") for _, k := range blanks { fmt.Fprintf(w, "%s\t%032x\n", k, hashes[k]) } w.Flush() } fmt.Println() } }
Output: Node Hash _:a d4db6df055d5611e9d8aa6ea621561d1 _:a1 d4db6df055d5611e9d8aa6ea621561d1 _:b ad70e47f2b026064c7f0922060512b9a _:b1 ad70e47f2b026064c7f0922060512b9a _:c dafd81e6fa603d3e11c898d631e8673f _:c1 dafd81e6fa603d3e11c898d631e8673f _:d 7e318557b09444e88791721becc2a8e7 _:d1 7e318557b09444e88791721becc2a8e7
Example (Isomorphisms) ¶
package main import ( "crypto/md5" "fmt" "os" "slices" "strings" "text/tabwriter" "gonum.org/v1/gonum/graph/formats/rdf" ) func main() { for _, statements := range []string{ ` <https://example.com/1> <https://example.com/2> <https://example.com/3> . <https://example.com/3> <https://example.com/4> <https://example.com/5> . `, ` _:a <ex:q> <ex:p> . _:b <ex:q> <ex:p> . _:c <ex:p> _:a . _:d <ex:p> _:b . _:c <ex:r> _:d . `, ` _:a1 <ex:q> <ex:p> . _:b1 <ex:q> <ex:p> . _:c1 <ex:p> _:a1 . _:d1 <ex:p> _:b1 . _:c1 <ex:r> _:d1 . `, ` # G <ex:p> <ex:q> _:a . <ex:p> <ex:q> _:b . <ex:s> <ex:p> _:a . <ex:s> <ex:r> _:c . _:c <ex:p> _:b . # H <ex:p> <ex:q> _:d . <ex:p> <ex:q> _:e . _:f <ex:p> _:d . _:f <ex:r> _:g . _:g <ex:p> _:e . `, ` _:greet <l:is> "hola"@es . `, } { // Decode the statement stream. dec := rdf.NewDecoder(strings.NewReader(statements)) var s []*rdf.Statement for { l, err := dec.Unmarshal() if err != nil { break } s = append(s, l) } // Get the node label to hash look-up table. hashes, _ := rdf.IsoCanonicalHashes(s, false, true, md5.New(), make([]byte, 16)) // Get all the blank nodes. var blanks []string for k := range hashes { if strings.HasPrefix(k, "_:") { blanks = append(blanks, k) } } slices.Sort(blanks) if len(blanks) == 0 { fmt.Println("No blank nodes.") } else { w := tabwriter.NewWriter(os.Stdout, 0, 4, 1, ' ', 0) fmt.Fprintln(w, "Node\tHash") for _, k := range blanks { fmt.Fprintf(w, "%s\t%032x\n", k, hashes[k]) } w.Flush() } fmt.Println() } }
Output: No blank nodes. Node Hash _:a d4db6df055d5611e9d8aa6ea621561d1 _:b ad70e47f2b026064c7f0922060512b9a _:c dafd81e6fa603d3e11c898d631e8673f _:d 7e318557b09444e88791721becc2a8e7 Node Hash _:a1 d4db6df055d5611e9d8aa6ea621561d1 _:b1 ad70e47f2b026064c7f0922060512b9a _:c1 dafd81e6fa603d3e11c898d631e8673f _:d1 7e318557b09444e88791721becc2a8e7 Node Hash _:a 44ad49b6df3aea91ddbcef932c93e3b4 _:b ba3ffd8b271a8545b1a3a9042e75ce4b _:c 34e1bd90b6758b4a766e000128caa6a6 _:d eb2a47c1032f623647d0497a2ff74052 _:e 1d9ed02f28d87e555feb904688bc2449 _:f d3b00d36ea503dcc8d234e4405feab81 _:g 55127e4624c0a4fe5990933a48840af8 Node Hash _:greet 0d9ba18a037a3fa67e46fce821fe51b4
func Isomorphic ¶
Isomorphic returns whether the RDF graph datasets a and b are isomorphic, where there is a bijective mapping between blank nodes in a and b using the given hash function. If decomp is true, the graphs are decomposed before canonicalization.
Types ¶
type Decoder ¶
type Decoder struct {
// contains filtered or unexported fields
}
Decoder is an RDF stream decoder. Statements returned by calls to the Unmarshal method have their Terms' UID fields set so that unique terms will have unique IDs and so can be used directly in a graph.Multi, or in a graph.Graph if all predicate terms are identical. IDs created by the decoder all exist within a single namespace and so Terms can be uniquely identified by their UID. Term UIDs are based from 1 to allow RDF-aware client graphs to assign ID if no ID has been assigned.
func NewDecoder ¶
NewDecoder returns a new Decoder that takes input from r.
func (*Decoder) Reset ¶
Reset resets the decoder to use the provided io.Reader, retaining the existing Term ID mapping.
type Graph ¶
type Graph struct {
// contains filtered or unexported fields
}
Graph implements an RDF graph satisfying the graph.Graph and graph.Multigraph interfaces.
Example ¶
package main import ( "io" "log" "os" "gonum.org/v1/gonum/graph/formats/rdf" ) func main() { f, err := os.Open("path/to/graph.nq") if err != nil { log.Fatal(err) } dec := rdf.NewDecoder(f) var statements []*rdf.Statement for { s, err := dec.Unmarshal() if err != nil { if err != io.EOF { log.Fatalf("error during decoding: %v", err) } break } // Statements can be filtered at this point to exclude unwanted // or irrelevant parts of the graph. statements = append(statements, s) } f.Close() // Canonicalize blank nodes to reduce memory footprint. statements, err = rdf.URDNA2015(statements, statements) if err != nil { log.Fatal(err) } g := rdf.NewGraph() for _, s := range statements { g.AddStatement(s) } // Do something with the graph. }
Output:
func (*Graph) AddStatement ¶
AddStatement adds s to the graph. It panics if Term UIDs in the statement are not consistent with existing terms in the graph. Statements must not be altered while being held by the graph. If the UID fields of the terms in s are zero, they will be set to values consistent with the rest of the graph on return, mutating the parameter, otherwise the UIDs must match terms that already exist in the graph. The statement must be a valid RDF statement otherwise AddStatement will panic.
func (*Graph) AllStatements ¶
func (g *Graph) AllStatements() *Statements
AllStatements returns an iterator of the statements that make up the graph.
func (*Graph) Edge ¶
Edge returns the edge from u to v if such an edge exists and nil otherwise. The node v must be directly reachable from u as defined by the From method. The returned graph.Edge is a multi.Edge if an edge exists.
func (*Graph) Edges ¶
Edges returns all the edges in the graph. Each edge in the returned slice is a multi.Edge.
func (*Graph) From ¶
From returns all nodes in g that can be reached directly from n.
The returned graph.Nodes is only valid until the next mutation of the receiver.
func (*Graph) FromSubject ¶
FromSubject returns all nodes in g that can be reached directly from an RDF subject term.
The returned graph.Nodes is only valid until the next mutation of the receiver.
func (*Graph) HasEdgeBetween ¶
HasEdgeBetween returns whether an edge exists between nodes x and y without considering direction.
func (*Graph) HasEdgeFromTo ¶
HasEdgeFromTo returns whether an edge exists in the graph from u to v.
func (*Graph) Lines ¶
Lines returns the lines from u to v if such any such lines exists and nil otherwise. The node v must be directly reachable from u as defined by the From method.
func (*Graph) Node ¶
Node returns the node with the given ID if it exists in the graph, and nil otherwise.
func (*Graph) Nodes ¶
Nodes returns all the nodes in the graph.
The returned graph.Nodes is only valid until the next mutation of the receiver.
func (*Graph) Predicates ¶
Predicates returns a slice of all the predicates used in the graph.
func (*Graph) Query ¶
Query returns a query of the receiver starting from the given nodes. Queries may not be mixed between distinct graphs.
func (*Graph) RemoveStatement ¶
RemoveStatement removes s from the graph, leaving the terminal nodes if they are part of another statement. If the statement does not exist in g it is a no-op.
func (*Graph) RemoveTerm ¶
RemoveTerm removes t and any statements referencing t from the graph. If the term is a predicate, all statements with the predicate are removed. If the term does not exist it is a no-op.
func (*Graph) Statements ¶
func (g *Graph) Statements(uid, vid int64) *Statements
Statements returns an iterator of the statements that connect the subject term node u to the object term node v.
func (*Graph) TermFor ¶
TermFor returns the Term for the given text. The text must be an exact match for the Term's Value field.
type Kind ¶
type Kind int
Kind represents the kind of an RDF term.
const ( // Invalid is an invalid RDF term. Invalid Kind = iota // IRI is the kind of an IRI term. // https://www.w3.org/TR/n-quads/#sec-iri IRI // Literal is the kind of an RDF literal. // https://www.w3.org/TR/n-quads/#sec-literals Literal // Blank is the kind of an RDF blank node term. // https://www.w3.org/TR/n-quads/#BNodes Blank )
type Query ¶
type Query struct {
// contains filtered or unexported fields
}
Query represents a step in an RDF graph query. The methods on Query provide a simple graph query language.
Example ¶
package main import ( "fmt" "io" "log" "strings" "gonum.org/v1/gonum/graph/formats/rdf" ) const gods = ` _:alcmene <l:type> "human" . _:alcmene <p:name> "Alcmene" . _:cerberus <a:lives> _:cerberushome . _:cerberus <l:type> "monster" . _:cerberus <p:name> "Cerberus" . _:cerberushome <p:location> _:tartarus . _:cronos <l:type> "titan" . _:cronos <p:name> "Cronos" . _:hades <a:lives> _:hadeshome . _:hades <h:brother> _:poseidon . _:hades <h:brother> _:zeus . _:hades <h:pet> _:cerberus . _:hades <l:type> "god" . _:hades <p:name> "Hades" . _:hadeshome <p:location> _:tartarus . _:hadeshome <p:reason> "it is peaceful" . _:heracles <a:battled> _:cerberus . _:heracles <a:battled> _:hydra . _:heracles <a:battled> _:nemean . _:heracles <h:father> _:zeus . _:heracles <h:mother> _:alcmene . _:heracles <l:type> "demigod" . _:heracles <p:name> "Heracles" . _:hydra <l:type> "monster" . _:hydra <p:name> "Lernean Hydra" . _:nemean <l:type> "monster" . _:nemean <p:name> "Nemean Lion" . _:olympus <l:type> "location" . _:olympus <p:name> "Olympus" . _:poseidon <a:lives> _:poseidonhome . _:poseidon <h:brother> _:hades . _:poseidon <h:brother> _:zeus . _:poseidon <l:type> "god" . _:poseidon <p:name> "Poseidon" . _:poseidonhome <p:location> _:sea . _:poseidonhome <p:reason> "it was given to him" . _:sea <l:type> "location" . _:sea <p:name> "Sea" . _:tartarus <l:type> "location" . _:tartarus <p:name> "Tartarus" . _:theseus <a:battled> _:cerberus . _:theseus <h:father> _:poseidon . _:theseus <l:type> "human" . _:theseus <p:name> "Theseus" . _:zeus <a:lives> _:zeushome . _:zeus <h:brother> _:hades . _:zeus <h:brother> _:poseidon . _:zeus <h:father> _:cronos . _:zeus <l:type> "god" . _:zeus <p:name> "Zeus" . _:zeushome <p:location> _:olympus . _:zeushome <p:reason> "he can see everything" . ` func main() { g := rdf.NewGraph() dec := rdf.NewDecoder(strings.NewReader(gods)) for { s, err := dec.Unmarshal() if err != nil { if err != io.EOF { log.Fatalf("error during decoding: %v", err) } break } g.AddStatement(s) } it := g.Nodes() nodes := make([]rdf.Term, 0, it.Len()) for it.Next() { nodes = append(nodes, it.Node().(rdf.Term)) } // Construct a query start point. This can be reused. If a specific // node is already known it can be used to reduce the work required here. heracles := g.Query(nodes...).In(func(s *rdf.Statement) bool { // Traverse in from the name "Heracles". return s.Predicate.Value == "<p:name>" && s.Object.Value == `"Heracles"` }) // father and name filter statements on their predicate values. These // are used in the queries that follow. father := func(s *rdf.Statement) bool { // Traverse across <h:father>. return s.Predicate.Value == "<h:father>" } name := func(s *rdf.Statement) bool { // Traverse across <p:name>. return s.Predicate.Value == "<p:name>" } // g.V().has('name', 'heracles').out('father').out('father').values('name') for _, r := range heracles. Out(father). // Traverse out across <h:father> to get to Zeus. Out(father). // and again to get to Cronos. Out(name). // Retrieve the name by traversing the <p:name> edges. Result() { fmt.Printf("Heracles' grandfather: %s\n", r.Value) } // g.V().has('name', 'heracles').repeat(out('father')).emit().values('name') var i int heracles.Repeat(func(q rdf.Query) (rdf.Query, bool) { q = q.Out(father) for _, r := range q.Out(name).Result() { fmt.Printf("Heracles' lineage %d: %s\n", i, r.Value) } i++ return q, true }) // parents and typ are helper filters for queries below. parents := func(s *rdf.Statement) bool { // Traverse across <h:father> or <h:mother> return s.Predicate.Value == "<h:father>" || s.Predicate.Value == "<h:mother>" } typ := func(s *rdf.Statement) bool { // Traverse across <l:type>. return s.Predicate.Value == "<l:type>" } // g.V(heracles).out('father', 'mother').label() for _, r := range heracles.Out(parents).Out(typ).Result() { fmt.Printf("Heracles' parents' types: %s\n", r.Value) } // battled is a helper filter for queries below. battled := func(s *rdf.Statement) bool { // Traverse across <a:battled>. return s.Predicate.Value == "<a:battled>" } // g.V(heracles).out('battled').label() for _, r := range heracles.Out(battled).Out(typ).Result() { fmt.Printf("Heracles' antagonists' types: %s\n", r.Value) } // g.V(heracles).out('battled').valueMap() for _, r := range heracles.Out(battled).Result() { m := make(map[string]string) g.Query(r).Out(func(s *rdf.Statement) bool { // Store any p: namespace in the map. if strings.HasPrefix(s.Predicate.Value, "<p:") { prop := strings.TrimSuffix(strings.TrimPrefix(s.Predicate.Value, "<p:"), ">") m[prop] = s.Object.Value } // But don't store the result into the query. return false }) fmt.Println(m) } // g.V(heracles).as('h').out('battled').in('battled').where(neq('h')).values('name') for _, r := range heracles.Out(battled).In(battled).Not(heracles).Out(name).Result() { fmt.Printf("Heracles' allies: %s\n", r.Value) } // Construct a query start point for Hades, this time using a restricted // starting set only including the name. It would also be possible to // start directly from a query with the term _:hades, but that depends // on the blank node identity, which may be altered, for example by // canonicalization. h, ok := g.TermFor(`"Hades"`) if !ok { log.Fatal("could not find term for Hades") } hades := g.Query(h).In(name) // g.V(hades).as('x').out('lives').in('lives').where(neq('x')).values('name') // // This is more complex with RDF since properties are encoded by // attachment to anonymous blank nodes, so we take two steps, the // first to the blank node for where Hades lives and then the second // to get the actual location. lives := func(s *rdf.Statement) bool { // Traverse across <a:lives>. return s.Predicate.Value == "<a:lives>" } location := func(s *rdf.Statement) bool { // Traverse across <p:location>. return s.Predicate.Value == "<p:location>" } for _, r := range hades.Out(lives).Out(location).In(location).In(lives).Not(hades).Out(name).Result() { fmt.Printf("Hades lives with: %s\n", r.Value) } // g.V(hades).out('brother').as('god').out('lives').as('place').select('god', 'place').by('name') brother := func(s *rdf.Statement) bool { // Traverse across <h:brother>. return s.Predicate.Value == "<h:brother>" } for _, r := range hades.Out(brother).Result() { m := make(map[string]string) as := func(key string) func(s *rdf.Statement) bool { return func(s *rdf.Statement) bool { // Store any <p:name> objects in the map. if s.Predicate.Value == "<p:name>" { m[key] = s.Object.Value } // But don't store the result into the query. return false } } sub := g.Query(r) sub.Out(as("god")) sub.Out(lives).Out(location).Out(as("place")) fmt.Println(m) } // The query above but with the reason for their choice. for _, r := range hades.Out(brother).Result() { m := make(map[string]string) // as stores the query result under the provided key // for m, and if cont is not nil, allows the chain // to continue. as := func(query, key string, cont func(s *rdf.Statement) bool) func(s *rdf.Statement) bool { return func(s *rdf.Statement) bool { // Store any objects matching the query in the map. if s.Predicate.Value == query { m[key] = s.Object.Value } // Continue with chain if cont is not nil and // the statement satisfies its condition. if cont == nil { return false } return cont(s) } } sub := g.Query(r) sub.Out(as("<p:name>", "god", nil)) sub.Out(lives). Out(as("<p:reason>", "reason", location)). Out(as("<p:name>", "place", nil)) fmt.Println(m) } }
Output: Heracles' grandfather: "Cronos" Heracles' lineage 0: "Zeus" Heracles' lineage 1: "Cronos" Heracles' parents' types: "god" Heracles' parents' types: "human" Heracles' antagonists' types: "monster" Heracles' antagonists' types: "monster" Heracles' antagonists' types: "monster" map[name:"Cerberus"] map[name:"Lernean Hydra"] map[name:"Nemean Lion"] Heracles' allies: "Theseus" Hades lives with: "Cerberus" map[god:"Zeus" place:"Olympus"] map[god:"Poseidon" place:"Sea"] map[god:"Zeus" place:"Olympus" reason:"he can see everything"] map[god:"Poseidon" place:"Sea" reason:"it was given to him"]
func NewQuery ¶
NewQuery returns a query of g starting from the given nodes. Queries may not be mixed between distinct graphs. The type of g must be comparable. Query operations only consider edges that are represented by a *Statement or is an edge with lines held in a graph.Lines with at least one *Statement.
func (Query) HasAllIn ¶
HasAllIn returns a query holding nodes from the receiver's initial set where all incoming statements satisfy fn. The query short circuits, so fn is not called after the first failure to match.
func (Query) HasAllOut ¶
HasAllOut returns a query holding nodes from the receiver's initial set where all outgoing statements satisfy fn. The query short circuits, so fn is not called after the first failure to match.
func (Query) HasAnyIn ¶
HasAnyIn returns a query holding nodes from the receiver's initial set where any incoming statements satisfies fn. The query short circuits, so fn is not called after the first match.
func (Query) HasAnyOut ¶
HasAnyOut returns a query holding nodes from the receiver's initial set where any outgoing statements satisfies fn. The query short circuits, so fn is not called after the first match.
func (Query) In ¶
In returns a query holding nodes reachable in from the receiver's starting nodes via statements that satisfy fn.
func (Query) Out ¶
Out returns a query holding nodes reachable out from the receiver's starting nodes via statements that satisfy fn.
func (Query) Repeat ¶
Repeat repeatedly calls fn on q until the set of results is empty or ok is false, and then returns the result. If the last non-empty result is wanted, fn should return its input and false when the partial traversal returns an empty result.
result := start.Repeat(func(q rdf.Query) (rdf.Query, bool) { r := q.Out(condition) if r.Len() == 0 { return q, false } return r, true }).Result()
type Statement ¶
Statement is an RDF statement. It implements the graph.Edge and graph.Line interfaces.
func C14n ¶
C14n performs a relabeling of the statements in src based on the terms obtained from IsoCanonicalHashes, placing the results in dst and returning them. The relabeling scheme is the same as for the Universal RDF Dataset Normalization Algorithm, blank terms are ordered lexically by their hash value and then given a blank label with the prefix "_:c14n" and an identifier counter corresponding to the label's sort rank.
If dst is nil, it is allocated, otherwise the length of dst must match the length of src.
Example ¶
package main import ( "crypto/md5" "fmt" "log" "strings" "gonum.org/v1/gonum/graph/formats/rdf" ) func main() { for _, statements := range []string{ ` _:a <ex:q> <ex:p> . _:b <ex:q> <ex:p> . _:c <ex:p> _:a . _:d <ex:p> _:b . _:c <ex:r> _:d . `, ` _:c1 <ex:p> _:a1 . _:b1 <ex:q> <ex:p> . _:d1 <ex:p> _:b1 . _:a1 <ex:q> <ex:p> . _:c1 <ex:r> _:d1 . `, } { // Decode the statement stream. dec := rdf.NewDecoder(strings.NewReader(statements)) var s []*rdf.Statement for { l, err := dec.Unmarshal() if err != nil { break } s = append(s, l) } // Get the hash to term label look-up table. _, terms := rdf.IsoCanonicalHashes(s, false, true, md5.New(), make([]byte, 16)) relabeled, err := rdf.C14n(nil, s, terms) if err != nil { log.Fatal(err) } for _, s := range relabeled { fmt.Println(s) } fmt.Println() } }
Output: _:c14n0 <ex:p> _:c14n1 . _:c14n1 <ex:q> <ex:p> . _:c14n2 <ex:q> <ex:p> . _:c14n3 <ex:p> _:c14n2 . _:c14n3 <ex:r> _:c14n0 . _:c14n0 <ex:p> _:c14n1 . _:c14n1 <ex:q> <ex:p> . _:c14n2 <ex:q> <ex:p> . _:c14n3 <ex:p> _:c14n2 . _:c14n3 <ex:r> _:c14n0 .
func Deduplicate ¶
Deduplicate removes duplicate statements in s, working in place, and returns the deduplicated slice with statements sorted in lexical order. Term UID fields are not considered and their values may be lost during deduplication.
func Lean ¶
Lean returns an RDF core of g that entails g. If g contains any non-zero labels, Lean will return a non-nil error and a core of g assuming no graph labels exist.
See http://aidanhogan.com/docs/rdf-canonicalisation.pdf for details of the algorithm.
Example ¶
package main import ( "fmt" "strings" "gonum.org/v1/gonum/graph/formats/rdf" ) func main() { for i, statements := range []string{ 0: ` _:author1 <ex:contributesTo> _:go . _:author2 <ex:contributesTo> _:go . _:author2 <ex:contributesTo> _:gonum . _:author3 <ex:contributesTo> _:gonum . `, 1: ` _:author1 <ex:contributesTo> _:go . _:author2 <ex:contributesTo> _:go . _:author2 <ex:contributesTo> _:gonum . _:author3 <ex:contributesTo> _:gonum . _:gonum <ex:dependsOn> _:go . `, 2: ` _:author1 <ex:contributesTo> _:go . _:author1 <ex:notContributesTo> _:gonum . _:author2 <ex:contributesTo> _:go . _:author2 <ex:contributesTo> _:gonum . _:author3 <ex:contributesTo> _:gonum . _:gonum <ex:dependsOn> _:go . `, 3: ` _:author1 <ex:is> "Alice" . _:author1 <ex:contributesTo> _:go . _:author2 <ex:contributesTo> _:go . _:author2 <ex:contributesTo> _:gonum . _:author3 <ex:contributesTo> _:gonum . _:gonum <ex:dependsOn> _:go . `, 4: ` _:author1 <ex:contributesTo> _:go . _:author1 <ex:notContributesTo> _:gonum . _:author2 <ex:contributesTo> _:go . _:author2 <ex:contributesTo> _:gonum . _:author3 <ex:contributesTo> _:gonum . _:author3 <ex:notContributesTo> _:go . _:gonum <ex:dependsOn> _:go . `, 5: ` _:author1 <ex:is> "Alice" . _:author2 <ex:is> "Bob" . _:author1 <ex:contributesTo> _:go . _:author2 <ex:contributesTo> _:go . _:author2 <ex:contributesTo> _:gonum . _:author3 <ex:contributesTo> _:gonum . _:gonum <ex:dependsOn> _:go . `, 6: ` _:author1 <ex:is> "Alice" . _:author2 <ex:is> "Bob" . _:author3 <ex:is> "Charlie" . _:author1 <ex:contributesTo> _:go . _:author2 <ex:contributesTo> _:go . _:author2 <ex:contributesTo> _:gonum . _:author3 <ex:contributesTo> _:gonum . _:gonum <ex:dependsOn> _:go . `, } { // Decode the statement stream. dec := rdf.NewDecoder(strings.NewReader(statements)) var s []*rdf.Statement for { l, err := dec.Unmarshal() if err != nil { break } s = append(s, l) } // Lean the graph to remove redundant statements. lean, err := rdf.Lean(s) if err != nil { fmt.Println(err) } // Canonicalize the blank nodes in-place. _, err = rdf.URDNA2015(lean, lean) if err != nil { fmt.Println(err) continue } fmt.Printf("%d:\n", i) for _, s := range lean { fmt.Println(s) } fmt.Println() } }
Output: 0: _:c14n0 <ex:contributesTo> _:c14n1 . 1: _:c14n0 <ex:contributesTo> _:c14n1 . _:c14n0 <ex:contributesTo> _:c14n2 . _:c14n2 <ex:dependsOn> _:c14n1 . 2: _:c14n0 <ex:contributesTo> _:c14n1 . _:c14n0 <ex:contributesTo> _:c14n3 . _:c14n2 <ex:contributesTo> _:c14n1 . _:c14n2 <ex:notContributesTo> _:c14n3 . _:c14n3 <ex:dependsOn> _:c14n1 . 3: _:c14n0 <ex:contributesTo> _:c14n1 . _:c14n0 <ex:contributesTo> _:c14n3 . _:c14n2 <ex:contributesTo> _:c14n1 . _:c14n2 <ex:is> "Alice" . _:c14n3 <ex:dependsOn> _:c14n1 . 4: _:c14n0 <ex:contributesTo> _:c14n1 . _:c14n0 <ex:contributesTo> _:c14n2 . _:c14n2 <ex:dependsOn> _:c14n1 . _:c14n3 <ex:contributesTo> _:c14n1 . _:c14n3 <ex:notContributesTo> _:c14n2 . _:c14n4 <ex:contributesTo> _:c14n2 . _:c14n4 <ex:notContributesTo> _:c14n1 . 5: _:c14n1 <ex:contributesTo> _:c14n0 . _:c14n1 <ex:contributesTo> _:c14n3 . _:c14n1 <ex:is> "Bob" . _:c14n2 <ex:contributesTo> _:c14n0 . _:c14n2 <ex:is> "Alice" . _:c14n3 <ex:dependsOn> _:c14n0 . 6: _:c14n0 <ex:dependsOn> _:c14n1 . _:c14n2 <ex:contributesTo> _:c14n0 . _:c14n2 <ex:contributesTo> _:c14n1 . _:c14n2 <ex:is> "Bob" . _:c14n3 <ex:contributesTo> _:c14n1 . _:c14n3 <ex:is> "Alice" . _:c14n4 <ex:contributesTo> _:c14n0 . _:c14n4 <ex:is> "Charlie" .
func ParseNQuad ¶
ParseNQuad parses the statement and returns the corresponding Statement. All Term UID fields are zero on return.
func URDNA2015 ¶
URDNA2015 applies the Universal RDF Dataset Normalization Algorithm 2015 to the statements in src, placing the result in dst and returning it. If dst is nil a slice of statements will be allocated. If dst is not nil and not the same length as src, URDNA2015 will return an error.
See https://json-ld.github.io/rdf-dataset-canonicalization/spec/index.html for details.
Example ¶
package main import ( "fmt" "log" "strings" "gonum.org/v1/gonum/graph/formats/rdf" ) func main() { for _, statements := range []string{ ` _:a <ex:q> <ex:p> . _:b <ex:q> <ex:p> . _:c <ex:p> _:a . _:d <ex:p> _:b . _:c <ex:r> _:d . `, ` _:c1 <ex:p> _:a1 . _:b1 <ex:q> <ex:p> . _:d1 <ex:p> _:b1 . _:a1 <ex:q> <ex:p> . _:c1 <ex:r> _:d1 . `, } { // Decode the statement stream. dec := rdf.NewDecoder(strings.NewReader(statements)) var s []*rdf.Statement for { l, err := dec.Unmarshal() if err != nil { break } s = append(s, l) } relabeled, err := rdf.URDNA2015(nil, s) if err != nil { log.Fatal(err) } for _, s := range relabeled { fmt.Println(s) } fmt.Println() } }
Output: _:c14n0 <ex:p> _:c14n2 . _:c14n1 <ex:p> _:c14n3 . _:c14n1 <ex:r> _:c14n0 . _:c14n2 <ex:q> <ex:p> . _:c14n3 <ex:q> <ex:p> . _:c14n0 <ex:p> _:c14n2 . _:c14n1 <ex:p> _:c14n3 . _:c14n1 <ex:r> _:c14n0 . _:c14n2 <ex:q> <ex:p> . _:c14n3 <ex:q> <ex:p> .
func URGNA2012 ¶
URGNA2012 applies the Universal RDF Graph Normalization Algorithm 2012 to the statements in src, placing the result in dst and returning it. If dst is nil a slice of statements will be allocated. If dst is not nil and not the same length as src, URGNA2012 will return an error.
See https://json-ld.github.io/rdf-dataset-canonicalization/spec/index.html for details.
Example ¶
package main import ( "fmt" "log" "strings" "gonum.org/v1/gonum/graph/formats/rdf" ) func main() { for _, statements := range []string{ ` _:a <ex:q> <ex:p> . _:b <ex:q> <ex:p> . _:c <ex:p> _:a . _:d <ex:p> _:b . _:c <ex:r> _:d . `, ` _:c1 <ex:p> _:a1 . _:b1 <ex:q> <ex:p> . _:d1 <ex:p> _:b1 . _:a1 <ex:q> <ex:p> . _:c1 <ex:r> _:d1 . `, } { // Decode the statement stream. dec := rdf.NewDecoder(strings.NewReader(statements)) var s []*rdf.Statement for { l, err := dec.Unmarshal() if err != nil { break } s = append(s, l) } relabeled, err := rdf.URGNA2012(nil, s) if err != nil { log.Fatal(err) } for _, s := range relabeled { fmt.Println(s) } fmt.Println() } }
Output: _:c14n0 <ex:p> _:c14n3 . _:c14n0 <ex:r> _:c14n1 . _:c14n1 <ex:p> _:c14n2 . _:c14n2 <ex:q> <ex:p> . _:c14n3 <ex:q> <ex:p> . _:c14n0 <ex:p> _:c14n3 . _:c14n0 <ex:r> _:c14n1 . _:c14n1 <ex:p> _:c14n2 . _:c14n2 <ex:q> <ex:p> . _:c14n3 <ex:q> <ex:p> .
func (*Statement) ReversedEdge ¶
ReversedEdge returns the receiver unaltered. If there is a semantically valid edge reversal operation for the data, the user should implement this by wrapping Statement in a type performing that operation. See the ReversedLine example for details.
func (*Statement) ReversedLine ¶
ReversedLine returns the receiver unaltered. If there is a semantically valid line reversal operation for the data, the user should implement this by wrapping Statement in a type performing that operation.
Example ¶
package main import ( "fmt" "log" "strings" "gonum.org/v1/gonum/graph" "gonum.org/v1/gonum/graph/encoding" "gonum.org/v1/gonum/graph/encoding/dot" "gonum.org/v1/gonum/graph/formats/rdf" "gonum.org/v1/gonum/graph/multi" ) // foodNode implements graph.Node, dot.Node and encoding.Attributer // to allow the RDF term value to be given to the DOT encoder. type foodNode struct { rdf.Term } func (n foodNode) DOTID() string { text, _, kind, err := n.Term.Parts() if err != nil { return fmt.Sprintf("error:%s", n.Term.Value) } switch kind { case rdf.Blank: return n.Term.Value case rdf.IRI: return text case rdf.Literal: return fmt.Sprintf("%q", text) default: return fmt.Sprintf("invalid:%s", n.Term.Value) } } func (n foodNode) Attributes() []encoding.Attribute { _, qual, _, err := n.Term.Parts() if err != nil { return []encoding.Attribute{{Key: "error", Value: err.Error()}} } if qual == "" { return nil } parts := strings.Split(qual, ":") return []encoding.Attribute{{Key: parts[0], Value: parts[1]}} } // foodLine implements graph.Line and encoding.Attributer to // allow the line's RDF term value to be given to the DOT // encoder and for the nodes to be shimmed to the foodNode // type. // // It also implements line reversal for the semantics of // a food web with some taxonomic information. type foodLine struct { *rdf.Statement } func (l foodLine) From() graph.Node { return foodNode{l.Subject} } func (l foodLine) To() graph.Node { return foodNode{l.Object} } func (l foodLine) ReversedLine() graph.Line { if l.Predicate.Value == "<tax:is>" { // This should remain unreversed, so return as is. return l } s := *l.Statement // Reverse the line end points. s.Subject, s.Object = s.Object, s.Subject // Invert the semantics of the predicate. switch s.Predicate.Value { case "<eco:eats>": s.Predicate.Value = "<eco:eaten-by>" case "<eco:eaten-by>": s.Predicate.Value = "<eco:eats>" case "<tax:is-a>": s.Predicate.Value = "<tax:includes>" case "<tax:includes>": s.Predicate.Value = "<tax:is-a>" default: panic("invalid predicate") } // All IDs returned by the RDF parser are positive, so // sign reverse the edge ID to avoid any collisions. s.Predicate.UID *= -1 return foodLine{&s} } func (l foodLine) Attributes() []encoding.Attribute { text, _, _, err := l.Predicate.Parts() if err != nil { return []encoding.Attribute{{Key: "error", Value: err.Error()}} } parts := strings.Split(text, ":") return []encoding.Attribute{{Key: parts[0], Value: parts[1]}} } // expand copies src into dst, adding the reversal of each line if it is // distinct. func expand(dst, src *multi.DirectedGraph) { it := src.Edges() for it.Next() { lit := it.Edge().(multi.Edge) for lit.Next() { l := lit.Line() r := l.ReversedLine() dst.SetLine(l) if l == r { continue } dst.SetLine(r) } } } func main() { const statements = ` _:wolf <tax:is-a> _:animal . _:wolf <tax:is> "Wolf"^^<tax:common> . _:wolf <tax:is> "Canis lupus"^^<tax:binomial> . _:wolf <eco:eats> _:sheep . _:sheep <tax:is-a> _:animal . _:sheep <tax:is> "Sheep"^^<tax:common> . _:sheep <tax:is> "Ovis aries"^^<tax:binomial> . _:sheep <eco:eats> _:grass . _:grass <tax:is-a> _:plant . _:grass <tax:is> "Grass"^^<tax:common> . _:grass <tax:is> "Lolium perenne"^^<tax:binomial> . _:grass <tax:is> "Festuca rubra"^^<tax:binomial> . _:grass <tax:is> "Poa pratensis"^^<tax:binomial> . ` // Decode the statement stream and insert the lines into a multigraph. g := multi.NewDirectedGraph() dec := rdf.NewDecoder(strings.NewReader(statements)) for { l, err := dec.Unmarshal() if err != nil { break } // Wrap the line with a shim type to allow the RDF values // to be passed to the DOT marshaling routine. g.SetLine(foodLine{l}) } h := multi.NewDirectedGraph() expand(h, g) // Marshal the graph into DOT. b, err := dot.MarshalMulti(h, "food web", "", "\t") if err != nil { log.Fatal(err) } fmt.Printf("%s\n\n", b) }
Output: digraph "food web" { // Node definitions. "_:wolf"; "_:animal"; "Wolf" [tax=common]; "Canis lupus" [tax=binomial]; "_:sheep"; "Sheep" [tax=common]; "Ovis aries" [tax=binomial]; "_:grass"; "_:plant"; "Grass" [tax=common]; "Lolium perenne" [tax=binomial]; "Festuca rubra" [tax=binomial]; "Poa pratensis" [tax=binomial]; // Edge definitions. "_:wolf" -> "_:animal" [tax="is-a"]; "_:wolf" -> "Wolf" [tax=is]; "_:wolf" -> "Canis lupus" [tax=is]; "_:wolf" -> "_:sheep" [eco=eats]; "_:animal" -> "_:wolf" [tax=includes]; "_:animal" -> "_:sheep" [tax=includes]; "_:sheep" -> "_:wolf" [eco="eaten-by"]; "_:sheep" -> "_:animal" [tax="is-a"]; "_:sheep" -> "Sheep" [tax=is]; "_:sheep" -> "Ovis aries" [tax=is]; "_:sheep" -> "_:grass" [eco=eats]; "_:grass" -> "_:sheep" [eco="eaten-by"]; "_:grass" -> "_:plant" [tax="is-a"]; "_:grass" -> "Grass" [tax=is]; "_:grass" -> "Lolium perenne" [tax=is]; "_:grass" -> "Festuca rubra" [tax=is]; "_:grass" -> "Poa pratensis" [tax=is]; "_:plant" -> "_:grass" [tax=includes]; }
type Statements ¶
type Statements struct {
// contains filtered or unexported fields
}
Statements is an RDF statement iterator.
func (*Statements) Next ¶
func (s *Statements) Next() bool
Next returns whether the iterator holds any additional statements.
func (*Statements) Statement ¶
func (s *Statements) Statement() *Statement
Statement returns the current statement.
type Term ¶
type Term struct { // Value is the text value of term. Value string // UID is the unique ID for the term // in a collection of RDF terms. UID int64 }
Term is an RDF term. It implements the graph.Node interface.
func NewBlankTerm ¶
NewBlankTerm returns a Term based on the provided RDF blank node label. The label should not include the "_:" prefix. The returned Term will not have the UID set.
func NewIRITerm ¶
NewIRITerm returns a Term based on the provided IRI which must be valid and include a scheme. The returned Term will not have the UID set.
func NewLiteralTerm ¶
NewLiteralTerm returns a Term based on the literal text and an optional qualifier which may either be a "@"-prefixed language tag or a valid IRI. The text will be escaped if necessary and quoted, and if an IRI is given it will be escaped if necessary. The returned Term will not have the UID set.
func (Term) Parts ¶
Parts returns the parts of the term and the kind of the term. IRI node text is returned as a valid IRI with the quoting angle brackets removed and escape sequences interpreted, and blank nodes are stripped of the "_:" prefix. When the term is a literal, qual will either be empty, an unescaped IRI, or an RDF language tag prefixed with an @ symbol. The literal text is returned unquoted and unescaped.
Notes ¶
Bugs ¶
Graph leaning does not take into account graph label terms since the formal semantics for a multiple graph data model have not been defined. See https://www.w3.org/TR/rdf11-datasets/#declaring.