Solved Keeping a tab on the status of Windows 11 (25H2) indexing


jacobacci

Well-known member
Member
Local time
5:20 PM
Posts
64
OS
Windows 11 25H2
I am trying to build a small dashboard (with the help of Claude AI, as I am not a PowerShell programmer) to display
  • Number of files indexed
  • Number of files pending to be indexed
  • Number of files processed per minute
  • Estimated time to completion
  • File currently being indexed (so I can see which file indexer is choking on)
So far this endeavor has been hit and miss, so I am looking to the experts on this forum to maybe help me along.
With 25H2, Microsoft seems to have transitioned to an SQLite database and they seem to have changed all of the counter names in Perfmon.
The script fails to retrieve the following counters:
\Search Gatherer Projects(1~SystemIndex)\Waiting Documents
\Search Gatherer Projects(1~SystemIndex)\Processed Documents and
\Search Gatherer Projects(1~SystemIndex)\Processed Documents Rate

Can someone possibly shed some light on the valid names of these counters in 25H2?
Thanks a million for any help
 
Windows Build/Version
Win 11 25H2 26200.7840
Last edited:

My Computer My Computer

At a glance

Windows 11 25H2i7-12700FCORSAIR Vengeance LPX - DDR4 - kit - 32 GB: 2...NVIDIA GeForce GTX 1050 Ti
OS
Windows 11 25H2
Computer type
PC/Desktop
Manufacturer/Model
MSI
CPU
i7-12700F
Motherboard
MSI PRO B660-A DDR4
Memory
CORSAIR Vengeance LPX - DDR4 - kit - 32 GB: 2 x 16 GB
Graphics Card(s)
NVIDIA GeForce GTX 1050 Ti
Sound Card
onboard
Monitor(s) Displays
ASUS PA32UCX
Screen Resolution
3840x2160
Hard Drives
ADATA SP900
AVAGO MR9361-8i SCSI with 3x Exos X20
PSU
Corsair RM650X
Case
Fractal Design Define R5 Blackout Edition
Cooling
Noctua CPU and Case Fans
Keyboard
Unicomp Model M
Mouse
Logitech MX Master
Internet Speed
1Gb/s
Browser
MS Edge
Antivirus
Microsoft Defender
Hi again to all
In the meantime I have used Claude AI to put together a PS script which seems to work quite well to monitor progress of Windows Indexing and to see whether it is progressing normally.

Code:
#Requires -RunAsAdministrator
<#
.SYNOPSIS
    Windows 11 25H2 Search Indexing Progress Monitor
.PARAMETER RefreshSeconds
    Poll interval in seconds. Default: 30.
.EXAMPLE
    .\Watch-SearchIndexing.ps1
    .\Watch-SearchIndexing.ps1 -RefreshSeconds 30
#>
param([int]$RefreshSeconds = 30)

$HandleExe = "C:\Program Files (x86)\Sysinternals\Handle\handle64.exe"
$BaseDir   = "$env:ProgramData\Microsoft\Search\Data\Applications\Windows"

$Files = @{
    DB        = Join-Path $BaseDir "Windows.db"
    GatherDB  = Join-Path $BaseDir "Windows-gather.db"
    WAL       = Join-Path $BaseDir "Windows.db-wal"
    GatherWAL = Join-Path $BaseDir "Windows-gather.db-wal"
}

# Always display in KB so small changes are visible
function Format-KB {
    param([long]$Bytes)
    return "{0:N0} KB" -f ($Bytes / 1KB)
}

function Format-Rate {
    param([double]$BytesPerSec)
    if ($BytesPerSec -le 0) { return "0 KB/min" }
    if ($BytesPerSec -ge 1MB) { return "{0:N2} MB/min" -f ($BytesPerSec * 60 / 1MB) }
    return "{0:N0} KB/min" -f ($BytesPerSec * 60 / 1KB)
}

function Format-Age {
    param([double]$Seconds)
    if ($Seconds -lt 60)   { return "$([int]$Seconds)s ago" }
    if ($Seconds -lt 3600) { return "$([int]($Seconds/60))m $([int]($Seconds%60))s ago" }
    return "$([int]($Seconds/3600))h $([int](($Seconds%3600)/60))m ago"
}

function Get-FileInfo {
    param([string]$Path)
    try {
        $f = Get-Item $Path -ErrorAction Stop
        return [PSCustomObject]@{ Size=$f.Length; LastWrite=$f.LastWriteTime }
    } catch {
        return [PSCustomObject]@{ Size=0L; LastWrite=$null }
    }
}

