Skip to content

Augment Search-IshDocumentObj with an OpenAPI implementation, preparing for semantic search #271

Description

@ddemeyer

Augment Search-IshDocumentObj with an OpenAPI implementation, preparing for semantic search

Background

Search-IshDocumentObj is currently implemented exclusively over the legacy SOAP Search 2.5 API (IshSession.Search25.PerformSearch), even when the active session protocol is OpenApiWithOpenIdConnectIshSession.Search25 silently routes through the WCF SOAP channel regardless of protocol (IshSession.cs). Per the Web Services API compatibility table:

  • API25.Search.PerformSearch → deprecated from 15.1 onward, replaced by API30.Search
  • API30.Search (POST /v3/Search) → supported since Tridion Docs 15.1
  • API30.SearchInPublication (POST /v3/Publications/.../Search) → supported since 15.2, out of scope for this issue

Upcoming semantic search (vector/embedding-based similarity search) enhancements will land only on the OpenAPI /v3/Search endpoint. This issue augments Search-IshDocumentObj with a genuine OpenAPI implementation and a PowerShell-native query-building surface, so semantic search can be layered on top later without another architectural rework.

Scope

In scope:

  • OpenAPI implementation of Search-IshDocumentObj alongside the existing SOAP implementation
  • A new, PowerShell-idiomatic, structured query-building parameter set (-SearchCriteria/-SortField/etc.) that works across both SOAP and OpenAPI transports from one object model
  • Reserved (throw-on-unsupported) parameter surface for the upcoming AnywhereInContent/SemanticInContent search variants, so the follow-up semantic-search issue needs no new parameter sets
  • Format.ps1xml support for visually verifying a constructed query tree
  • A documentation update capturing the "silent version-gated fallback" pattern this issue reinforces

Out of scope:

  • API30.SearchInPublication / publication-scoped search (separate Augment issue)
  • Actual semantic/vector search implementation (follow-up issue; this issue only reserves the naming/parameter surface)
  • ValueType=Element resolution on Card-typed fields (e.g. resolving FAUTHOR to an actual IshUser reference rather than label-text match) — requires a DataSource→concrete-Set*-subtype mapping not solved here; remains -XmlQuery (SOAP)-only
  • -XmlQuery: remains a raw SOAP pass-through only, forever. No XML→OpenAPI model conversion is attempted — it always executes against Search25.PerformSearch regardless of session protocol.

Part 1 — Search-IshDocumentObj.cs: dual-protocol execution for -SimpleQuery/-XmlQuery

Add a switch (IshSession.Protocol) (never a bare if) around the existing SOAP call, following the established silent version-gated fallback shape from GetIshPublicationOutputData.cs:

switch (IshSession.Protocol)
{
    case Enumerations.Protocol.OpenApiWithOpenIdConnect:
        if (IshSession.ServerIshVersion.MajorVersion > 15 ||
            (IshSession.ServerIshVersion.MajorVersion == 15 && IshSession.ServerIshVersion.MinorVersion >= 1))
        {
            // IshSession.OpenApiISH30Client.SearchAsync(...)
            break;
        }
        // Server < 15.1: silently fall back to SOAP
        goto case Enumerations.Protocol.WcfSoapWithOpenIdConnect;
    case Enumerations.Protocol.WcfSoapWithWsTrust:
    case Enumerations.Protocol.WcfSoapWithOpenIdConnect:
        // existing SOAP call, unchanged
        break;
}
  • -SimpleQuery maps to SearchRequest{ Expression = AndSearchExpression{ AnywhereSearchFieldValue{ Contains, SimpleQuery } }, SortFields = [Score desc], VersionFilter = Latest, Languages = [UserLanguage] } when the OpenAPI path is taken.
  • -XmlQuery always executes via SOAP Search25.PerformSearch, regardless of protocol — no goto/no version guard needed for this parameter set; it's a deliberate, permanent exception documented in the parameter help.
  • Result extraction: SearchResponse.Entries[].LanguageCardId maps 1:1 to the existing lngRef used for DocumentObj25.RetrieveMetadataByIshLngRefs batching — that downstream enrichment logic is unchanged and reused by both transports.
  • Count path: SearchResponse.TotalHitsFound maps to the existing performSearchResponse.totalHitsFound output.
  • Add using Trisoft.ISHRemote.OpenApiISH30; and an OpenApiISH30Exception<InfoShareProblemDetails> catch block (copy shape from GetIshPublicationOutputData.cs:190-201).

