Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pkg/postgres/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ Search entries use typed table routing (same convention as sqlite):
- `token.<name>` → token index
- `string.<name>` → string index (default when no prefix)
- `date.<name>`, `number.<name>`, `reference.<name>` / `ref.<name>` → typed tables
- `uri.<name>` → string index (canonical/URI search parameters)

FHIR search parsing lives outside this package; callers pass prepared `SearchIndexEntry` values on writes.

Expand Down
1 change: 1 addition & 0 deletions pkg/postgres/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@
// - date.<name> → hai_search_date
// - number.<name> → hai_search_number
// - reference.<name> or ref.<name> → hai_search_reference
// - uri.<name> → hai_search_string
//
// Keys without a prefix default to hai_search_string. QueryPrepared supports the "by-field"
// plan (args: key, value). AnalyticsStore QueryPrepared supports "by-name-since"
Expand Down
14 changes: 14 additions & 0 deletions pkg/postgres/search_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ func parseSearchFieldKey(key string) (searchTable, string, error) {
return searchTableComposite, parts[1], nil
case "text":
return searchTableText, parts[1], nil
case "uri":
return searchTableString, parts[1], nil
default:
return searchTableString, key, nil
}
Expand Down Expand Up @@ -193,6 +195,18 @@ func (s *SearchStore) LookupMatch(ctx context.Context, match store.SearchMatch)
AND (value LIKE $4 || '/%%' OR value LIKE $4 || '|%%')
ORDER BY resource_id`, table)
args = []any{s.tenantID, match.ResourceType, fieldKey, match.Value}
case (table == searchTableString) && op == "below":
query = fmt.Sprintf(`
SELECT resource_id FROM %s
WHERE tenant_id = $1 AND resource_type = $2 AND field_key = $3 AND value LIKE $4 || '%%'
ORDER BY resource_id`, table)
args = []any{s.tenantID, match.ResourceType, fieldKey, match.Value}
case (table == searchTableString) && op == "above":
query = fmt.Sprintf(`
SELECT resource_id FROM %s
WHERE tenant_id = $1 AND resource_type = $2 AND field_key = $3 AND $4 LIKE value || '%%'
ORDER BY resource_id`, table)
args = []any{s.tenantID, match.ResourceType, fieldKey, match.Value}
case (table == searchTableDate || table == searchTableNumber) && isComparator(op):
sqlOp, err := comparatorSQL(op)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ Build failures roll back partially opened resources before returning an error.
| SQLite | Embedded/basic search via local `SearchStore` executor |
| Postgres | Full search service + background reindex worker |

Advanced FHIR search features (`_include`, chained search, composites, FTS) remain **Postgres-first** per `pkg/search`. SQLite persists index rows and supports basic lookups.
Advanced FHIR search features (`_include` wildcards, `_has`, two-hop chains, uri, composites, FTS) remain **Postgres-first** per `pkg/search`. SQLite persists index rows and supports basic lookups.

## Sync by mode

Expand Down
12 changes: 7 additions & 5 deletions pkg/search/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,11 @@ Postgres-first advanced FHIR search:
| Registry-backed parameters | All installed SearchParameters for enabled resource types |
| `_count` / `_offset` | Paging with max `_count` of 100 |
| `_sort` | Registry-backed fields plus `_id` and `_lastUpdated` |
| Modifiers | `string:exact`, `string:contains`; token/reference modifiers per type |
| Modifiers | `string:exact`, `string:contains`; `uri:below`, `uri:above`; token/reference modifiers per type |
| Prefixes | Date/number comparators: `eq`, `ne`, `gt`, `ge`, `lt`, `le`, `sa`, `eb`, `ap` |
| Chained search | Single-hop only (e.g. `subject.name`) |
| `_include` / `_revinclude` | Direct includes; wildcards deferred |
| Chained search | Up to two hops (e.g. `subject.name`, `subject.organization.name`) |
| Reverse chaining | `_has:Type:ref:param` (one extra nested `_has`) |
| `_include` / `_revinclude` | Direct includes plus `ResourceType:*` and `*:*` wildcards |
| Composite search | Declared composite SearchParameters from registry |
| `_summary` / `_elements` | Response projection at assembly time |
| Full text | Postgres native FTS via indexed text documents |
Expand All @@ -153,7 +154,7 @@ Unsupported semantics return explicit errors (`ErrUnsupportedFeature`, `ErrInval
- Comma-separated values **OR** within one occurrence (`?name=Smith,Jones`)
- `_count` and `_offset` apply to primary matches only (not included resources)
- `_sort` uses registry metadata; tiebreak on resource id
- Chain depth limited to 1; wildcard includes and recursive includes are rejected
- Chain depth is limited to 2 hops; `_include:iterate` remains unsupported

## Index field keys

Expand All @@ -165,6 +166,7 @@ Unsupported semantics return explicit errors (`ErrUnsupportedFeature`, `ErrInval
| `string.*` | `string.family` | Normalized strings |
| `date.*` | `date.birthdate` | Comparable date strings |
| `reference.*` | `reference.patient` | Reference targets (typed/id/canonical forms) |
| `uri.*` | `uri.url` | Canonical/URI strings (stored with string indexes) |
| `composite.*` | `composite.context-type-value` | Composite component values |
| `text.*` | `text.document` | Postgres full-text document |

Expand Down Expand Up @@ -209,7 +211,7 @@ url.Values → ParseQuery → ResolveQuery → BuildPlan → StoreExecut
## Current limits

- Postgres is the primary complete execution backend; SQLite stores indexes and supports basic lookups but not advanced execution
- Chain depth is limited to 1; wildcard/recursive includes are deferred
- Chain depth is limited to 2; recursive `_include:iterate` is deferred
- OpenSearch adapter seam is preserved via `SearchAdvancedExecutor`; not implemented yet
- No HTTP `_search` endpoint in this package
- Custom SearchParameters become searchable after snapshot rebuild and reindex completion
Expand Down
8 changes: 5 additions & 3 deletions pkg/search/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
//
// Postgres-first execution supports:
//
// - _include and _revinclude (direct, non-wildcard)
// - single-hop chained search (e.g. subject.name)
// - _include and _revinclude, including ResourceType:* and *:* wildcards
// - chained search up to two hops (e.g. subject.name, subject.organization.name)
// - reverse chaining via _has:Type:ref:param
// - uri SearchParameters with :below / :above
// - composite SearchParameters from the registry
// - modifiers (:exact, :contains on string; date/number prefixes)
// - _sort on registry-backed parameters plus _id / _lastUpdated
Expand All @@ -33,7 +35,7 @@
//
// RegistryIndexer evaluates each installed SearchParameter expression with pkg/fhirpath,
// normalizes extracted values into typed field keys (token.*, string.*, date.*,
// reference.*, composite.*, text.*), and emits store.SearchIndexEntry rows consumed
// reference.*, uri.*, composite.*, text.*), and emits store.SearchIndexEntry rows consumed
// by store.SearchStore.
//
// # Query semantics
Expand Down
Loading
Loading