001/*- 002 * #%L 003 * HAPI FHIR JPA Model 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.model.search; 021 022import ca.uhn.fhir.jpa.model.entity.StorageSettings; 023import ca.uhn.fhir.jpa.model.util.UcumServiceUtil; 024import org.apache.commons.lang3.StringUtils; 025import org.fhir.ucum.Pair; 026import org.hibernate.search.engine.backend.document.DocumentElement; 027import org.hl7.fhir.instance.model.api.IBaseCoding; 028import org.slf4j.Logger; 029import org.slf4j.LoggerFactory; 030 031import java.math.BigDecimal; 032import java.util.Collection; 033import java.util.Set; 034 035public class HSearchIndexWriter { 036 private static final Logger ourLog = LoggerFactory.getLogger(HSearchIndexWriter.class); 037 038 public static final String NESTED_SEARCH_PARAM_ROOT = "nsp"; 039 public static final String SEARCH_PARAM_ROOT = "sp"; 040 public static final String INDEX_TYPE_STRING = "string"; 041 public static final String IDX_STRING_NORMALIZED = "norm"; 042 public static final String IDX_STRING_EXACT = "exact"; 043 public static final String IDX_STRING_TEXT = "text"; 044 public static final String IDX_STRING_LOWER = "lower"; 045 046 public static final String INDEX_TYPE_TOKEN = "token"; 047 public static final String TOKEN_CODE = "code"; 048 public static final String TOKEN_SYSTEM = "system"; 049 public static final String TOKEN_SYSTEM_CODE = "code-system"; 050 public static final String INDEX_TYPE_QUANTITY = "quantity"; 051 052 // numeric 053 public static final String VALUE_FIELD = "value"; 054 public static final String QTY_CODE = TOKEN_CODE; 055 public static final String QTY_SYSTEM = TOKEN_SYSTEM; 056 public static final String QTY_VALUE = VALUE_FIELD; 057 public static final String QTY_CODE_NORM = "code-norm"; 058 public static final String QTY_VALUE_NORM = "value-norm"; 059 060 public static final String URI_VALUE = "uri-value"; 061 062 public static final String NUMBER_VALUE = "number-value"; 063 064 public static final String DATE_LOWER_ORD = "lower-ord"; 065 public static final String DATE_LOWER = "lower"; 066 public static final String DATE_UPPER_ORD = "upper-ord"; 067 public static final String DATE_UPPER = "upper"; 068 069 final HSearchElementCache myNodeCache; 070 final StorageSettings myStorageSettings; 071 072 HSearchIndexWriter(StorageSettings theStorageSettings, DocumentElement theRoot) { 073 myStorageSettings = theStorageSettings; 074 myNodeCache = new HSearchElementCache(theRoot); 075 } 076 077 public DocumentElement getSearchParamIndexNode(String theSearchParamName, String theIndexType) { 078 return myNodeCache.getObjectElement(SEARCH_PARAM_ROOT, theSearchParamName, theIndexType); 079 } 080 081 public static HSearchIndexWriter forRoot(StorageSettings theStorageSettings, DocumentElement theDocument) { 082 return new HSearchIndexWriter(theStorageSettings, theDocument); 083 } 084 085 public void writeStringIndex(String theSearchParam, String theValue) { 086 DocumentElement stringIndexNode = getSearchParamIndexNode(theSearchParam, INDEX_TYPE_STRING); 087 088 // we are assuming that our analyzer matches 089 // StringUtil.normalizeStringForSearchIndexing(theValue).toLowerCase(Locale.ROOT)) 090 writeBasicStringFields(stringIndexNode, theValue); 091 addDocumentValue(stringIndexNode, IDX_STRING_EXACT, theValue); 092 addDocumentValue(stringIndexNode, IDX_STRING_TEXT, theValue); 093 addDocumentValue(stringIndexNode, IDX_STRING_LOWER, theValue); 094 095 ourLog.debug("Adding Search Param Text: {} -- {}", theSearchParam, theValue); 096 } 097 098 public void writeBasicStringFields(DocumentElement theIndexNode, String theValue) { 099 addDocumentValue(theIndexNode, IDX_STRING_NORMALIZED, theValue); 100 } 101 102 public void writeTokenIndex(String theSearchParam, IBaseCoding theValue) { 103 DocumentElement nestedRoot = myNodeCache.getObjectElement(NESTED_SEARCH_PARAM_ROOT); 104 DocumentElement nestedSpNode = nestedRoot.addObject(theSearchParam); 105 DocumentElement nestedTokenNode = nestedSpNode.addObject(INDEX_TYPE_TOKEN); 106 107 writeTokenFields(nestedTokenNode, theValue); 108 109 if (StringUtils.isNotEmpty(theValue.getDisplay())) { 110 DocumentElement nestedStringNode = nestedSpNode.addObject(INDEX_TYPE_STRING); 111 addDocumentValue(nestedStringNode, IDX_STRING_TEXT, theValue.getDisplay()); 112 } 113 114 DocumentElement tokenIndexNode = getSearchParamIndexNode(theSearchParam, INDEX_TYPE_TOKEN); 115 writeTokenFields(tokenIndexNode, theValue); 116 ourLog.debug("Adding Search Param Token: {} -- {}", theSearchParam, theValue); 117 } 118 119 public void writeTokenFields(DocumentElement theDocumentElement, IBaseCoding theValue) { 120 addDocumentValue(theDocumentElement, TOKEN_CODE, theValue.getCode()); 121 addDocumentValue(theDocumentElement, TOKEN_SYSTEM, theValue.getSystem()); 122 addDocumentValue(theDocumentElement, TOKEN_SYSTEM_CODE, theValue.getSystem() + "|" + theValue.getCode()); 123 } 124 125 private void addDocumentValue(DocumentElement theDocumentElement, String theKey, Object theValue) { 126 if (theValue != null) { 127 theDocumentElement.addValue(theKey, theValue); 128 } 129 } 130 131 public void writeReferenceIndex(String theSearchParam, String theValue) { 132 DocumentElement referenceIndexNode = getSearchParamIndexNode(theSearchParam, "reference"); 133 addDocumentValue(referenceIndexNode, VALUE_FIELD, theValue); 134 ourLog.trace("Adding Search Param Reference: {} -- {}", theSearchParam, theValue); 135 } 136 137 public void writeDateIndex(String theSearchParam, DateSearchIndexData theValue) { 138 DocumentElement dateIndexNode = getSearchParamIndexNode(theSearchParam, "dt"); 139 writeDateFields(dateIndexNode, theValue); 140 141 ourLog.trace("Adding Search Param Date. param: {} -- {}", theSearchParam, theValue); 142 } 143 144 public void writeDateFields(DocumentElement dateIndexNode, DateSearchIndexData theValue) { 145 // Lower bound 146 addDocumentValue(dateIndexNode, DATE_LOWER_ORD, theValue.getLowerBoundOrdinal()); 147 addDocumentValue(dateIndexNode, DATE_LOWER, theValue.getLowerBoundDate().toInstant()); 148 // Upper bound 149 addDocumentValue(dateIndexNode, DATE_UPPER_ORD, theValue.getUpperBoundOrdinal()); 150 addDocumentValue(dateIndexNode, DATE_UPPER, theValue.getUpperBoundDate().toInstant()); 151 } 152 153 public void writeQuantityIndex(String theSearchParam, QuantitySearchIndexData theValue) { 154 DocumentElement nestedRoot = myNodeCache.getObjectElement(NESTED_SEARCH_PARAM_ROOT); 155 156 DocumentElement nestedSpNode = nestedRoot.addObject(theSearchParam); 157 DocumentElement nestedQtyNode = nestedSpNode.addObject(INDEX_TYPE_QUANTITY); 158 159 ourLog.trace("Adding Search Param Quantity: {} -- {}", theSearchParam, theValue); 160 writeQuantityFields(nestedQtyNode, theValue); 161 } 162 163 public void writeQuantityFields(DocumentElement nestedQtyNode, QuantitySearchIndexData theValue) { 164 addDocumentValue(nestedQtyNode, QTY_CODE, theValue.getCode()); 165 addDocumentValue(nestedQtyNode, QTY_SYSTEM, theValue.getSystem()); 166 addDocumentValue(nestedQtyNode, QTY_VALUE, theValue.getValue()); 167 168 if (!myStorageSettings.getNormalizedQuantitySearchLevel().storageOrSearchSupported()) { 169 return; 170 } 171 172 // -- convert the value/unit to the canonical form if any 173 Pair canonicalForm = UcumServiceUtil.getCanonicalForm( 174 theValue.getSystem(), BigDecimal.valueOf(theValue.getValue()), theValue.getCode()); 175 if (canonicalForm == null) { 176 return; 177 } 178 179 double canonicalValue = Double.parseDouble(canonicalForm.getValue().asDecimal()); 180 String canonicalUnits = canonicalForm.getCode(); 181 182 addDocumentValue(nestedQtyNode, QTY_CODE_NORM, canonicalUnits); 183 addDocumentValue(nestedQtyNode, QTY_VALUE_NORM, canonicalValue); 184 } 185 186 public void writeUriIndex(String theParamName, Collection<String> theUriValueCollection) { 187 DocumentElement uriNode = 188 myNodeCache.getObjectElement(SEARCH_PARAM_ROOT).addObject(theParamName); 189 for (String uriSearchIndexValue : theUriValueCollection) { 190 ourLog.trace("Adding Search Param Uri: {} -- {}", theParamName, uriSearchIndexValue); 191 writeUriFields(uriNode, uriSearchIndexValue); 192 } 193 } 194 195 public void writeUriFields(DocumentElement uriNode, String uriSearchIndexValue) { 196 addDocumentValue(uriNode, URI_VALUE, uriSearchIndexValue); 197 } 198 199 public void writeNumberIndex(String theParamName, Collection<BigDecimal> theNumberValueCollection) { 200 DocumentElement numberNode = 201 myNodeCache.getObjectElement(SEARCH_PARAM_ROOT).addObject(theParamName); 202 for (BigDecimal numberSearchIndexValue : theNumberValueCollection) { 203 ourLog.trace("Adding Search Param Number: {} -- {}", theParamName, numberSearchIndexValue); 204 writeNumberFields(numberNode, numberSearchIndexValue); 205 } 206 } 207 208 public void writeNumberFields(DocumentElement numberNode, BigDecimal numberSearchIndexValue) { 209 addDocumentValue(numberNode, NUMBER_VALUE, numberSearchIndexValue.doubleValue()); 210 } 211 212 /** 213 * @param ignoredParamName unused - for consistent api 214 * @param theCompositeSearchIndexData extracted index data for this sp 215 */ 216 public void writeCompositeIndex( 217 String ignoredParamName, Set<CompositeSearchIndexData> theCompositeSearchIndexData) { 218 // must be nested. 219 for (CompositeSearchIndexData compositeSearchIndexDatum : theCompositeSearchIndexData) { 220 compositeSearchIndexDatum.writeIndexEntry(this, myNodeCache); 221 } 222 } 223}