Skip to content

Commit

Permalink
Merge pull request #107 from mongodb-developer/feature/vector_similar…
Browse files Browse the repository at this point in the history
…_players
  • Loading branch information
ctcac00 authored Mar 14, 2024
2 parents ffa6006 + 1a3742c commit 266d4f2
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 25 deletions.
12 changes: 12 additions & 0 deletions deployment/game_database/vector_index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
110 changes: 107 additions & 3 deletions rest_service/Controllers/RecordingsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using RestService.Entities;
using RestService.Exceptions;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;

namespace RestService.Controllers;

Expand Down Expand Up @@ -60,8 +59,16 @@ public async Task<IActionResult> 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<double[]>() {
newRecording.StatsVector,
newRecording.SpeedVector,
newRecording.AccelVector });

} catch (Exception) {
// Favor persisting Recording over setting vectors
}
Expand Down Expand Up @@ -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<Snapshot> snapshots)
{
long vectorSize = snapshots.Count - 1;
Expand Down Expand Up @@ -176,6 +200,51 @@ private static double[] CalculateAcceleration(double[] speedVector)
return accelVector;
}

private static double[] CalculateSimilarityVector(List<double[]> vectors)
{
double[] similar = Array.Empty<double>();
foreach (double[] vector in vectors)
similar = similar.Concat(vector).ToArray();
return similar;
}

[HttpGet("similar", Name = "GetSimilar")]
public async Task<List<SimilarRecordingResponse>> 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<Recording> similarRecordings = _recordingsCollection.Aggregate()
.VectorSearch(
r => r.SimilarityVector,
topRecording.SimilarityVector,
3,
new VectorSearchOptions<Recording>()
{
IndexName = "vector_index",
NumberOfCandidates = 1000,
Filter = Builders<Recording>.Filter
.Where(r => !r.Player.Name.Equals(playerRequest.Name))
})
.ToList();

// Return this player's top recording + top similar
List<SimilarRecordingResponse> response = new()
{
new SimilarRecordingResponse(topRecording)
};
response.AddRange(
similarRecordings
.Select(r => new SimilarRecordingResponse(r))
.ToList());

return response;
}

[HttpGet("similarBySpeed", Name = "GetSimilarBySpeed")]
public async Task<List<SimilarRecordingResponse>> SimilarBySpeed([FromQuery] PlayerRequest playerRequest)
{
Expand Down Expand Up @@ -212,7 +281,6 @@ public async Task<List<SimilarRecordingResponse>> SimilarBySpeed([FromQuery] Pla

return response;
}


[HttpGet("similarByAcceleration", Name = "GetSimilarByAcceleration")]
public async Task<List<SimilarRecordingResponse>> SimilarByAcceleration([FromQuery] PlayerRequest playerRequest)
Expand Down Expand Up @@ -250,5 +318,41 @@ public async Task<List<SimilarRecordingResponse>> SimilarByAcceleration([FromQue

return response;
}


[HttpGet("similarByStats", Name = "GetSimilarByStats")]
public async Task<List<SimilarRecordingResponse>> 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<Recording> similarRecordings = _recordingsCollection.Aggregate()
.VectorSearch(
r => r.StatsVector,
topRecording.StatsVector,
3,
new VectorSearchOptions<Recording>()
{
IndexName = "vector_index",
NumberOfCandidates = 1000,
Filter = Builders<Recording>.Filter
.Where(r => !r.Player.Name.Equals(playerRequest.Name))
})
.ToList();

// Return this player's top recording + top similar
List<SimilarRecordingResponse> response = new()
{
new SimilarRecordingResponse(topRecording)
};
response.AddRange(
similarRecordings
.Select(r => new SimilarRecordingResponse(r))
.ToList());

return response;
}
}
4 changes: 4 additions & 0 deletions rest_service/Entities/Recording.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
2 changes: 1 addition & 1 deletion rest_service/RestService.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<ItemGroup>
<PackageReference Include="dotenv.net" Version="3.1.3" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.9" />
<PackageReference Include="MongoDB.Driver" Version="2.22.0" />
<PackageReference Include="MongoDB.Driver" Version="2.24.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />

Expand Down
84 changes: 63 additions & 21 deletions website/Pages/PlayerSimilar.razor
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@
</style>

<img src="\mongodb-logo-green-bg.jpg" class="rounded mx-auto d-block" alt="logo">

