diff --git a/deployment/game_database/vector_index.js b/deployment/game_database/vector_index.js index 4c3481c..90582cf 100644 --- a/deployment/game_database/vector_index.js +++ b/deployment/game_database/vector_index.js @@ -10,6 +10,18 @@ "path": "Player.Nickname", "type": "filter" }, + { + "numDimensions": 1186, + "path": "similarity_vector", + "similarity": "euclidean", + "type": "vector" + }, + { + "numDimensions": 9, + "path": "stats_vector", + "similarity": "euclidean", + "type": "vector" + }, { "numDimensions": 589, "path": "speed_vector", diff --git a/rest_service/Controllers/RecordingsController.cs b/rest_service/Controllers/RecordingsController.cs index 8ee7b81..5753283 100644 --- a/rest_service/Controllers/RecordingsController.cs +++ b/rest_service/Controllers/RecordingsController.cs @@ -5,7 +5,6 @@ using RestService.Entities; using RestService.Exceptions; using MongoDB.Bson; -using MongoDB.Bson.Serialization; namespace RestService.Controllers; @@ -60,8 +59,16 @@ public async Task PostRecording([FromBody] RecordingRequest recor // Calculate vectors try { + newRecording.StatsVector = CalculateStatsVector(newRecording.SessionStatisticsPlain); newRecording.SpeedVector = CalculateSpeedVector(newRecording.Snapshots); newRecording.AccelVector = CalculateAcceleration(newRecording.SpeedVector); + + newRecording.SimilarityVector = CalculateSimilarityVector( + new List() { + newRecording.StatsVector, + newRecording.SpeedVector, + newRecording.AccelVector }); + } catch (Exception) { // Favor persisting Recording over setting vectors } @@ -143,6 +150,23 @@ private async Task AddPlayer(Recording recording) } } + private static double[] CalculateStatsVector(SessionStatisticsPlain ssp) + { + double[] stats = new double[9]; + + stats[0] = Convert.ToDouble(ssp.Score); + stats[1] = Convert.ToDouble(ssp.DamageDone); + stats[2] = Convert.ToDouble(ssp.BulletsFired); + stats[3] = Convert.ToDouble(ssp.PelletsDestroyedLarge); + stats[4] = Convert.ToDouble(ssp.PelletsDestroyedMedium); + stats[5] = Convert.ToDouble(ssp.PelletsDestroyedSmall); + stats[6] = Convert.ToDouble(ssp.PowerUpBulletDamageCollected); + stats[7] = Convert.ToDouble(ssp.PowerUpBulletSpeedCollected); + stats[8] = Convert.ToDouble(ssp.PowerUpPlayerSpeedCollected); + + return stats; + } + private static double[] CalculateSpeedVector(List snapshots) { long vectorSize = snapshots.Count - 1; @@ -176,6 +200,51 @@ private static double[] CalculateAcceleration(double[] speedVector) return accelVector; } + private static double[] CalculateSimilarityVector(List vectors) + { + double[] similar = Array.Empty(); + foreach (double[] vector in vectors) + similar = similar.Concat(vector).ToArray(); + return similar; + } + + [HttpGet("similar", Name = "GetSimilar")] + public async Task> Similar([FromQuery] PlayerRequest playerRequest) + { + // Get the highest scoring run for this player + Recording topRecording = _recordingsCollection + .Find(r => r.Player.Name.Equals(playerRequest.Name)) + .SortByDescending(r => r.SessionStatisticsPlain.Score) + .Limit(1).ToList().First(); + + // Now get similar recordings + List similarRecordings = _recordingsCollection.Aggregate() + .VectorSearch( + r => r.SimilarityVector, + topRecording.SimilarityVector, + 3, + new VectorSearchOptions() + { + IndexName = "vector_index", + NumberOfCandidates = 1000, + Filter = Builders.Filter + .Where(r => !r.Player.Name.Equals(playerRequest.Name)) + }) + .ToList(); + + // Return this player's top recording + top similar + List response = new() + { + new SimilarRecordingResponse(topRecording) + }; + response.AddRange( + similarRecordings + .Select(r => new SimilarRecordingResponse(r)) + .ToList()); + + return response; + } + [HttpGet("similarBySpeed", Name = "GetSimilarBySpeed")] public async Task> SimilarBySpeed([FromQuery] PlayerRequest playerRequest) { @@ -212,7 +281,6 @@ public async Task> SimilarBySpeed([FromQuery] Pla return response; } - [HttpGet("similarByAcceleration", Name = "GetSimilarByAcceleration")] public async Task> SimilarByAcceleration([FromQuery] PlayerRequest playerRequest) @@ -250,5 +318,41 @@ public async Task> SimilarByAcceleration([FromQue return response; } - + + [HttpGet("similarByStats", Name = "GetSimilarByStats")] + public async Task> SimilarByStats([FromQuery] PlayerRequest playerRequest) + { + // Get the highest scoring run for this player + Recording topRecording = _recordingsCollection + .Find(r => r.Player.Name.Equals(playerRequest.Name)) + .SortByDescending(r => r.SessionStatisticsPlain.Score) + .Limit(1).ToList().First(); + + // Now get similar recordings + List similarRecordings = _recordingsCollection.Aggregate() + .VectorSearch( + r => r.StatsVector, + topRecording.StatsVector, + 3, + new VectorSearchOptions() + { + IndexName = "vector_index", + NumberOfCandidates = 1000, + Filter = Builders.Filter + .Where(r => !r.Player.Name.Equals(playerRequest.Name)) + }) + .ToList(); + + // Return this player's top recording + top similar + List response = new() + { + new SimilarRecordingResponse(topRecording) + }; + response.AddRange( + similarRecordings + .Select(r => new SimilarRecordingResponse(r)) + .ToList()); + + return response; + } } \ No newline at end of file diff --git a/rest_service/Entities/Recording.cs b/rest_service/Entities/Recording.cs index 4dc0fdf..0b8ab9e 100644 --- a/rest_service/Entities/Recording.cs +++ b/rest_service/Entities/Recording.cs @@ -18,4 +18,8 @@ public class Recording public double[]? SpeedVector { get; set; } [BsonElement("accel_vector")] public double[]? AccelVector { get; set; } + [BsonElement("stats_vector")] + public double[]? StatsVector { get; set; } + [BsonElement("similarity_vector")] + public double[]? SimilarityVector { get; set; } } \ No newline at end of file diff --git a/rest_service/RestService.csproj b/rest_service/RestService.csproj index 728b6d1..03012b6 100644 --- a/rest_service/RestService.csproj +++ b/rest_service/RestService.csproj @@ -11,7 +11,7 @@ - + diff --git a/website/Pages/PlayerSimilar.razor b/website/Pages/PlayerSimilar.razor index 3ec4c2e..90f312f 100644 --- a/website/Pages/PlayerSimilar.razor +++ b/website/Pages/PlayerSimilar.razor @@ -26,7 +26,15 @@ logo - +
+
+ Solution +    |    + MDB for Gaming +    |    + GitHub +
+
@if (Player == null) { @@ -39,7 +47,7 @@

Player Dashboard

- Similar Players by Speed based on Highest Score Run + Similar Players based on Highest Score, Speed and Acceleration

@_errorMessage @@ -91,31 +99,62 @@ } else { -

Similar Players by Speed

- foreach (SimilarRecording rec in SimilarBySpeed) - { -
@rec.Name
- string chartUrl = ChartsUrl.CreateSimilarUrl(_atlasChartIdSimilar, rec.Id); - - } - - //TO-DO: Revisit acceleration - - }
@code { private Player Player { get; set; } = new(); + private List Similar { get; set; } = new(); private List SimilarBySpeed { get; set; } = new(); private List SimilarByAccel { get; set; } = new(); @@ -143,8 +182,11 @@ var players = await _restClient.GetJsonAsync>(playersUrlWithQuery); Player = players.First(); - string similarBySpeedUrlWithQuery = UrlHelper.BuildUrlWithQuery(Constants.RestServiceEndpointSimilarBySpeed, playerFilter); - SimilarBySpeed = await _restClient.GetJsonAsync>(similarBySpeedUrlWithQuery); + //string similarBySpeedUrlWithQuery = UrlHelper.BuildUrlWithQuery(Constants.RestServiceEndpointSimilarBySpeed, playerFilter); + //SimilarBySpeed = await _restClient.GetJsonAsync>(similarBySpeedUrlWithQuery); + + string similarUrlWithQuery = UrlHelper.BuildUrlWithQuery(Constants.RestServiceEndpointSimilar, playerFilter); + Similar = await _restClient.GetJsonAsync>(similarUrlWithQuery); //TO-DO: Revisit acceleration //string similarByAccelUrlWithQuery = UrlHelper.BuildUrlWithQuery(Constants.RestServiceEndpointSimilarByAccel, playerFilter); diff --git a/website/Utils/Constants.cs b/website/Utils/Constants.cs index 75256fa..e3f2e89 100644 --- a/website/Utils/Constants.cs +++ b/website/Utils/Constants.cs @@ -8,8 +8,10 @@ public static class Constants public const string RestServiceEndpointPlayers = "players"; public const string RestServiceEndpointPlayersAutoComplete = "players/autocomplete"; public const string RestServiceEndpointPlayersSearch = "players/search"; + public const string RestServiceEndpointSimilar = "recordings/similar"; public const string RestServiceEndpointSimilarBySpeed = "recordings/similarBySpeed"; public const string RestServiceEndpointSimilarByAccel = "recordings/similarByAcceleration"; + public const string RestServiceEndpointSimilarByStats = "recordings/similarByStats"; public const string QueryParameterEventId = "EventId"; public const string QueryParameterName = "Name"; } \ No newline at end of file