Tagsistant: семантическая файловая система

Привет. На хабре уже был материал, посвященный Tagsistant, но мне он показался сбивчивым и неполным. Эта попытка подать его по-другому является краткой выжимкой из англоязычного мануала и собственных наблюдений.

Проект Tagsistant позиционирует свое творение, tagfs, как следование за общей тенденцией. Интернет шаг за шагом пытаются переводить на семантические рельсы, а файловые системы, считают авторы проекта, закоснели в устарелых принципах — иерархия, директории, вот это всё.
И в принципе, я с ними в чем-то согласен. Представьте, что у вас есть несколько сотен фотографий, одни из которых сделаны в Кёльне, другие сделаны на закате, на третьих изображены девушки, а четвертые сделаны в 2010 году. Теперь вообразите, что вы хотите выполнить следующую операцию: получить список фотографий, которые сделаны на закате в Кёльне с вашей подругой, исключая те, которые были сделаны в 2010 году.
Да, возможно, скажет кто-то, можно ведь создать директории, например, Koeln, sunset, girls, 2010, потом рассовать в них софтлинки на файлы… Как-то так, но разве это предоставит необходимую гибкость и удобство в составлении запросов (хотя бы в решении приведенного выше примера)?
Да, можно попытаться воспользоваться EXIF-тегами. Но камера не указывает в них присутствия девушек на фото и других критериев, ограниченных вашей фантазией. А если речь вообще не о фотографиях, а об отчетах?
Можно попытаться писать своеобразные теги в атрибуты файлов, используя ext4, при помощи setattr\getattr — по крайней мере, я видел такое предложение в вопросе тегирования файлов, не пробовал. Но это тоже половинчатое решение, даже если будет работать.
Реальный пример для затравки, который я могу придумать, исходя из моих потребностей. У меня есть папка с огромным количеством разного картиночного хлама, когда-либо сохраненного в Downloads и позже протегированного (на самом деле, не одна). Я хочу получить из всего этого мусорного полигона список фотографий форумчан-девушек, которые сделаны в Киеве, содержат изображения пива и сделаны раньше 2012 года. Вместе с ними я хочу получить изображения всех админов форума, которые у меня есть:

$ ls ~/tagsistant/store/forum/girls/beer/=Kyiv/time:/year/lt/2012/+/admin/@/


Рассмотрим, что предлагает tagfs.


Самое первое — тегирование файлов, директорий, устройств и даже pipe-ов (!). Второе — отношения между тегами, которые могут быть include, exclude, equivalent и requires. Файлы хранятся в технической директории /archive, теги — в /tags, соответствия файлов тегам — в директории /store. Всего директорий 6:
alias/ archive/ relations/ stats/ store/ tags/

Тегов у файла может быть сколько угодно (в разумных пределах). Синтаксис приписывания тегов файлу выглядит так:

$ ln -s ~/foto1.jpg ~/tagsistant/store/koeln/wife/sunset/@


Мы приписали фотографии набор из трёх не зависящих друг от друга тега: «Кёльн», «жена» и «закат». Теперь эта фотография попадет в выборку по любому из этих тегов, и в любой комбинации из них.

Почему ln -s и зачем «собачка» в конце? Первое — а почему нет? Зачем копировать целый файл, занимая больше места и времени на его копирование, если файл как таковой уже существует, а нам нужно только создать соответствие между ним и тегами?
Второе — символ @ служит маркером, обозначающим окончание ряда тегов. Tagsistant в пути указывает на точку монтирования tagfs, директория store используется для непосредственно связывания файлов с тегами и обращения к ним. Все дальнейшее является рядом тегов, приписываемых файлу. Теперь представьте, что мы добавили еще десять файлов разными наборами тегов: одни содержат только /wife/sunset, другие только /koeln/wife и т.д. Теперь можно делать различные выборки:

$ ls tagsistant/store/koeln/@/
результат: все фотографии, сделанные в Кёльне
$ ls tagsistant/store/koeln/wife/@/
результат: все фотографии жены, сделанные в Кёльне
$ ls tagsistant/store/koeln/wife/-/sunset/@/
результат: то же самое, исключая «закатные» фотографии

Зачем все-таки маркер @? А вот зачем:

fbi (консольный просмотрщик, открывает фото) tagsistant/store/koeln/sunset/@/ (указываем набор тегов и завершаем его) foto2.jpg (указываем конкретный файл из набора фотографий, соответствующего заданным тегам)


Как бы иначе сервис файловой системы разобрался, где тег, а где уже имя файла?..

Операторы


Более сложные выборки можно делать при помощи операторов +, - и фигурных скобок. Примеры:

