Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Neo4j starter kit #34

Merged
merged 13 commits into from
Jan 8, 2025
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ For more information on getting started with Modus, check out

The following recipes have associated recorded content:

| Code | Video |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| [modus101](modus101/) | [Introducing ModusHack: Modus 101 livestream](https://www.youtube.com/watch?v=8vgXmZPKjbo) |
| [modus-getting-started](modus-getting-started/) | [Getting Started With Modus video](https://www.youtube.com/watch?v=3CcJTXTmz88) |
| [modushack-data-models](modushack-data-models/) | [ModusHack: Working With Data & AI Models livestream](https://www.youtube.com/watch?v=gB-v7YWwkCw&list=PLzOEKEHv-5e3zgRGzDysyUm8KQklHQQgi&index=3) |
| [modus-press](modus-press/) | Coming soon |
| [dgraph-101](dgraph-101/) | [Working with Dgraph in Modus](https://youtu.be/Z2fB-nBf4Wo) |
| [function-calling](function-calling/) | [Use LLM function calling aka tools in Modus.](https://youtu.be/afFk7JzSIm0) |
| [instant-vector-search](instant-vector-search) | [Instant vector search with Modus & Hypermode](https://www.youtube.com/watch?v=4H_xPTUbwL8) |
| Code | Video |
| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| [modus101](modus101/) | [Introducing ModusHack: Modus 101 livestream](https://www.youtube.com/watch?v=8vgXmZPKjbo) |
| [modus-getting-started](modus-getting-started/) | [Getting Started With Modus video](https://www.youtube.com/watch?v=3CcJTXTmz88) |
| [modushack-data-models](modushack-data-models/) | [ModusHack: Working With Data & AI Models livestream](https://www.youtube.com/watch?v=gB-v7YWwkCw&list=PLzOEKEHv-5e3zgRGzDysyUm8KQklHQQgi&index=3) |
| [modus-press](modus-press/) | Coming soon |
| [dgraph-101](dgraph-101/) | [Working with Dgraph in Modus](https://youtu.be/Z2fB-nBf4Wo) |
| [function-calling](function-calling/) | [Use LLM function calling aka tools in Modus.](https://youtu.be/afFk7JzSIm0) |
| [instant-vector-search](instant-vector-search) | [Instant vector search with Modus & Hypermode](https://www.youtube.com/watch?v=4H_xPTUbwL8) |
| [neo4j-modus-starter-kit](neo4j/neo4j-modus-starter-kit/) | [Hypermode Live: Knowledge Graphs + AI](https://www.youtube.com/live/22g-QZPvFPw?si=IAG46M9K6atKv3vD&t=1471) |
15 changes: 15 additions & 0 deletions neo4j/neo4j-modus-starter-kit/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Ignore macOS system files
.DS_Store

# Ignore environment variable files
.env
.env.*

# Ignore build output directories
build/

# Ignore node_modules folders
node_modules/

# Ignore logs generated by as-test
logs/
67 changes: 67 additions & 0 deletions neo4j/neo4j-modus-starter-kit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Neo4j Modus Starter Kit

A full stack app showing how to use the Neo4j knowledge graph with Modus and AI models.

To get started:

- Create a free [Neo4j Sandbox instance](https://sandbox.neo4j.com)

- Add your Neo4j credentials to `.env`

```env
MODUS_NEO4J_NEO4J_URI=<YOUR_NEO4J_CONNECTION_URI_HERE>
MODUS_NEO4J_USERNAME=<YOUR_NEO4J_USER_HERE>
MODUS_NEO4J_PASSWORD=<YOUR_NEO4J_PASSWORD_HERE>
```

- Install the Modus CLI

```bash
npm i -g @hypermode/modus-cli
```

- Install the Hyp CLI and sign in to Hypermode

By default we can take advantage of shared model hosting on Hypermode

```bash
npm i -g @hypermode/hyp-cli
```

- Start your Modus app

```shell
modus dev
```

This will start your Modus app locally

- Open the API Explorer

Open `http://localhost:8686/graphql` in a web browser

First, run the `saveEmbeddingsToNeo4j` mutation to generate movie embeddings and save to Neo4j.

```GraphQL
mutation {
saveEmbeddingsToNeo4j
}
```

Then search for movies using the `findSimilarMovies` Query field

```GraphQL
query($title: String!, $num: Int!) {
findSimilarMovies(title: $title, num: $num) {
movie {
id
title
plot
rating
}
score
}
}
```

![Searching for similar movies using the Modus API Explorer](img/apiExplorer.png)
6 changes: 6 additions & 0 deletions neo4j/neo4j-modus-starter-kit/asconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "./node_modules/@hypermode/modus-sdk-as/plugin.asconfig.json",
"options": {
"transform": ["@hypermode/modus-sdk-as/transform", "json-as/transform"]
}
}
35 changes: 35 additions & 0 deletions neo4j/neo4j-modus-starter-kit/assembly/classes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

@json
/**
* A Movie
*/
export class Movie {
id!: string
title!: string
plot!: string
rating!: f32
embedding: f32[] = []

constructor(id: string, title: string, plot: string, rating: f32) {
this.id = id
this.title = title
this.plot = plot
this.rating = rating
this.embedding = []
}
}


@json
/**
* Results of a movie search, includes movie details and a similarity score
*/
export class MovieResult {
movie!: Movie
score: f32 = 0.0

constructor(movie: Movie, score: f32) {
this.movie = movie
this.score = score
}
}
141 changes: 141 additions & 0 deletions neo4j/neo4j-modus-starter-kit/assembly/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { neo4j } from "@hypermode/modus-sdk-as"
import { models } from "@hypermode/modus-sdk-as"
import { EmbeddingsModel } from "@hypermode/modus-sdk-as/models/experimental/embeddings"
import { Movie, MovieResult } from "./classes"
import { JSON } from "json-as"

// Should match the name of the Neo4j connection declared in modus.json
const hostName = "neo4j"

/**
* Create embeddings using the minilm model for an array of texts
*/
export function generateEmbeddings(texts: string[]): f32[][] {
const model = models.getModel<EmbeddingsModel>("minilm")
const input = model.createInput(texts)
const output = model.invoke(input)
return output.predictions
}

/**
*
* Create embeddings for an array of movies
*
*/
export function getEmbeddingsForMovies(movies: Movie[]): Movie[] {
const texts: string[] = []

for (let i = 0; i < movies.length; i++) {
texts.push(movies[i].plot)
}

const embeddings = generateEmbeddings(texts)

for (let i = 0; i < movies.length; i++) {
movies[i].embedding = embeddings[i]
}

return movies
}

/**
*
* Update movie nodes in Neo4j with generated embeddings and create a vector index
*
*/
export function saveEmbeddingsToNeo4j(count: i32): i32 {
const query = `
MATCH (m:Movie)
WHERE m.embedding IS NULL AND m.plot IS NOT NULL AND m.imdbRating > 0.0
RETURN m.imdbRating AS rating, m.title AS title, m.plot AS plot, m.imdbId AS id
johnymontana marked this conversation as resolved.
Show resolved Hide resolved
ORDER BY m.imdbRating DESC
LIMIT toInteger($count)`

const countVars = new neo4j.Variables()
countVars.set("count", count)
const result = neo4j.executeQuery(hostName, query, countVars)

const movies: Movie[] = []

// Here we iterate through each row returned and explicitly access each column value
// An alternative would be to return an object from the Cypher query and use JSON.parse to handle type marshalling
// see findSimilarMovies function below for an example of this approach
for (let i = 0; i < result.Records.length; i++) {
const record = result.Records[i]
const plot = record.getValue<string>("plot")
const rating = record.getValue<f32>("rating")
const title = record.getValue<string>("title")
const id = record.getValue<string>("id")
movies.push(new Movie(id, title, plot, rating))
}

// Batch calls to embedding model in chunks of 100

const movieChunks: Movie[][] = []
for (let i = 0; i < movies.length; i += 100) {
movieChunks.push(movies.slice(i, i + 100))
}

for (let i = 0, len = movieChunks.length; i < len; i++) {
let movieChunk = movieChunks[i]

// Generate embeddings for a chunk of movies
const embeddedMovies = getEmbeddingsForMovies(movieChunk)

// Update the Movie.embedding property in Neo4j with the new embedding values
const vars = new neo4j.Variables()
vars.set("movies", embeddedMovies)

const updateQuery = `
UNWIND $movies AS embeddedMovie
MATCH (m:Movie {imdbId: embeddedMovie.id})
SET m.embedding = embeddedMovie.embedding
`

neo4j.executeQuery(hostName, updateQuery, vars)
}

// Create vector index in Neo4j to enable vector search on Movie embeddings
const indexQuery =
"CREATE VECTOR INDEX `movie-index` IF NOT EXISTS FOR (m:Movie) ON (m.embedding)"

neo4j.executeQuery(hostName, indexQuery)

return movies.length
}

/**
* Given a movie title, find similar movies using vector search based on the movie embeddings
*/
export function findSimilarMovies(title: string, num: i16): MovieResult[] {
const vars = new neo4j.Variables()
vars.set("title", title)
vars.set("num", num)

const searchQuery = `
MATCH (m:Movie {title: $title})
WHERE m.embedding IS NOT NULL
CALL db.index.vector.queryNodes('movie-index', $num, m.embedding)
YIELD node AS searchResult, score
WITH * WHERE searchResult <> m
RETURN COLLECT({
movie: {
title: searchResult.title,
plot: searchResult.plot,
rating: searchResult.imdbRating,
id: searchResult.imdbId
},
score: score
}) AS movieResults
`

const results = neo4j.executeQuery(hostName, searchQuery, vars)
let movieResults: MovieResult[] = []

if (results.Records.length > 0) {
const recordResults = results.Records[0].get("movieResults")
movieResults = JSON.parse<MovieResult[]>(recordResults)
}

return movieResults
}
4 changes: 4 additions & 0 deletions neo4j/neo4j-modus-starter-kit/assembly/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "assemblyscript/std/assembly.json",
"include": ["./**/*.ts"]
}
11 changes: 11 additions & 0 deletions neo4j/neo4j-modus-starter-kit/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// @ts-check

import eslint from "@eslint/js"
import tseslint from "typescript-eslint"
import aseslint from "@hypermode/modus-sdk-as/tools/assemblyscript-eslint"

export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
aseslint.config,
)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions neo4j/neo4j-modus-starter-kit/modus.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "https://schema.hypermode.com/modus.json",
"endpoints": {
"default": {
"type": "graphql",
"path": "/graphql",
"auth": "bearer-token"
}
},
"models": {
"minilm": {
"sourceModel": "sentence-transformers/all-MiniLM-L6-v2",
"provider": "hugging-face",
"connection": "hypermode"
}
},
"connections": {
"neo4j": {
"type": "neo4j",
"dbUri": "{{NEO4J_URI}}",
"username": "{{USERNAME}}",
"password": "{{PASSWORD}}"
}
}
}
Loading
Loading