【问题标题】:RavenDb - Query on a field that is aggregated in the Reduce functionRavenDb - 查询在 Reduce 函数中聚合的字段
【发布时间】:2012-11-06 11:04:17
【问题描述】:

我有一组代表一些工作项的文档:

public class WorkItem
{
    public string Id {get;set;
    public string DocumentId { get; set; }
    public string FieldId { get; set; }
    public bool IsValidated { get; set; }
}

public class ExtractionUser
{
    public string Id {get;set;}
    public string Name {get;set;}
    public string[] AssignedFields {get;set;}
}

用户有权访问一组 FieldId。我需要根据这组字段查询 WorkItems 并获取每个文档的状态:

public class UserWorkItems
{
    public string DocumentId { get; set; }
    public int Validated { get; set; }
    public int Total { get; set; }
}

我的查询是这样的:

using (var session = RavenDb.OpenSession())
{
    string[] userFields = session.Load<User>("users/1").Fields;
    session.Query<WorkItem>()
        .Where(w => w.FieldId.In(userFields))
        .GroupBy(w => w.DocumentId)
        .Select(g => new
        {
            DocumentId = g.Key,
            Validated = g.Where(w => w.IsValidated).Count(),
            Total = g.Count()
        }).Skip(page * perPage).Take(perPage)
        .ToArray();
}

我已尝试创建 Map/Reduce 索引,但主要问题是我需要能够对未包含在 Reduce 输出中的 FieldId 应用过滤器,因为它是被计算的属性。

我还尝试在查询部分的 FieldId 上创建一个简单的 Map 索引,并使用 TransformResults 来执行 GroupBy - 但由于在 TransformResults 之前应用了分页,因此页面和总计反映了分组之前的文档,这是不好的。

然后我尝试使用 Multi Map 索引来映射用户及其字段集合,并映射工作项和字段,然后尝试将结果减少到我想要的。我用索引定义创建了a gist。减少部分涉及按字段分组,然后是多个 SelectMany 和最后一个 GroupBy 和 Select。该索引已被 raven 接受,但 i 不返回任何结果。我有点卡在 Multi Map 索引上,因为我不知道如何实际调试它。

我想最后我的问题可能会减少(双关语)如何查询“减少”字段?

有什么想法可以实现这样的功能吗?除了 Map/MultiMap/Reduce/TransformResults 之外,我还有其他选择吗?

更新:在阅读 Ayende's Map Reduce post 时,我意识到我正在接近 mapreduce 错误。仍在寻找解决方案...

更新 2:经过更多研究,我最终得到了这个索引,它看起来像我想要做的,但没有返回任何数据(索引是直接在工作室中定义的) :

地图:

from user in docs
where user["@metadata"]["Raven-Entity-Name"] == "ExtractionUsers"
from field in user.AssignedFields
from item in docs
where item["@metadata"]["Raven-Entity-Name"] == "WorkItems" && item.FieldId == field
select new {
    UserId = user.Id,
    DocumentId = item.DocumentId,
    Validated = item.Status=="Validated"? 1: 0,
    Count = 1
}

减少:

from r in results
group r by new { r.UserId , r.DocumentId } into g
select new {
    UserId = g.Key.UserId,
    DocumentId = g.Key.DocumentId,
    Validated = g.Sum(d => d.Validated),
    Count = g.Sum(d => d.Count),
}

这个想法是尝试在索引中映射所有文档,并从用户链接到字段和工作项。

【问题讨论】:

  • 您能否更详细地描述一下您的结构(UserField 类)?
  • 我添加了 User ( ExtractionUser ) 类的结构 - 它只是一个 Id、Name 和 AssignedFields 集合。该字段仅作为字段名称的 Id 存在 - 所以那里没有实际的类。需要考虑的一件事是,AssignedFields 会随着用户获得分配给它们的新字段而及时更改 - 因此非规范化工作项中的信息不是一种选择。
  • 您发布的更新索引不起作用,您不能在这样的索引中包含 2 个“from x in docs”。如果您想跨多种类型建立索引,请查看 Multi-Map,ravendb.net/kb/11/…
  • @MattWarren 谢谢,我现在正在尝试使用多映射减少,但减少部分没有返回所有结果。我创建了一个gist here
  • 我在mailinglist上发布了更多信息

标签: mapreduce ravendb


【解决方案1】:

一周后,我设法解决了这个问题。我采用了一种稍微不同(关系较少)的方法,这种方法很简单,而且看起来效果很好。以下是其他人遇到此类问题的详细信息:

我按 DocumentId 对 WorkItems 进行分组,并将 Validated 和 NonValidated 字段放入一个集合中。 map reduce 的结果如下所示:

public class Result
{
    public string DocumentId { get; set; }
    public string[] ValidatedFields { get; set; }
    public string[] ReadyFields { get; set; }
}

地图函数如下所示:

Map = items => items.Select(i => new
{
    DocumentId = i.DocumentId,
    ValidatedFields = i.IsValidated ? new string[] { i.FieldId } : new string[0],
    ReadyFields = !i.IsValidated ? new string[] { i.FieldId } : new string[0]
});

还有减少

Reduce = result => result
    .GroupBy(i => i.DocumentId)
    .Select(g => new
    {
        DocumentId = g.Key,
        ValidatedFields = g.SelectMany(i => i.ValidatedFields),
        ReadyFields = g.SelectMany(i => i.ReadyFields)
    });

要查询索引,我现在使用以下表达式:

User user = session.Load<User>("users/1");
var result = session.Query<WorkItem, UserWorkItemIndex>()
    .As<UserWorkItemIndex.Result>()
    .Where(d => d.ValidatedFields.Any(f => f.In(user.AssignedFields)))
    .ToArray();

客户端我唯一需要做的就是只计算属于用户的字段。

解决方案还有gist

【讨论】:

