Source File
rdf.go
Belonging Package
github.com/freeeve/libcodex/rdf
// Package rdf is a small, fast, dependency-free RDF toolkit. It provides the RDF// triple model (Term, Triple, Graph), parsers and serializers for the four common// serializations — RDF/XML, JSON-LD, Turtle and N-Triples — and a streaming// decoder for the line-based formats.//// Two reading modes://// - Whole document: ParseRDFXML, ParseJSONLD, ParseTurtle and ParseNTriples take// a []byte and return a *Graph. Fast and convenient for inputs that fit in// memory. The line-based parsers have zero-copy variants (ParseNTriplesShared,// ParseNQuadsShared) that back terms with the caller's buffer instead of a// private copy — one input-sized allocation less, for callers that keep the// buffer immutable.// - Streaming: NewDecoder reads N-Triples, N-Quads, RDF/XML or Turtle from an// io.Reader one triple at a time in constant memory, for inputs too large to// materialize (e.g. the multi-gigabyte Library of Congress authority dumps).// JSON-LD is whole-document only.//// A parsed N-Quads document is a Dataset. Reading one of its named graphs has two// modes: Dataset.Graph materializes the graph's triples as a Graph, while// Dataset.GraphView answers the same queries (the GraphQuery surface) over// positions into the dataset's own quads, copying no triple. Prefer the view for// read-only access at corpus scale, where the copy dominates the allocation// profile.//// The parsers target the constructs real-world RDF uses rather than the whole of// each specification; see each parser's documentation for what is and isn't// handled (notably, relative-IRI resolution against a document base is not// performed). There are no third-party dependencies.package rdfimport ()// Well-known IRIs.const (NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"TypeIRI = NS + "type"FirstIRI = NS + "first"RestIRI = NS + "rest"NilIRI = NS + "nil"langString = NS + "langString"XSDString = "http://www.w3.org/2001/XMLSchema#string")// Kind distinguishes the three RDF term types.type Kind uint8const (IRI Kind = iotaBlankLiteral)// Term is an RDF term: an IRI, a blank node, or a literal.type Term struct {Kind KindValue string // IRI, blank-node identifier, or a literal's lexical formLang string // literal language tag (Literal only)Datatype string // literal datatype IRI (Literal only)}// NewIRI, NewBlank and NewLiteral construct the three term kinds.func ( string) Term { return Term{Kind: IRI, Value: } }func ( string) Term { return Term{Kind: Blank, Value: } }// NewLiteral makes a literal; an empty datatype defaults per RDF (xsd:string, or// rdf:langString when a language tag is present).func (, , string) Term {if == "" {if != "" {= langString} else {= XSDString}}return Term{Kind: Literal, Value: , Lang: , Datatype: }}// IsIRI, IsBlank and IsLiteral report the term kind.func ( Term) () bool { return .Kind == IRI }func ( Term) () bool { return .Kind == Blank }func ( Term) () bool { return .Kind == Literal }// Triple is an RDF statement.type Triple struct {S, P, O Term}// Graph holds the triples of one RDF graph in document order, with simple// lookup helpers built on first use. Term is comparable, so it indexes by// subject term directly — no key strings.//// Triples is a list, not a set. RDF 1.1 defines a graph as "a set of RDF// triples", but a serialization is free to state the same triple more than// once, and real ones do: LC's own marc2bibframe2 output repeats a shared// agency or vocabulary node under every property that references it, so one of// their N-Triples files carries 449 lines for 389 distinct triples. Parsing// preserves what the document said, which keeps the parse allocation-light and// keeps redundant statements visible to a caller who cares. It also means// len(g.Triples) can exceed the triple count a set-backed parser such as rdflib// reports for the same document.//// The query helpers below hide that: [Graph.Objects] and [Graph.SubjectsOfType]// return each distinct answer once. Callers who want set semantics over the// triples themselves can call [Graph.Dedupe], and [Graph.Canonical] already// collapses duplicates on its way to canonical form.type Graph struct {// Triples are in document order and may repeat; see the type doc.Triples []Triplespo map[Term][]int32 // subject -> positions in Triples, carved from one shared arena}// Dedupe removes duplicate triples in place, keeping the first occurrence of// each and preserving document order among the survivors, and reports how many// it removed. It gives the graph the set semantics RDF 1.1 ascribes to one, at// the cost of a hash of every triple — which is why parsing does not do it.func ( *Graph) () int {if len(.Triples) < 2 {return 0}:= make(map[Triple]struct{}, len(.Triples)):= .Triples[:0]for , := range .Triples {if , := []; {continue}[] = struct{}{}= append(, )}:= len(.Triples) - len()if > 0 {.Triples =.spo = nil // positions shifted; invalidate the index}return}// Add appends a triple.func ( *Graph) (, , Term) {.Triples = append(.Triples, Triple{, , }).spo = nil // invalidate the index}// index builds the lazy subject index in two passes: count the triples per// subject, then carve each subject's bucket from one shared arena at exactly// its final size. Buckets hold int32 positions into Triples rather than Triple// copies, so a corpus-scale build costs a few bytes per triple instead of the// hundreds that per-subject append growth over copied triples did. (int32// bounds a graph at 2^31 triples — hundreds of gigabytes of Triple values,// far past in-memory reach.)func ( *Graph) () {if .spo != nil {return}:= make(map[Term]int32, len(.Triples)/4+1)for := range .Triples {[.Triples[].S]++}:= make([]int32, 0, len(.Triples)):= make(map[Term][]int32, len())for := range .Triples {:= .Triples[].S, := []if ! {:= len() + int([])= [len():len():]= [:]}[] = append(, int32())}.spo =}// Objects returns the distinct objects of every triple with the given subject// and predicate IRI, in document order. A document that states the same triple// twice yields that object once: the answer is a property's value set, not a// count of how often the document repeated itself.func ( *Graph) ( Term, string) []Term {.index():= .spo[]var []Term:= objectSet{hint: len()}for , := range {if := &.Triples[]; .P.Kind == IRI && .P.Value == {= .append(, .O)}}return}// objectSetMapAbove is the result length past which objectSet stops scanning// linearly and builds a hash set. A property's value set is almost always a// handful of terms, so the scan wins for every realistic subject; the map only// exists so a pathological subject with thousands of objects cannot go// quadratic.const objectSetMapAbove = 16// objectSet accumulates the distinct objects of one (subject, predicate) as// they are found, cheaply for the small results that dominate and safely for// the large ones that do not. hint bounds the distinct objects from above --// the subject's whole index bucket -- and sizes the hash set in one allocation// if the result ever grows into one. The zero value degrades to an unsized map,// so callers should set hint.type objectSet struct {seen map[Term]struct{}hint int}// append adds o to out unless an equal term is already there, returning the// possibly-grown slice.func ( *objectSet) ( []Term, Term) []Term {if .seen != nil {if , := .seen[]; {return}.seen[] = struct{}{}return append(, )}if slices.Contains(, ) {return}if len() >= objectSetMapAbove {.seen = make(map[Term]struct{}, max(.hint, len()+1))for , := range {.seen[] = struct{}{}}.seen[] = struct{}{}}return append(, )}// ObjectsWithRepeats returns the objects of every triple with the given subject// and predicate IRI, statement for statement, in document order -- including a// term the document stated more than once.//// This is the raw list view, and it exists because [Graph.Triples] is a list.// Prefer [Graph.Objects]: a repeated statement carries no information in RDF, so// a caller that treats its multiplicity as meaningful is reading something the// abstract syntax does not say, and will disagree with any set-backed store that// touches the same document. The one honest use is inspecting a serialization as// written -- counting redundant statements, or round-tripping a positional// encoding that a conformant parser would already have flattened.func ( *Graph) ( Term, string) []Term {.index()var []Termfor , := range .spo[] {if := &.Triples[]; .P.Kind == IRI && .P.Value == {= append(, .O)}}return}// Object returns the first object for (subject, predicate), or false. It scans the// index without allocating an intermediate slice.func ( *Graph) ( Term, string) (Term, bool) {.index()for , := range .spo[] {if := &.Triples[]; .P.Kind == IRI && .P.Value == {return .O, true}}return Term{}, false}// HasType reports whether the subject has rdf:type typeIRI. It scans the index// without allocating.func ( *Graph) ( Term, string) bool {.index()for , := range .spo[] {if := &.Triples[]; .P.Kind == IRI && .P.Value == TypeIRI && .O.Kind == IRI && .O.Value == {return true}}return false}// Literal returns the value of the subject's first literal object for the// predicate, or false. It scans the index without allocating an intermediate// slice, serving the frequent single-value field reads.func ( *Graph) ( Term, string) (string, bool) {.index()for , := range .spo[] {if := &.Triples[]; .P.Kind == IRI && .P.Value == && .O.Kind == Literal {return .O.Value, true}}return "", false}// SubjectsOfType returns every subject with rdf:type typeIRI, in document order// (deduplicated).func ( *Graph) ( string) []Term {var []Term:= map[Term]bool{}for , := range .Triples {if .P.Kind == IRI && .P.Value == TypeIRI && .O.Kind == IRI && .O.Value == {if ![.S] {[.S] = true= append(, .S)}}}return}// LocalName returns the fragment or last path segment of an IRI (the part after// the final '#' or '/').func ( string) string {if := strings.LastIndexByte(, '#'); >= 0 {return [+1:]}if := strings.LastIndexByte(, '/'); >= 0 {return [+1:]}return}
![]() |
The pages are generated with Golds v0.8.7. (GOOS=linux GOARCH=amd64) Golds is a Go 101 project developed by Tapir Liu. PR and bug reports are welcome and can be submitted to the issue list. Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds. |