TagStudio 搜索语法完全指南:从布尔查询语法到底层查询语言实现
【免费下载链接】TagStudioA User-Focused Photo & File Management System项目地址: https://gitcode.com/GitHub_Trending/ta/TagStudio
本文以 TagStudio 的搜索文档(docs/search.md)为主体,系统讲解其搜索语法的全部能力——布尔运算符(AND/OR/NOT)、分组嵌套、字符转义、标签搜索、path:文件路径搜索(含 smartcase 与 glob)、special:特殊搜索,并结合query_lang查询语言模块与 SQL 构建源码,剖析一条搜索语句如何被分词、解析为 AST 并转换为 SQLite 布尔表达式,帮助你在大型媒体库中精准检索,并理解每一项语法背后的实现机制。
搜索能力总览:搜索什么?
TagStudio 提供的搜索覆盖两大类数据:
- TagStudio 自身数据:标签(tags)、标签 ID 等;
- 文件固有数据:文件路径(
path:)、文件类型(filetype:)、媒体类型(mediatype:)等。
从源码结构看,所有搜索能力都汇聚到一条统一管道:用户输入的字符串经过 Tokenizer 分词、Parser 构建 AST,再由 SQLBoolExpressionBuilder 访问器翻译成 SQL 布尔条件,最终交给 Library.search_library 执行:
# src/tagstudio/core/library/alchemy/enums.py L92-L99 @property def ast(self) -> AST | None: if self.query is None: return None return Parser(self.query).parse()BrowsingState(浏览状态数据类)持有查询字符串,其ast属性每次按需调用Parser(self.query).parse()重新解析。UI 层的网格视图通过BrowsingState.from_search_query(...)等工厂方法构造状态,如 from_tag_id 会生成tag_id:123查询,from_path则生成path:"..."查询——这正是右键菜单“Search for Tag”等快捷入口背后的机制。
布尔运算符
TagStudio 支持标准布尔搜索运算符,并支持分组、嵌套与字符转义。注意:要得到理想结果时可能需要显式使用分组括号,因为运算符的优先级(NOT>AND>OR)由解析器硬性规定。
从源码看,运算符就是普通的未加引号字面量,由解析器做大小写不敏感的文本匹配(见 parser.py 中的__is_next_or/__is_next_and/__is_next_not,均用.upper()比较)。这与文档声明的“大小写不敏感”一致:and、AND、AnD等价。
AND
AND运算符仅返回同时满足两侧条件的结果。未显式书写布尔运算符时,解析器会隐式插入 AND——这一点由 Parser.__and_list 实现:只要下一个 token 是字面量或左括号(且不是OR),就调用__skip_and()(显式and时才消费它)并继续把元素并入同一ANDList。显式用法只需在搜索项之间输入and。
示例:搜索
"Tag1 Tag2"与"Tag1 AND Tag2"完全等价,只返回同时包含 Tag1 和 Tag2 的结果。
OR
OR返回匹配左侧或右侧任一侧的结果。同样只需在搜索项之间输入or。
示例:
"Tag1 OR Tag2"返回包含 "Tag1"、"Tag2" 或两者的结果。
NOT
NOT返回右侧条件为假的结果。可以在搜索项之间输入not,也可以在搜索开头使用NOT,仅查看不包含后一项的结果。
示例:
"Tag1 NOT Tag2"只返回包含 "Tag1" 但不包含 "Tag2" 的结果。
源码层面NOT由 Parser.__term 处理,有一个值得注意的优化:Not(Not(x))会直接化简为x(双重否定消除)。测试用例 tests/core/library/test_search.py 中有对应验证:not not path:*返回全部 32 条,not unexistant返回 32 条(排除不存在的标签等于不排除任何内容)。
分组与嵌套
使用括号()对查询片段分组并嵌套。
示例:
"(Tag1 OR Tag2) AND Tag3"返回包含 Tag3,且包含 Tag1 和 Tag2 中至少一个(或两者)的结果。
解析器在 __term 中遇到左括号时递归调用__or_list()并消费匹配的右括号,因此支持任意深度嵌套——测试中(((tag_id:1041)))这类三重括号查询同样通过。
转义字符
有些查询词存在歧义,需要转义。最常见的是含空格的标签名,或名称恰好与搜索关键字冲突(如[path:](https://link.gitcode.com/i/7b1bb3d29f3e8202ca837e529084ff02#file-name-and-path)of exile)。转义方式有两种:
- 用引号包裹搜索片段(最常用);
- 将标签名中的空格替换为下划线。
合法的转义标签搜索:
"Tag Name With Spaces"Tag_Name_With_Spaces
非法的转义标签搜索:
Tag Name With Spaces- 原因:产生歧义——无法区分是一个名为 "Tag Name With Spaces" 的标签,还是 "Tag"、"Name"、"With"、"Spaces" 四个独立标签。
分词器对引号字符串有专门的解析路径 Tokenizer.__quoted_string:引号内的内容(包括空格、冒号)不会被拆分,且支持反斜杠\转义\\、"、'三种字符;引号未闭合时会抛出Unterminated quoted string解析错误。而未加引号的字面量一旦遇到:、(空格)、[、]、(、)、=、,中任一字符即终止(见 tokenizer.py 的 NOT_IN_ULITERAL)。
标签搜索
标签搜索是 TagStudio 的默认搜索模式。无需关键字前缀,直接使用tag:前缀同样有效。标签匹配会尝试命中标签的名称、缩写、别名,并且允许子标签替代其任意父标签。
tag_id:前缀关键字会在你对标签右键选择 "Search for Tag" 时出现。它面向内部用途,未来版本将不再显示或暴露给用户。
源码印证:SQLBoolExpressionBuilder.visit_constraint 中ConstraintType.Tag分支调用__get_tag_ids(),后者在 visitors.py L123-L144 中通过三路并集查询匹配标签 ID:
select(Tag.id) .where(or_(Tag.name.ilike(tag_name), Tag.shorthand.ilike(tag_name))) .union(select(TagAlias.tag_id).where(TagAlias.name.ilike(tag_name)))即标签名、缩写、别名的模糊匹配合并去重。随后默认include_children=True,对每个匹配标签再展开 TAG_CHILDREN_ID_QUERY 取全部后代标签 ID——这正是“子标签可替代父标签”(搜索父标签时子标签命中的文件也能返回)的实现基础。若同名标签存在多个匹配,解析结果按“任一命中”处理并记录歧义日志(visitors.py L133-L138)。
AND 列表的标签还会经过专门的优化:__separate_tags 会把纯标签约束收集起来,用一条关系除法子查询(__entry_has_all_tags:GROUP BY entry_id HAVING COUNT(DISTINCT tag_id) = N)一次性表达“同时拥有全部 N 个标签”,而非生成 N 个嵌套子句。
字段搜索(尚未实现)
字段搜索当前尚未纳入程序,将在未来版本提供。这有源码佐证:visit_constraint中只要约束带有[key=value]属性列表就抛出NotImplementedError("Properties are not implemented yet")(visitors.py L66-L67)——解析器本身已能解析tag:foo[bar=baz]形式的属性语法(Parser.__property),只是执行端还未落地。
文件条目搜索:文件名与路径
path:关键字提供文件名与路径搜索,具有几种不同风格。默认情况下,path:后的任意字符串都作为子串在文件完整路径中搜索。也就是说,对于文件folder/my_file.txt,搜索path: my_file或path: folder都能命中。
大小写敏感性(Smartcase)
TagStudio 采用类似 smartcase 的大小写策略:
- 全
lowercase输入 →大小写不敏感; - 任意
MixedCase输入 →大小写敏感。
这让你在不需要区分大小写时打字更快,需要时也能一键切换。注意其边界:目前没有方式在“区分大小写”模式下搜索全小写的目标词(因为全小写永远走不敏感路径)。
源码中该逻辑一目了然(visitors.py L73-L81):
# Smartcase check if node.value == node.value.lower(): ilike = True if node.value.startswith("*") or node.value.endswith("*"): glob = TrueGlob 语法
可选地,你可以使用 glob 通配语法搜索文件路径。触发条件是查询以*开头或结尾(见上面的glob判断)。
path 搜索的四种 SQL 分支
四种模式组合产生四个实现分支(visitors.py L83-L96):
| 模式 | ilike | glob | 生成的 SQL |
|---|---|---|---|
| 全小写 + 通配符 | 是 | 是 | lower(path) GLOB '...'(大小写不敏感通配) |
| 全小写,无通配符 | 是 | 否 | path ILIKE '%...%'(不敏感子串) |
| 含大写 + 通配符 | 否 | 是 | path GLOB '...'(敏感通配) |
| 含大写,无通配符 | 否 | 否 | path regexp_match(escape(...))(敏感正则匹配) |
示例
给定文件Artwork/Piece.jpg,以下搜索会命中:
path: artwork/piece.jpgpath: Artwork/Piece.jpgpath: piece.jpgpath: Piece.jpgpath: artworkpath: rtworpath: ece.jpgpath: iecpath: artwork/*path: Artwork/*path: *piece.jpg*path: *Piece.jpg*path: *artwork*path: *Artwork*path: *rtwor*path: *ece.jpg*path: *iec*path: *.jpg
以下搜索不会命中:
path: ARTWORK/Piece.jpg(原因:大小写不匹配)path: *aRtWoRk/Piece*(原因:大小写不匹配)path: PieCe.jpg(原因:大小写不匹配)path: *PieCe.jpg*(原因:大小写不匹配)
这些示例与测试用例精确对应,例如 test_search.py 中path:*inherit*命中 24 条、path:*comp*命中 5 条,path:*命中全部 32 条。
特殊搜索
special:关键字前缀提供若干预定义快捷查询。
Untagged
查看所有不含任何标签的文件条目,使用special: untagged。
实现是取“没有任何 TagEntry 关联”的条目(visitors.py L108-L110):
return ~Entry.id.in_(select(Entry.id).join(TagEntry))Empty
注意:v9.5.0 中暂不可用。
查看既不含任何标签也不含任何字段的文件条目,使用special: empty。源码中special:分支目前只处理了untagged,未识别的值会落到NotImplementedError("This type of constraint is not implemented yet")——与文档标注一致。
完整关键字速查表
从 ConstraintType 枚举可知,解析器识别的全部关键字为:
| 关键字 | 用途 | 状态 |
|---|---|---|
tag: | 按名称/缩写/别名搜索标签(可省略前缀) | 可用 |
tag_id: | 按内部 ID 搜索标签(右键 "Search for Tag" 使用) | 可用,将逐步对用户隐藏 |
path: | 路径/文件名搜索(子串 + smartcase + glob) | 可用 |
filetype: | 按扩展名(含等价扩展名集合)搜索 | 可用 |
mediatype: | 按媒体类别(image/video 等)搜索 | 可用 |
special: | 特殊查询,目前仅untagged | 部分可用 |
filetype:与mediatype:在源码中的实现:filetype:支持等价扩展名集合 FILETYPE_EQUIVALENTS(如 jpg 与其等价扩展名互认),mediatype:则把类别映射为扩展名集合后做IN匹配(visitors.py L97-L107)。测试中filetype:png命中 25 条、filetype:jpg命中 6 条,且引号写法filetype:'jpg'结果相同。
语法错误的边界情况
test_syntax 列出了会触发ParsingError的非法输入,可作为快速排错参照:
asd AND(悬挂的 AND)asd AND AND(连续 AND)tag:((约束后缺值)(asd、asd](括号不配对):(孤立冒号——分词器在词首遇到:即报错,见 tokenizer.py L106-L108)tag: :(约束后缺值)
解析规则本身是一条递归下降文法(parser.py):or_list := and_list (OR and_list)*,and_list := term (term)*(隐式 AND),term := NOT term | "(" or_list ")" | constraint,constraint := [关键字:] 字面量 [ [属性(, 属性)*] ]。理解这条文法后,任何查询的优先级与合法性都可以直接推演。
小结
TagStudio 的搜索体系可以用三层来概括:
- 语法层:布尔运算符(隐式 AND 优先级最高)、括号分组、引号转义、下划线等价替换,规则简单且大小写不敏感;
- 关键字层:
tag/tag_id/path/filetype/mediatype/special六类约束,标签搜索默认匹配名称、缩写、别名并自动包含子标签; - 实现层:字符串 → Token → AST(
ANDList/ORList/Constraint/Not)→ SQLite 布尔表达式,其中 smartcase、glob 通配与标签关系除法子查询是最具工程细节的三处优化。
掌握这套语法与 tests/core/library/test_search.py 中的真实用例,即可在任意规模的库中组合出精确的检索式。
【免费下载链接】TagStudioA User-Focused Photo & File Management System项目地址: https://gitcode.com/GitHub_Trending/ta/TagStudio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考