Skip to content

Add SearchBuilder extension library for idiomatic Find-Item queries #1427

@michaellwest

Description

@michaellwest

Summary

The current Find-Item cmdlet requires verbose hashtable-based SearchCriteria arrays that are error-prone and difficult to discover. This adds a SearchBuilder extension library (following the DialogBuilder pattern) that provides a fluent, pipeline-based API for constructing search queries.

Based on recommendations for improving Find-Item usability in MCP/LLM-driven interactions.

Features

Core Builder

  • New-SearchBuilder -- creates builder with -Index, -Path, -OrderBy, -First, -Skip, -Last, -MaxResults, -Strict, -IncludeMetadata, -Property, -QueryType, -FacetOn, -FacetMinCount
  • Invoke-Search -- executes builder via Find-Item, returns result object with pagination state, auto-advances _Skip each call
  • Reset-SearchBuilder -- resets pagination for re-use

Filter Functions (fluent, pipeline-based)

  • Add-SearchFilter -- core filter: -Field, -Filter, -Value, -Invert, -Boost, -CaseSensitive
  • Add-TemplateFilter -- convenience: -Name or -Id
  • Add-FieldContains / Add-FieldEquals -- shorthand wrappers
  • Add-DateRangeFilter -- relative syntax (-Last "7d", "2w", "3m", "1y") and absolute (-From/-To)

Predicate Grouping

  • New-SearchFilterGroup / Add-SearchFilterGroup -- compose OR/AND groups within queries (CLM-safe, no scriptblocks)

Discovery & Validation

  • Get-SearchFilter -- lists all 14 valid FilterType values with descriptions at runtime
  • Get-SearchIndexField -- lists all indexed fields via Schema.AllFieldNames
  • -Strict mode validates field names against the index schema before executing

Performance & Advanced

  • -Property -- select specific SearchResultItem properties (e.g., Name, Path, TemplateName) to avoid deserializing full objects
  • -QueryType -- pass a custom SearchResultItem subclass for strongly-typed index fields
  • -FacetOn / -FacetMinCount -- faceted search returning category aggregations instead of items

Result Object (CLM-safe via New-PSObject)

Standard search:

Items, HasMore, PageNumber, PageSize, TotalCount, Truncated, MaxResults, IndexName, Query

With -IncludeMetadata: adds IndexLastUpdated

Faceted search (-FacetOn):

Facets (raw FacetResults), Categories (convenience accessor), IndexName, Query

Pagination

Auto-advancing pagination with HasMore signal for paged accumulation loops:

$search = New-SearchBuilder -Index "sitecore_master_index" -First 50 -MaxResults 500
$search | Add-TemplateFilter -Name "Article"
do {
    $results = $search | Invoke-Search
    $results.Items | ForEach-Object { # process }
} while ($results.HasMore)

Implementation

  • 3 YAML files following DialogBuilder 3-level serialization pattern (Script Folder > Script Module > Script)
  • 14 PowerShell functions (12 public + 2 internal helpers)
  • CLM allowlist -- all functions added to content-editor profile in Spe.config
  • No C# changes -- pure PowerShell extension loaded via Import-Function -Name SearchBuilder

Testing

  • Unit tests (tests/unit/SPE.SearchBuilder.Tests.ps1): 102 assertions covering all functions
  • Integration tests (tests/integration/Remoting.SearchBuilder.Tests.ps1): 11 test sections against live Sitecore
  • Live validation: all scenarios verified against Sitecore 10.4 instance

Usage Examples

Simple search

Import-Function -Name SearchBuilder

$search = New-SearchBuilder -Index "sitecore_master_index" -First 25
$search | Add-TemplateFilter -Name "Article"
$search | Add-FieldContains -Field "Title" -Value "Welcome"
$results = $search | Invoke-Search

$results.Items | Initialize-Item | ForEach-Object { $_.Name }

Complex OR group with date range

$search = New-SearchBuilder -Index "sitecore_master_index" -Strict
$group = New-SearchFilterGroup -Operation Or
$group | Add-TemplateFilter -Name "Article"
$group | Add-TemplateFilter -Name "Blog Post"
$search | Add-SearchFilterGroup -Group $group
$search | Add-DateRangeFilter -Field "__Updated" -Last "30d"
$results = $search | Invoke-Search

Paged accumulation with safety cap

$search = New-SearchBuilder -Index "sitecore_master_index" -First 100 -MaxResults 500
$search | Add-TemplateFilter -Name "Article"
$all = [System.Collections.ArrayList]@()
do {
    $results = $search | Invoke-Search
    $all.AddRange($results.Items)
} while ($results.HasMore)
Write-Host "Collected $($all.Count) items, truncated: $($results.Truncated)"

Select specific properties for performance

$search = New-SearchBuilder -Index "sitecore_master_index" -First 10 -Property @("Name", "Path", "TemplateName")
$search | Add-TemplateFilter -Name "Template Folder"
$results = $search | Invoke-Search
$results.Items | ForEach-Object { "$($_.Name) [$($_.TemplateName)]" }

Faceted search

$search = New-SearchBuilder -Index "sitecore_master_index" -FacetOn @("TemplateName") -FacetMinCount 50
$search | Add-FieldContains -Field "_fullpath" -Value "powershell"
$results = $search | Invoke-Search
$results.Categories | ForEach-Object {
    Write-Host "$($_.Name):"
    $_.Values | ForEach-Object { Write-Host "  $($_.Name): $($_.AggregateCount)" }
}

Inverted filter with boost

$search = New-SearchBuilder -Index "sitecore_master_index" -First 5
$search | Add-TemplateFilter -Name "Template Folder"
$search | Add-SearchFilter -Field "_name" -Filter "Contains" -Value "system" -Invert -Boost 5
$results = $search | Invoke-Search
# Returns Template Folders NOT containing "system" in name

Strict mode -- field validation

$search = New-SearchBuilder -Index "sitecore_master_index" -Strict
$search | Add-SearchFilter -Field "bogus_field" -Filter "Equals" -Value "test"
$search | Invoke-Search
# Throws: "Strict mode: The following fields are not indexed: 'bogus_field'"

Discovery

# List all valid filter types
Get-SearchFilter

# List all indexed fields
Get-SearchIndexField -Index "sitecore_master_index"

Reset and re-run

$search | Reset-SearchBuilder   # resets Skip and PageNumber to 0
$results = $search | Invoke-Search   # starts from page 1 again

Notes

  • -Property and -FacetOn use C# property names on SearchResultItem (Name, Path, TemplateName), not index field names (_name, _fullpath, _templatename). This matches native Find-Item behavior.
  • Invoke-Search returns a wrapper object. To pipe to Initialize-Item, use $results.Items | Initialize-Item.
  • All output objects use New-PSObject for CLM (Constrained Language Mode) compatibility.
  • The builder is a hashtable (reference type) -- Add-* functions mutate in-place, no reassignment needed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions