From b231e088d435afda46214ae952ab09d01b7fa97e Mon Sep 17 00:00:00 2001 From: Saravanan Ganapathi Date: Fri, 21 Nov 2025 16:57:06 +0530 Subject: [PATCH 1/2] Fix: Fixed the issue of tag search NOT operator not working --- .../Utils/Storage/Search/FolderSearch.cs | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/Files.App/Utils/Storage/Search/FolderSearch.cs b/src/Files.App/Utils/Storage/Search/FolderSearch.cs index 29042ca0f959..714dbf456846 100644 --- a/src/Files.App/Utils/Storage/Search/FolderSearch.cs +++ b/src/Files.App/Utils/Storage/Search/FolderSearch.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using System.IO; +using System.Text.RegularExpressions; using Windows.Storage; using Windows.Storage.FileProperties; using Windows.Storage.Search; @@ -183,19 +184,47 @@ private async Task AddItemsForLibraryAsync(LibraryLocationItem library, IList
  • includeTags, HashSet excludeTags) ParseTagQuery(string query) + { + var includeTags = new HashSet(); + var excludeTags = new HashSet(); + + var matches = Regex.Matches(query, @"(NOT\s+)?tag:([^\s]+)", RegexOptions.IgnoreCase); + + foreach (Match match in matches) + { + var isExclude = !string.IsNullOrEmpty(match.Groups[1].Value); + var tagValues = match.Groups[2].Value.Split(',', StringSplitOptions.RemoveEmptyEntries); + + foreach (var tagName in tagValues) + { + var tagUids = fileTagsSettingsService.GetTagsByName(tagName).Select(t => t.Uid); + foreach (var uid in tagUids) + { + if (isExclude) + excludeTags.Add(uid); + else + includeTags.Add(uid); + } + } + } + + return (includeTags, excludeTags); + } + private async Task SearchTagsAsync(string folder, IList results, CancellationToken token) { //var sampler = new IntervalSampler(500); - var tags = AQSQuery.Substring("tag:".Length)?.Split(',').Where(t => !string.IsNullOrWhiteSpace(t)) - .SelectMany(t => fileTagsSettingsService.GetTagsByName(t), (_, t) => t.Uid).ToHashSet(); - if (tags?.Any() != true) + var (includeTags, excludeTags) = ParseTagQuery(AQSQuery); + + if (includeTags.Count == 0) { return; } var dbInstance = FileTagsHelper.GetDbInstance(); var matches = dbInstance.GetAllUnderPath(folder) - .Where(x => tags.All(x.Tags.Contains)); + .Where(x => includeTags.All(x.Tags.Contains) && !excludeTags.Any(x.Tags.Contains)); if (string.IsNullOrEmpty(folder)) matches = matches.Where(x => !StorageTrashBinService.IsUnderTrashBin(x.FilePath)); From afbdb8f7d0b1db684ca0931650498c4167b72827 Mon Sep 17 00:00:00 2001 From: Saravanan Ganapathi Date: Wed, 26 Nov 2025 10:32:07 +0530 Subject: [PATCH 2/2] Fix: Enhanced tag search with AND/OR operators and improved NOT handling --- .../Utils/Storage/Search/FolderSearch.cs | 92 +++++++++++++++---- .../Storage/Search/TagQueryExpression.cs | 10 ++ src/Files.App/Utils/Storage/Search/TagTerm.cs | 12 +++ 3 files changed, 94 insertions(+), 20 deletions(-) create mode 100644 src/Files.App/Utils/Storage/Search/TagQueryExpression.cs create mode 100644 src/Files.App/Utils/Storage/Search/TagTerm.cs diff --git a/src/Files.App/Utils/Storage/Search/FolderSearch.cs b/src/Files.App/Utils/Storage/Search/FolderSearch.cs index 714dbf456846..9d8fcb21f497 100644 --- a/src/Files.App/Utils/Storage/Search/FolderSearch.cs +++ b/src/Files.App/Utils/Storage/Search/FolderSearch.cs @@ -95,7 +95,7 @@ public Task SearchAsync(IList results, CancellationToken token) private async Task AddItemsForHomeAsync(IList results, CancellationToken token) { - if (AQSQuery.StartsWith("tag:", StringComparison.Ordinal)) + if (IsTagQuery(AQSQuery)) { await SearchTagsAsync("", results, token); // Search tags everywhere, not only local drives } @@ -184,47 +184,99 @@ private async Task AddItemsForLibraryAsync(LibraryLocationItem library, IList
  • includeTags, HashSet excludeTags) ParseTagQuery(string query) + private bool IsTagQuery(string query) { - var includeTags = new HashSet(); - var excludeTags = new HashSet(); + return query?.Contains("tag:", StringComparison.OrdinalIgnoreCase) == true; + } - var matches = Regex.Matches(query, @"(NOT\s+)?tag:([^\s]+)", RegexOptions.IgnoreCase); + private TagQueryExpression ParseTagQuery(string query) + { + var expression = new TagQueryExpression(); + var orParts = Regex.Split(query, @"\s+OR\s+", RegexOptions.IgnoreCase); - foreach (Match match in matches) + foreach (var orPart in orParts) { - var isExclude = !string.IsNullOrEmpty(match.Groups[1].Value); - var tagValues = match.Groups[2].Value.Split(',', StringSplitOptions.RemoveEmptyEntries); + var andGroup = new List(); + var andParts = Regex.Split(orPart, @"\s+AND\s+", RegexOptions.IgnoreCase); + + foreach (var andPart in andParts) + { + var matches = Regex.Matches(andPart.Trim(), @"(NOT\s+)?tag:([^\s]+)", RegexOptions.IgnoreCase); + foreach (Match match in matches) + { + var isExclude = !string.IsNullOrEmpty(match.Groups[1].Value); + var tagValues = match.Groups[2].Value.Split(',', StringSplitOptions.RemoveEmptyEntries); + var tagUids = new HashSet(); + + foreach (var tagName in tagValues) + { + var uids = fileTagsSettingsService.GetTagsByName(tagName).Select(t => t.Uid); + foreach (var uid in uids) + { + tagUids.Add(uid); + } + } + + andGroup.Add(new TagTerm { TagUids = tagUids, IsExclude = isExclude }); + } + } + + if (andGroup.Count > 0) + { + expression.OrGroups.Add(andGroup); + } + } + + return expression; + } - foreach (var tagName in tagValues) + private bool MatchesTagExpression(IEnumerable fileTags, TagQueryExpression expression) + { + foreach (var orGroup in expression.OrGroups) + { + bool groupMatches = true; + foreach (var term in orGroup) { - var tagUids = fileTagsSettingsService.GetTagsByName(tagName).Select(t => t.Uid); - foreach (var uid in tagUids) + if (term.IsExclude) + { + if (term.TagUids.Count > 0 && term.TagUids.Any(fileTags.Contains)) + { + groupMatches = false; + break; + } + } + else { - if (isExclude) - excludeTags.Add(uid); - else - includeTags.Add(uid); + if (term.TagUids.Count == 0 || !term.TagUids.Any(fileTags.Contains)) + { + groupMatches = false; + break; + } } } + + if (groupMatches) + { + return true; + } } - return (includeTags, excludeTags); + return false; } private async Task SearchTagsAsync(string folder, IList results, CancellationToken token) { //var sampler = new IntervalSampler(500); - var (includeTags, excludeTags) = ParseTagQuery(AQSQuery); + var expression = ParseTagQuery(AQSQuery); - if (includeTags.Count == 0) + if (expression.OrGroups.Count == 0) { return; } var dbInstance = FileTagsHelper.GetDbInstance(); var matches = dbInstance.GetAllUnderPath(folder) - .Where(x => includeTags.All(x.Tags.Contains) && !excludeTags.Any(x.Tags.Contains)); + .Where(x => MatchesTagExpression(x.Tags, expression)); if (string.IsNullOrEmpty(folder)) matches = matches.Where(x => !StorageTrashBinService.IsUnderTrashBin(x.FilePath)); @@ -287,7 +339,7 @@ private async Task SearchTagsAsync(string folder, IList results, Can private async Task AddItemsAsync(string folder, IList results, CancellationToken token) { - if (AQSQuery.StartsWith("tag:", StringComparison.Ordinal)) + if (IsTagQuery(AQSQuery)) { await SearchTagsAsync(folder, results, token); } diff --git a/src/Files.App/Utils/Storage/Search/TagQueryExpression.cs b/src/Files.App/Utils/Storage/Search/TagQueryExpression.cs new file mode 100644 index 000000000000..df34979d1743 --- /dev/null +++ b/src/Files.App/Utils/Storage/Search/TagQueryExpression.cs @@ -0,0 +1,10 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Files.App.Utils.Storage +{ + public sealed class TagQueryExpression + { + public List> OrGroups { get; set; } = new(); + } +} diff --git a/src/Files.App/Utils/Storage/Search/TagTerm.cs b/src/Files.App/Utils/Storage/Search/TagTerm.cs new file mode 100644 index 000000000000..0614dfd35013 --- /dev/null +++ b/src/Files.App/Utils/Storage/Search/TagTerm.cs @@ -0,0 +1,12 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Files.App.Utils.Storage +{ + public class TagTerm + { + public HashSet TagUids { get; set; } = new(); + + public bool IsExclude { get; set; } + } +}