Skip to content

Commit 93e9fd5

Browse files
abbottdevJames Abbottslorello89
authored
feat: Add the ability to compare existing index definitions to the projected 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>
1 parent 7aa7ab7 commit 93e9fd5

File tree

4 files changed

+193
-2
lines changed

4 files changed

+193
-2
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,20 @@ var provider = new RedisConnectionProvider("redis://localhost:6379");
107107
provider.Connection.CreateIndex(typeof(Customer));
108108
```
109109

110+
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:
111+
112+
```csharp
113+
var provider = new RedisConnectionProvider("redis://localhost:6379");
114+
var definition = provider.Connection.GetIndexInfo(typeof(Customer));
115+
116+
if (!provider.Connection.IsIndexCurrent(typeof(Customer)))
117+
{
118+
provider.Connection.DropIndex(typeof(Customer));
119+
provider.Connection.CreateIndex(typeof(Customer));
120+
}
121+
```
122+
123+
110124
### Indexing Embedded Documents
111125

112126
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:

src/Redis.OM/Modeling/RedisIndex.cs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
34
using Redis.OM;
45
using Redis.OM.Modeling;
56

@@ -8,8 +9,73 @@ namespace Redis.OM.Modeling
89
/// <summary>
910
/// A utility class for serializing objects into Redis Indices.
1011
/// </summary>
11-
internal static class RedisIndex
12+
public static class RedisIndex
1213
{
14+
/// <summary>
15+
/// Verifies whether the given index schema definition matches the current definition.
16+
/// </summary>
17+
/// <param name="redisIndexInfo">The index definition.</param>
18+
/// <param name="type">The type to be indexed.</param>
19+
/// <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>
20+
public static bool IndexDefinitionEquals(this RedisIndexInfo redisIndexInfo, Type type)
21+
{
22+
var serialisedDefinition = SerializeIndex(type);
23+
var existingSet = redisIndexInfo.Attributes?.Select(a => (Property: a.Attribute!, a.Type!)).OrderBy(a => a.Property);
24+
var isJson = redisIndexInfo.IndexDefinition?.Identifier == "JSON";
25+
26+
if (serialisedDefinition.Length < 5)
27+
{
28+
throw new ArgumentException($"Could not parse the index definition for type: {type.Name}.");
29+
}
30+
31+
if (redisIndexInfo.IndexName != serialisedDefinition[0])
32+
{
33+
return false;
34+
}
35+
36+
if (redisIndexInfo.IndexDefinition?.Identifier?.Equals(serialisedDefinition[2], StringComparison.OrdinalIgnoreCase) == false)
37+
{
38+
return false;
39+
}
40+
41+
if (redisIndexInfo.IndexDefinition?.Prefixes.FirstOrDefault().Equals(serialisedDefinition[5]) == false)
42+
{
43+
return false;
44+
}
45+
46+
var target = redisIndexInfo.Attributes?.SelectMany(a =>
47+
{
48+
var attr = new List<string>();
49+
50+
if (a.Identifier == null)
51+
{
52+
return Array.Empty<string>();
53+
}
54+
55+
if (isJson)
56+
{
57+
attr.Add(a.Identifier);
58+
attr.Add("AS");
59+
}
60+
61+
attr.Add(a.Attribute!);
62+
63+
if (a.Type != null)
64+
{
65+
attr.Add(a.Type);
66+
}
67+
68+
if (a.Sortable == true)
69+
{
70+
attr.Add("SORTABLE");
71+
}
72+
73+
return attr.ToArray();
74+
});
75+
76+
return target.SequenceEqual(serialisedDefinition.Skip(7));
77+
}
78+
1379
/// <summary>
1480
/// Pull out the Document attribute from a Type.
1581
/// </summary>

src/Redis.OM/RediSearchCommands.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ public static async Task<bool> CreateIndexAsync(this IRedisConnection connection
178178
try
179179
{
180180
var indexName = type.SerializeIndex().First();
181-
var redisReply = await connection.ExecuteAsync("FT.INFO", indexName);
181+
var redisReply = await connection.ExecuteAsync("FT.INFO", indexName).ConfigureAwait(false);
182182
var redisIndexInfo = new RedisIndexInfo(redisReply);
183183
return redisIndexInfo;
184184
}
@@ -268,6 +268,30 @@ public static bool DropIndexAndAssociatedRecords(this IRedisConnection connectio
268268
}
269269
}
270270

271+
/// <summary>
272+
/// Check if the index exists and is up to date.
273+
/// </summary>
274+
/// <param name="connection">The connection.</param>
275+
/// <param name="type">The type the index was built from.</param>
276+
/// <returns>Whether or not the index is current.</returns>
277+
public static bool IsIndexCurrent(this IRedisConnection connection, Type type)
278+
{
279+
var definition = connection.GetIndexInfo(type);
280+
return definition?.IndexDefinitionEquals(type) ?? false;
281+
}
282+
283+
/// <summary>
284+
/// Check if the index exists and is up to date.
285+
/// </summary>
286+
/// <param name="connection">The connection.</param>
287+
/// <param name="type">The type the index was built from.</param>
288+
/// <returns>Whether or not the index is current.</returns>
289+
public static async Task<bool> IsIndexCurrentAsync(this IRedisConnection connection, Type type)
290+
{
291+
var definition = await connection.GetIndexInfoAsync(type).ConfigureAwait(false);
292+
return definition?.IndexDefinitionEquals(type) ?? false;
293+
}
294+
271295
/// <summary>
272296
/// Search redis with the given query.
273297
/// </summary>

test/Redis.OM.Unit.Tests/RediSearchTests/RedisIndexTests.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Linq;
33
using System.Threading.Tasks;
4+
using Redis.OM.Contracts;
45
using Redis.OM.Modeling;
56
using Xunit;
67

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

24+
[Document(IndexName = "TestPersonClassHappyPath-idx", Prefixes = new []{"Simple"}, StorageType = StorageType.Hash)]
25+
public class TestPersonClassHappyPathWithMutatedDefinition
26+
{
27+
public string Name { get; set; }
28+
[Indexed(Sortable = true)]
29+
public int Age { get; set; }
30+
public double Height { get; set; }
31+
}
32+
33+
[Document(IndexName = "SerialisedJson-idx", Prefixes = new []{"Simple"}, StorageType = StorageType.Json)]
34+
public class SerialisedJsonType
35+
{
36+
[Searchable(Sortable = true)]
37+
public string Name { get; set; }
38+
39+
public int Age { get; set; }
40+
}
41+
42+
[Document(IndexName = "SerialisedJson-idx", Prefixes = new []{"Simple"}, StorageType = StorageType.Json)]
43+
public class SerialisedJsonTypeNotMatch
44+
{
45+
[Searchable(Sortable = true)]
46+
public string Name { get; set; }
47+
48+
[Indexed(Sortable = true)]
49+
public int Age { get; set; }
50+
}
51+
2352
[Document(IndexName = "TestPersonClassHappyPath-idx", StorageType = StorageType.Hash, Prefixes = new []{"Person:"})]
2453
public class TestPersonClassOverridenPrefix
2554
{
@@ -188,6 +217,34 @@ public void TestGetIndexInfoWhichDoesNotExist()
188217
Assert.Null(indexInfo);
189218
}
190219

220+
[Fact]
221+
public void TestCheckIndexUpToDate()
222+
{
223+
var host = Environment.GetEnvironmentVariable("STANDALONE_HOST_PORT") ?? "localhost";
224+
var provider = new RedisConnectionProvider($"redis://{host}");
225+
var connection = provider.Connection;
226+
connection.DropIndex(typeof(SerialisedJsonType));
227+
Assert.False(connection.IsIndexCurrent(typeof(SerialisedJsonType)));
228+
229+
connection.CreateIndex(typeof(SerialisedJsonType));
230+
Assert.False(connection.IsIndexCurrent(typeof(SerialisedJsonTypeNotMatch)));
231+
Assert.True(connection.IsIndexCurrent(typeof(SerialisedJsonType)));
232+
}
233+
234+
[Fact]
235+
public async Task TestCheckIndexUpToDateAsync()
236+
{
237+
var host = Environment.GetEnvironmentVariable("STANDALONE_HOST_PORT") ?? "localhost";
238+
var provider = new RedisConnectionProvider($"redis://{host}");
239+
var connection = provider.Connection;
240+
await connection.DropIndexAsync(typeof(SerialisedJsonType));
241+
Assert.False(await connection.IsIndexCurrentAsync(typeof(SerialisedJsonType)));
242+
243+
await connection.CreateIndexAsync(typeof(SerialisedJsonType));
244+
Assert.False(await connection.IsIndexCurrentAsync(typeof(SerialisedJsonTypeNotMatch)));
245+
Assert.True(await connection.IsIndexCurrentAsync(typeof(SerialisedJsonType)));
246+
}
247+
191248
[Fact]
192249
public async Task TestGetIndexInfoWhichDoesNotExistAsync()
193250
{
@@ -198,5 +255,35 @@ public async Task TestGetIndexInfoWhichDoesNotExistAsync()
198255
var indexInfo = await connection.GetIndexInfoAsync(typeof(TestPersonClassHappyPath));
199256
Assert.Null(indexInfo);
200257
}
258+
259+
[Fact]
260+
public async Task TestGetIndexInfoWhichDoesNotMatchExisting()
261+
{
262+
var host = Environment.GetEnvironmentVariable("STANDALONE_HOST_PORT") ?? "localhost";
263+
var provider = new RedisConnectionProvider($"redis://{host}");
264+
var connection = provider.Connection;
265+
266+
await connection.DropIndexAsync(typeof(TestPersonClassHappyPath));
267+
await connection.CreateIndexAsync(typeof(TestPersonClassHappyPath));
268+
var indexInfo = await connection.GetIndexInfoAsync(typeof(TestPersonClassHappyPath));
269+
270+
Assert.False(indexInfo.IndexDefinitionEquals(typeof(TestPersonClassHappyPathWithMutatedDefinition)));
271+
Assert.True(indexInfo.IndexDefinitionEquals(typeof(TestPersonClassHappyPath)));
272+
}
273+
274+
[Fact]
275+
public async Task TestGetIndexInfoWhichDoesNotMatchExistingJson()
276+
{
277+
var host = Environment.GetEnvironmentVariable("STANDALONE_HOST_PORT") ?? "localhost";
278+
var provider = new RedisConnectionProvider($"redis://{host}");
279+
var connection = provider.Connection;
280+
281+
await connection.DropIndexAsync(typeof(SerialisedJsonType));
282+
await connection.CreateIndexAsync(typeof(SerialisedJsonType));
283+
var indexInfo = await connection.GetIndexInfoAsync(typeof(SerialisedJsonType));
284+
285+
Assert.False(indexInfo.IndexDefinitionEquals(typeof(SerialisedJsonTypeNotMatch)));
286+
Assert.True(indexInfo.IndexDefinitionEquals(typeof(SerialisedJsonType)));
287+
}
201288
}
202289
}

0 commit comments

Comments
 (0)