Flow.Launcher/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs

147 lines
6.4 KiB
C#
Raw Normal View History

using System;
using System.Text.RegularExpressions;
2020-05-26 11:49:59 +00:00
using Microsoft.Search.Interop;
namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
{
public class QueryConstructor
{
2025-04-12 13:10:39 +00:00
private static readonly Regex _specialCharacterMatcher = new(@"[\@\\#\\&\_;,\%\|\!\(\)\{\}\[\]\^\~\?\\""\/\:\=\-]+", RegexOptions.Compiled);
private static readonly Regex _multiWhiteSpacesMatcher = new(@"\s+", RegexOptions.Compiled);
2025-04-12 13:10:26 +00:00
private Settings Settings { get; }
2020-05-11 20:47:45 +00:00
private const string SystemIndex = "SystemIndex";
public QueryConstructor(Settings settings)
{
2025-04-12 13:10:26 +00:00
Settings = settings;
}
2020-05-11 20:47:45 +00:00
public CSearchQueryHelper CreateBaseQuery()
{
2020-05-18 11:31:28 +00:00
var baseQuery = CreateQueryHelper();
2020-05-11 20:47:45 +00:00
// Set the number of results we want. Don't set this property if all results are needed.
2025-04-12 13:10:26 +00:00
baseQuery.QueryMaxResults = Settings.MaxResult;
2020-05-11 20:47:45 +00:00
// Set list of columns we want to display, getting the path presently
baseQuery.QuerySelectColumns = "System.FileName, System.ItemUrl, System.ItemType";
2020-05-11 20:47:45 +00:00
2020-05-18 11:31:28 +00:00
// Filter based on file name
2020-05-11 20:47:45 +00:00
baseQuery.QueryContentProperties = "System.FileName";
// Set sorting order
//baseQuery.QuerySorting = "System.ItemType DESC";
return baseQuery;
}
2025-04-12 13:10:26 +00:00
internal static CSearchQueryHelper CreateQueryHelper()
2020-05-18 11:31:28 +00:00
{
// This uses the Microsoft.Search.Interop assembly
// Throws COMException if Windows Search service is not running/disabled, this needs to be caught
2020-05-18 11:31:28 +00:00
var manager = new CSearchManager();
// SystemIndex catalog is the default catalog in Windows
var catalogManager = manager.GetCatalog(SystemIndex);
// Get the ISearchQueryHelper which will help us to translate AQS --> SQL necessary to query the indexer
var queryHelper = catalogManager.GetQueryHelper();
2020-05-18 11:31:28 +00:00
return queryHelper;
2020-05-27 21:41:14 +00:00
}
2022-11-04 04:57:47 +00:00
public static string TopLevelDirectoryConstraint(ReadOnlySpan<char> path) => $"directory='file:{path}'";
public static string RecursiveDirectoryConstraint(ReadOnlySpan<char> path) => $"scope='file:{path}'";
2020-05-27 21:41:14 +00:00
///<summary>
2020-05-18 10:13:31 +00:00
/// Search will be performed on all folders and files on the first level of a specified directory.
2020-05-27 21:41:14 +00:00
///</summary>
public string Directory(ReadOnlySpan<char> path, ReadOnlySpan<char> searchString = default, bool recursive = false)
2020-05-27 21:41:14 +00:00
{
var queryConstraint = searchString.IsWhiteSpace() ? "" : $"AND (System.FileName LIKE '{searchString}%' OR CONTAINS(System.FileName,'\"{searchString}*\"'))";
var scopeConstraint = recursive
? RecursiveDirectoryConstraint(path)
: TopLevelDirectoryConstraint(path);
2025-04-12 13:10:26 +00:00
var query = $"SELECT TOP {Settings.MaxResult} {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE {scopeConstraint} {queryConstraint} ORDER BY {OrderIdentifier}";
return query;
}
2020-05-18 10:13:31 +00:00
///<summary>
/// Search will be performed on all folders and files based on user's search keywords.
2020-05-18 10:13:31 +00:00
///</summary>
public string FilesAndFolders(ReadOnlySpan<char> userSearchString)
{
if (userSearchString.IsWhiteSpace())
userSearchString = "*";
// Remove any special characters that might cause issues with the query
var replacedSearchString = ReplaceSpecialCharacterWithTwoSideWhiteSpace(userSearchString);
2020-05-18 11:31:28 +00:00
// Generate SQL from constructed parameters, converting the userSearchString from AQS->WHERE clause
2025-04-12 12:47:43 +00:00
return $"{CreateBaseQuery().GenerateSQLFromUserQuery(replacedSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {OrderIdentifier}";
}
/// <summary>
/// If one special character have white space on one side, replace it with one white space.
/// So command will not have "[special character]+*" which will cause OLEDB exception.
/// </summary>
private static string ReplaceSpecialCharacterWithTwoSideWhiteSpace(ReadOnlySpan<char> input)
{
const string whiteSpace = " ";
var inputString = input.ToString();
// Use regex to match special characters with whitespace on one side
// and replace them with a single space
var result = _specialCharacterMatcher.Replace(inputString, match =>
{
// Check if the match has whitespace on one side
bool hasLeadingWhitespace = match.Index > 0 && char.IsWhiteSpace(inputString[match.Index - 1]);
bool hasTrailingWhitespace = match.Index + match.Length < inputString.Length && char.IsWhiteSpace(inputString[match.Index + match.Length]);
if (hasLeadingWhitespace || hasTrailingWhitespace)
{
return whiteSpace;
}
return match.Value;
});
// Remove any extra spaces that might have been introduced
return _multiWhiteSpacesMatcher.Replace(result, whiteSpace).Trim();
}
///<summary>
/// Set the required WHERE clause restriction to search for all files and folders.
///</summary>
public const string RestrictionsForAllFilesAndFoldersSearch = "scope='file:'";
/// <summary>
2025-04-12 12:47:43 +00:00
/// Order identifier: System.Search.Rank DESC
/// </summary>
2025-04-12 12:47:43 +00:00
/// <remarks>
/// <see href="https://docs.microsoft.com/en-us/windows/win32/properties/props-system-search-rank"/>
/// </remarks>
public const string OrderIdentifier = "System.Search.Rank DESC";
///<summary>
/// Search will be performed on all indexed file contents for the specified search keywords.
///</summary>
public string FileContent(ReadOnlySpan<char> userSearchString)
{
string query =
2025-04-12 13:10:26 +00:00
$"SELECT TOP {Settings.MaxResult} {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE {RestrictionsForFileContentSearch(userSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {OrderIdentifier}";
return query;
}
///<summary>
/// Set the required WHERE clause restriction to search within file content.
///</summary>
public static string RestrictionsForFileContentSearch(ReadOnlySpan<char> searchQuery) => $"FREETEXT('{searchQuery}')";
}
}