# Query handle64 across all search-related processes
# PST indexing runs in searchprotocolhost.exe, not searchindexer.exe
$SearchProcesses = @("searchindexer", "searchprotocolhost", "searchfilterhost")

function Get-CurrentIndexFile {
    if (-not (Test-Path $HandleExe)) { return $null }
    try {
        # Single pipeline — avoids $Matches persistence issues in PowerShell 7
        $pst = & $HandleExe -accepteula -p searchprotocolhost 2>$null | ForEach-Object {
            if ($_ -match 'File\s+\([RW\-]+\)\s+([A-Za-z]:\\[^\r\n]+)') { $Matches[1].Trim() }
        } | Where-Object {
            $_ -notlike "*\Windows\*"            -and
            $_ -notlike "*\Microsoft\Search\*"  -and
            $_ -notlike "*\Microsoft\Windows\*" -and
            $_ -notlike "*.dll"                    -and
            $_ -notlike "*.mui"                    -and
            $_ -notlike "*.db"                     -and
            $_ -notlike "*.db-wal"                 -and
            $_ -notlike "*.db-shm"
        } | Where-Object { $_ -like "*.pst" -or $_ -like "*.ost" } | Select-Object -Last 1

        return $pst
    } catch { return $null }
}

# ── State tracking ────────────────────────────────────────────────────────────
# Activity is detected by ANY of:
#   - WAL size change (growing or shrinking/checkpointing)
#   - WAL timestamp change (in-place writes)
#   - DB timestamp change (in-place page updates)
$samples          = [System.Collections.Generic.List[PSCustomObject]]::new()
$lastActivityTime = Get-Date
$lastWalSize      = -1L
$lastWalWrite     = $null
$lastDbWrite      = $null
$lastGatherWrite  = $null
$lastFile         = $null
$lastFileTime     = $null
$stallThreshold   = 120   # seconds
$iteration        = 0
$walCheckpoints      = 0
$lastCheckpointTime  = $null
$checkpointIntervals = [System.Collections.Generic.List[double]]::new()
$dbSizeAtCheckpoint  = 0L
$dbSizeAtStart       = 0L
$monitorStartTime    = Get-Date

# Accept handle EULA silently
if (Test-Path $HandleExe) { & $HandleExe -accepteula 2>$null | Out-Null }

Write-Host "Monitoring search index... (Ctrl+C to stop)" -ForegroundColor White
Start-Sleep -Seconds 2

while ($true) {
    $iteration++
    $now = Get-Date
    if ($iteration -eq 1) { $dbSizeAtStart = (Get-FileInfo $Files.DB).Size; $monitorStartTime = $now }

    # ── Read file info ────────────────────────────────────────────────────────
    $dbInfo        = Get-FileInfo $Files.DB
    $gatherInfo    = Get-FileInfo $Files.GatherDB
    $walInfo       = Get-FileInfo $Files.WAL
    $gatherWalInfo = Get-FileInfo $Files.GatherWAL

    $dbSize        = $dbInfo.Size
    $gatherSize    = $gatherInfo.Size
    $walSize       = $walInfo.Size
    $gatherWalSize = $gatherWalInfo.Size
    $totalWal      = $walSize + $gatherWalSize

    # Detect WAL checkpoint — any reduction in WAL size indicates a checkpoint
    if ($lastWalSize -gt 0 -and $totalWal -lt $lastWalSize) {
        $walCheckpoints++
        if ($lastCheckpointTime) {
            $interval = ($now - $lastCheckpointTime).TotalSeconds
            $checkpointIntervals.Add($interval)
        }
        $lastCheckpointTime  = $now
        $dbSizeAtCheckpoint  = $dbSize
    }

    # ── Activity detection: use most recent file timestamp directly ────────────
    # This avoids the poll interval inflating the "last activity" display
    $activity = $false
    if ($totalWal -ne $lastWalSize)                                            { $activity = $true }
    if ($walInfo.LastWrite    -and $walInfo.LastWrite    -ne $lastWalWrite)    { $activity = $true }
    if ($dbInfo.LastWrite     -and $dbInfo.LastWrite     -ne $lastDbWrite)     { $activity = $true }
    if ($gatherInfo.LastWrite -and $gatherInfo.LastWrite -ne $lastGatherWrite) { $activity = $true }

    if ($activity) {
        $lastWalSize     = $totalWal
        $lastWalWrite    = $walInfo.LastWrite
        $lastDbWrite     = $dbInfo.LastWrite
        $lastGatherWrite = $gatherInfo.LastWrite
    }

    # Most recent timestamp across all watched files — independent of poll interval
    $timestamps = @($walInfo.LastWrite, $gatherWalInfo.LastWrite, $dbInfo.LastWrite, $gatherInfo.LastWrite) |
                  Where-Object { $_ -ne $null } |
                  Sort-Object -Descending
    $mostRecentWrite = if ($timestamps) { $timestamps[0] } else { $lastActivityTime }
    $stallSeconds    = ($now - $mostRecentWrite).TotalSeconds
    $isStalled       = $stallSeconds -ge $stallThreshold

    # ── Growth rate (DB size over sample window) ──────────────────────────────
    $samples.Add([PSCustomObject]@{ Time=$now; DbSize=$dbSize; WalSize=$totalWal })
    while ($samples.Count -gt 12) { $samples.RemoveAt(0) }

    $dbRate = 0.0
    if ($samples.Count -ge 2) {
        $oldest  = $samples[0]
        $newest  = $samples[$samples.Count - 1]
        $elapsed = ($newest.Time - $oldest.Time).TotalSeconds
        if ($elapsed -gt 0) {
            $dbRate = ($newest.DbSize - $oldest.DbSize) / $elapsed
        }
    }

    # ── Current file ──────────────────────────────────────────────────────────
    $currentFile = Get-CurrentIndexFile
    if ($currentFile -and $currentFile -ne $lastFile) {
        $lastFile     = $currentFile
        $lastFileTime = $now
    }
    $fileAge = if ($lastFileTime) { ($now - $lastFileTime).TotalSeconds } else { $null }

    # ── State ─────────────────────────────────────────────────────────────────
    if ($isStalled) {
        $state      = "*** INDEXING STALLED ***"
        $stateColor = "Red"
    } elseif ($activity -or $totalWal -gt 100KB) {
        $state      = "Crawling"
        $stateColor = "Yellow"
    } else {
        $state      = "Idle"
        $stateColor = "Green"
    }

    # ── Render ────────────────────────────────────────────────────────────────
    Clear-Host
    Write-Host ("=" * 64) -ForegroundColor Cyan
    Write-Host "  Windows Search Index Progress" -ForegroundColor Cyan
    Write-Host "  $($now.ToString('HH:mm:ss'))   poll #$iteration  (every ${RefreshSeconds}s)" -ForegroundColor White
    Write-Host ("=" * 64) -ForegroundColor Cyan
    Write-Host ""

    Write-Host ("  {0,-26}" -f "State:") -ForegroundColor White -NoNewline
    Write-Host $state -ForegroundColor $stateColor

    if ($isStalled) {
        $stallStr = "$([int]($stallSeconds/60))m $([int]($stallSeconds%60))s"
        Write-Host ("  {0,-26}{1}" -f "Stalled for:", $stallStr) -ForegroundColor Red
    } else {
        Write-Host ("  {0,-26}{1}" -f "Last file write:", "$([int]$stallSeconds)s before last poll") -ForegroundColor White
    }

    Write-Host ""

    # Current / last file
    if ($currentFile) {
        Write-Host ("  {0,-26}{1}" -f "Current file:", $currentFile) -ForegroundColor White
    } elseif ($lastFile) {
        $col = if ($isStalled) { "Red" } else { "DarkGray" }
        Write-Host ("  {0,-26}{1}" -f "Last file seen:", $lastFile) -ForegroundColor $col
        if ($fileAge) {
            Write-Host ("  {0,-26}{1}" -f "File last changed:", (Format-Age $fileAge)) -ForegroundColor White
        }
    } else {
        Write-Host ("  {0,-26}{1}" -f "Current file:", "(none visible yet)") -ForegroundColor White
    }

    Write-Host ""
    Write-Host "  -- Database files --" -ForegroundColor DarkCyan
    $dbTs      = if ($dbInfo.LastWrite)     { $dbInfo.LastWrite.ToString('HH:mm:ss') }     else { "n/a" }
    $gatherTs  = if ($gatherInfo.LastWrite) { $gatherInfo.LastWrite.ToString('HH:mm:ss') } else { "n/a" }
    Write-Host ("  {0,-26}{1,-16} modified {2}" -f "Windows.db:",        (Format-KB $dbSize),     $dbTs) -ForegroundColor White
    Write-Host ("  {0,-26}{1,-16} modified {2}" -f "Windows-gather.db:", (Format-KB $gatherSize), $gatherTs) -ForegroundColor White

    Write-Host ""
    Write-Host "  -- WAL files --" -ForegroundColor DarkCyan
    $walTs      = if ($walInfo.LastWrite)        { $walInfo.LastWrite.ToString('HH:mm:ss') }        else { "n/a" }
    $gatherWalTs= if ($gatherWalInfo.LastWrite)  { $gatherWalInfo.LastWrite.ToString('HH:mm:ss') }  else { "n/a" }
    Write-Host ("  {0,-26}{1,-16} modified {2}" -f "Windows.db-wal:",        (Format-KB $walSize),       $walTs) -ForegroundColor White
    Write-Host ("  {0,-26}{1,-16} modified {2}" -f "Windows-gather.db-wal:", (Format-KB $gatherWalSize), $gatherWalTs) -ForegroundColor White
    $cpTime = if ($lastCheckpointTime) { $lastCheckpointTime.ToString("HH:mm:ss") } else { "none yet" }
    $avgInterval = if ($checkpointIntervals.Count -gt 0) {
        $avgSec = [int](($checkpointIntervals | Measure-Object -Sum).Sum / $checkpointIntervals.Count)
        if ($avgSec -ge 60) { "$([int]($avgSec/60))m $($avgSec % 60)s" } else { "${avgSec}s" }
    } else { "n/a" }
    Write-Host ("  {0,-43}{1}" -f "Last WAL checkpoint seen:", $cpTime) -ForegroundColor White
    Write-Host ("  {0,-43}{1}" -f "Number of WAL checkpoints detected:", $walCheckpoints) -ForegroundColor White
    Write-Host ("  {0,-43}{1}" -f "Avg interval between checkpoints:", $avgInterval) -ForegroundColor White

    Write-Host ""
    Write-Host "  -- Growth rate --" -ForegroundColor DarkCyan
    # Growth in KB since last poll (absolute, not per-minute rate)
    $ratePoll = if ($samples.Count -ge 2) {
        $prev    = $samples[$samples.Count - 2]
        $deltaKB = [math]::Round(($dbSize - $prev.DbSize) / 1KB, 0)
        if ($deltaKB -gt 0) { "+{0:N0} KB" -f $deltaKB } elseif ($deltaKB -eq 0) { "+0 KB" } else { "{0:N0} KB" -f $deltaKB }
    } else { "+0 KB" }
    # Avg rate since monitor start
    $elapsedTotal = ($now - $monitorStartTime).TotalSeconds
    $rateAvg = if ($elapsedTotal -gt 0 -and $dbSizeAtStart -gt 0) {
        Format-Rate (($dbSize - $dbSizeAtStart) / $elapsedTotal)
    } else { "0 KB/min" }
    $growthSinceStart = if ($dbSizeAtStart -gt 0) {
        $deltaKB = [math]::Round(($dbSize - $dbSizeAtStart) / 1KB, 0)
        if ($deltaKB -gt 0) { "+{0:N0} KB" -f $deltaKB } elseif ($deltaKB -eq 0) { "+0 KB" } else { "{0:N0} KB" -f $deltaKB }
    } else { "+0 KB" }
    Write-Host ("  {0,-43}{1}" -f "DB growth since last poll:", $ratePoll) -ForegroundColor White
    Write-Host ("  {0,-43}{1}" -f "DB growth since monitor start:", $growthSinceStart) -ForegroundColor White
    Write-Host ("  {0,-43}{1}" -f "Avg DB growth rate since monitor start:", $rateAvg) -ForegroundColor White

    Write-Host ""
    Write-Host ("=" * 64) -ForegroundColor Cyan

    $lastWalSize = $totalWal

    Start-Sleep -Seconds $RefreshSeconds
}

Maybe this is useful to someone
Best Regards
Rudi
 

My Computer My Computer

At a glance

Windows 11 25H2i7-12700FCORSAIR Vengeance LPX - DDR4 - kit - 32 GB: 2...NVIDIA GeForce GTX 1050 Ti
OS
Windows 11 25H2
Computer type
PC/Desktop
Manufacturer/Model
MSI
CPU
i7-12700F
Motherboard
MSI PRO B660-A DDR4
Memory
CORSAIR Vengeance LPX - DDR4 - kit - 32 GB: 2 x 16 GB
Graphics Card(s)
NVIDIA GeForce GTX 1050 Ti
Sound Card
onboard
Monitor(s) Displays
ASUS PA32UCX
Screen Resolution
3840x2160
Hard Drives
ADATA SP900
AVAGO MR9361-8i SCSI with 3x Exos X20
PSU
Corsair RM650X
Case
Fractal Design Define R5 Blackout Edition
Cooling
Noctua CPU and Case Fans
Keyboard
Unicomp Model M
Mouse
Logitech MX Master
Internet Speed
1Gb/s
Browser
MS Edge
Antivirus
Microsoft Defender

Similar threads

Latest Support Threads

Back
Top Bottom