Skip to content

Adding algorithms to GraphQL - #2696

Closed
arienandalibi wants to merge 44 commits into
db_v4from
db_v4_graphql_algorithms
Closed

Adding algorithms to GraphQL#2696
arienandalibi wants to merge 44 commits into
db_v4from
db_v4_graphql_algorithms

Conversation

@arienandalibi

Copy link
Copy Markdown
Collaborator

What changes were proposed in this pull request?

Exposing the algorithms in GraphQL. They are currently only available in core Rust and Python.

Why are the changes needed?

Accessing them in GraphQL.

Does this PR introduce any user-facing change? If yes is this documented?

Yes, many GraphQL queries. All documented.

How was this patch tested?

Unit tests for each algorithm.

Are there any further changes required?

Yes, potential follow-ups on GqlNodeState ( #2694 ) and the algorithms ( #2695 )

…r for NodeState (we use OutputTypedNodeState because there are no generic type parameters), similar to how PyOutputNodeState does it. Currently added pagerank
… contain all the values for that column. Each column is an output of the algorithm. We can then access the column's name and each output value. These values are row-aligned with the nodes of the node state. These nodes are queried and returned separately.
…tations. Added a comparison function to use for these. Added sum and mean functions in GraphQL. They can't use the Rust implementations because of trait bounds on the functions, so we must re-implement them to handle number casting (and possible errors there).
…by node) instead of by column. There is also a variant to return rows without column names
…bounded, temporal_three_node_motif, and temporally_reachable_nodes algorithms in GraphQL
… input as well as an optional node filter. We want to match python by making this a general filter, which doesn't currently exist in GraphQL.
…s (NodeFilter, EdgeFilter, GraphFilter). This allows a combination of all 3 filters to be passed as a single filter. Use them in in_component and out_component.
…icient, global_reciprocity algorithms in GraphQL
…egree, min_out_degree, triangle_count, and triplet_count algorithms in GraphQL
…ti, Matching, and max_weight_matching algorithms in GraphQL

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'Rust Benchmark'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 2.

Benchmark suite Current: 6aa20a0 Previous: 9823ef7 Ratio
lotr_graph/num_edges 4 ns/iter (± 0) 0 ns/iter (± 0) +∞
lotr_graph/num_nodes 6 ns/iter (± 0) 1 ns/iter (± 0) 6
lotr_graph/graph_latest 3 ns/iter (± 0) 0 ns/iter (± 0) +∞
lotr_graph_materialise/materialize 7909266 ns/iter (± 82084) 1564816 ns/iter (± 35303) 5.05
lotr_graph_window_100/num_nodes 15 ns/iter (± 0) 5 ns/iter (± 0) 3
lotr_graph_window_100_materialise/materialize 7908831 ns/iter (± 35091) 1669150 ns/iter (± 10700) 4.74
lotr_graph_window_10/has_node_existing 136 ns/iter (± 11) 62 ns/iter (± 11) 2.19
lotr_graph_window_10_materialise/materialize 3317912 ns/iter (± 14022) 971980 ns/iter (± 4278) 3.41
lotr_graph_subgraph_10pc_materialise/materialize 2059252 ns/iter (± 12281) 334634 ns/iter (± 1287) 6.15
lotr_graph_subgraph_10pc_windowed/has_node_existing 148 ns/iter (± 10) 62 ns/iter (± 14) 2.39
lotr_graph_subgraph_10pc_windowed_materialise/materialize 1264716 ns/iter (± 20374) 230399 ns/iter (± 2617) 5.49
lotr_graph_window_50_layered/num_edges_temporal 146584 ns/iter (± 2063) 70121 ns/iter (± 7586) 2.09
lotr_graph_window_50_layered/has_node_existing 377 ns/iter (± 22) 129 ns/iter (± 12) 2.92
lotr_graph_window_50_layered/graph_latest 88810 ns/iter (± 1281) 36649 ns/iter (± 916) 2.42
lotr_graph_window_50_layered_materialise/materialize 28985918 ns/iter (± 177795) 3488825 ns/iter (± 24948) 8.31
lotr_graph_persistent_window_50_layered/num_edges_temporal 586545 ns/iter (± 5990) 192686 ns/iter (± 1569) 3.04
lotr_graph_persistent_window_50_layered/has_node_existing 425 ns/iter (± 375) 174 ns/iter (± 83) 2.44
lotr_graph_persistent_window_50_layered/has_node_nonexisting 5 ns/iter (± 0) 2 ns/iter (± 0) 2.50
lotr_graph_persistent_window_50_layered/graph_latest 124762 ns/iter (± 2834) 57549 ns/iter (± 4809) 2.17
lotr_graph_persistent_window_50_layered_materialise/materialize 51626948 ns/iter (± 591544) 5298035 ns/iter (± 147912) 9.74

