Skip to content

Instantly share code, notes, and snippets.

@arebee
Last active September 13, 2024 07:17
Show Gist options
  • Save arebee/1928da03047aee4167fabee0f501c72d to your computer and use it in GitHub Desktop.
Save arebee/1928da03047aee4167fabee0f501c72d to your computer and use it in GitHub Desktop.
Use Windows Search from PowerShell
function Search-FileIndex {
<#
.PARAMETER Path
Absoloute or relative path. Has to be in the Search Index for results to be presented.
.PARAMETER Pattern
File name or pattern to search for. Defaults to no values. Aliased to Filter to ergonomically match Get-ChildItem.
.PARAMETER Text
Free text to search for in the files defined by the pattern.
.PARAMETER Recurse
Add the parameter to perform a recursive search. Default is false.
.PARAMETER Freetext
Use the freetext comparison as opposed to the Inflection comparison.
.PARAMETER AsFSInfo
Add the parameter to return System.IO.FileSystemInfo objects instead of String objects.
.PARAMETER Timeout
An integer that specifies a timeout period in seconds. Set to 0 (disable timeout) by default. Must be less or equal to 60.
.SYNOPSIS
Uses the Windows Search index to search for files.
.DESCRIPTION
Fast, flexible command line search of file names and or content using the Windows Search index.
SQL Syntax documented at
https://learn.microsoft.com/en-us/windows/win32/search/-search-sql-windowssearch-entry
For use on Windows operating systems (Windows Vista and later only). Although this could be modified
to run on MacOS I am not going to do that tonight.
USE
===
Use by dot sourcing the file into your session, or adding to your $PROFILE.
I add
set-alias -Name sfi -Value Search-FileIndex
for convenience.
.OUTPUTS
By default one string per file found with full path.
If the AsFSInfo switch is set, one System.IO.FileSystemInfo object per file found is returned.
.EXAMPLE
Search-FileIndex . Changes
Search for exact text in current directory
This will search the current folder for the string Changes in all files indexed by Windows Search.
By default this is a case insensitive text search, and will not substring
.EXAMPLE
Search-FileIndex . "diaper changes"
Search for exact phrase in file contents.
This will search the current folder for the string diaper changes in all files indexed by Windows Search.
.EXAMPLE
Search-FileIndex -Path . -Text Changes -Pattern *.txt -Recurse
This will search the current folder and all child folders for the string Happy in all files with an extension of txt indexed by Windows Search.
.EXAMPLE
Search-FileIndex -Path . -filter *.txt -Recurse
Search file names and paths for extensions
Will find all files ending in .txt in the current path and child folders.
.EXAMPLE
Search-FileIndex -Path . -filter *ollo*.pdf -Recurse
Search file names and paths with substring
Will find all files and folders containing the string ollo in the current path and child folders names.
Will return "People to follow.pdf" but not "Domain Controller Follow Up.txt"
.NOTES
Updates
2024-07-22
- Wildcard support in text search. Only * supported at the end of a word(s).
- Bug fixes.
2024-07-20
- added default positional parameters
- added a new parameter to set timeouts for searches. Default timeout of 0 seconds (no timeout) (thanks @poundy).
- added a new default value of $path to $pwd.
- added new error conditions if junk paths and filters are passed.
- added new parameter to switch between the stemmer and free text.
#>
[CmdletBinding()]
param (
[Parameter(ValueFromPipeline = $true, Position=0)]
[string]$Path = $PWD,
[Parameter(Mandatory=$false,ParameterSetName="FullText", Position=1)]
[string]$Text = $null,
[Parameter(Mandatory=$false,ParameterSetName="FullText")]
[Parameter(Mandatory=$false)]
[alias("Filter")]
[string]$Pattern = '*.*',
[Parameter(Mandatory=$false)]
[switch]$Recurse = $false,
[Parameter(Mandatory=$false)]
[switch]$AsFSInfo = $false,
[Parameter(Mandatory=$false)]
[switch]$Freetext = $false,
[Parameter(Mandatory=$false)]
[ValidateRange(0,60)]
[int]$Timeout = 0,
[Parameter(Position=2, ValueFromRemainingArguments)]
[string[]]$Remaining
)
if($Path -eq ""){
$Path = $PWD;
Write-Verbose "Empty path set to $PWD - Shouldn't be called with new default"
}
# Badly formed paths get caught here
if ($false -eq (Test-Path -path $Path -IsValid)) {
Throw '$Path variable is invalid'
}
# Non-existent paths get caught here.
$Path = (Resolve-Path -Path $Path).Path
# Replace wildcard characters in the file filter pattern, and path.
$Pattern = $Pattern.Replace('*', '%')
$Pattern = $Pattern.Replace('?', '_')
if ($Pattern.Contains('%') -or $Pattern.Contains('_')) {
$patternContainsRegex = $true
} else {
$patternContainsRegex = $false
}
$path = $path.Replace('\','/')
write-verbose $Pattern
write-verbose $Path
write-verbose $Text
if (Get-Variable 'fsSearchCon' -Scope 'Global' -ErrorAction 'Ignore') {
if ($global:fsSearchRs.State -ne 0) {$global:fsSearchRs.Close()}
if ($global:fsSearchCon.State -eq 0) {$global:fsSearchCon.Open()}
} else {
$global:fsSearchCon = New-Object -ComObject ADODB.Connection
$global:fsSearchRs = New-Object -ComObject ADODB.Recordset
$global:fsSearchCon.ConnectionTimeout=$Timeout
$global:fsSearchCon.CommandTimeout=$Timeout
$global:fsSearchCon.Open('Provider=Search.CollatorDSO;Extended Properties=''Application=Windows'';')
}
[string]$queryString = 'SELECT System.ItemPathDisplay FROM SYSTEMINDEX WHERE '
if ($patternContainsRegex) {
$queryString += 'System.FileName LIKE ''' + $pattern + ''' '
} else {
$queryString += 'CONTAINS(System.FileName, ''' + $pattern + ''') '
}
if ([System.String]::IsNullOrEmpty($Text) -eq $false){
if ($Freetext) {
# FREETEXT doesn't support wildcards
# See https://learn.microsoft.com/en-us/windows/win32/search/-search-sql-where
# Replace or use as literals?
# If more than one word, treat it as a phrase, using double quotes.
$Text = $Text.Replace('*', '')
$Text = $Text.Replace('?', '')
if ($Text.Split(" ").Count -gt 1) {
$Text = '''"' + $Text + '"'''
} else {
$Text = "'$Text'"
}
$queryString += 'AND FREETEXT(*,' + $Text + ') '
} else {
# CONTAINS supports wildcard, but only at the end of a word.
# https://learn.microsoft.com/en-us/windows/win32/search/-search-sql-where
if ($Text.Contains('*')) {
$queryString += 'AND CONTAINS(''"' + $Text + '"'') '
} else {
$queryString += 'AND CONTAINS(''FORMSOF(INFLECTIONAL, "' + $Text + '")'') '
}
}
}
if ($Recurse){
$queryString += "AND SCOPE='file:" + $path + "' ORDER BY System.ItemPathDisplay"
}
else {
$queryString += "AND DIRECTORY='file:" + $path + "' ORDER BY System.ItemPathDisplay"
}
write-verbose $queryString
$global:fsSearchRs.Open($queryString, $global:fsSearchCon)
# return
write-verbose $global:fsSearchRs.EOF
While(-Not $global:fsSearchRs.EOF){
if ($AsFSInfo){
# Return a FileSystemInfo object
[System.IO.FileSystemInfo]$(Get-Item -LiteralPath ($global:fsSearchRs.Fields.Item("System.ItemPathDisplay").Value) -Force)
}
else {
Write-Output -NoEnumerate $global:fsSearchRs.Fields.Item("System.ItemPathDisplay").Value
}
$global:fsSearchRs.MoveNext()
}
$global:fsSearchRs.Close()
$global:fsSearchCon.Close()
}
@salemsap
Copy link

please share sample parameters to check the functionality

@alvalea
Copy link

alvalea commented Nov 28, 2022

I had to make the following changes to make it work:

  1. Rename the function as searchFileIndex: It seems that function names starting with a capital letter are not valid in powershell. I also removed the dash character, just in case special characters are not allowed either.
  2. Call the function at the end of the script forwarding the script arguments as parameters: searchFileIndex @args

All parameters are optional, but this would be an example of calling the script:
SearchFileIndex.ps1 -Path Documentation -Pattern *.pdf -Text "Next release" -Recursive

@poundy
Copy link

poundy commented Dec 14, 2022

There's nothing needed to make this work as it stands @alvalea. This is a PowerShell Function. You can save the file (for example, as search-fileindex.ps1) then you can simply dot-source it and then use it in your other powershell scripts (or in your interactive session):

. c:\filepath\for\this\script\search-fileindex.ps1

then just invoke it like:
search-fileindex -path c:\here -pattern *.docx -recurse -text "Find this text"

I did add some parameters to set the connection and command timeouts to 0 so a long query didn't end with an error. If you want to do that, you can simply add these two lines prior to line 51 above (that opens the search connection)

$fsSearchCon.ConnectionTimeout=0
$fsSearchCon.CommandTimeout=0

@cpbotha
Copy link

cpbotha commented Mar 12, 2023

On my Windows 11 22624.1391 system, certain search strings (the name of a specific super small scale product) would yield the following error:

{Application Error}
The exception %s (0x
At C:\Users\cpbot\OneDrive\configs\powershell\search-fileindex.ps1:76 char:9
+         $fsSearchRs.MoveNext()
+         ~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (:) [], COMException
    + FullyQualifiedErrorId : System.Runtime.InteropServices.COMException

Weirdly, for these strings, EOF would only evaluate correctly the second time. In other words, if I did e.g. Write-Host $fsSearchRs.EOF right before the while loop, it would correctly not enter. If I did not do that, it would enter, and then MoveNext would fail.

Other gibberish strings that also yield no results do correctly evaluate. This is currently reproducible.

To work around this, I changed the while loop to the following, so that it just gracefully yields no results:

While(-Not $fsSearchRs.EOF -and $fsSearchRs.Fields.Item("System.ItemPathDisplay").Value){

In this strange state, $fsSearchRs evals to the following on first eval:

Properties       : System.__ComObject
AbsolutePosition :
ActiveConnection : System.__ComObject
BOF              : True
Bookmark         :
CacheSize        : 1
CursorType       : 0
EOF              : True
Fields           : System.__ComObject
LockType         : 1
MaxRecords       : 0
RecordCount      : -1
Source           : SELECT System.ItemPathDisplay FROM SYSTEMINDEX WHERE System.FileName LIKE '%.%' AND FREETEXT('telesensi') AND SCOPE='file:C:/users/cpbot/OneDrive' ORDER BY System.ItemPathDisplay
AbsolutePage     : -1
EditMode         : 0
Filter           : 0
PageCount        : -1
PageSize         : 10
Sort             :
Status           :
State            : 1
CursorLocation   : 2
MarshalOptions   : 0
DataSource       : System.__ComObject
ActiveCommand    : System.__ComObject
StayInSync       : True
DataMember       :
Index            :

I'm leaving this here in case anyone else runs into similar issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment