Created
May 3, 2012 10:46
-
-
Save mattwarren/2584969 to your computer and use it in GitHub Desktop.
Indexed properties for RavenDB
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.ComponentModel.Composition.Hosting; | |
using Lucene.Net.Documents; | |
using Raven.Abstractions.Data; | |
using Raven.Abstractions.Indexing; | |
using Raven.Client; | |
using Raven.Client.Embedded; | |
using Raven.Client.Indexes; | |
using Raven.Client.Linq; | |
using System.Linq; | |
using Raven.Database; | |
using Raven.Database.Plugins; | |
using Raven.Tests; | |
using Raven.Tests.Queries; | |
namespace Raven.Tryouts | |
{ | |
internal class Program | |
{ | |
private static void Main() | |
{ | |
//new IntersectionQueryWithLargeDataset().CanPeformIntersectionQuery_Embedded(); | |
//new IntersectionQueryWithLargeDataset().CanPerformIntersectionQuery_Remotely(); | |
//new IntersectionQuery().CanPerformIntersectionQuery_Linq(); | |
//new IntersectionQuery().CanPeformIntersectionQuery_Embedded(); | |
//new IntersectionQuery().CanPerformIntersectionQuery_Remotely(); | |
//new IndexTriggers().CanReplicateValuesFromIndexToDataTable(); | |
new IndexedProperty().RunTest(); | |
} | |
} | |
public class IndexedProperty : RavenTest | |
{ | |
public void RunTest() | |
{ | |
using (var store = new EmbeddableDocumentStore()) | |
{ | |
store.Configuration.RunInMemory = true; | |
store.Configuration.Container = new CompositionContainer(new TypeCatalog(typeof(IndexedPropertyTrigger))); | |
store.Initialize(); | |
IndexCreation.CreateIndexes(typeof(Customers_ByOrder_Count).Assembly, store); | |
CreateDocumentsByEntityNameIndex(store); | |
ExecuteTest(store); | |
} | |
} | |
private void CreateDocumentsByEntityNameIndex(EmbeddableDocumentStore store) | |
{ | |
var database = store.DocumentDatabase; | |
if (database.GetIndexDefinition("Raven/DocumentsByEntityName") == null) | |
{ | |
database.PutIndex("Raven/DocumentsByEntityName", new IndexDefinition | |
{ | |
Map = | |
@"from doc in docs | |
let Tag = doc[""@metadata""][""Raven-Entity-Name""] | |
select new { Tag, LastModified = (DateTime)doc[""@metadata""][""Last-Modified""] };", | |
Indexes = | |
{ | |
{"Tag", FieldIndexing.NotAnalyzed}, | |
}, | |
Stores = | |
{ | |
{"Tag", FieldStorage.No}, | |
{"LastModified", FieldStorage.No} | |
} | |
}); | |
} | |
} | |
private void ExecuteTest(IDocumentStore store) | |
{ | |
var customer1 = new Customer | |
{ | |
Name = "Matt", | |
Country = "UK", | |
Orders = new[] | |
{ | |
new Order {Cost = 9.99m}, | |
new Order {Cost = 12.99m}, | |
new Order {Cost = 1.25m} | |
} | |
}; | |
var customer2 = new Customer | |
{ | |
Name = "Debs", | |
Country = "UK", | |
Orders = new[] | |
{ | |
new Order {Cost = 99.99m}, | |
new Order {Cost = 105.99m} | |
} | |
}; | |
using (var session = store.OpenSession()) | |
{ | |
session.Store(customer1); | |
session.Store(customer2); | |
session.SaveChanges(); | |
} | |
//Proposed Config doc structure (from https://groups.google.com/d/msg/ravendb/Ik6Iv96Z_3I/PXs7h-hawpEJ) | |
//DocId = Raven/IndexedProperties/Orders/AveragePurchaseAmount | |
// "Orders/AveragePurchaseAmount" from the doc key is the index name we get the data from | |
//{ | |
// //The field name that gives us the docId of the doc to write to (is this right???) | |
// "DocumentKey": "CustomerId", | |
// | |
// //mapping from index field to doc field (to store it in) | |
// "Properties": [ | |
// "AveragePurchaseAmount": "AveragePurchaseAmount" | |
// ] | |
//} | |
//The whole idea is so we can do this query | |
//using the AverageValue taken from the Map/Reduce index and stored in the doc | |
//Country:UK SortBy:AveragePurchaseAmount desc | |
using (var session = store.OpenSession()) | |
{ | |
//Just issue a query to ensure that it's not stale | |
RavenQueryStatistics stats; | |
var customers = session.Query<CustomerResult>("Customers/ByOrder/Count") | |
.Customize(s => s.WaitForNonStaleResultsAsOfNow()) | |
.Statistics(out stats) | |
.ToList(); | |
var totalOrders = customers.Count; | |
foreach (var id in new[] { customer1.Id, customer2.Id }) | |
{ | |
var customerEx = session.Advanced.DatabaseCommands.Get(id); | |
Console.WriteLine("\n\nReading back calculated Average from doc[{0}] = {1}", id, customerEx.DataAsJson["AverageOrderCost"]); | |
} | |
//Would like to be able to do it like this, but the doc was stored as Customer | |
//so we can't load it as CustomerEx (even though that's the shape of the Json) | |
//var cutstomerEx = session.Load<CustomerEx>(customer1.Id); | |
} | |
Console.WriteLine("Test completed, press <ENTER> to exit"); | |
Console.ReadLine(); | |
} | |
} | |
public class IndexedPropertyTrigger : AbstractIndexUpdateTrigger | |
{ | |
//We can't keep any state in the IndexPropertyBatcher itself, as it's create each time a batch is run | |
//Whereas this class (IndexPropertyTrigger is only create once during the app lifetime (as far as I can tell) | |
private readonly Dictionary<string, Guid?> previouslyModModifiedDocIds = new Dictionary<string, Guid?>(); | |
public override AbstractIndexUpdateTriggerBatcher CreateBatcher(string indexName) | |
{ | |
//This solves Problem #1 (see https://groups.google.com/d/msg/ravendb/Zq9-4xjwxNM/b0HdivNuodMJ) | |
if (indexName == "Customers/ByOrder/Count") | |
return new IndexPropertyBatcher(this, Database); | |
return null; | |
} | |
public class IndexPropertyBatcher : AbstractIndexUpdateTriggerBatcher | |
{ | |
private readonly IndexedPropertyTrigger _parent; | |
private readonly DocumentDatabase _database; | |
public IndexPropertyBatcher(IndexedPropertyTrigger parent, DocumentDatabase database) | |
{ | |
_parent = parent; | |
_database = database; | |
} | |
public override void OnIndexEntryDeleted(string entryKey) | |
{ | |
//I think a delete should just be the same as an update, i.e. we pull the lasted value of out the index and use that | |
//But how does this work with Map/Reduce, when does a Map/Reduce result (the index entry) get deleted | |
//and how do we access the Lucene Document (to give us the values) | |
} | |
public override void OnIndexEntryCreated(string entryKey, Document document) | |
{ | |
Console.WriteLine("Indexing doc {0}:", entryKey); | |
PrintIndexDocInfo(document); | |
try | |
{ | |
var fields = document.GetFields().OfType<Field>().ToList(); | |
var numericFields = document.GetFields().OfType<NumericField>().ToList(); | |
var isMapReduce = fields.Any(x => x.Name() == Constants.ReduceKeyFieldName); | |
if (isMapReduce) | |
{ | |
//All these magic strings will eventually be read from a configuration doc that the user will have created | |
var docKeyField = fields.FirstOrDefault(x => x.Name() == "CustomerId"); | |
var totalCostField = fields.FirstOrDefault(x => x.Name() == "TotalCost"); | |
var countField = fields.FirstOrDefault(x => x.Name() == "Count"); | |
//Field avgField = numericFields.FirstOrDefault(x => x.Name() == "Average"); | |
NumericField avgField = numericFields.FirstOrDefault(x => x.Name() == "Average_Range"); | |
if (docKeyField != null && totalCostField != null && countField != null) | |
{ | |
var docId = docKeyField.StringValue(); | |
//Does this robustly stop us from handling a trigger that we ourselved caused (by modifying a doc)???? | |
if (_parent.previouslyModModifiedDocIds.ContainsKey(docId)) | |
{ | |
var entry = _parent.previouslyModModifiedDocIds[docId]; | |
Guid? currentEtag = _database.GetDocumentMetadata(docId, null).Etag; | |
if (entry != null && currentEtag == entry.Value) | |
{ | |
_parent.previouslyModModifiedDocIds.Remove(docId); | |
return; | |
} | |
} | |
var existingDoc = _database.Get(docId, null); | |
var avgNum = (double)avgField.GetNumericValue(); | |
Console.WriteLine("DocId = {0}, TotalCost = {1}, Count = {2}, Avg = {3:0.0000}", | |
docId, totalCostField.StringValue(), countField.StringValue(), avgNum); | |
//Need a better way of doing this, should use NumericField instead of Field | |
existingDoc.DataAsJson["AverageOrderCost"] = avgField.StringValue(); | |
var prevEtag = _database.GetDocumentMetadata(docId, null).Etag; | |
_database.Put(docId, existingDoc.Etag, existingDoc.DataAsJson, existingDoc.Metadata, null); | |
var etag = _database.GetDocumentMetadata(docId, null).Etag; | |
_parent.previouslyModModifiedDocIds.Add(docId, etag); | |
} | |
else | |
{ | |
Console.WriteLine("The indexed doc doesn't have the expected fields"); | |
} | |
} | |
} | |
catch (Exception ex) | |
{ | |
Console.WriteLine(ex.Message); | |
} | |
} | |
private void PrintIndexDocInfo(Document document) | |
{ | |
foreach (object field in document.GetFields()) | |
{ | |
try | |
{ | |
if (field is NumericField) | |
{ | |
var numbericField = field as NumericField; | |
Console.WriteLine("\t{0}: {1} - (NumericField)", numbericField.Name(), numbericField.GetNumericValue()); | |
} | |
else if (field is Field) | |
{ | |
var stdField = field as Field; | |
Console.WriteLine("\t{0}: {1} - (Field)", stdField.Name(), stdField.StringValue()); | |
} | |
else | |
{ | |
Console.WriteLine("Unknown field type: " + field.GetType()); | |
} | |
} | |
catch (Exception ex) | |
{ | |
Console.WriteLine(ex.Message); | |
} | |
} | |
} | |
} | |
} | |
public class Customers_ByOrder_Count : AbstractIndexCreationTask<Customer, CustomerResult> | |
{ | |
public Customers_ByOrder_Count() | |
{ | |
Map = customers => from customer in customers | |
from order in customer.Orders | |
select new { CustomerId = customer.Id, TotalCost = order.Cost, Count = 1, Average = 0 }; | |
Reduce = results => from result in results | |
group result by new { result.CustomerId } | |
into g | |
select new | |
{ | |
g.Key.CustomerId, | |
TotalCost = g.Sum(x => x.TotalCost), | |
Count = g.Sum(x => x.Count), | |
Average = g.Sum(x => x.TotalCost) / g.Sum(x => x.Count), | |
}; | |
} | |
} | |
public class CustomerResult | |
{ | |
public String CustomerId { get; set; } | |
public Decimal TotalCost { get; set; } | |
public int Count { get; set; } | |
public double Average { get; set; } | |
} | |
public class Customer | |
{ | |
public String Id { get; set; } | |
public String Name { get; set; } | |
public String Country { get; set; } | |
public IList<Order> Orders { get; set; } | |
} | |
public class CustomerEx : Customer | |
{ | |
public Decimal AverageOrderCost { get; set; } | |
public Decimal NumberOfOrders { get; set; } | |
} | |
public class Order | |
{ | |
public String Id { get; set; } | |
public Decimal Cost { get; set; } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment