{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Graph Store Module\n",
    "\n",
    "## Overview\n",
    "\n",
    "The Graph Store module provides a unified interface for working with property graph databases. It supports multiple backends (Neo4j, FalkorDB) and offers comprehensive features for storing, querying, and analyzing graph data.\n",
    "\n",
    "### Key Features\n",
    "\n",
    "- **Multi-Backend Support**: Neo4j (Enterprise), FalkorDB (Redis-based)\n",
    "- **Full CRUD Operations**: Create, read, update, delete nodes and relationships\n",
    "- **Cypher Query Language**: Execute complex graph queries with OpenCypher support\n",
    "- **Graph Analytics**: Built-in algorithms for centrality, community detection, path finding\n",
    "- **Batch Operations**: Optimized bulk data loading with progress tracking\n",
    "- **Transaction Support**: ACID transactions with rollback capabilities\n",
    "- **Index Management**: Create and manage indexes for performance optimization\n",
    "- **Convenience Functions**: Simple function-based API for common operations\n",
    "\n",
    "### Learning Objectives\n",
    "\n",
    "By the end of this notebook, you will be able to:\n",
    "\n",
    "1. Initialize and configure GraphStore with different backends\n",
    "2. Perform CRUD operations on nodes and relationships\n",
    "3. Execute Cypher queries for complex graph operations\n",
    "4. Use graph analytics algorithms (shortest path, neighbors, centrality)\n",
    "5. Update and delete graph data\n",
    "6. Use batch operations for efficient data loading\n",
    "7. Work with convenience functions and configuration management\n",
    "8. Choose the right backend for your use case\n",
    "\n",
    "---\n",
    "\n",
    "## Installation\n",
    "\n",
    "### Core Installation\n",
    "\n",
    "```bash\n",
    "# Install Semantica\n",
    "pip install semantica\n",
    "\n",
    "# Or install with all optional dependencies\n",
    "pip install semantica[all]\n",
    "```\n",
    "\n",
    "### Backend-Specific Dependencies\n",
    "\n",
    "```bash\n",
    "# For Neo4j (requires Neo4j server)\n",
    "pip install neo4j\n",
    "\n",

    "# For FalkorDB (requires Redis/FalkorDB server)\n",
    "pip install falkordb\n",
    "```\n",
    "\n",
    "### Docker Setup (Optional)\n",
    "\n",
    "For FalkorDB, you can run it in Docker:\n",
    "\n",
    "```bash\n",
    "docker run -p 6379:6379 -p 3000:3000 -it --rm \\\n",
    "  -v ./data:/var/lib/falkordb/data \\\n",
    "  falkordb/falkordb\n",
    "```\n",
    "\n",
    "---\n",
    "\n",
    "## Backend Comparison\n",
    "\n",
    "| Backend | Best For | Deployment | Features |\n",
    "|---------|----------|------------|----------|\n",
    "| **Neo4j** | Enterprise applications, production systems | Server/Cloud | Full Cypher, APOC procedures, multi-database |\n",
    "| **FalkorDB** | LLM applications, real-time systems, high performance | Redis-based | Ultra-fast, sparse matrix operations |\n",
    "\n",
    "**Recommendation**: Use **Neo4j** for enterprise production systems or **FalkorDB** for high-performance real-time applications.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 1: Initialize Graph Store\n",
    "\n",
    "Initialize a `GraphStore` instance with your preferred backend. For this tutorial, we'll use **Neo4j** (requires a running server).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from semantica.graph_store import GraphStore\n",
    "\n",
    "# Option 1: Neo4j (requires Neo4j server running)\n",
    "store = GraphStore(\n",
    "    backend=\"neo4j\",\n",
    "    uri=\"bolt://localhost:7687\",\n",
    "    user=\"neo4j\",\n",
    "    password=\"password\"\n",
    ")\n",
    "\n",
    "# Option 2: FalkorDB (requires Redis/FalkorDB server)\n",
    "# store = GraphStore(\n",
    "#     backend=\"falkordb\",\n",
    "#     host=\"localhost\",\n",
    "#     port=6379,\n",
    "#     graph_name=\"demo_graph\"\n",
    "# )\n",
    "\n",
    "# Connect to the database\n",
    "store.connect()\n",
    "print(\"Connected to graph database successfully!\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 2: Node Operations\n",
    "\n",
    "### Creating Nodes\n",
    "\n",
    "Nodes represent entities in your graph. Each node can have:\n",
    "- **Labels**: Categories/types (e.g., `Person`, `Company`, `Location`)\n",
    "- **Properties**: Key-value pairs (e.g., `{\"name\": \"Alice\", \"age\": 30}`)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create individual nodes with labels and properties\n",
    "apple = store.create_node(\n",
    "    labels=[\"Company\"],\n",
    "    properties={\"name\": \"Apple Inc.\", \"founded\": 1976, \"industry\": \"Technology\"}\n",
    ")\n",
    "print(f\"Created company node: {apple.get('properties', {}).get('name')} (ID: {apple.get('id')})\")\n",
    "\n",
    "tim_cook = store.create_node(\n",
    "    labels=[\"Person\"],\n",
    "    properties={\"name\": \"Tim Cook\", \"title\": \"CEO\", \"age\": 63}\n",
    ")\n",
    "print(f\"Created person node: {tim_cook.get('properties', {}).get('name')} (ID: {tim_cook.get('id')})\")\n",
    "\n",
    "cupertino = store.create_node(\n",
    "    labels=[\"Location\"],\n",
    "    properties={\"name\": \"Cupertino\", \"state\": \"California\", \"country\": \"USA\"}\n",
    ")\n",
    "print(f\"Created location node: {cupertino.get('properties', {}).get('name')} (ID: {cupertino.get('id')})\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create multiple nodes in batch (more efficient for large datasets)\n",
    "other_companies = store.create_nodes([\n",
    "    {\"labels\": [\"Company\"], \"properties\": {\"name\": \"Microsoft\", \"founded\": 1975}},\n",
    "    {\"labels\": [\"Company\"], \"properties\": {\"name\": \"Google\", \"founded\": 1998}},\n",
    "    {\"labels\": [\"Company\"], \"properties\": {\"name\": \"Amazon\", \"founded\": 1994}},\n",
    "])\n",
    "print(f\"Created {len(other_companies)} company nodes in batch\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 3: Relationship Operations\n",
    "\n",
    "### Creating Relationships\n",
    "\n",
    "Relationships connect nodes and represent connections between entities. Each relationship has:\n",
    "- **Type**: The relationship type (e.g., `CEO_OF`, `LOCATED_IN`, `KNOWS`)\n",
    "- **Properties**: Key-value pairs (e.g., `{\"since\": 2011}`)\n",
    "- **Direction**: From `start_node_id` to `end_node_id`\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create relationships between nodes\n",
    "ceo_rel = store.create_relationship(\n",
    "    start_node_id=tim_cook[\"id\"],\n",
    "    end_node_id=apple[\"id\"],\n",
    "    rel_type=\"CEO_OF\",\n",
    "    properties={\"since\": 2011}\n",
    ")\n",
    "print(f\"Created relationship: {ceo_rel.get('type')} (ID: {ceo_rel.get('id')})\")\n",
    "\n",
    "location_rel = store.create_relationship(\n",
    "    start_node_id=apple[\"id\"],\n",
    "    end_node_id=cupertino[\"id\"],\n",
    "    rel_type=\"HEADQUARTERED_IN\",\n",
    "    properties={\"since\": 1977}\n",
    ")\n",
    "print(f\"Created relationship: {location_rel.get('type')} (ID: {location_rel.get('id')})\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 4: Querying Nodes and Relationships\n",
    "\n",
    "### Retrieving Nodes\n",
    "\n",
    "You can query nodes by labels, properties, or node IDs.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Get nodes by label\n",
    "companies = store.get_nodes(labels=[\"Company\"], limit=10)\n",
    "print(f\"Found {len(companies)} companies:\")\n",
    "for company in companies:\n",
    "    name = company.get('properties', {}).get('name', 'Unknown')\n",
    "    founded = company.get('properties', {}).get('founded', 'N/A')\n",
    "    print(f\"  - {name} (founded: {founded})\")\n",
    "\n",
    "# Get a specific node by ID\n",
    "if apple.get('id'):\n",
    "    node = store.get_node(node_id=apple[\"id\"])\n",
    "    print(f\"\\nRetrieved node by ID: {node.get('properties', {}).get('name')}\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Get relationships for a node\n",
    "relationships = store.get_relationships(node_id=apple[\"id\"], direction=\"both\")\n",
    "print(f\"Found {len(relationships)} relationships for Apple:\")\n",
    "for rel in relationships:\n",
    "    rel_type = rel.get('type', 'Unknown')\n",
    "    props = rel.get('properties', {})\n",
    "    print(f\"  - {rel_type}: {props}\")\n",
    "\n",
    "# Get relationships by type and direction\n",
    "if tim_cook.get('id'):\n",
    "    outgoing = store.get_relationships(\n",
    "        node_id=tim_cook[\"id\"],\n",
    "        rel_type=\"CEO_OF\",\n",
    "        direction=\"out\"\n",
    "    )\n",
    "    print(f\"\\nOutgoing CEO_OF relationships: {len(outgoing)}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 5: Cypher Query Execution\n",
    "\n",
    "### Executing Cypher Queries\n",
    "\n",
    "Cypher is a powerful graph query language that allows you to express complex graph patterns and operations. The Graph Store module supports **OpenCypher** syntax across all backends.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Execute a Cypher query to find CEO relationships\n",
    "results = store.execute_query(\"\"\"\n",
    "    MATCH (p:Person)-[r:CEO_OF]->(c:Company)\n",
    "    RETURN p.name as person, c.name as company, r.since as since\n",
    "\"\"\")\n",
    "\n",
    "print(\"CEO Relationships:\")\n",
    "for record in results.get(\"records\", []):\n",
    "    person = record.get('person', 'Unknown')\n",
    "    company = record.get('company', 'Unknown')\n",
    "    since = record.get('since', 'N/A')\n",
    "    print(f\"  - {person} is CEO of {company} since {since}\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Parameterized query (safer and more efficient)\n",
    "results = store.execute_query(\n",
    "    \"MATCH (c:Company) WHERE c.founded > $year RETURN c.name, c.founded ORDER BY c.founded\",\n",
    "    parameters={\"year\": 1990}\n",
    ")\n",
    "\n",
    "print(\"Companies founded after 1990:\")\n",
    "for record in results.get(\"records\", []):\n",
    "    name = record.get('c.name', 'Unknown')\n",
    "    founded = record.get('c.founded', 'N/A')\n",
    "    print(f\"  - {name} (founded: {founded})\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 6: Graph Analytics\n",
    "\n",
    "### Built-in Analytics Algorithms\n",
    "\n",
    "The Graph Store module provides several graph analytics algorithms for analyzing your graph structure.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Get neighbors of a node (traverse the graph)\n",
    "if apple.get('id'):\n",
    "    neighbors = store.get_neighbors(\n",
    "        node_id=apple[\"id\"],\n",
    "        direction=\"both\",\n",
    "        depth=2\n",
    "    )\n",
    "    \n",
    "    print(f\"Found {len(neighbors)} neighbors (up to depth 2) for Apple:\")\n",
    "    for neighbor in neighbors:\n",
    "        name = neighbor.get('properties', {}).get('name', 'Unknown')\n",
    "        labels = neighbor.get('labels', [])\n",
    "        print(f\"  - {name} ({', '.join(labels)})\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Find shortest path between two nodes\n",
    "if tim_cook.get('id') and cupertino.get('id'):\n",
    "    path = store.shortest_path(\n",
    "        start_node_id=tim_cook[\"id\"],\n",
    "        end_node_id=cupertino[\"id\"],\n",
    "        max_depth=5\n",
    "    )\n",
    "    \n",
    "    if path:\n",
    "        print(f\"Shortest path found:\")\n",
    "        print(f\"  - Path length: {path.get('length')}\")\n",
    "        print(f\"  - Nodes in path: {len(path.get('nodes', []))}\")\n",
    "        print(f\"  - Relationships: {len(path.get('relationships', []))}\")\n",
    "    else:\n",
    "        print(\"No path found between the nodes\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 7: Update and Delete Operations\n",
    "\n",
    "### Updating Nodes\n",
    "\n",
    "You can update node properties using the `update_node` method.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Update node properties (merge mode - adds/updates properties)\n",
    "if tim_cook.get('id'):\n",
    "    updated = store.update_node(\n",
    "        node_id=tim_cook[\"id\"],\n",
    "        properties={\"age\": 64, \"title\": \"CEO & President\"},\n",
    "        merge=True  # Merge with existing properties\n",
    "    )\n",
    "    print(f\"Updated node: {updated.get('properties', {}).get('name')}\")\n",
    "    print(f\"  New age: {updated.get('properties', {}).get('age')}\")\n",
    "    print(f\"  New title: {updated.get('properties', {}).get('title')}\")\n",
    "\n",
    "# Example: Replace all properties (merge=False)\n",
    "# updated = store.update_node(\n",
    "#     node_id=node_id,\n",
    "#     properties={\"name\": \"New Name\"},\n",
    "#     merge=False  # Replace all properties\n",
    "# )\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 8: Delete Operations\n",
    "\n",
    "### Deleting Nodes and Relationships\n",
    "\n",
    "You can delete nodes and relationships when needed.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Delete a relationship\n",
    "if location_rel.get('id'):\n",
    "    deleted = store.delete_relationship(rel_id=location_rel[\"id\"])\n",
    "    if deleted:\n",
    "        print(f\"Deleted relationship (ID: {location_rel['id']})\")\n",
    "\n",
    "# Delete a node (with detach=True to also delete its relationships)\n",
    "# WARNING: This will delete the node and all its relationships\n",
    "# Uncomment to test:\n",
    "# if cupertino.get('id'):\n",
    "#     deleted = store.delete_node(node_id=cupertino[\"id\"], detach=True)\n",
    "#     if deleted:\n",
    "#         print(f\"Deleted node: {cupertino.get('properties', {}).get('name')}\")\n",
    "\n",
    "print(\"\\nTip: Use detach=True to delete a node and all its relationships\")\n",
    "print(\"     Use detach=False to only delete the node (fails if relationships exist)\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 9: Graph Statistics\n",
    "\n",
    "Get comprehensive statistics about your graph.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Get comprehensive graph statistics\n",
    "stats = store.get_stats()\n",
    "\n",
    "print(\"Graph Statistics:\")\n",
    "print(f\"  Total nodes: {stats.get('node_count', 'N/A')}\")\n",
    "print(f\"  Total relationships: {stats.get('relationship_count', 'N/A')}\")\n",
    "print(f\"\\nNode labels:\")\n",
    "for label, count in stats.get('label_counts', {}).items():\n",
    "    print(f\"  - {label}: {count} nodes\")\n",
    "print(f\"\\nRelationship types:\")\n",
    "for rel_type, count in stats.get('relationship_type_counts', {}).items():\n",
    "    print(f\"  - {rel_type}: {count} relationships\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 10: Convenience Functions\n",
    "\n",
    "The Graph Store module provides convenience functions for simpler, function-based operations.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Using convenience functions (alternative to class methods)\n",
    "from semantica.graph_store import (\n",
    "    create_node,\n",
    "    create_relationship,\n",
    "    get_nodes,\n",
    "    execute_query,\n",
    "    shortest_path\n",
    ")\n",
    "\n",
    "# These functions work with a default store instance\n",
    "# For this example, we'll continue using the store instance we created\n",
    "\n",
    "# Example: Using convenience functions\n",
    "# node = create_node(\n",
    "#     labels=[\"Person\"],\n",
    "#     properties={\"name\": \"Alice\", \"age\": 30}\n",
    "# )\n",
    "\n",
    "print(\"Convenience functions available:\")\n",
    "print(\"  - create_node, create_nodes\")\n",
    "print(\"  - create_relationship, create_relationships\")\n",
    "print(\"  - get_nodes, get_relationships\")\n",
    "print(\"  - update_node, delete_node\")\n",
    "print(\"  - execute_query, shortest_path, get_neighbors\")\n",
    "print(\"  - run_analytics\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 11: Index Management\n",
    "\n",
    "Create indexes to improve query performance, especially for large graphs.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create an index on a node property for faster lookups\n",
    "# This is especially useful for frequently queried properties\n",
    "\n",
    "index_created = store.create_index(\n",
    "    label=\"Company\",\n",
    "    property_name=\"name\",\n",
    "    index_type=\"btree\"  # Default index type\n",
    ")\n",
    "\n",
    "if index_created:\n",
    "    print(\"Created index on Company.name for faster queries\")\n",
    "else:\n",
    "    print(\"Index may already exist or not be supported by this backend\")\n",
    "\n",
    "# Note: Index creation support varies by backend\n",
    "# Neo4j: Full support for various index types\n",
    "# FalkorDB: Limited index support\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 12: Clean Up\n",
    "\n",
    "Always close the connection when you're done to free up resources.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Close the connection\n",
    "store.close()\n",
    "print(\"Connection closed successfully\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "This notebook covered the Graph Store module, a unified interface for property graph databases supporting Neo4j and FalkorDB.\n",
    "\n",
    "### What You Learned\n",
    "\n",
    "- **CRUD Operations**: Create, read, update, and delete nodes and relationships\n",
    "- **Cypher Queries**: Execute complex graph queries with OpenCypher syntax\n",
    "- **Graph Analytics**: Shortest path, neighbor traversal, and centrality algorithms\n",
    "- **Batch Operations**: Efficient bulk data loading for large datasets\n",
    "- **Index Management**: Performance optimization through indexing\n",
    "\n",
    "### Key Takeaways\n",
    "\n",
    "- **Backend Selection**: Use Neo4j for production, FalkorDB for high-performance applications\n",
    "- **Best Practices**: Use batch operations, parameterized queries, and proper connection management\n",
    "- **Next Steps**: Explore advanced analytics, graph quality, and visualization modules\n"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