Part 2 — New structured query-building cmdlets (works across SOAP and OpenAPI from one model)

Three new public POCOs + cmdlets, replacing the earlier "MetadataFilter reuse" idea (rejected: Find's flat-AND IshMetadataFilterField model loses And/Or nesting and AllVersions/LatestVersion richness that Search genuinely needs) and the "raw JsonQuery" idea (rejected: no discoverability benefit over -XmlQuery, and the object model below covers the schema with no meaningful loss).

IshSearchExpression (abstract base) → IshSearchCriteriaField (leaf) / IshSearchCriteriaGroup (node)

Mirrors SearchExpression/SearchFieldValue/GroupSearchExpression 1:1. Both leaf and group serialize to either an <ishquery> XML fragment (SOAP) or a SearchExpression JSON object (OpenAPI) at execution time, decided by the same protocol/version switch as Part 1. If the OpenAPI target can't express a constructed tree (e.g. reserved variants below), it throws — no silent fallback to SOAP for capability gaps (silent fallback is reserved exclusively for version gaps, per Part 1).

Set-IshSearchCriteriaField

Parameter Mandatory Parameter Set Default
Name Yes FieldGroup
Level No FieldGroup None (technical default only — set explicitly per field)
Operator No FieldGroup Contains (matches SOAP ishoperator default)
Value Yes FieldGroup, AnywhereGroup, AnywhereInContentGroup, SemanticInContentGroup — (string[]; multiple values = OR-accrue)
ValueType No FieldGroup Value (label/string match; Element on Card fields is SOAP/-XmlQuery-only, out of scope here)
Anywhere Yes (switch) AnywhereGroup Maps to legacy ISHANYWHERE — searches metadata + xmlcontent + content. Fully implemented. No -Operator exposed (hardcoded Contains — the other 6 operators are meaningless against free text).
AnywhereInContent Yes (switch) AnywhereInContentGroup Maps to reserved future keyword ISHANYWHEREINCONTENT — content field only, lexical match. Reserved: throws a clear "not yet supported by the server" error if selected; no server-side type exists yet. No -Operator.
SemanticInContent Yes (switch) SemanticInContentGroup Reserved for the upcoming semantic/vector search follow-up — content field only, embedding similarity match. Reserved: throws until the follow-up issue wires it up. No -Operator (semantic search has no boolean operator concept).
IshSession No all resolved from session state, standard convention

Set-IshSearchCriteriaGroup

Parameter Mandatory Parameter Set Default
And Yes (switch) AndGroup
Or Yes (switch) OrGroup
Expression Yes both — (object[]; accepts IshSearchCriteriaField and/or nested IshSearchCriteriaGroup, arbitrary depth)
IshSession No both resolved from session state

Set-IshSearchSortField

Parameter Mandatory Parameter Set Default
Score Yes (switch) ScoreGroup Maps to ISHSCORE
Name Yes FieldGroup
Level No FieldGroup None (technical default, set explicitly per field)
Order No both Ascending (enum default; help text should flag that -Score -Order Descending is the typical/expected combination)
IshSession No both resolved from session state
(pipeline) No both accepts IshSearchSortField[]array order = sort priority, first item wins

Search-IshDocumentObj: new parameter set

Parameter Mandatory Default if omitted
SearchCriteria No Wildcard: single Anywhere/Contains "*" leaf, auto-wrapped in an AndSearchExpression root (SearchRequest.Expression requires a GroupSearchExpression, so a bare leaf passed here is wrapped internally — no forced Set-IshSearchCriteriaGroup call for one-criterion queries)
SortField No Single Score, Descending
VersionFilter No LatestVersion
ObjectTypeFilter No Any (omits <ishtypefilter> for SOAP; emits any for OpenAPI)
LanguageFilter No @($IshSession.UserLanguage)
ResolutionFilter No empty (OpenAPI-only — no <ishquery> equivalent exists at all; WriteWarning + ignore if the resolved target is SOAP)
MaxHitsToReturn No 20 (existing, unchanged; cast to int for SearchRequest.SizeLimit)
RequestedMetadata No IshSession.DefaultRequestedMetadata (existing, unchanged)
Count Yes, in Count variant — (existing, unchanged)
IshSession No resolved from session state (existing, unchanged)

-ObjectTypeFilter value mapping (SOAP-native enum chosen as the PowerShell-facing type — zero translation needed for SOAP, one lookup table for OpenAPI):

PowerShell enum SOAP <ishtypefilter> OpenAPI SearchObjectTypeFilter
ISHModule ISHModule topic
ISHMasterDoc ISHMasterDoc map
ISHLibrary ISHLibrary library
ISHTemplate ISHTemplate other
ISHIllustration ISHIllustration illustration
Any (default) (omit all <ishtypefilter> elements) any

Part 3 — Format.ps1xml: visualizing the And/Or tree

PowerShell's Format.ps1xml has no recursive/self-referencing view construct, and this repo's format file uses TableControl exclusively (24 views, zero ListControl/CustomControl/ScriptBlock). Following the existing IshTypeFieldDefinitionCompare.CompareResult pattern (a plain computed read-only C# string property referenced via <PropertyName>, no ScriptBlock):

  • IshSearchCriteriaGroup/IshSearchCriteriaField get a read-only Preview property whose C# getter recursively renders indented pseudo-query text:
    AND
      OR
        FSTATUS[Lng] equal 'Translated'
        FAUTHOR[Lng] equal 'Dave De Meyer'
      ISHANYWHERE contains 'change oil filter'
      FDUEDATE[Version] lessthan '2006-10-27'
    
  • ToString() on both types delegates to Preview (single source of truth for interactive echo and formatted display).
  • Format.ps1xml gets a new single-column TableControl view per type, referencing Preview, structurally identical to the existing IshTypeFieldDefinitionCompare view.

Part 4 — Cleanup: dead ToString() comments

IshMetadataField.cs:155, IshMetadataFilterField.cs:111, IshRequestedMetadataField.cs:55 each carry a commented-out, unused alternate XML-string ToString() body sitting beside the live implementation — remove them (IshVersion.cs's ToString() is unrelated and already clean; leave it).

Part 5 — Documentation: source-api-webservices--csharp.instructions.md

Add a new section documenting the silent version-gated fallback pattern (distinct from the existing PlatformNotSupportedException guard pattern in §6):

  • When to use it: SOAP can fully satisfy the request on older servers and OpenAPI is a transparent enhancement — use silent fallback (goto case, WriteDebug, no user-facing warning).
  • When not to use it: capability gaps within an already-resolved OpenAPI target (e.g. a query shape OpenAPI can't express) — these throw, they do not fall back to SOAP. Version gaps and capability gaps are not the same kind of gap and must not share a resolution strategy.
  • Reference implementations: GetIshPublicationOutputData.cs (existing) and SearchIshDocumentObj.cs (this issue, once merged).

Appendix: worked example — same query, three representations

Source XML (from the docs page):

<ishquery>
  <and>
    <or>
      <ishfield name='FSTATUS' level='lng' ishoperator='equal'>Translated</ishfield>
      <ishfield name='FAUTHOR' level='lng' ishoperator='equal' ishvaluetype='label'>Dave De Meyer</ishfield>
    </or>
    <ishfield name='ISHANYWHERE' level='none' ishoperator='contains'>change oil filter</ishfield>
    <ishfield name='FDUEDATE' level='version' ishoperator='lessthan'>20061027</ishfield>
  </and>
  <ishsort>
    <ishsortfield name='ISHSCORE' level='none' ishorder='d'/>
    <ishsortfield name='FSTATUS' level='lng' ishorder='a'/>
    <ishsortfield name='FAUTHOR' level='lng' ishorder='a'/>
  </ishsort>
  <ishobjectfilters>
    <ishversionfilter>LatestVersion</ishversionfilter>
    <ishtypefilter>ISHModule</ishtypefilter>
    <ishtypefilter>ISHMasterDoc</ishtypefilter>
    <ishlanguagefilter>en</ishlanguagefilter>
    <ishlanguagefilter>nl</ishlanguagefilter>
  </ishobjectfilters>
</ishquery>

Equivalent OpenAPI SearchRequest JSON:

{
  "expression": {
    "type": "AndSearchExpression",
    "expressions": [
      {
        "type": "OrSearchExpression",
        "expressions": [
          {
            "type": "StringSearchFieldValue",
            "fullTextSearchOperator": "equal",
            "ishField": { "name": "FSTATUS", "level": "language" },
            "value": [ "Translated" ]
          },
          {
            "type": "StringSearchFieldValue",
            "fullTextSearchOperator": "equal",
            "ishField": { "name": "FAUTHOR", "level": "language" },
            "value": [ "Dave De Meyer" ]
          }
        ]
      },
      {
        "type": "AnywhereSearchFieldValue",
        "operator": "contains",
        "value": "change oil filter"
      },
      {
        "type": "DateTimeSearchFieldValue",
        "fullTextSearchOperator": "lessThan",
        "ishField": { "name": "FDUEDATE", "level": "version" },
        "value": [ "2006-10-27T00:00:00Z" ]
      }
    ]
  },
  "sortFields": [
    { "type": "ScoreSearchSortField", "sortOrder": "descending" },
    { "type": "SearchSortField", "sortOrder": "ascending", "ishField": { "name": "FSTATUS", "level": "language" } },
    { "type": "SearchSortField", "sortOrder": "ascending", "ishField": { "name": "FAUTHOR", "level": "language" } }
  ],
  "versionFilter": "latest",
  "objectTypes": [ "topic", "map" ],
  "languages": [ "en", "nl" ],
  "sizeLimit": 100
}

Equivalent new PowerShell syntax:

$orGroup = Set-IshSearchCriteriaGroup -Or -Expression @(
    (Set-IshSearchCriteriaField -Name 'FSTATUS' -Level Lng -Operator Equal -Value 'Translated'),
    (Set-IshSearchCriteriaField -Name 'FAUTHOR' -Level Lng -Operator Equal -Value 'Dave De Meyer')
)

$rootGroup = Set-IshSearchCriteriaGroup -And -Expression @(
    $orGroup,
    (Set-IshSearchCriteriaField -Anywhere -Value 'change oil filter'),
    (Set-IshSearchCriteriaField -Name 'FDUEDATE' -Level Version -Operator LessThan -Value '2006-10-27')
)

$sortFields = Set-IshSearchSortField -Score -Order Descending |
              Set-IshSearchSortField -Name 'FSTATUS' -Level Lng -Order Ascending |
              Set-IshSearchSortField -Name 'FAUTHOR' -Level Lng -Order Ascending

Search-IshDocumentObj -SearchCriteria $rootGroup -SortField $sortFields `
    -VersionFilter LatestVersion -ObjectTypeFilter ISHModule,ISHMasterDoc `
    -LanguageFilter en,nl -MaxHitsToReturn 100

$rootGroup.Preview (or just $rootGroup printed, via the new Format.ps1xml view) would render as:

AND
  OR
    FSTATUS[Lng] equal 'Translated'
    FAUTHOR[Lng] equal 'Dave De Meyer'
  ISHANYWHERE contains 'change oil filter'
  FDUEDATE[Version] lessthan '2006-10-27'

Note -Anywhere doesn't take -Operator per the earlier decision (hardcoded Contains), so the PowerShell line is slightly terser than the XML/JSON equivalents which both explicitly carry contains/ishoperator='contains'.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions