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:
Any node can carry a dense embedding vector. Pass the
embedding argument when creating the node:
# Create nodes representing documents, each with a 4-dimensional embedding
doc1 <- client$create_node(
labels = c("Document"),
properties = list(title = "Graph Databases", topic = "databases"),
embedding = c(0.9, 0.1, 0.2, 0.05)
)
doc2 <- client$create_node(
labels = c("Document"),
properties = list(title = "Vector Search", topic = "search"),
embedding = c(0.1, 0.85, 0.3, 0.1)
)
doc3 <- client$create_node(
labels = c("Document"),
properties = list(title = "Neural Networks", topic = "ML"),
embedding = c(0.15, 0.8, 0.9, 0.7)
)
doc4 <- client$create_node(
labels = c("Document"),
properties = list(title = "Knowledge Graphs", topic = "databases"),
embedding = c(0.85, 0.2, 0.25, 0.1)
)Embedding dimensionality is flexible – AstraeaDB does not enforce a fixed size, though all embeddings used together in a search should share the same dimensionality.
Use vector_search() to find the k nodes
whose embeddings are most similar to a query vector:
results <- client$vector_search(
query_vector = c(0.88, 0.15, 0.2, 0.08),
k = 3L
)
for (r in results) {
cat(sprintf("Node %d -- distance: %.4f\n", r$node_id, r$distance))
}
#> Node 1 -- distance: 0.0048
#> Node 4 -- distance: 0.0129
#> Node 2 -- distance: 0.5877Each result entry contains at least node_id and
distance (smaller is closer). A legacy score
field equal to distance is also returned for backward
compatibility. The distance metric is determined by the server
configuration (typically cosine distance).
Hybrid search combines graph proximity (how close a
node is to an anchor node in the graph) with vector
similarity (how close its embedding is to a query vector). The
alpha parameter controls the blend:
alpha = 0.0 – Pure graph proximity (closer in hops =
higher rank).alpha = 1.0 – Pure vector similarity (ignore graph
structure).alpha = 0.5 – Equal weight to both signals.# Find nodes that are both near doc1 in the graph AND similar to a query vector
results <- client$hybrid_search(
anchor = doc1,
query_vector = c(0.8, 0.3, 0.25, 0.1),
max_hops = 3L,
k = 5L,
alpha = 0.7
)
for (r in results) {
cat(sprintf("Node %d -- combined score: %.4f\n", r$node_id, r$score))
}Use case: In a knowledge base, you want to find
documents that are topically similar to a query (vector similarity) and
also structurally related to a known starting point (graph proximity).
By tuning alpha you can control how much the graph
structure influences the ranking.
Semantic search operations use a “concept” embedding to rank or navigate the graph. Unlike vector search, which operates globally, semantic search is local – it starts from a specific node and considers only its neighborhood.
semantic_neighbors() retrieves the neighbors of a node
and ranks them by how similar their embeddings are to a concept
vector:
# First, connect the documents with edges
client$create_edge(doc1, doc2, "RELATED_TO")
client$create_edge(doc1, doc3, "RELATED_TO")
client$create_edge(doc1, doc4, "RELATED_TO")
# Rank doc1's neighbors by similarity to a "search-oriented" concept
ranked <- client$semantic_neighbors(
node_id = doc1,
concept = c(0.1, 0.9, 0.3, 0.1),
direction = "outgoing",
k = 5L
)
for (r in ranked) {
cat(sprintf("Neighbor %d -- distance: %.4f\n", r$node_id, r$distance))
}semantic_walk() performs a greedy traversal of the
graph. At each step it moves to the neighbor whose embedding is most
similar to the concept vector:
# Walk the graph following the "ML" concept
path <- client$semantic_walk(
start = doc1,
concept = c(0.1, 0.8, 0.9, 0.7),
max_hops = 4L
)
cat("Walk path:", paste(path, collapse = " -> "), "\n")This is useful for exploring how a topic “flows” through a knowledge graph.
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.
# 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")
)# 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
#> CarolTemporal queries allow you to answer questions such as:
By sweeping a timestamp across a range, you can reconstruct the evolution of a graph.
GraphRAG integrates graph data with large language models. AstraeaDB provides two methods:
extract_subgraph() – Extracts a
subgraph centered on a node and linearizes it to text.graph_rag() – Full pipeline that
extracts a subgraph, linearizes it, and sends the context and a question
to a language model.# 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. |
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:
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.
# 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.
# 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")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.
For small CRUD operations (create a node, get an edge), JSON/TCP is perfectly adequate and has no extra dependencies.
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:
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.
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. |
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:
The UnifiedClient passes the token to the JSON/TCP
client:
If the token is missing, invalid, or the user’s role lacks the
required permissions, the server returns an error. Handle it with
tryCatch():
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")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").