Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

Commit

Permalink
feat: Add flexibility to searches against static ES mapping (#85) (#89)
Browse files Browse the repository at this point in the history
  • Loading branch information
rsmayda authored Jul 1, 2021
1 parent 59ad535 commit a1e7683
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 34 deletions.
1 change: 1 addition & 0 deletions .github/workflows/unit-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
pull_request:
branches:
- mainline
- v2.x.x
jobs:
unit-test:
name: Unit Test and Linting
Expand Down
17 changes: 10 additions & 7 deletions src/QueryBuilder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function typeQueryWithConditions(
searchParam: SearchParam,
compiledSearchParam: CompiledSearchParam,
searchValue: string,
useKeywordSubFields: boolean,
): any {
let typeQuery: any;
switch (searchParam.type) {
Expand All @@ -27,16 +28,16 @@ function typeQueryWithConditions(
typeQuery = dateQuery(compiledSearchParam, searchValue);
break;
case 'token':
typeQuery = tokenQuery(compiledSearchParam, searchValue);
typeQuery = tokenQuery(compiledSearchParam, searchValue, useKeywordSubFields);
break;
case 'number':
typeQuery = numberQuery(compiledSearchParam, searchValue);
break;
case 'quantity':
typeQuery = quantityQuery(compiledSearchParam, searchValue);
typeQuery = quantityQuery(compiledSearchParam, searchValue, useKeywordSubFields);
break;
case 'reference':
typeQuery = referenceQuery(compiledSearchParam, searchValue);
typeQuery = referenceQuery(compiledSearchParam, searchValue, useKeywordSubFields);
break;
case 'composite':
case 'special':
Expand Down Expand Up @@ -68,9 +69,9 @@ function typeQueryWithConditions(
return typeQuery;
}

function searchParamQuery(searchParam: SearchParam, searchValue: string): any {
function searchParamQuery(searchParam: SearchParam, searchValue: string, useKeywordSubFields: boolean): any {
const queries = searchParam.compiled.map(compiled => {
return typeQueryWithConditions(searchParam, compiled, searchValue);
return typeQueryWithConditions(searchParam, compiled, searchValue, useKeywordSubFields);
});

if (queries.length === 1) {
Expand Down Expand Up @@ -107,6 +108,7 @@ function normalizeQueryParams(queryParams: any): { [key: string]: string[] } {
function searchRequestQuery(
fhirSearchParametersRegistry: FHIRSearchParametersRegistry,
request: TypeSearchRequest,
useKeywordSubFields: boolean,
): any[] {
const { queryParams, resourceType } = request;
return Object.entries(normalizeQueryParams(queryParams))
Expand All @@ -118,20 +120,21 @@ function searchRequestQuery(
`Invalid search parameter '${searchParameter}' for resource type ${resourceType}`,
);
}
return searchValues.map(searchValue => searchParamQuery(fhirSearchParam, searchValue));
return searchValues.map(searchValue => searchParamQuery(fhirSearchParam, searchValue, useKeywordSubFields));
});
}

// eslint-disable-next-line import/prefer-default-export
export const buildQueryForAllSearchParameters = (
fhirSearchParametersRegistry: FHIRSearchParametersRegistry,
request: TypeSearchRequest,
useKeywordSubFields: boolean,
additionalFilters: any[] = [],
): any => {
return {
bool: {
filter: additionalFilters,
must: searchRequestQuery(fhirSearchParametersRegistry, request),
must: searchRequestQuery(fhirSearchParametersRegistry, request, useKeywordSubFields),
},
};
};
Expand Down
47 changes: 41 additions & 6 deletions src/QueryBuilder/typeQueries/quantityQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const quantityParam = fhirSearchParametersRegistry.getSearchParameter('Observati
describe('quantityQuery', () => {
describe('valid inputs', () => {
test('5.4|http://unitsofmeasure.org|mg', () => {
expect(quantityQuery(quantityParam, '5.4|http://unitsofmeasure.org|mg')).toMatchInlineSnapshot(`
expect(quantityQuery(quantityParam, '5.4|http://unitsofmeasure.org|mg', true)).toMatchInlineSnapshot(`
Object {
"bool": Object {
"must": Array [
Expand Down Expand Up @@ -50,7 +50,7 @@ describe('quantityQuery', () => {
`);
});
test('5.40e-3|http://unitsofmeasure.org|g', () => {
expect(quantityQuery(quantityParam, '5.40e-3|http://unitsofmeasure.org|g')).toMatchInlineSnapshot(`
expect(quantityQuery(quantityParam, '5.40e-3|http://unitsofmeasure.org|g', true)).toMatchInlineSnapshot(`
Object {
"bool": Object {
"must": Array [
Expand Down Expand Up @@ -86,7 +86,7 @@ describe('quantityQuery', () => {
`);
});
test('5.4||mg', () => {
expect(quantityQuery(quantityParam, '5.4||mg')).toMatchInlineSnapshot(`
expect(quantityQuery(quantityParam, '5.4||mg', true)).toMatchInlineSnapshot(`
Object {
"bool": Object {
"must": Array [
Expand Down Expand Up @@ -114,7 +114,7 @@ describe('quantityQuery', () => {
`);
});
test('5.4', () => {
expect(quantityQuery(quantityParam, '5.4')).toMatchInlineSnapshot(`
expect(quantityQuery(quantityParam, '5.4', true)).toMatchInlineSnapshot(`
Object {
"range": Object {
"valueQuantity.value": Object {
Expand All @@ -126,7 +126,7 @@ describe('quantityQuery', () => {
`);
});
test('le5.4|http://unitsofmeasure.org|mg', () => {
expect(quantityQuery(quantityParam, 'le5.4|http://unitsofmeasure.org|mg')).toMatchInlineSnapshot(`
expect(quantityQuery(quantityParam, 'le5.4|http://unitsofmeasure.org|mg', true)).toMatchInlineSnapshot(`
Object {
"bool": Object {
"must": Array [
Expand Down Expand Up @@ -160,6 +160,41 @@ describe('quantityQuery', () => {
}
`);
});
test('le5.4|http://unitsofmeasure.org|mg with no keyword', () => {
expect(quantityQuery(quantityParam, 'le5.4|http://unitsofmeasure.org|mg', false)).toMatchInlineSnapshot(`
Object {
"bool": Object {
"must": Array [
Object {
"range": Object {
"valueQuantity.value": Object {
"lte": 5.4,
},
},
},
Object {
"multi_match": Object {
"fields": Array [
"valueQuantity.code",
],
"lenient": true,
"query": "mg",
},
},
Object {
"multi_match": Object {
"fields": Array [
"valueQuantity.system",
],
"lenient": true,
"query": "http://unitsofmeasure.org",
},
},
],
},
}
`);
});
});

describe('invalid inputs', () => {
Expand All @@ -171,7 +206,7 @@ describe('quantityQuery', () => {
['100xxx|system|code'],
['100e-2x|system|code'],
]).test('%s', param => {
expect(() => quantityQuery(quantityParam, param)).toThrow(InvalidSearchParameterError);
expect(() => quantityQuery(quantityParam, param, true)).toThrow(InvalidSearchParameterError);
});
});
});
16 changes: 12 additions & 4 deletions src/QueryBuilder/typeQueries/quantityQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,27 @@ export const parseQuantitySearchParam = (param: string): QuantitySearchParameter
};
};

export const quantityQuery = (compiledSearchParam: CompiledSearchParam, value: string): any => {
export const quantityQuery = (
compiledSearchParam: CompiledSearchParam,
value: string,
useKeywordSubFields: boolean,
): any => {
const { prefix, implicitRange, number, system, code } = parseQuantitySearchParam(value);
const queries = [prefixRangeNumber(prefix, number, implicitRange, `${compiledSearchParam.path}.value`)];
const keywordSuffix = useKeywordSubFields ? '.keyword' : '';

if (!isEmpty(system) && !isEmpty(code)) {
queries.push({
multi_match: {
fields: [`${compiledSearchParam.path}.code.keyword`],
fields: [`${compiledSearchParam.path}.code${keywordSuffix}`],
query: code,
lenient: true,
},
});

queries.push({
multi_match: {
fields: [`${compiledSearchParam.path}.system.keyword`],
fields: [`${compiledSearchParam.path}.system${keywordSuffix}`],
query: system,
lenient: true,
},
Expand All @@ -68,7 +73,10 @@ export const quantityQuery = (compiledSearchParam: CompiledSearchParam, value: s
// https://www.hl7.org/fhir/search.html#quantity
queries.push({
multi_match: {
fields: [`${compiledSearchParam.path}.code.keyword`, `${compiledSearchParam.path}.unit.keyword`],
fields: [
`${compiledSearchParam.path}.code${keywordSuffix}`,
`${compiledSearchParam.path}.unit${keywordSuffix}`,
],
query: code,
lenient: true,
},
Expand Down
17 changes: 15 additions & 2 deletions src/QueryBuilder/typeQueries/referenceQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ const fhirSearchParametersRegistry = new FHIRSearchParametersRegistry('4.0.1');
const organizationParam = fhirSearchParametersRegistry.getSearchParameter('Patient', 'organization')!.compiled[0];

describe('referenceQuery', () => {
test('simple value', () => {
expect(referenceQuery(organizationParam, 'Organization/111')).toMatchInlineSnapshot(`
test('simple value; with keyword', () => {
expect(referenceQuery(organizationParam, 'Organization/111', true)).toMatchInlineSnapshot(`
Object {
"multi_match": Object {
"fields": Array [
Expand All @@ -23,4 +23,17 @@ describe('referenceQuery', () => {
}
`);
});
test('simple value; without keyword', () => {
expect(referenceQuery(organizationParam, 'Organization/111', false)).toMatchInlineSnapshot(`
Object {
"multi_match": Object {
"fields": Array [
"managingOrganization.reference",
],
"lenient": true,
"query": "Organization/111",
},
}
`);
});
});
6 changes: 4 additions & 2 deletions src/QueryBuilder/typeQueries/referenceQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
import { CompiledSearchParam } from '../../FHIRSearchParametersRegistry';

// eslint-disable-next-line import/prefer-default-export
export function referenceQuery(compiled: CompiledSearchParam, value: string): any {
const fields = [`${compiled.path}.reference.keyword`];
export function referenceQuery(compiled: CompiledSearchParam, value: string, useKeywordSubFields: boolean): any {
const keywordSuffix = useKeywordSubFields ? '.keyword' : '';

const fields = [`${compiled.path}.reference${keywordSuffix}`];
return {
multi_match: {
fields,
Expand Down
40 changes: 36 additions & 4 deletions src/QueryBuilder/typeQueries/tokenQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe('parseTokenSearchParam', () => {

describe('tokenQuery', () => {
test('system|code', () => {
expect(tokenQuery(identifierParam, 'http://acme.org/patient|2345')).toMatchInlineSnapshot(`
expect(tokenQuery(identifierParam, 'http://acme.org/patient|2345', true)).toMatchInlineSnapshot(`
Object {
"bool": Object {
"must": Array [
Expand Down Expand Up @@ -113,7 +113,7 @@ describe('tokenQuery', () => {
`);
});
test('system|', () => {
expect(tokenQuery(identifierParam, 'http://acme.org/patient')).toMatchInlineSnapshot(`
expect(tokenQuery(identifierParam, 'http://acme.org/patient', true)).toMatchInlineSnapshot(`
Object {
"multi_match": Object {
"fields": Array [
Expand All @@ -129,7 +129,7 @@ describe('tokenQuery', () => {
`);
});
test('|code', () => {
expect(tokenQuery(identifierParam, '|2345')).toMatchInlineSnapshot(`
expect(tokenQuery(identifierParam, '|2345', true)).toMatchInlineSnapshot(`
Object {
"bool": Object {
"must": Array [
Expand Down Expand Up @@ -160,7 +160,7 @@ describe('tokenQuery', () => {
`);
});
test('code', () => {
expect(tokenQuery(identifierParam, 'http://acme.org/patient|2345')).toMatchInlineSnapshot(`
expect(tokenQuery(identifierParam, 'http://acme.org/patient|2345', true)).toMatchInlineSnapshot(`
Object {
"bool": Object {
"must": Array [
Expand Down Expand Up @@ -191,4 +191,36 @@ describe('tokenQuery', () => {
}
`);
});
test('code; without keyword', () => {
expect(tokenQuery(identifierParam, 'http://acme.org/patient|2345', false)).toMatchInlineSnapshot(`
Object {
"bool": Object {
"must": Array [
Object {
"multi_match": Object {
"fields": Array [
"identifier.system",
"identifier.coding.system",
],
"lenient": true,
"query": "http://acme.org/patient",
},
},
Object {
"multi_match": Object {
"fields": Array [
"identifier.code",
"identifier.coding.code",
"identifier.value",
"identifier",
],
"lenient": true,
"query": "2345",
},
},
],
},
}
`);
});
});
13 changes: 7 additions & 6 deletions src/QueryBuilder/typeQueries/tokenQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ export const parseTokenSearchParam = (param: string): TokenSearchParameter => {
return { system, code, explicitNoSystemProperty };
};

export function tokenQuery(compiled: CompiledSearchParam, value: string): any {
export function tokenQuery(compiled: CompiledSearchParam, value: string, useKeywordSubFields: boolean): any {
const { system, code, explicitNoSystemProperty } = parseTokenSearchParam(value);
const queries = [];
const keywordSuffix = useKeywordSubFields ? '.keyword' : '';

// Token search params are used for many different field types. Search is not aware of the types of the fields in FHIR resources.
// The field type is specified in StructureDefinition, but not in SearchParameter.
Expand All @@ -50,8 +51,8 @@ export function tokenQuery(compiled: CompiledSearchParam, value: string): any {
// See: https://www.hl7.org/fhir/search.html#token
if (system !== undefined) {
const fields = [
`${compiled.path}.system.keyword`, // Coding, Identifier
`${compiled.path}.coding.system.keyword`, // CodeableConcept
`${compiled.path}.system${keywordSuffix}`, // Coding, Identifier
`${compiled.path}.coding.system${keywordSuffix}`, // CodeableConcept
];

queries.push({
Expand All @@ -65,9 +66,9 @@ export function tokenQuery(compiled: CompiledSearchParam, value: string): any {

if (code !== undefined) {
const fields = [
`${compiled.path}.code.keyword`, // Coding
`${compiled.path}.coding.code.keyword`, // CodeableConcept
`${compiled.path}.value.keyword`, // Identifier, ContactPoint
`${compiled.path}.code${keywordSuffix}`, // Coding
`${compiled.path}.coding.code${keywordSuffix}`, // CodeableConcept
`${compiled.path}.value${keywordSuffix}`, // Identifier, ContactPoint
`${compiled.path}`, // code, boolean, uri, string
];

Expand Down
Loading

0 comments on commit a1e7683

Please sign in to comment.