neo4j graph db를 닷넷에서 이용하는 방법을 먼저 알아본후
액터 메시지큐에 그래프db이벤트를 발생시켜, 메시지큐를 통해 이벤트를 추가하는 방법을 알아보겠습니다.
사전셋팅 StandAlone Neo4j
docker-compose를 통해 로컬환경구축이 가능하며, neo4j 프로토콜을 사용하여 초기에 로그인을해주면
사용준비가 끝나게됩니다.
version: '2' services: neo4j: image: bitnami/neo4j:latest ports: - '7474:7474' - '7473:7473' - '7687:7687'
- 초기계정 : neo4j / bitnami
- Neo4j Browser : http://localhost:7474
- Neo4j DB : neo4j://localhost:7687
셋팅
의존모듈
<PackageReference Include="Neo4jClient" Version="4.1.14" /> <PackageReference Include="AkkaDotModule.Webnori" Version="1.1.1" />
{ "Logging": { "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } }, "AppSettings": { "GraphConnection": "http://localhost:7474", "GraphConnectionUser": "neo4j", "GraphConnectionPw": "bitnami" } }
using Neo4jClient; using Neo4jClient.Cypher; public class GraphEngine { private readonly AppSettings appSettings; private readonly ILogger logger; private readonly GraphClient neo4jClient; public GraphEngine(AppSettings _appSettings, ILogger<GraphEngine> _logger) { appSettings = _appSettings; logger = _logger; neo4jClient = new GraphClient(new Uri(appSettings.GraphConnection), appSettings.GraphConnectionUser, appSettings.GraphConnectionPw); } public async Task<ICypherFluentQuery> GetCypher() { if (!neo4jClient.IsConnected) { await neo4jClient.ConnectAsync(); } return neo4jClient.Cypher; } public async Task<GraphClient> GetClient() { if (!neo4jClient.IsConnected) { await neo4jClient.ConnectAsync(); } return neo4jClient; } public async Task RemoveAll() { var cyper = await GetCypher(); await cyper .Match("(n)") .DetachDelete("n") .ExecuteWithoutResultsAsync(); } } DI셋팅 public void ConfigureServices(IServiceCollection services) { services.AddSingleton<GraphEngine>(); }
유닛테스트기를 사용하여 Graph DB활용해보기
UseCase :
- 홍길동 사용자생성
- 철수 사용자생성
- 스파이더맨 영화생성
- 타이타닉 영화생성
- 홍길동은 스파이더맨을 보았다
- 철수는 타이타닉을 보았다
- 홍길동과 철수 친구연결
namespace SearchApiTest.Adapter { public class GraphEngineTest { private GraphEngine _graphEngine; [SetUp] public void SetUp() { var logger = TestLogger.Create<GraphEngine>(); var builder = new ConfigurationBuilder() .AddJsonFile("appsettings.Development.json"); var Configuration = builder.Build(); var options = new AppSettings(); Configuration.GetSection("AppSettings") .Bind(options); _graphEngine = new GraphEngine(options, logger); } [TestCase(TestName = "Step0 - 초기화 생성및 연결 Test")] public async Task CreatePersonAreOK() { _graphEngine.RemoveAll().Wait(); var cypher = await _graphEngine.GetCypher(); await cypher.Write .Create(@"(alice:Person {name:'홍길동'})") .ExecuteWithoutResultsAsync(); await cypher.Write .Create(@"(alice:Person {name:'철수'})") .ExecuteWithoutResultsAsync(); await cypher.Write .Create(@"(alice:Movie {name:'스파이더맨'})") .ExecuteWithoutResultsAsync(); await cypher.Write .Create(@"(alice:Movie {name:'타이타닉'})") .ExecuteWithoutResultsAsync(); await cypher.Write .Match(@"(a: Person),(b: Movie)") .Where(@"a.name = '홍길동' AND b.name = '스파이더맨'") .Create(@"(a)-[r:뷰]->(b)").ExecuteWithoutResultsAsync(); await cypher.Write .Match(@"(a: Person),(b: Movie)") .Where(@"a.name = '철수' AND b.name = '타이타닉'") .Create(@"(a)-[r:뷰]->(b)").ExecuteWithoutResultsAsync(); await cypher.Write .Match(@"(a: Person),(b: Person)") .Where(@"a.name = '철수' AND b.name = '홍길동'") .Create(@"(a)-[r:친구]->(b)").ExecuteWithoutResultsAsync(); } } }
친구가본 영화를 추천하는 간단한 그래프 모델입니다.
이벤트 큐로 확장하기
서비스에서 이벤트가 발생할때마다 Crud를 직접하는것은 서비스의 성능을 느리게할수 있으며, 발생이벤트를 메시징큐에 적재하여
백그라운드에서 순차적으로 또는 분리된 리모트에서 해당이벤트를 처리할수 있습니다. ( AkkaRemote또는 Kafka가 활용될수 있습니다.)
그래프 이벤트를 처리하는 액터
namespace SearchApi.Actors { public class GraphElementIdenty { public string Alice { get; set; } public string Name { get; set; } } public class GraphEvent { public string Action { get; set; } // Create , Relation, Reset public string Alice { get; set; } // AliceName public string Name { get; set; } public GraphElementIdenty From { get; set; } public GraphElementIdenty To { get; set; } } public class GraphEventActor : ReceiveActor { private readonly ILoggingAdapter logger = Context.GetLogger(); private readonly GraphEngine graphEngine; public GraphEventActor(GraphEngine _graphEngine) { logger.Info($"Create GraphEventActor:{Context.Self.Path.Name}"); graphEngine = _graphEngine; ReceiveAsync<GraphEvent>(async graphEvent => { var cypher = await _graphEngine.GetCypher(); switch (graphEvent.Action) { case "Reset": { await _graphEngine.RemoveAll(); } break; case "Create": { await cypher.Write .Create($"(alice:{graphEvent.Alice} {{name:'{graphEvent.Name}'}})") .ExecuteWithoutResultsAsync(); } break; case "Relation": { await cypher.Write .Match($"(a:{graphEvent.From.Alice}),(b:{graphEvent.To.Alice})") .Where($"a.name = '{graphEvent.From.Name}' AND b.name = '{graphEvent.To.Name}'") .Create($"(a)-[r:{graphEvent.Name}]->(b)").ExecuteWithoutResultsAsync(); } break; } }); } } }
var graphEngine = app.ApplicationServices.GetService<GraphEngine>(); var graphEventActor = AkkaLoad.RegisterActor( "GraphEventActor", actorSystem.ActorOf(Props.Create<GraphEventActor>(graphEngine), "GraphEventActor" )); //Test For Graph graphEventActor.Tell(new GraphEvent() { Action = "Reset" }); graphEventActor.Tell(new GraphEvent() { Action = "Create", Alice = "Person", Name = "홍길동" }); graphEventActor.Tell(new GraphEvent() { Action = "Create", Alice = "Movie", Name = "스파이더맨" }); graphEventActor.Tell(new GraphEvent() { Action = "Relation", Name = "시청", From = new GraphElementIdenty() { Alice = "Person", Name = "홍길동" }, To = new GraphElementIdenty() { Alice = "Movie", Name = "스파이더맨" } });
추가참고 링크
- Neo4j : https://neo4j.com/docs/cypher-manual/current/clauses/create/
- Neo4j를 활용한 추천 시스템 : https://ichi.pro/ko/neo4jleul-sayonghayeo-leseutolang-chucheon-enjin-guchug-172708887086902