Merge pull request #333 from barakmich/label_queries
query/gremlin, graph/path: Add support for querying with labels
This commit is contained in:
commit
792fe5d42c
8 changed files with 145 additions and 21 deletions
|
|
@ -11,3 +11,5 @@
|
|||
<greg> <status> "cool_person" .
|
||||
<predicates> <are> <follows> .
|
||||
<predicates> <are> <status> .
|
||||
<emily> <status> "smart_person" "smart_graph" .
|
||||
<greg> <status> "smart_person" "smart_graph" .
|
||||
|
|
|
|||
|
|
@ -52,11 +52,11 @@ For these examples, suppose we have the following graph:
|
|||
+-------+ +------+
|
||||
| alice |----- ->| fred |<--
|
||||
+-------+ \---->+-------+-/ +------+ \-+-------+
|
||||
----->| #bob# | | | emily |
|
||||
----->| #bob# | | |*emily*|
|
||||
+---------+--/ --->+-------+ | +-------+
|
||||
| charlie | / v
|
||||
+---------+ / +--------+
|
||||
\--- +--------+ | #greg# |
|
||||
\--- +--------+ |*#greg#*|
|
||||
\-->| #dani# |------------>+--------+
|
||||
+--------+
|
||||
```
|
||||
|
|
@ -66,7 +66,11 @@ Where every link is a "follows" relationship, and the nodes with an extra `#` in
|
|||
```
|
||||
dani -- status --> cool_person
|
||||
```
|
||||
Perhaps these are the influencers in our community.
|
||||
Perhaps these are the influencers in our community. So too are extra `*`s in the name -- these are our smart people, according to the `smart_graph` label, eg, the quad:
|
||||
```
|
||||
greg status smart_person smart_graph .
|
||||
```
|
||||
|
||||
|
||||
|
||||
To load above graph into cayley and reproduce the following examples:
|
||||
|
|
@ -193,6 +197,31 @@ g.V().Has("follows", "bob")
|
|||
g.V("charlie").Out("follows").Has("follows", "fred")
|
||||
```
|
||||
|
||||
####**`path.LabelContext([labelPath], [tags])`**
|
||||
|
||||
Arguments:
|
||||
|
||||
* `predicatePath` (Optional): One of:
|
||||
* null or undefined: In future traversals, consider all edges, regardless of subgraph.
|
||||
* a string: The name of the subgraph to restrict traversals to.
|
||||
* a list of strings: A set of subgraphs to restrict traversals to.
|
||||
* a query path object: The target of which is a set of subgraphs.
|
||||
* `tags` (Optional): One of:
|
||||
* null or undefined: No tags
|
||||
* a string: A single tag to add the last traversed label to the output set.
|
||||
* a list of strings: Multiple tags to use as keys to save the label used to the output set.
|
||||
|
||||
Sets (or removes) the subgraph context to consider in the following traversals. Affects all In(), Out(), and Both() calls that follow it. The default LabelContext is null (all subgraphs)
|
||||
Example:
|
||||
```javascript
|
||||
// Find the status of people Dani follows
|
||||
g.V("dani").Out("follows").Out("status")
|
||||
// Find only the statuses provided by the smart_graph
|
||||
g.V("dani").Out("follows").LabelContext("smart_graph").Out("status")
|
||||
// Find all people followed by people with statuses in the smart_graph.
|
||||
g.V().LabelContext("smart_graph").In("status").LabelContext(null).In("follows")
|
||||
```
|
||||
|
||||
### Tagging
|
||||
|
||||
####**`path.Tag(tag)`**
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ package path
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/google/cayley/graph"
|
||||
"github.com/google/cayley/graph/iterator"
|
||||
|
|
@ -25,11 +26,14 @@ import (
|
|||
// join puts two iterators together by intersecting their result sets with an AND
|
||||
// Since we're using an and iterator, it's a good idea to put the smallest result
|
||||
// set first so that Next() produces fewer values to check Contains().
|
||||
func join(qs graph.QuadStore, itL, itR graph.Iterator) graph.Iterator {
|
||||
func join(qs graph.QuadStore, its ...graph.Iterator) graph.Iterator {
|
||||
and := iterator.NewAnd(qs)
|
||||
and.AddSubIterator(itL)
|
||||
and.AddSubIterator(itR)
|
||||
|
||||
for _, it := range its {
|
||||
if it == nil {
|
||||
continue
|
||||
}
|
||||
and.AddSubIterator(it)
|
||||
}
|
||||
return and
|
||||
}
|
||||
|
||||
|
|
@ -122,8 +126,9 @@ func outMorphism(tags []string, via ...interface{}) morphism {
|
|||
Reversal: func(ctx *context) (morphism, *context) { return inMorphism(tags, via...), ctx },
|
||||
Apply: func(qs graph.QuadStore, in graph.Iterator, ctx *context) (graph.Iterator, *context) {
|
||||
path := buildViaPath(qs, via...)
|
||||
return inOutIterator(path, in, false, tags), ctx
|
||||
return inOutIterator(path, in, false, tags, ctx), ctx
|
||||
},
|
||||
tags: tags,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -134,8 +139,9 @@ func inMorphism(tags []string, via ...interface{}) morphism {
|
|||
Reversal: func(ctx *context) (morphism, *context) { return outMorphism(tags, via...), ctx },
|
||||
Apply: func(qs graph.QuadStore, in graph.Iterator, ctx *context) (graph.Iterator, *context) {
|
||||
path := buildViaPath(qs, via...)
|
||||
return inOutIterator(path, in, true, tags), ctx
|
||||
return inOutIterator(path, in, true, tags, ctx), ctx
|
||||
},
|
||||
tags: tags,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -145,13 +151,38 @@ func bothMorphism(tags []string, via ...interface{}) morphism {
|
|||
Reversal: func(ctx *context) (morphism, *context) { return bothMorphism(tags, via...), ctx },
|
||||
Apply: func(qs graph.QuadStore, in graph.Iterator, ctx *context) (graph.Iterator, *context) {
|
||||
path := buildViaPath(qs, via...)
|
||||
inSide := inOutIterator(path, in, true, tags)
|
||||
outSide := inOutIterator(path, in.Clone(), false, tags)
|
||||
inSide := inOutIterator(path, in, true, tags, ctx)
|
||||
outSide := inOutIterator(path, in.Clone(), false, tags, ctx)
|
||||
or := iterator.NewOr()
|
||||
or.AddSubIterator(inSide)
|
||||
or.AddSubIterator(outSide)
|
||||
return or, ctx
|
||||
},
|
||||
tags: tags,
|
||||
}
|
||||
}
|
||||
|
||||
func labelContextMorphism(tags []string, via ...interface{}) morphism {
|
||||
var path *Path
|
||||
if len(via) == 0 {
|
||||
path = nil
|
||||
} else {
|
||||
path = buildViaPath(nil, via...)
|
||||
path = path.Tag(tags...)
|
||||
}
|
||||
return morphism{
|
||||
Name: "label_context",
|
||||
Reversal: func(ctx *context) (morphism, *context) {
|
||||
out := ctx.copy()
|
||||
ctx.labelSet = path
|
||||
return labelContextMorphism(tags, via...), &out
|
||||
},
|
||||
Apply: func(qs graph.QuadStore, in graph.Iterator, ctx *context) (graph.Iterator, *context) {
|
||||
out := ctx.copy()
|
||||
out.labelSet = path
|
||||
return in, &out
|
||||
},
|
||||
tags: tags,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -290,7 +321,7 @@ func buildSave(
|
|||
return join(qs, from, save)
|
||||
}
|
||||
|
||||
func inOutIterator(viaPath *Path, from graph.Iterator, inIterator bool, tags []string) graph.Iterator {
|
||||
func inOutIterator(viaPath *Path, from graph.Iterator, inIterator bool, tags []string, ctx *context) graph.Iterator {
|
||||
start, goal := quad.Subject, quad.Object
|
||||
if inIterator {
|
||||
start, goal = goal, start
|
||||
|
|
@ -303,8 +334,14 @@ func inOutIterator(viaPath *Path, from graph.Iterator, inIterator bool, tags []s
|
|||
|
||||
source := iterator.NewLinksTo(viaPath.qs, from, start)
|
||||
trail := iterator.NewLinksTo(viaPath.qs, viaIter, quad.Predicate)
|
||||
|
||||
route := join(viaPath.qs, source, trail)
|
||||
var label graph.Iterator
|
||||
if ctx != nil {
|
||||
if ctx.labelSet != nil {
|
||||
labeliter := ctx.labelSet.BuildIteratorOn(viaPath.qs)
|
||||
label = iterator.NewLinksTo(viaPath.qs, labeliter, quad.Label)
|
||||
}
|
||||
}
|
||||
route := join(viaPath.qs, source, trail, label)
|
||||
|
||||
return iterator.NewHasA(viaPath.qs, route, goal)
|
||||
}
|
||||
|
|
@ -330,7 +367,7 @@ func buildViaPath(qs graph.QuadStore, via ...interface{}) *Path {
|
|||
case string:
|
||||
return StartPath(qs, p)
|
||||
default:
|
||||
panic(fmt.Sprint("Invalid type passed to buildViaPath. ", p))
|
||||
panic(fmt.Sprintln("Invalid type passed to buildViaPath.", reflect.TypeOf(v), p))
|
||||
}
|
||||
}
|
||||
var strings []string
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ type morphism struct {
|
|||
Reversal func(*context) (morphism, *context)
|
||||
Apply applyMorphism
|
||||
tags []string
|
||||
context context
|
||||
}
|
||||
|
||||
// context allows a high-level change to the way paths are constructed. Some
|
||||
|
|
@ -51,6 +50,12 @@ type context struct {
|
|||
labelSet *Path
|
||||
}
|
||||
|
||||
func (c context) copy() context {
|
||||
return context{
|
||||
labelSet: c.labelSet,
|
||||
}
|
||||
}
|
||||
|
||||
// Path represents either a morphism (a pre-defined path stored for later use),
|
||||
// or a concrete path, consisting of a morphism and an underlying QuadStore.
|
||||
type Path struct {
|
||||
|
|
@ -266,6 +271,20 @@ func (p *Path) Has(via interface{}, nodes ...string) *Path {
|
|||
return p
|
||||
}
|
||||
|
||||
// LabelContext restricts the following operations (such as In, Out) to only
|
||||
// traverse edges that match the given set of labels.
|
||||
func (p *Path) LabelContext(via ...interface{}) *Path {
|
||||
p.stack = append(p.stack, labelContextMorphism(nil, via...))
|
||||
return p
|
||||
}
|
||||
|
||||
// LabelContextWithTags is exactly like LabelContext, except it tags the value
|
||||
// of the label used in the traversal with the tags provided.
|
||||
func (p *Path) LabelContextWithTags(tags []string, via ...interface{}) *Path {
|
||||
p.stack = append(p.stack, labelContextMorphism(tags, via...))
|
||||
return p
|
||||
}
|
||||
|
||||
// Back returns to a previously tagged place in the path. Any constraints applied after the Tag will remain in effect, but traversal continues from the tagged point instead, not from the end of the chain.
|
||||
//
|
||||
// For example:
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ func testSet(qs graph.QuadStore) []test {
|
|||
{
|
||||
message: "implicit All",
|
||||
path: StartPath(qs),
|
||||
expect: []string{"alice", "bob", "charlie", "dani", "emily", "fred", "greg", "follows", "status", "cool_person", "predicates", "are"},
|
||||
expect: []string{"alice", "bob", "charlie", "dani", "emily", "fred", "greg", "follows", "status", "cool_person", "predicates", "are", "smart_graph", "smart_person"},
|
||||
},
|
||||
{
|
||||
message: "follow",
|
||||
|
|
@ -178,7 +178,7 @@ func testSet(qs graph.QuadStore) []test {
|
|||
message: "show a simple save",
|
||||
path: StartPath(qs).Save("status", "somecool"),
|
||||
tag: "somecool",
|
||||
expect: []string{"cool_person", "cool_person", "cool_person"},
|
||||
expect: []string{"cool_person", "cool_person", "cool_person", "smart_person", "smart_person"},
|
||||
},
|
||||
{
|
||||
message: "show a simple saveR",
|
||||
|
|
@ -228,6 +228,22 @@ func testSet(qs graph.QuadStore) []test {
|
|||
path: StartPath(qs, "fred").FollowReverse(grandfollows),
|
||||
expect: []string{"alice", "charlie", "dani"},
|
||||
},
|
||||
// Context tests
|
||||
{
|
||||
message: "query without label limitation",
|
||||
path: StartPath(qs, "greg").Out("status"),
|
||||
expect: []string{"smart_person", "cool_person"},
|
||||
},
|
||||
{
|
||||
message: "query with label limitation",
|
||||
path: StartPath(qs, "greg").LabelContext("smart_graph").Out("status"),
|
||||
expect: []string{"smart_person"},
|
||||
},
|
||||
{
|
||||
message: "reverse context",
|
||||
path: StartPath(qs, "greg").Tag("base").LabelContext("smart_graph").Out("status").Tag("status").Back("base"),
|
||||
expect: []string{"greg"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -163,6 +163,12 @@ func buildPathFromObject(obj *otto.Object) *path.Path {
|
|||
return p.InPredicates()
|
||||
case "OutPredicates":
|
||||
return p.OutPredicates()
|
||||
case "LabelContext":
|
||||
labels, tags, ok := getViaData(obj)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return p.LabelContextWithTags(tags, labels...)
|
||||
default:
|
||||
panic(fmt.Sprint("Unimplemented Gremlin function", gremlinType))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ var testQueries = []struct {
|
|||
g.V().Save("status", "somecool").All()
|
||||
`,
|
||||
tag: "somecool",
|
||||
expect: []string{"cool_person", "cool_person", "cool_person"},
|
||||
expect: []string{"cool_person", "cool_person", "cool_person", "smart_person", "smart_person"},
|
||||
},
|
||||
{
|
||||
message: "show a simple saveR",
|
||||
|
|
@ -278,12 +278,26 @@ var testQueries = []struct {
|
|||
`,
|
||||
expect: []string{"are", "follows", "status"},
|
||||
},
|
||||
{
|
||||
message: "traverse using LabelContext",
|
||||
query: `
|
||||
g.V("greg").LabelContext("smart_graph").Out("status").All()
|
||||
`,
|
||||
expect: []string{"smart_person"},
|
||||
},
|
||||
{
|
||||
message: "open and close a LabelContext",
|
||||
query: `
|
||||
g.V().LabelContext("smart_graph").In("status").LabelContext(null).In("follows").All()
|
||||
`,
|
||||
expect: []string{"dani", "fred"},
|
||||
},
|
||||
}
|
||||
|
||||
func runQueryGetTag(g []quad.Quad, query string, tag string) []string {
|
||||
js := makeTestSession(g)
|
||||
c := make(chan interface{}, 5)
|
||||
js.Execute(query, c, -1)
|
||||
c := make(chan interface{}, 1)
|
||||
go js.Execute(query, c, -1)
|
||||
|
||||
var results []string
|
||||
for res := range c {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ var traversals = []string{
|
|||
"Difference",
|
||||
"InPredicates",
|
||||
"OutPredicates",
|
||||
"LabelContext",
|
||||
}
|
||||
|
||||
func (wk *worker) embedTraversals(env *otto.Otto, obj *otto.Object) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue