diff --git a/.searchignore b/.searchignore new file mode 100644 index 0000000..8e6520d --- /dev/null +++ b/.searchignore @@ -0,0 +1,22 @@ +# Drive letters required, wildcards accepted + +# Final backslash indicates a folder +C:\*\.git\ +D:\*\.git\ +E:\*\.git\ + +C:\*\node_modules\ +D:\*\node_modules\ +E:\*\node_modules\ + +C:\*\FileHistory\ +D:\*\FileHistory\ +E:\*\FileHistory\ + +# Windows Backups can also be in the root of the drive +C:\FileHistory\ +D:\FileHistory\ +E:\FileHistory\ + +; Another valid comment line +# Empty lines are also ignored diff --git a/README.md b/README.md index 953ca06..12e1cfb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Windows-Search-Full-Index + A PowerShell script to prevent Windows from excluding folders to being indexed. ## Overview @@ -10,45 +11,39 @@ This can be problematic for developers who use the repository folder as their wo ## Workaround -Fortunately, there's a workaround. If a `.git` or `.svn` folder is **already present** in the list of excluded folders, then **the parent folder will no longer be automatically excluded**. -But manually adding each of these hidden folders to the exclusion list is tedious. -This script was created to automate the process: it recursively scans the selected volume and adds **every folder starting with a dot (`.`)** to the Windows Search exclusion list. - ---- +Fortunately, there's a workaround. If a `.git` or `.svn` folder is **already present** in the list of excluded folders, then **the parent folder will no longer be automatically excluded.** -## Usage Instructions +This script automates the process of adding these exclusion patterns to Windows Search. You can specify them using wildcards such as `D:\*\.git\` in a `.searchignore` file located in the same folder as the script. -The inclusion/exclusion list is stored in a **protected key** of the Windows Registry. -Therefore, running the script as Administrator is required — but not sufficient. You must also: +Note that setting exclusion patterns using Local Group Policy **will not fix the bug** in Windows Search. It has to be done like this script does it. -### 1. Grant Registry Permissions +--- -In `regedit`, navigate to: +## Usage -``` -Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows Search\CrawlScopeManager\Windows\SystemIndex\WorkingSetRules -``` +1. Start from a clean slate, no indexed locations. Backup your registry now. +2. Edit the provided `.searchignore` file with your wildcard patterns, one per line. + Make sure to include eg. `D:\*\.git\` to fix the bug that excludes parent repos. +3. Run the script as Administrator + ```powershell + # From an elevated prompt + .\Windows-Search-Exceptions.ps1 + ``` -Right-click → **Permissions** → Grant **Full Control** to the **Administrators** group. +This should add the rules. You may add your indexed locations now. +Git repos and your other wildcard patterns won't be excluded anymore. -### 2. Configure Volume +## How it works -Open `Windows-Search.ps1` in Notepad and edit the line: +Running from an elevated prompt the script has permission to register a scheduled task that will run it as the SYSTEM account. +This is necessary because the inclusion/exclusion list is stored in a **protected key** of the Windows Registry: -```powershell -$volumeLetter = "E:\" ``` - -Change the drive letter if necessary. - -### 3. Run the Script - -Open **PowerShell as Administrator** and execute: - -```powershell -.\Windows-Search.ps1 +Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows Search\CrawlScopeManager\Windows\SystemIndex\WorkingSetRules ``` +The task is run on-demand as SYSTEM and sets the required data under the `WorkingSetRules` key in the registry, based on the lines in `.searchignore`. + --- ## Disclaimer diff --git a/Windows-Search-Exclusions.ps1 b/Windows-Search-Exclusions.ps1 new file mode 100644 index 0000000..984a54d --- /dev/null +++ b/Windows-Search-Exclusions.ps1 @@ -0,0 +1,215 @@ +# Windows Search Indexing - Wildcard Exclusions + +# Configure Windows Search to exclude specific folders via wildcard patterns. +# For fixing a Windows bug that excludes repo parent folders instead of just .git\ + +####################################################### +# WARNING! This script will RESET EXISTING EXCLUSIONS.# +####################################################### + +# Exclusion patterns are defined in `.searchignore` file, see repo. +# Backup your registry before running this script! + +# Usage + +# 1. Start from a clean slate, no indexed locations. Backup your registry now. +# 2. Edit the provided `.searchignore` file with your wildcard patterns, one per line. +# Make sure to include eg. `D:\*\.git\` to fix the bug that excludes parent repos. +# 3. In an elevated Powershell prompt run the script ".\Windows-Search-Exceptions.ps1" +# 4. This should add the rules. You may add your indexed locations now. +# Git repos won't be excluded anymore, and your other wildcard patterns will work. + +# The script will: + +# - Stop the Windows Search service +# - Reset existing exclusions +# - Add exclusions from the `.searchignore` file +# - Restart the Windows Search service + +# Register script to be run as a scheduled task with SYSTEM permissions +# Necessary for modifying the protected Registry keys for Windows Search exclusions +if ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name -ne "NT AUTHORITY\SYSTEM") { + # Self-elevate the script if required + # Solution from https://stackoverflow.com/a/64576446 + if (-Not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) { + if ([int](Get-CimInstance -Class Win32_OperatingSystem | Select-Object -ExpandProperty BuildNumber) -ge 6000) { + $CommandLine = "-File `"" + $MyInvocation.MyCommand.Path + "`" " + $MyInvocation.UnboundArguments + Start-Process -FilePath PowerShell.exe -Verb Runas -ArgumentList $CommandLine + Exit + } + } + + $taskName = ".searchignore Script" + $taskDescription = "Sets the registry keys for Windows Search exclusions from a .searchignore file in the script folder" + + Write-Warning "Script needs SYSTEM permissions to modify protected registry keys." + Write-Warning "Registering scheduled task to run as SYSTEM..." + + # Set start time in the past so it only runs on demand + $startTime = $((Get-Date).AddHours(-4).ToString("HH:mm")) + $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`"" + $trigger = New-ScheduledTaskTrigger -Once -At $startTime + Register-ScheduledTask -TaskName $taskName -Description $taskDescription -Action $action -Trigger $trigger -User "SYSTEM" -Force | Out-Null + + Write-Host "Invoking this script as scheduled task..." + Start-ScheduledTask -TaskName $taskName + + Write-Host "Cleaning up scheduled task..." + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false + + exit +} + +# Below will only run as SYSTEM account + +function Stop-SearchService { + Stop-Service WSearch -Force + Write-Host "Search service stopped" +} + +function Start-SearchService { + Start-Service WSearch + Write-Host "Search service started" +} + +function Get-RootGuid { + $roots = Get-ChildItem -Path $searchRoots -ErrorAction SilentlyContinue + foreach ($root in $roots) { + $props = Get-ItemProperty -Path $root.PSPath -ErrorAction SilentlyContinue + if ($props.URL -match "file:///$volumeLetter\\[([0-9a-fA-F\-]{36})\]\\") { + return $matches[1] + } + } + Write-Host "Warning: using default GUID" -ForegroundColor Yellow + return $defaultGuid +} + +function Reset-Exclusions { + $subKeys = Get-ChildItem -Path $registryBase + foreach ($subKey in $subKeys) { + Remove-Item -Path $subKey.PSPath -Recurse -Force + } + Write-Host "Exclusions reset" +} + +function Edit-Exclusions { # Not used + # Get all the numbered subkeys under the base registry path. + $subKeys = Get-ChildItem -Path $registryBase -ErrorAction SilentlyContinue + + # Check if there are any subkeys to process. + if (-not $subKeys) { + Write-Host "No exclusions found to clean" + return + } + + foreach ($subKey in $subKeys) { + # Try to get the 'URL' string value from the current subkey. + # -ErrorAction SilentlyContinue prevents errors if the 'URL' value doesn't exist. + $urlValue = Get-ItemProperty -Path $subKey.PSPath -Name "URL" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty "URL" + + # The condition to check before removing the item. + # We use -notlike with a wildcard (*) to check if the URL value does NOT start with "file:///D:\". + # This condition is also true if $urlValue is null (the URL property doesn't exist). + if (-not ($urlValue -like "file:///[CD]:\*")) { + Write-Host "Removing key: $($subKey)" -ForegroundColor Red + Remove-Item -Path $subKey.PSPath -Recurse -Force + } + else { + Write-Host "Skipping $($urlValue)" -ForegroundColor Gray + } + } + Write-Host "Exclusion cleanup complete" -ForegroundColor Green +} + +function Initialize-DefaultSearch { + param ( + [String]$path, + [String]$url, + [Int]$isDefault + ) + $subKeyPath = Join-Path $registryBase $counter + New-Item -Path $subKeyPath -Force | Out-Null + New-ItemProperty -Path $subKeyPath -Name "Container" -PropertyType DWord -Value 0 -Force | Out-Null + New-ItemProperty -Path $subKeyPath -Name "Default" -PropertyType DWord -Value $isDefault -Force | Out-Null + New-ItemProperty -Path $subKeyPath -Name "Include" -PropertyType DWord -Value 1 -Force | Out-Null + New-ItemProperty -Path $subKeyPath -Name "NoContent" -PropertyType DWord -Value 0 -Force | Out-Null + New-ItemProperty -Path $subKeyPath -Name "Policy" -PropertyType DWord -Value 0 -Force | Out-Null + New-ItemProperty -Path $subKeyPath -Name "Suppress" -PropertyType DWord -Value 0 -Force | Out-Null + New-ItemProperty -Path $subKeyPath -Name "URL" -PropertyType String -Value $url -Force | Out-Null + $global:counter++ + Set-ItemProperty -Path $registryBase -Name "ItemCount" -Value $global:counter -Force | Out-Null + Write-Host "Included folder $counter : $path" -ForegroundColor Green +} + +function Set-SearchExclusions { + # This function reads exclusion patterns from a `.searchignore` file + # and creates corresponding registry entries for each pattern. + + # --- Configuration --- + # Define the path to the .searchignore file. + # $PSScriptRoot is an automatic variable that contains the full path + # of the script's parent directory. + $ignoreFilePath = Join-Path $PSScriptRoot ".searchignore" + + # --- Pre-flight Check --- + # Check if the .searchignore file actually exists before we try to read it. + if (-not (Test-Path $ignoreFilePath)) { + Write-Error "The .searchignore file was not found at $ignoreFilePath" + # Stop the function if the file is missing. + return + } + + # --- Processing --- + # Read all patterns from the .searchignore file into an array. + $patterns = Get-Content -Path $ignoreFilePath + + # Iterate over each pattern (which is one line from the file). + foreach ($pattern in $patterns) { + # Skip any empty or whitespace-only lines in the file + # Comment with # or ; at the begining of the line + if ($pattern -match '^\s*(#|;|$)') { + continue + } + + # The subKeyPath is created by joining the base registry path and the current counter. + $subKeyPath = Join-Path $registryBase $counter + + # Construct the URL as requested: "file:///" followed by the pattern from the file. + $url = "file:///$pattern" + + # Create the new registry key for the current pattern. + # Write-Host $subKeyPath $url -ForegroundColor Magenta # DEBUG + New-Item -Path $subKeyPath -Force | Out-Null + + # Set all the required registry properties for this key. + New-ItemProperty -Path $subKeyPath -Name "Container" -PropertyType DWord -Value 0 -Force | Out-Null + New-ItemProperty -Path $subKeyPath -Name "Default" -PropertyType DWord -Value 0 -Force | Out-Null + New-ItemProperty -Path $subKeyPath -Name "Include" -PropertyType DWord -Value 0 -Force | Out-Null + New-ItemProperty -Path $subKeyPath -Name "NoContent" -PropertyType DWord -Value 0 -Force | Out-Null + New-ItemProperty -Path $subKeyPath -Name "Policy" -PropertyType DWord -Value 0 -Force | Out-Null + New-ItemProperty -Path $subKeyPath -Name "Suppress" -PropertyType DWord -Value 0 -Force | Out-Null + New-ItemProperty -Path $subKeyPath -Name "URL" -PropertyType String -Value $url -Force | Out-Null + + # Increment the global counter for the next item. + $global:counter++ + + # Update the master ItemCount in the parent registry key. + Set-ItemProperty -Path $registryBase -Name "ItemCount" -Value $global:counter -Force | Out-Null + + # Provide feedback to the console that the pattern was processed. + Write-Host "Added pattern $counter : $pattern" -ForegroundColor Green + } +} + +$registryBase = "HKLM:\SOFTWARE\Microsoft\Windows Search\CrawlScopeManager\Windows\SystemIndex\WorkingSetRules" +$searchRoots = "HKLM:\SOFTWARE\Microsoft\Windows Search\CrawlScopeManager\Windows\SystemIndex\SearchRoots" +$volumeLetter = "C:\" +$defaultGuid = "98da7f83-099d-4591-afed-be354bdaf82b" +$guid = Get-RootGuid +$global:counter = 0 # Key with last number that exists in Registry, not incremented by 1 + +Stop-SearchService +Reset-Exclusions +Initialize-DefaultSearch -path "$volumeLetter" -url "file:///$volumeLetter[$guid]\" -isDefault 0 +Set-SearchExclusions +Start-SearchService diff --git a/Windows-Search.ps1 b/Windows-Search.ps1 deleted file mode 100644 index 5d4ece1..0000000 --- a/Windows-Search.ps1 +++ /dev/null @@ -1,91 +0,0 @@ -function Service-Stop { - Stop-Service WSearch -Force - Write-Host "Search service stopped" -} - -function Service-Start { - Start-Service WSearch - Write-Host "Search service started" -} - -function Get-Root-Guid { - $roots = Get-ChildItem -Path $searchRoots -ErrorAction SilentlyContinue - foreach ($root in $roots) { - $props = Get-ItemProperty -Path $root.PSPath -ErrorAction SilentlyContinue - if ($props.URL -match "file:///$volumeLetter\\[([0-9a-fA-F\-]{36})\]\\") { - return $matches[1] - } - } - Write-Host "Warning: using default GUID" -ForegroundColor Yellow - return $defaultGuid -} - -function Clean-Init { - $subKeys = Get-ChildItem -Path $registryBase - foreach ($subKey in $subKeys) { - Remove-Item -Path $subKey.PSPath -Recurse -Force - } - Write-Host "Exclusions reset" -} - -function Default-Search { - param ( - [String]$path, - [String]$url, - [Int]$isDefault - ) - $subKeyPath = Join-Path $registryBase $counter - New-Item -Path $subKeyPath -Force | Out-Null - New-ItemProperty -Path $subKeyPath -Name "Container" -PropertyType DWord -Value 0 -Force | Out-Null - New-ItemProperty -Path $subKeyPath -Name "Default" -PropertyType DWord -Value $isDefault -Force | Out-Null - New-ItemProperty -Path $subKeyPath -Name "Include" -PropertyType DWord -Value 1 -Force | Out-Null - New-ItemProperty -Path $subKeyPath -Name "NoContent" -PropertyType DWord -Value 0 -Force | Out-Null - New-ItemProperty -Path $subKeyPath -Name "Policy" -PropertyType DWord -Value 0 -Force | Out-Null - New-ItemProperty -Path $subKeyPath -Name "Suppress" -PropertyType DWord -Value 0 -Force | Out-Null - New-ItemProperty -Path $subKeyPath -Name "URL" -PropertyType String -Value $url -Force | Out-Null - $global:counter++ - Set-ItemProperty -Path $registryBase -Name "ItemCount" -Value $global:counter -Force | Out-Null - Write-Host "Included folder $counter : $path" -ForegroundColor Green -} - -function Recur-Search { - param ( - [String]$path - ) - $folders = Get-ChildItem -Path $path -Directory -Force -ErrorAction SilentlyContinue - foreach ($folder in $folders) { - if ($folder.Name.StartsWith(".")) { - $subKeyPath = Join-Path $registryBase $counter - $relativePath = $folder.FullName.Substring(3) - $url = "file:///$volumeLetter[$guid]\$relativePath\" - New-Item -Path $subKeyPath -Force | Out-Null - New-ItemProperty -Path $subKeyPath -Name "Container" -PropertyType DWord -Value 0 -Force | Out-Null - New-ItemProperty -Path $subKeyPath -Name "Default" -PropertyType DWord -Value 0 -Force | Out-Null - New-ItemProperty -Path $subKeyPath -Name "Include" -PropertyType DWord -Value 0 -Force | Out-Null - New-ItemProperty -Path $subKeyPath -Name "NoContent" -PropertyType DWord -Value 0 -Force | Out-Null - New-ItemProperty -Path $subKeyPath -Name "Policy" -PropertyType DWord -Value 0 -Force | Out-Null - New-ItemProperty -Path $subKeyPath -Name "Suppress" -PropertyType DWord -Value 0 -Force | Out-Null - New-ItemProperty -Path $subKeyPath -Name "URL" -PropertyType String -Value $url -Force | Out-Null - $global:counter++ - Set-ItemProperty -Path $registryBase -Name "ItemCount" -Value $global:counter -Force | Out-Null - Write-Host "Excluded folder $counter : $($folder.FullName)" -ForegroundColor Green - } else { - Recur-Search -path $folder.FullName - } - } -} - -$registryBase = "HKLM:\SOFTWARE\Microsoft\Windows Search\CrawlScopeManager\Windows\SystemIndex\WorkingSetRules" -$searchRoots = "HKLM:\SOFTWARE\Microsoft\Windows Search\CrawlScopeManager\Windows\SystemIndex\SearchRoots" -$volumeLetter = "E:\" -$defaultGuid = "98da7f83-099d-4591-afed-be354bdaf82b" -$guid = Get-Root-Guid -$global:counter = 0 - -Service-Stop -Clean-Init -#Default-Search -path "Microsoft Outlook" -url "csc://{S-1-5-21-2626148534-851844826-3743222949-1001}/" -isDefault 1 -#Default-Search -path "Microsoft Outlook" -url "mapi16://{S-1-5-21-2626148534-851844826-3743222949-1001}/" -isDefault 0 -Default-Search -path "$volumeLetter" -url "file:///$volumeLetter[$guid]\" -isDefault 0 -Recur-Search -path "$volumeLetter" -Service-Start