TagStudio 搜索语法完全指南:从布尔查询语法到底层查询语言实现
2026/9/16 22:16:39 网站建设 项目流程

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()比较)。这与文档声明的“大小写不敏感”一致:andANDAnD等价。

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)。转义方式有两种:

  1. 引号包裹搜索片段(最常用);
  2. 将标签名中的空格替换为下划线

合法的转义标签搜索:

  • "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_filepath: 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 = True

Glob 语法

可选地,你可以使用 glob 通配语法搜索文件路径。触发条件是查询以*开头或结尾(见上面的glob判断)。

path 搜索的四种 SQL 分支

四种模式组合产生四个实现分支(visitors.py L83-L96):

模式ilikeglob生成的 SQL
全小写 + 通配符lower(path) GLOB '...'(大小写不敏感通配)
全小写,无通配符path ILIKE '%...%'(不敏感子串)
含大写 + 通配符path GLOB '...'(敏感通配)
含大写,无通配符path regexp_match(escape(...))(敏感正则匹配)

示例

给定文件Artwork/Piece.jpg,以下搜索命中:

  • path: artwork/piece.jpg
  • path: Artwork/Piece.jpg
  • path: piece.jpg
  • path: Piece.jpg
  • path: artwork
  • path: rtwor
  • path: ece.jpg
  • path: iec
  • path: 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:((约束后缺值)
  • (asdasd](括号不配对)
  • :(孤立冒号——分词器在词首遇到:即报错,见 tokenizer.py L106-L108)
  • tag: :(约束后缺值)

解析规则本身是一条递归下降文法(parser.py):or_list := and_list (OR and_list)*and_list := term (term)*(隐式 AND),term := NOT term | "(" or_list ")" | constraintconstraint := [关键字:] 字面量 [ [属性(, 属性)*] ]。理解这条文法后,任何查询的优先级与合法性都可以直接推演。

小结

TagStudio 的搜索体系可以用三层来概括:

  1. 语法层:布尔运算符(隐式 AND 优先级最高)、括号分组、引号转义、下划线等价替换,规则简单且大小写不敏感;
  2. 关键字层tag/tag_id/path/filetype/mediatype/special六类约束,标签搜索默认匹配名称、缩写、别名并自动包含子标签;
  3. 实现层:字符串 → 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询