Advanced Features: Vector Search, Temporal Queries, and GraphRAG

This vignette covers the advanced capabilities of the AstraeaDB R package: vector similarity search, hybrid and semantic search, temporal (time-travel) queries, GraphRAG for LLM integration, Apache Arrow Flight transport, and authentication.

All examples assume you have a running AstraeaDB server and a connected client:

library(AstraeaDB)
client <- astraea_connect()

Temporal Queries (Time-Travel)

AstraeaDB edges can have temporal validity windows. When an edge has valid_from and/or valid_to set (milliseconds since the Unix epoch), it is only visible in temporal queries whose timestamp falls within the window.

Setting Up Temporal Edges

# Create a social network with temporal edges
alice <- client$create_node(c("Person"), list(name = "Alice"))
bob   <- client$create_node(c("Person"), list(name = "Bob"))
carol <- client$create_node(c("Person"), list(name = "Carol"))
dave  <- client$create_node(c("Person"), list(name = "Dave"))

# Helper: convert date string to epoch milliseconds
to_epoch_ms <- function(date_str) {
  as.numeric(as.POSIXct(date_str, tz = "UTC")) * 1000
}

# Alice knew Bob from 2020 to 2022
client$create_edge(
  source     = alice,
  target     = bob,
  edge_type  = "FRIENDS",
  valid_from = to_epoch_ms("2020-01-01"),
  valid_to   = to_epoch_ms("2022-01-01")
)

# Alice has known Carol since 2021 (no end date -- still active)
client$create_edge(
  source     = alice,
  target     = carol,
  edge_type  = "FRIENDS",
  valid_from = to_epoch_ms("2021-06-01")
)

# Alice has known Dave since 2023
client$create_edge(
  source     = alice,
  target     = dave,
  edge_type  = "FRIENDS",
  valid_from = to_epoch_ms("2023-01-01")
)

Querying at a Point in Time

neighbors_at

# Who was Alice friends with on July 1, 2021?
mid_2021 <- to_epoch_ms("2021-07-01")
friends_2021 <- client$neighbors_at(alice, "outgoing", mid_2021)

# Result includes Bob and Carol, but not Dave (not yet friends)
for (f in friends_2021) {
  node <- client$get_node(f$node_id)
  cat(node$properties$name, "\n")
}
#> Bob
#> Carol

bfs_at

# BFS at a point in time
bfs_2021 <- client$bfs_at(alice, max_depth = 2L, timestamp = mid_2021)

for (entry in bfs_2021) {
  cat(sprintf("Node %d at depth %d\n", entry$node_id, entry$depth))
}

shortest_path_at

# Shortest path at a specific time
early_2024 <- to_epoch_ms("2024-01-15")

sp <- client$shortest_path_at(
  from_node = alice,
  to_node   = dave,
  timestamp = early_2024,
  weighted  = FALSE
)

cat("Path:", paste(sp$path, collapse = " -> "), "\n")
cat("Hops:", sp$length, "\n")

Use Case: Evolving Relationships

Temporal queries allow you to answer questions such as:

By sweeping a timestamp across a range, you can reconstruct the evolution of a graph.

GraphRAG

GraphRAG integrates graph data with large language models. AstraeaDB provides two methods:

  1. extract_subgraph() – Extracts a subgraph centered on a node and linearizes it to text.
  2. graph_rag() – Full pipeline that extracts a subgraph, linearizes it, and sends the context and a question to a language model.

Subgraph Extraction

# Extract a 2-hop subgraph around Alice, linearized as structured text
sg <- client$extract_subgraph(
  center    = alice,
  hops      = 2L,
  max_nodes = 20L,
  format    = "structured"
)

cat("Nodes:", sg$node_count, "\n")
cat("Edges:", sg$edge_count, "\n")
cat("\n", sg$text, "\n")

The format parameter controls how the subgraph is serialized:

Format Description
"structured" Indented, human-readable summary of nodes and edges.
"prose" Natural-language narrative describing the subgraph.
"triples" List of (subject, predicate, object) triples.
"json" Machine-readable JSON representation.

Full GraphRAG Pipeline

graph_rag() sends the extracted subgraph context along with your question to the language model configured on the server:

answer <- client$graph_rag(
  question  = "Who are Alice's current friends and what do they work on?",
  anchor    = alice,
  hops      = 2L,
  max_nodes = 30L,
  format    = "prose"
)

cat(answer$answer, "\n")

You can also provide a question_embedding instead of (or in addition to) an anchor node. This lets the server find the most relevant anchor automatically via vector search:

