Skip to main content

Ignixa.Search

Search parameter definitions, indexing, and compartment management for FHIR resources.

Installation

dotnet add package Ignixa.Search

Quick Start

using Ignixa.Search.Definition;
using Ignixa.Specification;
using Microsoft.Extensions.Logging;

// Create search parameter definition manager
var schemaProvider = FhirVersion.R4.GetSchemaProvider();
var manager = new SearchParameterDefinitionManager(schemaProvider, logger);

// Get parameters for a resource type
var patientParams = manager.GetSearchParameters("Patient");

foreach (var param in patientParams)
{
Console.WriteLine($"{param.Code}: {param.Type}");
}

Search Parameter Management

ISearchParameterDefinitionManager

public interface ISearchParameterDefinitionManager
{
// All search parameters across all resource types
IEnumerable<SearchParameterInfo> AllSearchParameters { get; }

// Get search parameters for a resource type
IEnumerable<SearchParameterInfo> GetSearchParameters(string resourceType);

// Try to get search parameters (returns false if resource type unknown)
bool TryGetSearchParameters(string resourceType, out IEnumerable<SearchParameterInfo> searchParameters);

// Get specific parameter by resource type and code
SearchParameterInfo GetSearchParameter(string resourceType, string code);

// Try to get specific parameter
bool TryGetSearchParameter(string resourceType, string code, out SearchParameterInfo searchParameter);

// Get parameter by definition URL
SearchParameterInfo GetSearchParameter(Uri definitionUri);

// Add custom search parameters at runtime
void AddNewSearchParameters(IReadOnlyCollection<IElement> searchParameters, bool calculateHash = true);

// Remove custom search parameter
void DeleteSearchParameter(string url, bool calculateHash = true);
}

Filtered Managers

// Only returns parameters marked as supported
var supportedManager = new SupportedSearchParameterDefinitionManager(manager);
var supported = supportedManager.GetSearchParameters("Patient");

// Only returns parameters marked as searchable
var searchableManager = new SearchableSearchParameterDefinitionManager(manager);
var searchable = searchableManager.GetSearchParameters("Patient");

// Optionally admit parameters that are registered but not yet reindexed ("partially indexed").
// Each accessor invokes the delegate exactly once and applies that answer to every result it
// returns; omitting the delegate defaults to refusing them.
var partial = new SearchableSearchParameterDefinitionManager(manager, () => true);

:::warning Partially indexed parameters

Opting in makes results wrong in both directions, not merely incomplete. A resource that has not been reindexed yet has no index rows for the parameter, so a positive filter omits it, while a negation (:not, :missing=true) lowers to Except(every resource of the type, the inner match) and hands that same resource back as a match. Nothing in the response bundle distinguishes such a result from a complete one, so only enable this when the caller has explicitly asked for partial results.

:::

SearchParameterInfo

public class SearchParameterInfo
{
// Parameter name (e.g., "family")
public string Name { get; }

// Parameter code used in queries (e.g., "family")
public string Code { get; }

// Parameter type (string, token, reference, date, etc.)
public SearchParamType Type { get; }

// Canonical URL
public Uri Url { get; }

// FHIRPath expression for extraction
public string Expression { get; }

// Description
public string Description { get; }

// Base resource types this parameter applies to
public IReadOnlyList<string> BaseResourceTypes { get; }

// Target resource types (for reference parameters)
public IReadOnlyList<string> TargetResourceTypes { get; }

// Components (for composite parameters)
public IReadOnlyList<SearchParameterComponentInfo> Component { get; }

// Whether this parameter is searchable (mutated in place as reindexing progresses)
public bool IsSearchable { get; set; }

// Whether this parameter is supported (mutated in place as reindexing progresses)
public bool IsSupported { get; set; }
}

Search Parameter Types

TypeDescriptionExample
stringText searchname, address
tokenCoded valuesidentifier, code
referenceResource referencessubject, patient
dateDate/DateTimebirthdate, date
numberNumeric valueslength
quantityValue with unitvalue-quantity
uriURI valuesurl
compositeMultiple valuescomponent-code-value-quantity

Compartment Support

CompartmentDefinitionManager

using Ignixa.Search.Definition;
using Ignixa.Specification.ValueSets.Normative;

var compartmentManager = new CompartmentDefinitionManager(FhirVersion.R4);

// Get search params for a resource in a compartment
if (compartmentManager.TryGetSearchParams("Observation", CompartmentType.Patient, out var searchParams))
{
Console.WriteLine($"Patient compartment search params for Observation:");
foreach (var param in searchParams)
{
Console.WriteLine($" - {param}");
}
}

// Get all resource types in a compartment
if (compartmentManager.TryGetResourceTypes(CompartmentType.Patient, out var resourceTypes))
{
Console.WriteLine($"Resources in Patient compartment: {string.Join(", ", resourceTypes)}");
}

CompartmentType

Available compartment types from the FHIR specification:

  • CompartmentType.Patient
  • CompartmentType.Encounter
  • CompartmentType.RelatedPerson
  • CompartmentType.Practitioner
  • CompartmentType.Device

Parameter Conflict Resolution

When multiple IGs define SearchParameters with the same code, use conflict resolution:

using Ignixa.Search.Definition;

var options = new SearchParameterResolutionOptions
{
// Higher priority packages win (first = highest priority)
PackagePriorityOrder = ["hl7.fhir.us.core", "hl7.fhir.r4.core"],
UseSemanticVersioning = true,
LogConflicts = true
};

var resolver = new SearchParameterConflictResolver(options, logger);

// Resolve conflict among candidates with same code for a resource type
var winner = resolver.ResolveConflict(
candidates: conflictingParams,
code: "identifier",
resourceType: "Patient",
packageMetadata: packageMetadataLookup
);

Resolution Strategy

  1. Explicit priority - Packages listed in PackagePriorityOrder win (first = highest)
  2. Semantic versioning - Highest version wins when no priority configured
  3. Alphabetical - Package ID sort for deterministic ordering when versions equal

Search expression parsing

IExpressionParser remains the entry point used by SearchOptionsBuilder. Parser instances are created per tenant and FHIR version, so SearchParameterInfo lookup and reference-target validation use the active definition manager and schema.

Handwritten syntax scanners parse ordinary parameters, modifiers, typed forward chains, nested _has, include/revinclude forms, _not-referenced, escaped separators (\,, \$, \|, \\), comma alternatives, dollar composites, comparator prefixes, :missing, :text, and :of-type. The scanners emit immutable syntax records; semantic binders remain the only schema-aware layer.

Malformed key or value syntax raises InvalidSearchOperationException with a positioned line/column diagnostic. Semantic failures retain the existing SearchParameterNotSupportedException, BadSearchRequestException, and resource-backed InvalidSearchOperationException messages. Atomic date, number, quantity, reference, string, token, and URI conversion continues to use the existing *SearchValue.Parse implementations.

The mandatory BenchmarkDotNet result and acceptance decision are recorded in the handwritten syntax parser comparison. The comparison uses the unchanged public-facade harness and six inputs against the original handwritten baseline. The replacement was classified as Mixed, with a -6.31% geometric-mean time change, and all ratified performance limits passed. It was not classified as Faster; no speedup is claimed.