# Neo4j Go Driver Development Guide **Library**: `github.com/neo4j/neo4j-go-driver/v5` **Used In**: MVP - Graph database for resource matching **Purpose**: Connect to Neo4j and execute Cypher queries --- ## Where It's Used - Store graph structure (Business → Site → ResourceFlow) - Query matches between ResourceFlows - Create and manage relationships - Graph traversal for matching algorithm --- ## Official Documentation - **GitHub**: https://github.com/neo4j/neo4j-go-driver - **Official Docs**: https://neo4j.com/docs/driver-manual/current/drivers/go/ - **GoDoc**: https://pkg.go.dev/github.com/neo4j/neo4j-go-driver/v5 - **Cypher Query Language**: https://neo4j.com/docs/cypher-manual/current/ --- ## Installation ```bash go get github.com/neo4j/neo4j-go-driver/v5 ``` --- ## Key Concepts ### 1. Driver Initialization ```go import ( "github.com/neo4j/neo4j-go-driver/v5/neo4j" "context" ) func NewNeo4jDriver(uri, username, password string) (neo4j.DriverWithContext, error) { driver, err := neo4j.NewDriverWithContext( uri, // e.g., "neo4j://localhost:7687" neo4j.BasicAuth(username, password, ""), ) if err != nil { return nil, err } // Verify connectivity ctx := context.Background() if err := driver.VerifyConnectivity(ctx); err != nil { driver.Close(ctx) return nil, err } return driver, nil } ``` ### 2. Session Management ```go // Create session session := driver.NewSession(ctx, neo4j.SessionConfig{ AccessMode: neo4j.AccessModeWrite, // or AccessModeRead DatabaseName: "neo4j", // or your database name }) defer session.Close(ctx) // Execute query result, err := session.Run(ctx, cypher, params) ``` ### 3. Basic Cypher Queries ```go // Create node cypher := "CREATE (b:Business {id: $id, name: $name, email: $email}) RETURN b" params := map[string]interface{}{ "id": uuid.New().String(), "name": "Factory A", "email": "contact@factorya.com", } result, err := session.Run(ctx, cypher, params) if err != nil { return err } // Process single record record, err := result.Single(ctx) if err != nil { return err } node, _ := record.Get("b") businessNode := node.(neo4j.Node) ``` ### 4. Create Relationships ```go // Match nodes and create relationship cypher := ` MATCH (b:Business {id: $businessID}) MATCH (s:Site {id: $siteID}) CREATE (b)-[:OPERATES_AT]->(s) RETURN b, s ` params := map[string]interface{}{ "businessID": businessID, "siteID": siteID, } _, err := session.Run(ctx, cypher, params) ``` ### 5. Query with Results ```go // Query multiple records cypher := ` MATCH (b:Business)-[:OPERATES_AT]->(s:Site) WHERE b.id = $businessID RETURN b, s ` params := map[string]interface{}{ "businessID": businessID, } result, err := session.Run(ctx, cypher, params) if err != nil { return err } var sites []Site for result.Next(ctx) { record := result.Record() siteNode, _ := record.Get("s") site := parseSiteNode(siteNode.(neo4j.Node)) sites = append(sites, site) } if err := result.Err(); err != nil { return err } ``` ### 6. Transactions ```go // Transaction with automatic commit/rollback _, err = session.ExecuteWrite(ctx, func(tx neo4j.ManagedTransaction) (interface{}, error) { // Create business result, err := tx.Run(ctx, "CREATE (b:Business {id: $id, name: $name}) RETURN b.id", map[string]interface{}{"id": id, "name": name}, ) if err != nil { return nil, err } record, err := result.Single(ctx) if err != nil { return nil, err } businessID, _ := record.Get("b.id") // Create site in same transaction _, err = tx.Run(ctx, "MATCH (b:Business {id: $businessID}) CREATE (s:Site {id: $siteID}) CREATE (b)-[:OPERATES_AT]->(s)", map[string]interface{}{"businessID": businessID, "siteID": siteID}, ) return businessID, err }) ``` ### 7. Parameter Binding ```go // Using struct for parameters type BusinessParams struct { ID string Name string Email string } params := BusinessParams{ ID: uuid.New().String(), Name: "Factory A", Email: "contact@factorya.com", } cypher := ` CREATE (b:Business { id: $id, name: $name, email: $email }) RETURN b ` // Neo4j driver automatically converts struct to map result, err := session.Run(ctx, cypher, params) ``` ### 8. Extracting Values from Records ```go record, _ := result.Single(ctx) // Get by key node, _ := record.Get("b") relationship, _ := record.Get("r") // Type assertions businessNode := node.(neo4j.Node) props := businessNode.Props // Extract properties id, _ := props["id"].(string) name, _ := props["name"].(string) // Or use helper function func getString(record neo4j.Record, key string) string { val, ok := record.Get(key) if !ok { return "" } str, _ := val.(string) return str } ``` --- ## MVP-Specific Patterns ### Resource Flow Service ```go type Neo4jService struct { driver neo4j.DriverWithContext } func (s *Neo4jService) CreateResourceFlow(ctx context.Context, flow ResourceFlow) error { session := s.driver.NewSession(ctx, neo4j.SessionConfig{ AccessMode: neo4j.AccessModeWrite, }) defer session.Close(ctx) cypher := ` MATCH (site:Site {id: $siteID}) CREATE (flow:ResourceFlow { id: $id, direction: $direction, type: $type, temperature_celsius: $temperature, quantity_kwh_per_month: $quantity, cost_per_kwh_euro: $cost }) CREATE (site)-[:HOSTS]->(flow) RETURN flow.id ` params := map[string]interface{}{ "id": flow.ID, "siteID": flow.SiteID, "direction": flow.Direction, "type": flow.Type, "temperature": flow.TemperatureCelsius, "quantity": flow.QuantityKwhPerMonth, "cost": flow.CostPerKwhEuro, } _, err := session.Run(ctx, cypher, params) return err } ``` ### Matching Query ```go func (s *Neo4jService) FindMatches(ctx context.Context, flowID string, maxDistanceKm float64) ([]Match, error) { session := s.driver.NewSession(ctx, neo4j.SessionConfig{ AccessMode: neo4j.AccessModeRead, }) defer session.Close(ctx) cypher := ` MATCH (sourceFlow:ResourceFlow {id: $flowID})-[:HOSTS]->(sourceSite:Site), (targetFlow:ResourceFlow)-[:HOSTS]->(targetSite:Site) WHERE sourceFlow.direction = 'output' AND targetFlow.direction = 'input' AND sourceFlow.type = 'heat' AND targetFlow.type = 'heat' AND ABS(sourceFlow.temperature_celsius - targetFlow.temperature_celsius) <= 10 WITH sourceFlow, targetFlow, sourceSite, targetSite, point.distance( point({longitude: sourceSite.longitude, latitude: sourceSite.latitude}), point({longitude: targetSite.longitude, latitude: targetSite.latitude}) ) / 1000 AS distance_km WHERE distance_km <= $maxDistance RETURN targetFlow.id AS target_flow_id, targetFlow.temperature_celsius AS target_temp, targetFlow.quantity_kwh_per_month AS target_quantity, distance_km ORDER BY distance_km ASC LIMIT 20 ` params := map[string]interface{}{ "flowID": flowID, "maxDistance": maxDistanceKm, } result, err := session.Run(ctx, cypher, params) if err != nil { return nil, err } var matches []Match for result.Next(ctx) { record := result.Record() match := Match{ TargetFlowID: getString(record, "target_flow_id"), TargetTemp: getFloat(record, "target_temp"), TargetQuantity: getFloat(record, "target_quantity"), DistanceKm: getFloat(record, "distance_km"), } matches = append(matches, match) } return matches, result.Err() } ``` ### Connection Pooling ```go // Driver automatically manages connection pool // Configure during driver creation driver, err := neo4j.NewDriverWithContext( uri, neo4j.BasicAuth(username, password, ""), func(config *neo4j.Config) { config.MaxConnectionPoolSize = 50 config.ConnectionAcquisitionTimeout = 30 * time.Second config.MaxTransactionRetryTime = 30 * time.Second }, ) ``` --- ## Error Handling ```go result, err := session.Run(ctx, cypher, params) if err != nil { // Check for specific Neo4j errors if neo4jErr, ok := err.(*neo4j.Neo4jError); ok { switch neo4jErr.Code { case "Neo.ClientError.Statement.SyntaxError": // Handle syntax error case "Neo.ClientError.Security.Unauthorized": // Handle auth error default: // Handle other errors } } return err } // Check result errors if err := result.Err(); err != nil { return err } ``` --- ## Performance Tips 1. **Reuse sessions** - create session per request/operation, not per query 2. **Use transactions** - batch operations in single transaction 3. **Parameterize queries** - always use parameters, never string concatenation 4. **Create indexes** - for frequently queried properties 5. **Use LIMIT** - always limit query results 6. **Profile queries** - use `EXPLAIN` and `PROFILE` to optimize Cypher --- ## Indexes ```go // Create indexes for better performance indexes := []string{ "CREATE INDEX business_id IF NOT EXISTS FOR (b:Business) ON (b.id)", "CREATE INDEX site_id IF NOT EXISTS FOR (s:Site) ON (s.id)", "CREATE INDEX resource_flow_direction IF NOT EXISTS FOR (r:ResourceFlow) ON (r.direction)", "CREATE INDEX resource_flow_type IF NOT EXISTS FOR (r:ResourceFlow) ON (r.type)", } for _, index := range indexes { _, err := session.Run(ctx, index, nil) if err != nil { log.Printf("Failed to create index: %v", err) } } ``` --- ## Tutorials & Resources - **Neo4j Go Driver Examples**: https://github.com/neo4j/neo4j-go-driver/tree/5.0/examples - **Getting Started Tutorial**: https://neo4j.com/developer/go/ - **Cypher Manual**: https://neo4j.com/docs/cypher-manual/current/ - **Best Practices**: https://neo4j.com/developer/go-driver/#_best_practices --- ## Common Patterns ### Repository Pattern ```go type BusinessRepository struct { driver neo4j.DriverWithContext } func (r *BusinessRepository) FindByID(ctx context.Context, id string) (*Business, error) { session := r.driver.NewSession(ctx, neo4j.SessionConfig{ AccessMode: neo4j.AccessModeRead, }) defer session.Close(ctx) // ... query logic ... } func (r *BusinessRepository) Create(ctx context.Context, business *Business) error { session := r.driver.NewSession(ctx, neo4j.SessionConfig{ AccessMode: neo4j.AccessModeWrite, }) defer session.Close(ctx) // ... create logic ... } ``` ### Helper Functions ```go func parseBusinessNode(node neo4j.Node) *Business { props := node.Props return &Business{ ID: getString(props, "id"), Name: getString(props, "name"), Email: getString(props, "email"), } } func getString(m map[string]interface{}, key string) string { if val, ok := m[key]; ok { if str, ok := val.(string); ok { return str } } return "" } func getFloat(m map[string]interface{}, key string) float64 { if val, ok := m[key]; ok { if f, ok := val.(float64); ok { return f } } return 0 } ```