├── .github └── workflows │ ├── docker-publish.yaml │ └── pypi-release.yaml ├── .gitignore ├── .readthedocs.yaml ├── Dockerfile ├── HACKING.md ├── LICENSE ├── README.md ├── docs ├── assets │ ├── nebulagraph.html │ └── nebulagraph_schema.html ├── build.md ├── cheatsheet.md ├── configurations.md ├── get_started.ipynb ├── get_started_docs.ipynb ├── images │ ├── favicon.png │ ├── nebula_jupyter_logo_dark.png │ ├── nebulagraph_jupyter_ext_logo.png │ └── nebulagraph_jupyter_ext_logo_dark.png ├── index.md ├── installation.md ├── magic_words │ ├── ng_draw.md │ ├── ng_draw_schema.md │ ├── ng_load.md │ └── ngql.md ├── overrides │ └── main.html └── requirements.txt ├── examples ├── actor.csv ├── follow.csv ├── follow_with_rank.csv └── shareholding.ipynb ├── mkdocs.yml ├── ngql ├── __init__.py ├── magic.py ├── ng_load.py ├── types.py └── utils.py ├── requirements.txt ├── setup.py └── setup_ipython.py /.github/workflows/docker-publish.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Release Publish 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | 10 | docker: 11 | name: build docker image 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Set up QEMU 15 | uses: docker/setup-qemu-action@v1 16 | - name: Set up Docker Buildx 17 | uses: docker/setup-buildx-action@v1 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | - name: Docker meta 21 | id: meta 22 | uses: docker/metadata-action@v4 23 | with: 24 | images: | 25 | weygu/nebulagraph-jupyter 26 | tags: | 27 | # git tag & latest coverred 28 | type=ref,event=tag 29 | # git branch 30 | type=ref,event=branch 31 | # v3.0.0 32 | type=semver,pattern=v{{version}} 33 | # v3 34 | type=semver,pattern=v{{major}} 35 | # v3.0 36 | type=semver,pattern=v{{major}}.{{minor}} 37 | - name: Log into registry 38 | uses: docker/login-action@v2 39 | with: 40 | username: ${{ secrets.DOCKER_USERNAME }} 41 | password: ${{ secrets.DOCKER_PASSWORD }} 42 | - name: Build and push Docker images 43 | uses: docker/build-push-action@v3 44 | with: 45 | platforms: linux/amd64,linux/arm64 46 | push: true 47 | tags: ${{ steps.meta.outputs.tags }} 48 | labels: ${{ steps.meta.outputs.labels }} 49 | -------------------------------------------------------------------------------- /.github/workflows/pypi-release.yaml: -------------------------------------------------------------------------------- 1 | name: Upload Python Packages 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | deploy-legacy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python for legacy package 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: '3.x' 19 | - name: Install dependencies for legacy package 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install build 23 | - name: Build legacy package 24 | run: | 25 | mv setup_ipython.py setup.py 26 | python -m build 27 | - name: Publish legacy package 28 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 29 | with: 30 | user: __token__ 31 | password: ${{ secrets.PYPI_API_TOKEN }} 32 | 33 | deploy-new: 34 | needs: deploy-legacy 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | - name: Set up Python for new package 39 | uses: actions/setup-python@v5 40 | with: 41 | python-version: '3.x' 42 | - name: Install dependencies for new package 43 | run: | 44 | python -m pip install --upgrade pip 45 | pip install build 46 | - name: Build new package 47 | run: python -m build 48 | - name: Publish new package 49 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 50 | with: 51 | user: __token__ 52 | password: ${{ secrets.PYPI_API_TOKEN }} 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build 2 | build 3 | dist 4 | *.egg-info 5 | */__pycache__ 6 | 7 | # examples 8 | examples/*.html 9 | */.ipynb_checkpoints/* 10 | 11 | # macOS 12 | .DS_Store 13 | 14 | # mkdocs 15 | .cache 16 | 17 | # python 18 | .venv 19 | .ipynb_checkpoints/ 20 | Untitled.ipynb 21 | 22 | *.html 23 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the OS, Python version and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | mkdocs: 15 | configuration: mkdocs.yml 16 | 17 | # Optionally declare the Python requirements required to build your docs 18 | python: 19 | install: 20 | - requirements: docs/requirements.txt 21 | 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 AS builder 2 | 3 | COPY . /tmp/nebula-jupyter 4 | WORKDIR /tmp/nebula-jupyter 5 | RUN python setup.py bdist_wheel 6 | 7 | FROM jupyter/minimal-notebook:python-3.9.13 8 | COPY --from=builder /tmp/nebula-jupyter/dist/*.whl /tmp/ 9 | RUN pip install --no-cache-dir /tmp/*.whl pyvis && \ 10 | rm -rf /home/$NB_USER/.cache/pip 11 | 12 | ENV JUPYTER_TOKEN=nebula 13 | 14 | # docker buildx build --push --platform linux/arm64/v8,linux/amd64 --tag weygu/nebulagraph-jupyter:0.7.3 . 15 | # docker buildx build --push --platform linux/arm64/v8,linux/amd64 --tag weygu/nebulagraph-jupyter:latest . 16 | -------------------------------------------------------------------------------- /HACKING.md: -------------------------------------------------------------------------------- 1 | ## Reference 2 | 3 | About ipython/Jupyter Notebook: 4 | 5 | - https://ipython.readthedocs.io/en/stable/config/custommagics.html 6 | - https://ipython.readthedocs.io/en/stable/development/how_ipython_works.html 7 | 8 | About nebula3-python 9 | 10 | - https://github.com/vesoft-inc/nebula-python 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 vesoft inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/wey-gu/jupyter_nebulagraph/assets/1651790/61fccff2-c9be-43a0-a26f-6f5a00b1c198) 2 | 3 | 4 | [![for NebulaGraph](https://img.shields.io/badge/Toolchain-NebulaGraph-blue)](https://github.com/vesoft-inc/nebula) [![Jupyter](https://img.shields.io/badge/Jupyter-Supported-brightgreen)](https://github.com/jupyterlab/jupyterlab) [![Docker Image](https://img.shields.io/docker/v/weygu/nebulagraph-jupyter?label=Image&logo=docker)](https://hub.docker.com/r/weygu/nebulagraph-jupyter) [![Docker Extension](https://img.shields.io/badge/Docker-Extension-blue?logo=docker)](https://hub.docker.com/extensions/weygu/nebulagraph-dd-ext) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/wey-gu/jupyter_nebulagraph?label=Version)](https://github.com/wey-gu/jupyter_nebulagraph/releases) 5 | [![pypi-version](https://img.shields.io/pypi/v/jupyter_nebulagraph)](https://pypi.org/project/jupyter_nebulagraph/) 6 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/wey-gu/jupyter_nebulagraph/blob/main/docs/get_started.ipynb) 7 | [![Documentation](https://img.shields.io/badge/docs-Read%20The%20Docs-blue)](https://jupyter-nebulagraph.readthedocs.io/) 8 | 9 | 10 | https://github.com/wey-gu/jupyter_nebulagraph/assets/1651790/10135264-77b5-4d3c-b68f-c5810257feeb 11 | 12 | `jupyter_nebulagraph`, formerly `ipython-ngql`, is a Python package that simplifies the process of connecting to NebulaGraph from Jupyter Notebooks or iPython environments. It enhances the user experience by streamlining the creation, debugging, and sharing of Jupyter Notebooks. With `jupyter_nebulagraph`, users can effortlessly connect to NebulaGraph, load data, execute queries, visualize results, and fine-tune query outputs, thereby boosting collaborative efforts and productivity. 13 | 14 | ![](https://github.com/wey-gu/jupyter_nebulagraph/assets/1651790/b3d9ca07-2eb1-45ae-949b-543f58a57760) 15 | 16 | ## Getting Started 17 | 18 | ```bash 19 | pip install jupyter_nebulagraph 20 | ``` 21 | 22 | Load the extension in Jupyter Notebook or iPython: 23 | 24 | ```python 25 | %load_ext ngql 26 | %ngql --address 127.0.0.1 --port 9669 --user root --password nebula 27 | ``` 28 | 29 | Make queries: 30 | 31 | ```python 32 | %ngql USE basketballplayer; 33 | %ngql MATCH p=(v:player)-->(v2:player) WHERE id(v) == "player100" RETURN p; 34 | ``` 35 | 36 | Draw the graph: 37 | 38 | ```python 39 | %ng_draw 40 | ``` 41 | 42 | Discover the features of `jupyter_nebulagraph` by experimenting with it on [Google Colab](https://colab.research.google.com/github/wey-gu/jupyter_nebulagraph/blob/main/docs/get_started.ipynb). You can also access a similar Jupyter Notebook in the documentation [here](https://jupyter-nebulagraph.readthedocs.io/en/stable/get_started_docs/). 43 | 44 | For a detailed guide, refer to the [official documentation](https://jupyter-nebulagraph.readthedocs.io/en/stable). 45 | 46 | | Feature | Cheat Sheet | Example | Command Documentation | 47 | | ------- | ----------- | --------- | ---------------------- | 48 | | Connect | `%ngql --address 127.0.0.1 --port 9669 --user user --password password` | [Connect](https://jupyter-nebulagraph.readthedocs.io/en/stable/get_started_docs/#connect-to-nebulagraph) | [`%ngql`](https://jupyter-nebulagraph.readthedocs.io/en/stable/magic_words/ngql/#connect-to-nebulagraph) | 49 | | Load Data from CSV | `%ng_load --source actor.csv --tag player --vid 0 --props 1:name,2:age --space basketballplayer` | [Load Data](https://jupyter-nebulagraph.readthedocs.io/en/stable/get_started_docs/#load-data-from-csv) | [`%ng_load`](https://jupyter-nebulagraph.readthedocs.io/en/stable/magic_words/ng_load/) | 50 | | Query Execution | `%ngql MATCH p=(v:player{name:"Tim Duncan"})-->(v2:player) RETURN p;`| [Query Execution](https://jupyter-nebulagraph.readthedocs.io/en/stable/get_started_docs/#query) | [`%ngql` or `%%ngql`(multi-line)](https://jupyter-nebulagraph.readthedocs.io/en/stable/magic_words/ngql/#make-queries) | 51 | | Result Visualization | `%ng_draw` | [Draw Graph](https://jupyter-nebulagraph.readthedocs.io/en/stable/magic_words/ng_draw/) | [`%ng_draw`](https://jupyter-nebulagraph.readthedocs.io/en/stable/magic_words/ng_draw/) | 52 | | Draw Schema | `%ng_draw_schema` | [Draw Schema](https://jupyter-nebulagraph.readthedocs.io/en/stable/magic_words/ng_draw_schema/) | [`%ng_draw_schema`](https://jupyter-nebulagraph.readthedocs.io/en/stable/magic_words/ng_draw_schema/) | 53 | | Tweak Query Result | `df = _` to get last query result as `pd.dataframe` or [`ResultSet`](https://github.com/vesoft-inc/nebula-python/blob/master/nebula3/data/ResultSet.py) | [Tweak Result](https://jupyter-nebulagraph.readthedocs.io/en/stable/get_started_docs/#result-handling) | [Configure `ngql_result_style`](https://jupyter-nebulagraph.readthedocs.io/en/stable/configurations/#configure-ngql_result_style) | 54 | 55 | 56 |
57 | Click to see more! 58 | 59 | ### Installation 60 | 61 | `jupyter_nebulagraph` could be installed either via pip or from this git repo itself. 62 | 63 | > Install via pip 64 | 65 | ```bash 66 | pip install jupyter_nebulagraph 67 | ``` 68 | 69 | > Install inside the repo 70 | 71 | ```bash 72 | git clone git@github.com:wey-gu/jupyter_nebulagraph.git 73 | cd jupyter_nebulagraph 74 | python setup.py install 75 | ``` 76 | 77 | ### Load it in Jupyter Notebook or iPython 78 | 79 | ```python 80 | %load_ext ngql 81 | ``` 82 | 83 | ### Connect to NebulaGraph 84 | 85 | Arguments as below are needed to connect a NebulaGraph DB instance: 86 | 87 | | Argument | Description | 88 | | ---------------------- | ---------------------------------------- | 89 | | `--address` or `-addr` | IP address of the NebulaGraph Instance | 90 | | `--port` or `-P` | Port number of the NebulaGraph Instance | 91 | | `--user` or `-u` | User name | 92 | | `--password` or `-p` | Password | 93 | 94 | Below is an exmple on connecting to `127.0.0.1:9669` with username: "user" and password: "password". 95 | 96 | ```python 97 | %ngql --address 127.0.0.1 --port 9669 --user user --password password 98 | ``` 99 | 100 | ### Make Queries 101 | 102 | Now two kind of iPtython Magics are supported: 103 | 104 | Option 1: The one line stype with `%ngql`: 105 | 106 | ```python 107 | %ngql USE basketballplayer; 108 | %ngql MATCH (v:player{name:"Tim Duncan"})-->(v2:player) RETURN v2.player.name AS Name; 109 | ``` 110 | 111 | Option 2: The multiple lines stype with `%%ngql ` 112 | 113 | ```python 114 | %%ngql 115 | SHOW TAGS; 116 | SHOW HOSTS; 117 | ``` 118 | 119 | ### Query String with Variables 120 | 121 | `jupyter_nebulagraph` supports taking variables from the local namespace, with the help of [Jinja2](https://jinja.palletsprojects.com/) template framework, it's supported to have queries like the below example. 122 | 123 | The actual query string should be `GO FROM "Sue" OVER owns_pokemon ...`, and `"{{ trainer }}"` was renderred as `"Sue"` by consuming the local variable `trainer`: 124 | 125 | ```python 126 | In [8]: vid = "player100" 127 | 128 | In [9]: %%ngql 129 | ...: MATCH (v)<-[e:follow]- (v2)-[e2:serve]->(v3) 130 | ...: WHERE id(v) == "{{ vid }}" 131 | ...: RETURN v2.player.name AS FriendOf, v3.team.name AS Team LIMIT 3; 132 | Out[9]: RETURN v2.player.name AS FriendOf, v3.team.name AS Team LIMIT 3; 133 | FriendOf Team 134 | 0 LaMarcus Aldridge Trail Blazers 135 | 1 LaMarcus Aldridge Spurs 136 | 2 Marco Belinelli Warriors 137 | ``` 138 | 139 | ### Draw query results 140 | 141 | **Draw Last Query** 142 | 143 | Just call `%ng_draw` after queries with graph data. 144 | 145 | ```python 146 | # one query 147 | %ngql GET SUBGRAPH 2 STEPS FROM "player101" YIELD VERTICES AS nodes, EDGES AS relationships; 148 | %ng_draw 149 | 150 | # another query 151 | %ngql match p=(:player)-[]->() return p LIMIT 5 152 | %ng_draw 153 | ``` 154 | 155 | ![](https://github.com/wey-gu/jupyter_nebulagraph/assets/1651790/b3d9ca07-2eb1-45ae-949b-543f58a57760) 156 | 157 | **Draw a Query** 158 | 159 | Or `%ng_draw `, `%%ng_draw ` instead of drawing the result of the last query. 160 | 161 | ng_draw_demo_1 162 | 163 | One line query: 164 | 165 | ```python 166 | %ng_draw GET SUBGRAPH 2 STEPS FROM "player101" YIELD VERTICES AS nodes, EDGES AS relationships; 167 | ``` 168 | 169 | Multiple lines query: 170 | 171 | ```python 172 | %%ng_draw 173 | MATCH path_0=(n)--() WHERE id(n) == "p_0" 174 | OPTIONAL MATCH path_1=(n)--()--() 175 | RETURN path_0, path_1 176 | ``` 177 | ### Draw Graph Schema 178 | 179 | ```python 180 | %ng_draw_schema 181 | ``` 182 | 183 | ![](https://github.com/wey-gu/jupyter_nebulagraph/assets/1651790/81fd71b5-61e7-4c65-93be-c2f4e507611b) 184 | 185 | ### Load Data from CSV 186 | 187 | It's supported to load data from a CSV file into NebulaGraph with the help of `ng_load_csv` magic. 188 | 189 | For example, to load data from a CSV file `actor.csv` into a space `basketballplayer` with tag `player` and vid in column `0`, and props in column `1` and `2`: 190 | 191 | ```csv 192 | "player999","Tom Hanks",30 193 | "player1000","Tom Cruise",40 194 | "player1001","Jimmy X",33 195 | ``` 196 | 197 | Just run the below line: 198 | 199 | ```python 200 | %ng_load --source actor.csv --tag player --vid 0 --props 1:name,2:age --space basketballplayer 201 | ``` 202 | 203 | Some other examples: 204 | 205 | ```python 206 | # load CSV from a URL 207 | %ng_load --source https://github.com/wey-gu/jupyter_nebulagraph/raw/main/examples/actor.csv --tag player --vid 0 --props 1:name,2:age --space demo_basketballplayer 208 | # with rank column 209 | %ng_load --source follow_with_rank.csv --edge follow --src 0 --dst 1 --props 2:degree --rank 3 --space basketballplayer 210 | # without rank column 211 | %ng_load --source follow.csv --edge follow --src 0 --dst 1 --props 2:degree --space basketballplayer 212 | ``` 213 | 214 | ### Tweak Query Result 215 | 216 | By default, the query result is a Pandas Dataframe, and we could access that by read from variable `_`. 217 | 218 | ```python 219 | In [1]: %ngql MATCH (v:player{name:"Tim Duncan"})-->(v2:player) RETURN v2.player.name AS Name; 220 | 221 | In [2]: df = _ 222 | ``` 223 | 224 | It's also configurable to have the result in raw ResultSet, to enable handy NebulaGraph Python App Development. 225 | 226 | See more via [Docs: Result Handling](https://jupyter-nebulagraph.readthedocs.io/en/stable/get_started_docs/#result-handling) 227 | 228 | ### CheatSheet 229 | 230 | If you find yourself forgetting commands or not wanting to rely solely on the cheat sheet, remember this one thing: seek help through the help command! 231 | 232 | ```python 233 | %ngql help 234 | ``` 235 | 236 |
237 | 238 | ## Acknowledgments ♥️ 239 | 240 | - Inspiration for this project comes from [ipython-sql](https://github.com/catherinedevlin/ipython-sql), courtesy of [Catherine Devlin](https://catherinedevlin.blogspot.com/). 241 | - Graph visualization features are enabled by [pyvis](https://github.com/WestHealth/pyvis), a project by [WestHealth](https://github.com/WestHealth). 242 | - Generous sponsorship and support provided by [Vesoft Inc.](https://www.vesoft.com/) and the [NebulaGraph community](https://github.com/vesoft-inc/nebula). 243 | -------------------------------------------------------------------------------- /docs/build.md: -------------------------------------------------------------------------------- 1 | ```bash 2 | pip3 install -r requirements.txt 3 | ``` 4 | 5 | ```bash 6 | mkdocs serve -a 0.0.0.0:8000 7 | ``` 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/cheatsheet.md: -------------------------------------------------------------------------------- 1 | ## `%ngql help` 2 | 3 | Don't remember anything but one command: 4 | 5 | ```bash 6 | %load_ext ngql 7 | ``` 8 | 9 | Then you can get help by: 10 | 11 | ```bash 12 | %ngql help 13 | ``` 14 | 15 | All the magic commands and examples are there for you to copy and paste. 16 | 17 | ## `%ngql` 18 | 19 | **Connect & oneliner query** 20 | 21 | ```python 22 | %ngql --address 127.0.0.1 --port 9669 --user root --password nebula 23 | %ngql USE demo_basketballplayer; 24 | %ngql MATCH (v:player{name:"Tim Duncan"})-->(v2:player) RETURN v2.player.name AS Name; 25 | ``` 26 | 27 | See more from [magic_words/ngql](magic_words/ngql.md). 28 | 29 | ## `%%ngql` 30 | 31 | **Multiple lines query** 32 | 33 | ```python 34 | %%ngql 35 | CREATE TAG player(name string, age int); 36 | CREATE EDGE follow(degree int); 37 | ``` 38 | 39 | See more from [magic_words/ngql](magic_words/ngql.md). 40 | 41 | ## `%ngql_load` 42 | 43 | **Load data from CSV** 44 | 45 | `%ng_load --source [--header] --space [--tag ] [--vid ] [--edge ] [--src ] [--dst ] [--rank ] [--props ] [-b ]` 46 | 47 | ```python 48 | # load CSV from a URL 49 | %ng_load --source https://github.com/wey-gu/jupyter_nebulagraph/raw/main/examples/actor.csv --tag player --vid 0 --props 1:name,2:age --space demo_basketballplayer 50 | # with rank column 51 | %ng_load --source follow_with_rank.csv --edge follow --src 0 --dst 1 --props 2:degree --rank 3 --space basketballplayer 52 | # without rank column 53 | %ng_load --source follow.csv --edge follow --src 0 --dst 1 --props 2:degree --space basketballplayer 54 | ``` 55 | 56 | See more from [magic_words/ng_load](magic_words/ng_load.md). 57 | 58 | ## `%ngql_draw` 59 | 60 | **Draw Last Query Result** 61 | 62 | ```python 63 | # one query 64 | %ngql GET SUBGRAPH 2 STEPS FROM "player101" YIELD VERTICES AS nodes, EDGES AS relationships; 65 | %ng_draw 66 | 67 | # another query 68 | %ngql match p=(:player)-[]->() return p LIMIT 5 69 | %ng_draw 70 | ``` 71 | 72 | **Draw a Query** 73 | 74 | Or `%ng_draw `, `%%ng_draw ` instead of drawing the result of the last query. 75 | 76 | One line query: 77 | 78 | ```python 79 | %ng_draw GET SUBGRAPH 2 STEPS FROM "player101" YIELD VERTICES AS nodes, EDGES AS relationships; 80 | ``` 81 | 82 | Multiple lines query: 83 | 84 | ```python 85 | %%ng_draw 86 | MATCH path_0=(n)--() WHERE id(n) == "p_0" 87 | OPTIONAL MATCH path_1=(n)--()--() 88 | RETURN path_0, path_1 89 | ``` 90 | 91 | See more from [magic_words/ng_draw](magic_words/ng_draw.md). 92 | 93 | ## `%ngql_draw_schema` 94 | 95 | **Draw Graph Schema** 96 | 97 | > Note: This call assumes there are data in the graph per each edge type. 98 | 99 | ```python 100 | %ng_draw_schema 101 | ``` 102 | 103 | See more from [magic_words/ng_draw_schema](magic_words/ng_draw_schema.md). -------------------------------------------------------------------------------- /docs/configurations.md: -------------------------------------------------------------------------------- 1 | 2 | ## Configure `ngql_result_style` 3 | 4 | By default, `jupyter_nebulagraph` will use pandas dataframe as output style to enable more human-readable output, while it's supported to use the raw thrift data format that comes from the `nebula3-python` itself. 5 | 6 | This can be done ad-hoc with below one line: 7 | 8 | ```python 9 | %config IPythonNGQL.ngql_result_style="raw" 10 | ``` 11 | 12 | After the above line is executed, the output will be like this: 13 | 14 | ```python 15 | ResultSet(ExecutionResponse( 16 | error_code=0, 17 | latency_in_us=2844, 18 | data=DataSet( 19 | column_names=[b'Trainer_Name'], 20 | rows=[Row( 21 | values=[Value( 22 | sVal=b'Tom')]), 23 | ... 24 | Row( 25 | values=[Value( 26 | sVal=b'Wey')])]), 27 | space_name=b'pokemon_club')) 28 | ``` 29 | 30 | The result are always stored in variable `_` in Jupyter Notebook, thus, to tweak the result, just refer a new var to it like: 31 | 32 | ```python 33 | In [1] : %config IPythonNGQL.ngql_result_style="raw" 34 | 35 | In [2] : %%ngql USE pokemon_club; 36 | ...: GO FROM "Tom" OVER owns_pokemon YIELD owns_pokemon._dst as pokemon_id 37 | ...: | GO FROM $-.pokemon_id OVER owns_pokemon REVERSELY YIELD owns_pokemon._dst AS Trainer_Name; 38 | ...: 39 | ...: 40 | Out[3]: 41 | ResultSet(ExecutionResponse( 42 | error_code=0, 43 | latency_in_us=3270, 44 | data=DataSet( 45 | column_names=[b'Trainer_Name'], 46 | rows=[Row( 47 | values=[Value( 48 | sVal=b'Tom')]), 49 | ... 50 | Row( 51 | values=[Value( 52 | sVal=b'Wey')])]), 53 | space_name=b'pokemon_club')) 54 | 55 | In [4]: r = _ 56 | 57 | In [5]: r.column_values(key='Trainer_Name')[0].cast() 58 | Out[5]: 'Tom' 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/get_started_docs.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "1dcefb88", 6 | "metadata": { 7 | "colab_type": "text", 8 | "id": "view-in-github" 9 | }, 10 | "source": [ 11 | "\"Open" 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "id": "favorite-value", 17 | "metadata": { 18 | "id": "favorite-value" 19 | }, 20 | "source": [ 21 | "This guide will help you walk through end to end process of tweaking NebulaGraph within Jupyter Notebook." 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "id": "0d54dc6d-a87c-4290-8101-a058f04118d9", 27 | "metadata": { 28 | "jp-MarkdownHeadingCollapsed": true 29 | }, 30 | "source": [ 31 | "### Prerequirements" 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "id": "m7GeBbhBJXvM", 37 | "metadata": { 38 | "id": "m7GeBbhBJXvM" 39 | }, 40 | "source": [ 41 | "We need to have a running NebulaGraph Cluster. If you don't have one, you could leverage [NebulaGraph-Lite](https://github.com/wey-gu/nebulagraph-lite/) to do spawn an ad-hoc cluster, for more options please refer to [NebulaGraph Docs](https://docs.nebula-graph.io).\n", 42 | "\n", 43 | "> See also here for more NebulaGraph installation options: [NebulaGraph Installation Options](https://jupyter-nebulagraph.readthedocs.io/en/latest/installation/#nebulagraph-installation-options) from jupyter-nebulagraph documentation." 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": null, 49 | "id": "DhZ5wSL7Jg9S", 50 | "metadata": { 51 | "id": "DhZ5wSL7Jg9S" 52 | }, 53 | "outputs": [], 54 | "source": [ 55 | "%pip install nebulagraph-lite" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": null, 61 | "id": "OdHPvUUMJ1k4", 62 | "metadata": { 63 | "colab": { 64 | "base_uri": "https://localhost:8080/" 65 | }, 66 | "id": "OdHPvUUMJ1k4", 67 | "outputId": "4acc2d7c-6b85-4146-9e5b-23b5d7a4274f" 68 | }, 69 | "outputs": [ 70 | { 71 | "name": "stdout", 72 | "output_type": "stream", 73 | "text": [ 74 | "\u001b[1;30;43mStreaming output truncated to the last 5000 lines.\u001b[0m\n", 75 | "...var/lib/rpm/__db.002\n", 76 | "var/lib/rpm/__db.003\n", 77 | "2ccae830-d547-3d25-8d86-c8e24b20d62e\n", 78 | "Debug: using curl executable \n", 79 | "Debug: Localrepo homedir is /home/user/.udocker\n", 80 | "Debug: using curl executable \n", 81 | "Debug: already installed, installation skipped\n", 82 | "\u001b[1;3;38;2;102;81;145mSHOW TAGS: ResultSet(None)\u001b[0m\n", 83 | "Info: downloading layer sha256:73dde089847b2e7be0b3e12a438aa50dab9d29587a3f37512daf74271d6f7eb1\n", 84 | "Info: downloading layer sha256:8a5d5aed99ca3dd1343afcb47ea7136fa6350a36a31c12ebc62a6a92ddf1c7ef\n", 85 | "Info: downloading layer sha256:19bc7f3f0d802b7e8dc89786cfa0e18ab81bc501d90e1d24720d470e4e213c03\n", 86 | "Info: downloading layer sha256:7264a8db6415046d36d16ba98b79778e18accee6ffa71850405994cffa9be7de\n", 87 | "\u001b[1;3;38;2;160;81;149mInfo: loading basketballplayer dataset...\u001b[0m\n", 88 | "\u001b[1;3;38;2;212;80;135m\n", 89 | " _ _ _ _ ____ _ \n", 90 | " | \\ | | ___| |__ _ _| | __ _ / ___|_ __ __ _ _ __ | |__ \n", 91 | " | \\| |/ _ | '_ \\| | | | |/ _` | | _| '__/ _` | '_ \\| '_ \\ \n", 92 | " | |\\ | __| |_) | |_| | | (_| | |_| | | | (_| | |_) | | | |\n", 93 | " |_| \\_|\\___|_.__/ \\__,_|_|\\__,_|\\____|_| \\__,_| .__/|_| |_|\n", 94 | " |_| \n", 95 | " lite version\n", 96 | "\u001b[0m\n", 97 | "\u001b[1;3;38;2;102;81;145m[ OK ] nebulagraph_lite started successfully!\u001b[0m\n", 98 | "CONTAINER ID P M NAMES IMAGE \n", 99 | "cac33b42-6851-316a-b32c-96641cb38492 . W ['nebula-metad'] vesoft/nebula-metad:v3\n", 100 | "2ccae830-d547-3d25-8d86-c8e24b20d62e . W ['nebula-storaged'] vesoft/nebula-storaged:v3\n", 101 | "2887f6d9-a872-3e17-b5cf-8ef4a7eb6e37 . W ['nebula-graphd'] vesoft/nebula-graphd:v3\n" 102 | ] 103 | } 104 | ], 105 | "source": [ 106 | "from nebulagraph_lite import nebulagraph_let as ng_let\n", 107 | "\n", 108 | "n = ng_let()\n", 109 | "\n", 110 | "# This takes around 5 mins\n", 111 | "n.start()" 112 | ] 113 | }, 114 | { 115 | "cell_type": "markdown", 116 | "id": "british-cream", 117 | "metadata": { 118 | "id": "british-cream" 119 | }, 120 | "source": [ 121 | "### Installation\n", 122 | "\n", 123 | "First, install with pip:\n", 124 | "\n", 125 | "```bash\n", 126 | "%pip install jupyter_nebulagraph\n", 127 | "```\n", 128 | "\n", 129 | "Second, load extension:\n", 130 | "\n", 131 | "```bash\n", 132 | "$load_ext ngql\n", 133 | "```" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": null, 139 | "id": "70634402-5058-4577-8655-99ec7694eaed", 140 | "metadata": {}, 141 | "outputs": [], 142 | "source": [ 143 | "%pip install jupyter_nebulagraph" 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": 1, 149 | "id": "biological-summary", 150 | "metadata": { 151 | "colab": { 152 | "base_uri": "https://localhost:8080/" 153 | }, 154 | "id": "biological-summary", 155 | "outputId": "6e92d22e-fb9d-4d05-eace-c2e729ad274d" 156 | }, 157 | "outputs": [], 158 | "source": [ 159 | "%load_ext ngql" 160 | ] 161 | }, 162 | { 163 | "cell_type": "markdown", 164 | "id": "embedded-olive", 165 | "metadata": { 166 | "id": "embedded-olive" 167 | }, 168 | "source": [ 169 | "### Connect to NebulaGraph\n", 170 | "\n", 171 | "With:\n", 172 | "\n", 173 | "```bash\n", 174 | "%ngql --address --port --user --password \n", 175 | "```\n", 176 | "\n", 177 | "By default, spaces of the cluster will be printed." 178 | ] 179 | }, 180 | { 181 | "cell_type": "code", 182 | "execution_count": 2, 183 | "id": "nasty-angel", 184 | "metadata": { 185 | "colab": { 186 | "base_uri": "https://localhost:8080/", 187 | "height": 99 188 | }, 189 | "id": "nasty-angel", 190 | "outputId": "e7781f17-cb40-4487-d305-fb6ab16d83de" 191 | }, 192 | "outputs": [ 193 | { 194 | "name": "stdout", 195 | "output_type": "stream", 196 | "text": [ 197 | "Connection Pool Created\n" 198 | ] 199 | }, 200 | { 201 | "data": { 202 | "text/html": [ 203 | "
\n", 204 | "\n", 217 | "\n", 218 | " \n", 219 | " \n", 220 | " \n", 221 | " \n", 222 | " \n", 223 | " \n", 224 | " \n", 225 | " \n", 226 | " \n", 227 | " \n", 228 | " \n", 229 | " \n", 230 | " \n", 231 | " \n", 232 | " \n", 233 | " \n", 234 | " \n", 235 | " \n", 236 | " \n", 237 | " \n", 238 | " \n", 239 | " \n", 240 | " \n", 241 | " \n", 242 | "
Name
0demo_basketballplayer
1freebase_15k
2nba
3news
\n", 243 | "
" 244 | ], 245 | "text/plain": [ 246 | " Name\n", 247 | "0 demo_basketballplayer\n", 248 | "1 freebase_15k\n", 249 | "2 nba\n", 250 | "3 news" 251 | ] 252 | }, 253 | "execution_count": 2, 254 | "metadata": {}, 255 | "output_type": "execute_result" 256 | } 257 | ], 258 | "source": [ 259 | "%ngql --address 127.0.0.1 --port 9669 --user root --password nebula" 260 | ] 261 | }, 262 | { 263 | "cell_type": "markdown", 264 | "id": "8e97e917-7d57-4c44-8ae0-bfca59752032", 265 | "metadata": {}, 266 | "source": [ 267 | "## Query\n", 268 | "\n", 269 | "Then we could make a query after `Connection Pool Created` shown in the connection from last step:\n", 270 | "\n", 271 | "\n", 272 | "### Oneliner Query `%ngql`\n", 273 | "\n", 274 | "Option 1, it supports one line query as:\n", 275 | "```ngql\n", 276 | "%ngql ;\n", 277 | "```" 278 | ] 279 | }, 280 | { 281 | "cell_type": "code", 282 | "execution_count": 17, 283 | "id": "copyrighted-aberdeen", 284 | "metadata": { 285 | "colab": { 286 | "base_uri": "https://localhost:8080/", 287 | "height": 112 288 | }, 289 | "id": "copyrighted-aberdeen", 290 | "outputId": "dce6f075-417e-4fc6-fedd-d78f29aea19d" 291 | }, 292 | "outputs": [ 293 | { 294 | "data": { 295 | "text/html": [ 296 | "\n", 297 | "
\n", 298 | "
\n", 299 | "\n", 312 | "\n", 313 | " \n", 314 | " \n", 315 | " \n", 316 | " \n", 317 | " \n", 318 | " \n", 319 | " \n", 320 | " \n", 321 | " \n", 322 | " \n", 323 | " \n", 324 | " \n", 325 | " \n", 326 | " \n", 327 | " \n", 328 | " \n", 329 | "
Name
0Tony Parker
1Manu Ginobili
\n", 330 | "
\n", 331 | "
\n", 332 | "\n", 333 | "
\n", 334 | " \n", 342 | "\n", 343 | " \n", 383 | "\n", 384 | " \n", 408 | "
\n", 409 | "\n", 410 | "\n", 411 | "
\n", 412 | " \n", 423 | "\n", 424 | "\n", 513 | "\n", 514 | " \n", 536 | "
\n", 537 | "\n", 538 | "
\n", 539 | "
\n" 540 | ], 541 | "text/plain": [ 542 | " Name\n", 543 | "0 Tony Parker\n", 544 | "1 Manu Ginobili" 545 | ] 546 | }, 547 | "execution_count": 17, 548 | "metadata": {}, 549 | "output_type": "execute_result" 550 | } 551 | ], 552 | "source": [ 553 | "%ngql USE basketballplayer;\n", 554 | "%ngql MATCH (v:player{name:\"Tim Duncan\"})-->(v2:player) RETURN v2.player.name AS Name;" 555 | ] 556 | }, 557 | { 558 | "cell_type": "markdown", 559 | "id": "necessary-performance", 560 | "metadata": { 561 | "id": "necessary-performance" 562 | }, 563 | "source": [ 564 | "### Multiline Query `%%ngql`\n", 565 | "\n", 566 | "Option 2, to perform multiple queries in one go.\n", 567 | "\n", 568 | "```ngql\n", 569 | "%%ngql\n", 570 | ";\n", 571 | ";\n", 572 | "```" 573 | ] 574 | }, 575 | { 576 | "cell_type": "code", 577 | "execution_count": 4, 578 | "id": "northern-anatomy", 579 | "metadata": { 580 | "colab": { 581 | "base_uri": "https://localhost:8080/", 582 | "height": 143 583 | }, 584 | "id": "northern-anatomy", 585 | "outputId": "6b757289-223d-4e51-d4f3-b18718265872" 586 | }, 587 | "outputs": [ 588 | { 589 | "data": { 590 | "text/html": [ 591 | "
\n", 592 | "\n", 605 | "\n", 606 | " \n", 607 | " \n", 608 | " \n", 609 | " \n", 610 | " \n", 611 | " \n", 612 | " \n", 613 | " \n", 614 | " \n", 615 | " \n", 616 | " \n", 617 | " \n", 618 | " \n", 619 | " \n", 620 | " \n", 621 | " \n", 622 | " \n", 623 | " \n", 624 | " \n", 625 | " \n", 626 | " \n", 627 | " \n", 628 | " \n", 629 | " \n", 630 | " \n", 631 | " \n", 632 | " \n", 633 | " \n", 634 | " \n", 635 | " \n", 636 | " \n", 637 | " \n", 638 | " \n", 639 | " \n", 640 | " \n", 641 | " \n", 642 | " \n", 643 | " \n", 644 | " \n", 645 | " \n", 646 | " \n", 647 | " \n", 648 | " \n", 649 | " \n", 650 | " \n", 651 | " \n", 652 | "
TypeNameCount
0Tagplayer54
1Tagteam30
2Edgefollow82
3Edgeserve146
4Spacevertices84
5Spaceedges228
\n", 653 | "
" 654 | ], 655 | "text/plain": [ 656 | " Type Name Count\n", 657 | "0 Tag player 54\n", 658 | "1 Tag team 30\n", 659 | "2 Edge follow 82\n", 660 | "3 Edge serve 146\n", 661 | "4 Space vertices 84\n", 662 | "5 Space edges 228" 663 | ] 664 | }, 665 | "execution_count": 4, 666 | "metadata": {}, 667 | "output_type": "execute_result" 668 | } 669 | ], 670 | "source": [ 671 | "%%ngql\n", 672 | "USE basketballplayer;\n", 673 | "SUBMIT JOB STATS;\n", 674 | "SHOW STATS;" 675 | ] 676 | }, 677 | { 678 | "cell_type": "markdown", 679 | "id": "7235d052-f095-4c1c-80c8-b0aba7b693b8", 680 | "metadata": {}, 681 | "source": [ 682 | "### Cheatsheet" 683 | ] 684 | }, 685 | { 686 | "cell_type": "markdown", 687 | "id": "apart-questionnaire", 688 | "metadata": { 689 | "id": "apart-questionnaire" 690 | }, 691 | "source": [ 692 | "The only takeout should be:\n", 693 | "\n", 694 | "You could always get help from `%ngql help` for some details of supported magics && examples you could copy from.\n", 695 | "\n", 696 | "```ngql\n", 697 | "%ngql help\n", 698 | "```" 699 | ] 700 | }, 701 | { 702 | "cell_type": "markdown", 703 | "id": "oriental-vault", 704 | "metadata": { 705 | "id": "oriental-vault" 706 | }, 707 | "source": [ 708 | "### Using Variables in Query String\n", 709 | "\n", 710 | "We used Jinja2(https://jinja.palletsprojects.com/) as templating method for variables in query string:\n", 711 | "\n", 712 | "```python\n", 713 | "trainer = \"Sue\"\n", 714 | "```\n", 715 | "\n", 716 | "```ngql\n", 717 | "%%ngql\n", 718 | "GO FROM \"{{ trainer }}\" OVER owns_pokemon YIELD owns_pokemon._dst as pokemon_id | GO FROM $-.pokemon_id OVER owns_pokemon REVERSELY YIELD owns_pokemon._dst AS Trainer_Name;\n", 719 | "```" 720 | ] 721 | }, 722 | { 723 | "cell_type": "code", 724 | "execution_count": 5, 725 | "id": "disciplinary-sustainability", 726 | "metadata": { 727 | "id": "disciplinary-sustainability" 728 | }, 729 | "outputs": [], 730 | "source": [ 731 | "vid = \"player100\"" 732 | ] 733 | }, 734 | { 735 | "cell_type": "code", 736 | "execution_count": 6, 737 | "id": "bizarre-absorption", 738 | "metadata": { 739 | "colab": { 740 | "base_uri": "https://localhost:8080/", 741 | "height": 143 742 | }, 743 | "id": "bizarre-absorption", 744 | "outputId": "bfe52aaa-e049-490f-f068-c0cfa73d4d9d" 745 | }, 746 | "outputs": [ 747 | { 748 | "data": { 749 | "text/html": [ 750 | "
\n", 751 | "\n", 764 | "\n", 765 | " \n", 766 | " \n", 767 | " \n", 768 | " \n", 769 | " \n", 770 | " \n", 771 | " \n", 772 | " \n", 773 | " \n", 774 | " \n", 775 | " \n", 776 | " \n", 777 | " \n", 778 | " \n", 779 | " \n", 780 | " \n", 781 | " \n", 782 | " \n", 783 | " \n", 784 | " \n", 785 | " \n", 786 | " \n", 787 | " \n", 788 | " \n", 789 | "
FriendOfTeam
0Boris DiawSpurs
1Boris DiawJazz
2Boris DiawSuns
\n", 790 | "
" 791 | ], 792 | "text/plain": [ 793 | " FriendOf Team\n", 794 | "0 Boris Diaw Spurs\n", 795 | "1 Boris Diaw Jazz\n", 796 | "2 Boris Diaw Suns" 797 | ] 798 | }, 799 | "execution_count": 6, 800 | "metadata": {}, 801 | "output_type": "execute_result" 802 | } 803 | ], 804 | "source": [ 805 | "%%ngql\n", 806 | "MATCH (v)<-[e:follow]- (v2)-[e2:serve]->(v3)\n", 807 | " WHERE id(v) == \"{{ vid }}\"\n", 808 | "RETURN v2.player.name AS FriendOf, v3.team.name AS Team LIMIT 3;" 809 | ] 810 | }, 811 | { 812 | "cell_type": "markdown", 813 | "id": "e7ac3264-b8f4-407d-977c-221af74c5d0f", 814 | "metadata": {}, 815 | "source": [ 816 | "## Result Handling\n", 817 | "\n", 818 | "By default, the query result is a Pandas Dataframe, and we could access that by read from variable `_`.\n", 819 | "\n", 820 | "### Dataframe Result(default)\n", 821 | "\n", 822 | "For instance:" 823 | ] 824 | }, 825 | { 826 | "cell_type": "code", 827 | "execution_count": 7, 828 | "id": "23a4d66a-db0e-4e31-a93e-eb4a6198b7b8", 829 | "metadata": {}, 830 | "outputs": [], 831 | "source": [ 832 | "df = _" 833 | ] 834 | }, 835 | { 836 | "cell_type": "code", 837 | "execution_count": 8, 838 | "id": "deda4471-f260-42ec-9abb-7f8d3994f139", 839 | "metadata": {}, 840 | "outputs": [ 841 | { 842 | "data": { 843 | "text/html": [ 844 | "
\n", 845 | "\n", 858 | "\n", 859 | " \n", 860 | " \n", 861 | " \n", 862 | " \n", 863 | " \n", 864 | " \n", 865 | " \n", 866 | " \n", 867 | " \n", 868 | " \n", 869 | " \n", 870 | " \n", 871 | " \n", 872 | " \n", 873 | " \n", 874 | " \n", 875 | " \n", 876 | " \n", 877 | " \n", 878 | " \n", 879 | " \n", 880 | " \n", 881 | " \n", 882 | " \n", 883 | "
FriendOfTeam
0Boris DiawSpurs
1Boris DiawJazz
2Boris DiawSuns
\n", 884 | "
" 885 | ], 886 | "text/plain": [ 887 | " FriendOf Team\n", 888 | "0 Boris Diaw Spurs\n", 889 | "1 Boris Diaw Jazz\n", 890 | "2 Boris Diaw Suns" 891 | ] 892 | }, 893 | "execution_count": 8, 894 | "metadata": {}, 895 | "output_type": "execute_result" 896 | } 897 | ], 898 | "source": [ 899 | "df" 900 | ] 901 | }, 902 | { 903 | "cell_type": "markdown", 904 | "id": "motivated-pantyhose", 905 | "metadata": { 906 | "id": "motivated-pantyhose" 907 | }, 908 | "source": [ 909 | "### Tweaking Raw Result(Optional)\n", 910 | "\n", 911 | "By default the result `ngql_result_style` is `pandas`, this enabled us to have a table view rendered by Jupyter Notebook.\n", 912 | "\n", 913 | "While, if you would like to get raw results from `neutron3-python` itself, just configure it as below on the fly:\n", 914 | "\n", 915 | "```\n", 916 | "%config IPythonNGQL.ngql_result_style=\"raw\"\n", 917 | "```\n", 918 | "\n", 919 | "And after querying, the result will be stored in `_`, plesae then refer it to a new variable for further ad-hoc tweaking on it like:\n", 920 | "```\n", 921 | "$ngql ;\n", 922 | "\n", 923 | "result = _\n", 924 | "\n", 925 | "dir(result)\n", 926 | "```" 927 | ] 928 | }, 929 | { 930 | "cell_type": "code", 931 | "execution_count": 9, 932 | "id": "designed-civilian", 933 | "metadata": { 934 | "id": "designed-civilian" 935 | }, 936 | "outputs": [], 937 | "source": [ 938 | "%config IPythonNGQL.ngql_result_style=\"raw\"" 939 | ] 940 | }, 941 | { 942 | "cell_type": "code", 943 | "execution_count": 11, 944 | "id": "quiet-armor", 945 | "metadata": { 946 | "colab": { 947 | "base_uri": "https://localhost:8080/" 948 | }, 949 | "id": "quiet-armor", 950 | "outputId": "be9dd4fd-ae87-40fd-f402-3f9df53798bf" 951 | }, 952 | "outputs": [ 953 | { 954 | "data": { 955 | "text/plain": [ 956 | "ResultSet(keys: ['dst(EDGE)'], values: [\"player100\"],[\"player102\"],[\"player125\"],[\"player101\"],[\"player125\"])" 957 | ] 958 | }, 959 | "execution_count": 11, 960 | "metadata": {}, 961 | "output_type": "execute_result" 962 | } 963 | ], 964 | "source": [ 965 | "%%ngql\n", 966 | "USE demo_basketballplayer;\n", 967 | "GO 2 STEPS FROM \"player102\" OVER follow YIELD dst(edge);" 968 | ] 969 | }, 970 | { 971 | "cell_type": "code", 972 | "execution_count": 12, 973 | "id": "rising-strip", 974 | "metadata": { 975 | "id": "rising-strip" 976 | }, 977 | "outputs": [], 978 | "source": [ 979 | "r = _" 980 | ] 981 | }, 982 | { 983 | "cell_type": "code", 984 | "execution_count": 13, 985 | "id": "attractive-steel", 986 | "metadata": { 987 | "colab": { 988 | "base_uri": "https://localhost:8080/", 989 | "height": 35 990 | }, 991 | "id": "attractive-steel", 992 | "outputId": "7e863ba2-18f1-4a3c-bce8-407993fb757c" 993 | }, 994 | "outputs": [ 995 | { 996 | "data": { 997 | "text/plain": [ 998 | "'player100'" 999 | ] 1000 | }, 1001 | "execution_count": 13, 1002 | "metadata": {}, 1003 | "output_type": "execute_result" 1004 | } 1005 | ], 1006 | "source": [ 1007 | "r.column_values(\"dst(EDGE)\")[0].cast()" 1008 | ] 1009 | }, 1010 | { 1011 | "cell_type": "markdown", 1012 | "id": "located-freeze", 1013 | "metadata": { 1014 | "id": "located-freeze" 1015 | }, 1016 | "source": [ 1017 | "Now we change back to `pandas` `ngql_result_style`" 1018 | ] 1019 | }, 1020 | { 1021 | "cell_type": "code", 1022 | "execution_count": 14, 1023 | "id": "characteristic-preliminary", 1024 | "metadata": { 1025 | "id": "characteristic-preliminary" 1026 | }, 1027 | "outputs": [], 1028 | "source": [ 1029 | "%config IPythonNGQL.ngql_result_style=\"pandas\"" 1030 | ] 1031 | }, 1032 | { 1033 | "cell_type": "code", 1034 | "execution_count": 16, 1035 | "id": "commercial-orchestra", 1036 | "metadata": { 1037 | "colab": { 1038 | "base_uri": "https://localhost:8080/", 1039 | "height": 143 1040 | }, 1041 | "id": "commercial-orchestra", 1042 | "outputId": "c84ef119-7c66-4bfa-add6-1ee77756817b" 1043 | }, 1044 | "outputs": [ 1045 | { 1046 | "data": { 1047 | "text/html": [ 1048 | "
\n", 1049 | "\n", 1062 | "\n", 1063 | " \n", 1064 | " \n", 1065 | " \n", 1066 | " \n", 1067 | " \n", 1068 | " \n", 1069 | " \n", 1070 | " \n", 1071 | " \n", 1072 | " \n", 1073 | " \n", 1074 | " \n", 1075 | " \n", 1076 | " \n", 1077 | " \n", 1078 | " \n", 1079 | " \n", 1080 | " \n", 1081 | " \n", 1082 | " \n", 1083 | " \n", 1084 | " \n", 1085 | " \n", 1086 | " \n", 1087 | " \n", 1088 | " \n", 1089 | " \n", 1090 | " \n", 1091 | "
team_namestart_yearplayer_name
0Spurs1997Tim Duncan
1Trail Blazers2006LaMarcus Aldridge
2Spurs2015LaMarcus Aldridge
\n", 1092 | "
" 1093 | ], 1094 | "text/plain": [ 1095 | " team_name start_year player_name\n", 1096 | "0 Spurs 1997 Tim Duncan\n", 1097 | "1 Trail Blazers 2006 LaMarcus Aldridge\n", 1098 | "2 Spurs 2015 LaMarcus Aldridge" 1099 | ] 1100 | }, 1101 | "execution_count": 16, 1102 | "metadata": {}, 1103 | "output_type": "execute_result" 1104 | } 1105 | ], 1106 | "source": [ 1107 | "%%ngql\n", 1108 | "GO FROM \"player100\", \"player102\" OVER serve\n", 1109 | " WHERE properties(edge).start_year > 1995\n", 1110 | "YIELD DISTINCT properties($$).name AS team_name, properties(edge).start_year AS start_year, properties($^).name AS player_name;" 1111 | ] 1112 | }, 1113 | { 1114 | "cell_type": "markdown", 1115 | "id": "5b91e20d-98f0-4470-abef-72a9b376c96c", 1116 | "metadata": {}, 1117 | "source": [ 1118 | "## Load Data from CSV\n", 1119 | "\n", 1120 | "Since 0.9.0, it is supported to load data into NebulaGraph with ease.\n", 1121 | "\n", 1122 | "We could load data from a local path or a URL:" 1123 | ] 1124 | }, 1125 | { 1126 | "cell_type": "code", 1127 | "execution_count": 18, 1128 | "id": "a4748070-8bb7-449a-b81d-900bb9368425", 1129 | "metadata": {}, 1130 | "outputs": [ 1131 | { 1132 | "name": "stdout", 1133 | "output_type": "stream", 1134 | "text": [ 1135 | "Parsed 3 vertices 'demo_basketballplayer' for tag 'player' in memory\n" 1136 | ] 1137 | }, 1138 | { 1139 | "data": { 1140 | "application/vnd.jupyter.widget-view+json": { 1141 | "model_id": "f690001c9b7c4475a1633b8bcfc2865e", 1142 | "version_major": 2, 1143 | "version_minor": 0 1144 | }, 1145 | "text/plain": [ 1146 | "Loading Vertices: 0%| | 0/1 [00:00 [--header] --space [--tag ] [--vid ] [--edge ] [--src ] [--dst ] [--rank ] [--props ] [-b ]\n", 1179 | "```\n", 1180 | "\n", 1181 | "#### Arguments\n", 1182 | "\n", 1183 | "- `--header`: (Optional) Indicates if the CSV file contains a header row. If this flag is set, the first row of the CSV will be treated as column headers.\n", 1184 | "- `-n`, `--space` (Required): Specifies the name of the NebulaGraph space where the data will be loaded.\n", 1185 | "- `-s`, `--source` (Required): The file path or URL to the CSV file. Supports both local paths and remote URLs.\n", 1186 | "- `-t`, `--tag`: The tag name for vertices. Required if loading vertex data.\n", 1187 | "- `--vid`: The column index for the vertex ID. Required if loading vertex data.\n", 1188 | "- `-e`, `--edge`: The edge type name. Required if loading edge data.\n", 1189 | "- `--src`: The column index for the source vertex ID when loading edges.\n", 1190 | "- `--dst`: The column index for the destination vertex ID when loading edges.\n", 1191 | "- `--rank`: (Optional) The column index for the rank value of edges. Default is None.\n", 1192 | "- `--props`: (Optional) Comma-separated column indexes for mapping to properties. The format for mapping is column_index:property_name.\n", 1193 | "- `-b`, `--batch` (Optional): Batch size for data loading. Default is 256.\n", 1194 | "\n", 1195 | "#### Examples\n", 1196 | "\n", 1197 | "Loading Vertices\n", 1198 | "\n", 1199 | "To load vertex data from a local CSV file named actor.csv into the basketballplayer space with the player tag, where the vertex ID is in the first column, and the properties name and age are in the second and third columns, respectively:" 1200 | ] 1201 | }, 1202 | { 1203 | "cell_type": "code", 1204 | "execution_count": 19, 1205 | "id": "c2e4195b-3b3f-46a2-a154-7642afb5ac45", 1206 | "metadata": {}, 1207 | "outputs": [ 1208 | { 1209 | "name": "stdout", 1210 | "output_type": "stream", 1211 | "text": [ 1212 | "Parsed 3 vertices 'demo_basketballplayer' for tag 'player' in memory\n" 1213 | ] 1214 | }, 1215 | { 1216 | "data": { 1217 | "application/vnd.jupyter.widget-view+json": { 1218 | "model_id": "dc3a99261e7b41d492b7ac055a898b2a", 1219 | "version_major": 2, 1220 | "version_minor": 0 1221 | }, 1222 | "text/plain": [ 1223 | "Loading Vertices: 0%| | 0/1 [00:00\n", 1304 | "\n", 1305 | "\"ng_draw_demo_1\"\n" 1306 | ] 1307 | }, 1308 | { 1309 | "cell_type": "markdown", 1310 | "id": "9b573f5a", 1311 | "metadata": {}, 1312 | "source": [ 1313 | "Or `%ng_draw `, `%%ng_draw ` instead of drawing the result of the last query.\n", 1314 | "\n", 1315 | "\n", 1316 | "\"ng_draw_demo_1\"\n" 1317 | ] 1318 | }, 1319 | { 1320 | "cell_type": "code", 1321 | "execution_count": null, 1322 | "id": "c911efa2", 1323 | "metadata": { 1324 | "colab": { 1325 | "base_uri": "https://localhost:8080/" 1326 | }, 1327 | "id": "c911efa2", 1328 | "outputId": "a99b212d-ab22-4be5-d432-3e6e526dc40a" 1329 | }, 1330 | "outputs": [], 1331 | "source": [ 1332 | "%pip install pyvis" 1333 | ] 1334 | }, 1335 | { 1336 | "cell_type": "code", 1337 | "execution_count": 21, 1338 | "id": "423e852e", 1339 | "metadata": { 1340 | "colab": { 1341 | "base_uri": "https://localhost:8080/", 1342 | "height": 206 1343 | }, 1344 | "id": "423e852e", 1345 | "outputId": "db1cf97c-ab27-4ad4-9d40-4f10b75dbf16" 1346 | }, 1347 | "outputs": [ 1348 | { 1349 | "data": { 1350 | "text/html": [ 1351 | "
\n", 1352 | "\n", 1365 | "\n", 1366 | " \n", 1367 | " \n", 1368 | " \n", 1369 | " \n", 1370 | " \n", 1371 | " \n", 1372 | " \n", 1373 | " \n", 1374 | " \n", 1375 | " \n", 1376 | " \n", 1377 | " \n", 1378 | " \n", 1379 | " \n", 1380 | " \n", 1381 | " \n", 1382 | " \n", 1383 | " \n", 1384 | " \n", 1385 | " \n", 1386 | " \n", 1387 | " \n", 1388 | " \n", 1389 | " \n", 1390 | " \n", 1391 | " \n", 1392 | " \n", 1393 | " \n", 1394 | "
p
0(\"player148\" :player{age: 45, name: \"Jason Kid...
1(\"player148\" :player{age: 45, name: \"Jason Kid...
2(\"player148\" :player{age: 45, name: \"Jason Kid...
3(\"player148\" :player{age: 45, name: \"Jason Kid...
4(\"player148\" :player{age: 45, name: \"Jason Kid...
\n", 1395 | "
" 1396 | ], 1397 | "text/plain": [ 1398 | " p\n", 1399 | "0 (\"player148\" :player{age: 45, name: \"Jason Kid...\n", 1400 | "1 (\"player148\" :player{age: 45, name: \"Jason Kid...\n", 1401 | "2 (\"player148\" :player{age: 45, name: \"Jason Kid...\n", 1402 | "3 (\"player148\" :player{age: 45, name: \"Jason Kid...\n", 1403 | "4 (\"player148\" :player{age: 45, name: \"Jason Kid..." 1404 | ] 1405 | }, 1406 | "execution_count": 21, 1407 | "metadata": {}, 1408 | "output_type": "execute_result" 1409 | } 1410 | ], 1411 | "source": [ 1412 | "%ngql match p=(:player)-[]->() return p LIMIT 5" 1413 | ] 1414 | }, 1415 | { 1416 | "cell_type": "code", 1417 | "execution_count": null, 1418 | "id": "69171089", 1419 | "metadata": { 1420 | "colab": { 1421 | "base_uri": "https://localhost:8080/", 1422 | "height": 551 1423 | }, 1424 | "id": "69171089", 1425 | "outputId": "806ee4da-49ab-4cab-ee69-7551486c1788" 1426 | }, 1427 | "outputs": [], 1428 | "source": [ 1429 | "##uncomment to draw\n", 1430 | "# %ng_draw" 1431 | ] 1432 | }, 1433 | { 1434 | "cell_type": "code", 1435 | "execution_count": 23, 1436 | "id": "84ac453e", 1437 | "metadata": { 1438 | "colab": { 1439 | "base_uri": "https://localhost:8080/", 1440 | "height": 143 1441 | }, 1442 | "id": "84ac453e", 1443 | "outputId": "0d377247-5e1f-473a-84e0-6e077e461bae" 1444 | }, 1445 | "outputs": [ 1446 | { 1447 | "data": { 1448 | "text/html": [ 1449 | "
\n", 1450 | "\n", 1463 | "\n", 1464 | " \n", 1465 | " \n", 1466 | " \n", 1467 | " \n", 1468 | " \n", 1469 | " \n", 1470 | " \n", 1471 | " \n", 1472 | " \n", 1473 | " \n", 1474 | " \n", 1475 | " \n", 1476 | " \n", 1477 | " \n", 1478 | " \n", 1479 | " \n", 1480 | " \n", 1481 | " \n", 1482 | " \n", 1483 | " \n", 1484 | " \n", 1485 | " \n", 1486 | " \n", 1487 | " \n", 1488 | "
nodesrelationships
0[(\"player101\" :player{})][(\"player101\")-[:serve@0{}]->(\"team204\"), (\"pl...
1[(\"player102\" :player{}), (\"player100\" :player...[(\"player102\")-[:serve@0{}]->(\"team203\"), (\"pl...
2[(\"player144\" :player{}), (\"player112\" :player...[(\"player144\")-[:serve@0{}]->(\"team214\"), (\"pl...
\n", 1489 | "
" 1490 | ], 1491 | "text/plain": [ 1492 | " nodes \\\n", 1493 | "0 [(\"player101\" :player{})] \n", 1494 | "1 [(\"player102\" :player{}), (\"player100\" :player... \n", 1495 | "2 [(\"player144\" :player{}), (\"player112\" :player... \n", 1496 | "\n", 1497 | " relationships \n", 1498 | "0 [(\"player101\")-[:serve@0{}]->(\"team204\"), (\"pl... \n", 1499 | "1 [(\"player102\")-[:serve@0{}]->(\"team203\"), (\"pl... \n", 1500 | "2 [(\"player144\")-[:serve@0{}]->(\"team214\"), (\"pl... " 1501 | ] 1502 | }, 1503 | "execution_count": 23, 1504 | "metadata": {}, 1505 | "output_type": "execute_result" 1506 | } 1507 | ], 1508 | "source": [ 1509 | "%ngql GET SUBGRAPH 2 STEPS FROM \"player101\" YIELD VERTICES AS nodes, EDGES AS relationships;" 1510 | ] 1511 | }, 1512 | { 1513 | "cell_type": "code", 1514 | "execution_count": 24, 1515 | "id": "8d406b2d", 1516 | "metadata": { 1517 | "colab": { 1518 | "base_uri": "https://localhost:8080/", 1519 | "height": 551 1520 | }, 1521 | "id": "8d406b2d", 1522 | "outputId": "5bec7726-46b1-4c1e-94af-6603ca7370b9" 1523 | }, 1524 | "outputs": [ 1525 | { 1526 | "data": { 1527 | "text/html": [ 1528 | "\n", 1529 | " \n", 1537 | " " 1538 | ], 1539 | "text/plain": [ 1540 | "" 1541 | ] 1542 | }, 1543 | "metadata": {}, 1544 | "output_type": "display_data" 1545 | }, 1546 | { 1547 | "data": { 1548 | "text/plain": [ 1549 | " |N|=36 |E|=84" 1550 | ] 1551 | }, 1552 | "execution_count": 24, 1553 | "metadata": {}, 1554 | "output_type": "execute_result" 1555 | } 1556 | ], 1557 | "source": [ 1558 | "%ng_draw" 1559 | ] 1560 | }, 1561 | { 1562 | "cell_type": "markdown", 1563 | "id": "8f59b93d-1776-4a97-b318-20f74dd26458", 1564 | "metadata": { 1565 | "id": "8f59b93d-1776-4a97-b318-20f74dd26458" 1566 | }, 1567 | "source": [ 1568 | "## Draw Graph Schema `%ng_draw_schema`\n", 1569 | "\n", 1570 | "Also, we could quickly draw the schema with `%ng_draw_schema`, which samples all types of edges to show us what the graph looks like.\n", 1571 | "\n", 1572 | "This example comes from a dataset/space called demo_supplychain, to get those datasets named `demo_*`, you could install NebulaGraph Studio and click Download to have them ingested into NebulaGraph in one minute.\n", 1573 | "\n", 1574 | "\"ng_draw_schema_demo\"\n" 1575 | ] 1576 | }, 1577 | { 1578 | "cell_type": "code", 1579 | "execution_count": 25, 1580 | "id": "c8ab3493-37a5-42ea-80aa-30b9d88ffae8", 1581 | "metadata": { 1582 | "colab": { 1583 | "base_uri": "https://localhost:8080/", 1584 | "height": 53 1585 | }, 1586 | "id": "c8ab3493-37a5-42ea-80aa-30b9d88ffae8", 1587 | "outputId": "fab51f77-1560-4b4a-9bcf-a0cfd242c20b" 1588 | }, 1589 | "outputs": [ 1590 | { 1591 | "data": { 1592 | "text/html": [ 1593 | "
\n", 1594 | "\n", 1607 | "\n", 1608 | " \n", 1609 | " \n", 1610 | " \n", 1611 | " \n", 1612 | " \n", 1613 | " \n", 1614 | " \n", 1615 | "
\n", 1616 | "
" 1617 | ], 1618 | "text/plain": [ 1619 | "Empty DataFrame\n", 1620 | "Columns: []\n", 1621 | "Index: []" 1622 | ] 1623 | }, 1624 | "execution_count": 25, 1625 | "metadata": {}, 1626 | "output_type": "execute_result" 1627 | } 1628 | ], 1629 | "source": [ 1630 | "%ngql CREATE SPACE demo_supplychain(partition_num=1, replica_factor=1, vid_type=fixed_string(128));" 1631 | ] 1632 | }, 1633 | { 1634 | "cell_type": "code", 1635 | "execution_count": null, 1636 | "id": "4db6378d-f093-4e33-8fc4-99006a144ab9", 1637 | "metadata": {}, 1638 | "outputs": [], 1639 | "source": [ 1640 | "!sleep 10\n", 1641 | "%ngql USE demo_supplychain" 1642 | ] 1643 | }, 1644 | { 1645 | "cell_type": "code", 1646 | "execution_count": null, 1647 | "id": "9e718475-e0d3-4cea-bb96-3c245d8304fe", 1648 | "metadata": {}, 1649 | "outputs": [], 1650 | "source": [ 1651 | "%%ngql\n", 1652 | "\n", 1653 | "CREATE TAG IF NOT EXISTS car_model(name string, number string, year int, type string, engine_type string, size string, seats int);\n", 1654 | "CREATE TAG IF NOT EXISTS feature(name string, number string, type string, state string);\n", 1655 | "CREATE TAG IF NOT EXISTS `part`(name string, number string, price double, `date` string);\n", 1656 | "CREATE TAG IF NOT EXISTS supplier(name string, address string, contact string, phone_number string);\n", 1657 | "CREATE EDGE IF NOT EXISTS with_feature(version string);\n", 1658 | "CREATE EDGE IF NOT EXISTS is_composed_of(version string);\n", 1659 | "CREATE EDGE IF NOT EXISTS is_supplied_by(version string);" 1660 | ] 1661 | }, 1662 | { 1663 | "cell_type": "code", 1664 | "execution_count": null, 1665 | "id": "364e0e8d-0b67-460f-b78a-ebd2bb5d8f71", 1666 | "metadata": {}, 1667 | "outputs": [], 1668 | "source": [ 1669 | "!sleep 10" 1670 | ] 1671 | }, 1672 | { 1673 | "cell_type": "code", 1674 | "execution_count": null, 1675 | "id": "b68961c6-08bf-4d01-8185-2a5f96e7069e", 1676 | "metadata": {}, 1677 | "outputs": [], 1678 | "source": [ 1679 | "%%ngql\n", 1680 | "\n", 1681 | "INSERT VERTEX `car_model`(`name`, `number`, `year`, `type`, `engine_type`, `size`, `seats`) VALUES \"m_1\":(\"Model A\", \"001\", 2023, \"Sedan\", \"Gasoline\", \"Compact\", 4), \"m_2\":(\"Model B\", \"002\", 2023, \"Coupe\", \"Electric\", \"Compact\", 2), \"m_3\":(\"Model C\", \"003\", 2022, \"SUV\", \"Hybrid\", \"Large\", 7), \"m_4\":(\"Model D\", \"004\", 2022, \"Truck\", \"Diesel\", \"Extra Large\", 5), \"m_5\":(\"Model E\", \"005\", 2021, \"Sedan\", \"Electric\", \"Medium\", 5), \"m_6\":(\"Model F\", \"006\", 2021, \"Convertible\", \"Gasoline\", \"Compact\", 2), \"m_7\":(\"Model G\", \"007\", 2023, \"Crossover\", \"Hybrid\", \"Medium\", 5), \"m_8\":(\"Model H\", \"008\", 2020, \"Hatchback\", \"Electric\", \"Compact\", 4), \"m_9\":(\"Model I\", \"009\", 2022, \"Sedan\", \"Gasoline\", \"Large\", 5), \"m_10\":(\"Model J\", \"010\", 2021, \"SUV\", \"Hybrid\", \"Extra Large\", 7);\n", 1682 | "INSERT VERTEX `supplier`(`name`, `address`, `contact`, `phone_number`) VALUES \"s_31\":(\"Supplier A\", \"123 Street\", \"John Doe\", \"1234567890\"), \"s_32\":(\"Supplier B\", \"456 Avenue\", \"Emily Smith\", \"0987654321\"), \"s_33\":(\"Supplier C\", \"789 Boulevard\", \"Robert Brown\", \"1112233445\"), \"s_34\":(\"Supplier D\", \"101 Place\", \"Maria Johnson\", \"2223344556\"), \"s_35\":(\"Supplier E\", \"202 Drive\", \"Michael Williams\", \"3334455667\"), \"s_36\":(\"Supplier F\", \"303 Lane\", \"Susan Miller\", \"4445566778\"), \"s_37\":(\"Supplier G\", \"404 Road\", \"Chris Lee\", \"5556677889\"), \"s_38\":(\"Supplier H\", \"505 Street\", \"Jane Wilson\", \"6667788990\"), \"s_39\":(\"Supplier I\", \"606 Way\", \"Brian Anderson\", \"7778899001\"), \"s_40\":(\"Supplier J\", \"707 Avenue\", \"Linda Hall\", \"8889900112\");\n", 1683 | "INSERT VERTEX `feature`(`name`, `number`, `type`, `state`) VALUES \"f_11\":(\"Sunroof\", \"F001\", \"Optional\", \"Available\"), \"f_12\":(\"Bluetooth\", \"F002\", \"Standard\", \"Available\"), \"f_13\":(\"Navigation\", \"F003\", \"Optional\", \"N/A\"), \"f_14\":(\"Heated Seats\", \"F004\", \"Standard\", \"Available\"), \"f_15\":(\"Backup Camera\", \"F005\", \"Optional\", \"Available\"), \"f_16\":(\"Leather Seats\", \"F006\", \"Standard\", \"Available\"), \"f_17\":(\"Adaptive Cruise\", \"F007\", \"Optional\", \"Available\"), \"f_18\":(\"Blind Spot Monitor\", \"F008\", \"Standard\", \"Available\"), \"f_19\":(\"Remote Start\", \"F009\", \"Optional\", \"N/A\"), \"f_20\":(\"Apple CarPlay\", \"F010\", \"Standard\", \"Available\");\n", 1684 | "INSERT VERTEX `part`(`name`, `number`, `price`, `date`) VALUES \"p_21\":(\"Brake Pad\", \"P001\", 50, \"2023-01-01\"), \"p_22\":(\"Engine\", \"P002\", 2000, \"2023-05-03\"), \"p_23\":(\"Tire\", \"P003\", 100, \"2022-08-14\"), \"p_24\":(\"Transmission\", \"P004\", 1500, \"2022-02-20\"), \"p_25\":(\"Radiator\", \"P005\", 250, \"2022-06-15\"), \"p_26\":(\"Window Glass\", \"P006\", 60, \"2021-11-23\"), \"p_27\":(\"Battery\", \"P007\", 120, \"2023-03-09\"), \"p_28\":(\"Headlight\", \"P008\", 90, \"2023-07-30\"), \"p_29\":(\"Alternator\", \"P009\", 180, \"2022-09-04\"), \"p_30\":(\"Air Filter\", \"P010\", 20, \"2023-04-22\");\n", 1685 | "INSERT EDGE `with_feature`(`version`) VALUES \"m_1\"->\"f_12\":(\"1.0\"), \"m_2\"->\"f_13\":(\"1.0\"), \"m_3\"->\"f_14\":(\"1.1\"), \"m_4\"->\"f_15\":(\"1.2\"), \"m_5\"->\"f_11\":(\"1.0\"), \"m_6\"->\"f_12\":(\"1.0\"), \"m_7\"->\"f_13\":(\"1.0\"), \"m_8\"->\"f_14\":(\"1.0\"), \"m_9\"->\"f_15\":(\"1.0\"), \"m_10\"->\"f_11\":(\"1.0\"), \"m_2\"->\"f_12\":(\"1.0\"), \"m_3\"->\"f_13\":(\"1.1\"), \"m_4\"->\"f_14\":(\"1.2\"), \"m_5\"->\"f_15\":(\"1.0\"), \"m_6\"->\"f_11\":(\"1.0\"), \"m_7\"->\"f_12\":(\"1.0\"), \"m_8\"->\"f_13\":(\"1.0\"), \"m_9\"->\"f_14\":(\"1.0\"), \"m_10\"->\"f_15\":(\"1.0\"), \"m_1\"->\"f_11\":(\"1.0\"), \"m_2\"->\"f_12\":(\"1.2\"), \"m_3\"->\"f_13\":(\"1.1\"), \"m_4\"->\"f_12\":(\"1.0\"), \"m_5\"->\"f_15\":(\"1.3\"), \"m_6\"->\"f_11\":(\"1.2\"), \"m_7\"->\"f_14\":(\"1.0\"), \"m_8\"->\"f_13\":(\"1.1\"), \"m_9\"->\"f_15\":(\"1.2\"), \"m_10\"->\"f_12\":(\"1.1\"), \"m_1\"->\"f_13\":(\"1.3\"), \"m_2\"->\"f_14\":(\"1.0\"), \"m_3\"->\"f_11\":(\"1.1\"), \"m_4\"->\"f_14\":(\"1.0\"), \"m_5\"->\"f_15\":(\"1.2\"), \"m_6\"->\"f_13\":(\"1.0\"), \"m_7\"->\"f_12\":(\"1.1\"), \"m_8\"->\"f_15\":(\"1.1\"), \"m_9\"->\"f_11\":(\"1.2\"), \"m_10\"->\"f_14\":(\"1.3\"), \"m_2\"->\"f_11\":(\"1.0\"), \"m_3\"->\"f_12\":(\"1.1\"), \"m_5\"->\"f_14\":(\"1.0\"), \"m_6\"->\"f_15\":(\"1.1\"), \"m_8\"->\"f_12\":(\"1.2\"), \"m_9\"->\"f_13\":(\"1.0\"), \"m_1\"->\"f_15\":(\"1.2\"), \"m_7\"->\"f_13\":(\"1.3\"), \"m_4\"->\"f_11\":(\"1.0\"), \"m_10\"->\"f_15\":(\"1.1\");\n", 1686 | "INSERT EDGE `is_composed_of`(`version`) VALUES \"f_11\"->\"p_21\":(\"1.0\"), \"f_12\"->\"p_22\":(\"1.0\"), \"f_13\"->\"p_23\":(\"1.1\"), \"f_14\"->\"p_24\":(\"1.2\"), \"f_15\"->\"p_21\":(\"1.0\"), \"f_16\"->\"p_22\":(\"1.0\"), \"f_17\"->\"p_23\":(\"1.0\"), \"f_18\"->\"p_24\":(\"1.0\"), \"f_19\"->\"p_25\":(\"1.0\"), \"f_20\"->\"p_26\":(\"1.0\"), \"f_11\"->\"p_27\":(\"1.0\"), \"f_12\"->\"p_28\":(\"1.0\"), \"f_13\"->\"p_29\":(\"1.1\"), \"f_14\"->\"p_30\":(\"1.2\"), \"f_15\"->\"p_21\":(\"1.0\"), \"f_16\"->\"p_22\":(\"1.0\"), \"f_17\"->\"p_23\":(\"1.0\"), \"f_18\"->\"p_24\":(\"1.0\"), \"f_19\"->\"p_25\":(\"1.0\"), \"f_20\"->\"p_26\":(\"1.0\");\n", 1687 | "INSERT EDGE `is_supplied_by`(`version`) VALUES \"p_21\"->\"s_31\":(\"1.0\"), \"p_22\"->\"s_32\":(\"1.0\"), \"p_23\"->\"s_33\":(\"1.1\"), \"p_24\"->\"s_34\":(\"1.2\"), \"p_25\"->\"s_35\":(\"1.0\"), \"p_26\"->\"s_36\":(\"1.0\"), \"p_27\"->\"s_37\":(\"1.0\"), \"p_28\"->\"s_38\":(\"1.0\"), \"p_29\"->\"s_39\":(\"1.1\"), \"p_30\"->\"s_40\":(\"1.2\"), \"p_21\"->\"s_31\":(\"1.0\"), \"p_22\"->\"s_32\":(\"1.0\"), \"p_23\"->\"s_33\":(\"1.1\"), \"p_24\"->\"s_34\":(\"1.2\"), \"p_25\"->\"s_35\":(\"1.0\"), \"p_26\"->\"s_36\":(\"1.0\"), \"p_27\"->\"s_37\":(\"1.0\"), \"p_28\"->\"s_38\":(\"1.0\"), \"p_29\"->\"s_39\":(\"1.1\"), \"p_30\"->\"s_40\":(\"1.2\");" 1688 | ] 1689 | }, 1690 | { 1691 | "cell_type": "code", 1692 | "execution_count": 26, 1693 | "id": "1c71735c-a039-43af-b3c8-414323a21d75", 1694 | "metadata": { 1695 | "colab": { 1696 | "base_uri": "https://localhost:8080/", 1697 | "height": 551 1698 | }, 1699 | "id": "1c71735c-a039-43af-b3c8-414323a21d75", 1700 | "outputId": "a75c602c-a55e-4446-9a4e-3881433f9f5a" 1701 | }, 1702 | "outputs": [ 1703 | { 1704 | "data": { 1705 | "text/html": [ 1706 | "\n", 1707 | " \n", 1715 | " " 1716 | ], 1717 | "text/plain": [ 1718 | "" 1719 | ] 1720 | }, 1721 | "metadata": {}, 1722 | "output_type": "display_data" 1723 | }, 1724 | { 1725 | "data": { 1726 | "text/plain": [ 1727 | " |N|=4 |E|=3" 1728 | ] 1729 | }, 1730 | "execution_count": 26, 1731 | "metadata": {}, 1732 | "output_type": "execute_result" 1733 | } 1734 | ], 1735 | "source": [ 1736 | "%ng_draw_schema" 1737 | ] 1738 | }, 1739 | { 1740 | "cell_type": "markdown", 1741 | "id": "forbidden-carter", 1742 | "metadata": { 1743 | "id": "forbidden-carter" 1744 | }, 1745 | "source": [ 1746 | "## `% ngql help`!\n", 1747 | "\n", 1748 | "Again, all you have to remember is to use `$ngql help` to have all hints :-)" 1749 | ] 1750 | }, 1751 | { 1752 | "cell_type": "code", 1753 | "execution_count": 27, 1754 | "id": "5a67dfdf", 1755 | "metadata": { 1756 | "colab": { 1757 | "base_uri": "https://localhost:8080/" 1758 | }, 1759 | "id": "5a67dfdf", 1760 | "outputId": "276bacba-c855-42ee-8248-0a990a828db4" 1761 | }, 1762 | "outputs": [ 1763 | { 1764 | "name": "stdout", 1765 | "output_type": "stream", 1766 | "text": [ 1767 | "\n", 1768 | "\n", 1769 | " Supported Configurations:\n", 1770 | " ------------------------\n", 1771 | " \n", 1772 | " > How to config ngql_result_style in \"raw\", \"pandas\"\n", 1773 | " %config IPythonNGQL.ngql_result_style=\"raw\"\n", 1774 | " %config IPythonNGQL.ngql_result_style=\"pandas\"\n", 1775 | "\n", 1776 | " > How to config ngql_verbose in True, False\n", 1777 | " %config IPythonNGQL.ngql_verbose=True\n", 1778 | "\n", 1779 | " > How to config max_connection_pool_size\n", 1780 | " %config IPythonNGQL.max_connection_pool_size=10\n", 1781 | "\n", 1782 | " Quick Start:\n", 1783 | " -----------\n", 1784 | "\n", 1785 | " > Connect to Neubla Graph\n", 1786 | " %ngql --address 127.0.0.1 --port 9669 --user user --password password\n", 1787 | "\n", 1788 | " > Use Space\n", 1789 | " %ngql USE basketballplayer\n", 1790 | "\n", 1791 | " > Query\n", 1792 | " %ngql SHOW TAGS;\n", 1793 | "\n", 1794 | " > Multile Queries\n", 1795 | " %%ngql\n", 1796 | " SHOW TAGS;\n", 1797 | " SHOW HOSTS;\n", 1798 | "\n", 1799 | " Reload ngql Magic\n", 1800 | " %reload_ext ngql\n", 1801 | "\n", 1802 | " > Variables in query, we are using Jinja2 here\n", 1803 | " name = \"nba\"\n", 1804 | " %ngql USE \"{{ name }}\"\n", 1805 | "\n", 1806 | " > Query and draw the graph\n", 1807 | "\n", 1808 | " %ngql GET SUBGRAPH 2 STEPS FROM \"player101\" YIELD VERTICES AS nodes, EDGES AS relationships;\n", 1809 | "\n", 1810 | " %ng_draw\n", 1811 | "\n", 1812 | " > Query and draw the graph schema\n", 1813 | "\n", 1814 | " %ng_draw_schema\n", 1815 | "\n", 1816 | " > Load data from CSV file into NebulaGraph as vertices or edges\n", 1817 | " %ng_load --source actor.csv --tag player --vid 0 --props 1:name,2:age --space basketballplayer\n", 1818 | "\n", 1819 | " #actor.csv\n", 1820 | " \"player999\",\"Tom Hanks\",30\n", 1821 | " \"player1000\",\"Tom Cruise\",40\n", 1822 | "\n", 1823 | " %ng_load --source follow_with_rank.csv --edge follow --src 0 --dst 1 --props 2:degree --rank 3 --space basketballplayer\n", 1824 | "\n", 1825 | " #follow_with_rank.csv\n", 1826 | " \"player999\",\"player1000\",50,1\n", 1827 | "\n", 1828 | " %ng_load --source follow.csv --edge follow --src 0 --dst 1 --props 2:degree --space basketballplayer\n", 1829 | "\n", 1830 | " #follow.csv\n", 1831 | " \"player999\",\"player1000\",50\n", 1832 | "\n", 1833 | " %ng_load --source https://github.com/wey-gu/ipython-ngql/raw/main/examples/actor.csv --tag player --vid 0 --props 1:name,2:age --space demo_basketballplayer -b 2\n", 1834 | "\n", 1835 | "\n", 1836 | " \n" 1837 | ] 1838 | } 1839 | ], 1840 | "source": [ 1841 | "%ngql help" 1842 | ] 1843 | }, 1844 | { 1845 | "cell_type": "code", 1846 | "execution_count": null, 1847 | "id": "69f72765-480c-4c7d-b5b4-9ccd26dbffb1", 1848 | "metadata": {}, 1849 | "outputs": [], 1850 | "source": [] 1851 | } 1852 | ], 1853 | "metadata": { 1854 | "colab": { 1855 | "include_colab_link": true, 1856 | "provenance": [] 1857 | }, 1858 | "kernelspec": { 1859 | "display_name": "Python 3 (ipykernel)", 1860 | "language": "python", 1861 | "name": "python3" 1862 | }, 1863 | "language_info": { 1864 | "codemirror_mode": { 1865 | "name": "ipython", 1866 | "version": 3 1867 | }, 1868 | "file_extension": ".py", 1869 | "mimetype": "text/x-python", 1870 | "name": "python", 1871 | "nbconvert_exporter": "python", 1872 | "pygments_lexer": "ipython3", 1873 | "version": "3.12.2" 1874 | } 1875 | }, 1876 | "nbformat": 4, 1877 | "nbformat_minor": 5 1878 | } 1879 | -------------------------------------------------------------------------------- /docs/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wey-gu/jupyter_nebulagraph/f17465b0e0dc63879c3c484d6094ed4c6cbd4542/docs/images/favicon.png -------------------------------------------------------------------------------- /docs/images/nebula_jupyter_logo_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wey-gu/jupyter_nebulagraph/f17465b0e0dc63879c3c484d6094ed4c6cbd4542/docs/images/nebula_jupyter_logo_dark.png -------------------------------------------------------------------------------- /docs/images/nebulagraph_jupyter_ext_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wey-gu/jupyter_nebulagraph/f17465b0e0dc63879c3c484d6094ed4c6cbd4542/docs/images/nebulagraph_jupyter_ext_logo.png -------------------------------------------------------------------------------- /docs/images/nebulagraph_jupyter_ext_logo_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wey-gu/jupyter_nebulagraph/f17465b0e0dc63879c3c484d6094ed4c6cbd4542/docs/images/nebulagraph_jupyter_ext_logo_dark.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | [![for NebulaGraph](https://img.shields.io/badge/Toolchain-NebulaGraph-blue)](https://github.com/vesoft-inc/nebula) [![Docker Image](https://img.shields.io/docker/v/weygu/nebulagraph-jupyter?label=Image&logo=docker)](https://hub.docker.com/r/weygu/nebulagraph-jupyter) [![Docker Extension](https://img.shields.io/badge/Docker-Extension-blue?logo=docker)](https://hub.docker.com/extensions/weygu/nebulagraph-dd-ext) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/wey-gu/jupyter_nebulagraph?label=Version)](https://github.com/wey-gu/jupyter_nebulagraph/releases) 3 | [![pypi-version](https://img.shields.io/pypi/v/jupyter_nebulagraph)](https://pypi.org/project/jupyter_nebulagraph/) 4 | 5 | ## Google Colab 6 | 7 | Experience it directly on Google Colab: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/wey-gu/jupyter_nebulagraph/blob/main/docs/get_started.ipynb) 8 | 9 | ## Introduction 10 | 11 | `jupyter_nebulagraph`, formerly `ipython-ngql`, is a Python package that simplifies the process of connecting to NebulaGraph from Jupyter Notebooks or iPython environments. It enhances the user experience by streamlining the creation, debugging, and sharing of Jupyter Notebooks. With `jupyter_nebulagraph`, users can effortlessly connect to NebulaGraph, load data, execute queries, visualize results, and fine-tune query outputs, thereby boosting collaborative efforts and productivity. 12 | 13 | 17 | 18 | 19 | 20 | ## Get Started 21 | 22 | For a more comprehensive guide on how to get started with `jupyter_nebulagraph`, please refer to the [Get Started Guide](/get_started_docs). This guide provides step-by-step instructions on installation, connecting to NebulaGraph, making queries, and visualizing query results, making it easier for new users to get up and running. 23 | 24 | ## Features 25 | 26 | | Feature | Cheat Sheet | Example | Command Documentation | 27 | | ------- | ----------- | --------- | ---------------------- | 28 | | Connect | `%ngql --address 127.0.0.1 --port 9669 --user user --password password` | [Connect](https://jupyter-nebulagraph.readthedocs.io/en/stable/get_started_docs/#connect-to-nebulagraph) | [`%ngql`](https://jupyter-nebulagraph.readthedocs.io/en/stable/magic_words/ngql/#connect-to-nebulagraph) | 29 | | Load Data from CSV or Parquet | `%ng_load --source actor.csv --tag player --vid 0 --props 1:name,2:age --space basketballplayer` | [Load Data](https://jupyter-nebulagraph.readthedocs.io/en/stable/get_started_docs/#load-data-from-csv) | [`%ng_load`](https://jupyter-nebulagraph.readthedocs.io/en/stable/magic_words/ng_load/) | 30 | | Query Execution | `%ngql MATCH p=(v:player{name:"Tim Duncan"})-->(v2:player) RETURN p;`| [Query Execution](https://jupyter-nebulagraph.readthedocs.io/en/stable/get_started_docs/#query) | [`%ngql` or `%%ngql`(multi-line)](https://jupyter-nebulagraph.readthedocs.io/en/stable/magic_words/ngql/#make-queries) | 31 | | Result Visualization | `%ng_draw` | [Draw Graph](https://jupyter-nebulagraph.readthedocs.io/en/stable/magic_words/ng_draw/) | [`%ng_draw`](https://jupyter-nebulagraph.readthedocs.io/en/stable/magic_words/ng_draw/) | 32 | | Draw Schema | `%ng_draw_schema` | [Draw Schema](https://jupyter-nebulagraph.readthedocs.io/en/stable/magic_words/ng_draw_schema/) | [`%ng_draw_schema`](https://jupyter-nebulagraph.readthedocs.io/en/stable/magic_words/ng_draw_schema/) | 33 | | Tweak Query Result | `df = _` to get last query result as `pd.dataframe` or [`ResultSet`](https://github.com/vesoft-inc/nebula-python/blob/master/nebula3/data/ResultSet.py) | [Tweak Result](https://jupyter-nebulagraph.readthedocs.io/en/stable/get_started_docs/#result-handling) | [Configure `ngql_result_style`](https://jupyter-nebulagraph.readthedocs.io/en/stable/configurations/#configure-ngql_result_style) | 34 | 35 | ## Acknowledgments ♥️ 36 | 37 | - Inspiration for this project comes from [ipython-sql](https://github.com/catherinedevlin/ipython-sql), courtesy of [Catherine Devlin](https://catherinedevlin.blogspot.com/). 38 | - Graph visualization features are enabled by [pyvis](https://github.com/WestHealth/pyvis), a project by [WestHealth](https://github.com/WestHealth). 39 | - Generous sponsorship and support provided by [Vesoft Inc.](https://www.vesoft.com/) and the [NebulaGraph community](https://github.com/vesoft-inc/nebula). 40 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | 2 | ## Installation 3 | 4 | `jupyter_nebulagraph` should be installed via pip. 5 | 6 | ```bash 7 | pip install jupyter_nebulagraph 8 | ``` 9 | 10 | > Note: if you are doing this in a Jupyter Notebook, you can use the `!` or `%` prefix to run shell commands directly in the notebook. 11 | 12 | ```bash 13 | %pip install jupyter_nebulagraph 14 | ``` 15 | 16 | ## Load it in Jupyter Notebook or iPython 17 | 18 | In each Jupyter Notebook or iPython environment, you need to load the extension before using it. 19 | 20 | ```python 21 | %load_ext ngql 22 | ``` 23 | 24 | ## Appendix 25 | 26 | ### NebulaGraph Installation Options 27 | 28 | But how to get a NebulaGraph instance to connect to? Here are some options: 29 | 30 | - [Documents](https://docs.nebula-graph.io/), to go through the official installation guide for NebulaGraph. 31 | - [Docker Compose](https://github.com/vesoft-inc/nebula-docker-compose), if you are comfortable to play with Docker on single server. 32 | - [NebulaGraph-Lite](https://github.com/nebula-contrib/nebulagraph-lite), install on Linux or Colab with `pip install` for ad-hoc playground. 33 | - [Docker Extension](https://github.com/nebula-contrib/nebulagraph-docker-ext), one-click on Docker Desktop(macOS, windows) on desktop machines, in GUI flavor, jupyter environment included. 34 | - [nebula-up](https://github.com/nebula-contrib/nebula-up), one-liner test env installer on single server, support studio, dashboard, nebulagraph algorithm, exchange etc, all-in-one. 35 | - [Nebula-Operator-KinD](https://github.com/nebula-contrib/nebula-operator-kind), Nebula K8s Operator with K8s-in-Docker, one-liner test env with docker+k8s+nebulagrpah-operator, try NebulaGraph on K8s with ease on your single server. 36 | -------------------------------------------------------------------------------- /docs/magic_words/ng_draw.md: -------------------------------------------------------------------------------- 1 | 2 | ## Draw query results 3 | 4 | **Draw Last Query** 5 | 6 | To render the graph, just call `%ng_draw` after queries with graph data. 7 | 8 | ```python 9 | # one query 10 | %ngql GET SUBGRAPH 2 STEPS FROM "player101" YIELD VERTICES AS nodes, EDGES AS relationships; 11 | %ng_draw 12 | 13 | # another query 14 | %ngql match p=(:player)-[]->() return p LIMIT 5 15 | %ng_draw 16 | ``` 17 | 18 | ng_draw_demo_0 19 | 20 | And the result will be displayed as below: 21 | 22 |
23 | 24 |
25 | 26 | **Draw a Query** 27 | 28 | Or `%ng_draw `, `%%ng_draw ` instead of drawing the result of the last query. 29 | 30 | ng_draw_demo_1 31 | 32 | One line query: 33 | 34 | ```python 35 | %ng_draw GET SUBGRAPH 2 STEPS FROM "player101" YIELD VERTICES AS nodes, EDGES AS relationships; 36 | ``` 37 | 38 | Multiple lines query: 39 | 40 | ```python 41 | %%ng_draw 42 | MATCH path_0=(n)--() WHERE id(n) == "p_0" 43 | OPTIONAL MATCH path_1=(n)--()--() 44 | RETURN path_0, path_1 45 | ``` -------------------------------------------------------------------------------- /docs/magic_words/ng_draw_schema.md: -------------------------------------------------------------------------------- 1 | ## Draw Graph Schema 2 | 3 | It's supported to draw the graph schema with the help of `ng_draw_schema` magic. 4 | 5 | ```python 6 | %ng_draw_schema 7 | ``` 8 | 9 | ![](https://github.com/wey-gu/jupyter_nebulagraph/assets/1651790/81fd71b5-61e7-4c65-93be-c2f4e507611b) 10 | 11 | And the result will be displayed as below: 12 | 13 |
14 | 15 |
16 | -------------------------------------------------------------------------------- /docs/magic_words/ng_load.md: -------------------------------------------------------------------------------- 1 | ## Load Data from CSV or Parquet file 2 | 3 | It's supported to load data from a CSV or Parquet file into NebulaGraph with the help of `ng_load` magic. 4 | 5 | ### Examples 6 | 7 | For example, to load data from a CSV file actor.csv into a space basketballplayer with tag player and vid in column 0, and props in column 1 and 2: 8 | 9 | ```csv 10 | player_id,name,age 11 | "player999","Tom Hanks",30 12 | "player1000","Tom Cruise",40 13 | "player1001","Jimmy X",33 14 | ``` 15 | 16 | Then the `%ng_load` line would be: 17 | 18 | ```ascii 19 | %ng_load --header --source actor.csv --tag player --vid 0 --props 1:name,2:age --space basketballplayer 20 | ────┬─── ────┬───────────── ─────┬────── ───┬─── ─────────┬────────── ────────────┬─────────── 21 | │ │ │ │ │ │ 22 | │ │ │ │ │ │ 23 | │ │ │ │ │ │ 24 | │ │ │ │ │ │ 25 | │ │ │ │ │ │ 26 | │ │ │ │ │ │ 27 | │ │ ┌────────────────┘ │ │ ┌────────────────┐ 28 | │ │ │ │ │ │Graph Space Name│ 29 | │ │ │ ┌──────────────┘ │ └────────────────┘ 30 | │ │ │ │ ┌──────────────────────────────────────────────────────────┐ 31 | │ │ │ │ │Properties on : if there are any.│ 32 | │ │ │ │ └──────────────────────────────────────────────────────────┘ 33 | │ │ │ ┌────────┴───────────────────────────────────────────────────────────────┐ 34 | │ │ │ │ For tag, there will be column index of VID│ 35 | │ │ │ │ For edge, there will be src/dst VID index, or optionally the rank index│ 36 | │ │ │ └────────────────────────────────────────────────────────────────────────┘ 37 | │ │ │ ┌───────────────────────┐ 38 | │ │ └────────────────────────────────────────────────────┤vertex tag or edge type│ 39 | │ │ └───────────────────────┘ 40 | │ │ ┌────────────────────────────┐ 41 | │ └──────────────────────────────────────────────────┤File to parse, a path or URL│ 42 | │ └────────────────────────────┘ 43 | │ ┌──────────────────────────────┐ 44 | └─────────────────────────────────────────────────────────┤With Header in Row:0, Optional│ 45 | └──────────────────────────────┘ 46 | ``` 47 | 48 | Some other examples: 49 | 50 | ```python 51 | # load CSV from a URL 52 | %ng_load --source https://github.com/wey-gu/jupyter_nebulagraph/raw/main/examples/actor.csv --tag player --vid 0 --props 1:name,2:age --space demo_basketballplayer 53 | # with rank column 54 | %ng_load --source follow_with_rank.csv --edge follow --src 0 --dst 1 --props 2:degree --rank 3 --space basketballplayer 55 | # without rank column 56 | %ng_load --source follow.csv --edge follow --src 0 --dst 1 --props 2:degree --space basketballplayer 57 | ``` 58 | 59 | ### Usage 60 | 61 | ```python 62 | %ng_load --source [--header] --space [--tag ] [--vid ] [--edge ] [--src ] [--dst ] [--rank ] [--props ] [-b ] [--limit ] 63 | ``` 64 | 65 | ### Arguments 66 | 67 | | Argument | Requirement | Description | 68 | |----------|-------------|-------------| 69 | | `--header` | Optional | Indicates if the CSV file contains a header row. If this flag is set, the first row of the CSV will be treated as column headers. | 70 | | `-n`, `--space` | Required | Specifies the name of the NebulaGraph space where the data will be loaded. | 71 | | `-s`, `--source` | Required | The file path or URL to the CSV file. Supports both local paths and remote URLs. | 72 | | `-t`, `--tag` | Optional | The tag name for vertices. Required if loading vertex data. | 73 | | `--vid` | Optional | The column index for the vertex ID. Required if loading vertex data. | 74 | | `-e`, `--edge` | Optional | The edge type name. Required if loading edge data. | 75 | | `--src` | Optional | The column index for the source vertex ID when loading edges. | 76 | | `--dst` | Optional | The column index for the destination vertex ID when loading edges. | 77 | | `--rank` | Optional | The column index for the rank value of edges. Default is None. | 78 | | `--props` | Optional | Comma-separated column indexes for mapping to properties. The format for mapping is column_index:property_name. | 79 | | `-b`, `--batch` | Optional | Batch size for data loading. Default is 256. | 80 | | `--limit` | Optional | The maximum number of rows to load. Default is -1(unlimited). | 81 | -------------------------------------------------------------------------------- /docs/magic_words/ngql.md: -------------------------------------------------------------------------------- 1 | 2 | ## Connect to NebulaGraph 3 | 4 | Arguments as below are needed to connect a NebulaGraph DB instance: 5 | 6 | | Argument | Description | 7 | | ---------------------- | ---------------------------------------- | 8 | | `--address` or `-addr` | IP address of the NebulaGraph Instance | 9 | | `--port` or `-P` | Port number of the NebulaGraph Instance | 10 | | `--user` or `-u` | User name | 11 | | `--password` or `-p` | Password | 12 | 13 | Below is an exmple on connecting to `127.0.0.1:9669` with username: "user" and password: "password". 14 | 15 | ```python 16 | %ngql --address 127.0.0.1 --port 9669 --user user --password password 17 | ``` 18 | 19 | ## Make Queries 20 | 21 | Now two kind of iPtython Magics are supported: 22 | 23 | Option 1: The one line stype with `%ngql`: 24 | 25 | ```python 26 | %ngql USE basketballplayer; 27 | %ngql MATCH (v:player{name:"Tim Duncan"})-->(v2:player) RETURN v2.player.name AS Name; 28 | ``` 29 | 30 | Option 2: The multiple lines stype with `%%ngql ` 31 | 32 | ```python 33 | %%ngql 34 | SHOW TAGS; 35 | SHOW HOSTS; 36 | ``` 37 | 38 | ## Query String with Variables 39 | 40 | `jupyter_nebulagraph` supports taking variables from the local namespace, with the help of [Jinja2](https://jinja.palletsprojects.com/) template framework, it's supported to have queries like the below example. 41 | 42 | The actual query string should be `GO FROM "Sue" OVER owns_pokemon ...`, and `"{{ trainer }}"` was renderred as `"Sue"` by consuming the local variable `trainer`: 43 | 44 | ```python 45 | In [8]: vid = "player100" 46 | 47 | In [9]: %%ngql 48 | ...: MATCH (v)<-[e:follow]- (v2)-[e2:serve]->(v3) 49 | ...: WHERE id(v) == "{{ vid }}" 50 | ...: RETURN v2.player.name AS FriendOf, v3.team.name AS Team LIMIT 3; 51 | Out[9]: RETURN v2.player.name AS FriendOf, v3.team.name AS Team LIMIT 3; 52 | FriendOf Team 53 | 0 LaMarcus Aldridge Trail Blazers 54 | 1 LaMarcus Aldridge Spurs 55 | 2 Marco Belinelli Warriors 56 | ``` 57 | 58 | ## Draw query results 59 | 60 | Just call `%ng_draw` after queries with graph data. 61 | 62 | ```python 63 | # one query 64 | %ngql GET SUBGRAPH 2 STEPS FROM "player101" YIELD VERTICES AS nodes, EDGES AS relationships; 65 | %ng_draw 66 | 67 | # another query 68 | %ngql match p=(:player)-[]->() return p LIMIT 5 69 | %ng_draw 70 | ``` 71 | 72 | ![](https://github.com/wey-gu/jupyter_nebulagraph/assets/1651790/b3d9ca07-2eb1-45ae-949b-543f58a57760) -------------------------------------------------------------------------------- /docs/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | {% if page.nb_url %} 5 | 6 | {% include ".icons/material/download.svg" %} 7 | 8 | {% endif %} 9 | 10 | {{ super() }} 11 | {% endblock content %} -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs 2 | mkdocs-jupyter 3 | mkdocs-redirects==1.2.1 4 | mkdocs-material[imaging] 5 | -------------------------------------------------------------------------------- /examples/actor.csv: -------------------------------------------------------------------------------- 1 | "player999","Tom Hanks",30 2 | "player1000","Tom Cruise",40 3 | "player1001","Jimmy X",33 4 | -------------------------------------------------------------------------------- /examples/follow.csv: -------------------------------------------------------------------------------- 1 | "player999","player1000",50 -------------------------------------------------------------------------------- /examples/follow_with_rank.csv: -------------------------------------------------------------------------------- 1 | "player999","player1000",50,1 2 | "player999","player1000",50,2 3 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: NebulaGraph Jupyter Extension 2 | 3 | plugins: 4 | - mkdocs-jupyter: 5 | execute: false 6 | include_source: true 7 | - redirects: 8 | redirect_maps: 9 | "get_started.md": "get_started_docs.ipynb" 10 | - search 11 | - social: 12 | cards_layout: default 13 | 14 | nav: 15 | - Home: 16 | - NebulaGraph Jupyter: index.md 17 | - Get Started: get_started_docs.ipynb 18 | - Installation: installation.md 19 | - Commands: 20 | - ngql: magic_words/ngql.md 21 | - ng_draw: magic_words/ng_draw.md 22 | - ng_draw_schema: magic_words/ng_draw_schema.md 23 | - ng_load: magic_words/ng_load.md 24 | - Configurations: configurations.md 25 | - Cheat Sheet: cheatsheet.md 26 | - Try on Colab: https://colab.research.google.com/github/wey-gu/jupyter_nebulagraph/blob/main/docs/get_started.ipynb 27 | - Installation: installation.md 28 | - Commands: 29 | - ngql: magic_words/ngql.md 30 | - ng_draw: magic_words/ng_draw.md 31 | - ng_draw_schema: magic_words/ng_draw_schema.md 32 | - ng_load: magic_words/ng_load.md 33 | - Configurations: configurations.md 34 | - Cheat Sheet: cheatsheet.md 35 | - Get Started: get_started_docs.ipynb 36 | - Try on Colab: https://colab.research.google.com/github/wey-gu/jupyter_nebulagraph/blob/main/docs/get_started.ipynb 37 | 38 | repo_url: https://github.com/wey-gu/jupyter_nebulagraph 39 | repo_name: jupyter_nebulagraph 40 | 41 | theme: 42 | favicon: images/favicon.png 43 | name: material 44 | custom_dir: docs/overrides 45 | palette: 46 | primary: white 47 | accent: white 48 | scheme: slate 49 | icon: 50 | repo: fontawesome/brands/github 51 | font: 52 | text: 'Roboto' 53 | code: 'Roboto Mono' 54 | logo: 'images/nebula_jupyter_logo_dark.png' 55 | features: 56 | - navigation.instant 57 | - navigation.tabs 58 | - navigation.indexes 59 | - navigation.top 60 | - navigation.footer 61 | - toc.follow 62 | - content.code.copy 63 | palette: 64 | - media: (prefers-color-scheme) 65 | toggle: 66 | icon: material/brightness-auto 67 | name: Switch to light mode 68 | - accent: purple 69 | media: "(prefers-color-scheme: light)" 70 | primary: white 71 | scheme: default 72 | toggle: 73 | icon: material/brightness-7 74 | name: Switch to dark mode 75 | - accent: purple 76 | media: "(prefers-color-scheme: dark)" 77 | primary: black 78 | scheme: slate 79 | toggle: 80 | icon: material/brightness-4 81 | name: Switch to system preference 82 | 83 | extra: 84 | social: 85 | - icon: fontawesome/brands/github 86 | link: https://github.com/wey-gu/jupyter_nebulagraph 87 | - icon: fontawesome/brands/docker 88 | link: https://hub.docker.com/r/weygu/nebulagraph-jupyter 89 | - icon: fontawesome/brands/python 90 | link: https://github.com/vesoft-inc/nebula-python 91 | - icon: fontawesome/brands/twitter 92 | link: https://twitter.com/NebulaGraph 93 | 94 | copyright: Copyright © 2021-2024, NebulaGraph Community 95 | -------------------------------------------------------------------------------- /ngql/__init__.py: -------------------------------------------------------------------------------- 1 | from .magic import IPythonNGQL 2 | 3 | 4 | def load_ipython_extension(ipython): 5 | ipython.register_magics(IPythonNGQL) 6 | -------------------------------------------------------------------------------- /ngql/magic.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from typing import Any, Dict, Optional, List 4 | 5 | from IPython.core.magic import ( 6 | Magics, 7 | magics_class, 8 | line_cell_magic, 9 | needs_local_scope, 10 | ) 11 | from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring 12 | 13 | 14 | import networkx as nx 15 | 16 | 17 | from jinja2 import Template, Environment, meta 18 | from traitlets.config.configurable import Configurable 19 | from traitlets import Bool, Int, Unicode 20 | 21 | from nebula3.data.DataObject import Node, Relationship, PathWrapper 22 | from nebula3.gclient.net import ConnectionPool as NebulaConnectionPool 23 | from nebula3.Config import Config as NebulaConfig 24 | from nebula3.Config import SSL_config 25 | from nebula3.data.ResultSet import ResultSet 26 | 27 | from ngql.ng_load import ng_load 28 | from ngql.types import LoadDataArgsModel 29 | from ngql.utils import FancyPrinter 30 | 31 | 32 | fancy_print = FancyPrinter() 33 | 34 | 35 | rel_query_sample_edge = Template( 36 | """ 37 | MATCH ()-[e:`{{ edge_type }}`]->() 38 | RETURN [src(e), dst(e)] AS sample_edge LIMIT 10000 39 | """ 40 | ) 41 | 42 | 43 | rel_query_edge_type = Template( 44 | """ 45 | MATCH (m)-[:`{{ edge_type }}`]->(n) 46 | WHERE id(m) == "{{ src_id }}" AND id(n) == "{{ dst_id }}" 47 | RETURN tags(m)[0] AS src_tag, tags(n)[0] AS dst_tag 48 | """ 49 | ) 50 | 51 | 52 | CONNECTION_POOL_INIT_FAILURE = -2 # Failure occurred during connection_pool.init 53 | CONNECTION_POOL_NONE = -1 # self.connection_pool was never initiated 54 | CONNECTION_POOL_EXISTED = 0 # self.connection_pool existed & no new created 55 | CONNECTION_POOL_CREATED = 1 # self.connection_pool newly created/recreated 56 | 57 | STYLE_PANDAS = "pandas" 58 | STYLE_RAW = "raw" 59 | 60 | # COLORS = ["#E2DBBE", "#D5D6AA", "#9DBBAE", "#769FB6", "#188FA7"] 61 | # solarized dark 62 | COLORS = [ 63 | "#93A1A1", 64 | "#B58900", 65 | "#CB4B16", 66 | "#DC322F", 67 | "#D33682", 68 | "#6C71C4", 69 | "#268BD2", 70 | "#2AA198", 71 | "#859900", 72 | ] 73 | 74 | ESCAPE_ARROW_STRING = "__ar_row__" 75 | 76 | 77 | def truncate(string: str, length: int = 10) -> str: 78 | if len(string) > length: 79 | return string[:length] + ".." 80 | else: 81 | return string 82 | 83 | 84 | def get_color(input_str): 85 | hash_val = 0 86 | for char in input_str: 87 | hash_val = (hash_val * 31 + ord(char)) & 0xFFFFFFFF 88 | return COLORS[hash_val % len(COLORS)] 89 | 90 | 91 | def is_human_readable(field): 92 | return any(c.isalpha() for c in field) and len(field) < 20 93 | 94 | 95 | @magics_class 96 | class IPythonNGQL(Magics, Configurable): 97 | ngql_verbose = Bool(False, config=True, help="Set verbose mode") 98 | max_connection_pool_size = Int( 99 | None, 100 | config=True, 101 | allow_none=True, 102 | help="Maximum Nebula Connection Pool Size", 103 | ) 104 | ngql_result_style = Unicode( 105 | STYLE_PANDAS, 106 | config=True, 107 | allow_none=True, 108 | help="Accepted values in ('pandas', 'raw'):" 109 | " pandas refers to pandas DataFrame," 110 | " raw refers to raw thrift data type comes with nebula-python.", 111 | ) 112 | 113 | def __init__(self, shell): 114 | Magics.__init__(self, shell=shell) 115 | 116 | self.shell.configurables.append(self) 117 | self.connection_pool = None 118 | self.space = None 119 | self.connection_info = None 120 | self.credential = None 121 | 122 | @needs_local_scope 123 | @line_cell_magic 124 | @magic_arguments() 125 | @argument("line", default="", nargs="*", type=str, help="ngql line") 126 | @argument("-addr", "--address", type=str, help="IP address") 127 | @argument("-P", "--port", type=int, help="Port number") 128 | @argument("-u", "--user", type=str, help="Username") 129 | @argument("-p", "--password", type=str, help="Password") 130 | @argument("-f", "--file", type=str, help="Run a NGQL file from a path") # TBD 131 | @argument("-c", "--close", type=str, help="Close the connection") # TBD 132 | def ngql(self, line, cell=None, local_ns={}): 133 | """Magic that works both as %ngql and as %%ngql""" 134 | if line == "help": 135 | return self._help_info() 136 | 137 | cell = self._render_cell_vars(cell, local_ns) 138 | 139 | # Replace "->" with ESCAPE_ARROW_STRING to avoid argument parsing issues 140 | modified_line = line.replace("->", ESCAPE_ARROW_STRING) 141 | 142 | args = parse_argstring(self.ngql, modified_line) 143 | 144 | connection_state = self._init_connection_pool(args) 145 | if self.ngql_verbose: 146 | fancy_print(f"[DEBUG] Connection State: { connection_state }") 147 | if connection_state < 0: 148 | fancy_print("[ERROR] Connection is not ready", color="pink") 149 | return f"Connection State: { connection_state }" 150 | if connection_state == CONNECTION_POOL_CREATED: 151 | fancy_print("[OK] Connection Pool Created", color="green") 152 | if not cell: 153 | return self._stylized(self._show_spaces()) 154 | else: 155 | # When connection info in first line and with nGQL lines followed 156 | return self._stylized(self._execute(cell)) 157 | if connection_state == CONNECTION_POOL_EXISTED: 158 | # Restore "->" in the query before executing it 159 | query = ( 160 | line.replace(ESCAPE_ARROW_STRING, "->") + "\n" + (cell if cell else "") 161 | ) 162 | return self._stylized(self._execute(query)) 163 | else: # We shouldn't reach here 164 | return f"Nothing triggerred, Connection State: { connection_state }" 165 | 166 | def _init_connection_pool(self, args: Optional[Any] = None): 167 | if args is None: 168 | return ( 169 | CONNECTION_POOL_EXISTED 170 | if self.connection_pool is not None 171 | else CONNECTION_POOL_NONE 172 | ) 173 | 174 | connection_info = (args.address, args.port, args.user, args.password) 175 | if any(connection_info): 176 | if not all(connection_info): 177 | raise ValueError( 178 | "One or more arguments missing: address, port, user, " 179 | "password should None or all be provided." 180 | ) 181 | # all connection information ready 182 | connection_pool = NebulaConnectionPool() 183 | config = NebulaConfig() 184 | if self.max_connection_pool_size: 185 | config.max_connection_pool_size = self.max_connection_pool_size 186 | 187 | self.credential = args.user, args.password 188 | try: 189 | connect_init_result = connection_pool.init( 190 | [(args.address, args.port)], config 191 | ) 192 | except RuntimeError: 193 | # When GraphD is over TLS 194 | fancy_print( 195 | "[WARN] Got RuntimeError, trying to connect assuming NebulaGraph is over TLS", 196 | color="pink", 197 | ) 198 | ssl_config = SSL_config() 199 | connect_init_result = connection_pool.init( 200 | [(args.address, args.port)], config, ssl_config 201 | ) 202 | fancy_print( 203 | f"[OK] Connection State: { connect_init_result }, TLS: True", 204 | color="blue", 205 | ) 206 | if not connect_init_result: 207 | return CONNECTION_POOL_INIT_FAILURE 208 | else: 209 | self.connection_pool = connection_pool 210 | return CONNECTION_POOL_CREATED 211 | else: 212 | return ( 213 | CONNECTION_POOL_EXISTED 214 | if self.connection_pool is not None 215 | else CONNECTION_POOL_NONE 216 | ) 217 | 218 | def _render_cell_vars(self, cell, local_ns): 219 | if cell is not None: 220 | env = Environment() 221 | cell_vars = meta.find_undeclared_variables(env.parse(cell)) 222 | cell_params = {} 223 | for variable in cell_vars: 224 | if variable in local_ns: 225 | cell_params[variable] = local_ns[variable] 226 | else: 227 | raise NameError(variable) 228 | cell_template = Template(cell) 229 | cell = cell_template.render(**cell_params) 230 | if self.ngql_verbose: 231 | fancy_print(f"Query String:\n { cell }", color="blue") 232 | return cell 233 | 234 | def _get_session(self): 235 | logger = logging.getLogger() 236 | # FIXME(wey-gu): introduce configurable options here via traitlets 237 | # Here let's disable the nebula-python logger as we consider 238 | # most users here are data scientists who would share the 239 | # notebook, thus connection info shouldn't be revealed unless 240 | # explicitly specified 241 | logger.disabled = True 242 | if self.connection_pool is None: 243 | raise ValueError( 244 | "Please connect to NebulaGraph first, i.e. \n" 245 | "%ngql --address 127.0.0.1 --port 9669 --user root --password nebula" 246 | ) 247 | return self.connection_pool.get_session(*self.credential) 248 | 249 | def _show_spaces(self): 250 | session = self._get_session() 251 | try: 252 | result = session.execute("SHOW SPACES") 253 | self._auto_use_space(result=result) 254 | except Exception as e: 255 | fancy_print(f"[ERROR]:\n { e }", color="red") 256 | finally: 257 | session.release() 258 | return result 259 | 260 | def _auto_use_space(self, result=None): 261 | if result is None: 262 | session = self._get_session() 263 | result = session.execute("SHOW SPACES;") 264 | 265 | if result.row_size() == 1: 266 | self.space = result.row_values(0)[0].cast_primitive() 267 | 268 | def _execute(self, query): 269 | session = self._get_session() 270 | query = query.replace("\\\n", "\n") 271 | try: 272 | if self.space is not None: # Always use space automatically 273 | session.execute(f"USE { self.space }") 274 | result = session.execute(query) 275 | assert ( 276 | result.is_succeeded() 277 | ), f"Query Failed:\n { result.error_msg() }\n Query:\n { query }" 278 | self._remember_space(result) 279 | except Exception as e: 280 | fancy_print(f"[ERROR]:\n { e }", color="red") 281 | finally: 282 | session.release() 283 | return result 284 | 285 | def _remember_space(self, result): 286 | last_space_used = result.space_name() 287 | if last_space_used != "": 288 | self.space = last_space_used 289 | 290 | def _stylized(self, result: ResultSet, style=None): 291 | style = style or self.ngql_result_style 292 | if style == STYLE_PANDAS: 293 | try: 294 | import pandas as pd 295 | except ImportError: 296 | raise ImportError("Please install pandas to use STYLE_PANDAS") 297 | 298 | pd.set_option("display.max_columns", None) 299 | pd.set_option("display.max_colwidth", None) 300 | pd.set_option("display.max_rows", 300) 301 | pd.set_option("display.expand_frame_repr", False) 302 | 303 | columns = result.keys() 304 | d: Dict[str, list] = {} 305 | for col_num in range(result.col_size()): 306 | col_name = columns[col_num] 307 | col_list = result.column_values(col_name) 308 | d[col_name] = [x.cast() for x in col_list] 309 | df = pd.DataFrame(d) 310 | df.style.set_table_styles( 311 | [{"selector": "table", "props": [("overflow-x", "scroll")]}] 312 | ) 313 | return df 314 | elif style == STYLE_RAW: 315 | return result 316 | else: 317 | raise ValueError(f"Unknown ngql_result_style: { style }") 318 | 319 | @staticmethod 320 | def _help_info(): 321 | help_info = """ 322 | 323 | Supported Configurations: 324 | ------------------------ 325 | 326 | > How to config ngql_result_style in "raw", "pandas" 327 | %config IPythonNGQL.ngql_result_style="raw" 328 | %config IPythonNGQL.ngql_result_style="pandas" 329 | 330 | > How to config ngql_verbose in True, False 331 | %config IPythonNGQL.ngql_verbose=True 332 | 333 | > How to config max_connection_pool_size 334 | %config IPythonNGQL.max_connection_pool_size=10 335 | 336 | Quick Start: 337 | ----------- 338 | 339 | > Connect to Neubla Graph 340 | %ngql --address 127.0.0.1 --port 9669 --user user --password password 341 | 342 | > Use Space 343 | %ngql USE basketballplayer 344 | 345 | > Query 346 | %ngql SHOW TAGS; 347 | 348 | > Multile Queries 349 | %%ngql 350 | SHOW TAGS; 351 | SHOW HOSTS; 352 | 353 | Reload ngql Magic 354 | %reload_ext ngql 355 | 356 | > Variables in query, we are using Jinja2 here 357 | name = "nba" 358 | %ngql USE "{{ name }}" 359 | 360 | > Query and draw the graph of last executed query. 361 | 362 | %ngql GET SUBGRAPH WITH PROP 2 STEPS FROM "player101" YIELD VERTICES AS nodes, EDGES AS relationships; 363 | 364 | %ng_draw 365 | 366 | Or draw a Query 367 | 368 | %ng_draw GET SUBGRAPH WITH PROP 2 STEPS FROM "player101" YIELD VERTICES AS nodes, EDGES AS relationships; 369 | 370 | > Query and draw the graph schema 371 | 372 | %ng_draw_schema 373 | 374 | > Load data from CSV file into NebulaGraph as vertices or edges 375 | %ng_load --source actor.csv --tag player --vid 0 --props 1:name,2:age --space basketballplayer 376 | 377 | #actor.csv 378 | "player999","Tom Hanks",30 379 | "player1000","Tom Cruise",40 380 | 381 | %ng_load --source follow_with_rank.csv --edge follow --src 0 --dst 1 --props 2:degree --rank 3 --space basketballplayer 382 | 383 | #follow_with_rank.csv 384 | "player999","player1000",50,1 385 | 386 | %ng_load --source follow.csv --edge follow --src 0 --dst 1 --props 2:degree --space basketballplayer 387 | 388 | #follow.csv 389 | "player999","player1000",50 390 | 391 | %ng_load --source https://github.com/wey-gu/ipython-ngql/raw/main/examples/actor.csv --tag player --vid 0 --props 1:name,2:age --space demo_basketballplayer 392 | 393 | 394 | """ 395 | fancy_print(help_info, color="green") 396 | return 397 | 398 | def _draw_graph(self, g: Any) -> Any: 399 | try: 400 | from IPython.display import display, IFrame, HTML 401 | 402 | # import get_ipython 403 | from IPython import get_ipython 404 | except ImportError: 405 | raise ImportError("Please install IPython to draw the graph") 406 | 407 | g.repulsion( 408 | node_distance=90, 409 | central_gravity=0.2, 410 | spring_length=200, 411 | spring_strength=0.05, 412 | damping=0.09, 413 | ) 414 | # g.show_buttons(filter_='physics') 415 | # return g.show("nebulagraph.html", notebook=True) 416 | cell_num = get_ipython().execution_count 417 | graph_render_filename = f"nebulagraph_cell_{cell_num}.html" 418 | g_html_string = g.generate_html(graph_render_filename) 419 | with open(graph_render_filename, "w", encoding="utf-8") as f: 420 | f.write(g_html_string) 421 | # detect if we are in colab or not 422 | try: 423 | if "google.colab" in str(get_ipython()): 424 | display(HTML(g_html_string)) 425 | else: 426 | display(IFrame(src=graph_render_filename, width="100%", height="500px")) 427 | except Exception as e: 428 | fancy_print(f"[WARN]: failed to display the graph\n { e }") 429 | try: 430 | display(IFrame(src=graph_render_filename, width="100%", height="500px")) 431 | except Exception as e: 432 | fancy_print(f"[WARN]: failed to display the graph\n { e }") 433 | 434 | return g 435 | 436 | @needs_local_scope 437 | @line_cell_magic 438 | @magic_arguments() 439 | @argument("line", default="", nargs="*", type=str, help="ngql") 440 | def ng_draw(self, line, cell=None, local_ns={}): 441 | """ 442 | Draw the graph with the output of the last execution query 443 | """ 444 | try: 445 | import pandas as pd 446 | from pyvis.network import Network 447 | 448 | except ImportError: 449 | raise ImportError("Please install pyvis to draw the graph") 450 | # when `%ng_draw foo`, varible_name is "foo", else it's "_" 451 | arguments_line = line.strip() 452 | 453 | if not arguments_line and not cell: 454 | # No arguments and no cell content, draw the graph with the last execution result 455 | 456 | variable_name = arguments_line or "_" 457 | # Check if the last execution result is available in the local namespace 458 | if variable_name not in local_ns: 459 | return "No result found, please execute a query first." 460 | result_df = local_ns[variable_name] 461 | 462 | if not isinstance(result_df, pd.DataFrame): 463 | if isinstance(result_df, ResultSet): 464 | result_df = self._stylized(result_df, style=STYLE_PANDAS) 465 | elif isinstance(result_df, Network): 466 | # A rerun of %ng_draw with the last execution result 467 | g = self._draw_graph(result_df) 468 | return g 469 | else: 470 | fancy_print( 471 | "[ERROR]: No valid %ngql query result available. \n" 472 | "Please execute a valid query before using %ng_draw. \n" 473 | "Or pass a query as an argument to %ng_draw or %%ng_draw(multiline).", 474 | color="red", 475 | ) 476 | return "" 477 | 478 | else: 479 | # Arguments provided, execute the query and draw the graph 480 | 481 | if line == "help": 482 | return self._help_info() 483 | 484 | cell = self._render_cell_vars(cell, local_ns) 485 | 486 | # Replace "->" with ESCAPE_ARROW_STRING to avoid argument parsing issues 487 | modified_line = line.replace("->", ESCAPE_ARROW_STRING) 488 | 489 | connection_state = self._init_connection_pool() 490 | if connection_state == CONNECTION_POOL_EXISTED: 491 | # Restore "->" in the query before executing it 492 | query = ( 493 | modified_line.replace(ESCAPE_ARROW_STRING, "->") 494 | + "\n" 495 | + (cell if cell else "") 496 | ) 497 | result_df = self._stylized(self._execute(query), style=STYLE_PANDAS) 498 | 499 | # Create a graph 500 | g = Network( 501 | notebook=True, 502 | directed=True, 503 | cdn_resources="in_line", 504 | height="500px", 505 | width="100%", 506 | bgcolor="#002B36", 507 | font_color="#93A1A1", 508 | neighborhood_highlight=True, 509 | ) 510 | g_nx = nx.MultiDiGraph() 511 | 512 | edge_filter = set() 513 | for _, row in result_df.iterrows(): 514 | for item in row: 515 | self.render_pd_item(g, g_nx, item, edge_filter) 516 | 517 | try: 518 | # Calculate PageRank 519 | pagerank_scores = nx.pagerank(g_nx) 520 | 521 | # Update node sizes based on PageRank scores 522 | for node_id, score in pagerank_scores.items(): 523 | normalized_size = ( 524 | 10 + score * 90 525 | ) # Reduced multiplier for smaller size normalization 526 | g.get_node(node_id)["size"] = min(normalized_size, 80) 527 | except Exception as e: 528 | fancy_print( 529 | f"[WARN]: failed to calculate PageRank, left graph node unsized. Reason:\n { e }" 530 | ) 531 | 532 | g = self._draw_graph(g) 533 | 534 | return g 535 | 536 | @line_cell_magic 537 | @magic_arguments() 538 | @argument("line", default="", nargs="?", type=str, help="space name") 539 | def ng_draw_schema(self, line, cell=None, local_ns={}): 540 | try: 541 | from pyvis.network import Network 542 | from IPython.display import display, IFrame, HTML 543 | 544 | # import get_ipython 545 | from IPython import get_ipython 546 | 547 | except ImportError: 548 | raise ImportError("Please install pyvis to draw the graph schema") 549 | 550 | args = parse_argstring(self.ng_draw_schema, line) 551 | space = args.line if args.line else self.space 552 | if space is None: 553 | return "Please specify the space name or run `USE ` first." 554 | space = space.strip() 555 | 556 | tags_schema, edge_types_schema, relationship_samples = [], [], [] 557 | for tag in self._execute("SHOW TAGS").column_values("Name"): 558 | tag_name = tag.cast_primitive() 559 | tag_schema = {"tag": tag_name, "properties": []} 560 | r = self._execute(f"DESCRIBE TAG `{tag_name}`") 561 | props, types, comments = ( 562 | r.column_values("Field"), 563 | r.column_values("Type"), 564 | r.column_values("Comment"), 565 | ) 566 | for i in range(r.row_size()): 567 | # back compatible with old version of nebula-python 568 | property_defination = ( 569 | (props[i].cast_primitive(), types[i].cast_primitive()) 570 | if comments[i].is_empty() 571 | else ( 572 | props[i].cast_primitive(), 573 | types[i].cast_primitive(), 574 | comments[i].cast_primitive(), 575 | ) 576 | ) 577 | tag_schema["properties"].append(property_defination) 578 | tags_schema.append(tag_schema) 579 | for edge_type in self._execute("SHOW EDGES").column_values("Name"): 580 | edge_type_name = edge_type.cast_primitive() 581 | edge_schema = {"edge": edge_type_name, "properties": []} 582 | r = self._execute(f"DESCRIBE EDGE `{edge_type_name}`") 583 | props, types, comments = ( 584 | r.column_values("Field"), 585 | r.column_values("Type"), 586 | r.column_values("Comment"), 587 | ) 588 | for i in range(r.row_size()): 589 | # back compatible with old version of nebula-python 590 | property_defination = ( 591 | (props[i].cast_primitive(), types[i].cast_primitive()) 592 | if comments[i].is_empty() 593 | else ( 594 | props[i].cast_primitive(), 595 | types[i].cast_primitive(), 596 | comments[i].cast_primitive(), 597 | ) 598 | ) 599 | edge_schema["properties"].append(property_defination) 600 | edge_types_schema.append(edge_schema) 601 | 602 | # build sample edge 603 | sample_edge = self._execute( 604 | rel_query_sample_edge.render(edge_type=edge_type_name) 605 | ).column_values("sample_edge") 606 | if len(sample_edge) == 0: 607 | continue 608 | src_id, dst_id = sample_edge[0].cast_primitive() 609 | r = self._execute( 610 | rel_query_edge_type.render( 611 | edge_type=edge_type_name, src_id=src_id, dst_id=dst_id 612 | ) 613 | ) 614 | if ( 615 | len(r.column_values("src_tag")) == 0 616 | or len(r.column_values("dst_tag")) == 0 617 | ): 618 | continue 619 | src_tag, dst_tag = ( 620 | r.column_values("src_tag")[0].cast_primitive(), 621 | r.column_values("dst_tag")[0].cast_primitive(), 622 | ) 623 | relationship_samples.append( 624 | { 625 | "src_tag": src_tag, 626 | "dst_tag": dst_tag, 627 | "edge_type": edge_type_name, 628 | } 629 | ) 630 | 631 | # In case there are edges not be sampled(no data yet), add them as different node with id edge_src and edge_dst: 632 | for edge_schema in edge_types_schema: 633 | edge_type = edge_schema["edge"] 634 | if edge_type not in [r["edge_type"] for r in relationship_samples]: 635 | src_dummy_tag = edge_type + "_src" 636 | dst_dummy_tag = edge_type + "_dst" 637 | relationship_samples.append( 638 | { 639 | "src_tag": src_dummy_tag, 640 | "dst_tag": dst_dummy_tag, 641 | "edge_type": edge_type, 642 | } 643 | ) 644 | if src_dummy_tag not in [x["tag"] for x in tags_schema]: 645 | tags_schema.append({"tag": src_dummy_tag, "properties": []}) 646 | if dst_dummy_tag not in [x["tag"] for x in tags_schema]: 647 | tags_schema.append({"tag": dst_dummy_tag, "properties": []}) 648 | 649 | # In case there are None in relationship_samples, add placeholder node: 650 | for edge_sample in relationship_samples: 651 | edge_type = edge_sample["edge_type"] 652 | if edge_sample["src_tag"] is None: 653 | src_dummy_tag = edge_type + "_src" 654 | edge_sample["src_tag"] = src_dummy_tag 655 | if src_dummy_tag not in [x["tag"] for x in tags_schema]: 656 | tags_schema.append({"tag": src_dummy_tag, "properties": []}) 657 | if edge_sample["dst_tag"] is None: 658 | dst_dummy_tag = edge_type + "_dst" 659 | edge_sample["dst_tag"] = dst_dummy_tag 660 | if dst_dummy_tag not in [x["tag"] for x in tags_schema]: 661 | tags_schema.append({"tag": dst_dummy_tag, "properties": []}) 662 | 663 | if not tags_schema and not edge_types_schema: 664 | fancy_print("[WARN] No tags or edges found in the space", color="pink") 665 | return 666 | 667 | # Create a graph of relationship_samples 668 | # The nodes are tags, with their properties schema as attributes 669 | # The edges are relationship_samples, with their properties schema as attributes 670 | 671 | g = Network( 672 | notebook=True, 673 | directed=True, 674 | cdn_resources="in_line", 675 | height="500px", 676 | width="100%", 677 | bgcolor="#002B36", 678 | font_color="#93A1A1", 679 | neighborhood_highlight=True, 680 | ) 681 | g_nx = nx.MultiDiGraph() 682 | for tag_schema in tags_schema: 683 | tag_name = tag_schema["tag"] 684 | g.add_node( 685 | tag_name, 686 | label=tag_name, 687 | title=str(tag_schema), 688 | color=get_color(tag_name), 689 | ) 690 | g_nx.add_node(tag_name, **tag_schema) 691 | 692 | for edge_schema in relationship_samples: 693 | src_tag, dst_tag, edge_type = ( 694 | edge_schema["src_tag"], 695 | edge_schema["dst_tag"], 696 | edge_schema["edge_type"], 697 | ) 698 | title = ( 699 | "{\n " 700 | + "\n ".join([f"{k}: {v}" for k, v in edge_schema.items()]) 701 | + "\n}" 702 | ) 703 | g.add_edge(src_tag, dst_tag, label=edge_type, title=title) 704 | g_nx.add_edge(src_tag, dst_tag, **edge_schema) 705 | 706 | try: 707 | # Calculate PageRank 708 | pagerank_scores = nx.pagerank(g_nx) 709 | 710 | # Update node sizes based on PageRank scores 711 | for node_id, score in pagerank_scores.items(): 712 | g.get_node(node_id)["size"] = ( 713 | 10 + score * 130 714 | ) # Normalized size for visibility 715 | except Exception as e: 716 | fancy_print( 717 | f"[WARN]: failed to calculate PageRank, left graph node unsized. Reason:\n { e }" 718 | ) 719 | 720 | g.repulsion( 721 | node_distance=90, 722 | central_gravity=0.2, 723 | spring_length=200, 724 | spring_strength=0.05, 725 | damping=0.09, 726 | ) 727 | # g.show_buttons(filter_='physics') 728 | # return g.show("nebulagraph_draw.html", notebook=True) 729 | cell_num = get_ipython().execution_count 730 | schema_html_filename = f"nebulagraph_schema_cell_{cell_num}_{space}.html" 731 | g_html_string = g.generate_html(schema_html_filename) 732 | with open(schema_html_filename, "w", encoding="utf-8") as f: 733 | f.write(g_html_string) 734 | # detect if we are in colab or not 735 | try: 736 | if "google.colab" in str(get_ipython()): 737 | display(HTML(g_html_string)) 738 | else: 739 | display(IFrame(src=schema_html_filename, width="100%", height="500px")) 740 | except Exception as e: 741 | fancy_print(f"[WARN]: failed to display the graph\n { e }") 742 | try: 743 | display(IFrame(src=schema_html_filename, width="100%", height="500px")) 744 | except Exception as e: 745 | fancy_print(f"[WARN]: failed to display the graph\n { e }") 746 | 747 | return g 748 | 749 | def render_pd_item(self, g, g_nx, item, edges_filter: set): 750 | # g is pyvis graph 751 | # g_nx is networkx graph 752 | 753 | if isinstance(item, Node): 754 | node_id = str(item.get_id().cast()) 755 | tags = item.tags() # list of strings 756 | tags_str = tags[0] if len(tags) == 1 else ",".join(tags) 757 | props_raw = dict() 758 | for tag in tags: 759 | props_raw.update(item.properties(tag)) 760 | props = { 761 | k: str(v.cast()) if hasattr(v, "cast") else str(v) 762 | for k, v in props_raw.items() 763 | } 764 | # populating empty and null properties 765 | props = { 766 | k: v for k, v in props.items() if v not in ["__NULL__", "__EMPTY__"] 767 | } 768 | 769 | if "name" in props: 770 | label = props["name"] 771 | else: 772 | if is_human_readable(node_id): 773 | label = f"tag: {tags_str},\nid: {node_id}" 774 | else: 775 | label = f"tag: {tags_str},\nid: {node_id[:3]}..{node_id[-3:]}" 776 | for k in props: 777 | if "name" in str(k).lower(): 778 | label = props[k] 779 | break 780 | 781 | if "id" not in props: 782 | props["id"] = node_id 783 | title = "\n".join([f"{k}: {v}" for k, v in props.items()]) 784 | 785 | g.add_node(node_id, label=label, title=title, color=get_color(node_id)) 786 | 787 | # networkx 788 | if len(tags) > 1: 789 | props["__tags__"] = ",".join(tags) 790 | g_nx.add_node(node_id, **props) 791 | elif isinstance(item, Relationship): 792 | src_id = str(item.start_vertex_id().cast()) 793 | dst_id = str(item.end_vertex_id().cast()) 794 | edge_name = item.edge_name() 795 | props_raw = item.properties() 796 | rank = item.ranking() 797 | props = { 798 | k: str(v.cast()) if hasattr(v, "cast") else str(v) 799 | for k, v in props_raw.items() 800 | } 801 | if rank != 0: 802 | props.update({"rank": rank}) 803 | # populating empty and null properties 804 | props = { 805 | k: v for k, v in props.items() if v not in ["__NULL__", "__EMPTY__"] 806 | } 807 | # ensure start and end vertex exist in graph 808 | if src_id not in g.node_ids: 809 | label = ( 810 | f"tag: {src_id[:3]}..{src_id[-3:]}" 811 | if not is_human_readable(src_id) 812 | else src_id 813 | ) 814 | g.add_node( 815 | src_id, 816 | label=label, 817 | title=src_id, 818 | color=get_color(src_id), 819 | ) 820 | if dst_id not in g.node_ids: 821 | label = ( 822 | f"tag: {dst_id[:3]}..{dst_id[-3:]}" 823 | if not is_human_readable(dst_id) 824 | else dst_id 825 | ) 826 | g.add_node( 827 | dst_id, 828 | label=label, 829 | title=dst_id, 830 | color=get_color(dst_id), 831 | ) 832 | props_str_list: List[str] = [] 833 | for k in props: 834 | if len(props_str_list) >= 1: 835 | break 836 | props_str_list.append(f"{truncate(k, 7)}: {truncate(str(props[k]), 8)}") 837 | props_str = "\n".join(props_str_list) 838 | 839 | label = f"{props_str}\n{edge_name}" if props else edge_name 840 | if props: 841 | title = ( 842 | "{\n " 843 | + "\n ".join([f"{k}: {v}" for k, v in props.items()]) 844 | + "\n}" 845 | ) 846 | else: 847 | title = edge_name 848 | edge_key = f"{src_id}->{dst_id}@{rank}:{edge_name}" 849 | if edge_key not in edges_filter: 850 | # We don't have to ensure same policies for identical edges when adding to graph 851 | # for PyVis and NetworkX, thus we maintain a set to filter out identical edges 852 | g.add_edge( 853 | src_id, 854 | dst_id, 855 | label=label, 856 | title=title, 857 | weight=props.get("rank", 0), 858 | ) 859 | # networkx 860 | props["edge_type"] = edge_name 861 | g_nx.add_edge(src_id, dst_id, **props) 862 | edges_filter.add(edge_key) 863 | 864 | elif isinstance(item, PathWrapper): 865 | for node in item.nodes(): 866 | self.render_pd_item(g, g_nx, node, edges_filter) 867 | for edge in item.relationships(): 868 | self.render_pd_item(g, g_nx, edge, edges_filter) 869 | 870 | elif isinstance(item, list): 871 | for it in item: 872 | self.render_pd_item(g, g_nx, it, edges_filter) 873 | 874 | @line_cell_magic 875 | @magic_arguments() 876 | @argument( 877 | "--header", 878 | action="store_true", 879 | help="Specify if the CSV file contains a header row", 880 | ) 881 | @argument("-n", "--space", type=str, help="Space name") 882 | @argument("-s", "--source", type=str, help="File path or URL to the CSV file") 883 | @argument("-t", "--tag", type=str, help="Tag name for vertices") 884 | @argument("--vid", type=int, help="Vertex ID column index") 885 | @argument("-e", "--edge", type=str, help="Edge type name") 886 | @argument("--src", type=int, help="Source vertex ID column index") 887 | @argument("--dst", type=int, help="Destination vertex ID column index") 888 | @argument("--rank", type=int, help="Rank column index", default=None) 889 | @argument( 890 | "-l", 891 | "--limit", 892 | type=int, 893 | help="Sample maximum n lines of the CSV file", 894 | default=-1, 895 | ) 896 | @argument( 897 | "--props", 898 | type=str, 899 | help="Property mapping, comma-separated column indexes", 900 | default=None, 901 | ) 902 | @argument( 903 | "-b", "--batch", type=int, help="Batch size for data loading", default=256 904 | ) 905 | def ng_load(self, line, cell=None, local_ns={}): 906 | """ 907 | Load data from CSV file into NebulaGraph as vertices or edges 908 | 909 | Examples: 910 | %ng_load --source actor.csv --tag player --vid 0 --props 1:name,2:age --space basketballplayer 911 | %ng_load --source follow_with_rank.csv --edge follow --src 0 --dst 1 --props 2:degree --rank 3 --space basketballplayer 912 | %ng_load --source follow.csv --edge follow --src 0 --dst 1 --props 2:degree --space basketballplayer 913 | 914 | #actor.csv 915 | "player999","Tom Hanks",30 916 | "player1000","Tom Cruise",40 917 | 918 | #follow_with_rank.csv 919 | "player999","player1000",50,1 920 | """ 921 | if self.connection_pool is None: 922 | fancy_print( 923 | "[WARN]: Please connect to NebulaGraph first using %ngql magic before using ng_load" 924 | "\nExample: %ngql --address 127.0.0.1 --port 9669 --user root --password nebula" 925 | ) 926 | return 927 | 928 | args = parse_argstring(self.ng_load, line) 929 | ng_load( 930 | self._execute, LoadDataArgsModel.model_validate(args, from_attributes=True) 931 | ) 932 | -------------------------------------------------------------------------------- /ngql/ng_load.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import pandas as pd 3 | from io import BytesIO, StringIO 4 | from typing import Callable 5 | 6 | from nebula3.data.ResultSet import ResultSet 7 | from nebula3.gclient.net import ConnectionPool 8 | from nebula3.Config import Config as NebulaConfig 9 | 10 | from ngql.types import LoadDataArgsModel 11 | from ngql.utils import FancyPrinter 12 | 13 | 14 | try: 15 | import IPython 16 | 17 | if IPython.get_ipython() is not None: 18 | from tqdm.notebook import tqdm 19 | else: 20 | from tqdm import tqdm 21 | except ImportError: 22 | from tqdm import tqdm 23 | 24 | fancy_print = FancyPrinter() 25 | 26 | 27 | def ng_load(execute_fn: Callable[[str], ResultSet], args: LoadDataArgsModel): 28 | """ 29 | Load data from CSV file into NebulaGraph as vertices or edges 30 | 31 | Examples: 32 | %ng_load --source actor.csv --tag player --vid 0 --props 1:name,2:age --space basketballplayer 33 | %ng_load --source follow_with_rank.csv --edge follow --src 0 --dst 1 --props 2:degree --rank 3 --space basketballplayer 34 | %ng_load --source follow.csv --edge follow --src 0 --dst 1 --props 2:degree --space basketballplayer 35 | 36 | #actor.csv 37 | "player999","Tom Hanks",30 38 | "player1000","Tom Cruise",40 39 | 40 | #follow_with_rank.csv 41 | "player999","player1000",50,1 42 | """ 43 | 44 | # Check if space is specified 45 | space = args.space 46 | execute_fn(f"USE `{space}`") 47 | # Inspect space to get Vid Type 48 | r = execute_fn(f"DESC SPACE `{space}`") 49 | try: 50 | if not len(r.column_values("Vid Type")) == 1: 51 | raise ValueError("Space may not exist") 52 | vid_type = str(r.column_values("Vid Type")[0]) 53 | except Exception as e: 54 | raise ValueError(f"Failed to get Vid Type from space '{space}', error: {e}") 55 | 56 | vid_length = 0 57 | if vid_type.find("FIXED_STRING") != -1: 58 | vid_length = int(vid_type.split("(")[1].split(")")[0]) 59 | is_vid_int = vid_length == 0 60 | 61 | # Validate required arguments 62 | if not args.tag and not args.edge: 63 | raise ValueError( 64 | "Missing required argument: --tag tag_name for vertex loading or --edge edge_type for edge loading" 65 | ) 66 | 67 | # If with header 68 | with_header = args.header 69 | 70 | limit = args.limit 71 | 72 | # Function to safely load CSV or Parquet with limit 73 | def safe_load_file(source, file_type, header_option=None, limit=None): 74 | if file_type == "csv": 75 | temp_df = pd.read_csv(source, header=header_option) 76 | elif file_type == "parquet": 77 | temp_df = pd.read_parquet(source) 78 | else: 79 | raise ValueError(f"Unsupported file type: {file_type}") 80 | 81 | if isinstance(limit, int) and limit > 0: 82 | return temp_df.head(limit) 83 | return temp_df 84 | 85 | # Determine file type based on source extension 86 | file_type = ( 87 | "csv" 88 | if args.source.lower().endswith(".csv") 89 | else "parquet" 90 | if args.source.lower().endswith(".parquet") 91 | else None 92 | ) 93 | if file_type is None: 94 | raise ValueError( 95 | "Unsupported file type. Please use either CSV or Parquet files." 96 | ) 97 | 98 | # Load file from URL or local path 99 | if args.source.startswith("http://") or args.source.startswith("https://"): 100 | response = requests.get(args.source) 101 | if file_type == "csv": 102 | file_content = StringIO(response.content.decode("utf-8")) 103 | else: # parquet 104 | file_content = BytesIO(response.content) 105 | df = safe_load_file( 106 | file_content, 107 | file_type, 108 | header_option=0 if with_header else None, 109 | limit=limit, 110 | ) 111 | else: 112 | df = safe_load_file( 113 | args.source, 114 | file_type, 115 | header_option=0 if with_header else None, 116 | limit=limit, 117 | ) 118 | 119 | # Build schema type map for tag or edge type 120 | prop_schema_map = {} 121 | DESC_TYPE = "TAG" if args.tag else "EDGE" 122 | DESC_TARGET = args.tag if args.tag else args.edge 123 | r = execute_fn(f"DESCRIBE {DESC_TYPE} `{DESC_TARGET}`") 124 | props, types, nullable = ( 125 | r.column_values("Field"), 126 | r.column_values("Type"), 127 | r.column_values("Null"), 128 | ) 129 | for i in range(r.row_size()): 130 | # back compatible with old version of nebula-python 131 | prop_schema_map[props[i].cast()] = { 132 | "type": types[i].cast(), 133 | "nullable": nullable[i].cast() == "YES", 134 | } 135 | 136 | # Process properties mapping 137 | props_mapping = ( 138 | {int(k): v for k, v in (prop.split(":") for prop in args.props.split(","))} 139 | if args.props 140 | else {} 141 | ) 142 | # Values of props_mapping are property names they should be strings 143 | # Keys of props_mapping are column indexes they should be integers 144 | for k, v in props_mapping.items(): 145 | if not isinstance(k, int): 146 | raise ValueError( 147 | f"ERROR during prop mapping validation: Key '{k}' in property mapping is not an integer" 148 | ) 149 | if not isinstance(v, str): 150 | raise ValueError( 151 | f"ERROR during prop mapping validation: Value '{v}' in property mapping is not a string" 152 | ) 153 | if k >= len(df.columns) or k < 0: 154 | raise ValueError( 155 | f"ERROR during prop mapping validation: Key '{k}' in property mapping is out of range: 0-{len(df.columns)-1}" 156 | ) 157 | 158 | with_props = True if props_mapping else False 159 | 160 | # Validate props_mapping against schema 161 | matched = True 162 | for i, prop in props_mapping.items(): 163 | if prop not in prop_schema_map: 164 | fancy_print( 165 | f"[ERROR] Property '{prop}' not found in schema for {DESC_TYPE} '{args.tag}'", 166 | "orange", 167 | ) 168 | matched = False 169 | for prop in prop_schema_map: 170 | # For not nullable properties, check if they are in props_mapping 171 | if not prop_schema_map[prop]["nullable"] and prop not in props_mapping.values(): 172 | fancy_print( 173 | f"[ERROR] Property '{prop}' is not nullable and not found in property mapping", 174 | "orange", 175 | ) 176 | matched = False 177 | 178 | if not matched: 179 | raise ValueError("[ERROR] Property mapping does not match schema") 180 | 181 | if args.rank is not None: 182 | with_rank = True 183 | else: 184 | with_rank = False 185 | 186 | # Prepare data for loading 187 | if args.tag and not args.edge: 188 | if args.vid is None: 189 | raise ValueError("[ERROR] Missing required argument: --vid for vertex ID") 190 | # Process properties mapping 191 | vertex_data_columns = ["___vid"] + [ 192 | props_mapping[i] for i in sorted(props_mapping) 193 | ] 194 | vertex_data_indices = [args.vid] + sorted(props_mapping.keys()) 195 | vertex_data = df.iloc[:, vertex_data_indices] 196 | vertex_data.columns = vertex_data_columns 197 | # Here you would load vertex_data into NebulaGraph under the specified tag and space 198 | fancy_print( 199 | f"[INFO] Parsed {len(vertex_data)} vertices '{space}' for tag '{args.tag}' in memory", 200 | "light_blue", 201 | ) 202 | elif args.edge and not args.tag: 203 | if args.src is None or args.dst is None: 204 | raise ValueError( 205 | "[ERROR] Missing required arguments: --src and/or --dst for edge source and destination IDs" 206 | ) 207 | # Process properties mapping 208 | edge_data_columns = ["___src", "___dst"] + [ 209 | props_mapping[key] for key in sorted(props_mapping) 210 | ] 211 | edge_data_indices = [args.src, args.dst] + sorted(props_mapping.keys()) 212 | if with_rank: 213 | edge_data_columns.append("___rank") 214 | edge_data_indices.append(args.rank) 215 | edge_data = df.iloc[:, edge_data_indices] 216 | edge_data.columns = edge_data_columns 217 | # Here you would load edge_data into NebulaGraph under the specified edge type and space 218 | fancy_print( 219 | f"[INFO] Parsed {len(edge_data)} edges '{space}' for edge type '{args.edge}' in memory", 220 | "light_blue", 221 | ) 222 | else: 223 | raise ValueError( 224 | "[ERROR] Specify either --tag for vertex loading or --edge for edge loading, not both" 225 | ) 226 | 227 | # Load data into NebulaGraph 228 | batch_size = args.batch 229 | 230 | QUOTE_VID = "" if is_vid_int else '"' 231 | QUOTE = '"' 232 | 233 | if args.tag: 234 | # Load vertex_data into NebulaGraph under the specified tag and space 235 | # Now prepare INSERT query for vertices in batches 236 | # Example of QUERY: INSERT VERTEX t2 (name, age) VALUES "13":("n3", 12), "14":("n4", 8); 237 | 238 | for i in tqdm(range(0, len(vertex_data), batch_size), desc="Loading Vertices"): 239 | batch = vertex_data.iloc[i : i + batch_size] 240 | prop_columns = [col for col in vertex_data.columns if col != "___vid"] 241 | if len(vertex_data.columns) == 1: 242 | query = f"INSERT VERTEX `{args.tag}` () VALUES " 243 | else: 244 | query = f"INSERT VERTEX `{args.tag}` (`{'`, `'.join(prop_columns)}`) VALUES " 245 | for index, row in batch.iterrows(): 246 | if pd.isna(row["___vid"]) or row["___vid"] == "": 247 | fancy_print( 248 | f"[WARNING] Skipping row with empty VID: {row}", "yellow" 249 | ) 250 | continue 251 | elif isinstance(row["___vid"], str): 252 | raw_vid_str = row["___vid"].strip('"').replace('"', '\\"') 253 | else: 254 | raw_vid_str = str(row["___vid"]) 255 | vid_str = f"{QUOTE_VID}{raw_vid_str}{QUOTE_VID}" 256 | 257 | prop_str = "" 258 | if with_props: 259 | for prop_name in prop_columns: 260 | prop_value = row[prop_name] 261 | if pd.isnull(prop_value): 262 | if not prop_schema_map[prop_name]["nullable"]: 263 | raise ValueError( 264 | f"Error: Property '{prop_name}' is not nullable but received NULL value, " 265 | f"data: {row}, column: {prop_name}" 266 | ) 267 | prop_str += "NULL, " 268 | elif prop_schema_map[prop_name]["type"] == "string": 269 | raw_prop_str = ( 270 | prop_value.strip('"') 271 | .replace('"', '\\"') 272 | .replace("\n", "\\n") 273 | ) 274 | prop_str += f"{QUOTE}{raw_prop_str}{QUOTE}, " 275 | elif prop_schema_map[prop_name]["type"] == "date": 276 | prop_str += f"date({QUOTE}{prop_value}{QUOTE}), " 277 | elif prop_schema_map[prop_name]["type"] == "datetime": 278 | prop_str += f"datetime({QUOTE}{prop_value}{QUOTE}), " 279 | elif prop_schema_map[prop_name]["type"] == "time": 280 | prop_str += f"time({QUOTE}{prop_value}{QUOTE}), " 281 | elif prop_schema_map[prop_name]["type"] == "timestamp": 282 | prop_str += f"timestamp({QUOTE}{prop_value}{QUOTE}), " 283 | else: 284 | prop_str += f"{prop_value}, " 285 | prop_str = prop_str[:-2] 286 | query += f"{vid_str}:({prop_str}), " 287 | query = query[:-2] + ";" 288 | try: 289 | execute_fn(query) 290 | except Exception as e: 291 | raise Exception(f"INSERT Failed on row {i + index}, data: {row}") from e 292 | tqdm.write(f"Loaded {i + len(batch)} of {len(vertex_data)} vertices") 293 | 294 | fancy_print( 295 | f"[INFO] Successfully loaded {len(vertex_data)} vertices '{space}' for tag '{args.tag}'", 296 | "green", 297 | ) 298 | elif args.edge: 299 | # Load edge_data into NebulaGraph under the specified edge type and space 300 | # Now prepare INSERT query for edges in batches 301 | # Example of QUERY: 302 | # with_rank INSERT EDGE e1 (name, age) VALUES "13" -> "14"@1:("n3", 12), "14" -> "15"@132:("n4", 8); 303 | # without_rank INSERT EDGE e1 (name, age) VALUES "13" -> "14":("n3", 12), "14" -> "15":("n4", 8); 304 | 305 | for i in tqdm(range(0, len(edge_data), batch_size), desc="Loading Edges"): 306 | batch = edge_data.iloc[i : i + batch_size] 307 | prop_columns = [ 308 | col 309 | for col in edge_data.columns 310 | if col not in ["___src", "___dst", "___rank"] 311 | ] 312 | if len(prop_columns) == 0: 313 | query = f"INSERT EDGE `{args.edge}` () VALUES " 314 | else: 315 | query = ( 316 | f"INSERT EDGE `{args.edge}` (`{'`, `'.join(prop_columns)}`) VALUES " 317 | ) 318 | for index, row in batch.iterrows(): 319 | if pd.isna(row["___src"]) or row["___src"] == "": 320 | fancy_print( 321 | f"[WARNING] Skipping row with empty source VID: {row}", "yellow" 322 | ) 323 | continue 324 | elif isinstance(row["___src"], str): 325 | raw_src_str = row["___src"].strip('"').replace('"', '\\"') 326 | else: 327 | raw_src_str = str(row["___src"]) 328 | src_str = f"{QUOTE_VID}{raw_src_str}{QUOTE_VID}" 329 | 330 | if pd.isna(row["___dst"]) or row["___dst"] == "": 331 | fancy_print( 332 | f"[WARNING] Skipping row with empty destination VID: {row}", 333 | "yellow", 334 | ) 335 | continue 336 | elif isinstance(row["___dst"], str): 337 | raw_dst_str = row["___dst"].strip('"').replace('"', '\\"') 338 | else: 339 | raw_dst_str = str(row["___dst"]) 340 | dst_str = f"{QUOTE_VID}{raw_dst_str}{QUOTE_VID}" 341 | prop_str = "" 342 | if with_props: 343 | for prop_name in prop_columns: 344 | prop_value = row[prop_name] 345 | if pd.isnull(prop_value): 346 | if not prop_schema_map[prop_name]["nullable"]: 347 | raise ValueError( 348 | f"Error: Property '{prop_name}' is not nullable but received NULL value, " 349 | f"data: {row}, column: {prop_name}" 350 | ) 351 | prop_str += "NULL, " 352 | elif prop_schema_map[prop_name]["type"] == "string": 353 | raw_prop_str = ( 354 | prop_value.strip('"') 355 | .replace('"', '\\"') 356 | .replace("\n", "\\n") 357 | ) 358 | prop_str += f"{QUOTE}{raw_prop_str}{QUOTE}, " 359 | elif prop_schema_map[prop_name]["type"] == "date": 360 | prop_str += f"date({QUOTE}{prop_value}{QUOTE}), " 361 | elif prop_schema_map[prop_name]["type"] == "datetime": 362 | prop_str += f"datetime({QUOTE}{prop_value}{QUOTE}), " 363 | elif prop_schema_map[prop_name]["type"] == "time": 364 | prop_str += f"time({QUOTE}{prop_value}{QUOTE}), " 365 | elif prop_schema_map[prop_name]["type"] == "timestamp": 366 | prop_str += f"timestamp({QUOTE}{prop_value}{QUOTE}), " 367 | else: 368 | prop_str += f"{prop_value}, " 369 | prop_str = prop_str[:-2] 370 | if with_rank: 371 | rank_str = f"@{row['___rank']}" 372 | else: 373 | rank_str = "" 374 | query += f"{src_str} -> {dst_str}{rank_str}:({prop_str}), " 375 | query = query[:-2] + ";" 376 | try: 377 | execute_fn(query) 378 | except Exception as e: 379 | raise Exception(f"INSERT Failed on row {i + index}, data: {row}") from e 380 | tqdm.write(f"Loaded {i + len(batch)} of {len(edge_data)} edges") 381 | fancy_print( 382 | f"[INFO] Successfully loaded {len(edge_data)} edges '{space}' for edge type '{args.edge}'", 383 | "green", 384 | ) 385 | 386 | 387 | if __name__ == "__main__": 388 | conn_pool = ConnectionPool() 389 | conn_pool.init(addresses=[("127.0.0.1", 9669)], configs=NebulaConfig()) 390 | 391 | def args_load(line: str): 392 | kv_str = line.strip().split("--") 393 | kv = { 394 | k.strip(): v.strip() 395 | for k, v in [item.strip().split(" ") for item in kv_str if item] 396 | } 397 | if "header" in kv: 398 | kv["header"] = kv["header"] == "True" or kv["header"] == "true" 399 | for int_string in ["batch", "limit", "vid", "src", "dst", "rank"]: 400 | if int_string in kv: 401 | kv[int_string] = int(kv[int_string]) 402 | return LoadDataArgsModel.model_validate(kv) 403 | 404 | test = """ 405 | # https://graph-hub.siwei.io/en/stable/datasets/shareholding 406 | %ng_load --source https://github.com/wey-gu/awesome-graph-dataset/raw/main/datasets/shareholding/tiny/person.csv --tag person --vid 0 --props 1:name --space shareholding 407 | %ng_load --source https://github.com/wey-gu/awesome-graph-dataset/raw/main/datasets/shareholding/tiny/corp.csv --tag corp --vid 0 --props 1:name --space shareholding 408 | %ng_load --source https://github.com/wey-gu/awesome-graph-dataset/raw/main/datasets/shareholding/tiny/person_corp_role.csv --edge role_as --src 0 --dst 1 --props 2:role --space shareholding 409 | %ng_load --source https://github.com/wey-gu/awesome-graph-dataset/raw/main/datasets/shareholding/tiny/corp_rel.csv --edge is_branch_of --src 0 --dst 1 --space shareholding 410 | %ng_load --source https://github.com/wey-gu/awesome-graph-dataset/raw/main/datasets/shareholding/tiny/corp_share.csv --edge hold_share --src 0 --dst 1 --props 2:share --space shareholding 411 | %ng_load --source https://github.com/wey-gu/awesome-graph-dataset/raw/main/datasets/shareholding/tiny/person_corp_share.csv --edge hold_share --src 0 --dst 1 --props 2:share --space shareholding 412 | %ng_load --source https://github.com/wey-gu/awesome-graph-dataset/raw/main/datasets/shareholding/tiny/person_rel.csv --edge reletive_with --src 0 --dst 1 --props 2:degree --space shareholding 413 | %ng_load --header --source https://github.com/microsoft/graphrag/raw/main/examples_notebooks/inputs/operation%20dulce/create_final_entities.parquet --tag entity --vid 1 --props 1:name --space ms_paper 414 | """ 415 | execute_fn = conn_pool.get_session("root", "nebula").execute 416 | for line in test.split("\n"): 417 | if line.startswith("%ng_load"): 418 | args = args_load(line[9:]) 419 | ng_load(execute_fn, args) 420 | -------------------------------------------------------------------------------- /ngql/types.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import Optional 3 | 4 | 5 | class LoadDataArgsModel(BaseModel): 6 | source: str 7 | space: str 8 | # Args to load data 9 | batch: int = 100 10 | header: bool = False 11 | limit: Optional[int] = None 12 | # Args of data mapping 13 | tag: Optional[str] = None 14 | edge: Optional[str] = None 15 | vid: Optional[int] = None 16 | src: Optional[int] = None 17 | dst: Optional[int] = None 18 | props: Optional[str] = None 19 | rank: Optional[int] = None 20 | -------------------------------------------------------------------------------- /ngql/utils.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | from typing import Any, ClassVar, Dict, Optional 3 | 4 | 5 | class FancyPrinter: 6 | pp = pprint.PrettyPrinter(indent=2, sort_dicts=False) 7 | # Thanks to https://www.learnui.design/tools/data-color-picker.html 8 | COLORS_rgb: ClassVar[Dict[str, str]] = { 9 | "dark_blue": "38;2;0;63;92", 10 | "blue": "38;2;47;75;124", 11 | "light_blue": "38;2;0;120;215", 12 | "green": "38;2;0;135;107", 13 | "light_green": "38;2;102;187;106", 14 | "purple": "38;2;102;81;145", 15 | "magenta": "38;2;160;81;149", 16 | "pink": "38;2;212;80;135", 17 | "red": "38;2;249;93;106", 18 | "orange": "38;2;255;124;67", 19 | "yellow": "38;2;255;166;0", 20 | } 21 | 22 | color_idx: int = 0 23 | 24 | def __call__(self, val: Any, color: Optional[str] = None): 25 | if color in self.COLORS_rgb: 26 | self.color_idx = list(self.COLORS_rgb.keys()).index(color) 27 | color = self.COLORS_rgb[color] 28 | else: 29 | self.color_idx += 1 30 | self.color_idx %= len(self.COLORS_rgb) 31 | color = list(self.COLORS_rgb.values())[self.color_idx] 32 | 33 | if isinstance(val, str): 34 | print(f"\033[1;3;{color}m{val}\033[0m") 35 | else: 36 | text = self.pp.pformat(val) 37 | print(f"\033[1;3;{color}m{text}\033[0m") 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Jinja2 2 | nebula3-python>=3.8.0 3 | pandas 4 | tqdm 5 | pyvis 6 | requests 7 | pydantic 8 | scipy -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="jupyter_nebulagraph", 8 | version="0.14.3", 9 | author="Wey Gu", 10 | author_email="weyl.gu@gmail.com", 11 | description="Jupyter extension for NebulaGraph", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/wey-gu/jupyter_nebulagraph", 15 | project_urls={ 16 | "Bug Tracker": "https://github.com/wey-gu/jupyter_nebulagraph/issues", 17 | "Documentation": "https://jupyter-nebulagraph.readthedocs.io/", 18 | }, 19 | classifiers=[ 20 | "Programming Language :: Python :: 3", 21 | "License :: OSI Approved :: Apache Software License", 22 | "Operating System :: OS Independent", 23 | ], 24 | packages=setuptools.find_packages(), 25 | python_requires=">=3.6", 26 | install_requires=[ 27 | "Jinja2", 28 | "nebula3-python>=3.8.0", 29 | "pandas", 30 | "tqdm", 31 | "pyvis", 32 | "requests", 33 | "pydantic", 34 | "scipy", 35 | "ipywidgets", 36 | "pyarrow", 37 | ], 38 | ) 39 | -------------------------------------------------------------------------------- /setup_ipython.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="ipython-ngql", 8 | version="0.14.3", 9 | author="Wey Gu", 10 | author_email="weyl.gu@gmail.com", 11 | description="Jupyter extension for NebulaGraph", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/wey-gu/jupyter_nebulagraph", 15 | project_urls={ 16 | "Bug Tracker": "https://github.com/wey-gu/jupyter_nebulagraph/issues", 17 | "Documentation": "https://jupyter-nebulagraph.readthedocs.io/", 18 | }, 19 | classifiers=[ 20 | "Programming Language :: Python :: 3", 21 | "License :: OSI Approved :: Apache Software License", 22 | "Operating System :: OS Independent", 23 | ], 24 | packages=setuptools.find_packages(), 25 | python_requires=">=3.6", 26 | install_requires=[ 27 | "Jinja2", 28 | "nebula3-python>=3.8.0", 29 | "pandas", 30 | "tqdm", 31 | "pyvis", 32 | "requests", 33 | "pydantic", 34 | "scipy", 35 | "ipywidgets", 36 | "pyarrow", 37 | ], 38 | ) 39 | --------------------------------------------------------------------------------