$ ls ~/tagsistant/store/koeln/+/sunset/@/
Результат: фотографии из Кёльна и фотографии заката; не суперпозиция этих тегов (фотографии, сделанные в Кёльне на закате), а слияние двух различных выборок (фото Кёльна в любое время дня и фото заката, сделанные в абсолютно любом месте).


$ ls ~/tagsistant/store/koeln/-/sunset/-/wife/@/
Аналогично, все фотографии Кёльна, за исключением сделанных на закате. И жену тоже в сторону, нам нужны только фото с рыбалки. :)


(Точно так же работает и оператор слияния выборок +/. Каждый оператор относится только к одному последующему тегу, так что для слияния выборок по трем тегам понадобятся два оператора.)
Для группировки тегов служат фигурные скобки; представьте, что вам нужно составить выборку из трех наборов файлов. Первый набор протегирован одновременно как «starwars» и «image», второй — как «starwars» и «music», третий — как «starwars» и «video». С использованием операторов слияния это можно выразить так:

$ ls ~/tagsistant/store/starwars/image/+/starwars/music/+/starwars/video/@/


Но лучше так:

$ ls ~/tagsistant/store/starwars/{/image/music/video/}/@/

Более сложные варианты использования предполагают и такой пример запроса:

$ ls ~/myfiles/store/{/starwars/startrek/}/{/images/music/video/}/@/


Который выдаст нам выборку всех картинок, музыки и видео, относящихся к двум разных фильмам. Эквивалентный запрос, составленный без группировок, выглядел бы так:

$ ls ~/tagsistant/store/starwars/image/+/starwars/music/+/starwars/video/+/startrek/image/+/startrek/music/+/startrek/video/@/


