Repository navigation
LOINC: an in filter is the union of = each value - #386
Conversation
A relationship filter that names its part (SCALE_TYP = Doc) found the part with Codes.Description = ? COLLATE NOCASE inside every membership query; Description has no index, so each $validate-code read all 257,267 codes. A regex filter (CLASS regex) re-scanned the property's values on every request. Both answers depend only on the database, so the factory now keeps them (most recently used 256, at most 1,000,000 keys in all), and the filter's SQL uses the keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Property in filters kept only the values that were LOINC codes, so ORDER_OBS in Order,Both or CLASSTYPE in 3,4 matched nothing; relationship in filters took part codes only, so SCALE_TYP in Doc,Nar matched nothing; and answers-for in was read as the answers-for relationship (answer lists for a term), not the answers. Each value is now read as = reads it: a property value (CLASSTYPE also by number), a part code or else a part name, an answer list or a term. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Core defines [property] in as "a property value equal to one of the specified strings", so property in keeps the union of = each value (case-insensitive, CLASSTYPE numbers included). No source defines part names in a relationship in, so relationship in takes part codes again, as before. answers-for is not a property, so answers-for in is not defined: doesFilter now says no, and filter() refuses it with a not-supported 422 instead of returning a wrong set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
in filter is the union of = each valuein filter matches any of the listed values
|
I've narrowed this PR to what core FHIR's CodeSystem Filters table defines (https://hl7.org/fhir/R4/codesystem.html#filters; the same row is in R5): I dropped part names in relationship |
|
why is answers-for treated differently? I don't think it should be Also, |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The name lookup of a relationship filter (SCALE_TYP = Doc) was the only filter query that read a whole table. The importer now creates idx_codes_description ON Codes(Description COLLATE NOCASE), which the query uses (SEARCH ... USING COVERING INDEX): on LOINC 2.83 the lookup takes 0.04 ms instead of 18 ms, and the index is 14 MB. The CHANGELOG gives the sqlite3 command that adds it to an existing database. The factory's cache keys a name as NOCASE compares it (ASCII letters lower-cased), so Doc, DOC and doc are looked up once. A provider built without the factory's shared data (sqlite-callback-throws) makes its own cache instead of failing with a TypeError. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…x/loinc-in-filter HealthIntersections#386 builds on HealthIntersections#384: a part name in a relationship in is looked up with HealthIntersections#384's #partKeysByName. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An in filter is now the union of = each value for every filter that takes in: answers-for in (each value an answer list or a term, as answers-for = reads it), and a relationship in takes part names as well as part codes (SCALE_TYP in Doc,Nar), as = does - a legacy form servers must accept. Each name is looked up with HealthIntersections#384's #partKeysByName (one indexed query, kept by the factory), and the filter uses the union of the keys. answers-for in is no longer refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 🤖 Generated with [Claude Code](https://claude.com/claude-code)
in filter matches any of the listed valuesin filter is the union of = each value
|
Agreed.
Tests check that |
Builds on #384. This branch merges
perf/loinc-filter-part-lookup(#384). A part name in a relationshipinis looked up with #384's#partKeysByName, and the name lookup uses #384'sidx_codes_descriptionindex. Merge #384 first, and this PR's own diff is the last commit.Problem
doesFilteracceptsinfor every LOINC property and relationship, and foranswers-for. Butindidn't read its values the way=does:in.#executeFilterQuerykept only the values that are LOINC codes (#commaListOfCodes). Property values (CLASSTYPE, ORDER_OBS, ...) are never LOINC codes, so a propertyinfilter always returned an empty expansion, with no error.ORDER_OBS = OrderandCLASSTYPE = 3worked.inwith part names.SCALE_TYP = Doctakes a part's name, a legacy form that servers are required to accept, butSCALE_TYP in Doc,Nartook part codes only and returned nothing, with no error.answers-for in.answers-foris a relationship type too, soinfell into the generic relationship branch. It returned the answer lists for the listed terms instead of their answers (and nothing when the values were answer lists).$expandof an inline compose against LOINC 2.83 (before = main):=each valueCLASSTYPE in 3,4CLASSTYPE in Claims attachment,SurveysORDER_OBS in Order,BothORDER_OBS in order,BOTHSCALE_TYP in LP32888-7,LP7749-7(part codes)SCALE_TYP in Doc,Nar(part names)SCALE_TYP in doc,NAR,LP7753-9(names in other case, and a code)answers-for in LL1000-0,LL1001-8(answer lists)answers-for in 100002-5,10153-5(terms)answers-for in LL1000-0,10153-5(a list and a term)The "after" and union counts in the rows from
SCALE_TYP in Doc,Nardown come from the provider on 2.83, not a running server: the filter's codes are listed, and one of them is also found byfilterLocate, the$validate-codepath. Their "before" counts come from main's SQL for the filter, run against the same database. Every "after" set is equal to the union, not just the same size.Spec
Core CodeSystem Filters table, the same rows in R4 (https://hl7.org/fhir/R4/codesystem.html#filters), R4B and R5 (https://hl7.org/fhir/R5/codesystem.html#filters):
THO LOINC, Case Sensitivity (https://terminology.hl7.org/7.5.0/en/LOINC.html):
tx-ecosystem IG requirements (https://build.fhir.org/ig/HL7/fhir-tx-ecosystem-ig/en/requirements.html):
Change
tx/cs/cs-loinc.js:X in a,bisX = aorX = bfor everyXthat takesin. Each value is trimmed, and empty values are dropped.in: the values are compared as property values, case-insensitively, as=does.=already takes the CLASSTYPE numbers 1-4, soinmaps them to their names too, andCLASSTYPE in 3,4is the union ofCLASSTYPE = 3andCLASSTYPE = 4.in: a value that is a LOINC code is a part code, as for=. Any other value is a part name:#partKeysByName(LOINC: look up a filter's part name or regex once per factory; index Codes.Description #384) looks it up, in any case, once per factory. The filter's SQL lists the union of the parts' keys (TargetKey IN (...)). A value that matches no part adds nothing, and if none match, the filter is empty.answers-for in: each value is read asanswers-for =reads it. A value startingLLis an answer list, and any other value is a term that answer lists are for. The filter is the union of their answers.CHANGELOG.md: a Fixed entry under Unreleased.Tests
tests/cs/cs-loinc.test.js, "X in a,b is the union of X = a and X = b, listed and checked": 12 cases, covering CLASSTYPE numbers and names, ORDER_OBS (also in other case, with a space), SYSTEM, SCALE_TYP part codes, part names, names in mixed case (Doc,NAR), a name and a code, andanswers-forby answer lists, by terms, and by a list and a term. For each case,=each value is non-empty, and listing and checking give the same set for=. Forin,doesFiltersays yes, and both the codes listed and the codes checked without listing (getPrepContext(false), the$validate-codepath:filterLocateon every code) equal the union.QngiveSCALE_TYP = Qn's codes, and aninwhose values are all unknown (SCALE_TYP, answers-for) is empty when listed and when checked.cs-loinc,cs-loinc-classtype,sqlite-callback-throws,sqlite-provider-close,cs-worker-readonly,ancestors-of,tx/loinc-import-answer-lists): 118 passed, 2 skipped (7 suites)tests/csandtests/tx: 171 suites pass (5 skipped): 3,262 tests pass, 87 skipped (npx jest tests/cs tests/tx, test-cases run separately below)tests/tx/test-cases.test.js: 5,251 of 5,251 pass (validator jar viaFHIRSMITH_VALIDATOR_JAR)🤖 Generated with Claude Code