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

feat: Add flexibility to searches against static ES mapping #89

Merged
merged 2 commits into from
Jul 1, 2021
Merged
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 .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