This is an automated email from the ASF dual-hosted git repository. spmallette pushed a commit to branch TINKERPOP-3158 in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 2c178e9144a0a8d017df69620601e0fb0f6010b1 Author: Stephen Mallette <[email protected]> AuthorDate: Tue May 6 10:59:07 2025 -0400 Add ServiceRegistry for vector search with call() --- .../gremlin/structure/service/ServiceRegistry.java | 3 +- .../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs | 22 +-- gremlin-go/driver/cucumber/gremlin.go | 22 +-- .../gremlin-javascript/test/cucumber/gremlin.js | 22 +-- gremlin-python/src/main/python/radish/gremlin.py | 22 +-- .../gremlin/test/features/map/Call.feature | 38 ++--- .../services/TinkerVectorSearchFactory.java | 39 +++-- .../tinkergraph/structure/AbstractTinkerIndex.java | 2 +- .../structure/AbstractTinkerVectorIndex.java | 2 +- .../gremlin/tinkergraph/structure/TinkerGraph.java | 1 - .../tinkergraph/structure/TinkerIndexElement.java | 5 + .../structure/TinkerGraphServiceTest.java | 184 ++++++++++++++++++--- 12 files changed, 262 insertions(+), 100 deletions(-) diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/service/ServiceRegistry.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/service/ServiceRegistry.java index aae9bed8bf..83faccb1e2 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/service/ServiceRegistry.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/service/ServiceRegistry.java @@ -25,7 +25,6 @@ import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator; import org.apache.tinkerpop.shaded.jackson.core.JsonProcessingException; import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; @@ -87,7 +86,7 @@ public class ServiceRegistry implements DirectoryService, AutoCloseable { * {@link DirectoryService} execution. List or describe the registered callable services. */ @Override - public CloseableIterator execute(ServiceCallContext ctx, final Map params) { + public CloseableIterator execute(final ServiceCallContext ctx, final Map params) { final boolean verbose = (boolean) params.getOrDefault(Params.VERBOSE, false); final String serviceName = (String) params.get(Params.SERVICE); diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs index 774cd7043c..c44f70ae56 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs @@ -634,18 +634,18 @@ namespace Gremlin.Net.IntegrationTest.Gherkin {"g_V_hasLabelXpersonX_valuesXageX_asString_concatX_years_oldX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().HasLabel("person").Values<object>("age").AsString().Concat(" years old")}}, {"g_call", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>()}}, {"g_callXlistX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>((string) "--list")}}, - {"g_callXlistX_withXstring_stringX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>((string) "--list").With("service", "tinker.search")}}, - {"g_callXlistX_withXstring_traversalX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>((string) "--list").With("service", __.Constant<object>("tinker.search"))}}, + {"g_callXlistX_withXstring_stringX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>((string) "--list").With("service", "tinker.search.text")}}, + {"g_callXlistX_withXstring_traversalX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>((string) "--list").With("service", __.Constant<object>("tinker.search.text"))}}, {"g_callXlist_mapX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>("--list", (IDictionary<object, object>) p["xx1"])}}, - {"g_callXlist_traversalX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>("--list", (ITraversal) __.Project<object>("service").By(__.Constant<object>("tinker.search")))}}, - {"g_callXlist_map_traversalX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>("--list", (IDictionary<object, object>) p["xx1"], (ITraversal) __.Project<object>("service").By(__.Constant<object>("tinker.search")))}}, - {"g_callXsearch_mapX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>("tinker.search", (IDictionary<object, object>) p["xx1"]).Element()}}, - {"g_callXsearch_traversalX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>("tinker.search", (ITraversal) __.Project<object>("search").By(__.Constant<object>("vada"))).Element()}}, - {"g_callXsearchX_withXstring_stringX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>((string) "tinker.search").With("search", "vada").Element()}}, - {"g_callXsearchX_withXstring_traversalX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>((string) "tinker.search").With("search", __.Constant<object>("vada")).Element()}}, - {"g_callXsearch_mapX_withXstring_VertexX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>("tinker.search", (IDictionary<object, object>) p["xx1"]).With("type", "Vertex").Element()}}, - {"g_callXsearch_mapX_withXstring_EdgeX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>("tinker.search", (IDictionary<object, object>) p["xx1"]).With("type", "Edge").Element()}}, - {"g_callXsearch_mapX_withXstring_VertexPropertyX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>("tinker.search", (IDictionary<object, object>) p["xx1"]).With("type", "VertexProperty").Element()}}, + {"g_callXlist_traversalX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>("--list", (ITraversal) __.Project<object>("service").By(__.Constant<object>("tinker.search.text")))}}, + {"g_callXlist_map_traversalX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>("--list", (IDictionary<object, object>) p["xx1"], (ITraversal) __.Project<object>("service").By(__.Constant<object>("tinker.search.text")))}}, + {"g_callXsearch_mapX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>("tinker.search.text", (IDictionary<object, object>) p["xx1"]).Element()}}, + {"g_callXsearch_traversalX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>("tinker.search.text", (ITraversal) __.Project<object>("search").By(__.Constant<object>("vada"))).Element()}}, + {"g_callXsearchX_withXstring_stringX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>((string) "tinker.search.text").With("search", "vada").Element()}}, + {"g_callXsearchX_withXstring_traversalX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>((string) "tinker.search.text").With("search", __.Constant<object>("vada")).Element()}}, + {"g_callXsearch_mapX_withXstring_VertexX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>("tinker.search.text", (IDictionary<object, object>) p["xx1"]).With("type", "Vertex").Element()}}, + {"g_callXsearch_mapX_withXstring_EdgeX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>("tinker.search.text", (IDictionary<object, object>) p["xx1"]).With("type", "Edge").Element()}}, + {"g_callXsearch_mapX_withXstring_VertexPropertyX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Call<object>("tinker.search.text", (IDictionary<object, object>) p["xx1"]).With("type", "VertexProperty").Element()}}, {"g_V_callXdcX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().As("v").Call<object>((string) "tinker.degree.centrality").Project<object>("vertex", "degree").By(__.Select<object>("v")).By()}}, {"g_V_whereXcallXdcXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Where(__.Call<object>((string) "tinker.degree.centrality").Is(3))}}, {"g_V_callXdcX_withXdirection_OUTX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().As("v").Call<object>((string) "tinker.degree.centrality").With("direction", Direction.Out).Project<object>("vertex", "degree").By(__.Select<object>("v")).By()}}, diff --git a/gremlin-go/driver/cucumber/gremlin.go b/gremlin-go/driver/cucumber/gremlin.go index 17ec0da51c..8796f5f315 100644 --- a/gremlin-go/driver/cucumber/gremlin.go +++ b/gremlin-go/driver/cucumber/gremlin.go @@ -603,18 +603,18 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[ "g_V_hasLabelXpersonX_valuesXageX_asString_concatX_years_oldX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("person").Values("age").AsString().Concat(" years old")}}, "g_call": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call()}}, "g_callXlistX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("--list")}}, - "g_callXlistX_withXstring_stringX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("--list").With("service", "tinker.search")}}, - "g_callXlistX_withXstring_traversalX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("--list").With("service", gremlingo.T__.Constant("tinker.search"))}}, + "g_callXlistX_withXstring_stringX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("--list").With("service", "tinker.search.text")}}, + "g_callXlistX_withXstring_traversalX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("--list").With("service", gremlingo.T__.Constant("tinker.search.text"))}}, "g_callXlist_mapX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("--list", p["xx1"])}}, - "g_callXlist_traversalX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("--list", gremlingo.T__.Project("service").By(gremlingo.T__.Constant("tinker.search")))}}, - "g_callXlist_map_traversalX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("--list", p["xx1"], gremlingo.T__.Project("service").By(gremlingo.T__.Constant("tinker.search")))}}, - "g_callXsearch_mapX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("tinker.search", p["xx1"]).Element()}}, - "g_callXsearch_traversalX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("tinker.search", gremlingo.T__.Project("search").By(gremlingo.T__.Constant("vada"))).Element()}}, - "g_callXsearchX_withXstring_stringX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("tinker.search").With("search", "vada").Element()}}, - "g_callXsearchX_withXstring_traversalX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("tinker.search").With("search", gremlingo.T__.Constant("vada")).Element()}}, - "g_callXsearch_mapX_withXstring_VertexX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("tinker.search", p["xx1"]).With("type", "Vertex").Element()}}, - "g_callXsearch_mapX_withXstring_EdgeX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("tinker.search", p["xx1"]).With("type", "Edge").Element()}}, - "g_callXsearch_mapX_withXstring_VertexPropertyX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("tinker.search", p["xx1"]).With("type", "VertexProperty").Element()}}, + "g_callXlist_traversalX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("--list", gremlingo.T__.Project("service").By(gremlingo.T__.Constant("tinker.search.text")))}}, + "g_callXlist_map_traversalX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("--list", p["xx1"], gremlingo.T__.Project("service").By(gremlingo.T__.Constant("tinker.search.text")))}}, + "g_callXsearch_mapX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("tinker.search.text", p["xx1"]).Element()}}, + "g_callXsearch_traversalX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("tinker.search.text", gremlingo.T__.Project("search").By(gremlingo.T__.Constant("vada"))).Element()}}, + "g_callXsearchX_withXstring_stringX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("tinker.search.text").With("search", "vada").Element()}}, + "g_callXsearchX_withXstring_traversalX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("tinker.search.text").With("search", gremlingo.T__.Constant("vada")).Element()}}, + "g_callXsearch_mapX_withXstring_VertexX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("tinker.search.text", p["xx1"]).With("type", "Vertex").Element()}}, + "g_callXsearch_mapX_withXstring_EdgeX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("tinker.search.text", p["xx1"]).With("type", "Edge").Element()}}, + "g_callXsearch_mapX_withXstring_VertexPropertyX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Call("tinker.search.text", p["xx1"]).With("type", "VertexProperty").Element()}}, "g_V_callXdcX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().As("v").Call("tinker.degree.centrality").Project("vertex", "degree").By(gremlingo.T__.Select("v")).By()}}, "g_V_whereXcallXdcXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Where(gremlingo.T__.Call("tinker.degree.centrality").Is(3))}}, "g_V_callXdcX_withXdirection_OUTX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().As("v").Call("tinker.degree.centrality").With("direction", gremlingo.Direction.Out).Project("vertex", "degree").By(gremlingo.T__.Select("v")).By()}}, diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js index c261f4f92d..b49f5a43aa 100644 --- a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js @@ -633,18 +633,18 @@ const gremlins = { g_V_hasLabelXpersonX_valuesXageX_asString_concatX_years_oldX: [function({g}) { return g.V().hasLabel("person").values("age").asString().concat(" years old") }], g_call: [function({g}) { return g.call() }], g_callXlistX: [function({g}) { return g.call("--list") }], - g_callXlistX_withXstring_stringX: [function({g}) { return g.call("--list").with_("service", "tinker.search") }], - g_callXlistX_withXstring_traversalX: [function({g}) { return g.call("--list").with_("service", __.constant("tinker.search")) }], + g_callXlistX_withXstring_stringX: [function({g}) { return g.call("--list").with_("service", "tinker.search.text") }], + g_callXlistX_withXstring_traversalX: [function({g}) { return g.call("--list").with_("service", __.constant("tinker.search.text")) }], g_callXlist_mapX: [function({g, xx1}) { return g.call("--list", xx1) }], - g_callXlist_traversalX: [function({g}) { return g.call("--list", __.project("service").by(__.constant("tinker.search"))) }], - g_callXlist_map_traversalX: [function({g, xx1}) { return g.call("--list", xx1, __.project("service").by(__.constant("tinker.search"))) }], - g_callXsearch_mapX: [function({g, xx1}) { return g.call("tinker.search", xx1).element() }], - g_callXsearch_traversalX: [function({g}) { return g.call("tinker.search", __.project("search").by(__.constant("vada"))).element() }], - g_callXsearchX_withXstring_stringX: [function({g}) { return g.call("tinker.search").with_("search", "vada").element() }], - g_callXsearchX_withXstring_traversalX: [function({g}) { return g.call("tinker.search").with_("search", __.constant("vada")).element() }], - g_callXsearch_mapX_withXstring_VertexX: [function({g, xx1}) { return g.call("tinker.search", xx1).with_("type", "Vertex").element() }], - g_callXsearch_mapX_withXstring_EdgeX: [function({g, xx1}) { return g.call("tinker.search", xx1).with_("type", "Edge").element() }], - g_callXsearch_mapX_withXstring_VertexPropertyX: [function({g, xx1}) { return g.call("tinker.search", xx1).with_("type", "VertexProperty").element() }], + g_callXlist_traversalX: [function({g}) { return g.call("--list", __.project("service").by(__.constant("tinker.search.text"))) }], + g_callXlist_map_traversalX: [function({g, xx1}) { return g.call("--list", xx1, __.project("service").by(__.constant("tinker.search.text"))) }], + g_callXsearch_mapX: [function({g, xx1}) { return g.call("tinker.search.text", xx1).element() }], + g_callXsearch_traversalX: [function({g}) { return g.call("tinker.search.text", __.project("search").by(__.constant("vada"))).element() }], + g_callXsearchX_withXstring_stringX: [function({g}) { return g.call("tinker.search.text").with_("search", "vada").element() }], + g_callXsearchX_withXstring_traversalX: [function({g}) { return g.call("tinker.search.text").with_("search", __.constant("vada")).element() }], + g_callXsearch_mapX_withXstring_VertexX: [function({g, xx1}) { return g.call("tinker.search.text", xx1).with_("type", "Vertex").element() }], + g_callXsearch_mapX_withXstring_EdgeX: [function({g, xx1}) { return g.call("tinker.search.text", xx1).with_("type", "Edge").element() }], + g_callXsearch_mapX_withXstring_VertexPropertyX: [function({g, xx1}) { return g.call("tinker.search.text", xx1).with_("type", "VertexProperty").element() }], g_V_callXdcX: [function({g}) { return g.V().as("v").call("tinker.degree.centrality").project("vertex", "degree").by(__.select("v")).by() }], g_V_whereXcallXdcXX: [function({g}) { return g.V().where(__.call("tinker.degree.centrality").is(3)) }], g_V_callXdcX_withXdirection_OUTX: [function({g}) { return g.V().as("v").call("tinker.degree.centrality").with_("direction", Direction.OUT).project("vertex", "degree").by(__.select("v")).by() }], diff --git a/gremlin-python/src/main/python/radish/gremlin.py b/gremlin-python/src/main/python/radish/gremlin.py index 4985d650bc..bf3a144b73 100644 --- a/gremlin-python/src/main/python/radish/gremlin.py +++ b/gremlin-python/src/main/python/radish/gremlin.py @@ -606,18 +606,18 @@ world.gremlins = { 'g_V_hasLabelXpersonX_valuesXageX_asString_concatX_years_oldX': [(lambda g:g.V().has_label('person').values('age').as_string().concat(' years old'))], 'g_call': [(lambda g:g.call())], 'g_callXlistX': [(lambda g:g.call('--list'))], - 'g_callXlistX_withXstring_stringX': [(lambda g:g.call('--list').with_('service', 'tinker.search'))], - 'g_callXlistX_withXstring_traversalX': [(lambda g:g.call('--list').with_('service', __.constant('tinker.search')))], + 'g_callXlistX_withXstring_stringX': [(lambda g:g.call('--list').with_('service', 'tinker.search.text'))], + 'g_callXlistX_withXstring_traversalX': [(lambda g:g.call('--list').with_('service', __.constant('tinker.search.text')))], 'g_callXlist_mapX': [(lambda g, xx1=None:g.call('--list', xx1))], - 'g_callXlist_traversalX': [(lambda g:g.call('--list', __.project('service').by(__.constant('tinker.search'))))], - 'g_callXlist_map_traversalX': [(lambda g, xx1=None:g.call('--list', xx1, __.project('service').by(__.constant('tinker.search'))))], - 'g_callXsearch_mapX': [(lambda g, xx1=None:g.call('tinker.search', xx1).element())], - 'g_callXsearch_traversalX': [(lambda g:g.call('tinker.search', __.project('search').by(__.constant('vada'))).element())], - 'g_callXsearchX_withXstring_stringX': [(lambda g:g.call('tinker.search').with_('search', 'vada').element())], - 'g_callXsearchX_withXstring_traversalX': [(lambda g:g.call('tinker.search').with_('search', __.constant('vada')).element())], - 'g_callXsearch_mapX_withXstring_VertexX': [(lambda g, xx1=None:g.call('tinker.search', xx1).with_('type', 'Vertex').element())], - 'g_callXsearch_mapX_withXstring_EdgeX': [(lambda g, xx1=None:g.call('tinker.search', xx1).with_('type', 'Edge').element())], - 'g_callXsearch_mapX_withXstring_VertexPropertyX': [(lambda g, xx1=None:g.call('tinker.search', xx1).with_('type', 'VertexProperty').element())], + 'g_callXlist_traversalX': [(lambda g:g.call('--list', __.project('service').by(__.constant('tinker.search.text'))))], + 'g_callXlist_map_traversalX': [(lambda g, xx1=None:g.call('--list', xx1, __.project('service').by(__.constant('tinker.search.text'))))], + 'g_callXsearch_mapX': [(lambda g, xx1=None:g.call('tinker.search.text', xx1).element())], + 'g_callXsearch_traversalX': [(lambda g:g.call('tinker.search.text', __.project('search').by(__.constant('vada'))).element())], + 'g_callXsearchX_withXstring_stringX': [(lambda g:g.call('tinker.search.text').with_('search', 'vada').element())], + 'g_callXsearchX_withXstring_traversalX': [(lambda g:g.call('tinker.search.text').with_('search', __.constant('vada')).element())], + 'g_callXsearch_mapX_withXstring_VertexX': [(lambda g, xx1=None:g.call('tinker.search.text', xx1).with_('type', 'Vertex').element())], + 'g_callXsearch_mapX_withXstring_EdgeX': [(lambda g, xx1=None:g.call('tinker.search.text', xx1).with_('type', 'Edge').element())], + 'g_callXsearch_mapX_withXstring_VertexPropertyX': [(lambda g, xx1=None:g.call('tinker.search.text', xx1).with_('type', 'VertexProperty').element())], 'g_V_callXdcX': [(lambda g:g.V().as_('v').call('tinker.degree.centrality').project('vertex', 'degree').by(__.select('v')).by())], 'g_V_whereXcallXdcXX': [(lambda g:g.V().where(__.call('tinker.degree.centrality').is_(3)))], 'g_V_callXdcX_withXdirection_OUTX': [(lambda g:g.V().as_('v').call('tinker.degree.centrality').with_('direction', Direction.OUT).project('vertex', 'degree').by(__.select('v')).by())], diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Call.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Call.feature index 0c44d7e952..ce80e6f425 100644 --- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Call.feature +++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Call.feature @@ -27,7 +27,7 @@ Feature: Step - call() When iterated to list Then the result should be unordered | result | - | tinker.search | + | tinker.search.text | | tinker.degree.centrality | Scenario: g_callXlistX @@ -39,34 +39,34 @@ Feature: Step - call() When iterated to list Then the result should be unordered | result | - | tinker.search | + | tinker.search.text | | tinker.degree.centrality | Scenario: g_callXlistX_withXstring_stringX Given the empty graph And the traversal of """ - g.call("--list").with("service", "tinker.search") + g.call("--list").with("service", "tinker.search.text") """ When iterated to list Then the result should be unordered | result | - | tinker.search | + | tinker.search.text | Scenario: g_callXlistX_withXstring_traversalX Given the empty graph And the traversal of """ - g.call("--list").with("service", __.constant("tinker.search")) + g.call("--list").with("service", __.constant("tinker.search.text")) """ When iterated to list Then the result should be unordered | result | - | tinker.search | + | tinker.search.text | Scenario: g_callXlist_mapX Given the empty graph - And using the parameter xx1 defined as "m[{\"service\": \"tinker.search\"}]" + And using the parameter xx1 defined as "m[{\"service\": \"tinker.search.text\"}]" And the traversal of """ g.call("--list", xx1) @@ -74,37 +74,37 @@ Feature: Step - call() When iterated to list Then the result should be unordered | result | - | tinker.search | + | tinker.search.text | Scenario: g_callXlist_traversalX Given the empty graph And the traversal of """ - g.call("--list", __.project("service").by(__.constant("tinker.search"))) + g.call("--list", __.project("service").by(__.constant("tinker.search.text"))) """ When iterated to list Then the result should be unordered | result | - | tinker.search | + | tinker.search.text | Scenario: g_callXlist_map_traversalX Given the empty graph And using the parameter xx1 defined as "m[{\"x\": \"y\"}]" And the traversal of """ - g.call("--list", xx1, __.project("service").by(__.constant("tinker.search"))) + g.call("--list", xx1, __.project("service").by(__.constant("tinker.search.text"))) """ When iterated to list Then the result should be unordered | result | - | tinker.search | + | tinker.search.text | Scenario: g_callXsearch_mapX Given the modern graph And using the parameter xx1 defined as "m[{\"search\": \"mar\"}]" And the traversal of """ - g.call("tinker.search", xx1).element() + g.call("tinker.search.text", xx1).element() """ When iterated to list Then the result should be unordered @@ -115,7 +115,7 @@ Feature: Step - call() Given the modern graph And the traversal of """ - g.call("tinker.search", __.project("search").by(__.constant("vada"))).element() + g.call("tinker.search.text", __.project("search").by(__.constant("vada"))).element() """ When iterated to list Then the result should be unordered @@ -126,7 +126,7 @@ Feature: Step - call() Given the modern graph And the traversal of """ - g.call("tinker.search").with("search", "vada").element() + g.call("tinker.search.text").with("search", "vada").element() """ When iterated to list Then the result should be unordered @@ -137,7 +137,7 @@ Feature: Step - call() Given the modern graph And the traversal of """ - g.call("tinker.search").with("search", __.constant("vada")).element() + g.call("tinker.search.text").with("search", __.constant("vada")).element() """ When iterated to list Then the result should be unordered @@ -149,7 +149,7 @@ Feature: Step - call() And using the parameter xx1 defined as "m[{\"search\": \"mar\"}]" And the traversal of """ - g.call("tinker.search", xx1).with("type", "Vertex").element() + g.call("tinker.search.text", xx1).with("type", "Vertex").element() """ When iterated to list Then the result should be unordered @@ -161,7 +161,7 @@ Feature: Step - call() And using the parameter xx1 defined as "m[{\"search\": \"mar\"}]" And the traversal of """ - g.call("tinker.search", xx1).with("type", "Edge").element() + g.call("tinker.search.text", xx1).with("type", "Edge").element() """ When iterated to list Then the result should be empty @@ -171,7 +171,7 @@ Feature: Step - call() And using the parameter xx1 defined as "m[{\"search\": \"mar\"}]" And the traversal of """ - g.call("tinker.search", xx1).with("type", "VertexProperty").element() + g.call("tinker.search.text", xx1).with("type", "VertexProperty").element() """ When iterated to list Then the result should be empty diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/services/TinkerVectorSearchFactory.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/services/TinkerVectorSearchFactory.java index fb0b3c76f4..b0a793a814 100644 --- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/services/TinkerVectorSearchFactory.java +++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/services/TinkerVectorSearchFactory.java @@ -19,37 +19,28 @@ package org.apache.tinkerpop.gremlin.tinkergraph.services; import org.apache.tinkerpop.gremlin.process.traversal.Traverser; -import org.apache.tinkerpop.gremlin.structure.Direction; import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.Element; -import org.apache.tinkerpop.gremlin.structure.Property; import org.apache.tinkerpop.gremlin.structure.Vertex; -import org.apache.tinkerpop.gremlin.structure.VertexProperty; import org.apache.tinkerpop.gremlin.structure.service.Service; import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator; import org.apache.tinkerpop.gremlin.tinkergraph.structure.AbstractTinkerGraph; -import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerHelper; import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIndexElement; -import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils; import java.util.Collections; -import java.util.Iterator; import java.util.Map; -import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.LongStream; import static org.apache.tinkerpop.gremlin.tinkergraph.services.TinkerVectorSearchFactory.Params.KEY; import static org.apache.tinkerpop.gremlin.tinkergraph.services.TinkerVectorSearchFactory.Params.TOP_K; import static org.apache.tinkerpop.gremlin.util.CollectionUtil.asMap; /** - * + * Service to utilize a {@code TinkerVertexIndex} to do a vector search. */ public class TinkerVectorSearchFactory extends TinkerServiceRegistry.TinkerServiceFactory<Element, Map<String, Object>> implements Service<Element, Map<String, Object>> { - public static final String NAME = "tinker.search.vector.topKByVertex"; + public static final String NAME = "tinker.search.vector.topKByElement"; public interface Params { /** @@ -91,16 +82,38 @@ public class TinkerVectorSearchFactory extends TinkerServiceRegistry.TinkerServi if (isStart) { throw new UnsupportedOperationException(Exceptions.cannotStartTraversal); } + + if (!params.containsKey(KEY)) { + throw new IllegalArgumentException("The parameter map must contain the key where the embedding is: " + KEY); + } + return this; } @Override public CloseableIterator<Map<String,Object>> execute(final ServiceCallContext ctx, final Traverser.Admin<Element> in, final Map params) { final String key = (String) params.get(KEY); - final int k = (int) params.getOrDefault(TOP_K, 10); + + // add 1 because we always filter 1 out of the index because it will return a match on itself + final int k = (int) params.getOrDefault(TOP_K, 10) + 1; final Element e = in.get(); + + // if the current element does not have the specified key, then return no results + if (!e.keys().contains(key)) + return CloseableIterator.empty(); + final float[] embedding = e.value(key); - return CloseableIterator.of(graph.findNearestVertices(key, embedding, k).stream().map(TinkerIndexElement::toMap).iterator()); + if (e instanceof Vertex) { + return CloseableIterator.of(graph.findNearestVertices(key, embedding, k).stream() + .filter(tie -> !tie.getElement().equals(e)) + .map(TinkerIndexElement::toMap).iterator()); + } else if (e instanceof Edge) { + return CloseableIterator.of(graph.findNearestEdges(key, embedding, k).stream() + .filter(tie -> !tie.getElement().equals(e)) + .map(TinkerIndexElement::toMap).iterator()); + } else { + return CloseableIterator.empty(); + } } @Override diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerIndex.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerIndex.java index a1460d9fde..e0295e2599 100644 --- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerIndex.java +++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerIndex.java @@ -34,7 +34,7 @@ import java.util.Set; * * @author Valentyn Kahamlyk */ -public abstract class AbstractTinkerIndex<T extends Element> { +abstract class AbstractTinkerIndex<T extends Element> { protected final Class<T> indexClass; protected final AbstractTinkerGraph graph; diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerVectorIndex.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerVectorIndex.java index 7826d402e8..9141513daf 100644 --- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerVectorIndex.java +++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/AbstractTinkerVectorIndex.java @@ -27,7 +27,7 @@ import java.util.List; * * @param <T> the type of elements stored in the vector index */ -public abstract class AbstractTinkerVectorIndex<T extends Element> extends AbstractTinkerIndex<T> { +abstract class AbstractTinkerVectorIndex<T extends Element> extends AbstractTinkerIndex<T> { /** * Default number of nearest neighbors to return */ diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraph.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraph.java index 444fa74ecd..f26a386e3a 100644 --- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraph.java +++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraph.java @@ -99,7 +99,6 @@ public class TinkerGraph extends AbstractTinkerGraph { if (graphLocation != null) loadGraph(); serviceRegistry = new TinkerServiceRegistry(this); - serviceRegistry.registerService(new TinkerServiceRegistry(this)); configuration.getList(String.class, GREMLIN_TINKERGRAPH_SERVICE, Collections.emptyList()).forEach(serviceClass -> serviceRegistry.registerService(instantiate(serviceClass))); } diff --git a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerIndexElement.java b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerIndexElement.java index d8e43afb4c..24fb22d144 100644 --- a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerIndexElement.java +++ b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerIndexElement.java @@ -18,9 +18,14 @@ */ package org.apache.tinkerpop.gremlin.tinkergraph.structure; +import org.apache.tinkerpop.gremlin.structure.Element; + import java.util.HashMap; import java.util.Map; +/** + * A container object that holds an {@link Element} with a "distance" as determined by a vector index. + */ public class TinkerIndexElement<T> { private final T element; private final float distance; diff --git a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraphServiceTest.java b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraphServiceTest.java index 2901ef76da..b7ba27d88e 100644 --- a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraphServiceTest.java +++ b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraphServiceTest.java @@ -25,10 +25,12 @@ import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement; import org.apache.tinkerpop.gremlin.process.traversal.traverser.util.TraverserSet; import org.apache.tinkerpop.gremlin.structure.Direction; +import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.Vertex; import org.apache.tinkerpop.gremlin.tinkergraph.services.TinkerDegreeCentralityFactory; import org.apache.tinkerpop.gremlin.tinkergraph.services.TinkerServiceRegistry; import org.apache.tinkerpop.gremlin.tinkergraph.services.TinkerTextSearchFactory; +import org.apache.tinkerpop.gremlin.tinkergraph.services.TinkerVectorSearchFactory; import org.apache.tinkerpop.gremlin.util.function.TriFunction; import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils; import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper; @@ -38,6 +40,7 @@ import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -69,8 +72,12 @@ public class TinkerGraphServiceTest { final TinkerGraph graph = TinkerFactory.createModern(); final GraphTraversalSource g = graph.traversal(); + static final Map<String,Object> indexConfig = new HashMap<String,Object>() {{ + put(TinkerVectorIndex.CONFIG_DIMENSION, 3); + }}; + @Before - public void registerServices() { + public void setup() { graph.getServiceRegistry().registerService(new TinkerTextSearchFactory(graph)); graph.getServiceRegistry().registerService(new TinkerDegreeCentralityFactory(graph)); } @@ -81,31 +88,31 @@ public class TinkerGraphServiceTest { @Test public void g_call() throws Exception { /* - * Tinkergraph has one registered service by default ("tinker.search"). + * TinkerGraph has one registered service by default ("tinker.search"). */ assertArrayEquals(new String[] { - "tinker.search", - "tinker.degree.centrality" + TinkerTextSearchFactory.NAME, + TinkerDegreeCentralityFactory.NAME, }, toResultStrings( g.call() )); assertArrayEquals(new String[] { - "tinker.search", - "tinker.degree.centrality" + TinkerTextSearchFactory.NAME, + TinkerDegreeCentralityFactory.NAME, }, toResultStrings( g.call("--list") )); - checkResult("tinker.search", - g.call("--list").with("service", "tinker.search")); + checkResult(TinkerTextSearchFactory.NAME, + g.call("--list").with("service", TinkerTextSearchFactory.NAME)); - checkResult("{\"name\":\"tinker.search\",\"type:[requirements]:\":{\"Start\":[]}," + + checkResult("{\"name\":\"" + TinkerTextSearchFactory.NAME + "\",\"type:[requirements]:\":{\"Start\":[]}," + "\"params\":"+new ObjectMapper().writeValueAsString(TinkerTextSearchFactory.Params.DESCRIBE)+"}", - g.call("--list").with("service", "tinker.search").with("verbose")); + g.call("--list").with("service", TinkerTextSearchFactory.NAME).with("verbose")); } /** @@ -118,7 +125,7 @@ public class TinkerGraphServiceTest { */ assertEquals("[path[vp[name->marko], v[1]]]", toResultString( - g.call("tinker.search", asMap("search", "mar")) + g.call(TinkerTextSearchFactory.NAME, asMap("search", "mar")) .element().path() )); @@ -128,7 +135,7 @@ public class TinkerGraphServiceTest { */ assertEquals("[path[vp[name->vadas], v[2]]]", toResultString( - g.call("tinker.search", (Traversal) __.project("search").by(V(2).values("name"))) + g.call(TinkerTextSearchFactory.NAME, (Traversal) __.project("search").by(V(2).values("name"))) .element().path() )); @@ -143,7 +150,7 @@ public class TinkerGraphServiceTest { "path[p[weight->0.2], e[12][6-created->3]]" }, toResultStrings( - g.call("tinker.search").with("search", __.constant("2")) + g.call(TinkerTextSearchFactory.NAME).with("search", __.constant("2")) .element().path() )); @@ -157,7 +164,7 @@ public class TinkerGraphServiceTest { "path[vp[age->32], v[4]]", }, toResultStrings( - g.call("tinker.search").with("search", __.constant("2")).with("type", "Vertex") + g.call(TinkerTextSearchFactory.NAME).with("search", __.constant("2")).with("type", "Vertex") .element().path() )); @@ -175,7 +182,7 @@ public class TinkerGraphServiceTest { "{vertex=v[6], degree=0}", }, toResultStrings( - g.V().as("v").call("tinker.degree.centrality") + g.V().as("v").call(TinkerDegreeCentralityFactory.NAME) .project("vertex", "degree").by(select("v")).by() )); @@ -189,14 +196,14 @@ public class TinkerGraphServiceTest { "{vertex=peter, degree=1}", }, toResultStrings( - g.V().as("v").call("tinker.degree.centrality").with("direction", Direction.OUT) + g.V().as("v").call(TinkerDegreeCentralityFactory.NAME).with("direction", Direction.OUT) .project("vertex", "degree").by(select("v").values("name")).by() )); - checkResult("lop", g.V().where(__.call("tinker.degree.centrality").is(3)).values("name")); - checkResults(Arrays.asList("vadas","josh","ripple"), g.V().where(__.call("tinker.degree.centrality").is(1)).values("name")); - checkResult(0l, g.V().where(__.call("tinker.degree.centrality").is(100)).count()); + checkResult("lop", g.V().where(__.call(TinkerDegreeCentralityFactory.NAME).is(3)).values("name")); + checkResults(Arrays.asList("vadas","josh","ripple"), g.V().where(__.call(TinkerDegreeCentralityFactory.NAME).is(1)).values("name")); + checkResult(0l, g.V().where(__.call(TinkerDegreeCentralityFactory.NAME).is(100)).count()); } /** @@ -445,6 +452,145 @@ public class TinkerGraphServiceTest { checkResults(Arrays.asList(4, 2), g.V().call(serviceName, asMap(LambdaServiceFactory.Options.TYPE, Type.Barrier, LambdaServiceFactory.Options.CHUNK_SIZE, 4))); } + @Test + public void g_V_callXvector_topKByVertex_key_embeddingX() { + final TinkerGraph graf = TinkerGraph.open(); + graf.getServiceRegistry().registerService(new TinkerVectorSearchFactory(graf)); + graf.createIndex(TinkerIndexType.VECTOR, "embedding", Vertex.class, indexConfig); + final GraphTraversalSource gv = graf.traversal(); + + final Vertex vAlice = gv.addV("person").property("name", "Alice").property("embedding", new float[]{1.0f, 0.0f, 0.0f}).next(); + final Vertex vBob = gv.addV("person").property("name", "Bob").property("embedding", new float[]{0.05f, 1.0f, 0.0f}).next(); + final Vertex vCharlie = gv.addV("person").property("name", "Charlie").property("embedding", new float[]{0.0f, 0.0f, 1.0f}).next(); + final Vertex vDave = gv.addV("person").property("name", "Dave").property("embedding", new float[]{0.9f, 0.1f, 0.0f}).next(); + + final Map<String,Object> m = new HashMap<String,Object>() {{ + put("key", "embedding"); + }}; + final List<Object> list = gv.V(vAlice).call(TinkerVectorSearchFactory.NAME, m).toList(); + + final List<Map<String,Object>> expected = new ArrayList<>(); + expected.add(asMap("distance", 0.006116271f, "element", vDave)); + expected.add(asMap("distance", 0.9500624f, "element", vBob)); + expected.add(asMap("distance", 1.0f, "element", vCharlie)); + + // Use a custom comparison to ensure the lists are equal + assertEquals(expected.size(), list.size()); + for (int i = 0; i < expected.size(); i++) { + assertEquals(expected.get(i).get("distance"), ((Map) list.get(i)).get("distance")); + assertEquals(expected.get(i).get("element"), ((Map) list.get(i)).get("element")); + } + } + + @Test + public void g_E_callXvector_topKByEdge_key_embeddingX() { + final TinkerGraph graf = TinkerGraph.open(); + graf.getServiceRegistry().registerService(new TinkerVectorSearchFactory(graf)); + graf.createIndex(TinkerIndexType.VECTOR, "embedding", Edge.class, indexConfig); + final GraphTraversalSource gv = graf.traversal(); + + final Vertex vAlice = gv.addV("person").property("name", "Alice").next(); + final Vertex vBob = gv.addV("person").property("name", "Bob").next(); + final Vertex vCharlie = gv.addV("person").property("name", "Charlie").next(); + + final Edge e1 = gv.addE("knows").from(vAlice).to(vBob).property("embedding", new float[]{1.0f, 0.0f, 0.0f}).next(); + final Edge e2 = gv.addE("knows").from(vAlice).to(vCharlie).property("embedding", new float[]{0.0f, 1.0f, 0.0f}).next(); + final Edge e3 = gv.addE("knows").from(vBob).to(vCharlie).property("embedding", new float[]{0.5f, 0.5f, 0.0f}).next(); + + final Map<String, Object> m = new HashMap<String, Object>() {{ + put("key", "embedding"); + }}; + final List<Object> list = gv.E(e1).call(TinkerVectorSearchFactory.NAME, m).toList(); + + final List<Map<String, Object>> expected = new ArrayList<>(); + expected.add(asMap("distance", 0.29289323f, "element", e3)); + expected.add(asMap("distance", 1.0f, "element", e2)); + + // Use a custom comparison to ensure the lists are equal + assertEquals(expected.size(), list.size()); + for (int i = 0; i < expected.size(); i++) { + assertEquals(expected.get(i).get("distance"), ((Map) list.get(i)).get("distance")); + assertEquals(expected.get(i).get("element"), ((Map) list.get(i)).get("element")); + } + } + + @Test + public void g_V_callXvector_topKByVertex_key_embedding_topK_1X() { + final TinkerGraph graf = TinkerGraph.open(); + graf.getServiceRegistry().registerService(new TinkerVectorSearchFactory(graf)); + graf.createIndex(TinkerIndexType.VECTOR, "embedding", Vertex.class, indexConfig); + final GraphTraversalSource gv = graf.traversal(); + + final Vertex vAlice = gv.addV("person").property("name", "Alice").property("embedding", new float[]{1.0f, 0.0f, 0.0f}).next(); + final Vertex vBob = gv.addV("person").property("name", "Bob").property("embedding", new float[]{0.05f, 1.0f, 0.0f}).next(); + final Vertex vCharlie = gv.addV("person").property("name", "Charlie").property("embedding", new float[]{0.0f, 0.0f, 1.0f}).next(); + final Vertex vDave = gv.addV("person").property("name", "Dave").property("embedding", new float[]{0.9f, 0.1f, 0.0f}).next(); + + final Map<String,Object> m = new HashMap<String,Object>() {{ + put("key", "embedding"); + put("topK", 1); + }}; + final List<Object> list = gv.V(vAlice).call(TinkerVectorSearchFactory.NAME, m).toList(); + + final List<Map<String,Object>> expected = new ArrayList<>(); + expected.add(asMap("distance", 0.006116271f, "element", vDave)); + + // Use a custom comparison to ensure the lists are equal + assertEquals(expected.size(), list.size()); + for (int i = 0; i < expected.size(); i++) { + assertEquals(expected.get(i).get("distance"), ((Map) list.get(i)).get("distance")); + assertEquals(expected.get(i).get("element"), ((Map) list.get(i)).get("element")); + } + } + + @Test(expected = IllegalArgumentException.class) + public void g_V_callXvector_missing_keyX() { + final TinkerGraph graf = TinkerGraph.open(); + graf.getServiceRegistry().registerService(new TinkerVectorSearchFactory(graf)); + graf.createIndex(TinkerIndexType.VECTOR, "embedding", Vertex.class, indexConfig); + final GraphTraversalSource gv = graf.traversal(); + + final Vertex vAlice = gv.addV("person").property("name", "Alice").property("embedding", new float[]{1.0f, 0.0f, 0.0f}).next(); + + // Missing key parameter should cause a IllegalArgumentException when trying to access it + final Map<String,Object> emptyParams = new HashMap<>(); + gv.V(vAlice).call(TinkerVectorSearchFactory.NAME, emptyParams).toList(); + } + + @Test + public void g_V_callXvector_nonexistent_propertyX() { + final TinkerGraph graf = TinkerGraph.open(); + graf.getServiceRegistry().registerService(new TinkerVectorSearchFactory(graf)); + graf.createIndex(TinkerIndexType.VECTOR, "embedding", Vertex.class, indexConfig); + final GraphTraversalSource gv = graf.traversal(); + + final Vertex vAlice = gv.addV("person").property("name", "Alice").next(); // No embedding property + + // Referencing a non-existent property should return no results + final Map<String,Object> params = new HashMap<String,Object>() {{ + put("key", "embedding"); + }}; + + assertEquals(0, gv.V(vAlice).call(TinkerVectorSearchFactory.NAME, params).count().next().intValue()); + } + + @Test + public void g_V_properties_callXvector_nonexistent_propertyX() { + final TinkerGraph graf = TinkerGraph.open(); + graf.getServiceRegistry().registerService(new TinkerVectorSearchFactory(graf)); + graf.createIndex(TinkerIndexType.VECTOR, "embedding", Vertex.class, indexConfig); + final GraphTraversalSource gv = graf.traversal(); + + final Vertex vAlice = gv.addV("person").property("name", "Alice").property("embedding", new float[]{1.0f, 0.0f, 0.0f}).next(); + + final Map<String,Object> params = new HashMap<String,Object>() {{ + put("key", "embedding"); + }}; + + // Referencing things other than vertex or edge should return no results + assertEquals(0, gv.V(vAlice).properties().call(TinkerVectorSearchFactory.NAME, params).count().next().intValue()); + } + private String toResultString(final Traversal traversal) { return (String) IteratorUtils.stream(traversal).map(Object::toString).collect(Collectors.joining(",", "[", "]")); }
