Amazon Neptune Analytics: Graph and Vector Analytics for Fraud Detection and Recommendations
Quick summary: Neptune Analytics adds in-memory graph processing and vector similarity search to Neptune graph data. Production guide for fraud detection, recommendation engines, and supply chain risk.

Table of Contents
Neptune Database handles real-time graph traversals well. Finding all accounts connected to a suspicious transaction two hops away, looking up a customer’s product graph neighborhood, checking if a new merchant shares attributes with known fraudulent entities — these are OLTP graph queries and Neptune Database is the right tool for them.
The problem arises when you need to run analytics across the entire graph. Computing PageRank on 500 million nodes. Identifying all fraud rings in a payment network by finding weakly connected components across 2 billion edges. Finding product clusters via community detection for a recommendation system. These are OLAP workloads, and Neptune Database’s architecture — optimized for low-latency traversals with transactional consistency — is not designed for them. Running a full-graph PageRank against Neptune Database will lock the cluster, time out, and produce an expensive lesson.
Amazon Neptune Analytics, which reached GA in November 2023 and has expanded through 2024–2025, solves this by loading a snapshot of your graph into an in-memory analytics engine that is purpose-built for graph algorithm execution and vector similarity search. This post covers the OLTP/OLAP split, fraud ring detection walkthrough, combined graph+vector queries, data loading patterns, and cost analysis.
Neptune Database vs. Neptune Analytics: The OLTP/OLAP Split
Understanding the architectural boundary between the two products prevents misuse of both.
| Dimension | Neptune Database | Neptune Analytics |
|---|---|---|
| Architecture | Distributed storage-compute separation, persistent | In-memory, loaded from snapshots |
| Query languages | Gremlin, openCypher, SPARQL | openCypher only |
| Write support | Yes — full transactional writes | No — read-only analytics |
| Data freshness | Real-time (as transactions commit) | Snapshot-based (load time) |
| Graph algorithm support | No built-in algorithms | Yes — PageRank, WCC, betweenness centrality, shortest path, community detection, KNN |
| Vector similarity search | No | Yes — cosine, Euclidean, dot product similarity on node embeddings |
| Query latency (traversal) | Milliseconds for point queries | Milliseconds for algorithms on the full in-memory graph |
| Full-graph analytics latency | Minutes-to-timeout (storage I/O bound) | Sub-second for most algorithms on millions of nodes |
| Billing model | Instance hours (r5/r6g instance families) | Graph memory capacity (GB) + query hours |
| When to use | Transactional writes, real-time traversals, CRUD | Batch graph analytics, fraud ring detection, recommendations, embeddings |
The canonical deployment pattern is read-write against Neptune Database for transactional operations, periodic (nightly or hourly) export to S3, load into Neptune Analytics for batch analytics, and write results back to an operational store (DynamoDB or RDS) for application consumption.
Application Layer
│
├── Real-time traversals → Neptune Database (R/W)
│ │
│ └── Nightly export → S3
│ │
└── Read enrichment scores ← DynamoDB ← Neptune Analytics ← S3 snapshotFraud Ring Detection with Graph Analytics
Payment fraud rings are difficult to detect with row-level transaction analysis because the signal is in the connections between entities, not in individual transaction properties. A card used for three transactions, each below the fraud threshold — suspicious in isolation only if you know those three transactions share a device fingerprint with 40 other cards linked to the same merchant, all controlled by the same shell entity.
Graph analytics surfaces this pattern through Weakly Connected Components (WCC). In graph theory, a connected component is a subgraph where every node can reach every other node. In a payment network, an unusually large connected component where nodes share multiple attribute paths is a strong fraud ring signal.
Step 1: Build the payment network graph
Model the payment network as a tripartite graph with four node types:
Nodes: Card, Merchant, Device, IP Address
Edges:
(Card) -[USED_AT]-> (Merchant): transaction occurred
(Card) -[ACCESSED_FROM]-> (Device): card used on device
(Card) -[ACCESSED_FROM]-> (IP): card connected from IP
(Merchant) -[REGISTERED_FROM]-> (IP): merchant signup IPExport this graph from your transactional database as Neptune bulk load format:
import boto3
import pandas as pd
# Generate node file (cards)
cards_df = pd.DataFrame({
'id': card_ids,
':label': 'Card',
'card_last4': card_last4_list,
'issuer_bank': issuer_bank_list,
'account_age_days': account_age_list
})
cards_df.to_csv('s3://fraud-data/graph-export/nodes-cards.csv', index=False)
# Generate edge file (card-to-merchant transactions)
transactions_df = pd.DataFrame({
':ID': edge_ids,
':START_ID': source_card_ids,
':END_ID': destination_merchant_ids,
':TYPE': 'USED_AT',
'amount_usd': amounts,
'txn_date': dates,
'status': statuses
})
transactions_df.to_csv('s3://fraud-data/graph-export/edges-transactions.csv', index=False)Step 2: Load into Neptune Analytics
import boto3
neptune_analytics = boto3.client('neptune-graph', region_name='us-east-1')
# Create a Neptune Analytics graph with sufficient memory for the payment graph
response = neptune_analytics.create_graph(
graphName='payment-fraud-analytics',
provisionedMemory=64, # GB — size based on your graph: ~1 GB per 10M edges
replicaCount=0, # 0 replicas for batch-only use cases
deletionProtection=False,
vectorSearchConfiguration={
'dimension': 256 # Include if you're storing embedding vectors on nodes
}
)
graph_id = response['id']
print(f"Graph ID: {graph_id}")
# Import from S3
import_response = neptune_analytics.start_import_task(
graphIdentifier=graph_id,
source='s3://fraud-data/graph-export/',
format='CSV',
roleArn='arn:aws:iam::123456789012:role/NeptuneAnalyticsImportRole',
importOptions={
'neptune': {
'preserveDefaultVertexLabels': True,
'preserveEdgeIds': True
}
}
)
task_id = import_response['taskId']
print(f"Import task: {task_id}")
# Wait for import to complete
import time
while True:
status = neptune_analytics.get_import_task(taskId=task_id)
if status['status'] in ['SUCCEEDED', 'FAILED']:
print(f"Import completed: {status['status']}")
break
time.sleep(30)Step 3: Run Weakly Connected Components
# Execute WCC algorithm via openCypher
# Neptune Analytics graph algorithm invocations use openCypher with neptune.algo functions
query = """
CALL neptune.algo.wcc({
nodeLabels: ['Card', 'Merchant', 'Device', 'IP'],
relationshipTypes: ['USED_AT', 'ACCESSED_FROM', 'REGISTERED_FROM'],
writeProperty: 'component_id'
})
YIELD componentId, nodeCount
WHERE nodeCount > 50 // Components with > 50 members warrant investigation
RETURN componentId, nodeCount
ORDER BY nodeCount DESC
LIMIT 100
"""
response = neptune_analytics.execute_query(
graphIdentifier=graph_id,
queryString=query,
language='OPEN_CYPHER'
)
# Parse results
results = json.loads(response['payload'].read())
fraud_ring_components = results['results']
print(f"Suspicious components found: {len(fraud_ring_components)}")
for component in fraud_ring_components[:10]:
print(f"Component {component['componentId']}: {component['nodeCount']} connected entities")Step 4: Enrich and export for investigators
# For each suspicious component, pull entity details for case management
enrichment_query = """
MATCH (n)
WHERE n.component_id = $componentId
RETURN
labels(n)[0] as entity_type,
n.id as entity_id,
n.component_id as component_id,
CASE labels(n)[0]
WHEN 'Card' THEN n.card_last4
WHEN 'Merchant' THEN n.merchant_name
WHEN 'Device' THEN n.device_fingerprint
WHEN 'IP' THEN n.ip_address
END as identifier
"""
fraud_entities = []
for component in fraud_ring_components:
entities = neptune_analytics.execute_query(
graphIdentifier=graph_id,
queryString=enrichment_query,
language='OPEN_CYPHER',
parameters={'componentId': component['componentId']}
)
fraud_entities.extend(json.loads(entities['payload'].read())['results'])
# Write to DynamoDB for case management system
dynamodb = boto3.resource('dynamodb')
fraud_cases_table = dynamodb.Table('fraud-cases')
for entity in fraud_entities:
fraud_cases_table.put_item(Item={
'component_id': str(entity['component_id']),
'entity_type': entity['entity_type'],
'entity_id': entity['entity_id'],
'identifier': entity['identifier'],
'detected_date': datetime.now().isoformat(),
'status': 'PENDING_REVIEW'
})Graph + Vector Combined Queries
Neptune Analytics uniquely supports combining graph structure with vector similarity in a single query. This unlocks recommendation patterns that neither pure graph traversal nor pure vector search can achieve alone.
Use case: product recommendations combining semantic similarity and purchase graph
Store product embedding vectors (from a text embedding model like Amazon Titan Embeddings) as properties on product nodes in Neptune Database. When you load the graph into Neptune Analytics, the embedding vectors load with the node properties.
# When building your graph: add embedding vectors to product nodes
# These are stored as array properties on nodes in Neptune Database
import boto3
import json
bedrock = boto3.client('bedrock-runtime')
neptune_db = boto3.client('neptunedata', endpoint_url='https://YOUR-CLUSTER.neptune.amazonaws.com:8182')
def embed_product(description: str) -> list[float]:
response = bedrock.invoke_model(
modelId='amazon.titan-embed-text-v2:0',
body=json.dumps({'inputText': description})
)
return json.loads(response['body'].read())['embedding']
# Store as a node property via openCypher
for product in products:
embedding = embed_product(product['description'])
neptune_db.execute_open_cypher_query(
openCypherQuery="""
MATCH (p:Product {id: $productId})
SET p.embedding = $embedding
""",
parameters=json.dumps({
'productId': product['id'],
'embedding': embedding
})
)After loading this graph into Neptune Analytics with vector search configured, run combined queries:
# Combined query: find products that are semantically similar
# AND frequently co-purchased by users in target customer's neighborhood
combined_query = """
// Step 1: Find the query product's embedding and customer's purchase graph
MATCH (queryProduct:Product {id: $productId})
MATCH (customer:Customer {id: $customerId})-[:PURCHASED]->(ownedProduct:Product)
// Step 2: Find semantically similar products using vector search
CALL neptune.algo.vectors.topKByNode(queryProduct, {
topK: 50,
concurrency: 4
}) YIELD node AS similarProduct, score AS similarityScore
// Step 3: Intersect with products bought by similar customers (graph traversal)
MATCH (similarCustomer:Customer)-[:PURCHASED]->(candidateProduct:Product)
WHERE similarCustomer <> customer
AND (similarCustomer)-[:PURCHASED]->(ownedProduct) // Similar customers share purchases
AND candidateProduct IN similarProduct // Candidate is semantically similar
// Step 4: Score by combined signal
RETURN candidateProduct.id AS productId,
candidateProduct.name AS productName,
similarityScore,
count(DISTINCT similarCustomer) AS graphSignalStrength,
(similarityScore * 0.4 + toFloat(count(DISTINCT similarCustomer)) / 100.0 * 0.6) AS combinedScore
ORDER BY combinedScore DESC
LIMIT 10
"""
recommendations = neptune_analytics.execute_query(
graphIdentifier=graph_id,
queryString=combined_query,
language='OPEN_CYPHER',
parameters={'productId': 'prod-abc123', 'customerId': 'cust-xyz789'}
)The power here is that neither approach alone delivers the full signal:
- Pure vector similarity finds semantically similar products but ignores social purchase context
- Pure graph traversal finds what similar customers bought but can recommend contextually unrelated items
- Combined: semantically similar products that also have strong purchase graph signal — highest quality recommendations
Loading and Querying Neptune Analytics
Load from Neptune Database export:
# Export Neptune Database to S3 using the Neptune export service
neptune_export = boto3.client('s3', region_name='us-east-1')
# Trigger Neptune export (via Neptune Export API endpoint)
import requests
export_response = requests.post(
'https://YOUR-EXPORT-ENDPOINT.neptune-export.amazonaws.com/v1/neptune-export/export',
json={
'command': 'export-pg', # Property graph (Gremlin/openCypher)
'params': {
'endpoint': 'YOUR-NEPTUNE-CLUSTER.neptune.amazonaws.com',
'profile': 'neptune_export_default',
'outputS3Path': 's3://neptune-exports/fraud-graph/',
'jobSize': 'medium'
}
},
auth=('user', 'password') # Use SigV4 signing in production
)
export_job_id = export_response.json()['jobId']Load from S3 CSV directly (no Neptune Database required):
Neptune Analytics accepts S3 CSV files in the Neptune bulk load format — the same format used for Neptune Database bulk loading. Node files need ~id and ~label columns; edge files need ~id, ~from, ~to, and ~label columns:
# nodes-cards.csv
~id,~label,card_last4:String,account_age_days:Int,risk_tier:String
card-001,Card,4242,365,standard
card-002,Card,1234,12,new
# edges-transactions.csv
~id,~from,~to,~label,amount_usd:Double,txn_date:Date
txn-001,card-001,merchant-001,USED_AT,149.99,2026-06-01
txn-002,card-002,merchant-001,USED_AT,89.50,2026-06-01Error handling for large graph loads:
def wait_for_import_with_retry(neptune_analytics_client, task_id: str, max_wait_minutes: int = 120):
import time
start_time = time.time()
while (time.time() - start_time) < (max_wait_minutes * 60):
status = neptune_analytics_client.get_import_task(taskId=task_id)
if status['status'] == 'SUCCEEDED':
stats = status.get('importTaskDetails', {})
print(f"Import succeeded. Nodes: {stats.get('progressPercentage', 'N/A')}%")
return True
if status['status'] == 'FAILED':
error = status.get('statusReason', 'Unknown error')
print(f"Import failed: {error}")
# Common failures: insufficient memory, malformed CSV, S3 permissions
if 'OutOfMemory' in error:
print("Increase provisionedMemory parameter and retry")
elif 'AccessDenied' in error:
print("Check Neptune Analytics IAM role has s3:GetObject on export bucket")
return False
elapsed = int(time.time() - start_time)
print(f"Import in progress... ({elapsed}s elapsed, status: {status['status']})")
time.sleep(30)
print(f"Import timed out after {max_wait_minutes} minutes")
return FalseCost Model
Neptune Analytics pricing has two dimensions: provisioned memory capacity (GB) and query execution hours.
| Dimension | Neptune Analytics | Neptune Database (r5.2xlarge equivalent) |
|---|---|---|
| Memory-based cost | ~$0.118/GB-hour (varies by region) | Included in instance pricing |
| Instance cost | N/A | ~$0.52/hour (r5.2xlarge) |
| Full-graph algorithm | Included in query hours | Typically impractical (timeout/cost) |
| Daily 1-hour batch analytics | 64 GB × $0.118 × 1 hour = $7.55/day | N/A (would degrade cluster) |
| Monthly (1-hour daily batch) | ~$227/month | ~$374/month (always-on r5.2xlarge) |
For fraud ring detection running as a nightly batch job (1-2 hours), Neptune Analytics is cheaper than keeping a large Neptune Database instance sized for analytics workloads, because you only pay for the time the analytics graph is loaded and queried.
Self-hosted comparison:
An alternative is running Apache Spark GraphX with GraphFrames on an EMR cluster for graph algorithms, plus a separate vector similarity solution. The engineering overhead (cluster management, algorithm implementation, result persistence) and operational complexity make this path significantly more expensive in practice than Neptune Analytics for teams without existing Spark expertise.
Cost optimization tips:
- Delete the Neptune Analytics graph after batch analytics completes — you only pay for the loaded, running graph
- Reload from S3 for the next batch run (import from S3 takes minutes for graphs under 10 GB)
- For very large graphs that take 30+ minutes to load, keep the graph running overnight rather than deleting and reloading every run
- Use
replicaCount: 0for batch-only use cases (no read replicas needed)
Neptune Analytics fills a genuine gap in the AWS analytics portfolio. Graph algorithms against large graphs were previously either impractical (running on Neptune Database) or expensive engineering projects (Spark GraphX on EMR). Neptune Analytics makes PageRank, WCC, and community detection against billion-edge graphs a straightforward API call, with the added capability of combining graph algorithms with vector similarity search in a single query.
For fraud detection specifically, the WCC-based fraud ring detection pattern has better recall than rule-based systems for novel fraud rings — the graph structure reveals connections that row-level rules miss. For recommendation systems, the combined graph+vector query pattern produces recommendations that are both contextually relevant and informed by real purchase behavior.
For organizations building vector-enabled search infrastructure, compare Neptune Analytics’ graph+vector approach with Amazon S3 Vectors for native vector storage and Amazon OpenSearch Service’s vector and analytics patterns to choose the right architecture for your specific use case. See also the AWS AI services landscape for 2026 for the full picture of where graph analytics fits in modern AWS data architectures.
Need help designing a Neptune Analytics architecture for fraud detection or recommendation systems? FactualMinds works with data engineering teams on graph data modeling, Neptune Database to Neptune Analytics pipeline design, and combined graph+vector query patterns for production workloads.
AWS Cloud Architect & AI Expert
AWS-certified cloud architect and AI expert with deep expertise in cloud migrations, cost optimization, and generative AI on AWS.
