Skip to content

Commit

Permalink
feat: Add the ability to compare existing index definitions to the pr…
Browse files Browse the repository at this point in the history
…ojected one for a type (#479)

* feat: Add the ability to compare existing index definitions to the projected one for a type.

* fix(ci): docker-compose should now be docker compose (v2)

* adding `IsIndexCurrent` and tests

---------

Co-authored-by: James Abbott <james.abbott@crispthinking.com>
Co-authored-by: slorello89 <steve.lorello@redis.com>
  • Loading branch information
3 people authored Oct 4, 2024
1 parent 7aa7ab7 commit 93e9fd5
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 2 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ var provider = new RedisConnectionProvider("redis://localhost:6379");
provider.Connection.CreateIndex(typeof(Customer));
```

Redis OM provides limited support for schema migration at this time. You can check if the index definition in Redis matches your current index definition using the `IsIndexCurrent` method on the `RedisConnection`. Then you may use that output to determine when to re-create your indexes when your types change. An example implementation of this would look like:

```csharp
var provider = new RedisConnectionProvider("redis://localhost:6379");
var definition = provider.Connection.GetIndexInfo(typeof(Customer));

if (!provider.Connection.IsIndexCurrent(typeof(Customer)))
{
provider.Connection.DropIndex(typeof(Customer));
provider.Connection.CreateIndex(typeof(Customer));
}
```


### Indexing Embedded Documents

There are two methods for indexing embedded documents with Redis.OM, an embedded document is a complex object, e.g. if our `Customer` model had an `Address` property with the following model:
Expand Down
68 changes: 67 additions & 1 deletion src/Redis.OM/Modeling/RedisIndex.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Redis.OM;
using Redis.OM.Modeling;

Expand All @@ -8,8 +9,73 @@ namespace Redis.OM.Modeling
/// <summary>
/// A utility class for serializing objects into Redis Indices.
/// </summary>
internal static class RedisIndex
public static class RedisIndex
{
/// <summary>
/// Verifies whether the given index schema definition matches the current definition.
/// </summary>
/// <param name="redisIndexInfo">The index definition.</param>
/// <param name="type">The type to be indexed.</param>
/// <returns>A bool indicating whether the current index definition has drifted from the current definition, which may be used to determine when to re-create an index..</returns>
public static bool IndexDefinitionEquals(this RedisIndexInfo redisIndexInfo, Type type)
{
var serialisedDefinition = SerializeIndex(type);
var existingSet = redisIndexInfo.Attributes?.Select(a => (Property: a.Attribute!, a.Type!)).OrderBy(a => a.Property);
var isJson = redisIndexInfo.IndexDefinition?.Identifier == "JSON";

if (serialisedDefinition.Length < 5)
{
throw new ArgumentException($"Could not parse the index definition for type: {type.Name}.");
}

if (redisIndexInfo.IndexName != serialisedDefinition[0])
{
return false;
}

if (redisIndexInfo.IndexDefinition?.Identifier?.Equals(serialisedDefinition[2], StringComparison.OrdinalIgnoreCase) == false)
{
return false;
}

if (redisIndexInfo.IndexDefinition?.Prefixes.FirstOrDefault().Equals(serialisedDefinition[5]) == false)
{
return false;
}

var target = redisIndexInfo.Attributes?.SelectMany(a =>
{
var attr = new List<string>();

if (a.Identifier == null)
{
return Array.Empty<string>();
}

if (isJson)
{
attr.Add(a.Identifier);
attr.Add("AS");
}

attr.Add(a.Attribute!);

if (a.Type != null)
{
attr.Add(a.Type);
}

if (a.Sortable == true)
{
attr.Add("SORTABLE");
}

return attr.ToArray();
});

return target.SequenceEqual(serialisedDefinition.Skip(7));
}

/// <summary>
/// Pull out the Document attribute from a Type.
/// </summary>
Expand Down
26 changes: 25 additions & 1 deletion src/Redis.OM/RediSearchCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public static async Task<bool> CreateIndexAsync(this IRedisConnection connection
try
{
var indexName = type.SerializeIndex().First();
var redisReply = await connection.ExecuteAsync("FT.INFO", indexName);
var redisReply = await connection.ExecuteAsync("FT.INFO", indexName).ConfigureAwait(false);
var redisIndexInfo = new RedisIndexInfo(redisReply);
return redisIndexInfo;
}
Expand Down Expand Up @@ -268,6 +268,30 @@ public static bool DropIndexAndAssociatedRecords(this IRedisConnection connectio
}
}

/// <summary>
/// Check if the index exists and is up to date.
/// </summary>
/// <param name="connection">The connection.</param>
/// <param name="type">The type the index was built from.</param>
/// <returns>Whether or not the index is current.</returns>
public static bool IsIndexCurrent(this IRedisConnection connection, Type type)
{
var definition = connection.GetIndexInfo(type);
return definition?.IndexDefinitionEquals(type) ?? false;
}

/// <summary>
/// Check if the index exists and is up to date.
/// </summary>
/// <param name="connection">The connection.</param>
/// <param name="type">The type the index was built from.</param>
/// <returns>Whether or not the index is current.</returns>
public static async Task<bool> IsIndexCurrentAsync(this IRedisConnection connection, Type type)
{
var definition = await connection.GetIndexInfoAsync(type).ConfigureAwait(false);
return definition?.IndexDefinitionEquals(type) ?? false;
}

/// <summary>
/// Search redis with the given query.
/// </summary>
Expand Down
87 changes: 87 additions & 0 deletions test/Redis.OM.Unit.Tests/RediSearchTests/RedisIndexTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Redis.OM.Contracts;
using Redis.OM.Modeling;
using Xunit;

Expand All @@ -20,6 +21,34 @@ public class TestPersonClassHappyPath
public string[] NickNames { get; set; }
}

[Document(IndexName = "TestPersonClassHappyPath-idx", Prefixes = new []{"Simple"}, StorageType = StorageType.Hash)]
public class TestPersonClassHappyPathWithMutatedDefinition
{
public string Name { get; set; }
[Indexed(Sortable = true)]
public int Age { get; set; }
public double Height { get; set; }
}

[Document(IndexName = "SerialisedJson-idx", Prefixes = new []{"Simple"}, StorageType = StorageType.Json)]
public class SerialisedJsonType
{
[Searchable(Sortable = true)]
public string Name { get; set; }

public int Age { get; set; }
}

[Document(IndexName = "SerialisedJson-idx", Prefixes = new []{"Simple"}, StorageType = StorageType.Json)]
public class SerialisedJsonTypeNotMatch
{
[Searchable(Sortable = true)]
public string Name { get; set; }

[Indexed(Sortable = true)]
public int Age { get; set; }
}

[Document(IndexName = "TestPersonClassHappyPath-idx", StorageType = StorageType.Hash, Prefixes = new []{"Person:"})]
public class TestPersonClassOverridenPrefix
{
Expand Down Expand Up @@ -188,6 +217,34 @@ public void TestGetIndexInfoWhichDoesNotExist()
Assert.Null(indexInfo);
}

[Fact]
public void TestCheckIndexUpToDate()
{
var host = Environment.GetEnvironmentVariable("STANDALONE_HOST_PORT") ?? "localhost";
var provider = new RedisConnectionProvider($"redis://{host}");
var connection = provider.Connection;
connection.DropIndex(typeof(SerialisedJsonType));
Assert.False(connection.IsIndexCurrent(typeof(SerialisedJsonType)));

connection.CreateIndex(typeof(SerialisedJsonType));
Assert.False(connection.IsIndexCurrent(typeof(SerialisedJsonTypeNotMatch)));
Assert.True(connection.IsIndexCurrent(typeof(SerialisedJsonType)));
}

[Fact]
public async Task TestCheckIndexUpToDateAsync()
{
var host = Environment.GetEnvironmentVariable("STANDALONE_HOST_PORT") ?? "localhost";
var provider = new RedisConnectionProvider($"redis://{host}");
var connection = provider.Connection;
await connection.DropIndexAsync(typeof(SerialisedJsonType));
Assert.False(await connection.IsIndexCurrentAsync(typeof(SerialisedJsonType)));

await connection.CreateIndexAsync(typeof(SerialisedJsonType));
Assert.False(await connection.IsIndexCurrentAsync(typeof(SerialisedJsonTypeNotMatch)));
Assert.True(await connection.IsIndexCurrentAsync(typeof(SerialisedJsonType)));
}

[Fact]
public async Task TestGetIndexInfoWhichDoesNotExistAsync()
{
Expand All @@ -198,5 +255,35 @@ public async Task TestGetIndexInfoWhichDoesNotExistAsync()
var indexInfo = await connection.GetIndexInfoAsync(typeof(TestPersonClassHappyPath));
Assert.Null(indexInfo);
}

[Fact]
public async Task TestGetIndexInfoWhichDoesNotMatchExisting()
{
var host = Environment.GetEnvironmentVariable("STANDALONE_HOST_PORT") ?? "localhost";
var provider = new RedisConnectionProvider($"redis://{host}");
var connection = provider.Connection;

await connection.DropIndexAsync(typeof(TestPersonClassHappyPath));
await connection.CreateIndexAsync(typeof(TestPersonClassHappyPath));
var indexInfo = await connection.GetIndexInfoAsync(typeof(TestPersonClassHappyPath));

Assert.False(indexInfo.IndexDefinitionEquals(typeof(TestPersonClassHappyPathWithMutatedDefinition)));
Assert.True(indexInfo.IndexDefinitionEquals(typeof(TestPersonClassHappyPath)));
}

[Fact]
public async Task TestGetIndexInfoWhichDoesNotMatchExistingJson()
{
var host = Environment.GetEnvironmentVariable("STANDALONE_HOST_PORT") ?? "localhost";
var provider = new RedisConnectionProvider($"redis://{host}");
var connection = provider.Connection;

await connection.DropIndexAsync(typeof(SerialisedJsonType));
await connection.CreateIndexAsync(typeof(SerialisedJsonType));
var indexInfo = await connection.GetIndexInfoAsync(typeof(SerialisedJsonType));

Assert.False(indexInfo.IndexDefinitionEquals(typeof(SerialisedJsonTypeNotMatch)));
Assert.True(indexInfo.IndexDefinitionEquals(typeof(SerialisedJsonType)));
}
}
}

0 comments on commit 93e9fd5

Please sign in to comment.