首页 文章

如何创建一个在大型动态数据集中计算标记的性能系统

提问于
浏览
2

Overview

我有一个 iOS 应用程序,人们可以通过 tags 搜索某些标签将被预先定义,一些将由用户定义 .

当用户编写他/她想要搜索的 tags 时,我想显示一行显示那些 tags 可用的结果(参见示例搜索图片) .

Note: #Exercise#Routine 是父 tag ,这意味着该人总是首先使用其中一个 .

我正在使用 PHPMongoDB 服务器端 . 我创建一个标记每小时计数的文件,以便所有客户端都可以获取它并最大限度地减少资源消耗 .

鉴于被操纵的信息是用户控制的,列表将随着时间的推移而显着扩展 .

The challenge

  • 当考虑到创建,操作和存储这样的列表的性能和开销时,我很困惑最好的方法 .

My first idea 是创建一个 2d array (见图),它将存储我的所有值 . 然后将其转换为JSON,以便它可以存储到MongoDB中 .

但是这种方法会让我获取所有标签并将它们加载到内存中以执行任何1或-1 . 因此,我认为它可能不是最好的 .

所有操作都将在每个元素的插入,更新和删除时进行 . 因此,将有相当大的开销 .

My second idea 是创建一个 document ,我存储了所有用过的 tags 并每小时进行一次计数查询以生成客户端使用的列表 .

这意味着每次删除,更新和插入检查标签都存在于 document 上并根据条件创建或删除或不执行任何操作 .

每小时,获取所有标签,生成包含所有标签组合的数组 . 根据所有标签组合查询数据库,并计算返回的结果数并创建文件 .

我认为,这种方法可能是更好的方法,因为我使用 MongoDB 而不是 MySQL . 但我仍然不确定它的表现 .

有没有人 Build 过类似的系统,可以就更好的方法提出建议?

Example Picture of the search

2d Array

