【问题标题】:How to implement tagging system similar to SO in php/mysql?如何在 php/mysql 中实现类似于 SO 的标记系统?
【发布时间】:2010-12-04 11:38:00
【问题描述】:

我正在用 PHP/MySQL 编写一个网站,我想实现一个类似于 stackoverflow 的标记引擎。我在数据库中有 3 个相关表: 1. 物品 2. 标签 3. ItemTagMap(将标签映射到项目,n:n 映射)

现在,在搜索页面上,我想显示整个搜索结果(不仅仅是当前页面)的所有标签的不同列表,以便用户可以通过在该标签列表中添加/删除标签来“优化”他们的搜索。

问题是,这是对数据库的一个非常繁重的查询,并且可能有大量的搜索请求会导致不同的结果集,从而产生不同的标签集。

有人知道如何有效地实现这一点吗?

【问题讨论】:

  • 冒着增加需求的风险,同时显示每个标签的 COUNT,对应于特定搜索不是很好吗?
  • 是的,我会这样做 - 我什至将每个标签的统计信息存储在单独的表中。

标签: php mysql tagging


【解决方案1】:

在我们进入过早优化模式之前,查看以下查询模板可能会很有用。如果不出意外,这可以用作衡量可能优化效果的基准。

SELECT T.Tagid, TagInfo.TagName,  COUNT(*)
FROM Items I
JOIN Tags TagInfo ON TagInfo.TagId = T.TagId
JOIN ItemTagMap T  ON I.ItemId = T.ItemId 
--JOIN ItemTagMap T1 ON I.ItemId = T1.ItemId
WHERE I.ItemId IN
  (
      SELECT ItemId 
      FROM Items
      WHERE   -- Some typical initial search criteria
         Title LIKE 'Bug Report%'   -- Or some fulltext filter instead...
         AND  ItemDate > '02/22/2008'
         AND  Status = 'C'
  )
--AND T1.TagId = 'MySql'
GROUP BY T.TagId, TagInfo.TagName
ORDER BY COUNT(*) DESC

子查询是“驱动查询”,即与最终用户的初始条件相对应的查询。 (有关此查询的详细信息,请参阅下文,需要多次可能适合整体优化流程) Commented is the JOIN on T1 (and possibly T2, T3, when several tags are selected), and, with the WHERE clause, the associated criteria.当用户选择特定标签时需要这些,无论是作为初始搜索的一部分还是通过细化。 (将这些连接和 where 子句放在子查询中可能更有效;下面会详细介绍)

​​>

讨论... 两个不同的目的需要“驱动查询”或其变体:

  • 1 提供枚举所有关联标签所需的 ItemId 的完整列表。

  • 2 提供前 N 个 ItemId 值(N 为显示页面大小),用于在 Item 表中查找 Item 详细信息。

请注意,不需要对完整列表进行排序(或者它可能会受益于以不同的顺序排序),第二个列表需要根据用户的选择(例如按日期、降序或按标题)进行排序,按字母升序)。另请注意,如果需要任何排序顺序,查询的成本将意味着处理完整列表(SQL 本身的奇怪优化和/或一些非规范化,SQL 需要“查看”该列表中的最后一条记录,如果它们属于顶部,则按排序)。

后一个事实有利于为这两个目的使用相同的查询,相应的列表可以存储在临时表中。一般流程是快速查找前 N 项记录及其详细信息,并立即将其返回给应用程序。然后应用程序可以获取 ajax-fashion 的标签列表以进行细化。该列表将使用与上述类似的查询生成,其中子查询被“select * from temporaryTable”替换。 SQL 优化器决定对这个列表进行排序(在某些情况下)的可能性很大,让我们让它这样做,而不是再次猜测它并显式排序。

要考虑的另一点是可能将 ItemTagMap 表上的连接带入“驱动查询”,而不是如上所示。这样做可能是最好的选择,既是为了提高性能,又是因为它会为 #2 目的(显示一页项目)生成正确的列表。

上述查询/流程可能会很好地扩展,即使在相对适中的硬件上也是如此;暂时进入 1/2 百万+ 项,持续的用户搜索可能高达每秒 10 次。关键因素之一是初始搜索条件的选择性。

