# Directory Structure
```
├── .github
│ └── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── docker-compose.yml
├── Dockerfile
├── LICENSE
├── package.json
├── README.md
├── src
│ ├── index.ts
│ ├── logger.ts
│ ├── manager.ts
│ ├── types.ts
│ └── utils.ts
├── test
│ └── test-script.js
├── tsconfig.json
└── tsup.config.ts
```
# Files
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package.json ./
COPY tsconfig.json ./
COPY src ./src
RUN npm install
RUN npm run build
CMD ["npm", "start"]
```
--------------------------------------------------------------------------------
/tsup.config.ts:
--------------------------------------------------------------------------------
```typescript
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
clean: true,
outExtension: () => ({
js: '.mjs',
}),
});
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"declaration": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
```yaml
version: '3'
services:
neo4j:
image: neo4j:5.18.0
container_name: ai-info-neo4j
restart: always
ports:
- "7474:7474" # HTTP
- "7687:7687" # Bolt
environment:
- NEO4J_AUTH=neo4j/password # 用户名/密码
- NEO4J_apoc_export_file_enabled=true
- NEO4J_apoc_import_file_enabled=true
- NEO4J_apoc_import_file_use__neo4j__config=true
- NEO4J_PLUGINS=["apoc"] # 启用APOC插件
- NEO4J_dbms_security_procedures_unrestricted=apoc.*
- NEO4J_dbms_security_procedures_allowlist=apoc.*
volumes:
- neo4j_data:/data
- neo4j_logs:/logs
- neo4j_import:/var/lib/neo4j/import
- neo4j_plugins:/plugins
healthcheck:
test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:7474" ]
interval: 10s
timeout: 10s
retries: 5
start_period: 40s
```
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
// 实体对象模式
export const EntityObject = z.object({
name: z.string().describe("The name of the entity"),
entityType: z.string().describe("The type of the entity"),
observations: z.array(z.string()).describe("An array of observation contents associated with the entity")
});
// 关系对象模式
export const RelationObject = z.object({
from: z.string().describe("The name of the entity where the relation starts"),
to: z.string().describe("The name of the entity where the relation ends"),
relationType: z.string().describe("The type of the relation")
});
// 观察对象模式
export const ObservationObject = z.object({
entityName: z.string().describe("The name of the entity to add the observations to"),
contents: z.array(z.string()).describe("An array of observation contents to add")
});
// 实体类型
export type Entity = z.infer<typeof EntityObject>;
// 关系类型
export type Relation = z.infer<typeof RelationObject>;
// 观察类型
export type Observation = z.infer<typeof ObservationObject>;
// 知识图谱类型
export interface KnowledgeGraph {
entities: Entity[];
relations: Relation[];
}
// 删除观察类型
export interface ObservationDeletion {
entityName: string;
contents: string[];
}
```
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
```yaml
name: Release
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1
with:
publish: pnpm release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
```yaml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
neo4j:
image: neo4j:5-community
env:
NEO4J_AUTH: neo4j/password
ports:
- 7474:7474
- 7687:7687
options: >-
--health-cmd "cypher-shell -u neo4j -p password 'RETURN 1;'"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '22'
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Run tests
run: pnpm test
env:
NEO4J_URI: bolt://localhost:7687
NEO4J_USER: neo4j
NEO4J_PASSWORD: password
NEO4J_DATABASE: neo4j
```
--------------------------------------------------------------------------------
/src/logger.ts:
--------------------------------------------------------------------------------
```typescript
// 日志级别枚举
export enum LogLevel {
DEBUG = "debug",
INFO = "info",
WARN = "warning",
ERROR = "error",
}
// 日志记录器接口
export interface Logger {
debug(message: string, payload?: any): void;
info(message: string, payload?: any): void;
warn(message: string, payload?: any): void;
error(message: string, payload?: any): void;
setLevel(level: LogLevel): void;
}
// 空日志记录器实现
export class NullLogger implements Logger {
debug(message: string, payload?: any): void {}
info(message: string, payload?: any): void {}
warn(message: string, payload?: any): void {}
error(message: string, payload?: any): void {}
setLevel(level: LogLevel): void {}
}
// 控制台日志记录器实现
export class ConsoleLogger implements Logger {
private level: LogLevel = LogLevel.INFO;
setLevel(level: LogLevel): void {
this.level = level;
}
debug(message: string, payload?: any): void {
if (this.shouldLog(LogLevel.DEBUG)) {
console.debug(message, payload);
}
}
info(message: string, payload?: any): void {
if (this.shouldLog(LogLevel.INFO)) {
console.info(message, payload);
}
}
warn(message: string, payload?: any): void {
if (this.shouldLog(LogLevel.WARN)) {
console.warn(message, payload);
}
}
error(message: string, payload?: any): void {
if (this.shouldLog(LogLevel.ERROR)) {
console.error(message, payload);
}
}
private shouldLog(messageLevel: LogLevel): boolean {
const levels = [
LogLevel.DEBUG,
LogLevel.INFO,
LogLevel.WARN,
LogLevel.ERROR,
];
return levels.indexOf(messageLevel) >= levels.indexOf(this.level);
}
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@jovanhsu/mcp-neo4j-memory-server",
"version": "1.0.0",
"private": false,
"description": "MCP Memory Server with Neo4j backend for AI knowledge graph storage",
"homepage": "https://github.com/JovanHsu/mcp-neo4j-memory-server",
"repository": {
"type": "git",
"url": "git+https://github.com/JovanHsu/mcp-neo4j-memory-server.git"
},
"type": "module",
"main": "dist/index.mjs",
"types": "dist/index.d.ts",
"bin": {
"mcp-neo4j-memory-server": "dist/index.mjs"
},
"files": [
"dist",
"README.md",
"LICENSE"
],
"keywords": [
"mcp",
"memory",
"knowledge",
"graph",
"neo4j",
"ai",
"claude",
"anthropic",
"knowledge-graph",
"memory-server",
"model-context-protocol"
],
"author": "JovanHsu",
"license": "MIT",
"bugs": {
"url": "https://github.com/JovanHsu/mcp-neo4j-memory-server/issues"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.0",
"neo4j-driver": "^5.18.0",
"fuse.js": "^7.1.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@changesets/cli": "^2.28.1",
"@types/node": "^22.13.5",
"prettier": "^3.5.2",
"shx": "^0.3.4",
"tsup": "^8.4.0",
"typescript": "^5.7.3",
"vitest": "^3.0.7"
},
"engines": {
"node": ">=22.0.0"
},
"scripts": {
"dev": "pnpm build && npx @modelcontextprotocol/inspector pnpm start",
"build": "tsup src/index.ts && shx chmod +x dist/index.mjs",
"start": "node dist/index.mjs 2>/dev/null",
"test": "vitest run",
"lint": "prettier --write .",
"prepublishOnly": "pnpm build",
"release": "pnpm build && changeset publish"
},
"publishConfig": {
"access": "public"
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { ConsoleLogger, LogLevel } from './logger.js';
import { Neo4jKnowledgeGraphManager } from './manager.js';
import { EntityObject, ObservationObject, RelationObject } from './types.js';
import { extractError } from './utils.js';
// 创建MCP服务器
const server = new McpServer({
name: 'neo4j-memory-server',
version: '1.0.0',
});
// 创建日志记录器,并设置为仅输出错误信息
const logger = new ConsoleLogger();
logger.setLevel(LogLevel.ERROR);
// 创建知识图谱管理器
const knowledgeGraphManager = new Neo4jKnowledgeGraphManager(
/**
* 根据环境变量获取Neo4j配置
* @returns Neo4j配置
*/
() => {
return {
uri: process.env.NEO4J_URI || 'bolt://localhost:7687',
user: process.env.NEO4J_USER || 'neo4j',
password: process.env.NEO4J_PASSWORD || 'password',
database: process.env.NEO4J_DATABASE || 'neo4j',
};
},
logger
);
// 注册创建实体工具
server.tool(
'create_entities',
'Create multiple new entities in the knowledge graph',
{
entities: z.array(EntityObject),
},
async ({ entities }) => ({
content: [
{
type: 'text',
text: JSON.stringify(
await knowledgeGraphManager.createEntities(entities),
null,
2
),
},
],
})
);
// 注册创建关系工具
server.tool(
'create_relations',
'Create multiple new relations between entities in the knowledge graph. Relations should be in active voice',
{
relations: z.array(RelationObject),
},
async ({ relations }) => ({
content: [
{
type: 'text',
text: JSON.stringify(
await knowledgeGraphManager.createRelations(relations),
null,
2
),
},
],
})
);
// 注册添加观察工具
server.tool(
'add_observations',
'Add new observations to existing entities in the knowledge graph',
{
observations: z.array(ObservationObject),
},
async ({ observations }) => ({
content: [
{
type: 'text',
text: JSON.stringify(
await knowledgeGraphManager.addObservations(observations),
null,
2
),
},
],
})
);
// 注册删除实体工具
server.tool(
'delete_entities',
'Delete multiple entities and their associated relations from the knowledge graph',
{
entityNames: z
.array(z.string())
.describe('An array of entity names to delete'),
},
async ({ entityNames }) => {
await knowledgeGraphManager.deleteEntities(entityNames);
return {
content: [{ type: 'text', text: 'Entities deleted successfully' }],
};
}
);
// 注册删除观察工具
server.tool(
'delete_observations',
'Delete specific observations from entities in the knowledge graph',
{
deletions: z.array(
z.object({
entityName: z
.string()
.describe('The name of the entity containing the observations'),
contents: z
.array(z.string())
.describe('An array of observations to delete'),
})
),
},
async ({ deletions }) => {
await knowledgeGraphManager.deleteObservations(deletions);
return {
content: [{ type: 'text', text: 'Observations deleted successfully' }],
};
}
);
// 注册删除关系工具
server.tool(
'delete_relations',
'Delete multiple relations from the knowledge graph',
{
relations: z
.array(
z.object({
from: z
.string()
.describe('The name of the entity where the relation starts'),
to: z
.string()
.describe('The name of the entity where the relation ends'),
relationType: z.string().describe('The type of the relation'),
})
)
.describe('An array of relations to delete'),
},
async ({ relations }) => {
await knowledgeGraphManager.deleteRelations(relations);
return {
content: [{ type: 'text', text: 'Relations deleted successfully' }],
};
}
);
// 注册搜索节点工具
server.tool(
'search_nodes',
'Search for nodes in the knowledge graph based on a query',
{
query: z
.string()
.describe(
'The search query to match against entity names, types, and observation content'
),
},
async ({ query }) => ({
content: [
{
type: 'text',
text: JSON.stringify(
await knowledgeGraphManager.searchNodes(query),
null,
2
),
},
],
})
);
// 注册打开节点工具
server.tool(
'open_nodes',
'Open specific nodes in the knowledge graph by their names',
{
names: z.array(z.string()).describe('An array of entity names to retrieve'),
},
async ({ names }) => ({
content: [
{
type: 'text',
text: JSON.stringify(
await knowledgeGraphManager.openNodes(names),
null,
2
),
},
],
})
);
// 主函数
const main = async () => {
try {
// 初始化知识图谱管理器
await knowledgeGraphManager.initialize();
// 创建传输层
const transport = new StdioServerTransport();
// 连接服务器
await server.connect(transport);
// 使用logger代替console.info
logger.info('Neo4j Knowledge Graph MCP Server running on stdio');
} catch (error) {
// 使用logger代替console.error
logger.error('Failed to start server:', extractError(error));
process.exit(1);
}
};
// 启动服务器
main().catch((error) => {
// 使用logger代替console.error
logger.error('Error during server startup:', extractError(error));
process.exit(1);
});
```
--------------------------------------------------------------------------------
/src/manager.ts:
--------------------------------------------------------------------------------
```typescript
import Fuse from 'fuse.js';
import neo4j, { Driver, Session } from 'neo4j-driver';
import { ConsoleLogger, Logger } from './logger.js';
import { Entity, KnowledgeGraph, Observation, ObservationDeletion, Relation } from './types.js';
import { extractError } from './utils.js';
/**
* Neo4j知识图谱管理器
*/
export class Neo4jKnowledgeGraphManager {
private driver: Driver | null = null;
private fuse: Fuse<Entity>;
private initialized = false;
private logger: Logger;
private uri: string;
private user: string;
private password: string;
private database: string;
/**
* 构造函数
* @param configResolver 配置解析器函数
* @param logger 可选的日志记录器
*/
constructor(
configResolver: () => {
uri: string;
user: string;
password: string;
database: string;
},
logger?: Logger
) {
const config = configResolver();
this.uri = config.uri;
this.user = config.user;
this.password = config.password;
this.database = config.database;
this.logger = logger || new ConsoleLogger();
this.fuse = new Fuse([], {
keys: ['name', 'entityType', 'observations'],
includeScore: true,
threshold: 0.4, // 搜索严格度(越接近0越严格)
});
}
/**
* 获取会话
* @returns Neo4j会话
*/
private async getSession(): Promise<Session> {
if (!this.driver) {
await this.initialize();
}
return this.driver!.session({ database: this.database });
}
/**
* 初始化数据库
*/
public async initialize(): Promise<void> {
if (this.initialized) return;
try {
if (!this.driver) {
this.driver = neo4j.driver(
this.uri,
neo4j.auth.basic(this.user, this.password),
{ maxConnectionLifetime: 3 * 60 * 60 * 1000 } // 3小时
);
}
const session = await this.getSession();
try {
// 创建约束和索引
await session.run(`
CREATE CONSTRAINT entity_name_unique IF NOT EXISTS
FOR (e:Entity) REQUIRE e.name IS UNIQUE
`);
await session.run(`
CREATE INDEX entity_type_index IF NOT EXISTS
FOR (e:Entity) ON (e.entityType)
`);
// 使用新的语法创建全文索引
await session.run(`
CREATE FULLTEXT INDEX entity_fulltext IF NOT EXISTS
FOR (e:Entity)
ON EACH [e.name, e.entityType]
`);
// 加载所有实体到Fuse.js
const entities = await this.getAllEntities();
this.fuse.setCollection(entities);
this.initialized = true;
} finally {
await session.close();
}
} catch (error) {
this.logger.error('Failed to initialize database', extractError(error));
throw error;
}
}
/**
* 获取所有实体
* @returns 所有实体数组
*/
private async getAllEntities(): Promise<Entity[]> {
const session = await this.getSession();
try {
const result = await session.run(`
MATCH (e:Entity)
OPTIONAL MATCH (e)-[r:HAS_OBSERVATION]->(o)
RETURN e.name AS name, e.entityType AS entityType, collect(o.content) AS observations
`);
const entities: Entity[] = result.records.map(record => {
return {
name: record.get('name'),
entityType: record.get('entityType'),
observations: record.get('observations').filter(Boolean)
};
});
return entities;
} catch (error) {
this.logger.error('Error getting all entities', extractError(error));
return [];
} finally {
await session.close();
}
}
/**
* 创建实体
* @param entities 要创建的实体数组
* @returns 创建的实体数组
*/
public async createEntities(entities: Entity[]): Promise<Entity[]> {
if (entities.length === 0) return [];
const session = await this.getSession();
try {
const createdEntities: Entity[] = [];
const tx = session.beginTransaction();
try {
// 获取现有实体名称
const existingEntitiesResult = await tx.run(
'MATCH (e:Entity) RETURN e.name AS name'
);
const existingNames = new Set(
existingEntitiesResult.records.map(record => record.get('name'))
);
// 过滤出新实体
const newEntities = entities.filter(
entity => !existingNames.has(entity.name)
);
// 创建新实体
for (const entity of newEntities) {
await tx.run(
`
CREATE (e:Entity {name: $name, entityType: $entityType})
WITH e
UNWIND $observations AS observation
CREATE (o:Observation {content: observation})
CREATE (e)-[:HAS_OBSERVATION]->(o)
RETURN e
`,
{
name: entity.name,
entityType: entity.entityType,
observations: entity.observations
}
);
createdEntities.push(entity);
}
await tx.commit();
// 更新Fuse.js集合
const allEntities = await this.getAllEntities();
this.fuse.setCollection(allEntities);
return createdEntities;
} catch (error) {
await tx.rollback();
this.logger.error('Error creating entities', extractError(error));
throw error;
}
} finally {
await session.close();
}
}
/**
* 创建关系
* @param relations 要创建的关系数组
* @returns 创建的关系数组
*/
public async createRelations(relations: Relation[]): Promise<Relation[]> {
if (relations.length === 0) return [];
const session = await this.getSession();
try {
const tx = session.beginTransaction();
try {
// 获取所有实体名称
const entityNamesResult = await tx.run(
'MATCH (e:Entity) RETURN e.name AS name'
);
const entityNames = new Set(
entityNamesResult.records.map(record => record.get('name'))
);
// 过滤出有效关系(源实体和目标实体都存在)
const validRelations = relations.filter(
relation => entityNames.has(relation.from) && entityNames.has(relation.to)
);
// 获取现有关系
const existingRelationsResult = await tx.run(`
MATCH (from:Entity)-[r]->(to:Entity)
RETURN from.name AS fromName, to.name AS toName, type(r) AS relationType
`);
const existingRelations = existingRelationsResult.records.map(record => {
return {
from: record.get('fromName'),
to: record.get('toName'),
relationType: record.get('relationType')
};
});
// 过滤出新关系
const newRelations = validRelations.filter(
newRel => !existingRelations.some(
existingRel =>
existingRel.from === newRel.from &&
existingRel.to === newRel.to &&
existingRel.relationType === newRel.relationType
)
);
// 创建新关系
for (const relation of newRelations) {
await tx.run(
`
MATCH (from:Entity {name: $fromName})
MATCH (to:Entity {name: $toName})
CREATE (from)-[r:${relation.relationType}]->(to)
RETURN r
`,
{
fromName: relation.from,
toName: relation.to
}
);
}
await tx.commit();
return newRelations;
} catch (error) {
await tx.rollback();
this.logger.error('Error creating relations', extractError(error));
throw error;
}
} finally {
await session.close();
}
}
/**
* 添加观察
* @param observations 要添加的观察数组
* @returns 添加的观察数组
*/
public async addObservations(observations: Observation[]): Promise<Observation[]> {
if (observations.length === 0) return [];
const session = await this.getSession();
try {
const addedObservations: Observation[] = [];
const tx = session.beginTransaction();
try {
for (const observation of observations) {
// 检查实体是否存在
const entityResult = await tx.run(
'MATCH (e:Entity {name: $name}) RETURN e',
{ name: observation.entityName }
);
if (entityResult.records.length > 0) {
// 获取现有观察
const existingObservationsResult = await tx.run(
`
MATCH (e:Entity {name: $name})-[:HAS_OBSERVATION]->(o:Observation)
RETURN o.content AS content
`,
{ name: observation.entityName }
);
const existingObservations = new Set(
existingObservationsResult.records.map(record => record.get('content'))
);
// 过滤出新观察
const newContents = observation.contents.filter(
content => !existingObservations.has(content)
);
if (newContents.length > 0) {
// 添加新观察
await tx.run(
`
MATCH (e:Entity {name: $name})
UNWIND $contents AS content
CREATE (o:Observation {content: content})
CREATE (e)-[:HAS_OBSERVATION]->(o)
`,
{
name: observation.entityName,
contents: newContents
}
);
addedObservations.push({
entityName: observation.entityName,
contents: newContents
});
}
}
}
await tx.commit();
// 更新Fuse.js集合
const allEntities = await this.getAllEntities();
this.fuse.setCollection(allEntities);
return addedObservations;
} catch (error) {
await tx.rollback();
this.logger.error('Error adding observations', extractError(error));
throw error;
}
} finally {
await session.close();
}
}
/**
* 删除实体
* @param entityNames 要删除的实体名称数组
*/
public async deleteEntities(entityNames: string[]): Promise<void> {
if (entityNames.length === 0) return;
const session = await this.getSession();
try {
const tx = session.beginTransaction();
try {
// 删除实体及其关联的观察和关系
await tx.run(
`
UNWIND $names AS name
MATCH (e:Entity {name: name})
OPTIONAL MATCH (e)-[:HAS_OBSERVATION]->(o:Observation)
DETACH DELETE e, o
`,
{ names: entityNames }
);
await tx.commit();
// 更新Fuse.js集合
const allEntities = await this.getAllEntities();
this.fuse.setCollection(allEntities);
} catch (error) {
await tx.rollback();
this.logger.error('Error deleting entities', extractError(error));
throw error;
}
} finally {
await session.close();
}
}
/**
* 删除观察
* @param deletions 要删除的观察数组
*/
public async deleteObservations(deletions: ObservationDeletion[]): Promise<void> {
if (deletions.length === 0) return;
const session = await this.getSession();
try {
const tx = session.beginTransaction();
try {
for (const deletion of deletions) {
if (deletion.contents.length > 0) {
await tx.run(
`
MATCH (e:Entity {name: $name})-[:HAS_OBSERVATION]->(o:Observation)
WHERE o.content IN $contents
DETACH DELETE o
`,
{
name: deletion.entityName,
contents: deletion.contents
}
);
}
}
await tx.commit();
// 更新Fuse.js集合
const allEntities = await this.getAllEntities();
this.fuse.setCollection(allEntities);
} catch (error) {
await tx.rollback();
this.logger.error('Error deleting observations', extractError(error));
throw error;
}
} finally {
await session.close();
}
}
/**
* 删除关系
* @param relations 要删除的关系数组
*/
public async deleteRelations(relations: Relation[]): Promise<void> {
if (relations.length === 0) return;
const session = await this.getSession();
try {
const tx = session.beginTransaction();
try {
for (const relation of relations) {
await tx.run(
`
MATCH (from:Entity {name: $fromName})-[r:${relation.relationType}]->(to:Entity {name: $toName})
DELETE r
`,
{
fromName: relation.from,
toName: relation.to
}
);
}
await tx.commit();
} catch (error) {
await tx.rollback();
this.logger.error('Error deleting relations', extractError(error));
throw error;
}
} finally {
await session.close();
}
}
/**
* 搜索节点
* @param query 搜索查询
* @returns 包含匹配实体和关系的知识图谱
*/
public async searchNodes(query: string): Promise<KnowledgeGraph> {
if (!query || query.trim() === '') {
return { entities: [], relations: [] };
}
const session = await this.getSession();
try {
// 使用Neo4j全文搜索
const searchResult = await session.run(
`
CALL db.index.fulltext.queryNodes("entity_fulltext", $query)
YIELD node, score
RETURN node
`,
{ query }
);
// 获取所有实体用于Fuse.js搜索
const allEntities = await this.getAllEntities();
this.fuse.setCollection(allEntities);
// 使用Fuse.js进行模糊搜索
const fuseResults = this.fuse.search(query);
// 合并搜索结果
const uniqueEntities = new Map<string, Entity>();
// 添加Neo4j搜索结果
for (const record of searchResult.records) {
const node = record.get('node');
const name = node.properties.name;
if (!uniqueEntities.has(name)) {
const entity = allEntities.find(e => e.name === name);
if (entity) {
uniqueEntities.set(name, entity);
}
}
}
// 添加Fuse.js搜索结果
for (const result of fuseResults) {
if (!uniqueEntities.has(result.item.name)) {
uniqueEntities.set(result.item.name, result.item);
}
}
const entities = Array.from(uniqueEntities.values());
const entityNames = entities.map(entity => entity.name);
if (entityNames.length === 0) {
return { entities: [], relations: [] };
}
// 获取关系
const relationsResult = await session.run(
`
MATCH (from:Entity)-[r]->(to:Entity)
WHERE from.name IN $names OR to.name IN $names
RETURN from.name AS fromName, to.name AS toName, type(r) AS relationType
`,
{ names: entityNames }
);
const relations: Relation[] = relationsResult.records.map(record => {
return {
from: record.get('fromName'),
to: record.get('toName'),
relationType: record.get('relationType')
};
});
return {
entities,
relations
};
} catch (error) {
this.logger.error('Error searching nodes', extractError(error));
return { entities: [], relations: [] };
} finally {
await session.close();
}
}
/**
* 打开节点
* @param names 要打开的节点名称数组
* @returns 包含匹配实体和关系的知识图谱
*/
public async openNodes(names: string[]): Promise<KnowledgeGraph> {
if (names.length === 0) {
return { entities: [], relations: [] };
}
const session = await this.getSession();
try {
// 获取实体及其观察
const entitiesResult = await session.run(
`
MATCH (e:Entity)
WHERE e.name IN $names
OPTIONAL MATCH (e)-[:HAS_OBSERVATION]->(o:Observation)
RETURN e.name AS name, e.entityType AS entityType, collect(o.content) AS observations
`,
{ names }
);
const entities: Entity[] = entitiesResult.records.map(record => {
return {
name: record.get('name'),
entityType: record.get('entityType'),
observations: record.get('observations').filter(Boolean)
};
});
const entityNames = entities.map(entity => entity.name);
if (entityNames.length > 0) {
// 获取关系
const relationsResult = await session.run(
`
MATCH (from:Entity)-[r]->(to:Entity)
WHERE from.name IN $names OR to.name IN $names
RETURN from.name AS fromName, to.name AS toName, type(r) AS relationType
`,
{ names: entityNames }
);
const relations: Relation[] = relationsResult.records.map(record => {
return {
from: record.get('fromName'),
to: record.get('toName'),
relationType: record.get('relationType')
};
});
return {
entities,
relations
};
} else {
return {
entities,
relations: []
};
}
} catch (error) {
this.logger.error('Error opening nodes', extractError(error));
return { entities: [], relations: [] };
} finally {
await session.close();
}
}
/**
* 关闭连接
*/
public async close(): Promise<void> {
if (this.driver) {
await this.driver.close();
this.driver = null;
}
}
}
```