answer <- client$graph_rag(
  question            = "What research topics are related to graph databases?",
  question_embedding  = c(0.9, 0.1, 0.2, 0.05),
  hops                = 3L,
  max_nodes           = 50L,
  format              = "structured"
)

cat(answer$answer, "\n")

Graph Algorithms

AstraeaDB computes classic graph-analytics algorithms server-side and returns the results over the wire. Pass nodes = to any of them to restrict the computation to a subset of node IDs; the default is the whole graph.

Centrality and Ranking

# PageRank
scores <- client$run_pagerank(damping = 0.85, max_iterations = 100L)

# Degree and betweenness centrality
deg <- client$run_degree_centrality(direction = "both")
btw <- client$run_betweenness_centrality()

Each returns a named list mapping the node ID (as a character key) to its score.

Community Detection and Components

# Louvain community detection
louvain <- client$run_louvain()
cat("Communities found:", louvain$num_communities, "\n")

# Connected components (weakly connected by default; strong = TRUE for SCCs)
cc <- client$run_connected_components(strong = FALSE)
cat("Number of components:", cc$count, "\n")

Traversal, Lookups, and Statistics

Finding and Bulk-Deleting by Label

# All node IDs carrying a label
people <- client$find_by_label("Person")

# All edges of a given type, each as {edge_id, source, target}
knows_edges <- client$find_edge_by_type("KNOWS")

# Bulk-delete every node with a label (and its edges); returns the count
removed <- client$delete_by_label("Temporary")

Subgraph Export and Statistics

# Raw subgraph (nodes + edges) around a center node, for visualization
sg <- client$get_subgraph(alice, hops = 2L, max_nodes = 100L)

# Graph-wide statistics
stats <- client$graph_stats()
cat("Nodes:", stats$total_nodes, " Edges:", stats$total_edges, "\n")

Arrow Flight Transport

For large result sets and analytical workloads, the Arrow Flight transport provides significantly better performance than JSON/TCP. Arrow Flight uses Apache Arrow’s columnar format for zero-copy data exchange between the server and R.

When to Use Arrow Flight

For small CRUD operations (create a node, get an edge), JSON/TCP is perfectly adequate and has no extra dependencies.

ArrowClient (Direct Use)

The ArrowClient class communicates directly with the Arrow Flight endpoint (default port 7689). It requires the arrow package.

# install.packages("arrow")  # if not already installed
library(AstraeaDB)

ac <- ArrowClient$new("grpc://localhost:7689")
ac$connect()

# Execute a GQL query -- returns an Arrow Table
table <- ac$query("MATCH (p:Person) RETURN p.name, p.age")

# Convert to data.frame
df <- as.data.frame(table)

# Or use the convenience method
df <- ac$query_df("MATCH (p:Person) RETURN p.name, p.age")

For very large result sets, stream the results in batches to control memory usage:

ac$query_batches(
  "MATCH (n) RETURN n",
  callback = function(batch) {
    cat("Received batch with", nrow(batch), "rows\n")
    # Process each batch incrementally
  }
)

ac$disconnect()

You can also use the convenience wrapper:

ac <- astraea_arrow_connect("grpc://localhost:7689")
# ... work ...
ac$disconnect()

UnifiedClient (Automatic Transport Selection)

The UnifiedClient is the recommended choice when you want the best of both worlds. It delegates CRUD operations to the JSON/TCP client and routes GQL queries through Arrow Flight when the arrow package is installed:

uc <- UnifiedClient$new(
  host       = "127.0.0.1",
  port       = 7687L,
  flight_uri = "grpc://localhost:7689"
)
uc$connect()

# Check which transports are active
uc$is_arrow_enabled()
#> [1] TRUE

# CRUD operations go through JSON/TCP
node_id <- uc$create_node(c("Person"), list(name = "Grace", age = 42))

# Queries go through Arrow Flight (or fall back to JSON/TCP)
df <- uc$query_df("MATCH (p:Person) RETURN p.name, p.age")

# All other operations are available as usual
uc$neighbors(node_id, direction = "outgoing")
uc$vector_search(c(0.5, 0.5, 0.5, 0.5), k = 3L)

uc$disconnect()

If the arrow package is not installed, or if the Arrow Flight connection fails, the UnifiedClient silently falls back to JSON/TCP for everything. A message is printed when fallback occurs.

Authentication

AstraeaDB supports token-based authentication with server-side role-based access control (RBAC). Three roles are available:

