Поиск предметов в отношениях «многие ко многим» - PullRequest
7 голосов
/ 16 января 2012

В настоящее время я пишу приложение, которое позволяет хранить изображения, а затем помечать эти изображения. Я использую Python и ORM Peewee (http://charlesleifer.com/docs/peewee/),, который очень похож на ORM Джанго.

Моя модель данных выглядит так (упрощенно):

class Image(BaseModel):
    key = CharField()

class Tag(BaseModel):
    tag = CharField()

class TagRelationship(BaseModel):
    relImage = ForeignKeyField(Image)
    relTag   = ForeignKeyField(Tag)

Теперь я концептуально понимаю, как запрашивать все изображения с заданным набором тегов:

SELECT Image.key
  FROM Image
INNER JOIN TagRelationship
    ON Image.ID = TagRelationship.ImageID
INNER JOIN Tag
    ON TagRelationship.TagID = Tag.ID
 WHERE Tag.tag
       IN ( 'A' , 'B' )     -- list of multiple tags
GROUP BY Image.key
HAVING COUNT(*) = 2         -- where 2 == the number of tags specified, above

Однако я также хочу иметь возможность выполнять более сложные поиски. В частности, я хотел бы иметь возможность указать список «всех тегов» - то есть изображение должно иметь все указанные теги, которые должны быть возвращены, наряду со списком «any» и списком «none».

РЕДАКТИРОВАТЬ: Я хотел бы уточнить это немного. В частности, приведенный выше запрос является запросом в стиле «все теги». Возвращает изображения, которые имеют все заданные теги. Я хочу иметь возможность указать что-то вроде: «Дайте мне все изображения, которые имеют теги (зеленый, гора), любой из тегов (фон, пейзаж), но не теги (цифровой, рисунок)».

Теперь, в идеале, я бы хотел, чтобы это был один SQL-запрос, потому что тогда разбиение на страницы становится очень простым с LIMIT и OFFSET. На самом деле у меня работает реализация, в которой я просто загружаю все в наборы Python и затем использую различные операторы пересечения. Что мне интересно, так это если есть способ сделать все это сразу?

Кроме того, для тех, кто заинтересован, я написал автору Peewee о том, как представить вышеуказанный запрос, используя Peewee, и он ответил следующим решением:

Image.select(['key']).group_by('key').join(TagRelationship).join(Tag).where(tag__in=['tag1', 'tag2']).having('count(*) = 2')

Или, альтернативно, более короткая версия:

Image.filter(tagrelationship_set__relTag__tag__in=['tag1', 'tag2']).group_by(Image).having('count(*) = 2')

Заранее спасибо за ваше время.

Ответы [ 3 ]

5 голосов
/ 16 января 2012
SELECT Image.key
  FROM Image
  JOIN TagRelationship
    ON Image.ID = TagRelationship.ImageID
  JOIN Tag
    ON TagRelationship.TagID = Tag.ID
 GROUP BY Image.key
HAVING SUM(Tag.tag IN (<i>mandatory tags</i> )) = <i>N</i>  /*the number of mandatory tags*/
   AND SUM(Tag.tag IN (<i>optional tags</i>  )) > 0
   AND SUM(Tag.tag IN (<i>prohibited tags</i>)) = 0

UPDATE

Более общепринятая версия вышеуказанного запроса (преобразует логические результаты предикатов IN в целые числа с использованием выражений CASE):

SELECT Image.key
  FROM Image
  JOIN TagRelationship
    ON Image.ID = TagRelationship.ImageID
  JOIN Tag
    ON TagRelationship.TagID = Tag.ID
 GROUP BY Image.key
HAVING SUM(CASE WHEN Tag.tag IN (<i>mandatory tags</i> ) THEN 1 ELSE 0 END) = <i>N</i>  /*the number of mandatory tags*/
   AND SUM(CASE WHEN Tag.tag IN (<i>optional tags</i>  ) THEN 1 ELSE 0 END) > 0
   AND SUM(CASE WHEN Tag.tag IN (<i>prohibited tags</i>) THEN 1 ELSE 0 END) = 0

или с СЧЕТАМИ вместо СУММ:

SELECT Image.key
  FROM Image
  JOIN TagRelationship
    ON Image.ID = TagRelationship.ImageID
  JOIN Tag
    ON TagRelationship.TagID = Tag.ID
 GROUP BY Image.key
HAVING COUNT(CASE WHEN Tag.tag IN (<i>mandatory tags</i> ) THEN 1 END) = <i>N</i>  /*the number of mandatory tags*/
   AND COUNT(CASE WHEN Tag.tag IN (<i>optional tags</i>  ) THEN 1 END) > 0
   AND COUNT(CASE WHEN Tag.tag IN (<i>prohibited tags</i>) THEN 1 END) = 0
2 голосов
/ 16 января 2012

Верхняя половина получает слова, которые соответствуют обязательным тегам. Нижняя половина делает теги, где должен присутствовать хотя бы 1. Нижний запрос не имеет GROUP BY, потому что я хочу знать, появляется ли изображение дважды. Если это так, у него есть как фон, так и ландшафт. Счетчик ORDER BY (*) создаст изображения с ОБОИМИ фоновыми и альбомными тегами, которые появятся вверху. Так что зеленый, горный, фоновый пейзаж будет самым актуальным. Затем зеленые, горные, фоновые ИЛИ пейзажные картинки.

SELECT Image.key, count(*) AS 'relevance' 
FROM
     (SELECT Image.key
      FROM
        --good image candidates
        (SELECT Image.key
         FROM Image
         WHERE Image.key NOT IN 
            --Bad Images
            (SELECT DISTINCT(Image.key)   --Will reduce size of set, remove duplicates
             FROM Image
             INNER JOIN TagRelationship
                ON Image.ID = TagRelationship.ImageID
             INNER JOIN Tag
                ON TagRelationship.TagID = Tag.ID
              WHERE Tag.tag
                   IN ('digital', 'drawing' )))
    INNER JOIN TagRelationship
        ON Image.ID = TagRelationship.ImageID
    INNER JOIN Tag
        ON TagRelationship.TagID = Tag.ID
    WHERE Tag.tag
           IN ('green', 'mountain')
    GROUP BY Image.key
    HAVING COUNT(*) = count('green', 'mountain')
    --we need green AND mountain

    UNION ALL

    --Get all images with one of the following 2 tags
    SELECT * 
    FROM
        (SELECT Image.key
         FROM Image
         INNER JOIN TagRelationship
             ON Image.ID = TagRelationship.ImageID
         INNER JOIN Tag
             ON TagRelationship.TagID = Tag.ID
          WHERE Tag.tag
             IN ( 'background' , 'landscape' ))
)
GROUP BY Image.key
ORDER BY relevance DESC
0 голосов
/ 16 января 2012

Следующий запрос должен возвращать все изображения, которые отмечены как ('A' и 'B') и ('C' ИЛИ ​​'D'), но не 'E' и 'F'

SELECT Image.key
FROM Image
INNER JOIN TagRelationship
    ON Image.ID = TagRelationship.ImageID
INNER JOIN Tag tag1
    ON TagRelationship.TagID = tag1.ID
INNER JOIN Tag tag2
    ON TagRelationship.TagID = tag2.ID
WHERE tag1.tag
    IN ( 'A' , 'B' )
AND tag2.tag NOT IN ('E', 'F')

GROUP BY Image.key
HAVING COUNT(*) = 2 

UNION

SELECT Image.key
FROM Image
INNER JOIN TagRelationship
    ON Image.ID = TagRelationship.ImageID
INNER JOIN Tag tag1
    ON TagRelationship.TagID = tag1.ID
INNER JOIN Tag tag2
    ON TagRelationship.TagID = tag2.ID
WHERE tag1.tag
   IN ( 'C' , 'D' )
AND tag2.tag NOT IN ('E', 'F')
...