Private/Provider/Invoke-ProviderSearch.ps1

function Invoke-ProviderSearch {
    param(
        [string]$Query,
        [int]$MaxResults = 99,
        [int]$Skip = 0,
        [bool]$ExactSearch = $false,
        [string]$SortBy = 'Relevance',
        [string]$Repository = 'PSGallery',
        [bool]$AuthorSearch = $false,
        [bool]$TagSearch = $false
    )

    $provider = Get-ModuleProvider
    $maxRetries = 2
    $serverErrorPattern = '5\d{2}|Internal Server Error|Service Unavailable|Gateway Timeout|operation has timed out'

    for ($attempt = 1; $attempt -le ($maxRetries + 1); $attempt++) {
        try {
            Import-ActiveProvider -Provider $provider
            Write-PSMBLog -Message "Invoke-ProviderSearch: provider=$provider query='$Query' skip=$Skip max=$MaxResults exact=$ExactSearch author=$AuthorSearch tag=$TagSearch sort=$SortBy attempt=$attempt" -Level 'DEBUG'

            $totalNeeded = $Skip + $MaxResults

            if ($TagSearch) {
                if ($provider -eq 'PSResourceGet') {
                    if ($Repository -eq 'PSGallery') {
                        $raw = Invoke-PSGalleryApiSearch -SearchTerm $Query -TotalNeeded ($totalNeeded * 5)
                        $results = $raw | ConvertTo-ModuleInfo -ProviderType 'PSResourceGet' |
                            Where-Object { $_.Tags -contains $Query }
                    } else {
                        $raw = Find-PSResource -Tag $Query -Repository $Repository -ErrorAction Stop |
                            Where-Object { $_.Type -ne 'Script' } |
                            Select-Object -First $totalNeeded
                        $results = $raw | ConvertTo-ModuleInfo -ProviderType 'PSResourceGet'
                    }
                } else {
                    $raw = Find-Module -Tag $Query -Repository $Repository -ErrorAction Stop |
                        Select-Object -First $totalNeeded
                    $results = $raw | ConvertTo-ModuleInfo -ProviderType 'PowerShellGet'
                }
            } elseif ($AuthorSearch) {
                if ($provider -eq 'PSResourceGet') {
                    if ($Repository -eq 'PSGallery') {
                        $raw = Invoke-PSGalleryApiSearch -SearchTerm $Query -TotalNeeded ($totalNeeded * 5)
                        $results = $raw | ConvertTo-ModuleInfo -ProviderType 'PSResourceGet' |
                            Where-Object { $_.Author -like "*$Query*" }
                    } else {
                        try {
                            $raw = Find-PSResource -Tag $Query -Repository $Repository -ErrorAction Stop |
                                Where-Object { $_.Type -ne 'Script' -and $_.Author -like "*$Query*" } |
                                Select-Object -First $totalNeeded
                        } catch {
                            if ($_.Exception.Message -match $serverErrorPattern) { throw }
                            $raw = @()
                        }
                        if ($raw.Count -eq 0) {
                            try {
                                $raw = Find-PSResource -Name "${Query}*" -Repository $Repository -ErrorAction Stop |
                                    Where-Object { $_.Type -ne 'Script' -and $_.Author -like "*$Query*" } |
                                    Select-Object -First $totalNeeded
                            } catch {
                                if ($_.Exception.Message -match $serverErrorPattern) { throw }
                                $raw = @()
                            }
                        }
                    }
                    $results = $raw | ConvertTo-ModuleInfo -ProviderType 'PSResourceGet'
                } else {
                    $raw = Find-Module -Filter $Query -Repository $Repository -ErrorAction Stop |
                        Where-Object { $_.Author -like "*$Query*" } |
                        Select-Object -First $totalNeeded
                    $results = $raw | ConvertTo-ModuleInfo -ProviderType 'PowerShellGet'
                }
            } else {
                $resolvedQuery = if ($ExactSearch) { $Query } else { "*$Query*" }

                if ($provider -eq 'PSResourceGet') {
                    $fetchCount = if ($SortBy -eq 'Downloads') { $totalNeeded * 2 } else { $totalNeeded }
                    if (-not $ExactSearch -and $Repository -eq 'PSGallery') {
                        # Use direct PSGallery Search API (searchTerm) — fast full-text index
                        # instead of Find-PSResource substringof() OData filter which causes 500s
                        $raw = Invoke-PSGalleryApiSearch -SearchTerm $Query -TotalNeeded $fetchCount
                    } else {
                        $raw = Find-PSResource -Name $resolvedQuery -Repository $Repository -ErrorAction Stop |
                            Where-Object { $_.Type -ne 'Script' } |
                            Select-Object -First $fetchCount
                    }
                    $results = $raw | ConvertTo-ModuleInfo -ProviderType 'PSResourceGet'
                } else {
                    $raw = Find-Module -Name $resolvedQuery -Repository $Repository -ErrorAction Stop |
                        Select-Object -First $totalNeeded
                    $results = $raw | ConvertTo-ModuleInfo -ProviderType 'PowerShellGet'
                }
            }

            switch ($SortBy) {
                'Downloads' { $results = $results | Sort-Object -Property DownloadCount -Descending }
                'LastUpdated' { $results = $results | Sort-Object -Property PublishedDate -Descending }
            }

            # Deduplicate by module name (API may return multiple entries per module)
            $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
            $results = @($results | Where-Object { $seen.Add($_.Name) })

            if ($Skip -gt 0) {
                return $results | Select-Object -Skip $Skip -First $MaxResults
            }
            return $results | Select-Object -First $MaxResults

        } catch {
            if ($attempt -le $maxRetries -and $_.Exception.Message -match $serverErrorPattern) {
                $delaySec = 2 * $attempt
                Write-PSMBLog -Message "Retry $attempt/$maxRetries in ${delaySec}s after server error: $_" -Level 'WARN'
                Start-Sleep -Seconds $delaySec
                continue
            }
            Write-PSMBLog -Message "Invoke-ProviderSearch error: $_" -Level 'ERROR'
            Write-Error "Search failed: $_"
            return @()
        }
    }
}