2

概述

我有一个iOS应用程序,人们可以通过其中一些标签进行搜索,tags其中一些标签将是预定义的,而一些标签将是用户定义的。

当用户编写tags他/她想要搜索的内容时,我想显示一行显示可用结果的数量tags(参见示例搜索图片)。

注意: #Exercise or #Routine are parenttag意思是这个人总是会首先使用其中一个。

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

鉴于操纵的信息是tags用户控制的,随着时间的推移,该列表将大大扩展。

挑战

  • 当考虑到创建、操作和存储此类列表的性能和开销时,我对什么是最佳方法感到困惑。

我的第一个想法是创建一个2d array(见图)来存储我所有的值。然后将其转换为 JSON,以便可以存储到 MongoDB 中。

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

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

我的第二个想法是创建一个document存储我所有使用过的地方tags,并每小时进行一次计数查询以生成客户使用的列表。

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

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

我认为,考虑到我使用MongoDB而不是MySQL. 但我仍然不确定它的性能。

有没有人制作了类似的系统并可以就更好的方法提出建议?

搜索示例图片

二维数组

4

3 回答 3

2

使用 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" ] }

首先,您绝对必须在标签上创建索引。(如果需要,您可以创建一个复合索引)

> 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"]}}),但这不是一个好主意。让我告诉你为什么:

> 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 尚未解决的问题(至少据我所知)

不过幸运的是,我们可以解决这个问题,并且仍然使用我们在标签集合上创建的索引。方法如下:

> 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”、“trufala”。由于“bicep”和“chest”属于同一个文档,因此只返回1个对应的结果。一般来说,count() 和 find() 都会进行有限的扫描,并且非常有效。此外,您永远不必为用户提供过时的数据。您也可以完全避免运行任何类型的批处理作业!!!

因此,通过使用这种方法,我们可以得出以下结论:如果您搜索 n 个标签,并且每个标签出现 m 次,则扫描的文档总数将是 n * m。现在考虑到您有大量的标签和大量的文档,并且您扫描了几个标签(这些标签又对应于少数文档 - 尽管不是 1:1),结果总是非常快,因为发生了 1 个文档扫描每个标签和文档组合。

注意:这种方法永远无法覆盖索引,因为数组上有一个索引,即“isMultiKey”:true。您可以在此处阅读有关涵盖索引的更多信息

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

> 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 条记录 $or

最终结论:如果您对未排序的结果表示满意,或者愿意在前端付出额外的努力来自己对它们进行排序,请使用这种方法来获得非常有效的结果。

于 2013-09-08T06:05:11.980 回答
0

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

  1. 人们可以输入n个标签进行搜索。
  2. 标签是预定义的或用户定义的。
  3. 标签会随着时间的推移而显着增长。
  4. 我们想要给定标签集的总文档数。如果#tag1 有 10 个,#tag2 有 13 个,那么我们大约有 23 个文档(考虑到有些文档可能同时有两个标签)。

对于一种方法,我有一些建议:

  1. 您已经开始这样思考,但计划将读取与写入分开。如果有人用#chest标记了一个文档,请继续并立即将其写入,但可以在所有读取时都没有该信息。(这可以追溯到每小时运行的工作。)

  2. 您可以向标记文档的用户提供即时反馈。我读过 YouTube 会这样做。您喜欢 YouTube 上的某些内容,它会立即增加数字,即使写入尚未提交。这个想法是你立即给用户一些东西——即使它并不完全准确。

  3. 在 (2) 的基础上,考虑数字只需要“足够好”。搜索#chest#routine ... 假设返回100 个结果。随意显示 98 或 99。它在球场上。换句话说,只要用户能够感觉到有多少结果,就可以不立即考虑 +1 和 -1。

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

  5. 我不确定我会创建所有可能的组合。这将很快失去控制——尤其是如果人们可以创建自己的标签。相反,只需创建一个简单的数据结构,其中包含带有计数的搜索词。["#chest", 100], ["#routine", 543], ["#triceps", 12], ...

  6. 可以指出,如果一个文档有多个标签,我们将计算它两次。这是真的,但我会主张简单,不要管它。如果您愿意在计数方面牺牲一点精度,那么您的代码将更容易维护和运行良好。

于 2013-09-06T17:58:06.710 回答
0

更新:重写此概念以避免混淆 MySQL 示例

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

可能仍然有一种方法可以准确计数并具有良好的性能。这是基于您想要标签列表(包含所有请求标签的所有文档)的交集的假设。

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

  • tags_key : 主键 - 查询中所有标签的字符串
  • tags_count :包含 tags_list 中所有标签的文档数
  • tags_list : 标签数组 - tags_key 中的所有标签(应该是标签的索引)

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

  • 识别文档中所有已更改(添加或删除)的标签
  • 删除 tags_query_cache 集合中包含已识别的已更改标签的所有文档

3)当对您的原始集合执行查询(按标签)时:

  • 通过内爆(PHP内爆函数)一个排序的、小写的、被查询的标签列表创建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 集合
  • 每次原始集合中的文档更改其标签时,不要从 tags_query_cache 中删除
  • 从 tags_query_collection 检索时,如果 last_updated 日期/时间早于您所需的时间间隔,请重新计算原始集合中包含所需标签的文档数并更新 tags_query_cache 文档。

还应该注意,tags_query_cache 不必驻留在磁盘上某个地方的 MongoDB 中。可在所有正在运行的进程之间共享的内存缓存可用于提高性能。不利的一面是,当机器出现故障时,您会丢失缓存的结果。

祝你好运。希望这比原来的更清楚。

于 2013-09-07T01:11:03.543 回答