This comment was automatically generated by workflow using github-action-benchmark.

@miratepuffin miratepuffin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a good idea to split the algorithms folder into a similar structure as the actual algorithms folder

pub(crate) mod weakly_connected_components;

/// A graph algorithm executable through the GraphQL API.
pub(crate) trait GqlExecutableAlgorithm: 'static {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot packed into this mod - I would split it out into different files

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally docs would be sync'd with python docs somehow

Layers(GraphLayersExpr),
}

/// A composite filter producing a graph view, bundling the graph-view, node,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have something similar to this in my PR, we will need to merge - I don't think you are support And/Or here?

Comment thread raphtory-graphql/src/lib.rs Outdated
}

#[tokio::test]
async fn test_algorithm_node_state_ops() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put all of the tests into the actual algorithms themselves, gonna be easier to see which ones haven't been tested then.

Might not be a terrible idea to sanity check in python by running on the local graph and then via graphql and seeing if we get the same result?

Ok(alternating_mask(graph).into())
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to expose this - its a test algorithm

args: A::Args,
) -> Result<A::Output, GraphError> {
let graph = self.graph.clone();
blocking_compute(move || A::execute(&graph, args)).await

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably not an issue right now, but we should probably highly weight some of these algorithms in terms of graphql complexity?

@ljeub-pometry ljeub-pometry left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all the extra abstraction and code for each algorithm isn't actually needed?

/// Name of the column.
name: String,
/// The values of this column; `values[i]` belongs to `NodeState.nodes[i]`.
values: Vec<GqlNodeStateValue>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't great, you need to reallocate a new vector to make this instead of reusing the underlying column!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, SimpleObject is probably not the right thing to use here? Should be resolved with accessors for stuff like sum etc...


impl GqlNodeState {
/// A slice of `limit` rows starting at `start`, as a node state of its own.
fn slice(&self, start: usize, limit: usize) -> GqlNodeState {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should expose an api like this on the NodeState itself

}

/// Returns the values for a node, one entry per column; null if the node has no value in this NodeState.
async fn get(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this return a Option<GqlNodeStateRow>?

Comment on lines +368 to +550
async fn min(
&self,
#[graphql(desc = "Column name.")] column: String,
) -> Option<GqlNodeStateItem> {
let self_clone = self.clone();
blocking_compute(move || {
self_clone.check_comparable(&column)?;
let item = self_clone
.node_state
.min_item_by(column_cmp(&column, true))?;
self_clone.item_from_row(&column, item)
})
.await
}

/// Maximum `(node, value)` of a column. Null if the column does not exist, is empty,
/// or its values are not comparable (e.g. contains nodes).
async fn max(
&self,
#[graphql(desc = "Column name.")] column: String,
) -> Option<GqlNodeStateItem> {
let self_clone = self.clone();
blocking_compute(move || {
self_clone.check_comparable(&column)?;
let item = self_clone
.node_state
.max_item_by(column_cmp(&column, false))?;
self_clone.item_from_row(&column, item)
})
.await
}

/// Sum of a column's values, skipping empty cells. Null if the column does not exist, is empty,
/// or is not additive (e.g. contains nodes).
async fn sum(
&self,
#[graphql(desc = "Column name.")] column: String,
) -> Option<GqlPropertyOutputVal> {
let self_clone = self.clone();
blocking_compute(move || {
let mut values = self_clone.column_value_iter(&column)?;
let mut acc = values.next()?;
if !acc.dtype().has_add() {
return None;
}
for value in values {
acc = acc.add(value)?;
}
Some(GqlPropertyOutputVal(acc))
})
.await
}

/// Mean of a column's values as a float, skipping empty cells. Null if the column does not exist,
/// is empty, or has any non-numeric value.
async fn mean(
&self,
#[graphql(desc = "Column name.")] column: String,
) -> Option<GqlPropertyOutputVal> {
let self_clone = self.clone();
blocking_compute(move || {
let mut values = self_clone.column_value_iter(&column)?;
let mut sum = values.next()?.as_f64()?;
let mut count = 1usize;
for value in values {
sum += value.as_f64()?;
count += 1;
}
Some(GqlPropertyOutputVal(Prop::F64(sum / count as f64)))
})
.await
}

/// Median `(node, value)` of a column (upper median on even lengths). Null if the column
/// does not exist, is empty, or is not comparable (e.g. contains nodes).
async fn median(
&self,
#[graphql(desc = "Column name.")] column: String,
) -> Option<GqlNodeStateItem> {
let self_clone = self.clone();
blocking_compute(move || {
self_clone.check_comparable(&column)?;
let item = self_clone
.node_state
.median_item_by(column_cmp(&column, true))?;
self_clone.item_from_row(&column, item)
})
.await
}

/// Returns the `k` rows with the largest values in a column. Empty cells rank
/// lowest, so they are only included if fewer than `k` rows have a value.
/// Null if the column does not exist, is empty, or is not comparable.
async fn top_k(
&self,
#[graphql(desc = "Column name.")] column: String,
#[graphql(desc = "Number of rows to return.")] k: usize,
) -> Option<GqlNodeState> {
let self_clone = self.clone();
blocking_compute(move || {
self_clone.check_comparable(&column)?;
Some(GqlNodeState {
node_state: self_clone
.node_state
.top_k_by(column_cmp(&column, false), k),
})
})
.await
}

/// Returns the `k` rows with the smallest values in a column. Empty cells rank
/// highest, so they are only included if fewer than `k` rows have a value.
/// Null if the column does not exist, is empty, or is not comparable.
async fn bottom_k(
&self,
#[graphql(desc = "Column name.")] column: String,
#[graphql(desc = "Number of rows to return.")] k: usize,
) -> Option<GqlNodeState> {
let self_clone = self.clone();
blocking_compute(move || {
self_clone.check_comparable(&column)?;
Some(GqlNodeState {
node_state: self_clone
.node_state
.bottom_k_by(column_cmp(&column, true), k),
})
})
.await
}

/// Returns a view of this node state with the rows sorted by a column's values,
/// ascending with empty cells last. `reverse` flips the whole ordering, putting
/// empty cells first. Null if the column does not exist, is empty, or is not
/// comparable.
async fn sort_by_values(
&self,
#[graphql(desc = "Column name.")] column: String,
#[graphql(desc = "Sort in descending order instead. Defaults to false.")] reverse: Option<
bool,
>,
) -> Option<GqlNodeState> {
let self_clone = self.clone();
blocking_compute(move || {
self_clone.check_comparable(&column)?;
let cmp = column_cmp(&column, true);
let node_state = if reverse.unwrap_or(false) {
self_clone
.node_state
.sort_by_values_by(|a, b| cmp(a, b).reverse())
} else {
self_clone.node_state.sort_by_values_by(cmp)
};
Some(GqlNodeState { node_state })
})
.await
}

/// Groups the nodes by their value in a column. Nodes with an empty cell form
/// their own group. Null if the column does not exist or contains nodes.
async fn group_by(
&self,
#[graphql(desc = "Column name.")] column: String,
) -> Option<Vec<GqlNodeStateGroup>> {
let self_clone = self.clone();
blocking_compute(move || {
if !self_clone.is_prop_column(&column) {
return None;
}
let groups = self_clone.node_state.group_by(|mut row| {
GroupKey::new(row.swap_remove(&column).flatten().map(|value| value.0))
});
Some(
groups
.into_iter_groups()
.map(|(key, nodes)| GqlNodeStateGroup {
value: key.value.map(GqlPropertyOutputVal),
nodes: GqlNodes::new(nodes),
})
.collect(),
)
})
.await
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need proper single-column apis in the rust layer. However, the ordering based apis here should work with multiple columns, same as in rust.


/// The columns of the node state, one per output field of the algorithm.
/// `values` are row-aligned with `nodes`.
async fn columns(&self) -> Vec<GqlNodeStateColumn> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there should be column(name) as well to get a single column

#[graphql(desc = "Edge property to use as weight. If unset, all edges have weight 1.")]
weight: Option<String>,
) -> Result<GqlNodeState, GraphError> {
self.run::<GqlPagerank>(GqlPagerankArgs {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the point in the whole executable machinery when you are still writing all these functions here anyway? You could have e.g., just called blocking_compute(|| {pagerank()).await here?

#[graphql(desc = "Edge property to use as weight. If unset, all edges have weight 1.")]
weight: Option<String>,
) -> Result<GqlNodeState, GraphError> {
self.run::<GqlPagerank>(GqlPagerankArgs {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole executable machinery seems to be entirely useless? You could just change the run method to take a closure that has a DynamicGraph as input and a generic as output and have everything inline here without all the extra code for each algorithm!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants