1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4use astraea_core::traits::{GraphOps, VectorIndex};
5use astraea_core::types::*;
6use astraea_query::executor::Executor;
7
8use crate::protocol::{Request, Response};
9
10pub struct RequestHandler {
12 graph: Arc<dyn GraphOps>,
13 executor: Executor,
14 vector_index: Option<Arc<dyn VectorIndex>>,
15}
16
17impl RequestHandler {
18 pub fn new(graph: Arc<dyn GraphOps>, vector_index: Option<Arc<dyn VectorIndex>>) -> Self {
19 let executor = Executor::new(Arc::clone(&graph));
20 Self {
21 graph,
22 executor,
23 vector_index,
24 }
25 }
26
27 pub fn handle(&self, request: Request) -> Response {
29 match request {
30 Request::CreateNode {
31 labels,
32 properties,
33 embedding,
34 } => match self.graph.create_node(labels, properties, embedding) {
35 Ok(id) => Response::ok(serde_json::json!({"node_id": id.0})),
36 Err(e) => Response::error(e.to_string()),
37 },
38
39 Request::CreateEdge {
40 source,
41 target,
42 edge_type,
43 properties,
44 weight,
45 valid_from,
46 valid_to,
47 } => {
48 match self.graph.create_edge(
49 NodeId(source),
50 NodeId(target),
51 edge_type,
52 properties,
53 weight,
54 valid_from,
55 valid_to,
56 ) {
57 Ok(id) => Response::ok(serde_json::json!({"edge_id": id.0})),
58 Err(e) => Response::error(e.to_string()),
59 }
60 }
61
62 Request::GetNode { id } => match self.graph.get_node(NodeId(id)) {
63 Ok(Some(node)) => Response::ok(serde_json::json!({
64 "id": node.id.0,
65 "labels": node.labels,
66 "properties": node.properties,
67 "has_embedding": node.embedding.is_some(),
68 })),
69 Ok(None) => Response::error(format!("node {id} not found")),
70 Err(e) => Response::error(e.to_string()),
71 },
72
73 Request::GetEdge { id } => match self.graph.get_edge(EdgeId(id)) {
74 Ok(Some(edge)) => Response::ok(serde_json::json!({
75 "id": edge.id.0,
76 "source": edge.source.0,
77 "target": edge.target.0,
78 "edge_type": edge.edge_type,
79 "properties": edge.properties,
80 "weight": edge.weight,
81 "valid_from": edge.validity.valid_from,
82 "valid_to": edge.validity.valid_to,
83 })),
84 Ok(None) => Response::error(format!("edge {id} not found")),
85 Err(e) => Response::error(e.to_string()),
86 },
87
88 Request::UpdateNode { id, properties } => {
89 match self.graph.update_node(NodeId(id), properties) {
90 Ok(()) => Response::ok(serde_json::json!({"updated": true})),
91 Err(e) => Response::error(e.to_string()),
92 }
93 }
94
95 Request::UpdateEdge { id, properties } => {
96 match self.graph.update_edge(EdgeId(id), properties) {
97 Ok(()) => Response::ok(serde_json::json!({"updated": true})),
98 Err(e) => Response::error(e.to_string()),
99 }
100 }
101
102 Request::DeleteNode { id } => match self.graph.delete_node(NodeId(id)) {
103 Ok(()) => Response::ok(serde_json::json!({"deleted": true})),
104 Err(e) => Response::error(e.to_string()),
105 },
106
107 Request::DeleteEdge { id } => match self.graph.delete_edge(EdgeId(id)) {
108 Ok(()) => Response::ok(serde_json::json!({"deleted": true})),
109 Err(e) => Response::error(e.to_string()),
110 },
111
112 Request::Neighbors {
113 id,
114 direction,
115 edge_type,
116 } => {
117 let dir = match direction.as_str() {
118 "incoming" => Direction::Incoming,
119 "both" => Direction::Both,
120 _ => Direction::Outgoing,
121 };
122
123 let result = if let Some(et) = edge_type {
124 self.graph.neighbors_filtered(NodeId(id), dir, &et)
125 } else {
126 self.graph.neighbors(NodeId(id), dir)
127 };
128
129 match result {
130 Ok(neighbors) => {
131 let items: Vec<serde_json::Value> = neighbors
132 .into_iter()
133 .map(|(eid, nid)| {
134 serde_json::json!({"edge_id": eid.0, "node_id": nid.0})
135 })
136 .collect();
137 Response::ok(serde_json::json!({"neighbors": items}))
138 }
139 Err(e) => Response::error(e.to_string()),
140 }
141 }
142
143 Request::Bfs { start, max_depth } => match self.graph.bfs(NodeId(start), max_depth) {
144 Ok(nodes) => {
145 let items: Vec<serde_json::Value> = nodes
146 .into_iter()
147 .map(|(nid, depth)| serde_json::json!({"node_id": nid.0, "depth": depth}))
148 .collect();
149 Response::ok(serde_json::json!({"nodes": items}))
150 }
151 Err(e) => Response::error(e.to_string()),
152 },
153
154 Request::ShortestPath { from, to, weighted } => {
155 if weighted {
156 match self.graph.shortest_path_weighted(NodeId(from), NodeId(to)) {
157 Ok(Some((path, cost))) => {
158 let node_ids: Vec<u64> =
159 path.nodes().into_iter().map(|n| n.0).collect();
160 Response::ok(
161 serde_json::json!({"path": node_ids, "cost": cost, "length": path.len()}),
162 )
163 }
164 Ok(None) => Response::ok(serde_json::json!({"path": null})),
165 Err(e) => Response::error(e.to_string()),
166 }
167 } else {
168 match self.graph.shortest_path(NodeId(from), NodeId(to)) {
169 Ok(Some(path)) => {
170 let node_ids: Vec<u64> =
171 path.nodes().into_iter().map(|n| n.0).collect();
172 Response::ok(
173 serde_json::json!({"path": node_ids, "length": path.len()}),
174 )
175 }
176 Ok(None) => Response::ok(serde_json::json!({"path": null})),
177 Err(e) => Response::error(e.to_string()),
178 }
179 }
180 }
181
182 Request::HybridSearch {
183 anchor,
184 query,
185 max_hops,
186 k,
187 alpha,
188 } => {
189 match self
190 .graph
191 .hybrid_search(NodeId(anchor), &query, max_hops, k, alpha)
192 {
193 Ok(results) => {
194 let items: Vec<serde_json::Value> = results
195 .into_iter()
196 .map(|(nid, score)| {
197 serde_json::json!({"node_id": nid.0, "score": score})
198 })
199 .collect();
200 Response::ok(serde_json::json!({"results": items}))
201 }
202 Err(e) => Response::error(e.to_string()),
203 }
204 }
205
206 Request::SemanticNeighbors {
207 id,
208 concept,
209 direction,
210 k,
211 } => {
212 let dir = match direction.as_str() {
213 "incoming" => Direction::Incoming,
214 "both" => Direction::Both,
215 _ => Direction::Outgoing,
216 };
217 match self.graph.semantic_neighbors(NodeId(id), &concept, dir, k) {
218 Ok(results) => {
219 let items: Vec<serde_json::Value> = results
220 .into_iter()
221 .map(|(nid, dist)| {
222 serde_json::json!({"node_id": nid.0, "distance": dist})
223 })
224 .collect();
225 Response::ok(serde_json::json!({"results": items}))
226 }
227 Err(e) => Response::error(e.to_string()),
228 }
229 }
230
231 Request::SemanticWalk {
232 start,
233 concept,
234 max_hops,
235 } => match self.graph.semantic_walk(NodeId(start), &concept, max_hops) {
236 Ok(path) => {
237 let items: Vec<serde_json::Value> = path
238 .into_iter()
239 .map(|(nid, dist)| serde_json::json!({"node_id": nid.0, "distance": dist}))
240 .collect();
241 Response::ok(serde_json::json!({"path": items}))
242 }
243 Err(e) => Response::error(e.to_string()),
244 },
245
246 Request::VectorSearch { query, k } => {
247 match &self.vector_index {
248 Some(vi) => match vi.search(&query, k) {
249 Ok(results) => {
250 let items: Vec<serde_json::Value> = results
266 .into_iter()
267 .map(|r| {
268 serde_json::json!({
269 "node_id": r.node_id.0,
270 "distance": r.distance,
271 "score": r.distance,
272 })
273 })
274 .collect();
275 Response::ok(serde_json::json!({"results": items}))
276 }
277 Err(e) => Response::error(e.to_string()),
278 },
279 None => Response::error("vector index not configured"),
280 }
281 }
282
283 Request::Query { gql } => {
284 let stmt = match astraea_query::parse(&gql) {
286 Ok(s) => s,
287 Err(e) => return Response::error(format!("parse error: {e}")),
288 };
289
290 match self.executor.execute(stmt) {
292 Ok(result) => Response::ok(serde_json::json!({
293 "columns": result.columns,
294 "rows": result.rows,
295 "stats": {
296 "nodes_created": result.stats.nodes_created,
297 "edges_created": result.stats.edges_created,
298 "nodes_deleted": result.stats.nodes_deleted,
299 "edges_deleted": result.stats.edges_deleted,
300 },
301 })),
302 Err(e) => Response::error(format!("execution error: {e}")),
303 }
304 }
305
306 Request::ExtractSubgraph {
307 center,
308 hops,
309 max_nodes,
310 format,
311 } => {
312 let text_format = parse_text_format(&format);
313 match astraea_rag::extract_subgraph(&*self.graph, NodeId(center), hops, max_nodes) {
314 Ok(subgraph) => {
315 let text = astraea_rag::linearize_subgraph(&subgraph, text_format);
316 let tokens = astraea_rag::estimate_tokens(&text);
317 Response::ok(serde_json::json!({
318 "text": text,
319 "nodes_count": subgraph.nodes.len(),
320 "edges_count": subgraph.edges.len(),
321 "estimated_tokens": tokens,
322 }))
323 }
324 Err(e) => Response::error(e.to_string()),
325 }
326 }
327
328 Request::GraphRag {
329 question,
330 question_embedding,
331 anchor,
332 hops,
333 max_nodes,
334 format,
335 } => {
336 let text_format = parse_text_format(&format);
337
338 let center = if let Some(a) = anchor {
339 NodeId(a)
340 } else if let Some(emb) = &question_embedding {
341 match &self.vector_index {
342 Some(vi) => match vi.search(emb, 1) {
343 Ok(results) if !results.is_empty() => results[0].node_id,
344 Ok(_) => return Response::error("no matching nodes found"),
345 Err(e) => return Response::error(e.to_string()),
346 },
347 None => {
348 return Response::error(
349 "vector index not configured and no anchor provided",
350 );
351 }
352 }
353 } else {
354 return Response::error("either anchor or question_embedding must be provided");
355 };
356
357 match astraea_rag::extract_subgraph(&*self.graph, center, hops, max_nodes) {
358 Ok(subgraph) => {
359 let context = astraea_rag::linearize_subgraph(&subgraph, text_format);
360 let tokens = astraea_rag::estimate_tokens(&context);
361 Response::ok(serde_json::json!({
362 "anchor_node_id": center.0,
363 "context": context,
364 "question": question,
365 "nodes_in_context": subgraph.nodes.len(),
366 "edges_in_context": subgraph.edges.len(),
367 "estimated_tokens": tokens,
368 "note": "LLM completion requires server-side provider configuration. Use the context with your own LLM."
369 }))
370 }
371 Err(e) => Response::error(e.to_string()),
372 }
373 }
374
375 Request::NeighborsAt {
376 id,
377 direction,
378 timestamp,
379 edge_type,
380 } => {
381 let dir = match direction.as_str() {
382 "incoming" => Direction::Incoming,
383 "both" => Direction::Both,
384 _ => Direction::Outgoing,
385 };
386
387 match self.graph.neighbors_at(NodeId(id), dir, timestamp) {
388 Ok(neighbors) => {
389 let items: Vec<serde_json::Value> = if let Some(et) = edge_type {
390 neighbors
392 .into_iter()
393 .filter(|(eid, _)| {
394 self.graph
395 .get_edge(*eid)
396 .ok()
397 .flatten()
398 .is_some_and(|e| e.edge_type == et)
399 })
400 .map(|(eid, nid)| {
401 serde_json::json!({"edge_id": eid.0, "node_id": nid.0})
402 })
403 .collect()
404 } else {
405 neighbors
406 .into_iter()
407 .map(|(eid, nid)| {
408 serde_json::json!({"edge_id": eid.0, "node_id": nid.0})
409 })
410 .collect()
411 };
412 Response::ok(
413 serde_json::json!({"neighbors": items, "timestamp": timestamp}),
414 )
415 }
416 Err(e) => Response::error(e.to_string()),
417 }
418 }
419
420 Request::BfsAt {
421 start,
422 max_depth,
423 timestamp,
424 } => match self.graph.bfs_at(NodeId(start), max_depth, timestamp) {
425 Ok(nodes) => {
426 let items: Vec<serde_json::Value> = nodes
427 .into_iter()
428 .map(|(nid, depth)| serde_json::json!({"node_id": nid.0, "depth": depth}))
429 .collect();
430 Response::ok(serde_json::json!({"nodes": items, "timestamp": timestamp}))
431 }
432 Err(e) => Response::error(e.to_string()),
433 },
434
435 Request::ShortestPathAt {
436 from,
437 to,
438 timestamp,
439 weighted,
440 } => {
441 if weighted {
442 match self
443 .graph
444 .shortest_path_weighted_at(NodeId(from), NodeId(to), timestamp)
445 {
446 Ok(Some((path, cost))) => {
447 let node_ids: Vec<u64> =
448 path.nodes().into_iter().map(|n| n.0).collect();
449 Response::ok(serde_json::json!({
450 "path": node_ids, "cost": cost, "length": path.len(),
451 "timestamp": timestamp
452 }))
453 }
454 Ok(None) => {
455 Response::ok(serde_json::json!({"path": null, "timestamp": timestamp}))
456 }
457 Err(e) => Response::error(e.to_string()),
458 }
459 } else {
460 match self
461 .graph
462 .shortest_path_at(NodeId(from), NodeId(to), timestamp)
463 {
464 Ok(Some(path)) => {
465 let node_ids: Vec<u64> =
466 path.nodes().into_iter().map(|n| n.0).collect();
467 Response::ok(serde_json::json!({
468 "path": node_ids, "length": path.len(),
469 "timestamp": timestamp
470 }))
471 }
472 Ok(None) => {
473 Response::ok(serde_json::json!({"path": null, "timestamp": timestamp}))
474 }
475 Err(e) => Response::error(e.to_string()),
476 }
477 }
478 }
479
480 Request::Dfs { start, max_depth } => match self.graph.dfs(NodeId(start), max_depth) {
481 Ok(nodes) => {
482 let ids: Vec<u64> = nodes.into_iter().map(|n| n.0).collect();
483 Response::ok(serde_json::json!({"nodes": ids}))
484 }
485 Err(e) => Response::error(e.to_string()),
486 },
487
488 Request::DfsAt {
489 start,
490 max_depth,
491 timestamp,
492 } => {
493 match dfs_at_impl(&*self.graph, NodeId(start), max_depth, timestamp) {
495 Ok(nodes) => {
496 let ids: Vec<u64> = nodes.into_iter().map(|n| n.0).collect();
497 Response::ok(serde_json::json!({"nodes": ids, "timestamp": timestamp}))
498 }
499 Err(e) => Response::error(e.to_string()),
500 }
501 }
502
503 Request::FindByLabel { label } => match self.graph.find_by_label(&label) {
504 Ok(ids) => {
505 let node_ids: Vec<u64> = ids.into_iter().map(|n| n.0).collect();
506 Response::ok(serde_json::json!({"node_ids": node_ids}))
507 }
508 Err(e) => Response::error(e.to_string()),
509 },
510
511 Request::DeleteByLabel { label } => match self.graph.find_by_label(&label) {
516 Ok(ids) => {
517 let total = ids.len();
518 let mut deleted = 0u64;
519 for id in ids {
520 match self.graph.delete_node(id) {
521 Ok(()) => deleted += 1,
522 Err(e) => {
523 return Response::error(format!(
524 "DeleteByLabel '{label}': deleted {deleted}/{total} before error at {id}: {e}"
525 ));
526 }
527 }
528 }
529 Response::ok(serde_json::json!({ "deleted": deleted }))
530 }
531 Err(e) => Response::error(e.to_string()),
532 },
533
534 Request::FindEdgeByType { edge_type } => {
537 match self.graph.find_edges_by_type(&edge_type) {
538 Ok(triples) => {
539 let edges: Vec<serde_json::Value> = triples
540 .into_iter()
541 .map(|(eid, src, tgt)| {
542 serde_json::json!({
543 "edge_id": eid.0,
544 "source": src.0,
545 "target": tgt.0,
546 })
547 })
548 .collect();
549 Response::ok(serde_json::json!({ "edges": edges }))
550 }
551 Err(e) => Response::error(e.to_string()),
552 }
553 }
554
555 Request::RunPageRank {
556 nodes,
557 damping,
558 max_iterations,
559 tolerance,
560 } => {
561 let node_ids = match resolve_node_ids(&*self.graph, nodes) {
562 Ok(ids) => ids,
563 Err(e) => return Response::error(e.to_string()),
564 };
565
566 let config = astraea_algorithms::pagerank::PageRankConfig {
567 damping,
568 max_iterations,
569 tolerance,
570 };
571
572 match astraea_algorithms::pagerank::pagerank(&*self.graph, &node_ids, &config) {
573 Ok(scores) => {
574 let map: HashMap<String, f64> = scores
575 .into_iter()
576 .map(|(k, v)| (k.0.to_string(), v))
577 .collect();
578 Response::ok(serde_json::json!({"scores": map}))
579 }
580 Err(e) => Response::error(e.to_string()),
581 }
582 }
583
584 Request::RunLouvain { nodes } => {
585 let node_ids = match resolve_node_ids(&*self.graph, nodes) {
586 Ok(ids) => ids,
587 Err(e) => return Response::error(e.to_string()),
588 };
589
590 match astraea_algorithms::community::louvain(&*self.graph, &node_ids) {
591 Ok(communities) => {
592 let map: HashMap<String, usize> = communities
593 .into_iter()
594 .map(|(k, v)| (k.0.to_string(), v))
595 .collect();
596 let num_communities = map.values().collect::<HashSet<_>>().len();
597 Response::ok(serde_json::json!({
598 "communities": map,
599 "num_communities": num_communities,
600 }))
601 }
602 Err(e) => Response::error(e.to_string()),
603 }
604 }
605
606 Request::RunConnectedComponents { nodes, strong } => {
607 let node_ids = match resolve_node_ids(&*self.graph, nodes) {
608 Ok(ids) => ids,
609 Err(e) => return Response::error(e.to_string()),
610 };
611
612 let result = if strong {
613 astraea_algorithms::components::strongly_connected_components(
614 &*self.graph,
615 &node_ids,
616 )
617 } else {
618 astraea_algorithms::components::connected_components(&*self.graph, &node_ids)
619 };
620
621 match result {
622 Ok(components) => {
623 let count = components.len();
624 let comps: Vec<Vec<u64>> = components
625 .into_iter()
626 .map(|c| c.into_iter().map(|n| n.0).collect())
627 .collect();
628 Response::ok(serde_json::json!({
629 "components": comps,
630 "count": count,
631 }))
632 }
633 Err(e) => Response::error(e.to_string()),
634 }
635 }
636
637 Request::RunDegreeCentrality { nodes, direction } => {
638 let node_ids = match resolve_node_ids(&*self.graph, nodes) {
639 Ok(ids) => ids,
640 Err(e) => return Response::error(e.to_string()),
641 };
642
643 let dir = parse_direction(&direction);
644
645 match astraea_algorithms::centrality::degree_centrality(
646 &*self.graph,
647 &node_ids,
648 dir,
649 ) {
650 Ok(scores) => {
651 let map: HashMap<String, f64> = scores
652 .into_iter()
653 .map(|(k, v)| (k.0.to_string(), v))
654 .collect();
655 Response::ok(serde_json::json!({"scores": map}))
656 }
657 Err(e) => Response::error(e.to_string()),
658 }
659 }
660
661 Request::RunBetweennessCentrality { nodes } => {
662 let node_ids = match resolve_node_ids(&*self.graph, nodes) {
663 Ok(ids) => ids,
664 Err(e) => return Response::error(e.to_string()),
665 };
666
667 match astraea_algorithms::centrality::betweenness_centrality(
668 &*self.graph,
669 &node_ids,
670 ) {
671 Ok(scores) => {
672 let map: HashMap<String, f64> = scores
673 .into_iter()
674 .map(|(k, v)| (k.0.to_string(), v))
675 .collect();
676 Response::ok(serde_json::json!({"scores": map}))
677 }
678 Err(e) => Response::error(e.to_string()),
679 }
680 }
681
682 Request::GraphStats => {
683 match collect_graph_stats(&*self.graph, self.vector_index.as_deref()) {
684 Ok(stats) => Response::ok(stats),
685 Err(e) => Response::error(e.to_string()),
686 }
687 }
688
689 Request::GetSubgraph {
690 center,
691 hops,
692 max_nodes,
693 } => {
694 match astraea_rag::extract_subgraph(&*self.graph, NodeId(center), hops, max_nodes) {
695 Ok(subgraph) => {
696 let nodes: Vec<serde_json::Value> = subgraph
697 .nodes
698 .iter()
699 .map(|n| {
700 serde_json::json!({
701 "id": n.id.0,
702 "labels": n.labels,
703 "properties": n.properties,
704 "has_embedding": n.embedding.is_some(),
705 })
706 })
707 .collect();
708 let edges: Vec<serde_json::Value> = subgraph
709 .edges
710 .iter()
711 .map(|e| {
712 serde_json::json!({
713 "id": e.id.0,
714 "source": e.source.0,
715 "target": e.target.0,
716 "edge_type": e.edge_type,
717 "properties": e.properties,
718 "weight": e.weight,
719 "valid_from": e.validity.valid_from,
720 "valid_to": e.validity.valid_to,
721 })
722 })
723 .collect();
724 Response::ok(serde_json::json!({
725 "nodes": nodes,
726 "edges": edges,
727 }))
728 }
729 Err(e) => Response::error(e.to_string()),
730 }
731 }
732
733 Request::Ping => {
734 let mut payload = serde_json::json!({
739 "pong": true,
740 "version": env!("CARGO_PKG_VERSION"),
741 });
742 if let Some(vi) = self.vector_index.as_deref() {
743 payload["vector_dim"] = serde_json::json!(vi.dimension());
744 payload["vector_metric"] = serde_json::json!(format!("{:?}", vi.metric()));
745 }
746 Response::ok(payload)
747 }
748 }
749 }
750}
751
752fn parse_text_format(s: &str) -> astraea_rag::TextFormat {
754 match s.to_lowercase().as_str() {
755 "prose" => astraea_rag::TextFormat::Prose,
756 "triples" => astraea_rag::TextFormat::Triples,
757 "json" => astraea_rag::TextFormat::Json,
758 _ => astraea_rag::TextFormat::Structured,
759 }
760}
761
762fn parse_direction(s: &str) -> Direction {
764 match s {
765 "incoming" => Direction::Incoming,
766 "both" => Direction::Both,
767 _ => Direction::Outgoing,
768 }
769}
770
771fn resolve_node_ids(
776 graph: &dyn GraphOps,
777 nodes: Option<Vec<u64>>,
778) -> astraea_core::error::Result<Vec<NodeId>> {
779 if let Some(ids) = nodes {
780 Ok(ids.into_iter().map(NodeId).collect())
781 } else {
782 let mut found = Vec::new();
785 for id in 1..10_000u64 {
786 if graph.get_node(NodeId(id))?.is_some() {
787 found.push(NodeId(id));
788 }
789 }
790 Ok(found)
791 }
792}
793
794fn collect_graph_stats(
796 graph: &dyn GraphOps,
797 vector_index: Option<&dyn VectorIndex>,
798) -> astraea_core::error::Result<serde_json::Value> {
799 let mut total_nodes: u64 = 0;
800 let mut total_edges: u64 = 0;
801 let mut label_counts: HashMap<String, u64> = HashMap::new();
802 let mut edge_type_counts: HashMap<String, u64> = HashMap::new();
803
804 for id in 1..10_000u64 {
806 if let Some(node) = graph.get_node(NodeId(id))? {
807 total_nodes += 1;
808 for label in &node.labels {
809 *label_counts.entry(label.clone()).or_insert(0) += 1;
810 }
811 }
812 }
813
814 for id in 1..10_000u64 {
816 if let Some(edge) = graph.get_edge(EdgeId(id))? {
817 total_edges += 1;
818 *edge_type_counts.entry(edge.edge_type.clone()).or_insert(0) += 1;
819 }
820 }
821
822 let mut stats = serde_json::json!({
823 "total_nodes": total_nodes,
824 "total_edges": total_edges,
825 "labels": label_counts,
826 "edge_types": edge_type_counts,
827 });
828
829 if let Some(vi) = vector_index {
830 stats["vector_index"] = serde_json::json!({
831 "dimension": vi.dimension(),
832 "metric": format!("{:?}", vi.metric()),
833 "size": vi.len(),
834 });
835 }
836
837 Ok(stats)
838}
839
840fn dfs_at_impl(
844 graph: &dyn GraphOps,
845 start: NodeId,
846 max_depth: usize,
847 timestamp: i64,
848) -> astraea_core::error::Result<Vec<NodeId>> {
849 let mut visited = HashSet::new();
850 let mut result = Vec::new();
851 let mut stack: Vec<(NodeId, usize)> = vec![(start, 0)];
852
853 while let Some((node, depth)) = stack.pop() {
854 if !visited.insert(node) {
855 continue;
856 }
857 result.push(node);
858
859 if depth < max_depth {
860 let neighbors = graph.neighbors_at(node, Direction::Outgoing, timestamp)?;
861 for (_eid, nid) in neighbors {
862 if !visited.contains(&nid) {
863 stack.push((nid, depth + 1));
864 }
865 }
866 }
867 }
868
869 Ok(result)
870}
871
872#[cfg(test)]
873mod tests {
874 use super::*;
875 use astraea_graph::Graph;
876 use astraea_graph::test_utils::InMemoryStorage;
877 use astraea_vector::HnswVectorIndex;
878
879 fn handler_with_vector_index(dim: usize) -> (RequestHandler, Arc<dyn VectorIndex>) {
881 let storage = InMemoryStorage::new();
882 let vector_index: Arc<dyn VectorIndex> =
883 Arc::new(HnswVectorIndex::new(dim, DistanceMetric::Cosine));
884 let graph = Graph::with_vector_index(Box::new(storage), Arc::clone(&vector_index));
885 let handler = RequestHandler::new(Arc::new(graph), Some(Arc::clone(&vector_index)));
886 (handler, vector_index)
887 }
888
889 fn handler_without_vector_index() -> RequestHandler {
891 let storage = InMemoryStorage::new();
892 let graph = Graph::new(Box::new(storage));
893 RequestHandler::new(Arc::new(graph), None)
894 }
895
896 #[test]
897 fn test_vector_search_basic() {
898 let (handler, vi) = handler_with_vector_index(3);
899
900 vi.insert(NodeId(100), &[1.0, 0.0, 0.0]).unwrap();
902 vi.insert(NodeId(200), &[0.0, 1.0, 0.0]).unwrap();
903 vi.insert(NodeId(300), &[0.0, 0.0, 1.0]).unwrap();
904
905 let resp = handler.handle(Request::VectorSearch {
906 query: vec![1.0, 0.0, 0.0],
907 k: 2,
908 });
909
910 match resp {
911 Response::Ok { data } => {
912 let results = data.get("results").unwrap().as_array().unwrap();
913 assert_eq!(results.len(), 2);
914 assert_eq!(results[0].get("node_id").unwrap().as_u64().unwrap(), 100);
916 }
917 Response::Error { message } => panic!("expected Ok, got Error: {}", message),
918 }
919 }
920
921 #[test]
922 fn test_vector_search_no_index() {
923 let handler = handler_without_vector_index();
924
925 let resp = handler.handle(Request::VectorSearch {
926 query: vec![1.0, 0.0, 0.0],
927 k: 5,
928 });
929
930 match resp {
931 Response::Error { message } => {
932 assert_eq!(message, "vector index not configured");
933 }
934 Response::Ok { .. } => panic!("expected Error, got Ok"),
935 }
936 }
937
938 #[test]
939 fn test_auto_index_on_create_node() {
940 let (handler, _vi) = handler_with_vector_index(3);
941
942 let create_resp = handler.handle(Request::CreateNode {
944 labels: vec!["TestNode".into()],
945 properties: serde_json::json!({"name": "alpha"}),
946 embedding: Some(vec![0.5, 0.5, 0.0]),
947 });
948 match &create_resp {
949 Response::Ok { data } => {
950 assert!(data.get("node_id").is_some());
951 }
952 Response::Error { message } => panic!("create failed: {}", message),
953 }
954
955 handler.handle(Request::CreateNode {
957 labels: vec!["TestNode".into()],
958 properties: serde_json::json!({"name": "beta"}),
959 embedding: Some(vec![0.0, 0.0, 1.0]),
960 });
961
962 let search_resp = handler.handle(Request::VectorSearch {
964 query: vec![0.5, 0.5, 0.0],
965 k: 1,
966 });
967
968 match search_resp {
969 Response::Ok { data } => {
970 let results = data.get("results").unwrap().as_array().unwrap();
971 assert_eq!(results.len(), 1);
972 assert_eq!(results[0].get("node_id").unwrap().as_u64().unwrap(), 1);
974 }
975 Response::Error { message } => panic!("search failed: {}", message),
976 }
977 }
978
979 fn handler_with_semantic_graph() -> RequestHandler {
982 let storage = InMemoryStorage::new();
983 let vector_index: Arc<dyn VectorIndex> =
984 Arc::new(HnswVectorIndex::new(3, DistanceMetric::Euclidean));
985 let graph = Graph::with_vector_index(Box::new(storage), Arc::clone(&vector_index));
986 let handler = RequestHandler::new(Arc::new(graph), Some(Arc::clone(&vector_index)));
987
988 handler.handle(Request::CreateNode {
990 labels: vec!["Thing".into()],
991 properties: serde_json::json!({"name": "A"}),
992 embedding: Some(vec![1.0, 0.0, 0.0]),
993 }); handler.handle(Request::CreateNode {
995 labels: vec!["Thing".into()],
996 properties: serde_json::json!({"name": "closeA"}),
997 embedding: Some(vec![0.9, 0.1, 0.0]),
998 }); handler.handle(Request::CreateNode {
1000 labels: vec!["Thing".into()],
1001 properties: serde_json::json!({"name": "C"}),
1002 embedding: Some(vec![0.0, 0.0, 1.0]),
1003 }); handler.handle(Request::CreateEdge {
1007 source: 1,
1008 target: 2,
1009 edge_type: "LINK".into(),
1010 properties: serde_json::json!({}),
1011 weight: 1.0,
1012 valid_from: None,
1013 valid_to: None,
1014 });
1015 handler.handle(Request::CreateEdge {
1016 source: 2,
1017 target: 3,
1018 edge_type: "LINK".into(),
1019 properties: serde_json::json!({}),
1020 weight: 1.0,
1021 valid_from: None,
1022 valid_to: None,
1023 });
1024
1025 handler
1026 }
1027
1028 #[test]
1029 fn test_hybrid_search_handler() {
1030 let handler = handler_with_semantic_graph();
1031
1032 let resp = handler.handle(Request::HybridSearch {
1033 anchor: 1,
1034 query: vec![1.0, 0.0, 0.0],
1035 max_hops: 2,
1036 k: 10,
1037 alpha: 0.5,
1038 });
1039
1040 match resp {
1041 Response::Ok { data } => {
1042 let results = data.get("results").unwrap().as_array().unwrap();
1043 assert!(!results.is_empty());
1044 assert_eq!(results[0].get("node_id").unwrap().as_u64().unwrap(), 2);
1046 assert!(results[0].get("score").is_some());
1048 }
1049 Response::Error { message } => panic!("expected Ok, got Error: {message}"),
1050 }
1051 }
1052
1053 #[test]
1054 fn test_semantic_neighbors_handler() {
1055 let handler = handler_with_semantic_graph();
1056
1057 let resp = handler.handle(Request::SemanticNeighbors {
1058 id: 1,
1059 concept: vec![1.0, 0.0, 0.0],
1060 direction: "outgoing".into(),
1061 k: 10,
1062 });
1063
1064 match resp {
1065 Response::Ok { data } => {
1066 let results = data.get("results").unwrap().as_array().unwrap();
1067 assert_eq!(results.len(), 1); assert_eq!(results[0].get("node_id").unwrap().as_u64().unwrap(), 2);
1069 assert!(results[0].get("distance").is_some());
1070 }
1071 Response::Error { message } => panic!("expected Ok, got Error: {message}"),
1072 }
1073 }
1074
1075 #[test]
1076 fn test_semantic_walk_handler() {
1077 let handler = handler_with_semantic_graph();
1078
1079 let resp = handler.handle(Request::SemanticWalk {
1080 start: 1,
1081 concept: vec![0.0, 0.0, 1.0],
1082 max_hops: 5,
1083 });
1084
1085 match resp {
1086 Response::Ok { data } => {
1087 let path = data.get("path").unwrap().as_array().unwrap();
1088 assert!(path.len() >= 2);
1090 assert_eq!(path[0].get("node_id").unwrap().as_u64().unwrap(), 1);
1091 assert_eq!(
1093 path.last()
1094 .unwrap()
1095 .get("node_id")
1096 .unwrap()
1097 .as_u64()
1098 .unwrap(),
1099 3
1100 );
1101 assert!(path.last().unwrap().get("distance").is_some());
1102 }
1103 Response::Error { message } => panic!("expected Ok, got Error: {message}"),
1104 }
1105 }
1106
1107 fn handler_with_rag_graph() -> RequestHandler {
1112 let storage = InMemoryStorage::new();
1113 let vector_index: Arc<dyn VectorIndex> =
1114 Arc::new(HnswVectorIndex::new(3, DistanceMetric::Euclidean));
1115 let graph = Graph::with_vector_index(Box::new(storage), Arc::clone(&vector_index));
1116 let handler = RequestHandler::new(Arc::new(graph), Some(Arc::clone(&vector_index)));
1117
1118 handler.handle(Request::CreateNode {
1120 labels: vec!["Person".into()],
1121 properties: serde_json::json!({"name": "Alice"}),
1122 embedding: Some(vec![1.0, 0.0, 0.0]),
1123 }); handler.handle(Request::CreateNode {
1125 labels: vec!["Person".into()],
1126 properties: serde_json::json!({"name": "Bob"}),
1127 embedding: Some(vec![0.0, 1.0, 0.0]),
1128 }); handler.handle(Request::CreateNode {
1130 labels: vec!["Company".into()],
1131 properties: serde_json::json!({"name": "Acme"}),
1132 embedding: Some(vec![0.0, 0.0, 1.0]),
1133 }); handler.handle(Request::CreateEdge {
1136 source: 1,
1137 target: 2,
1138 edge_type: "KNOWS".into(),
1139 properties: serde_json::json!({"since": 2020}),
1140 weight: 1.0,
1141 valid_from: None,
1142 valid_to: None,
1143 });
1144 handler.handle(Request::CreateEdge {
1145 source: 2,
1146 target: 3,
1147 edge_type: "WORKS_AT".into(),
1148 properties: serde_json::json!({}),
1149 weight: 1.0,
1150 valid_from: None,
1151 valid_to: None,
1152 });
1153
1154 handler
1155 }
1156
1157 #[test]
1158 fn test_extract_subgraph_handler() {
1159 let handler = handler_with_rag_graph();
1160
1161 let resp = handler.handle(Request::ExtractSubgraph {
1162 center: 1,
1163 hops: 2,
1164 max_nodes: 50,
1165 format: "structured".into(),
1166 });
1167
1168 match resp {
1169 Response::Ok { data } => {
1170 assert!(
1171 data.get("text")
1172 .unwrap()
1173 .as_str()
1174 .unwrap()
1175 .contains("Alice")
1176 );
1177 let nodes_count = data.get("nodes_count").unwrap().as_u64().unwrap();
1178 assert!(
1179 nodes_count >= 2,
1180 "expected at least 2 nodes, got {nodes_count}"
1181 );
1182 let edges_count = data.get("edges_count").unwrap().as_u64().unwrap();
1183 assert!(
1184 edges_count >= 1,
1185 "expected at least 1 edge, got {edges_count}"
1186 );
1187 assert!(data.get("estimated_tokens").unwrap().as_u64().unwrap() > 0);
1188 }
1189 Response::Error { message } => panic!("expected Ok, got Error: {message}"),
1190 }
1191 }
1192
1193 #[test]
1194 fn test_graph_rag_handler_with_anchor() {
1195 let handler = handler_with_rag_graph();
1196
1197 let resp = handler.handle(Request::GraphRag {
1198 question: "Who does Alice know?".into(),
1199 question_embedding: None,
1200 anchor: Some(1),
1201 hops: 2,
1202 max_nodes: 50,
1203 format: "structured".into(),
1204 });
1205
1206 match resp {
1207 Response::Ok { data } => {
1208 assert_eq!(data.get("anchor_node_id").unwrap().as_u64().unwrap(), 1);
1209 let context = data.get("context").unwrap().as_str().unwrap();
1210 assert!(context.contains("Alice"));
1211 assert_eq!(
1212 data.get("question").unwrap().as_str().unwrap(),
1213 "Who does Alice know?"
1214 );
1215 assert!(data.get("nodes_in_context").unwrap().as_u64().unwrap() > 0);
1216 assert!(data.get("edges_in_context").unwrap().as_u64().unwrap() > 0);
1217 assert!(data.get("estimated_tokens").unwrap().as_u64().unwrap() > 0);
1218 assert!(data.get("note").is_some());
1219 }
1220 Response::Error { message } => panic!("expected Ok, got Error: {message}"),
1221 }
1222 }
1223
1224 #[test]
1225 fn test_graph_rag_handler_with_embedding() {
1226 let handler = handler_with_rag_graph();
1227
1228 let resp = handler.handle(Request::GraphRag {
1230 question: "Tell me about Alice".into(),
1231 question_embedding: Some(vec![1.0, 0.0, 0.0]),
1232 anchor: None,
1233 hops: 2,
1234 max_nodes: 50,
1235 format: "structured".into(),
1236 });
1237
1238 match resp {
1239 Response::Ok { data } => {
1240 assert_eq!(data.get("anchor_node_id").unwrap().as_u64().unwrap(), 1);
1242 let context = data.get("context").unwrap().as_str().unwrap();
1243 assert!(context.contains("Alice"));
1244 }
1245 Response::Error { message } => panic!("expected Ok, got Error: {message}"),
1246 }
1247 }
1248
1249 #[test]
1250 fn test_graph_rag_handler_no_anchor_no_embedding() {
1251 let handler = handler_with_rag_graph();
1252
1253 let resp = handler.handle(Request::GraphRag {
1254 question: "Some question".into(),
1255 question_embedding: None,
1256 anchor: None,
1257 hops: 2,
1258 max_nodes: 50,
1259 format: "structured".into(),
1260 });
1261
1262 match resp {
1263 Response::Error { message } => {
1264 assert_eq!(
1265 message,
1266 "either anchor or question_embedding must be provided"
1267 );
1268 }
1269 Response::Ok { .. } => panic!("expected Error, got Ok"),
1270 }
1271 }
1272
1273 #[test]
1274 fn test_auto_remove_on_delete_node() {
1275 let (handler, _vi) = handler_with_vector_index(3);
1276
1277 let create_resp = handler.handle(Request::CreateNode {
1279 labels: vec!["Temp".into()],
1280 properties: serde_json::json!({}),
1281 embedding: Some(vec![1.0, 0.0, 0.0]),
1282 });
1283 let node_id = match &create_resp {
1284 Response::Ok { data } => data.get("node_id").unwrap().as_u64().unwrap(),
1285 Response::Error { message } => panic!("create failed: {}", message),
1286 };
1287
1288 let search_resp = handler.handle(Request::VectorSearch {
1290 query: vec![1.0, 0.0, 0.0],
1291 k: 1,
1292 });
1293 match &search_resp {
1294 Response::Ok { data } => {
1295 let results = data.get("results").unwrap().as_array().unwrap();
1296 assert_eq!(results.len(), 1);
1297 assert_eq!(
1298 results[0].get("node_id").unwrap().as_u64().unwrap(),
1299 node_id
1300 );
1301 }
1302 Response::Error { message } => panic!("search failed before delete: {}", message),
1303 }
1304
1305 let del_resp = handler.handle(Request::DeleteNode { id: node_id });
1307 match &del_resp {
1308 Response::Ok { .. } => {}
1309 Response::Error { message } => panic!("delete failed: {}", message),
1310 }
1311
1312 let search_resp2 = handler.handle(Request::VectorSearch {
1314 query: vec![1.0, 0.0, 0.0],
1315 k: 10,
1316 });
1317 match search_resp2 {
1318 Response::Ok { data } => {
1319 let results = data.get("results").unwrap().as_array().unwrap();
1320 assert!(
1322 results.is_empty(),
1323 "expected no results after delete, got: {:?}",
1324 results
1325 );
1326 }
1327 Response::Error { message } => panic!("search failed after delete: {}", message),
1328 }
1329 }
1330
1331 #[test]
1332 fn test_delete_by_label_removes_matches() {
1333 let (handler, _vi) = handler_with_vector_index(2);
1335
1336 for _ in 0..3 {
1338 match handler.handle(Request::CreateNode {
1339 labels: vec!["Widget".into()],
1340 properties: serde_json::json!({}),
1341 embedding: None,
1342 }) {
1343 Response::Ok { .. } => {}
1344 Response::Error { message } => panic!("create failed: {message}"),
1345 }
1346 }
1347 match handler.handle(Request::CreateNode {
1348 labels: vec!["Gadget".into()],
1349 properties: serde_json::json!({}),
1350 embedding: None,
1351 }) {
1352 Response::Ok { .. } => {}
1353 Response::Error { message } => panic!("create failed: {message}"),
1354 }
1355
1356 let resp = handler.handle(Request::DeleteByLabel {
1358 label: "Widget".into(),
1359 });
1360 match resp {
1361 Response::Ok { data } => {
1362 assert_eq!(data.get("deleted").unwrap().as_u64().unwrap(), 3);
1363 }
1364 Response::Error { message } => panic!("DeleteByLabel failed: {message}"),
1365 }
1366
1367 match handler.handle(Request::FindByLabel {
1369 label: "Widget".into(),
1370 }) {
1371 Response::Ok { data } => {
1372 let ids = data.get("node_ids").unwrap().as_array().unwrap();
1373 assert!(ids.is_empty(), "expected no Widgets, got {:?}", ids);
1374 }
1375 Response::Error { message } => panic!("FindByLabel failed: {message}"),
1376 }
1377 match handler.handle(Request::FindByLabel {
1378 label: "Gadget".into(),
1379 }) {
1380 Response::Ok { data } => {
1381 let ids = data.get("node_ids").unwrap().as_array().unwrap();
1382 assert_eq!(ids.len(), 1, "Gadget should still be present");
1383 }
1384 Response::Error { message } => panic!("FindByLabel failed: {message}"),
1385 }
1386
1387 match handler.handle(Request::DeleteByLabel {
1389 label: "Nonexistent".into(),
1390 }) {
1391 Response::Ok { data } => {
1392 assert_eq!(data.get("deleted").unwrap().as_u64().unwrap(), 0);
1393 }
1394 Response::Error { message } => panic!("unexpected error: {message}"),
1395 }
1396 }
1397
1398 #[test]
1399 fn test_find_edges_by_type() {
1400 let (handler, _vi) = handler_with_vector_index(2);
1402
1403 let mut node_ids = Vec::new();
1405 for _ in 0..2 {
1406 match handler.handle(Request::CreateNode {
1407 labels: vec![],
1408 properties: serde_json::json!({}),
1409 embedding: None,
1410 }) {
1411 Response::Ok { data } => {
1412 node_ids.push(data.get("node_id").unwrap().as_u64().unwrap());
1413 }
1414 Response::Error { message } => panic!("create node failed: {message}"),
1415 }
1416 }
1417 let (src, tgt) = (node_ids[0], node_ids[1]);
1418
1419 let mut t1_ids: Vec<u64> = Vec::new();
1421 for _ in 0..3 {
1422 match handler.handle(Request::CreateEdge {
1423 source: src,
1424 target: tgt,
1425 edge_type: "T1".into(),
1426 properties: serde_json::json!({}),
1427 weight: 1.0,
1428 valid_from: None,
1429 valid_to: None,
1430 }) {
1431 Response::Ok { data } => {
1432 t1_ids.push(data.get("edge_id").unwrap().as_u64().unwrap());
1433 }
1434 Response::Error { message } => panic!("create edge T1 failed: {message}"),
1435 }
1436 }
1437 for _ in 0..2 {
1438 match handler.handle(Request::CreateEdge {
1439 source: src,
1440 target: tgt,
1441 edge_type: "T2".into(),
1442 properties: serde_json::json!({}),
1443 weight: 1.0,
1444 valid_from: None,
1445 valid_to: None,
1446 }) {
1447 Response::Ok { .. } => {}
1448 Response::Error { message } => panic!("create edge T2 failed: {message}"),
1449 }
1450 }
1451
1452 let resp = handler.handle(Request::FindEdgeByType {
1455 edge_type: "T1".into(),
1456 });
1457 match resp {
1458 Response::Ok { data } => {
1459 let edges = data.get("edges").unwrap().as_array().unwrap();
1460 assert_eq!(edges.len(), 3, "expected 3 T1 edges, got {}", edges.len());
1461 for e in edges {
1463 assert_eq!(
1464 e.get("source").unwrap().as_u64().unwrap(),
1465 src,
1466 "wrong source"
1467 );
1468 assert_eq!(
1469 e.get("target").unwrap().as_u64().unwrap(),
1470 tgt,
1471 "wrong target"
1472 );
1473 }
1474 let mut returned_ids: Vec<u64> = edges
1476 .iter()
1477 .map(|e| e.get("edge_id").unwrap().as_u64().unwrap())
1478 .collect();
1479 returned_ids.sort_unstable();
1480 let mut expected = t1_ids.clone();
1481 expected.sort_unstable();
1482 assert_eq!(returned_ids, expected, "T1 edge_ids mismatch");
1483 }
1484 Response::Error { message } => panic!("FindEdgeByType T1 failed: {message}"),
1485 }
1486
1487 match handler.handle(Request::FindEdgeByType {
1489 edge_type: "nonexistent".into(),
1490 }) {
1491 Response::Ok { data } => {
1492 let edges = data.get("edges").unwrap().as_array().unwrap();
1493 assert!(
1494 edges.is_empty(),
1495 "expected empty list for unknown type, got {:?}",
1496 edges
1497 );
1498 }
1499 Response::Error { message } => {
1500 panic!("FindEdgeByType nonexistent returned error: {message}")
1501 }
1502 }
1503 }
1504}