Role Permissions
Reader Read-only: get nodes/edges, traversals, queries.
Writer Read and write: CRUD, batch, import/export.
Admin Full access: all operations plus server management.

Using an Auth Token

Pass the auth_token parameter when creating any client. The token is automatically attached to every request:

# AstraeaClient with authentication
client <- AstraeaClient$new(
  host       = "127.0.0.1",
  port       = 7687L,
  auth_token = "my-secret-token"
)
client$connect()

# All operations now carry the token
client$ping()
client$create_node(c("Person"), list(name = "Secured"))
client$disconnect()

The convenience function also accepts a token:

client <- astraea_connect(auth_token = "my-secret-token")
# ... work ...
client$disconnect()

The UnifiedClient passes the token to the JSON/TCP client:

uc <- UnifiedClient$new(
  host       = "127.0.0.1",
  port       = 7687L,
  auth_token = "my-secret-token"
)
uc$connect()
# ... work ...
uc$disconnect()

Handling Authentication Errors

If the token is missing, invalid, or the user’s role lacks the required permissions, the server returns an error. Handle it with tryCatch():

tryCatch(
  {
    client <- astraea_connect(auth_token = "wrong-token")
    client$create_node(c("Test"), list(x = 1))
  },
  error = function(e) {
    message("Auth error: ", conditionMessage(e))
  }
)

Putting It All Together

The following example builds a small knowledge graph with embeddings and temporal edges, then demonstrates vector search, hybrid search, temporal queries, and GraphRAG in a single workflow:

library(AstraeaDB)
client <- astraea_connect()
on.exit(client$disconnect(), add = TRUE)

# --- Build the graph ---
ml   <- client$create_node(c("Topic"), list(name = "Machine Learning"),
                            embedding = c(0.1, 0.8, 0.9, 0.7))
nlp  <- client$create_node(c("Topic"), list(name = "NLP"),
                            embedding = c(0.2, 0.9, 0.7, 0.6))
kg   <- client$create_node(c("Topic"), list(name = "Knowledge Graphs"),
                            embedding = c(0.85, 0.2, 0.25, 0.1))
rag  <- client$create_node(c("Topic"), list(name = "RAG"),
                            embedding = c(0.6, 0.7, 0.5, 0.4))

to_ms <- function(d) as.numeric(as.POSIXct(d, tz = "UTC")) * 1000

client$create_edge(ml,  nlp, "RELATED_TO", weight = 0.9,
                   valid_from = to_ms("2018-01-01"))
client$create_edge(nlp, rag, "ENABLES",    weight = 0.8,
                   valid_from = to_ms("2022-01-01"))
client$create_edge(kg,  rag, "ENABLES",    weight = 0.85,
                   valid_from = to_ms("2020-01-01"))
client$create_edge(ml,  kg,  "RELATED_TO", weight = 0.7,
                   valid_from = to_ms("2015-01-01"))

# --- Vector search ---
cat("== Vector Search ==\n")
vs <- client$vector_search(c(0.15, 0.85, 0.8, 0.65), k = 2L)
for (r in vs) {
  cat(sprintf("  Node %d (dist %.4f)\n", r$node_id, r$distance))
}

# --- Hybrid search ---
cat("\n== Hybrid Search ==\n")
hs <- client$hybrid_search(
  anchor = ml, query_vector = c(0.6, 0.7, 0.5, 0.4),
  max_hops = 2L, k = 3L, alpha = 0.5
)
for (r in hs) {
  cat(sprintf("  Node %d\n", r$node_id))
}

# --- Temporal query ---
cat("\n== Temporal Query (2019) ==\n")
nbrs_2019 <- client$neighbors_at(ml, "outgoing", to_ms("2019-06-01"))
for (n in nbrs_2019) {
  node <- client$get_node(n$node_id)
  cat(sprintf("  %s\n", node$properties$name))
}
# Only "NLP" and "Knowledge Graphs" -- RAG edge did not exist in 2019

# --- GraphRAG ---
cat("\n== GraphRAG ==\n")
answer <- client$graph_rag(
  question  = "How are ML and RAG connected?",
  anchor    = ml,
  hops      = 2L,
  max_nodes = 20L,
  format    = "prose"
)
cat(answer$answer, "\n")

Summary

This vignette demonstrated the advanced features of the AstraeaDB R package:

For basic CRUD operations, traversals, GQL queries, and data-frame helpers, see vignette("getting-started"). For an overview of the package and data model, see vignette("introduction").