#3660 Neo4j Knowledge Graph GUI
#3660 Neo4j Knowledge Graph GUI
#3660 What Python library is best for creating an interactive GUI for creating and maintaining a knowledge graph using Python and Neo4j?
If you're aiming to build an interactive Python GUI for creating and maintaining a knowledge graph with Neo4j, here are some excellent options depending on your workflow, deployment environment, and goals:
1. neo4j-viz (Python wrapper for Neo4j’s Visualization Library)
-
What: A Python package that wraps the Neo4j Visualization Library (NVL), allowing interactive graph visualization in Jupyter Notebooks and Streamlit apps.
-
Features:
-
Supports node sizing, styling, captions, tooltips, pinning, zooming, panning, different layouts, and more
-
Create visualizations directly from Neo4j query results, GDS projections, or pandas DataFrames
-
Renders as
IPython.display.HTML
for seamless notebook integration Stack Overflow+4GitHub+4Graph Database & Analytics+4
-
-
Best for: Exploratory data work and rapid prototyping in notebook or lightweight web app environments.
2. ipysigma (Interactive Notebook Graph Visualization)
-
What: A Python library for rendering graph visualizations inside Jupyter notebooks, powered by Sigma.js.
-
Features:
-
Supports pan, zoom, hover, dynamic styling
-
Works with Neo4j or NetworkX graphs (though you’ll need to transform Neo4j results) Graph Database & Analytics
-
-
Best for: Interactive notebook exploration, especially for small to medium-size graphs.
3. Pyvis + VisJS
-
What: Pyvis is a Python library that builds on VisJS for interactive graph display (in notebooks or standalone web pages).
-
Features:
-
Declarative interface from Python to VisJS
-
Customizable nodes, edges, layouts, tooltips, and physics-driven interactions GitHub+6Graph Database & Analytics+6arXiv+6Graph Database & Analytics+4arXiv+4Tom Sawyer Software+4
-
-
Best for: Lightweight, web-based interactivity with full control over aesthetics.
4. NeoDash
-
What: A Neo4j Labs tool for quickly building interactive dashboards that incorporate graph visualizations alongside charts and tables.
-
Features:
-
Low-code interface for building dashboards from Cypher query results
-
Mix graph visuals, bar charts, markdown, input parameters; output dashboards for sharing Graph Database & Analytics+1
-
-
Best for: Non-programmer friendly, dashboard-style interfaces for knowledge graph monitoring.
5. Linkurious (Commercial)
-
What: A powerful commercial platform for visualizing and exploring Neo4j (and other graph DBs) using a sleek web-based interface.
-
Features:
-
Case management, detection, exploration tools
-
Used in heavyweight investigations like Panama Papers Stack OverflowWikipedia+11Wikipedia+11Graph Database & Analytics+11
-
-
Best for: Enterprise environments demanding robust, polished, and supported visualization tools.
6. General-Purpose Python GUI Frameworks
-
What: Use frameworks like Bokeh, Panel, or Holoviz to build custom interactive apps integrating Neo4j graph visuals.
-
Features:
-
Bokeh + Panel offer Pythonic control and web deployment options for charts and widgets discuss.python.org
-
-
Best for: Fully custom GUI workflows combining graph visualizations with rich interactive controls.
Feature Comparison
Tool / Library | Integration Style | Interactive Features | Best For... |
---|---|---|---|
neo4j-viz | Notebook / Streamlit | Zoom, pan, tooltips, styling, layouts | Rapid prototyping & interactive exploration |
ipysigma | Jupyter Notebook | Pan, zoom, hover, styling | Notebook-based exploratory visualizations |
Pyvis (VisJS) | Web / Notebook | Physics layouts, tooltips, styling | Lightweight custom web/displays |
NeoDash | Dashboard (low-code) | Composite visuals + widgets | Interactive dashboards from Neo4j |
Linkurious | Web App (commercial) | Advanced UI, case workflows | Enterprise-grade graph visualization |
Bokeh / Panel | Custom Python web GUI | Charts, widgets, control logic | Custom GUI integrating with Neo4j data |
Recommendations by Use Case
-
Exploration / Prototyping:
Use neo4j-viz or ipysigma if you're already working in notebooks and want interactivity built-in. -
Custom Light Web UI:
Pyvis, Bokeh, or Panel are flexible options where you'd code your own GUI logic and visuals. -
Dashboard or Non-Developer Interface:
NeoDash offers low-code construction of interactive dashboards ideal for sharing and quick visual oversight. -
Enterprise & Professional Needs:
If budget and requirements allow, Linkurious gives polished, supported interfaces built for real-world investigative use.
Final Thoughts
If you want a pure Python experience with interactive graph visuals for development or embedded apps, the combination of neo4j-viz for rendering rich visuals and something like Bokeh/Panel for UI logic gives a clean developer stack.
Alternatively, NeoDash provides that functionality with minimal coding, and Linkurious brings enterprise readiness when scale and polish are priorities.
Feel free to share more context—like environment (notebook vs standalone app), target users (developers vs analysts vs end-users), or dataset scale—and I’d be happy to help tailor the perfect stack.
What about for adding nodes, selecting files for emedding, and queries?
Great follow-up. For adding nodes/edges, uploading files to embed, and running queries from a Python GUI, the most productive stacks I recommend are:
Best all-around (fast to build, easy to ship)
Streamlit + Neo4j Python Driver + a graph widget
-
Why: dead-simple forms & file uploads, session state, quick auth, deploy anywhere.
-
Graph widgets:
-
streamlit-cytoscapejs
(stable, interactive) or -
streamlit-agraph
/pyvis
(vis.js under the hood).
-
-
Embeddings:
sentence-transformers
(local) or OpenAI embeddings. -
Perfect for: CRUD panels (add/update/delete nodes/relationships), file chooser for embedding, and an ad-hoc Cypher console.
For a richer web app with fine-grained graph editing
Plotly Dash + dash-cytoscape
-
Why: industrial-strength callbacks; cytoscape supports selecting, adding, deleting, dragging, styling; file uploads with
dcc.Upload
. -
Perfect for: power-user graph editors and multi-page apps.
Notebook-centric prototyping
Jupyter + ipywidgets + ipycytoscape
-
Why: fast experiments; OK for CRUD, uploads, and Cypher, less ideal for end users.
What this looks like (Streamlit pattern)
1) Add nodes/relationships (CRUD)
2) Select files and embed into Neo4j
Notes:
• If you’re on Neo4j 5.18+, use native vector indexes (CREATE VECTOR INDEX …
) and store embeddings asLIST<FLOAT>
.
• For PDFs, swap in PyMuPDF (fitz) for better extraction.
• For very large files, stream chunks and useexecute_write
batching.
3) A simple query console + result viewer
4) Interactive graph panel (one-liner drop-in)
With streamlit-cytoscapejs:
You can wire tap events (node/edge clicks) to populate edit forms and commit MERGEs/DELETEs.
Alternatives at a glance
-
Dash + dash-cytoscape: most control for graph editing (add/delete nodes & edges via callbacks), plus
dcc.Upload
for files and a Cypher console. Great when you want a multi-page app with role-based UI. -
NiceGUI (FastAPI under the hood): modern Python-only web UI, easy dialogs/file pickers, embed a cytoscape or vis.js component in a few lines.
-
Gradio: super fast prototypes for upload → embed → query workflows; less flexible for live graph editing.
Recommendation for you
Given your pace and Neo4j work, I’d start with Streamlit for speed, and add:
-
CRUD forms for labels/properties/relationships,
-
File uploader + embedding pipeline,
-
Cypher console, and
-
Cytoscape graph for visual editing.
If you outgrow it, port to Dash + dash-cytoscape for finer control.
If you want, I can package a minimal project scaffold (Streamlit + neo4j driver + embeddings + cytoscape) so you can run streamlit run app.py
and have all three: add, embed, query.
Do it
# 🧰 Neo4j Knowledge Graph Admin – Streamlit Scaffold
A minimal, production‑ready scaffold for **adding/editing nodes & relationships**, **uploading files for embedding**, and **running Cypher queries** against **Neo4j** — all in a single **Streamlit** app with an interactive **Cytoscape** graph panel.
---
## 📁 Project layout
```
kg-admin/
├─ app.py
├─ requirements.txt
├─ README.md
├─ .streamlit/
│ └─ secrets.toml # add your Neo4j creds here
└─ utils/
├─ __init__.py
└─ embedding.py
```
---
## 🔐 .streamlit/secrets.toml (template)
```toml
# Create this file locally at: .streamlit/secrets.toml
NEO4J_URI = "neo4j+s://<hosted-instance-or-bolt-uri>"
NEO4J_USER = "neo4j"
NEO4J_PASS = "password"
# Optional: tweak sizes
CHUNK_SIZE = 800
CHUNK_OVERLAP = 120
EMBED_MODEL = "sentence-transformers/all-MiniLM-L6-v2" # 384-dim
VECTOR_INDEX_NAME = "chunk_embed_index"
```
---
## 📦 requirements.txt
```txt
streamlit>=1.36
neo4j>=5.20
streamlit-cytoscapejs>=0.0.5
sentence-transformers>=3.0.1
numpy>=1.26
pandas>=2.0
pymupdf>=1.24.9
pdfminer.six>=20231228
python-dotenv>=1.0
```
---
## 🧠 utils/embedding.py
```python
from __future__ import annotations
import io
from typing import List, Tuple
import numpy as np
# PDF/text extraction
import fitz # PyMuPDF
from pdfminer.high_level import extract_text as pdfminer_extract
from sentence_transformers import SentenceTransformer
def load_model(model_name: str = "sentence-transformers/all-MiniLM-L6-v2") -> SentenceTransformer:
return SentenceTransformer(model_name)
def extract_text(file_bytes: bytes, mime: str) -> str:
"""Return plain text from uploaded file by MIME type."""
if mime == "application/pdf":
# Use PyMuPDF first for speed/quality; fallback to pdfminer if needed
try:
doc = fitz.open(stream=file_bytes, filetype="pdf")
text = []
for page in doc:
text.append(page.get_text())
return "\n".join(text)
except Exception:
return pdfminer_extract(io.BytesIO(file_bytes))
# default: any text-like
return file_bytes.decode("utf-8", errors="ignore")
def chunk_text(text: str, size: int = 800, overlap: int = 120) -> List[str]:
words = text.split()
if not words:
return []
chunks, i = [], 0
step = max(1, size - overlap)
while i < len(words):
chunk = " ".join(words[i:i + size])
chunks.append(chunk)
i += step
return chunks
def embed_chunks(model: SentenceTransformer, chunks: List[str]) -> np.ndarray:
if not chunks:
return np.zeros((0, 0), dtype=np.float32)
emb = model.encode(chunks, convert_to_numpy=True, normalize_embeddings=True)
return emb.astype("float32")
def prepare_embeddings(text: str, model: SentenceTransformer, size: int, overlap: int) -> Tuple[List[str], np.ndarray]:
chunks = chunk_text(text, size=size, overlap=overlap)
embs = embed_chunks(model, chunks)
return chunks, embs
```
```
---
## 🚀 app.py
```python
from __future__ import annotations
import json
import uuid
from typing import Any, Dict, List
import numpy as np
import pandas as pd
import streamlit as st
from neo4j import GraphDatabase
from streamlit_cytoscapejs import cytoscape
from utils.embedding import load_model, extract_text, prepare_embeddings
# -------------------------
# Config / Secrets
# -------------------------
st.set_page_config(page_title="KG Admin", layout="wide")
URI = st.secrets.get("NEO4J_URI")
USER = st.secrets.get("NEO4J_USER")
PASS = st.secrets.get("NEO4J_PASS")
CHUNK_SIZE = int(st.secrets.get("CHUNK_SIZE", 800))
CHUNK_OVERLAP = int(st.secrets.get("CHUNK_OVERLAP", 120))
EMBED_MODEL = st.secrets.get("EMBED_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
VECTOR_INDEX = st.secrets.get("VECTOR_INDEX_NAME", "chunk_embed_index")
if not all([URI, USER, PASS]):
st.error("Please configure NEO4J_URI / NEO4J_USER / NEO4J_PASS in .streamlit/secrets.toml")
st.stop()
# -------------------------
# Neo4j Driver Helpers
# -------------------------
@st.cache_resource(show_spinner=False)
def get_driver():
return GraphDatabase.driver(URI, auth=(USER, PASS))
def run_query(query: str, params: Dict[str, Any] | None = None) -> List[Dict[str, Any]]:
with get_driver().session() as sess:
res = sess.run(query, params or {})
return [r.data() for r in res]
def write_query(query: str, params: Dict[str, Any] | None = None) -> List[Dict[str, Any]]:
with get_driver().session() as sess:
res = sess.execute_write(lambda tx: list(tx.run(query, params or {})))
return [r.data() for r in res]
# -------------------------
# Vector Index bootstrap
# -------------------------
VECTOR_DIM = 384 # for all-MiniLM-L6-v2
def ensure_vector_index():
cypher = f"""
CREATE VECTOR INDEX {VECTOR_INDEX} IF NOT EXISTS
FOR (c:Doc_Chunk) ON (c.embed)
OPTIONS {{ indexConfig: {{ `vector.dimensions`: {VECTOR_DIM}, `vector.similarity_function`: 'cosine' }} }}
"""
write_query(cypher)
# -------------------------
# Sidebar – Connection & Tools
# -------------------------
with st.sidebar:
st.title("KG Admin")
st.caption("Neo4j + Streamlit scaffold")
st.markdown("### Connection")
if st.button("Test connection"):
try:
v = run_query("RETURN 1 AS ok")
st.success(f"OK → {v}")
except Exception as e:
st.exception(e)
st.markdown("---")
st.markdown("### Setup vector index")
if st.button("Ensure vector index"):
try:
ensure_vector_index()
st.success("Vector index ensured.")
except Exception as e:
st.exception(e)
# -------------------------
# Tabs
# -------------------------
T1, T2, T3, T4 = st.tabs([
"➕ Add/Edit", "📄 Upload & Embed", "🔎 Query Console", "🕸️ Graph"
])
# -------------------------
# Tab 1 – Add/Edit Nodes & Relationships
# -------------------------
with T1:
st.subheader("Create / Update Node")
colA, colB = st.columns(2)
with colA:
n_label = st.text_input("Label", value="Document")
key_prop = st.text_input("Key property name", value="id")
key_val = st.text_input("Key value", value=str(uuid.uuid4()))
with colB:
props_json = st.text_area("Additional Properties (JSON)", value='{"name":"Sample"}')
if st.button("MERGE Node"):
try:
props = json.loads(props_json or "{}")
props[key_prop] = key_val
q = f"MERGE (n:{n_label} {{{key_prop}: $key}}) SET n += $props RETURN labels(n) AS labels, n AS node"
data = write_query(q, {"key": key_val, "props": props})
st.success("Node merged.")
st.json(data)
except Exception as e:
st.exception(e)
st.markdown("---")
st.subheader("Create Relationship")
c1, c2 = st.columns(2)
with c1:
a_label = st.text_input("From Label", value="Document")
a_key_prop = st.text_input("From Key Prop", value="id")
a_key_val = st.text_input("From Key Val", value="")
rel_type = st.text_input("REL TYPE", value="HAS_CHUNK")
with c2:
b_label = st.text_input("To Label", value="Doc_Chunk")
b_key_prop = st.text_input("To Key Prop", value="id")
b_key_val = st.text_input("To Key Val", value="")
rel_props_json = st.text_area("Rel Properties (JSON)", value='{}')
if st.button("MERGE Relationship"):
try:
rprops = json.loads(rel_props_json or "{}")
q = f"""
MATCH (a:{a_label} {{{a_key_prop}: $a}}), (b:{b_label} {{{b_key_prop}: $b}})
MERGE (a)-[r:{rel_type}]->(b)
SET r += $rprops
RETURN type(r) AS rel, r AS properties
"""
data = write_query(q, {"a": a_key_val, "b": b_key_val, "rprops": rprops})
st.success("Relationship merged.")
st.json(data)
except Exception as e:
st.exception(e)
# -------------------------
# Tab 2 – Upload & Embed
# -------------------------
with T2:
st.subheader("Upload a file → chunk → embed → ingest")
up = st.file_uploader("Choose a file", type=["txt", "md", "pdf"])
cs, co = st.columns(2)
with cs:
size = st.number_input("Chunk size (words)", 100, 4000, CHUNK_SIZE, 50)
with co:
overlap = st.number_input("Chunk overlap (words)", 0, 4000, CHUNK_OVERLAP, 10)
model = load_model(EMBED_MODEL)
if up and st.button("Embed & Ingest into Neo4j"):
try:
text = extract_text(up.read(), up.type)
chunks, embs = prepare_embeddings(text, model, size=int(size), overlap=int(overlap))
doc_id = str(uuid.uuid4())
# Create Document + Chunks
q_doc = """
MERGE (d:Document {id:$id})
SET d.name=$name, d.source=$src, d.chunk_count=$n
RETURN d
"""
write_query(q_doc, {"id": doc_id, "name": up.name, "src": "upload", "n": len(chunks)})
# Ingest chunks in batches
batch = []
for i, (chunk, vec) in enumerate(zip(chunks, embs)):
batch.append({
"cid": str(uuid.uuid4()),
"idx": i,
"text": chunk,
"emb": vec.tolist(),
"doc": doc_id,
})
# Use UNWIND for efficiency
q_chunks = f"""
UNWIND $rows AS row
MERGE (c:Doc_Chunk {{id: row.cid}})
SET c.idx = row.idx, c.text = row.text, c.embed = row.emb
WITH c, row
MATCH (d:Document {{id: row.doc}})
MERGE (d)-[:HAS_CHUNK]->(c)
RETURN count(*) AS upserts
"""
write_query(q_chunks, {"rows": batch})
st.success(f"Ingested {len(chunks)} chunks for '{up.name}' (doc_id={doc_id}).")
st.info("If it's your first run, click ‘Ensure vector index’ in the sidebar.")
except Exception as e:
st.exception(e)
st.markdown("---")
st.subheader("Vector Search (Semantic)")
qtext = st.text_input("Query text", value="sample search")
topk = st.slider("Top K", 1, 50, 5)
if st.button("Search"):
try:
qvec = load_model(EMBED_MODEL).encode([qtext], normalize_embeddings=True)[0].astype("float32").tolist()
cy = f"""
CALL db.index.vector.queryNodes('{VECTOR_INDEX}', $k, $qvec)
YIELD node, score
RETURN node.id AS id, node.idx AS idx, node.text AS text, score
"""
rows = run_query(cy, {"k": int(topk), "qvec": qvec})
df = pd.DataFrame(rows)
st.dataframe(df, use_container_width=True)
except Exception as e:
st.exception(e)
# -------------------------
# Tab 3 – Query Console
# -------------------------
with T3:
st.subheader("Cypher Console")
default_q = """
MATCH (n)
RETURN labels(n) AS labels, n.id AS id, n.name AS name
LIMIT 50
""".strip()
cy = st.text_area("Cypher", value=default_q, height=160)
if st.button("Run"):
try:
rows = run_query(cy)
st.dataframe(pd.DataFrame(rows), use_container_width=True)
except Exception as e:
st.exception(e)
# -------------------------
# Tab 4 – Graph View (Cytoscape)
# -------------------------
with T4:
st.subheader("Interactive Graph")
max_edges = st.slider("Max relationships to visualize", 50, 2000, 500, 50)
cy = f"""
MATCH (a)-[r]->(b)
RETURN labels(a)[0] AS a_lbl, coalesce(a.id, a.name) AS a_id, coalesce(a.name, a.id) AS a_name,
type(r) AS rel,
labels(b)[0] AS b_lbl, coalesce(b.id, b.name) AS b_id, coalesce(b.name, b.id) AS b_name
LIMIT $limit
"""
rows = run_query(cy, {"limit": int(max_edges)})
nodes = {}
edges = []
for rec in rows:
akey = rec["a_id"]; bkey = rec["b_id"]
if akey and akey not in nodes:
nodes[akey] = {"data": {"id": akey, "label": f"{rec['a_lbl']}\n{rec['a_name']}"}}
if bkey and bkey not in nodes:
nodes[bkey] = {"data": {"id": bkey, "label": f"{rec['b_lbl']}\n{rec['b_name']}"}}
edges.append({"data": {"source": akey, "target": bkey, "label": rec["rel"]}})
elements = list(nodes.values()) + edges
cytoscape(
elements,
layout={"name": "cose"},
stylesheet=[
{"selector": "node", "style": {"label": "data(label)", "font-size": 10, "text-wrap": "wrap", "text-max-width": 120}},
{"selector": "edge", "style": {"label": "data(label)", "curve-style": "bezier", "target-arrow-shape": "vee"}},
],
height="700px",
)
st.caption("Tip: Use the Add/Edit tab to modify nodes/edges; refresh this tab to see updates.")
```
---
## 📖 README.md
```markdown
# Neo4j Knowledge Graph Admin – Streamlit Scaffold
A minimal UI to
**add/edit nodes & relationships**,
**upload/embed documents**, and
**run Cypher**
with a live **graph view**.
## Quick Start
1. **Python** 3.10+
2. `python -m venv .venv && source .venv/bin/activate` (Windows: `.venv\\Scripts\\activate`)
3. `pip install -r requirements.txt`
4. Create `.streamlit/secrets.toml` with your Neo4j credentials (see template).
5. (Optional) Click **Ensure vector index** in the sidebar on first run.
6. `streamlit run app.py`
## Notes
- Default embedding model = `all-MiniLM-L6-v2` (384‑dim). Adjust in `secrets.toml`.
- Embeddings stored as `LIST<FLOAT>` in `:Doc_Chunk(embed)`; uses **Neo4j native vector index**.
- For huge docs, raise chunk size carefully; prefer batching and server‑side limits.
- Secure prod creds via environment or Streamlit Cloud secrets; consider Neo4j roles.
## Common Cypher
Create vector index (if not using the sidebar button):
```cypher
CREATE VECTOR INDEX chunk_embed_index IF NOT EXISTS
FOR (c:Doc_Chunk) ON (c.embed)
OPTIONS {indexConfig: {`vector.dimensions`: 384, `vector.similarity_function`: 'cosine'}};
```
Vector search:
```cypher
CALL db.index.vector.queryNodes('chunk_embed_index', 5, $qvec)
YIELD node, score
RETURN node, score;
```
```
---
## ✅ Done
You can now:
- **Add nodes/edges** (MERGE forms)
- **Upload & embed** TXT/MD/PDF into `Document` → `Doc_Chunk` with vector index
- **Run Cypher** in‑app
- **Explore graph** interactively (Cytoscape)
If you want role‑based auth, multi‑page apps, or graph edit by clicks (tap to edit → MERGE), say the word and I’ll extend this scaffold.
Boom — scaffold delivered. It’s in the canvas with:
-
a ready-to-run
app.py
(Streamlit) -
upload → chunk → embed → Neo4j ingest
-
CRUD forms for nodes/relationships
-
a Cypher console
-
an interactive Cytoscape graph view
-
vector index bootstrap + semantic search
How to run
-
create
.streamlit/secrets.toml
(use the template in the canvas) -
pip install -r requirements.txt
-
streamlit run app.py
Want me to add click-to-edit on the graph (tap a node/edge → prefill the form), or role-based auth?
From <https://chatgpt.com/> ChatGPT 5
Comments
Post a Comment