<div class="card" style="max-width:400px;background:yellow;text-align:center">
<div style="display:inline">
<a href="https://www.mongodb.com/solutions/solutions-library/gaming-player-profiles-solution" target="_blank">Solution</a>
&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;
<a href="https://www.mongodb.com/use-cases/gaming" target="_blank">MDB for Gaming</a>
&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;
<a href="https://github.com/mongodb-developer/leafsteroids" target="_blank">GitHub</a>
</div>
</div>
<div class="card bg-white">
@if (Player == null)
{
Expand All @@ -39,7 +47,7 @@
<article class="card-body mx-auto" style="max-width: 400px;">
<h2 class="text-center">Player Dashboard</h2>
<h4 class="card-title mt-3 text-center">
Similar Players by Speed based on Highest Score Run
Similar Players based on Highest Score, Speed and Acceleration
</h4>
<h4 class="card-title mt-3 text-center">
<b class="validation-message">@_errorMessage</b>
Expand Down Expand Up @@ -91,31 +99,62 @@
}
else
{
<h4 class="card-title mt-3 text-center">Similar Players by Speed</h4>
foreach (SimilarRecording rec in SimilarBySpeed)
{
<h5>@rec.Name</h5>
string chartUrl = ChartsUrl.CreateSimilarUrl(_atlasChartIdSimilar, rec.Id);
<iframe style="background: #FFFFFF;border: none;border-radius: 2px;box-shadow: 0 2px 10px 0 rgba(70, 76, 79, .2);" width="480" height="480" src=@chartUrl></iframe>
}

//TO-DO: Revisit acceleration
<!--
<h4 class="card-title mt-3 text-center">Similar Players by Acceleration</h4>
foreach (SimilarRecording rec in SimilarByAccel)
<h4 class="card-title mt-3 text-center">Similar Players</h4>
foreach (SimilarRecording rec in Similar)
{
<h5>@rec.Name</h5>
string chartUrl = ChartsUrl.CreateSimilarUrl(_atlasChartIdSimilar, rec.Id);
<iframe style="background: #FFFFFF;border: none;border-radius: 2px;box-shadow: 0 2px 10px 0 rgba(70, 76, 79, .2);" width="480" height="480" src=@chartUrl></iframe>
string chartUrl = ChartsUrl.CreateSimilarUrl(_atlasChartIdSimilar, rec.Id);
<center>
<h5>@rec.Name</h5>
<table style="width:60%;">
<tr>
<td>Score</td>
<td>@rec.SessionStatisticsPlain.Score</td>
</tr>
<tr>
<td>DamageDone</td>
<td>@rec.SessionStatisticsPlain.DamageDone</td>
</tr>
<tr>
<td>BulletsFired</td>
<td>@rec.SessionStatisticsPlain.BulletsFired</td>
</tr>
<tr>
<td>PelletsDestroyedLarge</td>
<td>@rec.SessionStatisticsPlain.PelletsDestroyedLarge</td>
</tr>
<tr>
<td>PelletsDestroyedMedium</td>
<td>@rec.SessionStatisticsPlain.PelletsDestroyedMedium</td>
</tr>
<tr>
<td>PelletsDestroyedSmall</td>
<td>@rec.SessionStatisticsPlain.PelletsDestroyedSmall</td>
</tr>
<tr>
<td>PowerUpBulletDamageCollected</td>
<td>@rec.SessionStatisticsPlain.PowerUpBulletDamageCollected</td>
</tr>
<tr>
<td>PowerUpBulletSpeedCollected</td>
<td>@rec.SessionStatisticsPlain.PowerUpBulletSpeedCollected</td>
</tr>
<tr>
<td>PowerUpPlayerSpeedCollected</td>
<td>@rec.SessionStatisticsPlain.PowerUpPlayerSpeedCollected</td>
</tr>
</table>
<iframe style="background: #FFFFFF;border: none;border-radius: 2px;box-shadow: 0 2px 10px 0 rgba(70, 76, 79, .2);" width="440" height="480" src=@chartUrl></iframe>
</center>
}
}
}
-->
}
</div>


@code {

private Player Player { get; set; } = new();
private List<SimilarRecording> Similar { get; set; } = new();
private List<SimilarRecording> SimilarBySpeed { get; set; } = new();
private List<SimilarRecording> SimilarByAccel { get; set; } = new();

Expand Down Expand Up @@ -143,8 +182,11 @@
var players = await _restClient.GetJsonAsync<List<Player>>(playersUrlWithQuery);
Player = players.First();

string similarBySpeedUrlWithQuery = UrlHelper.BuildUrlWithQuery(Constants.RestServiceEndpointSimilarBySpeed, playerFilter);
SimilarBySpeed = await _restClient.GetJsonAsync<List<SimilarRecording>>(similarBySpeedUrlWithQuery);
//string similarBySpeedUrlWithQuery = UrlHelper.BuildUrlWithQuery(Constants.RestServiceEndpointSimilarBySpeed, playerFilter);
//SimilarBySpeed = await _restClient.GetJsonAsync<List<SimilarRecording>>(similarBySpeedUrlWithQuery);
string similarUrlWithQuery = UrlHelper.BuildUrlWithQuery(Constants.RestServiceEndpointSimilar, playerFilter);
Similar = await _restClient.GetJsonAsync<List<SimilarRecording>>(similarUrlWithQuery);

//TO-DO: Revisit acceleration
//string similarByAccelUrlWithQuery = UrlHelper.BuildUrlWithQuery(Constants.RestServiceEndpointSimilarByAccel, playerFilter);
Expand Down
2 changes: 2 additions & 0 deletions website/Utils/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}

0 comments on commit 266d4f2

Please sign in to comment.