core/apps/webapp/app/lib/neo4j.server.ts
Harshith Mullapudi 56adc246c8 Feat: UI changes
2025-06-10 12:26:04 +05:30

213 lines
6.1 KiB
TypeScript

import neo4j from "neo4j-driver";
import { type RawTriplet } from "~/components/graph/type";
import { env } from "~/env.server";
import { logger } from "~/services/logger.service";
// Create a driver instance
const driver = neo4j.driver(
env.NEO4J_URI,
neo4j.auth.basic(env.NEO4J_USERNAME, env.NEO4J_PASSWORD),
{
maxConnectionPoolSize: 50,
logging: {
level: "info",
logger: (level, message) => {
logger.info(message);
},
},
},
);
let schemaInitialized = false;
// Test the connection
const verifyConnectivity = async () => {
try {
await driver.verifyConnectivity();
logger.info("Connected to Neo4j database");
return true;
} catch (error) {
logger.error("Failed to connect to Neo4j database");
return false;
}
};
// Run a Cypher query
const runQuery = async (cypher: string, params = {}) => {
const session = driver.session();
try {
const result = await session.run(cypher, params);
return result.records;
} catch (error) {
logger.error(`Error running Cypher query: ${cypher} ${error}`);
throw error;
} finally {
await session.close();
}
};
// Get all nodes and relationships for a user
export const getAllNodesForUser = async (userId: string) => {
const session = driver.session();
try {
const result = await session.run(
`MATCH (n)-[r]->(m)
WHERE n.userId = $userId OR m.userId = $userId
RETURN n, r, m`,
{ userId },
);
return result.records;
} catch (error) {
logger.error(`Error getting nodes for user ${userId}: ${error}`);
throw error;
} finally {
await session.close();
}
};
export const getNodeLinks = async (userId: string) => {
const result = await getAllNodesForUser(userId);
const triplets: RawTriplet[] = [];
result.forEach((record) => {
const sourceNode = record.get("n");
const targetNode = record.get("m");
const edge = record.get("r");
triplets.push({
sourceNode: {
uuid: sourceNode.identity.toString(),
labels: sourceNode.labels,
attributes: sourceNode.properties,
name: sourceNode.properties.name || "",
created_at: sourceNode.properties.created_at || "",
updated_at: sourceNode.properties.updated_at || "",
},
edge: {
uuid: edge.identity.toString(),
type: edge.type,
source_node_uuid: sourceNode.identity.toString(),
target_node_uuid: targetNode.identity.toString(),
name: edge.properties.name || "",
created_at: edge.properties.created_at || "",
updated_at: edge.properties.updated_at || "",
},
targetNode: {
uuid: targetNode.identity.toString(),
labels: targetNode.labels,
attributes: targetNode.properties,
name: targetNode.properties.name || "",
created_at: targetNode.properties.created_at || "",
updated_at: targetNode.properties.updated_at || "",
},
});
});
return triplets;
};
export async function initNeo4jSchemaOnce() {
if (schemaInitialized) return;
const session = driver.session();
try {
// Check if schema already exists
const result = await session.run(`
SHOW INDEXES YIELD name WHERE name = "entity_name" RETURN name
`);
if (result.records.length === 0) {
// Run your schema creation here (indexes, constraints, etc.)
await initializeSchema();
}
schemaInitialized = true;
} catch (e: any) {
logger.error("Error in initialising", e);
} finally {
await session.close();
}
}
// Initialize the database schema
const initializeSchema = async () => {
try {
logger.info("Initialising neo4j schema");
// Create constraints for unique IDs
await runQuery(
"CREATE CONSTRAINT episode_uuid IF NOT EXISTS FOR (n:Episode) REQUIRE n.uuid IS UNIQUE",
);
await runQuery(
"CREATE CONSTRAINT entity_uuid IF NOT EXISTS FOR (n:Entity) REQUIRE n.uuid IS UNIQUE",
);
await runQuery(
"CREATE CONSTRAINT statement_uuid IF NOT EXISTS FOR (n:Statement) REQUIRE n.uuid IS UNIQUE",
);
// Create indexes for better query performance
await runQuery(
"CREATE INDEX episode_valid_at IF NOT EXISTS FOR (n:Episode) ON (n.validAt)",
);
await runQuery(
"CREATE INDEX statement_valid_at IF NOT EXISTS FOR (n:Statement) ON (n.validAt)",
);
await runQuery(
"CREATE INDEX statement_invalid_at IF NOT EXISTS FOR (n:Statement) ON (n.invalidAt)",
);
await runQuery(
"CREATE INDEX entity_name IF NOT EXISTS FOR (n:Entity) ON (n.name)",
);
// Create vector indexes for semantic search (if using Neo4j 5.0+)
await runQuery(`
CREATE VECTOR INDEX entity_embedding IF NOT EXISTS FOR (n:Entity) ON n.nameEmbedding
OPTIONS {indexConfig: {\`vector.dimensions\`: 1536, \`vector.similarity_function\`: 'cosine'}}
`);
await runQuery(`
CREATE VECTOR INDEX statement_embedding IF NOT EXISTS FOR (n:Statement) ON n.factEmbedding
OPTIONS {indexConfig: {\`vector.dimensions\`: 1536, \`vector.similarity_function\`: 'cosine'}}
`);
await runQuery(`
CREATE VECTOR INDEX episode_embedding IF NOT EXISTS FOR (n:Episode) ON n.contentEmbedding
OPTIONS {indexConfig: {\`vector.dimensions\`: 1536, \`vector.similarity_function\`: 'cosine'}}
`);
// Create fulltext indexes for BM25 search
await runQuery(`
CREATE FULLTEXT INDEX statement_fact_index IF NOT EXISTS
FOR (n:Statement) ON EACH [n.fact]
OPTIONS {
indexConfig: {
\`fulltext.analyzer\`: 'english'
}
}
`);
await runQuery(`
CREATE FULLTEXT INDEX entity_name_index IF NOT EXISTS
FOR (n:Entity) ON EACH [n.name, n.description]
OPTIONS {
indexConfig: {
\`fulltext.analyzer\`: 'english'
}
}
`);
logger.info("Neo4j schema initialized successfully");
return true;
} catch (error) {
logger.error("Failed to initialize Neo4j schema", { error });
return false;
}
};
// Close the driver when the application shuts down
const closeDriver = async () => {
await driver.close();
logger.info("Neo4j driver closed");
};
export { driver, verifyConnectivity, runQuery, initializeSchema, closeDriver };