turash/dev_guides/02_neo4j_driver.md
Damir Mukimov 4a2fda96cd
Initial commit: Repository setup with .gitignore, golangci-lint v2.6.0, and code quality checks
- Initialize git repository
- Add comprehensive .gitignore for Go projects
- Install golangci-lint v2.6.0 (latest v2) globally
- Configure .golangci.yml with appropriate linters and formatters
- Fix all formatting issues (gofmt)
- Fix all errcheck issues (unchecked errors)
- Adjust complexity threshold for validation functions
- All checks passing: build, test, vet, lint
2025-11-01 07:36:22 +01:00

12 KiB

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


Installation

go get github.com/neo4j/neo4j-go-driver/v5

Key Concepts

1. Driver Initialization

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

// 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

// 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

// 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

// 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

// 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

// 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

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

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

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

// 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

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

// 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


Common Patterns

Repository Pattern

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

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
}