如何在php / mysql中实现类似于SO的标记系统?

时间:2009-10-07 01:37:06

标签: php mysql tagging

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

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

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

有谁知道如何有效地实现这一目标?

3 个答案:

答案 0 :(得分:8)

在我们进入过早优化模式之前,查看以下查询模板可能很有用。如果没有别的可以用作可以衡量可能优化效果的基线。

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

子查询是“驱动查询”,即对应于最终用户初始标准的查询。 (有关此查询如何多次可能适合整体优化流程的详细信息,请参阅下文) 注释是T1上的JOIN(可能是T2,T3,当选择了几个标签时),以及与WHERE子句相关的标准。当用户选择特定标签时,无论是作为初始搜索的一部分还是通过细化,都需要这些。 (放置这些连接以及子查询中的where子句可能更有效;更多关于以下内容)

<强>讨论... 出于两个不同的目的,需要“驱动查询”或其变体:

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

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

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

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

要考虑的另一点是,可以将ItemTagMap表中的连接带到“驱动查询”中,而不是如上所示。最好这样做,既可以提高性能,也可以为#2目的生成正确的列表(显示一个项目页面)。

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

优化提示

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


- ''nough说! -
应根据实际要求和有效数据统计资料选择适当的架构和优化......

答案 1 :(得分:0)

假设:

  • 项目(id);
  • 标记(id,name),名称为索引;
  • 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的情况总是如此,值得测试(在足够大的数据集上)。

上面已经完成了列出单个项目的标签。您需要一组用于搜索结果的标签。从上述情况来看并不困难,但这取决于您获得搜索结果的方式。

答案 2 :(得分:0)

您需要尝试最小化数据库调用次数,将繁重的工作放入PHP中。

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

select * from items where (conditions);

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

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

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

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数组时,只要具有匹配的item_id值,就可以从执行的第二个SQL查询中找到所有匹配的标记。