diff --git a/docs/reference/graphql/graphql_API.md b/docs/reference/graphql/graphql_API.md
index b052036515..6cdaecc700 100644
--- a/docs/reference/graphql/graphql_API.md
+++ b/docs/reference/graphql/graphql_API.md
@@ -95,60 +95,60 @@ Graph path relative to the root namespace.
-| vectoriseGraph |
-Boolean! |
+namespaces |
+CollectionOfNamespace! |
-Update graph query, has side effects to update graph state
+Returns all namespaces using recursive search
-Returns:: GqlMutableGraph
+Returns:: List of namespaces on root
|
-| path |
-String! |
+namespace |
+Namespace! |
-Graph path relative to the root namespace.
+Returns a specific namespace at a given path
+
+Returns:: Namespace or error if no namespace found
|
-| model |
-EmbeddingModel |
-
-
-Optional embedding model; defaults to OpenAI's standard model.
-
- |
+path |
+String! |
+ |
-| nodes |
-Template |
+root |
+Namespace! |
-Optional node-document template (which fields go into each node's text representation); defaults to the built-in template.
+Returns root namespace
+
+Returns:: Root namespace
|
-| edges |
-Template |
+plugins |
+QueryPlugin! |
-Optional edge-document template; defaults to the built-in template.
+Returns a plugin.
|
-| vectorisedGraph |
-VectorisedGraph |
+receiveGraph |
+String! |
-Create vectorised graph in the format used for queries
+Encodes graph and returns as string.
-Returns:: GqlVectorisedGraph
+Returns:: Base64 url safe encoded string
|
@@ -162,78 +162,78 @@ Graph path relative to the root namespace.
-| namespaces |
-CollectionOfNamespace! |
+version |
+String! |
-Returns all namespaces using recursive search
-
-Returns:: List of namespaces on root
+Version string of the running `raphtory-graphql` server build.
|
-| namespace |
-Namespace! |
+vectoriseGraph |
+Boolean! |
-Returns a specific namespace at a given path
+Update graph query, has side effects to update graph state
-Returns:: Namespace or error if no namespace found
+Returns:: GqlMutableGraph
|
| path |
String! |
- |
+
+
+Graph path relative to the root namespace.
+
+ |
-| root |
-Namespace! |
+model |
+EmbeddingModel |
-Returns root namespace
-
-Returns:: Root namespace
+Optional embedding model; defaults to OpenAI's standard model.
|
-| plugins |
-QueryPlugin! |
+nodes |
+Template |
-Returns a plugin.
+Optional node-document template (which fields go into each node's text representation); defaults to the built-in template.
|
-| receiveGraph |
-String! |
+edges |
+Template |
-Encodes graph and returns as string.
-
-Returns:: Base64 url safe encoded string
+Optional edge-document template; defaults to the built-in template.
|
-| path |
-String! |
+vectorisedGraph |
+VectorisedGraph |
-Graph path relative to the root namespace.
+Create vectorised graph in the format used for queries
+
+Returns:: GqlVectorisedGraph
|
-| version |
+path |
String! |
-Version string of the running `raphtory-graphql` server build.
+Graph path relative to the root namespace.
|
@@ -774,38 +774,6 @@ If true, replace any graph already at `newPath`.
-| createIndex |
-Boolean! |
-
-
-(Experimental) Creates search index.
-
- |
-
-
-| path |
-String! |
-
-
-Graph path relative to the root namespace.
-
- |
-
-
-| indexSpec |
-IndexSpecInput |
-
-
-Optional spec selecting which node/edge property fields to index. Omit to index a default set.
-
- |
-
-
-| inRam |
-Boolean! |
- |
-
-
| flush |
Boolean! |
@@ -2291,7 +2259,7 @@ on the first key break to the second, etc.).
| [EdgeSortBy!]! |
-Ordered list of sort keys. Each entry chooses exactly one of `src` / `dst` / `time` / `property`, with an optional `reverse: true` to flip order.
+Ordered list of sort keys. Each entry chooses exactly one of `src` / `dst` / `neighbour` / `time` / `property`, with an optional `reverse: true` to flip order.
|
@@ -3467,91 +3435,6 @@ remain in the view even if all their edges are filtered out.
Composite edge filter (by property, layer, src/dst, etc.).
-
-
-
-| getIndexSpec |
-IndexSpec! |
-
-
-(Experimental) Get index specification.
-
- |
-
-
-| searchNodes |
-[Node!]! |
-
-
-(Experimental) Searches for nodes which match the given filter
-expression. Uses Tantivy's exact search; requires the graph to have
-been indexed.
-
- |
-
-
-| filter |
-NodeFilter! |
-
-
-Composite node filter (by name, property, type, etc.).
-
- |
-
-
-| limit |
-Int! |
-
-
-Maximum number of nodes to return.
-
- |
-
-
-| offset |
-Int! |
-
-
-Number of matches to skip before returning results.
-
- |
-
-
-| searchEdges |
-[Edge!]! |
-
-
-(Experimental) Searches the index for edges which match the given
-filter expression. Uses Tantivy's exact search; requires the graph to
-have been indexed.
-
- |
-
-
-| filter |
-EdgeFilter! |
-
-
-Composite edge filter (by property, layer, src/dst, etc.).
-
- |
-
-
-| limit |
-Int! |
-
-
-Maximum number of edges to return.
-
- |
-
-
-| offset |
-Int! |
-
-
-Number of matches to skip before returning results.
-
|
@@ -4336,57 +4219,6 @@ Zero-based page number; multiplies `limit` to determine where to start (default
-### IndexSpec
-
-
-
-
-| Field |
-Argument |
-Type |
-Description |
-
-
-
-
-| nodeMetadata |
-[String!]! |
-
-
-Returns node metadata.
-
- |
-
-
-| nodeProperties |
-[String!]! |
-
-
-Returns node properties.
-
- |
-
-
-| edgeMetadata |
-[String!]! |
-
-
-Returns edge metadata.
-
- |
-
-
-| edgeProperties |
-[String!]! |
-
-
-Returns edge properties.
-
- |
-
-
-
-
### Intervals
Provides access to the intervals between temporal entries of an object.
@@ -6740,7 +6572,7 @@ first key break to the second, etc.).
[NodeSortBy!]! |
-Ordered list of sort keys. Each entry chooses exactly one of `id` / `time` / `property`, with an optional `reverse: true` to flip order.
+Ordered list of sort keys. Each entry chooses exactly one of `id` / `name` / `type` / `time` / `property`, with an optional `reverse: true` to flip order.
|
@@ -8146,6 +7978,11 @@ Number of updates recorded for this property in the current view.
### VectorSelection
+A working set of documents / nodes / edges built up via similarity
+searches on a `VectorisedGraph`. Selections are mutable: you can grow
+them with more hops (`expand*`), dereference the contents (`nodes`,
+`edges`, `getDocuments`), or start fresh with `emptySelection`.
+
@@ -8368,6 +8205,11 @@ Optional `{start, end}` to restrict matches to edges active in that interval.
### VectorisedGraph
+A graph with embedded vector representations for its nodes and edges.
+Exposes similarity search over documents, nodes, and edges, plus
+selection building (`emptySelection`) and index maintenance
+(`optimizeIndex`).
+
@@ -8381,17 +8223,33 @@ Optional `{start, end}` to restrict matches to edges active in that interval.
| optimizeIndex |
Boolean! |
- |
+
+
+Rebuild (or incrementally update) the on-disk vector indexes for nodes
+and edges so subsequent similarity searches hit the fresh embeddings.
+Safe to call repeatedly; returns true on success.
+
+ |
| emptySelection |
VectorSelection! |
- |
+
+
+Returns an empty selection of documents.
+
+ |
| entitiesBySimilarity |
VectorSelection! |
- |
+
+
+Find the highest-scoring nodes *and* edges (mixed) by similarity to a
+natural-language query. The query is embedded server-side and matched
+against indexed entity vectors.
+
+ |
| query |
@@ -8423,7 +8281,13 @@ Optional `{start, end}` to restrict matches to entities active in that interval.
| nodesBySimilarity |
VectorSelection! |
- |
+
+
+Find the highest-scoring nodes by similarity to a natural-language
+query. The query is embedded server-side and matched against indexed
+node vectors.
+
+ |
| query |
@@ -8455,7 +8319,13 @@ Optional `{start, end}` to restrict matches to nodes active in that interval.
| edgesBySimilarity |
VectorSelection! |
- |
+
+
+Find the highest-scoring edges by similarity to a natural-language
+query. The query is embedded server-side and matched against indexed
+edge vectors.
+
+ |
| query |
@@ -8895,25 +8765,38 @@ Filter evaluated within the layer-restricted view.
Boolean |
-Reverse order
+Reverse order. Applies to the `time` / `property` keys; the node keys
+(`src` / `dst` / `neighbour`) carry their own `reverse` inside the
+nested `NodeSortBy` and ignore this flag.
|
| src |
-Boolean |
+NodeSortBy |
-Source node
+Sort by the source node.
|
| dst |
-Boolean |
+NodeSortBy |
+
+
+Sort by the destination node.
+
+ |
+
+
+| neighbour |
+NodeSortBy |
-Destination
+Sort by the neighbour node: the endpoint that is NOT the node these
+edges were traversed from (the destination for a graph-level edge
+collection).
|
@@ -9817,38 +9700,6 @@ Optional nested filter applied after the window restriction.
-### IndexSpecInput
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| nodeProps |
-PropsInput! |
-
-
-Node properties.
-
- |
-
-
-| edgeProps |
-PropsInput! |
-
-
-Edge properties.
-
- |
-
-
-
-
### InputEdge
@@ -10363,6 +10214,24 @@ Reverse order
Unique Id
+
+
+
+| name |
+Boolean |
+
+
+Node name
+
+ |
+
+
+| type |
+Boolean |
+
+
+Node type. Untyped nodes sort first (before any named type).
+
|
@@ -11355,72 +11224,6 @@ Value.
-### PropsInput
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| all |
-AllPropertySpec |
-
-
-All properties and metadata.
-
- |
-
-
-| some |
-SomePropertySpec |
-
-
-Some properties and metadata.
-
- |
-
-
-
-
-### SomePropertySpec
-
-SomePropertySpec object containing lists of metadata and property names.
-
-
-
-
-| Field |
-Type |
-Description |
-
-
-
-
-| metadata |
-[String!]! |
-
-
-List of metadata.
-
- |
-
-
-| properties |
-[String!]! |
-
-
-List of properties.
-
- |
-
-
-
-
### Template
@@ -11786,43 +11589,6 @@ Alignment unit used to align window boundaries.
-### AllPropertySpec
-
-
-
-
-| Value |
-Description |
-
-
-
-
-| ALL |
-
-
-All properties and metadata.
-
- |
-
-
-| ALL_METADATA |
-
-
-All metadata.
-
- |
-
-
-| ALL_PROPERTIES |
-
-
-All properties.
-
- |
-
-
-
-
### DegreeDirection
Filters nodes by computed degree with a directional scope.
diff --git a/python/python/raphtory/__init__.pyi b/python/python/raphtory/__init__.pyi
index 75340b82c7..55bf7b2a68 100644
--- a/python/python/raphtory/__init__.pyi
+++ b/python/python/raphtory/__init__.pyi
@@ -69,6 +69,7 @@ __all__ = [
"algorithms",
"graph_loader",
"graph_gen",
+ "vectors",
"node_state",
"filter",
"iterables",
@@ -668,6 +669,26 @@ class GraphView(object):
GraphView: The layered view
"""
+ def vectorise(
+ self,
+ model: VectorCache,
+ nodes: bool | str = True,
+ edges: bool | str = True,
+ verbose: bool = False,
+ ) -> VectorisedGraph:
+ """
+ Create a VectorisedGraph from the current graph.
+
+ Args:
+ model (VectorCache): Cache wrapping the embedding model used to embed documents.
+ nodes (bool | str): Enable for nodes to be embedded, disable for nodes to not be embedded or specify a custom document property to use if a string is provided. Defaults to True.
+ edges (bool | str): Enable for edges to be embedded, disable for edges to not be embedded or specify a custom document property to use if a string is provided. Defaults to True.
+ verbose (bool): Enable to print logs reporting progress. Defaults to False.
+
+ Returns:
+ VectorisedGraph: A VectorisedGraph with all the documents and their embeddings, with an initial empty selection.
+ """
+
def window(self, start: TimeInput, end: TimeInput) -> GraphView:
"""
Create a view of the GraphView including all events between `start` (inclusive) and `end` (exclusive)
diff --git a/python/python/raphtory/graphql/__init__.pyi b/python/python/raphtory/graphql/__init__.pyi
index 28fdf429c7..21a3bdcf62 100644
--- a/python/python/raphtory/graphql/__init__.pyi
+++ b/python/python/raphtory/graphql/__init__.pyi
@@ -122,9 +122,39 @@ class GraphServer(object):
RunningGraphServer: The running server
"""
- def turn_off_index(self) -> None:
+ def vectorise_all_graphs(
+ self,
+ embeddings: OpenAIEmbeddings,
+ nodes: bool | str = True,
+ edges: bool | str = True,
+ ) -> None:
+ """
+ Vectorise all graphs in the server working directory.
+
+ Arguments:
+ embeddings (OpenAIEmbeddings): the embeddings to use
+ nodes (bool | str): if nodes have to be embedded or not or the custom template to use if a str is provided. Defaults to True.
+ edges (bool | str): if edges have to be embedded or not or the custom template to use if a str is provided. Defaults to True.
+
+ Returns:
+ None:
"""
- Turn off index for all graphs.
+
+ def vectorise_graph(
+ self,
+ name: list[str],
+ embeddings: OpenAIEmbeddings,
+ nodes: bool | str = True,
+ edges: bool | str = True,
+ ) -> None:
+ """
+ Vectorise the graph name in the server working directory.
+
+ Arguments:
+ name (list[str]): the name of the graph to vectorise.
+ embeddings (OpenAIEmbeddings): the embeddings to use
+ nodes (bool | str): if nodes have to be embedded or not or the custom template to use if a str is provided. Defaults to True.
+ edges (bool | str): if edges have to be embedded or not or the custom template to use if a str is provided. Defaults to True.
Returns:
None:
diff --git a/python/python/raphtory/node_state/__init__.pyi b/python/python/raphtory/node_state/__init__.pyi
index 134a62396f..6e8c79273c 100644
--- a/python/python/raphtory/node_state/__init__.pyi
+++ b/python/python/raphtory/node_state/__init__.pyi
@@ -7818,6 +7818,17 @@ class OutputNodeState(object):
Optional[dict]: the value for the node or the default value
"""
+ def groups(self, cols: list[str]) -> list[tuple[dict, Nodes]]:
+ """
+ Group by value
+
+ Arguments:
+ cols (list[str]): columns by which to group nodes
+
+ Returns:
+ list[tuple[dict, Nodes]]: The grouped nodes
+ """
+
def items(self) -> Iterator[Tuple[Node, Dict]]:
"""
Iterate over items
@@ -7854,6 +7865,17 @@ class OutputNodeState(object):
Nodes: The nodes
"""
+ def sort_by(self, sort_params: Dict) -> OutputNodeState:
+ """
+ Get value for node
+
+ Arguments:
+ sort_params (Dict): Map of sort keys to sort option ('asc' or 'desc'). None defaults to 'asc'
+
+ Returns:
+ OutputNodeState: Sorted NodeState
+ """
+
def to_parquet(self, file_path: str, id_column: str = "id") -> None:
"""
Convert OutputNodeState to Parquet
@@ -7866,6 +7888,18 @@ class OutputNodeState(object):
None:
"""
+ def top_k(self, sort_params: Dict, k: int) -> OutputNodeState:
+ """
+ Get value for node
+
+ Arguments:
+ sort_params (Dict): Map of sort keys to sort option ('asc' or 'desc'). None defaults to 'asc'
+ k (int): Number of top entries to return.
+
+ Returns:
+ OutputNodeState: Sorted NodeState
+ """
+
def values(self) -> Iterator[Dict]:
"""
Iterate over values
diff --git a/python/tests/test_base_install/test_graphql/edit_graph/test_graphql.py b/python/tests/test_base_install/test_graphql/edit_graph/test_graphql.py
index 5fbd1882a5..cb057b4795 100644
--- a/python/tests/test_base_install/test_graphql/edit_graph/test_graphql.py
+++ b/python/tests/test_base_install/test_graphql/edit_graph/test_graphql.py
@@ -770,7 +770,7 @@ def test_graph_persistence_across_restarts():
# Verify persistence: check that nodes and edges are still there
query_nodes = """{graph(path: "persistent_graph") {nodes {sorted (sortBys: [{id: true}]){ list {name} }}}}"""
- query_edges = """{graph(path: "persistent_graph") {edges {sorted (sortBys: [{src: true, dst: true}]){ list {id} }}}}"""
+ query_edges = """{graph(path: "persistent_graph") {edges {sorted (sortBys: [{src: {id: true}, dst: {id: true}}]){ list {id} }}}}"""
assert client.query(query_nodes) == {
"graph": {
diff --git a/python/tests/test_base_install/test_graphql/test_edge_sorting.py b/python/tests/test_base_install/test_graphql/test_edge_sorting.py
index c4358c92de..aeb0325669 100644
--- a/python/tests/test_base_install/test_graphql/test_edge_sorting.py
+++ b/python/tests/test_base_install/test_graphql/test_edge_sorting.py
@@ -147,7 +147,7 @@ def test_graph_edge_sort_by_src(graph):
query {
graph(path: "g") {
edges {
- sorted(sortBys: [{ src: true }]) {
+ sorted(sortBys: [{ src: { id: true } }]) {
list {
src {
id
@@ -182,7 +182,7 @@ def test_graph_edge_sort_by_dst(graph):
query {
graph(path: "g") {
edges {
- sorted(sortBys: [{ dst: true }]) {
+ sorted(sortBys: [{ dst: { id: true } }]) {
list {
dst {
id
@@ -635,7 +635,7 @@ def test_graph_edge_sort_by_combined_2(graph):
query {
graph(path: "g") {
edges {
- sorted(sortBys: [{ dst: true }, { time: EARLIEST }, { property: "eprop3" }, { time: LATEST, reverse: true }]) {
+ sorted(sortBys: [{ dst: { id: true } }, { time: EARLIEST }, { property: "eprop3" }, { time: LATEST, reverse: true }]) {
list {
src {
id
diff --git a/raphtory-graphql/schema.graphql b/raphtory-graphql/schema.graphql
index 77451b5703..b480914e2d 100644
--- a/raphtory-graphql/schema.graphql
+++ b/raphtory-graphql/schema.graphql
@@ -13,21 +13,6 @@ enum AlignmentUnit {
YEAR
}
-enum AllPropertySpec {
- """
- All properties and metadata.
- """
- ALL
- """
- All metadata.
- """
- ALL_METADATA
- """
- All properties.
- """
- ALL_PROPERTIES
-}
-
"""
Collection of items
"""
@@ -746,17 +731,25 @@ type EdgeSchema {
input EdgeSortBy {
"""
- Reverse order
+ Reverse order. Applies to the `time` / `property` keys; the node keys
+ (`src` / `dst` / `neighbour`) carry their own `reverse` inside the
+ nested `NodeSortBy` and ignore this flag.
"""
reverse: Boolean
"""
- Source node
+ Sort by the source node.
+ """
+ src: NodeSortBy
+ """
+ Sort by the destination node.
"""
- src: Boolean
+ dst: NodeSortBy
"""
- Destination
+ Sort by the neighbour node: the endpoint that is NOT the node these
+ edges were traversed from (the destination for a graph-level edge
+ collection).
"""
- dst: Boolean
+ neighbour: NodeSortBy
"""
Time
"""
@@ -1124,7 +1117,7 @@ type Edges {
"""
sorted(
"""
- Ordered list of sort keys. Each entry chooses exactly one of `src` / `dst` / `time` / `property`, with an optional `reverse: true` to flip order.
+ Ordered list of sort keys. Each entry chooses exactly one of `src` / `dst` / `neighbour` / `time` / `property`, with an optional `reverse: true` to flip order.
"""
sortBys: [EdgeSortBy!]!
): Edges!
@@ -1815,48 +1808,6 @@ type Graph {
expr: EdgeFilter!
): Graph!
"""
- (Experimental) Get index specification.
- """
- getIndexSpec: IndexSpec!
- """
- (Experimental) Searches for nodes which match the given filter
- expression. Uses Tantivy's exact search; requires the graph to have
- been indexed.
- """
- searchNodes(
- """
- Composite node filter (by name, property, type, etc.).
- """
- filter: NodeFilter!,
- """
- Maximum number of nodes to return.
- """
- limit: Int!,
- """
- Number of matches to skip before returning results.
- """
- offset: Int!
- ): [Node!]!
- """
- (Experimental) Searches the index for edges which match the given
- filter expression. Uses Tantivy's exact search; requires the graph to
- have been indexed.
- """
- searchEdges(
- """
- Composite edge filter (by property, layer, src/dst, etc.).
- """
- filter: EdgeFilter!,
- """
- Maximum number of edges to return.
- """
- limit: Int!,
- """
- Number of matches to skip before returning results.
- """
- offset: Int!
- ): [Edge!]!
- """
Apply a list of view operations in the given order and return the
resulting graph view. Lets callers compose multiple view transforms
(window, layer, filter, snapshot, ...) in a single call.
@@ -2433,36 +2384,6 @@ type HistoryTimestamp {
): [Int!]!
}
-type IndexSpec {
- """
- Returns node metadata.
- """
- nodeMetadata: [String!]!
- """
- Returns node properties.
- """
- nodeProperties: [String!]!
- """
- Returns edge metadata.
- """
- edgeMetadata: [String!]!
- """
- Returns edge properties.
- """
- edgeProperties: [String!]!
-}
-
-input IndexSpecInput {
- """
- Node properties.
- """
- nodeProps: PropsInput!
- """
- Edge properties.
- """
- edgeProps: PropsInput!
-}
-
input InputEdge {
"""
Source node id (string or non-negative integer).
@@ -2907,19 +2828,6 @@ type MutRoot {
overwrite: Boolean!
): String!
"""
- (Experimental) Creates search index.
- """
- createIndex(
- """
- Graph path relative to the root namespace.
- """
- path: String!,
- """
- Optional spec selecting which node/edge property fields to index. Omit to index a default set.
- """
- indexSpec: IndexSpecInput, inRam: Boolean!
- ): Boolean!
- """
Flush any pending writes for the graph at `graphPath` to disk.
"""
flush(
@@ -3885,6 +3793,14 @@ input NodeSortBy {
"""
id: Boolean
"""
+ Node name
+ """
+ name: Boolean
+ """
+ Node type. Untyped nodes sort first (before any named type).
+ """
+ type: Boolean
+ """
Time
"""
time: SortByTime
@@ -4250,7 +4166,7 @@ type Nodes {
"""
sorted(
"""
- Ordered list of sort keys. Each entry chooses exactly one of `id` / `time` / `property`, with an optional `reverse: true` to flip order.
+ Ordered list of sort keys. Each entry chooses exactly one of `id` / `name` / `type` / `time` / `property`, with an optional `reverse: true` to flip order.
"""
sortBys: [NodeSortBy!]!
): Nodes!
@@ -5125,17 +5041,6 @@ type PropertyTuple {
value: PropertyOutput!
}
-input PropsInput @oneOf {
- """
- All properties and metadata.
- """
- all: AllPropertySpec
- """
- Some properties and metadata.
- """
- some: SomePropertySpec
-}
-
type QueryPlugin {
NoOps: String!
}
@@ -5180,40 +5085,6 @@ type QueryRoot {
path: String!
): MutableGraph!
"""
- Update graph query, has side effects to update graph state
-
- Returns:: GqlMutableGraph
- """
- vectoriseGraph(
- """
- Graph path relative to the root namespace.
- """
- path: String!,
- """
- Optional embedding model; defaults to OpenAI's standard model.
- """
- model: EmbeddingModel,
- """
- Optional node-document template (which fields go into each node's text representation); defaults to the built-in template.
- """
- nodes: Template,
- """
- Optional edge-document template; defaults to the built-in template.
- """
- edges: Template
- ): Boolean!
- """
- Create vectorised graph in the format used for queries
-
- Returns:: GqlVectorisedGraph
- """
- vectorisedGraph(
- """
- Graph path relative to the root namespace.
- """
- path: String!
- ): VectorisedGraph
- """
Returns all namespaces using recursive search
Returns:: List of namespaces on root
@@ -5250,6 +5121,40 @@ type QueryRoot {
Version string of the running `raphtory-graphql` server build.
"""
version: String!
+ """
+ Update graph query, has side effects to update graph state
+
+ Returns:: GqlMutableGraph
+ """
+ vectoriseGraph(
+ """
+ Graph path relative to the root namespace.
+ """
+ path: String!,
+ """
+ Optional embedding model; defaults to OpenAI's standard model.
+ """
+ model: EmbeddingModel,
+ """
+ Optional node-document template (which fields go into each node's text representation); defaults to the built-in template.
+ """
+ nodes: Template,
+ """
+ Optional edge-document template; defaults to the built-in template.
+ """
+ edges: Template
+ ): Boolean!
+ """
+ Create vectorised graph in the format used for queries
+
+ Returns:: GqlVectorisedGraph
+ """
+ vectorisedGraph(
+ """
+ Graph path relative to the root namespace.
+ """
+ path: String!
+ ): VectorisedGraph
}
type ShortestPathOutput {
@@ -5257,20 +5162,6 @@ type ShortestPathOutput {
nodes: [String!]!
}
-"""
-SomePropertySpec object containing lists of metadata and property names.
-"""
-input SomePropertySpec {
- """
- List of metadata.
- """
- metadata: [String!]!
- """
- List of properties.
- """
- properties: [String!]!
-}
-
enum SortByTime {
"""
Latest time
@@ -5512,6 +5403,12 @@ input Value @oneOf {
decimal: String
}
+"""
+A working set of documents / nodes / edges built up via similarity
+searches on a `VectorisedGraph`. Selections are mutable: you can grow
+them with more hops (`expand*`), dereference the contents (`nodes`,
+`edges`, `getDocuments`), or start fresh with `emptySelection`.
+"""
type VectorSelection {
"""
Returns a list of nodes in the current selection.
@@ -5618,9 +5515,28 @@ type VectorSelection {
): VectorSelection!
}
+"""
+A graph with embedded vector representations for its nodes and edges.
+Exposes similarity search over documents, nodes, and edges, plus
+selection building (`emptySelection`) and index maintenance
+(`optimizeIndex`).
+"""
type VectorisedGraph {
+ """
+ Rebuild (or incrementally update) the on-disk vector indexes for nodes
+ and edges so subsequent similarity searches hit the fresh embeddings.
+ Safe to call repeatedly; returns true on success.
+ """
optimizeIndex: Boolean!
+ """
+ Returns an empty selection of documents.
+ """
emptySelection: VectorSelection!
+ """
+ Find the highest-scoring nodes *and* edges (mixed) by similarity to a
+ natural-language query. The query is embedded server-side and matched
+ against indexed entity vectors.
+ """
entitiesBySimilarity(
"""
Natural-language search string; embedded by the server.
@@ -5635,6 +5551,11 @@ type VectorisedGraph {
"""
window: VectorisedGraphWindow
): VectorSelection!
+ """
+ Find the highest-scoring nodes by similarity to a natural-language
+ query. The query is embedded server-side and matched against indexed
+ node vectors.
+ """
nodesBySimilarity(
"""
Natural-language search string; embedded by the server.
@@ -5649,6 +5570,11 @@ type VectorisedGraph {
"""
window: VectorisedGraphWindow
): VectorSelection!
+ """
+ Find the highest-scoring edges by similarity to a natural-language
+ query. The query is embedded server-side and matched against indexed
+ edge vectors.
+ """
edgesBySimilarity(
"""
Natural-language search string; embedded by the server.
@@ -5720,3 +5646,4 @@ schema {
query: QueryRoot
mutation: MutRoot
}
+
diff --git a/raphtory-graphql/src/lib.rs b/raphtory-graphql/src/lib.rs
index 0541e0cf96..27ae8f2ac6 100644
--- a/raphtory-graphql/src/lib.rs
+++ b/raphtory-graphql/src/lib.rs
@@ -3042,4 +3042,401 @@ mod graphql_test {
// `flush_and_clear`, which would rewrite the sidecar and mask the bug.
drop(session);
}
+
+ #[tokio::test]
+ async fn test_nodes_sorted_by_type_then_name() {
+ let g = Graph::new();
+ g.add_node(1, "b", NO_PROPS, Some("Person"), None).unwrap();
+ g.add_node(1, "a", NO_PROPS, Some("Person"), None).unwrap();
+ g.add_node(1, "c", NO_PROPS, Some("Company"), None).unwrap();
+ g.add_node(1, "d", NO_PROPS, None, None).unwrap(); // untyped
+
+ let graph: MaterializedGraph = g.into();
+ let tmp_dir = tempdir().unwrap();
+ let setup = setup_with_graphs(&[("g", graph)], tmp_dir.path()).await;
+
+ // type ascending: None < "Company" < "Person"; then name ascending.
+ let query = r#"
+ {
+ graph(path: "g") {
+ nodes {
+ sorted(sortBys: [{ type: true }, { name: true }]) {
+ list { name }
+ }
+ }
+ }
+ }
+ "#;
+ let res = setup.schema.execute(Request::new(query)).await;
+ assert_eq!(res.errors, vec![], "{:?}", res.errors);
+ let data = res.data.into_json().unwrap();
+ assert_eq!(
+ data,
+ json!({ "graph": { "nodes": { "sorted": { "list": [
+ { "name": "d" }, { "name": "c" }, { "name": "a" }, { "name": "b" }
+ ] } } } })
+ );
+ }
+
+ #[tokio::test]
+ async fn test_nodes_sorted_by_type_reverse() {
+ let g = Graph::new();
+ g.add_node(1, "b", NO_PROPS, Some("Person"), None).unwrap();
+ g.add_node(1, "a", NO_PROPS, Some("Person"), None).unwrap();
+ g.add_node(1, "c", NO_PROPS, Some("Company"), None).unwrap();
+ g.add_node(1, "d", NO_PROPS, None, None).unwrap(); // untyped
+
+ let graph: MaterializedGraph = g.into();
+ let tmp_dir = tempdir().unwrap();
+ let setup = setup_with_graphs(&[("g", graph)], tmp_dir.path()).await;
+
+ // type descending (reverse: true): "Person" > "Company" > None; then name ascending.
+ let query = r#"
+ {
+ graph(path: "g") {
+ nodes {
+ sorted(sortBys: [{ type: true, reverse: true }, { name: true }]) {
+ list { name }
+ }
+ }
+ }
+ }
+ "#;
+ let res = setup.schema.execute(Request::new(query)).await;
+ assert_eq!(res.errors, vec![], "{:?}", res.errors);
+ let data = res.data.into_json().unwrap();
+ assert_eq!(
+ data,
+ json!({ "graph": { "nodes": { "sorted": { "list": [
+ { "name": "a" }, { "name": "b" }, { "name": "c" }, { "name": "d" }
+ ] } } } })
+ );
+ }
+
+ #[tokio::test]
+ async fn test_nodes_sorted_by_id_regression() {
+ let g = Graph::new();
+ g.add_node(1, "c", NO_PROPS, None, None).unwrap();
+ g.add_node(1, "a", NO_PROPS, None, None).unwrap();
+ g.add_node(1, "b", NO_PROPS, None, None).unwrap();
+
+ let graph: MaterializedGraph = g.into();
+ let tmp_dir = tempdir().unwrap();
+ let setup = setup_with_graphs(&[("g", graph)], tmp_dir.path()).await;
+
+ let query = r#"
+ {
+ graph(path: "g") {
+ nodes { sorted(sortBys: [{ id: true }]) { list { name } } }
+ }
+ }
+ "#;
+ let res = setup.schema.execute(Request::new(query)).await;
+ assert_eq!(res.errors, vec![], "{:?}", res.errors);
+ let data = res.data.into_json().unwrap();
+ assert_eq!(
+ data,
+ json!({ "graph": { "nodes": { "sorted": { "list": [
+ { "name": "a" }, { "name": "b" }, { "name": "c" }
+ ] } } } })
+ );
+ }
+
+ async fn neighbour_sort_setup(tmp: &std::path::Path) -> crate::test_support::TestSetup {
+ // Anchor "hub" with edges in BOTH directions to typed + untyped neighbours.
+ let g = Graph::new();
+ g.add_node(1, "hub", NO_PROPS, None, None).unwrap();
+ g.add_node(1, "x", [("score", 3i64)], Some("B"), None)
+ .unwrap();
+ g.add_node(1, "y", [("score", 1i64)], Some("A"), None)
+ .unwrap();
+ g.add_node(1, "w", [("score", 2i64)], Some("A"), None)
+ .unwrap();
+ g.add_node(1, "z", [("score", 4i64)], None, None).unwrap(); // untyped
+ g.add_edge(10, "hub", "x", NO_PROPS, None).unwrap(); // out -> nbr x (B)
+ g.add_edge(11, "y", "hub", NO_PROPS, None).unwrap(); // in -> nbr y (A)
+ g.add_edge(12, "w", "hub", NO_PROPS, None).unwrap(); // in -> nbr w (A)
+ g.add_edge(13, "hub", "z", NO_PROPS, None).unwrap(); // out -> nbr z (untyped)
+
+ let graph: MaterializedGraph = g.into();
+ setup_with_graphs(&[("g", graph)], tmp).await
+ }
+
+ #[tokio::test]
+ async fn test_edges_sorted_by_neighbour_type_then_name() {
+ let tmp_dir = tempdir().unwrap();
+ let setup = neighbour_sort_setup(tmp_dir.path()).await;
+
+ // type asc (None < "A" < "B"), then name asc: z(None), w(A), y(A), x(B)
+ let query = r#"
+ {
+ graph(path: "g") {
+ node(name: "hub") {
+ edges {
+ explodeLayers {
+ sorted(sortBys: [
+ { neighbour: { type: true } },
+ { neighbour: { name: true } },
+ { time: LATEST }
+ ]) {
+ count
+ page(limit: 10) { nbr { name nodeType } }
+ }
+ }
+ }
+ }
+ }
+ }
+ "#;
+ let res = setup.schema.execute(Request::new(query)).await;
+ assert_eq!(res.errors, vec![], "{:?}", res.errors);
+ let data = res.data.into_json().unwrap();
+ assert_eq!(
+ data,
+ json!({ "graph": { "node": { "edges": { "explodeLayers": { "sorted": {
+ "count": 4,
+ "page": [
+ { "nbr": { "name": "z", "nodeType": null } },
+ { "nbr": { "name": "w", "nodeType": "A" } },
+ { "nbr": { "name": "y", "nodeType": "A" } },
+ { "nbr": { "name": "x", "nodeType": "B" } }
+ ]
+ } } } } } })
+ );
+ }
+
+ #[tokio::test]
+ async fn test_edges_sorted_by_neighbour_property() {
+ let tmp_dir = tempdir().unwrap();
+ let setup = neighbour_sort_setup(tmp_dir.path()).await;
+
+ // neighbour "score" ascending: y(1), w(2), x(3), z(4)
+ let query = r#"
+ {
+ graph(path: "g") {
+ node(name: "hub") {
+ edges {
+ explodeLayers {
+ sorted(sortBys: [{ neighbour: { property: "score" } }]) {
+ page(limit: 10) { nbr { name } }
+ }
+ }
+ }
+ }
+ }
+ }
+ "#;
+ let res = setup.schema.execute(Request::new(query)).await;
+ assert_eq!(res.errors, vec![], "{:?}", res.errors);
+ let data = res.data.into_json().unwrap();
+ assert_eq!(
+ data,
+ json!({ "graph": { "node": { "edges": { "explodeLayers": { "sorted": {
+ "page": [
+ { "nbr": { "name": "y" } }, { "nbr": { "name": "w" } },
+ { "nbr": { "name": "x" } }, { "nbr": { "name": "z" } }
+ ]
+ } } } } } })
+ );
+ }
+
+ #[tokio::test]
+ async fn test_graph_edges_neighbour_id_equals_dst() {
+ // At graph level every ref is Dir::Out, so neighbour == dst.
+ let tmp_dir = tempdir().unwrap();
+ let setup = neighbour_sort_setup(tmp_dir.path()).await;
+
+ let query = r#"
+ {
+ graph(path: "g") {
+ edges {
+ sorted(sortBys: [{ neighbour: { id: true } }]) {
+ page(limit: 10) { dst { name } }
+ }
+ }
+ }
+ }
+ "#;
+ let res = setup.schema.execute(Request::new(query)).await;
+ assert_eq!(res.errors, vec![], "{:?}", res.errors);
+ let data = res.data.into_json().unwrap();
+ // dst ids ascending: hub, hub, x, z
+ assert_eq!(
+ data,
+ json!({ "graph": { "edges": { "sorted": {
+ "page": [
+ { "dst": { "name": "hub" } }, { "dst": { "name": "hub" } },
+ { "dst": { "name": "x" } }, { "dst": { "name": "z" } }
+ ]
+ } } } })
+ );
+ }
+
+ #[tokio::test]
+ async fn test_edges_sorted_by_neighbour_name_reverse() {
+ let tmp_dir = tempdir().unwrap();
+ let setup = neighbour_sort_setup(tmp_dir.path()).await;
+
+ // Neighbour name DESCENDING via the nested `reverse`; the outer
+ // `reverse: true` must be ignored (the neighbour arm early-returns).
+ // hub's neighbours are w, x, y, z -> descending: z, y, x, w.
+ let query = r#"
+ {
+ graph(path: "g") {
+ node(name: "hub") {
+ edges {
+ explodeLayers {
+ sorted(sortBys: [{ reverse: true, neighbour: { name: true, reverse: true } }]) {
+ page(limit: 10) { nbr { name } }
+ }
+ }
+ }
+ }
+ }
+ }
+ "#;
+ let res = setup.schema.execute(Request::new(query)).await;
+ assert_eq!(res.errors, vec![], "{:?}", res.errors);
+ let data = res.data.into_json().unwrap();
+ assert_eq!(
+ data,
+ json!({ "graph": { "node": { "edges": { "explodeLayers": { "sorted": {
+ "page": [
+ { "nbr": { "name": "z" } }, { "nbr": { "name": "y" } },
+ { "nbr": { "name": "x" } }, { "nbr": { "name": "w" } }
+ ]
+ } } } } } })
+ );
+ }
+
+ #[tokio::test]
+ async fn test_edges_sorted_by_neighbour_self_loop() {
+ // Self-loop hub->hub must resolve nbr to the anchor itself, sorting
+ // under its own type ("M"), not treated specially.
+ let g = Graph::new();
+ g.add_node(1, "hub", NO_PROPS, Some("M"), None).unwrap();
+ g.add_node(1, "a", NO_PROPS, Some("A"), None).unwrap();
+ g.add_edge(10, "hub", "a", NO_PROPS, None).unwrap();
+ g.add_edge(11, "hub", "hub", NO_PROPS, None).unwrap();
+
+ let graph: MaterializedGraph = g.into();
+ let tmp_dir = tempdir().unwrap();
+ let setup = setup_with_graphs(&[("g", graph)], tmp_dir.path()).await;
+
+ let query = r#"
+ {
+ graph(path: "g") {
+ node(name: "hub") {
+ edges {
+ explodeLayers {
+ sorted(sortBys: [{ neighbour: { type: true } }]) {
+ count
+ page(limit: 10) { nbr { name nodeType } }
+ }
+ }
+ }
+ }
+ }
+ }
+ "#;
+ let res = setup.schema.execute(Request::new(query)).await;
+ assert_eq!(res.errors, vec![], "{:?}", res.errors);
+ let data = res.data.into_json().unwrap();
+ assert_eq!(
+ data,
+ json!({ "graph": { "node": { "edges": { "explodeLayers": { "sorted": {
+ "count": 2,
+ "page": [
+ { "nbr": { "name": "a", "nodeType": "A" } },
+ { "nbr": { "name": "hub", "nodeType": "M" } }
+ ]
+ } } } } } })
+ );
+ }
+
+ #[tokio::test]
+ async fn test_edges_sorted_by_neighbour_type_reverse_untyped_last() {
+ let tmp_dir = tempdir().unwrap();
+ let setup = neighbour_sort_setup(tmp_dir.path()).await;
+
+ // neighbour type DESCENDING (untyped sorts last, not first), then
+ // neighbour name ascending to break the "A" tie: x(B), w(A), y(A), z(None)
+ let query = r#"
+ {
+ graph(path: "g") {
+ node(name: "hub") {
+ edges {
+ explodeLayers {
+ sorted(sortBys: [
+ { neighbour: { type: true, reverse: true } },
+ { neighbour: { name: true } }
+ ]) {
+ page(limit: 10) { nbr { name nodeType } }
+ }
+ }
+ }
+ }
+ }
+ }
+ "#;
+ let res = setup.schema.execute(Request::new(query)).await;
+ assert_eq!(res.errors, vec![], "{:?}", res.errors);
+ let data = res.data.into_json().unwrap();
+ assert_eq!(
+ data,
+ json!({ "graph": { "node": { "edges": { "explodeLayers": { "sorted": {
+ "page": [
+ { "nbr": { "name": "x", "nodeType": "B" } },
+ { "nbr": { "name": "w", "nodeType": "A" } },
+ { "nbr": { "name": "y", "nodeType": "A" } },
+ { "nbr": { "name": "z", "nodeType": null } }
+ ]
+ } } } } } })
+ );
+ }
+
+ #[tokio::test]
+ async fn test_graph_edges_sorted_by_src_node() {
+ // Sort a graph-level edge collection by the src node (type, then name).
+ // Enabled by making `src` take a NodeSortBy; a bare id flag could not
+ // express this.
+ let g = Graph::new();
+ g.add_node(1, "a", NO_PROPS, Some("Z"), None).unwrap();
+ g.add_node(1, "b", NO_PROPS, Some("A"), None).unwrap();
+ g.add_node(1, "c", NO_PROPS, None, None).unwrap(); // untyped
+ g.add_node(1, "x", NO_PROPS, None, None).unwrap();
+ g.add_edge(10, "a", "x", NO_PROPS, None).unwrap();
+ g.add_edge(11, "b", "x", NO_PROPS, None).unwrap();
+ g.add_edge(12, "c", "x", NO_PROPS, None).unwrap();
+
+ let graph: MaterializedGraph = g.into();
+ let tmp_dir = tempdir().unwrap();
+ let setup = setup_with_graphs(&[("g", graph)], tmp_dir.path()).await;
+
+ // src type ascending, untyped first: None(c) < "A"(b) < "Z"(a)
+ let query = r#"
+ {
+ graph(path: "g") {
+ edges {
+ sorted(sortBys: [{ src: { type: true } }, { src: { name: true } }]) {
+ page(limit: 10) { src { name nodeType } }
+ }
+ }
+ }
+ }
+ "#;
+ let res = setup.schema.execute(Request::new(query)).await;
+ assert_eq!(res.errors, vec![], "{:?}", res.errors);
+ let data = res.data.into_json().unwrap();
+ assert_eq!(
+ data,
+ json!({ "graph": { "edges": { "sorted": {
+ "page": [
+ { "src": { "name": "c", "nodeType": null } },
+ { "src": { "name": "b", "nodeType": "A" } },
+ { "src": { "name": "a", "nodeType": "Z" } }
+ ]
+ } } } })
+ );
+ }
}
diff --git a/raphtory-graphql/src/model/graph/edges.rs b/raphtory-graphql/src/model/graph/edges.rs
index 317a625d6d..0f7d6109a0 100644
--- a/raphtory-graphql/src/model/graph/edges.rs
+++ b/raphtory-graphql/src/model/graph/edges.rs
@@ -8,7 +8,7 @@ use crate::{
windowset::GqlEdgesWindowSet,
GqlAlignmentUnit, WindowDuration,
},
- sorting::{EdgeSortBy, SortByTime},
+ sorting::{compare_node, EdgeSortBy, SortByTime},
},
rayon::blocking_compute,
};
@@ -331,7 +331,7 @@ impl GqlEdges {
async fn sorted(
&self,
#[graphql(
- desc = "Ordered list of sort keys. Each entry chooses exactly one of `src` / `dst` / `time` / `property`, with an optional `reverse: true` to flip order."
+ desc = "Ordered list of sort keys. Each entry chooses exactly one of `src` / `dst` / `neighbour` / `time` / `property`, with an optional `reverse: true` to flip order."
)]
sort_bys: Vec,
) -> Self {
@@ -345,11 +345,32 @@ impl GqlEdges {
Ordering::Equal,
|current_ordering, sort_by| {
current_ordering.then_with(|| {
- let ordering = if sort_by.src == Some(true) {
- first_edge.src().id().partial_cmp(&second_edge.src().id())
- } else if sort_by.dst == Some(true) {
- first_edge.dst().id().partial_cmp(&second_edge.dst().id())
- } else if let Some(sort_by_time) = sort_by.time {
+ // Node keys resolve their endpoint and delegate
+ // to `compare_node`, which applies the nested
+ // `NodeSortBy.reverse`; they return directly so
+ // the outer `reverse` below never double-negates.
+ if let Some(src_sort) = sort_by.src.as_ref() {
+ return compare_node(
+ &first_edge.src(),
+ &second_edge.src(),
+ src_sort,
+ );
+ }
+ if let Some(dst_sort) = sort_by.dst.as_ref() {
+ return compare_node(
+ &first_edge.dst(),
+ &second_edge.dst(),
+ dst_sort,
+ );
+ }
+ if let Some(neighbour_sort) = sort_by.neighbour.as_ref() {
+ return compare_node(
+ &first_edge.nbr(),
+ &second_edge.nbr(),
+ neighbour_sort,
+ );
+ }
+ let ordering = if let Some(sort_by_time) = sort_by.time {
let (first_time, second_time) = match sort_by_time {
SortByTime::Latest => {
(first_edge.latest_time(), second_edge.latest_time())
diff --git a/raphtory-graphql/src/model/graph/nodes.rs b/raphtory-graphql/src/model/graph/nodes.rs
index 515f6a0c12..e989c2c467 100644
--- a/raphtory-graphql/src/model/graph/nodes.rs
+++ b/raphtory-graphql/src/model/graph/nodes.rs
@@ -8,7 +8,7 @@ use crate::{
windowset::GqlNodesWindowSet,
GqlAlignmentUnit, WindowDuration,
},
- sorting::{NodeSortBy, SortByTime},
+ sorting::{compare_node, NodeSortBy},
},
rayon::blocking_compute,
};
@@ -335,7 +335,7 @@ impl GqlNodes {
async fn sorted(
&self,
#[graphql(
- desc = "Ordered list of sort keys. Each entry chooses exactly one of `id` / `time` / `property`, with an optional `reverse: true` to flip order."
+ desc = "Ordered list of sort keys. Each entry chooses exactly one of `id` / `name` / `type` / `time` / `property`, with an optional `reverse: true` to flip order."
)]
sort_bys: Vec,
) -> Self {
@@ -348,39 +348,8 @@ impl GqlNodes {
sort_bys
.iter()
.fold(Ordering::Equal, |current_ordering, sort_by| {
- current_ordering.then_with(|| {
- let ordering = if sort_by.id == Some(true) {
- first_node.id().partial_cmp(&second_node.id())
- } else if let Some(sort_by_time) = sort_by.time.as_ref() {
- let (first_time, second_time) = match sort_by_time {
- SortByTime::Latest => {
- (first_node.latest_time(), second_node.latest_time())
- }
- SortByTime::Earliest => (
- first_node.earliest_time(),
- second_node.earliest_time(),
- ),
- };
- first_time.partial_cmp(&second_time)
- } else if let Some(sort_by_property) = sort_by.property.as_ref() {
- let first_prop_maybe =
- first_node.properties().get(sort_by_property);
- let second_prop_maybe =
- second_node.properties().get(sort_by_property);
- first_prop_maybe.partial_cmp(&second_prop_maybe)
- } else {
- None
- };
- if let Some(ordering) = ordering {
- if sort_by.reverse == Some(true) {
- ordering.reverse()
- } else {
- ordering
- }
- } else {
- Ordering::Equal
- }
- })
+ current_ordering
+ .then_with(|| compare_node(first_node, second_node, sort_by))
})
})
.map(|node_view| node_view.node)
diff --git a/raphtory-graphql/src/model/sorting.rs b/raphtory-graphql/src/model/sorting.rs
index 406a83d904..9e711eb33a 100644
--- a/raphtory-graphql/src/model/sorting.rs
+++ b/raphtory-graphql/src/model/sorting.rs
@@ -1,13 +1,21 @@
use dynamic_graphql::{Enum, InputObject};
+use raphtory::{db::graph::node::NodeView, prelude::*};
+use std::cmp::Ordering;
#[derive(InputObject, Clone, Debug, Eq, PartialEq)]
pub struct EdgeSortBy {
- /// Reverse order
+ /// Reverse order. Applies to the `time` / `property` keys; the node keys
+ /// (`src` / `dst` / `neighbour`) carry their own `reverse` inside the
+ /// nested `NodeSortBy` and ignore this flag.
pub reverse: Option,
- /// Source node
- pub src: Option,
- /// Destination
- pub dst: Option,
+ /// Sort by the source node.
+ pub src: Option,
+ /// Sort by the destination node.
+ pub dst: Option,
+ /// Sort by the neighbour node: the endpoint that is NOT the node these
+ /// edges were traversed from (the destination for a graph-level edge
+ /// collection).
+ pub neighbour: Option,
/// Time
pub time: Option,
/// Property
@@ -20,6 +28,11 @@ pub struct NodeSortBy {
pub reverse: Option,
/// Unique Id
pub id: Option,
+ /// Node name
+ pub name: Option,
+ /// Node type. Untyped nodes sort first (before any named type).
+ #[graphql(name = "type")]
+ pub type_: Option,
/// Time
pub time: Option,
/// Property
@@ -33,3 +46,37 @@ pub enum SortByTime {
/// Earliest time
Earliest,
}
+
+/// Compare two nodes by a single `NodeSortBy` key, applying that key's
+/// `reverse`. Returns `Ordering::Equal` when the key selects nothing or the
+/// values are incomparable. Shared by node sorting and edge neighbour sorting.
+pub(crate) fn compare_node<'graph, G: GraphViewOps<'graph>>(
+ a: &NodeView<'graph, G>,
+ b: &NodeView<'graph, G>,
+ sort_by: &NodeSortBy,
+) -> Ordering {
+ let ordering = if sort_by.id == Some(true) {
+ a.id().partial_cmp(&b.id())
+ } else if sort_by.name == Some(true) {
+ a.name().partial_cmp(&b.name())
+ } else if sort_by.type_ == Some(true) {
+ a.node_type().partial_cmp(&b.node_type())
+ } else if let Some(sort_by_time) = sort_by.time.as_ref() {
+ let (first, second) = match sort_by_time {
+ SortByTime::Latest => (a.latest_time(), b.latest_time()),
+ SortByTime::Earliest => (a.earliest_time(), b.earliest_time()),
+ };
+ first.partial_cmp(&second)
+ } else if let Some(prop) = sort_by.property.as_ref() {
+ a.properties()
+ .get(prop)
+ .partial_cmp(&b.properties().get(prop))
+ } else {
+ None
+ };
+ match ordering {
+ Some(o) if sort_by.reverse == Some(true) => o.reverse(),
+ Some(o) => o,
+ None => Ordering::Equal,
+ }
+}