Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Copy to .env and adjust. .env is ignored by git.

# --- Neo4j container ---
NEO4J_USER=neo4j
NEO4J_PASSWORD=change-me-please
NEO4J_HTTP_PORT=7474
NEO4J_BOLT_PORT=7687

# --- Backend connection to Neo4j ---
NEO4J_URI=bolt://localhost:7687
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,29 @@ OVERLAPS

Future versions may explore image embeddings, vector search, semantic relationships, and hybrid visual search.

## Local development

### Neo4j

Requirements: Docker with Compose, and [uv](https://docs.astral.sh/uv/) for the backend.

```bash
cp .env.example .env # then set NEO4J_PASSWORD (.env is not versioned)
docker compose up -d --wait # starts Neo4j and waits until it is healthy
```

* Neo4j Browser: <http://localhost:7474> (Connect URL `bolt://localhost:7687`, user/password from `.env`)
* Data and logs are stored in `./neo4j/` (git-ignored). Stop with `docker compose down`;
reset the database with `docker compose down` followed by deleting `./neo4j/data`.

Verify the backend can connect (needs the container running):

```bash
cd backend
uv sync
uv run pytest -m integration
```

## License

This project is licensed under the MIT License.
3 changes: 2 additions & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies = [
"fastapi>=0.115",
"uvicorn[standard]>=0.30",
"pydantic-settings>=2.4",
"neo4j>=5.26",
]

[project.scripts]
Expand Down Expand Up @@ -47,4 +48,4 @@ select = ["E", "F", "I", "UP"]

[tool.mypy]
files = ["src"]
strict = true
strict = true
9 changes: 7 additions & 2 deletions backend/src/visual_graph/config.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
from functools import lru_cache

from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
model_config = SettingsConfigDict(env_file=("../.env", ".env"), extra="ignore")

app_name: str = "VisualGraph"
api_host: str = "127.0.0.1"
api_port: int = 8000
api_reload: bool = False

neo4j_uri: str = "bolt://localhost:7687"
neo4j_user: str = "neo4j"
neo4j_password: SecretStr | None = None


@lru_cache
def get_settings() -> Settings:
return Settings()
return Settings()
30 changes: 30 additions & 0 deletions backend/src/visual_graph/graph/connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Neo4j driver factory."""

from neo4j import Driver, GraphDatabase

from visual_graph.config import Settings, get_settings


class MissingCredentialsError(RuntimeError):
"""Raised when NEO4J_PASSWORD is not configured."""


def create_driver(settings: Settings | None = None) -> Driver:
settings = settings or get_settings()
if settings.neo4j_password is None:
raise MissingCredentialsError(
"NEO4J_PASSWORD is not set. Copy .env.example to .env and set it."
)
return GraphDatabase.driver(
settings.neo4j_uri,
auth=(settings.neo4j_user, settings.neo4j_password.get_secret_value()),
)


def verify_connection(driver: Driver) -> None:
"""Raise if the database is unreachable or credentials are wrong."""
driver.verify_connectivity()
with driver.session() as session:
record = session.run("RETURN 1 AS ok").single()
if record is None or record["ok"] != 1:
raise RuntimeError("Unexpected response from Neo4j")
13 changes: 13 additions & 0 deletions backend/tests/integration/test_neo4j_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import pytest

from visual_graph.graph.connection import create_driver, verify_connection

pytestmark = pytest.mark.integration


def test_backend_can_connect_to_neo4j() -> None:
driver = create_driver()
try:
verify_connection(driver)
finally:
driver.close()
10 changes: 10 additions & 0 deletions backend/tests/unit/test_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import pytest

from visual_graph.config import Settings
from visual_graph.graph.connection import MissingCredentialsError, create_driver


def test_missing_password_raises() -> None:
settings = Settings(_env_file=None, neo4j_password=None) # type: ignore[call-arg]
with pytest.raises(MissingCredentialsError):
create_driver(settings)
23 changes: 23 additions & 0 deletions backend/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
services:
neo4j:
image: neo4j:5.26-community
container_name: visual-graph-neo4j
restart: unless-stopped
ports:
- "${NEO4J_HTTP_PORT:-7474}:7474" # Neo4j Browser
- "${NEO4J_BOLT_PORT:-7687}:7687" # Bolt
environment:
NEO4J_AUTH: "${NEO4J_USER:-neo4j}/${NEO4J_PASSWORD:?Set NEO4J_PASSWORD in .env (see .env.example)}"
volumes:
- ./neo4j/data:/data
- ./neo4j/logs:/logs
healthcheck:
test: ["CMD-SHELL", "cypher-shell -u \"$${NEO4J_USER:-neo4j}\" -p \"$${NEO4J_AUTH#*/}\" 'RETURN 1' || exit 1"]
interval: 10s
timeout: 10s
retries: 12
start_period: 20s
Loading