Сгруппированные теги не могут содержать других группировок (The only thing you can't do with tag groups is nest them). Также обязательно соблюдать парность скобок и не забывать их закрывать.

Перечисление тегов файла и мета-тег ALL


Еще одна, естественно ожидаемая, способность — это вывод списка всех тегов, ассоциированных с файлом. Для его получения нам нужно обратиться командой cat к файлу с суффиксом ".tags". Вот так:

$ cat tagsistant/store/koeln/@/photo1.jpg.tags
koeln
wife
sunset
image


Это в том случае, если мы помним хоть один тег, относящийся к файлу. А если нет, напрочь память отшибло? Нас выручает глобальный мета-тег ALL/. Результат — такой же.

$ cat tagsistant/store/ALL/@/photo1.jpg.tags

Мета-тег ALL выдает абсолютно полный список файлов, содержащихся в tagfs, и может быть полезен, например, для автоматической обработки всех файлов, поскольку рекурсивная обработка папки store не сработает, как в обычных иерархических системах. Либо в случае, как выше — вы помните, что тегировали определенный файл, но не помните ни одного из его тегов. Чтобы посмотреть их список, вы и используете общий мета-тег.

Тройные (композитные) теги


Пожалуй, пора закончить с плоскими тегами и переходить к пространствам имен и тройным тегам (triple tags). Несмотря на то, что и прежние примеры показывали неплохую гибкость использования, у них есть определенные ограничения. Не буду изобретать собственный велосипед и возьму пример из мануала: допустим, я хочу ввести теги с разграничением по годам. Как я могу это реализовать? Создать теги вроде 2000, year_2000 и т.д. по тегу для каждого года. Это захламит директорию тегов и кроме того, могут возникнуть коллизии в названиях тегов.
Второй уровень развития тегов в tagfs, преследуя структуризацию и удобство использования, выражается в композиции тегов из трех элементов, выглядящей как:

Пространство имен — Ключ — Значение


Пространство имен описывает семантическую принадлежность содержащихся пар ключ-значение и может быть достаточно общим, например, для приведенного примера с годами можно применить пространство имен time. Ключи будут выглядеть как year, ну а конкретные цифры будут содержаться в третьем элементе значение.
В реальном использовании в композитном теге есть еще один элемент: оператор. Из списка операторов становится ясной их роль: eq (equals to), inc (includes), gt (greater than), lt (less than). Таким образом полностью схема композитного тега выглядит так:

namespace:/key/operator/value/


Обратите внимание на двоеточие после пространства имен, оно обязательно и служит как раз для определения этого пространства.

Так мы можем расклассицифировать фотографии еще и по годам, месяцам и т.д., не захламляя директорию тегов. Приписывание всем фотографиям из директории с фотографиями из Кёльна тегов, характеризующих их как сделанные в Кёльне в августе 2010 года будет выглядеть так:

$ ln -s ~/Koeln_fotos/*.jpg ~/tagsistant/store/photos/koeln/time:/year/eq/2010/time:/month/eq/August/@/


Когда-нибудь после мы сможем поискать все фотографии, сделанные в 2010 году, и увидеть среди них фотографии из Кёльна.

$ ls ~/tagsistant/store/photos/time:/year/eq/2010/@/


В системе заложены также основы автоматического тегирования исходя из метаданных файла, но потестировать их пока толком не удалось за отсутствием набора файлов с нормальными метаданными (в большинстве случаев это фотографии). В мануале сказано, что можно настраивать регулярные выражения, влияющие на то, какая информация будет добыта из метаданных, редактируя настроечный ini-файл. Было бы удобно, если бы система также добавляла автоматические теги, исходя из расширения файла, например, всем jpeg-png-gif выдавала по тегу image, mp3-flac — music и т.д. Пока неясно, заложен ли такой функционал в проект или нет, возможно, можно написать свой плагин с такой функцией.

Что касается отношений


Их всего четыре: include, exclude, is_equivalent и requires. Стандартный мануал не дает подробного разъяснения по каждому из них. Дается только один пример:

$ mkdir ~/tagsistant/relations/TAG1/includes/TAG2/


После создания такого отношения любой запрос к TAG1 будет выдавать список файлов как с тегом TAG1, так и с TAG2. Пример реального использования — тег images содержит photos. Допустим, во время поездки в Лондон в 2014 году мы сделали несколько фотографий и попутно скачали из интернета какое-то количество картинок. Некоторые из них комиксы с башорга, а некоторые — обои для рабочего стола. В определенный момент мы захотели просмотреть фотографии из Лондона (/London/photos/) за тот период вместе с обоями (/images/), но не тратить время на комиксы (/comics/). Тогда запрос будет выглядеть как-то так:

$ ls ~/tagsistant/store/London/images/time:/year/eq/2014/-/comics/@/

Exclude работает стабильно и очевидно. Множество А включает (include) множество В, множество В исключает С. Теперь, если у нас будет три файла: fileA (тег А), fileB (тег В) и fileC (теги В и С), то запрос к тегу А выдаст fileA и fileB, а fileC будет исключен из поиска. fileC можно будет получить только при прямом обращении к тегу С.
Аналогично действовал бы запрос /store/A/+/B/-/C/@/. Отношения позволяют установить долговременные связи и сократить запросы.

Из источников, отличных от стандартного мануала, становится ясно, что отношение is_equivalent имеет самый очевидный и простой функционал: делает один тег эквивалентным другому в глазах блока рассуждений. Нашлись такие примеры: beatles становился эквивалентным the_beatles, а второй пример на фоне рассуждений о том, что кому-то могут не нравиться теги с использованием нижней черты, вроде my_home, делал my_home эквивалентным my\ home. Зачем — непонятно. (Это всего лишь мое мнение.)

Самое очевидное, что делает отношение requires, это упрятывание одного тега внутрь другого в иерархии файловой системы. То есть, к примеру, если выполнить:

$ mkdir ~/tagsistant/relations/TAG1/requires/TAG2/
$ ln -s ~/somefile.txt ~/tagsistant/store/TAG1/@/


То в дальнейшем мы сможем обращаться к somefile.txt по тегу TAG1, но в списке тегов в директории store мы TAG1 не увидим — он будет запрятан внутрь TAG2/.

$ ls ~/tagsistant/store/
+/ -/ @/ @@/ ALL/ TAG2 {/
$ ls ~/tagsistant/store/TAG2/
+/ -/ @/ @@/ ALL/ TAG1 {/
$ ls ~/tagsistant/store/TAG1/@/
somefile.txt #обращение идет через нужный тег, хотя на этом иерархическом уровне его нет. Впрочем, иерархия тут не очень при делах...
$ cat ~/tagsistant/store/ALL/@/somefile.txt.tags
TAG1 #то есть файл тегирован лишь одним тегом


В случае же, если отношения requires между этими тегами не будет, то TAG1 будет содержаться на верхнем уровне, в директории store/. Пока глубинный онтологический смысл этого отношения до меня не дошел. В скудных описаниях оно толком не расписано.
UPD: в ChangeLog проекта все же нашел упоминание о новом отношении под названием required. Дословно там сказано следующее (в переводе с англ.):

Введено отношение «необходим». Если тег M необходим тегу S, то тег S будет показан лишь в том случае, когда тег M содержится в в последней позиции запроса, например:

store/M/
store/P/Q/+/M/


Но он не будет показан в:

store/P/
store/P/+/Q/


Предназначение этого отношения — организация тегов в иерархическую структуру для предотвращения захламления корневой директории. В какой-то мере оно дополняет функционал пространства имен.


Честно говоря, ясности не внесло. По крайней мере, у меня описанного поведения не наблюдается. Возможно, я просто что-то не понимаю.

Дедупликация и другие палки в колеса


Дедупликация — механизм, предотвращающий использование одинаковыми файлами двух разных inode-ов в файловой системе. Это значит, что приколы с созданием пустого временного файла в качестве флага здесь бы не сработали, но это и не надо — это же вспомогательная специализированная система.
Выглядит это примерно так:

$ touch ~/tagsistant/store/tag1/@/tempfile1
$ touch ~/tagsistant/store/tag2/@/tempfile2
$ touch ~/tagsistant/store/tag2/@/tempfile3


Результатом этих манипуляций будет один лишь tempfile1 с приписанными ему тегами tag1 и tag2. Попытки создания остальных двух файлов натолкнутся на проверку содержания, окажется, что она у них с первым одинаковая (они все одинаково пусты) и теги, приписываемые последней паре, окажутся приписаны первому файлу с прежним именем.

Отключение reasoner'а (блока раздумий)


Завершение ряда тегов в запросе символом @ «включает» вышеупомянутый блок, заставляя его совершать всю логику запроса, использование же двух символов: @@ «выключает» его. Полезно в некоторых случаях, среди которых отдельные операции с файлами и просмотр ассоциированного с тегом множества без участия отношений. Например, если тег А содержит тег В, то по запросу к тегу А система выдаст оба множества. Если же мы отключим ризонер при аналогичном запросе, то получим только множество А:

$ ls ~/tagsistant/store/A/@/
Afile1 Afile2 Bfile1
$ ls ~/tagsistant/store/A/@@/
Afile1 Afile2

Алиасы


Привычную цепочку тегов можно законспирировать в краткий алиас, обозначающийся знаком =. Сопоставленная с алиасом цепочка-запрос будет подставлен as is, так что в результате могут случиться некоторые подвохи. Алиасы хранятся в директории aliases в виде файлов, которые содержат строку ассоциированного запроса. Допустим, файл алиаса с именем behemoth содержит строку behemoth/file:/format/eq/AVI/. В дальнейшем мы подставляем его в более общий запрос:

$ ls ~/tagsistant/store/=behemoth/time:/year/lt/2000/@/


Упомянутый подвох может заключаться в том, что если алиас содержит оператор +/, то вся часть запроса, которая следует после алиаса, будет относится только ко второй его части. Кстати, тоже не совсем понятно, ведь в мануале было сказано, что оператор относится только к одному следующему за ним тегу; возможно, информацию не успели обновить после очередного нововведения.

Слияние тегов


Тоже важная часть, не мог ее не упомянуть. Чтобы слить два тега в один, достаточно просто перенести все содержание директории одного в директорию другого. И удалить первый из них.

$ mv store/merged_tag/@/* store/destination_tag/@/
$ rmdir tags/merged_tag


Не менее важное примечание. Ни в коем случае нельзя удалять не пустую папку в директории /store. Каждая директория каждого тега содержит ссылки на все остальные теги (их директории), так что, удалив папку одного тега, вы снесете весь репозиторий. Все удаления в папке /store могут быть только при завершенном запросе. Запрос становится завершенным, когда содержит маркеры мыслителя ризонера: @ или @@. В таком случае будут удалены только файлы, которые будут результатом обращения к перечисленным в запросе тегам.
Чтобы удалить тег, нужно обращаться к директории /tags, а не /store. Все файлы, которым приписан этот тег, останутся в целости, но лишатся соответствующего тега.

Сборка


Tagsistant предназначен для сборки под Linux или BSD, требует библиотеки glib2, fuse, libdbi с плагинами libdbd-sqlite3, libdbd-mysql и libextractor. У меня не десктопный дистрибутив, поэтому я собирал вручную половину зависимостей. При этом Tagsistant собрался только с заголовками sqlite3 (на самом деле, как видно, ему достаточно либо-либо), но выдает какие-то мусорные сообщения. Возможно, как раз потому, что я собрал его без mysql-овских заголовков — после запуска и при работе мусорит в терминал сообщения типа «no tables in statement !». Достаточно перенаправить стандартный вывод в астрал 1>/dev/null, чтобы это прекратилось — на работу это видимым образом не влияет никак.

Конечно, кто-то может высказаться в духе: «зачем этот велосипед, если можно организовать иерархию папок». Я считаю, что никакая иерархия папок не даст такой гибкости и удобства, позволяя задавать совершенно любые запросы, какие только придут в голову. Кроме того, с моей точки зрения велосипедна как раз подобная возня с иерархией, ссылками и тому подобным. EXIF-теги, которые случайно могли кому-то придти в голову из-за примеров с картинками, вряд ли пригодны для тегирования архивов переписок и всего того, что может тегировать Tagsistant. Системе есть куда развиваться, но она уже удобная и стабильная. Обратите на нее внимание.

© Geektimes