Skip to content

Instantly share code, notes, and snippets.

@AlexZeitler
Forked from mattwarren/gist:2584969
Created August 11, 2012 10:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AlexZeitler/3323791 to your computer and use it in GitHub Desktop.
Save AlexZeitler/3323791 to your computer and use it in GitHub Desktop.
Indexed properties for RavenDB
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