Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .searchignore
Original file line number Diff line number Diff line change
@@ -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
49 changes: 22 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Windows-Search-Full-Index

A PowerShell script to prevent Windows from excluding folders to being indexed.

## Overview
Expand All @@ -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
Expand Down
215 changes: 215 additions & 0 deletions Windows-Search-Exclusions.ps1
Original file line number Diff line number Diff line change
@@ -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
Loading