First off I'm just getting started with RavenDB so please be patient as I explain the issue. I'm trying to create my first map, map, reduce, transform index. Yeah I know I'm trying to do a lot but I have most of it working.
So first off I do this in my global.asax to make sure that all "ID" properties are used as the identifier on documents.
_documentStore.Conventions.FindIdentityProperty = p => p.Name == "ID";
OK now lets take a look at the index.
public class ProblemListViewIndex : AbstractMultiMapIndexCreationTask<ProblemListView>
{
public ProblemListViewIndex()
{
AddMap<Problem>(problems => from problem in problems
select new
{
ID = problem.ID,
SolutionCount = 0,
});
AddMap<Solution>(solutions => from solution in solutions
select new
{
ID = solution.ProblemID,
SolutionCount = 1,
});
Reduce = results => from result in results
group result by result.ID
into g
select new
{
ID = g.Key,
SolutionCount = g.Sum(x => x.SolutionCount),
};
Indexes.Add(x => x.ID, FieldIndexing.Analyzed);
TransformResults = (database, results) => from result in results
let problem = database.Load<Problem>("problems/" + result.ID.ToString())
let user = database.Load<User>("users/" + problem.PostedByID.ToString())
select new
{
ID = result.ID,
PostedByID = problem.PostedByID,
PostedByName = user.DisplayName,
SolutionCount = result.SolutionCount,
};
}
}
So everything looks good and when I test the index in the RavenDB website I get mixed results. I have duplicate projections. I have the two projections that I expect but two copies of them. Here are what the "ID" looks like on the projection results.
problems/194
problems/195
194
195
I'm confused but then I went back and looked at the "maps". My code translated into something different in the created index. Here is what the first map looks like when created initially.
docs.Problems
.Select(problem => new {ID = problem.__document_id, SolutionCount = 0})
Even being new to RavenDB I see the problem. It's using the "__document_id" field when I want to use the "ID" field. I change the map in then save the index to the following.
docs.Problems
.Select(problem => new {ID = problem.ID, SolutionCount = 0})
Once I do that my projection looks exactly as I expected and as I want it.
194
195
My question is what do I need to do in my code to get my index to create using "ID" over "__document_id"?
I don't see what's wrong here. I#ve put together a test using your code and I get what I have expected:
public class MultiMapWithTransformAndCustomId
{
public class Problem
{
public string ID { get; set; }
public string PostedByID { get; set; }
public string Foo { get; set; }
}
public class Solution
{
public string ID { get; set; }
public string ProblemID { get; set; }
public string Foo { get; set; }
}
public class User
{
public string ID { get; set; }
public string DisplayName { get; set; }
}
public class ProblemListViewIndex : AbstractMultiMapIndexCreationTask<ProblemListViewIndex.ReduceResult>
{
public class ReduceResult
{
public string ID { get; set; }
public int SolutionCount { get; set; }
public string PostedByID { get; set; }
public string PostedByName { get; set; }
}
public ProblemListViewIndex()
{
AddMap<Problem>(problems => from problem in problems
select new
{
ID = problem.ID,
SolutionCount = 0,
});
AddMap<Solution>(solutions => from solution in solutions
select new
{
ID = solution.ProblemID,
SolutionCount = 1,
});
Reduce = results => from result in results
group result by result.ID
into g
select new
{
ID = g.Key,
SolutionCount = g.Sum(x => x.SolutionCount),
};
Indexes.Add(x => x.ID, FieldIndexing.Analyzed);
TransformResults = (database, results) => from result in results
let problem = database.Load<Problem>(result.ID.ToString())
let user = database.Load<User>(problem.PostedByID.ToString())
select new
{
ID = result.ID,
PostedByID = problem.PostedByID,
PostedByName = user.DisplayName,
SolutionCount = result.SolutionCount,
};
}
}
[Fact]
public void Can_do_simple_query()
{
using (var documentStore = new EmbeddableDocumentStore
{
RunInMemory = true
})
{
documentStore.Conventions.FindIdentityProperty = p => p.Name == "ID";
documentStore.Initialize();
using (var documentSession = documentStore.OpenSession())
{
documentSession.Store(new User {ID = "users/1", DisplayName = "Daniel"});
documentSession.Store(new User {ID = "users/2", DisplayName = "Lang"});
documentSession.Store(new Problem {ID = "problems/194", PostedByID = "users/1"});
documentSession.Store(new Problem {ID = "problems/195", PostedByID = "users/2"});
documentSession.Store(new Solution {ID = "solutions/1", ProblemID = "problems/194"});
documentSession.Store(new Solution {ID = "solutions/2", ProblemID = "problems/194"});
documentSession.Store(new Solution {ID = "solutions/3", ProblemID = "problems/195"});
documentSession.SaveChanges();
}
new ProblemListViewIndex().Execute(documentStore);
using (var documentSession = documentStore.OpenSession())
{
var results = documentSession.Query<ProblemListViewIndex.ReduceResult, ProblemListViewIndex>()
.Customize(x => x.WaitForNonStaleResultsAsOfLastWrite())
.ToList();
Assert.Equal(2, results.Count);
var daniel = results.First(x => x.PostedByName == "Daniel");
var lang = results.First(x => x.PostedByName == "Lang");
Assert.Equal("problems/194", daniel.ID);
Assert.Equal("problems/195", lang.ID);
}
}
}
}
I found this problem too, I found a work around by adding another field to my Index, for example SolutionId, and set it's value to the same as the Id.
You should then be able to run queries against this value without any issues.
I asked the same question on here before, and apparently this is by design.
Adding to what Daniel said, solution.ProblemID is an integer field.
The simplest solution is to change the solution map to be:
ID = "problems/" + solution.ProblemID,
But I would strong suggest that you will use string references, instead of integer references, it will make everything simpler.
Related
I would like to find the occurrences of a word in a text.
I have a class like this
public class Page
{
public string Id { get; set; }
public string BookId { get; set; }
public string Content { get; set; }
public int PageNumber { get; set; }
}
I have my index like this :
class Pages_SearchOccurrence : AbstractIndexCreationTask<Page, Pages_SearchOccurrence.ReduceResult>
{
public class ReduceResult
{
public string PageId { get; set; }
public int Count { get; set; }
public string Word { get; set; }
public string Content { get; set; }
}
public Pages_SearchOccurrence()
{
Map = pages => from page in pages
let words = page.Content
.ToLower()
.Split(new string[] { " ", "\n", ",", ";" }, StringSplitOptions.RemoveEmptyEntries)
from w in words
select new
{
page.Content,
PageId = page.Id,
Count = 1,
Word = w
};
Reduce = results => from result in results
group result by new { PageId = result.PageId, result.Word } into g
select new
{
Content = g.First().Content,
PageId = g.Key.PageId,
Word = g.Key.Word,
Count = g.ToList().Count()
};
Index(x => x.Content, Raven.Abstractions.Indexing.FieldIndexing.Analyzed);
}
}
Finally, my query is like this :
using (var session = documentStore.OpenSession())
{
RavenQueryStatistics stats;
var occurence = session.Query<Pages_SearchOccurrence.ReduceResult, Pages_SearchOccurrence>()
.Statistics(out stats)
.Where(x => x.Word == "works")
.ToList();
}
But I realize that RavenDb is very slow (or my query is not good )
stats.IsStale = true and raven studio take too much time and give only few results.
I have 1000 document “Pages” with a content of 1000 words per Page .
Why is my query not okay and how can I find the occurrences in a page ?
Thank you for your help!
You are doing it wrong. You should set the Content field as Analyzed and use RavenDB's Search() operator. The slowness is most likely because of the amount of un-optimized work your index code is doing.
I had found a partial result.
Perhaps I'm not clear : my goal is to find the occurrences of a word in the page.
I search the hits count of a word in the page and I would like to order by this count.
I changed my index like this :
class Pages_SearchOccurrence : AbstractIndexCreationTask<Page, Pages_SearchOccurrence.ReduceResult>{
public class ReduceResult
{
public string Content { get; set; }
public string PageId { get; set; }
public string Count { get; set; }
public string Word { get; set; }
}
public Pages_SearchOccurrence()
{
Map = pages => from page in pages
let words = page.Content.ToLower().Split(new string[] { " ", "\n", ",", ";" }, StringSplitOptions.RemoveEmptyEntries)
from w in words
select new
{
page.Content,
PageId = page.Id,
Count = 1,
Word = w
};
Index(x => x.Content, Raven.Abstractions.Indexing.FieldIndexing.Analyzed);
Index(x => x.PageId, Raven.Abstractions.Indexing.FieldIndexing.NotAnalyzed);
}
Finally, my new query looks like this :
using (var session = documentStore.OpenSession())
{
var query = session.Query<Pages_SearchOccurrence.ReduceResult, Pages_SearchOccurrence>()
.Search((x) => x.Word, "works")
.AggregateBy(x => x.PageId)
.CountOn(x => x.Count)
.ToList()
.Results
.FirstOrDefault();
var listFacetValues = query.Value.Values;
var finalResult = listFacetValues.GroupBy(x => x.Hits).OrderByDescending(x => x.Key).Take(5).ToList();
}
The finalResult gives me a group of Facetvalue which have a property Hits
( the properties Hits and Count of my FacetValue are the same here )
The Hits property gives me the result that I want but for me this code is not correct and ravendb studio doesn't like this too.
Do you have a better solution ?
I have a many to many relationship:
A post can have many tags
A tag can have many posts
Models:
public class Post
{
public virtual string Title { get; set; }
public virtual string Content{ get; set; }
public virtual User User { get; set; }
public virtual ICollection<Tag> Tags { get; set; }
}
public class Tag
{
public virtual string Title { get; set; }
public virtual string Description { get; set; }
public virtual User User { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
I want to count all posts that belong to multiple tags but I don't know how to do this in NHibernate. I am not sure if this is the best way to do this but I used this query in MS SQL:
SELECT COUNT(*)
FROM
(
SELECT Posts.Id FROM Posts
INNER JOIN Users ON Posts.UserId=Users.Id
LEFT JOIN TagsPosts ON Posts.Id=TagsPosts.PostId
LEFT JOIN Tags ON TagsPosts.TagId=Tags.Id
WHERE Users.Username='mr.nuub' AND (Tags.Title in ('c#', 'asp.net-mvc'))
GROUP BY Posts.Id
HAVING COUNT(Posts.Id)=2
)t
But NHibernate does not allow subqueries in the from clause. It would be great if someone could show me how to do this in HQL.
I found a way of how to get this result without a sub query and this works with nHibernate Linq. It was actually not that easy because of the subset of linq expressions which are supported by nHibernate... but anyways
query:
var searchTags = new[] { "C#", "C++" };
var result = session.Query<Post>()
.Select(p => new {
Id = p.Id,
Count = p.Tags.Where(t => searchTags.Contains(t.Title)).Count()
})
.Where(s => s.Count >= 2)
.Count();
It produces the following sql statment:
select cast(count(*) as INT) as col_0_0_
from Posts post0_
where (
select cast(count(*) as INT)
from PostsToTags tags1_, Tags tag2_
where post0_.Id=tags1_.Post_id
and tags1_.Tag_id=tag2_.Id
and (tag2_.Title='C#' or tag2_.Title='C++'))>=2
you should be able to build your user restriction into this, I hope.
The following is my test setup and random data which got generated
public class Post
{
public Post()
{
Tags = new List<Tag>();
}
public virtual void AddTag(Tag tag)
{
this.Tags.Add(tag);
tag.Posts.Add(this);
}
public virtual string Title { get; set; }
public virtual string Content { get; set; }
public virtual ICollection<Tag> Tags { get; set; }
public virtual int Id { get; set; }
}
public class PostMap : ClassMap<Post>
{
public PostMap()
{
Table("Posts");
Id(p => p.Id).GeneratedBy.Native();
Map(p => p.Content);
Map(p => p.Title);
HasManyToMany<Tag>(map => map.Tags).Cascade.All();
}
}
public class Tag
{
public Tag()
{
Posts = new List<Post>();
}
public virtual string Title { get; set; }
public virtual string Description { get; set; }
public virtual ICollection<Post> Posts { get; set; }
public virtual int Id { get; set; }
}
public class TagMap : ClassMap<Tag>
{
public TagMap()
{
Table("Tags");
Id(p => p.Id).GeneratedBy.Native();
Map(p => p.Description);
Map(p => p.Title);
HasManyToMany<Post>(map => map.Posts).LazyLoad().Inverse();
}
}
test run:
var sessionFactory = Fluently.Configure()
.Database(FluentNHibernate.Cfg.Db.MsSqlConfiguration.MsSql2012
.ConnectionString(#"Server=.\SQLExpress;Database=TestDB;Trusted_Connection=True;")
.ShowSql)
.Mappings(m => m.FluentMappings
.AddFromAssemblyOf<PostMap>())
.ExposeConfiguration(cfg => new SchemaUpdate(cfg).Execute(false, true))
.BuildSessionFactory();
using (var session = sessionFactory.OpenSession())
{
var t1 = new Tag() { Title = "C#", Description = "C#" };
session.Save(t1);
var t2 = new Tag() { Title = "C++", Description = "C/C++" };
session.Save(t2);
var t3 = new Tag() { Title = ".Net", Description = "Net" };
session.Save(t3);
var t4 = new Tag() { Title = "Java", Description = "Java" };
session.Save(t4);
var t5 = new Tag() { Title = "lol", Description = "lol" };
session.Save(t5);
var t6 = new Tag() { Title = "rofl", Description = "rofl" };
session.Save(t6);
var tags = session.Query<Tag>().ToList();
var r = new Random();
for (int i = 0; i < 1000; i++)
{
var post = new Post()
{
Title = "Title" + i,
Content = "Something awesome" + i,
};
var manyTags = r.Next(1, 3);
while (post.Tags.Count() < manyTags)
{
var index = r.Next(0, 6);
if (!post.Tags.Contains(tags[index]))
{
post.AddTag(tags[index]);
}
}
session.Save(post);
}
session.Flush();
/* query test */
var searchTags = new[] { "C#", "C++" };
var result = session.Query<Post>()
.Select(p => new {
Id = p.Id,
Count = p.Tags.Where(t => searchTags.Contains(t.Title)).Count()
})
.Where(s => s.Count >= 2)
.Count();
var resultOriginal = session.CreateQuery(#"
SELECT COUNT(*)
FROM
(
SELECT count(Posts.Id)P FROM Posts
LEFT JOIN PostsToTags ON Posts.Id=PostsToTags.Post_id
LEFT JOIN Tags ON PostsToTags.Tag_id=Tags.Id
WHERE Tags.Title in ('c#', 'C++')
GROUP BY Posts.Id
HAVING COUNT(Posts.Id)>=2
)t
").List()[0];
var isEqual = result == (int)resultOriginal;
}
As you can see at the end I do test against your original query (without the users) and it is actually the same count.
In HQL:
var hql = "select count(p) from Post p where p in " +
"(select t.Post from Tag t group by t.Post having count(t.Post) > 1)";
var result = session.Query(hql).UniqueResult<long>();
You can add additional criteria to the subquery if you need to specify tags or other criteria.
Edit : In the future I should read the questions until the last words. I would have seen in HQL...
After some seach, realizing that RowCount removes any grouping in the query ( https://stackoverflow.com/a/8034921/1236044 ). I found a solution using QueryOver and SubQuery which I post here as information.
I find this solution interesting as it offers some modularity, and seprates the counting from the subquery itself, which can be reused as it is.
var searchTags = new[] { "tag1", "tag3" };
var userNames = new[] { "mr.nuub" };
Tag tagAlias = null;
Post postAlias = null;
User userAlias = null;
var postsSubquery =
QueryOver.Of<Post>(() => postAlias)
.JoinAlias(() => postAlias.Tags, () => tagAlias)
.JoinAlias(() => postAlias.User, () => userAlias)
.WhereRestrictionOn(() => tagAlias.Title).IsIn(searchTags)
.AndRestrictionOn(() => userAlias.UserName).IsIn(userNames)
.Where(Restrictions.Gt(Projections.Count<Post>(p => tagAlias.Title), 1));
var numberOfPosts = session.QueryOver<Post>()
.WithSubquery.WhereProperty(p => p.Id).In(postsSubquery.Select(Projections.Group<Post>(p => p.Id)))
.RowCount();
Hope this will help
I would like to be able to query the first 10 documents from a collection in RavenDB ordered by the count with a constraint in a sublist. This is my entity:
public class Post
{
public string Title { get; set; }
public List<Like> Likes { get; set; }
}
public class Like
{
public DateTime Created { get; set; }
}
I've tried with the following query:
var oneMonthAgo = DateTime.Today.AddMonths(-1);
session
.Query<Post>()
.OrderByDescending(x => x.Likes.Count(y => y.Created > oneMonthAgo))
.Take(10);
Raven complaints that count should be done on index time rather than query time. I've tried moving the count to a index using the following code:
public class PostsIndex : AbstractIndexCreationTask<Post>
{
public PostsIndex()
{
var month = DateTime.Today.AddMonths(-1);
Map = posts => from doc in posts
select
new
{
doc.Title,
LikeCount = doc.Likes.Count(x => x.Created > month),
};
}
}
When adding this index, Raven throws a error 500.
What to do?
You can do this by creating a Map/Reduce index to flatten the Posts/Likes and then query over that.
The index:
public class PostLikesPerDay : AbstractIndexCreationTask<Post, PostLikesPerDay.Result>
{
public PostLikesPerDay()
{
Map = posts => from post in posts
from like in post.Likes
select new Result
{
Title = post.Title,
Date = like.Created,
Likes = 1
};
Reduce = results => from result in results
group result by new
{
result.Title,
result.Date.Date
}
into grp
select new Result
{
Title = grp.Key.Title,
Date = grp.Key.Date,
Likes = grp.Sum(l => l.Likes)
};
}
public class Result
{
public string Title { get; set; }
public DateTime Date { get; set; }
public int Likes { get; set; }
}
}
And the query:
using (var session = store.OpenSession())
{
var oneMonthAgo = DateTime.Today.AddMonths(-1);
var query = session.Query<PostLikesPerDay.Result, PostLikesPerDay>()
.Where(y => y.Date > oneMonthAgo)
.OrderByDescending(p => p.Likes)
.Take(10);
foreach (var post in query)
{
Console.WriteLine("'{0}' has {1} likes on {2:d}", post.Title, post.Likes, post.Date);
}
}
Say I have a User class like this:
public class User
{
public string Id {get; set;}
public string Name {get; set;}
}
Each User can be either a Mentor, a Mentee or both. This is represented by a Relationship class:
public class Relationship
{
public string MentorId {get; set;} // This is a User.Id
public string MenteeId {get; set;} // This is another User.Id
}
Now I would like to generate a report that lists all of my Users and contains a field called Mentor Count and another field called Mentee Count. To achieve this I have created a UserReportDTO class to hold my report data.
public class UserReportDTO
{
public string Name {get; set;}
public string MentorCount {get; set;}
public string MenteeCount {get; set;}
}
I then query my RavenDB to get a list of all the Users and transform this into a list of UserReportDTO instances.
UserService
public List<UserReportDTO> GetReportChunk(
IDocumentSession db,
int skip = 0,
int take = 1024)
{
return db.Query<User>()
.OrderBy(u => u.Id)
.Skip(skip)
.Take(take)
.ToList()
.Select(user =>
new UserReportDTO
{
Name = user.Name,
MentorCount = // What goes here?
MenteeCount = // What goes here?
})
.ToList();
}
As you can see, I am struggling to work out the best way to retrieve the MentorCount and MenteeCount values. I have written some Map/Reduce Indexes that I think should be doing the job but I am unsure how to use them to achieve the result I want.
Question
What is the best way to include multiple aggregate fields into a single query?
EDIT 1
#Matt Johnson: I have implemented your index (see end) and now have a working Report Query which, in case anybody is interested, looks like this:
Working User Report Query
public List<UserDTO> GetReportChunk(IDocumentSession db, Claim claim, int skip = 0, int take = 1024)
{
var results = new List<UserDTO>();
db.Query<RavenIndexes.Users_WithRelationships.Result, RavenIndexes.Users_WithRelationships>()
.Include(o => o.UserId)
.Where(x => x.Claims.Any(c => c == claim.ToString()))
.OrderBy(x => x.UserId)
.Skip(skip)
.Take(take)
.ToList()
.ForEach(p =>
{
var user = db.Load<User>(p.UserId);
results.Add(new UserDTO
{
UserName = user.UserName,
Email = user.Email,
// Lots of other User properties
MentorCount = p.MentorCount.ToString(),
MenteeCount = p.MenteeCount.ToString()
});
});
return results;
}
MultiMap Index
public class Users_WithRelationships :
AbstractMultiMapIndexCreationTask<Users_WithRelationships.Result>
{
public class Result
{
public string UserId { get; set; }
public string[] Claims { get; set; }
public int MentorCount { get; set; }
public int MenteeCount { get; set; }
}
public Users_WithRelationships()
{
AddMap<User>(users => users.Select(user => new
{
UserId = user.Id,
user.Claims,
MentorCount = 0,
MenteeCount = 0
}));
AddMap<Relationship>(relationships => relationships.Select(relationship => new
{
UserId = relationship.MentorId,
Claims = (string[]) null,
MentorCount = 0,
MenteeCount = 1
}));
AddMap<Relationship>(relationships => relationships.Select(relationship => new
{
UserId = relationship.MenteeId,
Claims = (string[]) null,
MentorCount = 1,
MenteeCount = 0
}));
Reduce = results => results.GroupBy(result => result.UserId).Select(g => new
{
UserId = g.Key,
Claims = g.Select(x => x.Claims).FirstOrDefault(x => x != null),
MentorCount = g.Sum(x => x.MentorCount),
MenteeCount = g.Sum(x => x.MenteeCount)
});
}
}
You might be better served with a model that already has your relationship data kept with the user. This might look something like:
public class User
{
public string Id { get; set; }
public string Name { get; set; }
public string[] MentorUserIds { get; set; }
public string[] MenteeUserIds { get; set; }
}
However, if you want to stick with the model you described, the solution is to get rid of the multiple separate indexes and create a single multi-map index that has the data you need.
public class Users_WithRelationships
: AbstractMultiMapIndexCreationTask<Users_WithRelationships.Result>
{
public class Result
{
public string UserId { get; set; }
public string Name { get; set; }
public int MentorCount { get; set; }
public int MenteeCount { get; set; }
}
public Users_WithRelationships()
{
AddMap<User>(users => from user in users
select new
{
UserId = user.Id,
Name = user.Name,
MentorCount = 0,
MenteeCount = 0
});
AddMap<Relationship>(relationships => from relationship in relationships
select new
{
UserId = relationship.MentorId,
Name = (string)null,
MentorCount = 1,
MenteeCount = 0
});
AddMap<Relationship>(relationships => from relationship in relationships
select new
{
UserId = relationship.MenteeId,
Name = (string)null,
MentorCount = 0,
MenteeCount = 1
});
Reduce = results =>
from result in results
group result by result.UserId
into g
select new
{
UserId = g.Key,
Name = g.Select(x => x.Name).FirstOrDefault(x => x != null),
MentorCount = g.Sum(x => x.MentorCount),
MenteeCount = g.Sum(x => x.MenteeCount)
};
}
}
Then you can update your GetReportChunk method to query against the one index if you still want to project a custom DTO.
return db.Query<Users_WithRelationships.Result, Users_WithRelationships>()
.OrderBy(x => x.UserId)
.Skip(skip)
.Take(take)
.Select(x =>
new UserReportDTO
{
Name = x.Name,
MentorCount = x.MentorCount,
MenteeCount = x.MenteeCount,
})
.ToList();
I'm attempting to apply Ayende's order search from here to an existing index.
The current index looks like this:
public class HomeBlurb_IncludeTotalCosts_Search2 : AbstractIndexCreationTask<MPDocument, HomeBlurb_IncludeTotalCosts_Search2.ReduceResult>
{
public class ReduceResult
{
public string Name { get; set; }
public string Constituency { get; set; }
public decimal? AmountPaid { get; set; }
}
public HomeBlurb_IncludeTotalCosts_Search2()
{
Map = mps => from mp in mps
from exp in mp.Expenses
select new
{
mp.Name,
mp.Constituency,
exp.AmountPaid
};
Reduce = results => from result in results
group result by new { result.Name, result.Constituency } into g
select new
{
Name = g.Key.Name,
Constituency = g.Key.Constituency,
AmountPaid = g.Sum(x => x.AmountPaid)
};
Index(x => x.Name, FieldIndexing.Analyzed);
Index(x => x.Constituency, FieldIndexing.Analyzed);
}
}
This index works fine. However when I try to change the Map to:
from mp in mps
from exp in mp.Expenses
select new
{
Query = new object[]{mp.Name,mp.Constituency},
mp.Name,
mp.Constituency,
exp.AmountPaid
};
and the reduce to
from result in results
group result by new { result.Name, result.Constituency } into g
select new
{
Query = "",
Name = g.Key.Name,
Constituency = g.Key.Constituency,
AmountPaid = g.Sum(x => x.AmountPaid)
};
I then get no results when querying on the Query property. If I remove the reduce the index returns data, but it always returns the full MPDocument, which is much more that I was to materialise. Is there a way to use the technique described in the original post that also utilises a reduce?
You can use this in your reduce function:
Query = g.Select(x => x.Query).Where(x => x != null).FirstOrDefault()
In order to query on that field, you need to have a seperate query model and a result model. Here is a full example using your code:
public class MultiTermFieldInMapReduce
{
public class MPDocument
{
public List<Epense> Expenses { get; set; }
public string Name { get; set; }
public string Constituency { get; set; }
public class Epense
{
public decimal? AmountPaid { get; set; }
}
}
public class HomeBlurb_IncludeTotalCosts_Search2 : AbstractIndexCreationTask<MPDocument, HomeBlurb_IncludeTotalCosts_Search2.ReduceResult>
{
public class ReduceResult
{
public string Name { get; set; }
public string Constituency { get; set; }
public decimal? AmountPaid { get; set; }
public object[] Query { get; set; }
}
public class SearchModel
{
public string Name { get; set; }
public string Constituency { get; set; }
public decimal? AmountPaid { get; set; }
public string Query { get; set; }
}
public HomeBlurb_IncludeTotalCosts_Search2()
{
Map = mps => from mp in mps
from exp in mp.Expenses
select new
{
mp.Name,
mp.Constituency,
exp.AmountPaid,
Query = new object[]
{
mp.Name,
mp.Constituency
}
};
Reduce = results => from result in results
group result by new { result.Name, result.Constituency } into g
select new
{
Name = g.Key.Name,
Constituency = g.Key.Constituency,
AmountPaid = g.Sum(x => x.AmountPaid),
Query = g.Select(x => x.Query).Where(x => x != null).FirstOrDefault()
};
Index(x => x.Name, FieldIndexing.Analyzed);
Index(x => x.Constituency, FieldIndexing.Analyzed);
}
}
[Fact]
public void Query_returns_results()
{
using (var store = new EmbeddableDocumentStore { RunInMemory = true }.Initialize())
{
using (var session = store.OpenSession())
{
session.Store(new MapReduceError.MPDocument
{
Name = "test1",
Expenses = new List<MapReduceError.MPDocument.Epense>
{
new MapReduceError.MPDocument.Epense {AmountPaid = 5.5m},
new MapReduceError.MPDocument.Epense {AmountPaid = 5.5m},
new MapReduceError.MPDocument.Epense {AmountPaid = 5.5m},
new MapReduceError.MPDocument.Epense {AmountPaid = 5.5m}
}
});
session.Store(new MapReduceError.MPDocument
{
Name = "test2",
Expenses = new List<MapReduceError.MPDocument.Epense>
{
new MapReduceError.MPDocument.Epense {AmountPaid = 10},
new MapReduceError.MPDocument.Epense {AmountPaid = 10},
new MapReduceError.MPDocument.Epense {AmountPaid = 10},
new MapReduceError.MPDocument.Epense {AmountPaid = 10}
}
});
session.SaveChanges();
}
new HomeBlurb_IncludeTotalCosts_Search2().Execute(store);
using (var session = store.OpenSession())
{
var results =
session.Query
<HomeBlurb_IncludeTotalCosts_Search2.SearchModel, HomeBlurb_IncludeTotalCosts_Search2>()
.Customize(x => x.WaitForNonStaleResultsAsOfLastWrite())
.Where(x => x.Query == "test1")
.As<HomeBlurb_IncludeTotalCosts_Search2.ReduceResult>()
.ToList();
Assert.Equal(1, results.Count);
Assert.Equal(22, results.First().AmountPaid);
}
}
}
}
I don't think you need a Map/Reduce index to do this, you seem to only be reducing on MPDocument Name and Constituency, which I guess are unique to each MP.
I think that you want to use TransformResults, so that you can change the shape of the output instead, something like so:
TransformResults =
(database, mps) => from mp in mps
select new
{
mp.Name,
mp.???
< JUST THE BITS YOU WANT RETURNED >
};
Then you query like so:
session.Query<mp>("index name")
.Where(..)
.As<MPQueryResult>()
.ToList()