3 回答

  • 0

    使用mongodb时要考虑的最重要的事情之一是,在决定数据库设计时,必须考虑应用程序的访问模式 . 我们将尝试使用示例来解决您的问题并查看其工作原理 .

    让我们考虑一下您的集合中包含以下文档,让我们看看如何使这种简单的数据格式工作奇迹:

    > db.performant.find()
    { "_id" : ObjectId("522bf7166094a4e72db22827"), "name" : "abc", "tags" : [  "chest",  "bicep",  "tricep" ] }
    { "_id" : ObjectId("522bf7406094a4e72db22828"), "name" : "def", "tags" : [  "routine",  "trufala",  "tricep" ] }
    { "_id" : ObjectId("522bf75f6094a4e72db22829"), "name" : "xyz", "tags" : [  "routine",  "myTag",  "tricep",  "myTag2" ] }
    { "_id" : ObjectId("522bf7876094a4e72db2282a"), "name" : "mno", "tags" : [  "exercise",  "myTag",  "tricep",  "myTag2",  "biceps" ] }
    

    首先,你 absolutely must 在标签上创建一个索引 . (如果需要,你可以创建一个复合索引)

    > db.performant.ensureIndex({tags:1})
    > db.performant.getIndexes()
    [
        {
                "v" : 1,
                "key" : {
                        "_id" : 1
                },
                "ns" : "test.performant",
                "name" : "_id_"
        },
        {
                "v" : 1,
                "key" : {
                        "tags" : 1
                },
                "ns" : "test.performant",
                "name" : "tags_1"
        }
    ]
    

    要查询上述集合中的标签数据,通常会使用db.performant.find({tags:{$ in:["bicep"]}}),但这是 not a good idea . 让我告诉你原因:

    > db.performant.find({tags:{$in:["bicep","chest","trufala"]}}).explain()
    {
        "cursor" : "BtreeCursor tags_1 multi",
        "isMultiKey" : true,
        "n" : 2,
        "nscannedObjects" : 3,
        "nscanned" : 5,
        "nscannedObjectsAllPlans" : 3,
        "nscannedAllPlans" : 5,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 0,
        "nChunkSkips" : 0,
        "millis" : 0,
        "indexBounds" : {
                "tags" : [
                        [
                                "bicep",
                                "bicep"
                        ],
                        [
                                "chest",
                                "chest"
                        ],
                        [
                                "trufala",
                                "trufala"
                        ]
                ]
        },
        "server" : "none-6674b8f4f2:27017"
    }
    

    您可能已经注意到,此查询正在执行整个集合扫描 . 这可能会让你想知道,为什么黑客我们添加了索引,如果它没有被使用,我也想知道 . 但不幸的是,这是一个尚未被mongoDB解决的问题(至少据我所知)

    幸运的是,我们可以解决这个问题,仍然使用我们在tags集合上创建的索引 . 方法如下:

    > db.performant.find({$or:[{tags:"bicep"},{tags:"chest"},{tags:"trufala"}]}).explain()
    {
        "clauses" : [
                {
                        "cursor" : "BtreeCursor tags_1",
                        "isMultiKey" : true,
                        "n" : 1,
                        "nscannedObjects" : 1,
                        "nscanned" : 1,
                        "nscannedObjectsAllPlans" : 1,
                        "nscannedAllPlans" : 1,
                        "scanAndOrder" : false,
                        "indexOnly" : false,
                        "nYields" : 0,
                        "nChunkSkips" : 0,
                        "millis" : 10,
                        "indexBounds" : {
                                "tags" : [
                                        [
                                                "bicep",
                                                "bicep"
                                        ]
                                ]
                        }
                },
                {
                        "cursor" : "BtreeCursor tags_1",
                        "isMultiKey" : true,
                        "n" : 0,
                        "nscannedObjects" : 1,
                        "nscanned" : 1,
                        "nscannedObjectsAllPlans" : 1,
                        "nscannedAllPlans" : 1,
                        "scanAndOrder" : false,
                        "indexOnly" : false,
                        "nYields" : 0,
                        "nChunkSkips" : 0,
                        "millis" : 0,
                        "indexBounds" : {
                                "tags" : [
                                        [
                                                "chest",
                                                "chest"
                                        ]
                                ]
                        }
                },
                {
                        "cursor" : "BtreeCursor tags_1",
                        "isMultiKey" : true,
                        "n" : 1,
                        "nscannedObjects" : 1,
                        "nscanned" : 1,
                        "nscannedObjectsAllPlans" : 1,
                        "nscannedAllPlans" : 1,
                        "scanAndOrder" : false,
                        "indexOnly" : false,
                        "nYields" : 0,
                        "nChunkSkips" : 0,
                        "millis" : 0,
                        "indexBounds" : {
                                "tags" : [
                                        [
                                                "trufala",
                                                "trufala"
                                        ]
                                ]
                        }
                }
        ],
        "n" : 2,
        "nscannedObjects" : 3,
        "nscanned" : 3,
        "nscannedObjectsAllPlans" : 3,
        "nscannedAllPlans" : 3,
        "millis" : 10,
        "server" : "none-6674b8f4f2:27017"
    }
    

    如您所见,n非常接近nscanned . 扫描有三个记录,每个记录对应于“二头肌”,“胸部”,“特拉法拉” . 由于“bicep”和“chest”属于同一文档,因此只返回1个与之对应的结果 . 通常,count()和find()都会进行有限的扫描并且非常有效 . 此外,您永远不必为陈旧数据的用户提供服务 . 您还可以完全避免运行任何类型的批处理作业!

    因此,通过使用这种方法,我们可以推导出以下 conclusion :如果您搜索n个标签,并且每个标签存在m次,则扫描的总文档将为n * m . 现在考虑到你有大量的标签和大量的文件,你扫描几个标签(反过来对应少数文件 - 虽然不是1:1),结果总是超快,因为1文件扫描发生每个标签和文档组合 .

    Note: 此方法永远不会涵盖索引,因为数组上有索引,即"isMultiKey":true . 您可以阅读有关覆盖索引的更多信息here

    Limitations: 每种方法都有局限性,而且这个方法也有限制!对结果进行排序将产生极差的性能,因为它将扫描整个集合的次数等于传递给此查询的标记,并且它会扫描与$或每个参数对应的其他记录 .

    > db.performant.find({$or:[{tags:"bicep"},{tags:"chest"},{tags:"trufala"}]}).sort({tags:1}).explain()
    {
        "cursor" : "BtreeCursor tags_1",
        "isMultiKey" : true,
        "n" : 2,
        "nscannedObjects" : 15,
        "nscanned" : 15,
        "nscannedObjectsAllPlans" : 15,
        "nscannedAllPlans" : 15,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 0,
        "nChunkSkips" : 0,
        "millis" : 0,
        "indexBounds" : {
                "tags" : [
                        [
                                {
                                        "$minElement" : 1
                                },
                                {
                                        "$maxElement" : 1
                                }
                        ]
                ]
        },
        "server" : "none-6674b8f4f2:27017"
    }
    

    在这种情况下,它扫描15次,相当于3次完整收集扫描,每次扫描4条记录加上每条参数扫描的3条记录,用于$或

    Final verdict: 如果您对未分类的结果没问题,或者愿意在前端做出额外的努力来对它们进行排序,请使用此方法获得非常有效的结果你自己 .

  • 0

    由于这个问题很长,我将列出一些我在阅读过几次问题后所做的假设:

    • 人们可以输入n个标签进行搜索 .

    • 标签是预定义的或用户定义的 .

    • 标签会随着时间的推移而大幅增长 .

    • 我们想要给定一组标签的总文件数 . 如果#tag1有10而#tag2有13,那么我们有大约23个文件(考虑到一些可能有两个标签) .

    我有一些方法的建议:

    • 您已经开始以这种方式思考,但计划将读取与写入分开 . 如果有人使用 #chest 标记文档,请立即写入,但可以在所有读取中没有该信息 . (这可以追溯到每小时运行一次的工作 . )

    • 您可以为标记文档的用户提供即时反馈 . 我承诺了've read that YouTube does this. You like something on YouTube and it increments the number immediately, even though the write hasn't . 这个想法是你立即给用户一些东西 - 即使它不完全准确 .

    • 在(2)的基础上,考虑数字只需要"good enough" . 搜索 #chest#routine ...让我们说返回 100 结果 . 随意显示98或99 . 只要用户能够感受到有多少结果,它就可以立即考虑1和-1 .

    • 考虑运行将数据移出Mongo的作业 . 你可以让Mongo运行map / reduce查询,输出到Mongo中的集合 . 然后你可以把它放在像Redis这样的东西上来提供数据 . 或者我想你可以把它保存在Mongo中 .

    • 我不确定我会创建所有可能的组合 . 这将非常快地失去控制 - 特别是如果人们可以创建自己的标签 . 相反,只需创建一个具有带计数的搜索项的简单数据结构 . ["#chest",100],["#routine",543],["#triceps",12],...

    • 可以指出,如果文档有多个标签,我们将对它进行两次计数 . 这是真的,但我认为简单而言只留下它 . 如果您愿意牺牲一点精确度,那么您的代码将更容易维护并且性能良好 .

  • 2

    Update: rewriting this concept to avoid confusion over MySQL examples

    正如原始答案所述,我对MongoDB一无所知 . 所以,我将简单解释这个概念 .

    可能仍然有一种方法可以获得具有良好性能的准确计数 . 这是基于您希望标记列表(包含所有请求的标记的所有文档)的交集的假设 .

    1)创建一个包含3个字段的新集合(tags_query_cache):

    • tags_key:primary key - 查询中所有标记的字符串

    • tags_count:包含tags_list中所有标记的文档数

    • tags_list:一个标签数组 - tags_key中的所有标签(应该是标签上的索引)

    2)更新时|添加|删除原始集合中的文档:

    • 标识文档中已更改(添加或删除)的所有标记

    • 删除tags_query_cache集合中包含已标识的已更改标记的所有文档

    3)当对原始集合执行查询(通过标签)时:

    • 通过imploding(PHP implode函数)创建tags_key,这是一个排序的小写,正在查询的标签列表

    • 在tags_query_cache中找到具有tags_key =标记的内嵌列表的文档

    • 如果找到文件,则tags_count包含您的计数

    • 如果找不到文档,查询原始集合以获取文档数量(参见@Nilesh Rajani的答案)

    • 使用刚刚检索到的计数为tags_query_cache集合添加一个新文档

    这种方法的优势:

    • 您始终拥有当前文档计数,并且能够通过一个完整的密钥匹配从tags_query_cache中检索它们(如果tags_query_cache文档存在)

    • 您只为相关的标签组合构建tags_query_cache文档(即您不必预测每种可能的组合)

    • 构建tags_query_cache的处理要求分布在多个用户查询中 .

    根据预期的原始集合更新量与预期的查询量以及所需文档数量的准确性,可以采取其他路径 . 如果您不必具有100%准确的计数,或者预期更新量与标记查询量(不太可能)相比是显着的,您可以执行以下操作:

    • 将last_updated日期/时间字段添加到tags_query_cache集合中

    • don 't delete from tags_query_cache every time a document in your original collection has a change to it' s标签
      从tags_query_collection中检索时,如果last_updated日期/时间早于所需的时间间隔,则重新计算原始集合中包含所需标记的文档数,并更新tags_query_cache文档 .

    还应该注意的是tags_query_cache不具备驻留在磁盘上的MongoDB中 . 可以使用可在所有正在运行的进程中共享的内存高速缓存来提高性能 . 缺点是,当机器出现故障时,您将丢失缓存结果 .

    祝好运 . 希望这比原来更清楚 .

相关问题