360 lines
13 KiB
Go
360 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/apache/arrow/go/v17/arrow"
|
|
"github.com/apache/arrow/go/v17/arrow/array"
|
|
"github.com/apache/arrow/go/v17/arrow/ipc"
|
|
"github.com/apache/arrow/go/v17/arrow/memory"
|
|
)
|
|
|
|
const graphTransportVersion = "1"
|
|
|
|
const (
|
|
graphArrowMetaBackendField = iota
|
|
graphArrowMetaGraphQueryIDField
|
|
graphArrowMetaNodeLimitField
|
|
graphArrowMetaEdgeLimitField
|
|
graphArrowMetaNodesField
|
|
graphArrowMetaEdgesField
|
|
graphArrowMetaRouteLineSegmentsField
|
|
graphArrowNodeIDField
|
|
graphArrowNodeXField
|
|
graphArrowNodeYField
|
|
graphArrowNodeIRIField
|
|
graphArrowNodeLabelField
|
|
graphArrowEdgeSourceField
|
|
graphArrowEdgeTargetField
|
|
graphArrowRouteX1Field
|
|
graphArrowRouteY1Field
|
|
graphArrowRouteX2Field
|
|
graphArrowRouteY2Field
|
|
)
|
|
|
|
var graphArrowSchema = arrow.NewSchema([]arrow.Field{
|
|
{Name: "meta_backend", Type: arrow.BinaryTypes.String, Nullable: true},
|
|
{Name: "meta_graph_query_id", Type: arrow.BinaryTypes.String, Nullable: true},
|
|
{Name: "meta_node_limit", Type: arrow.PrimitiveTypes.Int32, Nullable: true},
|
|
{Name: "meta_edge_limit", Type: arrow.PrimitiveTypes.Int32, Nullable: true},
|
|
{Name: "meta_nodes", Type: arrow.PrimitiveTypes.Int32, Nullable: true},
|
|
{Name: "meta_edges", Type: arrow.PrimitiveTypes.Int32, Nullable: true},
|
|
{Name: "meta_route_line_segments", Type: arrow.PrimitiveTypes.Int32, Nullable: true},
|
|
{Name: "node_id", Type: arrow.ListOf(arrow.PrimitiveTypes.Uint32), Nullable: true},
|
|
{Name: "node_x", Type: arrow.ListOf(arrow.PrimitiveTypes.Float32), Nullable: true},
|
|
{Name: "node_y", Type: arrow.ListOf(arrow.PrimitiveTypes.Float32), Nullable: true},
|
|
{Name: "node_iri", Type: arrow.ListOf(arrow.BinaryTypes.String), Nullable: true},
|
|
{Name: "node_label", Type: arrow.ListOf(arrow.BinaryTypes.String), Nullable: true},
|
|
{Name: "edge_source", Type: arrow.ListOf(arrow.PrimitiveTypes.Uint32), Nullable: true},
|
|
{Name: "edge_target", Type: arrow.ListOf(arrow.PrimitiveTypes.Uint32), Nullable: true},
|
|
{Name: "route_x1", Type: arrow.ListOf(arrow.PrimitiveTypes.Float32), Nullable: true},
|
|
{Name: "route_y1", Type: arrow.ListOf(arrow.PrimitiveTypes.Float32), Nullable: true},
|
|
{Name: "route_x2", Type: arrow.ListOf(arrow.PrimitiveTypes.Float32), Nullable: true},
|
|
{Name: "route_y2", Type: arrow.ListOf(arrow.PrimitiveTypes.Float32), Nullable: true},
|
|
}, nil)
|
|
|
|
type graphArrowRouteLines struct {
|
|
X1 []float32
|
|
Y1 []float32
|
|
X2 []float32
|
|
Y2 []float32
|
|
}
|
|
|
|
func writeGraphArrow(w http.ResponseWriter, snap GraphResponse) error {
|
|
start := time.Now()
|
|
routes := flattenGraphRouteLines(snap.RouteSegments)
|
|
meta := snap.Meta
|
|
if meta == nil {
|
|
meta = &GraphMeta{
|
|
GraphQueryID: "default",
|
|
NodeLimit: len(snap.Nodes),
|
|
EdgeLimit: len(snap.Edges),
|
|
Nodes: len(snap.Nodes),
|
|
Edges: len(snap.Edges),
|
|
}
|
|
}
|
|
|
|
alloc := memory.NewGoAllocator()
|
|
w.Header().Set("Content-Type", "application/vnd.apache.arrow.stream")
|
|
w.Header().Set("X-Graph-Transport-Version", graphTransportVersion)
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
writer := ipc.NewWriter(w, ipc.WithSchema(graphArrowSchema))
|
|
closed := false
|
|
defer func() {
|
|
if !closed {
|
|
_ = writer.Close()
|
|
}
|
|
}()
|
|
|
|
if err := writeGraphArrowMetaBatch(writer, alloc, meta, len(routes.X1)); err != nil {
|
|
return fmt.Errorf("write meta batch failed: %w", err)
|
|
}
|
|
if err := writeGraphArrowNodeBatch(writer, alloc, snap.Nodes); err != nil {
|
|
return fmt.Errorf("write node batch failed: %w", err)
|
|
}
|
|
if err := writeGraphArrowEdgeBatch(writer, alloc, snap.Edges); err != nil {
|
|
return fmt.Errorf("write edge batch failed: %w", err)
|
|
}
|
|
if err := writeGraphArrowRouteBatch(writer, alloc, routes); err != nil {
|
|
return fmt.Errorf("write route batch failed: %w", err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
return fmt.Errorf("close arrow writer failed: %w", err)
|
|
}
|
|
closed = true
|
|
|
|
log.Printf(
|
|
"[graph-arrow] encode_done graph_query_id=%s nodes=%d edges=%d route_line_segments=%d encode_time=%s",
|
|
meta.GraphQueryID,
|
|
len(snap.Nodes),
|
|
len(snap.Edges),
|
|
len(routes.X1),
|
|
time.Since(start).Truncate(time.Millisecond),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
func writeGraphArrowMetaBatch(writer *ipc.Writer, alloc memory.Allocator, meta *GraphMeta, routeLineSegments int) error {
|
|
builder := array.NewRecordBuilder(alloc, graphArrowSchema)
|
|
defer builder.Release()
|
|
|
|
appendStringBuilderValue(builder.Field(graphArrowMetaBackendField), meta.Backend)
|
|
appendStringBuilderValue(builder.Field(graphArrowMetaGraphQueryIDField), meta.GraphQueryID)
|
|
appendInt32BuilderValue(builder.Field(graphArrowMetaNodeLimitField), int32(meta.NodeLimit))
|
|
appendInt32BuilderValue(builder.Field(graphArrowMetaEdgeLimitField), int32(meta.EdgeLimit))
|
|
appendInt32BuilderValue(builder.Field(graphArrowMetaNodesField), int32(meta.Nodes))
|
|
appendInt32BuilderValue(builder.Field(graphArrowMetaEdgesField), int32(meta.Edges))
|
|
appendInt32BuilderValue(builder.Field(graphArrowMetaRouteLineSegmentsField), int32(routeLineSegments))
|
|
|
|
appendNullTopLevel(builder.Field(graphArrowNodeIDField))
|
|
appendNullTopLevel(builder.Field(graphArrowNodeXField))
|
|
appendNullTopLevel(builder.Field(graphArrowNodeYField))
|
|
appendNullTopLevel(builder.Field(graphArrowNodeIRIField))
|
|
appendNullTopLevel(builder.Field(graphArrowNodeLabelField))
|
|
appendNullTopLevel(builder.Field(graphArrowEdgeSourceField))
|
|
appendNullTopLevel(builder.Field(graphArrowEdgeTargetField))
|
|
appendNullTopLevel(builder.Field(graphArrowRouteX1Field))
|
|
appendNullTopLevel(builder.Field(graphArrowRouteY1Field))
|
|
appendNullTopLevel(builder.Field(graphArrowRouteX2Field))
|
|
appendNullTopLevel(builder.Field(graphArrowRouteY2Field))
|
|
|
|
return writeGraphArrowRecord(writer, builder)
|
|
}
|
|
|
|
func writeGraphArrowNodeBatch(writer *ipc.Writer, alloc memory.Allocator, nodes []Node) error {
|
|
builder := array.NewRecordBuilder(alloc, graphArrowSchema)
|
|
defer builder.Release()
|
|
|
|
appendNullTopLevel(builder.Field(graphArrowMetaBackendField))
|
|
appendNullTopLevel(builder.Field(graphArrowMetaGraphQueryIDField))
|
|
appendNullTopLevel(builder.Field(graphArrowMetaNodeLimitField))
|
|
appendNullTopLevel(builder.Field(graphArrowMetaEdgeLimitField))
|
|
appendNullTopLevel(builder.Field(graphArrowMetaNodesField))
|
|
appendNullTopLevel(builder.Field(graphArrowMetaEdgesField))
|
|
appendNullTopLevel(builder.Field(graphArrowMetaRouteLineSegmentsField))
|
|
|
|
appendUint32List(builder.Field(graphArrowNodeIDField), func(valueBuilder *array.Uint32Builder) {
|
|
for _, node := range nodes {
|
|
valueBuilder.Append(node.ID)
|
|
}
|
|
})
|
|
appendFloat32List(builder.Field(graphArrowNodeXField), func(valueBuilder *array.Float32Builder) {
|
|
for _, node := range nodes {
|
|
valueBuilder.Append(float32(node.X))
|
|
}
|
|
})
|
|
appendFloat32List(builder.Field(graphArrowNodeYField), func(valueBuilder *array.Float32Builder) {
|
|
for _, node := range nodes {
|
|
valueBuilder.Append(float32(node.Y))
|
|
}
|
|
})
|
|
appendStringList(builder.Field(graphArrowNodeIRIField), func(valueBuilder *array.StringBuilder) {
|
|
for _, node := range nodes {
|
|
valueBuilder.Append(node.IRI)
|
|
}
|
|
})
|
|
appendNullableStringList(builder.Field(graphArrowNodeLabelField), func(valueBuilder *array.StringBuilder) {
|
|
for _, node := range nodes {
|
|
if node.Label == nil {
|
|
valueBuilder.AppendNull()
|
|
continue
|
|
}
|
|
valueBuilder.Append(*node.Label)
|
|
}
|
|
})
|
|
|
|
appendNullTopLevel(builder.Field(graphArrowEdgeSourceField))
|
|
appendNullTopLevel(builder.Field(graphArrowEdgeTargetField))
|
|
appendNullTopLevel(builder.Field(graphArrowRouteX1Field))
|
|
appendNullTopLevel(builder.Field(graphArrowRouteY1Field))
|
|
appendNullTopLevel(builder.Field(graphArrowRouteX2Field))
|
|
appendNullTopLevel(builder.Field(graphArrowRouteY2Field))
|
|
|
|
return writeGraphArrowRecord(writer, builder)
|
|
}
|
|
|
|
func writeGraphArrowEdgeBatch(writer *ipc.Writer, alloc memory.Allocator, edges []Edge) error {
|
|
builder := array.NewRecordBuilder(alloc, graphArrowSchema)
|
|
defer builder.Release()
|
|
|
|
appendNullTopLevel(builder.Field(graphArrowMetaBackendField))
|
|
appendNullTopLevel(builder.Field(graphArrowMetaGraphQueryIDField))
|
|
appendNullTopLevel(builder.Field(graphArrowMetaNodeLimitField))
|
|
appendNullTopLevel(builder.Field(graphArrowMetaEdgeLimitField))
|
|
appendNullTopLevel(builder.Field(graphArrowMetaNodesField))
|
|
appendNullTopLevel(builder.Field(graphArrowMetaEdgesField))
|
|
appendNullTopLevel(builder.Field(graphArrowMetaRouteLineSegmentsField))
|
|
appendNullTopLevel(builder.Field(graphArrowNodeIDField))
|
|
appendNullTopLevel(builder.Field(graphArrowNodeXField))
|
|
appendNullTopLevel(builder.Field(graphArrowNodeYField))
|
|
appendNullTopLevel(builder.Field(graphArrowNodeIRIField))
|
|
appendNullTopLevel(builder.Field(graphArrowNodeLabelField))
|
|
|
|
appendUint32List(builder.Field(graphArrowEdgeSourceField), func(valueBuilder *array.Uint32Builder) {
|
|
for _, edge := range edges {
|
|
valueBuilder.Append(edge.Source)
|
|
}
|
|
})
|
|
appendUint32List(builder.Field(graphArrowEdgeTargetField), func(valueBuilder *array.Uint32Builder) {
|
|
for _, edge := range edges {
|
|
valueBuilder.Append(edge.Target)
|
|
}
|
|
})
|
|
|
|
appendNullTopLevel(builder.Field(graphArrowRouteX1Field))
|
|
appendNullTopLevel(builder.Field(graphArrowRouteY1Field))
|
|
appendNullTopLevel(builder.Field(graphArrowRouteX2Field))
|
|
appendNullTopLevel(builder.Field(graphArrowRouteY2Field))
|
|
|
|
return writeGraphArrowRecord(writer, builder)
|
|
}
|
|
|
|
func writeGraphArrowRouteBatch(writer *ipc.Writer, alloc memory.Allocator, routes graphArrowRouteLines) error {
|
|
builder := array.NewRecordBuilder(alloc, graphArrowSchema)
|
|
defer builder.Release()
|
|
|
|
appendNullTopLevel(builder.Field(graphArrowMetaBackendField))
|
|
appendNullTopLevel(builder.Field(graphArrowMetaGraphQueryIDField))
|
|
appendNullTopLevel(builder.Field(graphArrowMetaNodeLimitField))
|
|
appendNullTopLevel(builder.Field(graphArrowMetaEdgeLimitField))
|
|
appendNullTopLevel(builder.Field(graphArrowMetaNodesField))
|
|
appendNullTopLevel(builder.Field(graphArrowMetaEdgesField))
|
|
appendNullTopLevel(builder.Field(graphArrowMetaRouteLineSegmentsField))
|
|
appendNullTopLevel(builder.Field(graphArrowNodeIDField))
|
|
appendNullTopLevel(builder.Field(graphArrowNodeXField))
|
|
appendNullTopLevel(builder.Field(graphArrowNodeYField))
|
|
appendNullTopLevel(builder.Field(graphArrowNodeIRIField))
|
|
appendNullTopLevel(builder.Field(graphArrowNodeLabelField))
|
|
appendNullTopLevel(builder.Field(graphArrowEdgeSourceField))
|
|
appendNullTopLevel(builder.Field(graphArrowEdgeTargetField))
|
|
|
|
appendFloat32List(builder.Field(graphArrowRouteX1Field), func(valueBuilder *array.Float32Builder) {
|
|
for _, value := range routes.X1 {
|
|
valueBuilder.Append(value)
|
|
}
|
|
})
|
|
appendFloat32List(builder.Field(graphArrowRouteY1Field), func(valueBuilder *array.Float32Builder) {
|
|
for _, value := range routes.Y1 {
|
|
valueBuilder.Append(value)
|
|
}
|
|
})
|
|
appendFloat32List(builder.Field(graphArrowRouteX2Field), func(valueBuilder *array.Float32Builder) {
|
|
for _, value := range routes.X2 {
|
|
valueBuilder.Append(value)
|
|
}
|
|
})
|
|
appendFloat32List(builder.Field(graphArrowRouteY2Field), func(valueBuilder *array.Float32Builder) {
|
|
for _, value := range routes.Y2 {
|
|
valueBuilder.Append(value)
|
|
}
|
|
})
|
|
|
|
return writeGraphArrowRecord(writer, builder)
|
|
}
|
|
|
|
func writeGraphArrowRecord(writer *ipc.Writer, builder *array.RecordBuilder) error {
|
|
record := builder.NewRecord()
|
|
defer record.Release()
|
|
return writer.Write(record)
|
|
}
|
|
|
|
func appendStringBuilderValue(builder array.Builder, value string) {
|
|
builder.(*array.StringBuilder).Append(value)
|
|
}
|
|
|
|
func appendInt32BuilderValue(builder array.Builder, value int32) {
|
|
builder.(*array.Int32Builder).Append(value)
|
|
}
|
|
|
|
func appendUint32List(builder array.Builder, appendValues func(*array.Uint32Builder)) {
|
|
listBuilder := builder.(*array.ListBuilder)
|
|
listBuilder.Append(true)
|
|
valueBuilder := listBuilder.ValueBuilder().(*array.Uint32Builder)
|
|
appendValues(valueBuilder)
|
|
}
|
|
|
|
func appendFloat32List(builder array.Builder, appendValues func(*array.Float32Builder)) {
|
|
listBuilder := builder.(*array.ListBuilder)
|
|
listBuilder.Append(true)
|
|
valueBuilder := listBuilder.ValueBuilder().(*array.Float32Builder)
|
|
appendValues(valueBuilder)
|
|
}
|
|
|
|
func appendStringList(builder array.Builder, appendValues func(*array.StringBuilder)) {
|
|
listBuilder := builder.(*array.ListBuilder)
|
|
listBuilder.Append(true)
|
|
valueBuilder := listBuilder.ValueBuilder().(*array.StringBuilder)
|
|
appendValues(valueBuilder)
|
|
}
|
|
|
|
func appendNullableStringList(builder array.Builder, appendValues func(*array.StringBuilder)) {
|
|
listBuilder := builder.(*array.ListBuilder)
|
|
listBuilder.Append(true)
|
|
valueBuilder := listBuilder.ValueBuilder().(*array.StringBuilder)
|
|
appendValues(valueBuilder)
|
|
}
|
|
|
|
func appendNullTopLevel(builder array.Builder) {
|
|
switch typed := builder.(type) {
|
|
case *array.StringBuilder:
|
|
typed.AppendNull()
|
|
case *array.Int32Builder:
|
|
typed.AppendNull()
|
|
case *array.ListBuilder:
|
|
typed.AppendNull()
|
|
default:
|
|
panic(fmt.Sprintf("unsupported top-level builder type %T", builder))
|
|
}
|
|
}
|
|
|
|
func flattenGraphRouteLines(routeSegments []RouteSegment) graphArrowRouteLines {
|
|
lineCount := 0
|
|
for _, route := range routeSegments {
|
|
if len(route.Points) > 1 {
|
|
lineCount += len(route.Points) - 1
|
|
}
|
|
}
|
|
|
|
out := graphArrowRouteLines{
|
|
X1: make([]float32, 0, lineCount),
|
|
Y1: make([]float32, 0, lineCount),
|
|
X2: make([]float32, 0, lineCount),
|
|
Y2: make([]float32, 0, lineCount),
|
|
}
|
|
for _, route := range routeSegments {
|
|
for i := 1; i < len(route.Points); i++ {
|
|
prev := route.Points[i-1]
|
|
curr := route.Points[i]
|
|
out.X1 = append(out.X1, float32(prev.X))
|
|
out.Y1 = append(out.Y1, float32(prev.Y))
|
|
out.X2 = append(out.X2, float32(curr.X))
|
|
out.Y2 = append(out.Y2, float32(curr.Y))
|
|
}
|
|
}
|
|
return out
|
|
}
|