    【解决方案2】:

    首先,免责声明:我之前从未在真实系统中使用过 RavenDB,但我阅读了一些文章,观看了一些视频,并且非常喜欢它背后的想法。我认为这个问题是一个有趣的练习。因此,这种方法可能并不理想;欢迎使用 cmets 和改进。

    我的想法是必须在 WorkItems 集合上创建索引以包含这些字段:

    • DocumentId(因为这就是我们最终要分组的内容)
    • FieldId(因为这是我们要过滤的字段)
    • ValidatedCountIsValidated = true 的记录数)
    • TotalCount

    创建此索引后,我们可以使用.Where(x =&gt; x.FieldId.In(userFields)) 过滤器对其进行查询,并返回一组具有上述结构的结果。

    为了得到最终结果,我们需要对这些结果进行更多的分组DocumentId

    我想出的代码是这样的:

    索引定义

    public class WorkItems_ValidationStatistics :
        AbstractIndexCreationTask<WorkItem, WorkItems_ValidationStatistics.ReduceResult>
    {
        public class ReduceResult
        {
            public string DocumentId { get; set; }
            public string FieldId { get; set; }
            public int ValidatedCount { get; set; }
            public int TotalCount { get; set; }
        }
    
        public WorkItems_ValidationStatistics()
        {
            Map = workItems =>
                  from workItem in workItems
                  select new
                        {
                            workItem.DocumentId,
                            workItem.FieldId,
                            ValidatedCount = workItem.IsValidated ? 1 : 0,
                            TotalCount = 1
                        };
    
            Reduce = results =>
                     from result in results
                     group result by new { result.FieldId, result.DocumentId }
                         into g
                         select new
                            {
                                g.Key.DocumentId,
                                g.Key.FieldId,
                                ValidatedCount = g.Sum(x => x.ValidatedCount),
                                TotalCount = g.Sum(x => x.TotalCount)
                            };
        }
    }
    

    在数据库中创建索引的代码:

    public void CreateIndex()
    {
        using (var store = CreateDocumentStore())
        {
            IndexCreation.CreateIndexes(
                typeof(WorkItems_ValidationStatistics).Assembly, store);
        }
    }
    

    注意:或者,您可以直接在 RavenDB Management Studio 中创建索引

    查询索引并进行最终聚合的代码:

    public void GetWorkItemStatisticsGroupedByDocumentId()
    {
        using (var store = CreateDocumentStore())
        using (var documentSession = store.OpenSession())
        {
            var userFields = new[] { "fields/11", "fields/13" };
    
            var items = documentSession
                .Query<WorkItems_ValidationStatistics.ReduceResult, WorkItems_ValidationStatistics>()
                .Where(x => x.FieldId.In(userFields))
                .ToList();
    
            var results = items
                .GroupBy(x => x.DocumentId)
                .Select(g => new
                    {
                        DocumentId = g.Key,
                        ValidatedCount = g.Sum(x => x.ValidatedCount),
                        TotalCount = g.Sum(x => x.TotalCount)
                    });
    
            foreach (var r in results)
            {
                Console.WriteLine("DocId={0}: validated: {1}/{2}", 
                    r.DocumentId, r.ValidatedCount, r.TotalCount);
            }
        }
    }
    

    【讨论】:

    • 感谢您的回答,但这将在客户端上执行分组,并且由于我可能有大量工作项(每个文档的字段),这不是一个可行的选择。我尝试过的第一种方法是使用 Map 作为 FieldId 和 TransformResult 进行分组,但在服务器上进行分组但它在 TransformResult 之前应用分页,这在我的情况下是不行的我需要对最终分组结果进行分页,而不是对工作项进行分页。
    • @IulianMargarintescu 你说得对;出于某种原因,我假设在客户端上分组的记录会更少,但我的这个假设是错误的。我会想办法解决这个问题。
    • TransformResult中在客户端或服务器上分组的主要问题是分组前会对数据进行分页,处理起来不太方便。
    • @IulianMargarintescu 正确。我一直在想另一种方法来解决这个问题,但到目前为止还没有其他想法。我在动态查询页面中停留了一段时间...... :)
    • 感谢您投入的时间。我还在raven mailinglist 上发布了更多详细信息。问题是我可以解决这个问题,因为我可以完全控制应用程序,但我发现这是深入了解高级 RavenDB 的绝佳机会;-)
    猜你喜欢
    • 2014-04-09
    • 1970-01-01
    • 1970-01-01
    • 2021-02-16
    • 1970-01-01
    • 2021-10-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多