优化思路

  • [取决于典型的搜索案例和数据统计] 通过将某些项目的字段引入(实际上是复制)ItemTagMap 表来进行非规范化可能是有意义的。特别是短字段可能会在那里受到“欢迎”。
  • 随着数据以百万以上的方式增长,我们可以利用各种技巧来利用某些标签通常具有很强的相关性(例如:在 SO 中,PHP 经常附带 MySql,顺便说一句,通常没有充分的理由......)。例如,“多标签”TagIds 的引入可能会使输入逻辑更加复杂,但也可以显着减小 Map 的大小。


-- '不用说了! --
应根据实际需求和有效的数据统计概况选择合适的架构和优化...

【讨论】:

  • 这是一篇很棒的帖子,我想要你
  • 这是一篇关于数据库模式的精彩文章,其中包含一个、两个或三个表以满足不同的架构/性能需求:pui.ch/phred/archives/2005/04/tags-database-schemas.html
  • @ATSiem:感谢您的链接。引用的文章是相关的,尽管可能有点幼稚:我怀疑建议的三种解决方案中的任何一种都可以很好地扩展。另请注意,虽然标题更通用,但这个特定的 SO 问题是关于 pui.ch 文章中建议的查询的 "reverse" 的。这里的目标是一个查询/结构,用于有效地生成在用户搜索的结果列表中[至少一次]引用的标签的[按命中计数排序]列表(基于各种标准,包括可选的标签值的一些条件)。
【解决方案2】:

您会希望尽量减少 DB 调用的数量,将繁重的工作放到 PHP 中。

首先,从数据库中选择所有项目:

select * from items where (conditions);

然后,从结果集中创建一个包含所有 id 的数组。

$ids = array();
foreach ($items as $item) {
    $ids[] = $item['id'];
}
$ids = implode(',' $ids);

然后选择您之前检索到的项目 ID 的所有 ItemTagMap 和相关标签数据。

select map.item_id, t.id, t.name from tags t, item_tag_maps map where t.id = map.tag_id and map.item_id in ($ids);

现在,当您遍历 $items 数组时,您可以从您执行的第二个 SQL 查询中找到所有匹配的标签,只要它具有匹配的 item_id 值。

【讨论】:

  • 跟随不是更有效率吗? select * from items where (conditions); select t.name from tags t inner join item_tag_maps map on t.id = map.tag_id inner join items on map.item_id = item_id WHERE {same condition goes here...} ?
  • 另外,即使我打算只显示一页数据,您的方法也会从数据库中检索整个项目表。使用我的方法,我可以将 LIMIT () 添加到第一个选择以带来最少的数据
  • 不,迈克尔,你错了。请注意我传递给每个 SELECT 语句的条件。第二个 SELECT 只检索在第一个 SELECT 语句中检索到的匹配 item_id 的标签,第一个 SELECT 语句应该匹配第一页的条件。
【解决方案3】:

假设:

  • 项目(ID);
  • 在名称上带有索引的标签(id、名称);
  • ItemTag (item_id, tag_id)。

然后:

SELECT t.name
FROM Tag t
WHERE EXISTS (SELECT 1 FROM ItemTag WHERE item_id = 1234)
ORDER BY t.name

没有什么深入的。这很相似,但我猜它会更慢:

SELECT t.name
FROM Tag t
WHERE t.id IN (SELECT tag_id FROM ItemTag WHERE item_id = 1234)
ORDER BY t.name

这也可以作为连接来完成:

SELECT DISTINCT t.name
FROM Tag t
JOIN ItemTag i WHERE i.tag_id = t.id
WHERE i.item_id = 1234
ORDER BY t.name

我认为第一个会更快,但与 SQL 一样,它值得测试(在足够大的数据集上)。

上面已经完成列出单个项目的标签。您需要一组用于搜索结果的复合标签。从上面看这并不难,但这取决于您如何获得搜索结果。

【讨论】:

  • 我不确定这是否能回答 OP。底层搜索(来自站点用户)将产生许多 item_id 值。我怀疑您是否建议应该单独搜索每个这些 id...
  • @mvj 把它带到那个级别是一个简单的修改。要将标签与多个项目进行比较,请执行...WHERE item_id IN (...)...。此外,要按标签缩小结果范围,只需添加到子句 ...WHERE item_id IN (...) AND tag_id IN (...)...
  • @cletus Ok 在 item_id IN (...) 部分。然而,基于 Tad_Id 的缩小范围需要多次加入 ItemTag 表。正确的 ?   (不相关) cmets中的背景颜色怎么做?很酷。
猜你喜欢
  • 1970-01-01
  • 2015-08-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-04
  • 1970-01-01
  • 2012-02-28
相关资源
最近更新 更多