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, -LatestVersion
  • 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 (with [ValidateSet]), -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, using FieldNameTranslator to resolve logical names (e.g., __Updated, Title) before checking

Performance & Advanced

  • -Property -- select specific SearchResultItem C# 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
  • -LatestVersion -- filter to only the latest version of each item
  • [ArgumentCompleter] on -Index parameter for tab completion via Get-SearchIndex
  • [ValidateSet] on -Filter parameter catches typos at invocation time

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

Query Summary

The Query property on results includes full context:

_templatename Equals 'Template Folder' AND _fullpath Contains 'powershell' [Path: /sitecore/content | LatestVersion | OrderBy: score]

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)

Design Decisions

  • No dynamic LINQ (-Where/-WhereValues): The builder's fluent API already covers these capabilities. Users who need dynamic LINQ should use Find-Item directly. See comment.
  • -Property and -FacetOn use C# property names (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.

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): 124 assertions
  • Integration tests (tests/integration/Remoting.SearchBuilder.Tests.ps1): 18 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 -LatestVersion
$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

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

Get-SearchFilter                                     # list all 14 filter types with descriptions
Get-SearchIndexField -Index "sitecore_master_index"   # list all indexed fields

Reset and re-run

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

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