├── test-data ├── .gitignore ├── .settings │ ├── org.eclipse.m2e.core.prefs │ ├── org.eclipse.core.resources.prefs │ └── org.eclipse.jdt.core.prefs ├── .project ├── .classpath ├── src │ └── test │ │ ├── java │ │ └── io │ │ │ └── puntanegra │ │ │ └── fhir │ │ │ └── index │ │ │ ├── FhirIndexServiceTest.java │ │ │ ├── InitCassandraKeyspace.java │ │ │ ├── NameGenerator.java │ │ │ └── FhirTestDataTest.java │ │ └── resources │ │ └── fhir │ │ ├── observation_example001.json │ │ └── patient_f001.json └── pom.xml ├── fhir-index-plugin ├── .gitignore ├── src │ ├── main │ │ └── java │ │ │ └── io │ │ │ └── puntanegra │ │ │ └── fhir │ │ │ └── index │ │ │ ├── search │ │ │ ├── extractor │ │ │ │ ├── SearchParameterException.java │ │ │ │ ├── SearchParameterExtractorFactory.java │ │ │ │ ├── QuantitySearchParameterExtractor.java │ │ │ │ ├── DatesSearchParameterExtractor.java │ │ │ │ ├── UriSearchParameterExtractor.java │ │ │ │ ├── AbstractSearchParameterExtractor.java │ │ │ │ ├── NumberSearchParameterExtractor.java │ │ │ │ ├── StringsSearchParameterExtractor.java │ │ │ │ └── TokenSearchParameterExtractor.java │ │ │ ├── datatypes │ │ │ │ ├── SearchParamTypes.java │ │ │ │ ├── SearchParamString.java │ │ │ │ ├── SearchParamNumber.java │ │ │ │ ├── SearchParamToken.java │ │ │ │ ├── SearchParamDates.java │ │ │ │ ├── AbstractSearchParam.java │ │ │ │ └── SearchParamQuantity.java │ │ │ ├── FhirContextHelper.java │ │ │ ├── SearchParamExtractor.java │ │ │ └── SearchParamExtractorHelper.java │ │ │ ├── config │ │ │ ├── ResourceOptions.java │ │ │ └── IndexOptions.java │ │ │ ├── lucene │ │ │ ├── NoIDFSimilarity.java │ │ │ ├── LuceneRAMIndex.java │ │ │ └── LuceneDocumentIterator.java │ │ │ ├── FhirIndexException.java │ │ │ ├── cache │ │ │ ├── SearchCacheUpdater.java │ │ │ ├── SearchCache.java │ │ │ └── SearchCacheEntry.java │ │ │ ├── util │ │ │ ├── JsonSerializer.java │ │ │ ├── TimeCounter.java │ │ │ ├── TaskQueue.java │ │ │ └── ByteBufferUtils.java │ │ │ ├── mapper │ │ │ ├── KeyEntry.java │ │ │ ├── FhirMapper.java │ │ │ ├── TokenMapper.java │ │ │ ├── PartitionMapper.java │ │ │ └── KeyMapper.java │ │ │ ├── FhirIndexIndexer.java │ │ │ ├── FhirIndexSearcher.java │ │ │ └── FhirIndex.java │ └── test │ │ ├── java │ │ └── io │ │ │ └── puntanegra │ │ │ └── fhir │ │ │ └── index │ │ │ ├── SearchOptionsTest.java │ │ │ └── SearchParamExtractorTest.java │ │ └── resources │ │ └── fhir │ │ ├── patient_f001.json │ │ └── observation_example001.json └── pom.xml ├── Dockerfile ├── docker-entrypoint.sh └── pom.xml /test-data/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | -------------------------------------------------------------------------------- /fhir-index-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | .classpath 3 | .project 4 | .settings -------------------------------------------------------------------------------- /test-data/.settings/org.eclipse.m2e.core.prefs: -------------------------------------------------------------------------------- 1 | activeProfiles= 2 | eclipse.preferences.version=1 3 | resolveWorkspaceProjects=true 4 | version=1 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # vim:set ft=dockerfile: 2 | FROM cassandra:3.0.4 3 | 4 | ARG VERSION 5 | 6 | COPY fhir-index-plugin/target/fhir-index-plugin-${VERSION}.jar /usr/share/cassandra/lib/ 7 | -------------------------------------------------------------------------------- /test-data/.settings/org.eclipse.core.resources.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | encoding//src/test/java=UTF-8 3 | encoding//src/test/resources=UTF-8 4 | encoding/=UTF-8 5 | -------------------------------------------------------------------------------- /test-data/.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 3 | org.eclipse.jdt.core.compiler.compliance=1.8 4 | org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning 5 | org.eclipse.jdt.core.compiler.source=1.8 6 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/search/extractor/SearchParameterException.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.search.extractor; 2 | 3 | public class SearchParameterException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = -3757051848365800796L; 6 | 7 | public SearchParameterException(String message) { 8 | super(message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/search/datatypes/SearchParamTypes.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.search.datatypes; 2 | 3 | /** 4 | * Search parameter types. These types are the same defined in HAPI 5 | * RestSearchParameterTypeEnum. 6 | * 7 | * @author Jorge L. Middleton {@literal } 8 | * 9 | */ 10 | public enum SearchParamTypes { 11 | NUMBER, DATE, STRING, TOKEN, REFERENCE, COMPOSITE, QUANTITY, URI; 12 | } 13 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/search/FhirContextHelper.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.search; 2 | 3 | import ca.uhn.fhir.context.FhirContext; 4 | import ca.uhn.fhir.context.FhirVersionEnum; 5 | 6 | public class FhirContextHelper { 7 | 8 | private static FhirContext ctx; 9 | 10 | public static FhirContext getContext(FhirVersionEnum fhirVersion) { 11 | if (ctx == null) { 12 | ctx = new FhirContext(fhirVersion); 13 | } 14 | return ctx; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test-data/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | test-data 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.m2e.core.maven2Builder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.m2e.core.maven2Nature 22 | 23 | 24 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/search/SearchParamExtractor.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.search; 2 | 3 | import java.util.Set; 4 | 5 | import org.hl7.fhir.instance.model.api.IBaseResource; 6 | 7 | import ca.uhn.fhir.context.RuntimeSearchParam; 8 | import io.puntanegra.fhir.index.search.datatypes.AbstractSearchParam; 9 | 10 | /** 11 | * Classes of this type are used to extract information from a Fhir 12 | * {@link IBaseResource}.
13 | * Each type knows how to extract specific information based on the type. 14 | * 15 | * @author Jorge L. Middleton {@literal } 16 | * 17 | */ 18 | public interface SearchParamExtractor { 19 | 20 | /** 21 | * Extracts the search parameter values from the {@link IBaseResource}. 22 | * 23 | * @param instance 24 | * @param searchParam 25 | * @return 26 | */ 27 | public Set extractValues(IBaseResource instance, RuntimeSearchParam searchParam); 28 | } 29 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/search/datatypes/SearchParamString.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.search.datatypes; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | import org.apache.lucene.document.Field; 7 | import org.apache.lucene.document.TextField; 8 | 9 | public class SearchParamString extends AbstractSearchParam { 10 | 11 | private String value; 12 | 13 | public SearchParamString(String name, String path, SearchParamTypes type, String value) { 14 | super(name, path, type); 15 | this.value = value; 16 | } 17 | 18 | @Override 19 | public boolean hasValue() { 20 | return value != null; 21 | } 22 | 23 | public String getValue() { 24 | return value; 25 | } 26 | 27 | public void setValue(String str) { 28 | this.value = str; 29 | } 30 | 31 | public String getValueAsString() { 32 | return this.value; 33 | } 34 | 35 | @Override 36 | public List createIndexedFields() { 37 | Field field = new TextField(this.name, this.value, Field.Store.NO); 38 | return Arrays.asList(field); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/search/datatypes/SearchParamNumber.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.search.datatypes; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | import org.apache.lucene.document.DoubleField; 7 | import org.apache.lucene.document.Field; 8 | 9 | public class SearchParamNumber extends AbstractSearchParam { 10 | 11 | private Double value; 12 | 13 | public SearchParamNumber(String name, String path, SearchParamTypes type, Double value) { 14 | super(name, path, type); 15 | this.value = value; 16 | } 17 | 18 | @Override 19 | public boolean hasValue() { 20 | return this.value != null; 21 | } 22 | 23 | public Double getValue() { 24 | return this.value; 25 | } 26 | 27 | @Override 28 | public String getValueAsString() { 29 | if (this.value != null) { 30 | return this.value.toString(); 31 | } 32 | return null; 33 | } 34 | 35 | @Override 36 | public List createIndexedFields() { 37 | Field field = new DoubleField(name, this.value, Field.Store.NO); 38 | field.setBoost(0.1f); 39 | return Arrays.asList(field); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test-data/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/config/ResourceOptions.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.config; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.Set; 6 | 7 | import org.apache.lucene.analysis.Analyzer; 8 | import org.apache.lucene.analysis.core.WhitespaceAnalyzer; 9 | import org.codehaus.jackson.annotate.JsonCreator; 10 | import org.codehaus.jackson.annotate.JsonProperty; 11 | 12 | /** 13 | * Stores the default Lucene analyzer and search resources defined during index 14 | * creation. 15 | * 16 | * @author Jorge L. Middleton {@literal } 17 | * 18 | */ 19 | public class ResourceOptions { 20 | 21 | /** The wrapping all-in-one {@link Analyzer}. */ 22 | @JsonProperty("default_analyzer") 23 | public final Analyzer defaultAnalyzer; 24 | 25 | @JsonProperty("resources") 26 | public final Map> resources = new HashMap>(); 27 | 28 | @JsonCreator 29 | public ResourceOptions(@JsonProperty("default_analyzer") String analyzer, 30 | @JsonProperty("resources") Map> resources) 31 | throws InstantiationException, IllegalAccessException, ClassNotFoundException { 32 | if (analyzer == null) { 33 | this.defaultAnalyzer = new WhitespaceAnalyzer(); 34 | } else { 35 | this.defaultAnalyzer = (Analyzer) Class.forName(analyzer).newInstance(); 36 | } 37 | 38 | if (resources != null) { 39 | this.resources.putAll(resources); 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/lucene/NoIDFSimilarity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to STRATIO (C) under one or more contributor license agreements. 3 | * See the NOTICE file distributed with this work for additional information 4 | * regarding copyright ownership. The STRATIO (C) licenses this file 5 | * to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | package io.puntanegra.fhir.index.lucene; 20 | 21 | import org.apache.lucene.search.similarities.DefaultSimilarity; 22 | 23 | /** 24 | * {@link DefaultSimilarity} that ignores the inverse document frequency, doing the similarity independent of the index 25 | * context. 26 | * 27 | * @author Andres de la Pena {@literal } 28 | */ 29 | class NoIDFSimilarity extends DefaultSimilarity { 30 | 31 | /** 32 | * Returns a constant neutral score value of {@code 1.0}. 33 | */ 34 | @Override 35 | public float idf(long docFreq, long numDocs) { 36 | return 1.0f; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/search/extractor/SearchParameterExtractorFactory.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.search.extractor; 2 | 3 | import ca.uhn.fhir.context.FhirContext; 4 | import io.puntanegra.fhir.index.search.SearchParamExtractor; 5 | import io.puntanegra.fhir.index.search.datatypes.SearchParamTypes; 6 | 7 | public class SearchParameterExtractorFactory { 8 | private static SearchParameterExtractorFactory instance; 9 | private FhirContext ctx; 10 | 11 | protected SearchParameterExtractorFactory(FhirContext ctx) { 12 | this.ctx = ctx; 13 | 14 | } 15 | 16 | public static SearchParameterExtractorFactory getInstance(FhirContext ctx) { 17 | if (instance == null) { 18 | instance = new SearchParameterExtractorFactory(ctx); 19 | } 20 | return instance; 21 | } 22 | 23 | public SearchParamExtractor getParameterExtractor(SearchParamTypes type) { 24 | if (SearchParamTypes.TOKEN == type) { 25 | return new TokenSearchParameterExtractor(this.ctx); 26 | } else if (SearchParamTypes.URI == type || SearchParamTypes.REFERENCE == type) { 27 | return new UriSearchParameterExtractor(this.ctx); 28 | } else if (SearchParamTypes.QUANTITY == type) { 29 | return new QuantitySearchParameterExtractor(this.ctx); 30 | } else if (SearchParamTypes.NUMBER == type) { 31 | return new NumberSearchParameterExtractor(this.ctx); 32 | } else if (SearchParamTypes.DATE == type) { 33 | return new DatesSearchParameterExtractor(this.ctx); 34 | } else { 35 | return new StringsSearchParameterExtractor(this.ctx); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/FhirIndexException.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index; 2 | 3 | /** 4 | * {@code RuntimeException} to be thrown when there are Lucene {@link FhirIndex} 5 | * -related errors. 6 | * 7 | * @author Andres de la Pena {@literal } 8 | */ 9 | public class FhirIndexException extends RuntimeException { 10 | 11 | private static final long serialVersionUID = 2532456234653465436L; 12 | 13 | /** 14 | * Constructs a new index exception with the specified formatted detail 15 | * message. 16 | * 17 | * @param message 18 | * the detail message. 19 | * @param args 20 | * arguments referenced by the format specifiers in the format 21 | * message 22 | */ 23 | public FhirIndexException(String message, Object... args) { 24 | super(String.format(message, args)); 25 | } 26 | 27 | /** 28 | * Constructs a new index exception with the specified formatted detail 29 | * message. 30 | * 31 | * @param cause 32 | * the cause 33 | * @param message 34 | * the detail message 35 | * @param args 36 | * arguments referenced by the format specifiers in the format 37 | * message 38 | */ 39 | public FhirIndexException(Throwable cause, String message, Object... args) { 40 | super(String.format(message, args), cause); 41 | } 42 | 43 | /** 44 | * Constructs a new index exception with the specified cause. 45 | * 46 | * @param cause 47 | * the cause 48 | */ 49 | public FhirIndexException(Throwable cause) { 50 | super(cause); 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /fhir-index-plugin/src/test/java/io/puntanegra/fhir/index/SearchOptionsTest.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertTrue; 5 | import static org.junit.Assert.fail; 6 | 7 | import java.io.IOException; 8 | import java.util.Map; 9 | import java.util.Set; 10 | 11 | import org.apache.lucene.analysis.Analyzer; 12 | import org.apache.lucene.analysis.en.EnglishAnalyzer; 13 | import org.junit.Test; 14 | 15 | import io.puntanegra.fhir.index.config.ResourceOptions; 16 | import io.puntanegra.fhir.index.util.JsonSerializer; 17 | 18 | public class SearchOptionsTest { 19 | 20 | @Test 21 | public void testConfiguration() { 22 | //@formatter:off 23 | String json = "{" + 24 | " \"default_analyzer\" : \"org.apache.lucene.analysis.en.EnglishAnalyzer\"," + 25 | " \"resources\" : {"+ 26 | " \"Patient\" : [\"family\", \"email\"],"+ 27 | " \"Observation\" : [\"code\", \"value-quantity\"]"+ 28 | " }"+ 29 | "}"; 30 | //@formatter:on 31 | 32 | ResourceOptions options; 33 | try { 34 | options = JsonSerializer.fromString(json, ResourceOptions.class); 35 | 36 | Analyzer defaultAnalyzer = options.defaultAnalyzer; 37 | assertTrue("Expected english analyzer", defaultAnalyzer instanceof EnglishAnalyzer); 38 | 39 | Map> resources = options.resources; 40 | assertEquals("Expected 2", 2, resources.size()); 41 | 42 | resources.forEach((k, v) -> { 43 | System.out.println(k + "=" + v); 44 | assertEquals(2, v.size()); 45 | v.forEach(p -> { 46 | System.out.println(p); 47 | }); 48 | }); 49 | 50 | } catch (IOException e) { 51 | fail(e.getMessage()); 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # first arg is `-f` or `--some-option` 5 | if [ "${1:0:1}" = '-' ]; then 6 | set -- cassandra -f "$@" 7 | fi 8 | 9 | # allow the container to be started with `--user` 10 | if [ "$1" = 'cassandra' -a "$(id -u)" = '0' ]; then 11 | chown -R cassandra /var/lib/cassandra /var/log/cassandra "$CASSANDRA_CONFIG" 12 | exec gosu cassandra "$BASH_SOURCE" "$@" 13 | fi 14 | 15 | if [ "$1" = 'cassandra' ]; then 16 | : ${CASSANDRA_RPC_ADDRESS='0.0.0.0'} 17 | 18 | : ${CASSANDRA_LISTEN_ADDRESS='auto'} 19 | if [ "$CASSANDRA_LISTEN_ADDRESS" = 'auto' ]; then 20 | CASSANDRA_LISTEN_ADDRESS="$(hostname --ip-address)" 21 | fi 22 | 23 | : ${CASSANDRA_BROADCAST_ADDRESS="$CASSANDRA_LISTEN_ADDRESS"} 24 | 25 | if [ "$CASSANDRA_BROADCAST_ADDRESS" = 'auto' ]; then 26 | CASSANDRA_BROADCAST_ADDRESS="$(hostname --ip-address)" 27 | fi 28 | : ${CASSANDRA_BROADCAST_RPC_ADDRESS:=$CASSANDRA_BROADCAST_ADDRESS} 29 | 30 | if [ -n "${CASSANDRA_NAME:+1}" ]; then 31 | : ${CASSANDRA_SEEDS:="cassandra"} 32 | fi 33 | : ${CASSANDRA_SEEDS:="$CASSANDRA_BROADCAST_ADDRESS"} 34 | 35 | sed -ri 's/(- seeds:) "127.0.0.1"/\1 "'"$CASSANDRA_SEEDS"'"/' "$CASSANDRA_CONFIG/cassandra.yaml" 36 | 37 | for yaml in \ 38 | broadcast_address \ 39 | broadcast_rpc_address \ 40 | cluster_name \ 41 | endpoint_snitch \ 42 | listen_address \ 43 | num_tokens \ 44 | rpc_address \ 45 | start_rpc \ 46 | ; do 47 | var="CASSANDRA_${yaml^^}" 48 | val="${!var}" 49 | if [ "$val" ]; then 50 | sed -ri 's/^(# )?('"$yaml"':).*/\2 '"$val"'/' "$CASSANDRA_CONFIG/cassandra.yaml" 51 | fi 52 | done 53 | 54 | for rackdc in dc rack; do 55 | var="CASSANDRA_${rackdc^^}" 56 | val="${!var}" 57 | if [ "$val" ]; then 58 | sed -ri 's/^('"$rackdc"'=).*/\1 '"$val"'/' "$CASSANDRA_CONFIG/cassandra-rackdc.properties" 59 | fi 60 | done 61 | fi 62 | 63 | exec "$@" -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/search/datatypes/SearchParamToken.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.search.datatypes; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.apache.lucene.document.Field; 7 | import org.apache.lucene.document.StringField; 8 | 9 | public class SearchParamToken extends AbstractSearchParam { 10 | 11 | private String system; 12 | private String code; 13 | 14 | public SearchParamToken(String name, String path, SearchParamTypes type, String system, String code) { 15 | super(name, path, type); 16 | this.system = system; 17 | this.code = code; 18 | } 19 | 20 | @Override 21 | public boolean hasValue() { 22 | return this.code != null; 23 | } 24 | 25 | public String getSystem() { 26 | return this.system; 27 | } 28 | 29 | public void setSystem(String str) { 30 | this.system = str; 31 | } 32 | 33 | public String getCode() { 34 | return code; 35 | } 36 | 37 | public void setCode(String code) { 38 | this.code = code; 39 | } 40 | 41 | public String getValueAsString() { 42 | String str = ""; 43 | if (this.system != null) { 44 | str = this.system; 45 | } 46 | if (this.code != null) { 47 | str = str + this.code; 48 | } 49 | return str; 50 | } 51 | 52 | @Override 53 | public String getValue() { 54 | return getValueAsString(); 55 | } 56 | 57 | public boolean hasSystem() { 58 | return this.system != null; 59 | } 60 | 61 | @Override 62 | public List createIndexedFields() { 63 | List fields = new ArrayList(); 64 | 65 | Field field; 66 | if (hasSystem()) { 67 | field = new StringField(this.name + "_system", this.system, Field.Store.NO); 68 | fields.add(field); 69 | } 70 | 71 | field = new StringField(this.name, this.code, Field.Store.NO); 72 | fields.add(field); 73 | 74 | return fields; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/search/datatypes/SearchParamDates.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.search.datatypes; 2 | 3 | import java.text.SimpleDateFormat; 4 | import java.util.Arrays; 5 | import java.util.Date; 6 | import java.util.List; 7 | 8 | import org.apache.lucene.document.Field; 9 | import org.apache.lucene.document.StringField; 10 | 11 | public class SearchParamDates extends AbstractSearchParam { 12 | private Date low; 13 | private Date high; 14 | 15 | private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss"); 16 | 17 | public SearchParamDates(String name, String path, SearchParamTypes type, Date low, Date high) { 18 | super(name, path, type); 19 | this.low = low; 20 | this.high = high; 21 | } 22 | 23 | @Override 24 | public boolean hasValue() { 25 | return low != null; 26 | } 27 | 28 | @Override 29 | public String getValueAsString() { 30 | // TODO Auto-generated method stub 31 | return null; 32 | } 33 | 34 | public Date getLow() { 35 | return low; 36 | } 37 | 38 | public void setLow(Date low) { 39 | this.low = low; 40 | } 41 | 42 | public Date getHigh() { 43 | return high; 44 | } 45 | 46 | public void setHigh(Date high) { 47 | this.high = high; 48 | } 49 | 50 | @Override 51 | public Date getValue() { 52 | return this.low; 53 | } 54 | 55 | @Override 56 | public List createIndexedFields() { 57 | Field fieldLow = new StringField(name, dateFormat.format(this.low), Field.Store.NO); 58 | 59 | // TODO: ver como se busca por un periodo 60 | Field fieldHigh = null; 61 | if (this.high != null) { 62 | fieldHigh = new StringField(name + "_high", dateFormat.format(this.high), Field.Store.NO); 63 | } 64 | 65 | if (fieldHigh == null) { 66 | return Arrays.asList(fieldLow); 67 | } else { 68 | return Arrays.asList(fieldLow, fieldHigh); 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/search/datatypes/AbstractSearchParam.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.search.datatypes; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | 6 | import org.apache.lucene.document.Field; 7 | 8 | public abstract class AbstractSearchParam { 9 | 10 | static final int MAX_SP_NAME = 200; 11 | 12 | protected String name; 13 | protected String path; 14 | protected SearchParamTypes type; 15 | 16 | public AbstractSearchParam() { 17 | 18 | } 19 | 20 | public AbstractSearchParam(String name, String path, SearchParamTypes type) { 21 | super(); 22 | this.name = name; 23 | this.path = path; 24 | this.type = type; 25 | } 26 | 27 | public String getName() { 28 | return name; 29 | } 30 | 31 | public String getPath() { 32 | return path; 33 | } 34 | 35 | public SearchParamTypes getType() { 36 | return type; 37 | } 38 | 39 | public String getTypeAsString() { 40 | return type.name(); 41 | } 42 | 43 | public void setTypeAsString(String type) { 44 | this.type = SearchParamTypes.valueOf(type); 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | return "ResourceSearchParameters [name=" + name + ", path=" + path + ", type=" + type + "]"; 50 | } 51 | 52 | // TODO: finish this code 53 | public String getTypeAsCassandraDataType() { 54 | if (this.type == SearchParamTypes.NUMBER) { 55 | return "double"; 56 | } else if (this.type == SearchParamTypes.QUANTITY) { 57 | return "double"; 58 | } else if (this.type == SearchParamTypes.DATE) { 59 | return "timestamp"; 60 | } else { 61 | return "text"; 62 | } 63 | } 64 | 65 | public abstract boolean hasValue(); 66 | 67 | public abstract String getValueAsString(); 68 | 69 | public abstract Object getValue(); 70 | 71 | public abstract List createIndexedFields(); 72 | 73 | public List createSortedFields() { 74 | return Collections.emptyList(); 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/search/datatypes/SearchParamQuantity.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.search.datatypes; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.apache.lucene.document.DoubleField; 7 | import org.apache.lucene.document.Field; 8 | import org.apache.lucene.document.StringField; 9 | 10 | public class SearchParamQuantity extends AbstractSearchParam { 11 | private Double value = null; 12 | private String system; 13 | private String code; 14 | 15 | public SearchParamQuantity(String name, String path, SearchParamTypes type, Double value, String system, 16 | String code) { 17 | super(name, path, type); 18 | this.value = value; 19 | this.system = system; 20 | this.code = code; 21 | } 22 | 23 | @Override 24 | public boolean hasValue() { 25 | return value != null; 26 | } 27 | 28 | public boolean hasSystem() { 29 | return this.system != null; 30 | } 31 | 32 | public boolean hasCode() { 33 | return this.code != null; 34 | } 35 | 36 | public Double getValue() { 37 | return this.value; 38 | } 39 | 40 | public void setValue(Double bd) { 41 | this.value = bd; 42 | } 43 | 44 | @Override 45 | public String getValueAsString() { 46 | if (this.value != null) { 47 | return this.value.toString(); 48 | } 49 | return null; 50 | } 51 | 52 | public String getSystem() { 53 | return system; 54 | } 55 | 56 | public void setSystem(String system) { 57 | this.system = system; 58 | } 59 | 60 | public String getCode() { 61 | return code; 62 | } 63 | 64 | public void setCode(String code) { 65 | this.code = code; 66 | } 67 | 68 | @Override 69 | public List createIndexedFields() { 70 | List fields = new ArrayList(); 71 | Field field = new DoubleField(this.name, this.value, Field.Store.NO); 72 | fields.add(field); 73 | 74 | if (hasSystem()) { 75 | field = new StringField(this.name + "_system", this.system, Field.Store.NO); 76 | fields.add(field); 77 | } 78 | 79 | if (hasCode()) { 80 | field = new StringField(this.name + "_code", this.code, Field.Store.NO); 81 | fields.add(field); 82 | } 83 | 84 | return fields; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | io.puntanegra.fhir.cassandra 5 | cassandra-fhir-index 6 | 0.1.1 7 | pom 8 | Cassandra FHIR Index 9 | 10 | 11 | fhir-index-plugin 12 | 13 | 14 | 15 | UTF-8 16 | 1.8 17 | 1.8 18 | 1.8 19 | 1.9.2 20 | 1.5 21 | 4.12 22 | false 23 | false 24 | 25 | 26 | 27 | https://github.com/jmiddleton/cassandra-fhir-index 28 | scm:git:git:@github.com:jmiddleton/cassandra-fhir-index.git 29 | scm:git:git@github.com:jmiddleton/cassandra-fhir-index.git 30 | HEAD 31 | 32 | 33 | 34 | 35 | Apache License, Version 2.0 36 | http://www.apache.org/licenses/LICENSE-2.0 37 | 38 | 39 | 40 | 41 | 42 | jorge.middleton 43 | Jorge L. Middleton 44 | jorge.middleton at gmail dot com 45 | 46 | 47 | 48 | 49 | 50 | 51 | org.apache.maven.plugins 52 | maven-compiler-plugin 53 | 2.3.2 54 | 55 | ${maven.compiler.source} 56 | ${maven.compiler.target} 57 | ${project.build.sourceEncoding} 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/search/extractor/QuantitySearchParameterExtractor.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.search.extractor; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | import org.hl7.fhir.dstu3.model.Quantity; 7 | import org.hl7.fhir.instance.model.api.IBase; 8 | import org.hl7.fhir.instance.model.api.IBaseResource; 9 | 10 | import ca.uhn.fhir.context.FhirContext; 11 | import ca.uhn.fhir.context.RuntimeSearchParam; 12 | import io.puntanegra.fhir.index.search.datatypes.AbstractSearchParam; 13 | import io.puntanegra.fhir.index.search.datatypes.SearchParamQuantity; 14 | import io.puntanegra.fhir.index.search.datatypes.SearchParamTypes; 15 | 16 | /** 17 | * Extracts information from generic attribute types. 18 | * 19 | * @author Jorge L. Middleton {@literal } 20 | * 21 | */ 22 | public class QuantitySearchParameterExtractor extends AbstractSearchParameterExtractor { 23 | 24 | public QuantitySearchParameterExtractor(FhirContext ctx) { 25 | super(ctx); 26 | } 27 | 28 | @Override 29 | public Set extractValues(IBaseResource instance, RuntimeSearchParam searchParam) { 30 | 31 | Set values = new HashSet(); 32 | 33 | String path = searchParam.getPath(); 34 | String resourceName = searchParam.getName(); 35 | String paramType = getParamType(searchParam); 36 | 37 | for (Object obj : extractValues(path, instance)) { 38 | if (obj == null || ((IBase) obj).isEmpty()) { 39 | continue; 40 | } 41 | 42 | boolean multiType = false; 43 | if (path.endsWith("[x]")) { 44 | multiType = true; 45 | } 46 | 47 | if (obj instanceof Quantity) { 48 | Quantity quantity = (Quantity) obj; 49 | if (quantity.getValueElement().isEmpty()) { 50 | continue; 51 | } 52 | 53 | SearchParamQuantity defq = new SearchParamQuantity(resourceName, path, 54 | SearchParamTypes.valueOf(paramType), quantity.getValueElement().getValue().doubleValue(), 55 | quantity.getSystemElement().getValueAsString(), quantity.getCode()); 56 | values.add(defq); 57 | 58 | } else { 59 | if (!multiType) { 60 | throw new SearchParameterException( 61 | "Search param " + resourceName + " is of unexpected datatype: " + obj.getClass()); 62 | } else { 63 | continue; 64 | } 65 | } 66 | } 67 | return values; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/search/extractor/DatesSearchParameterExtractor.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.search.extractor; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | import org.hl7.fhir.dstu3.model.BaseDateTimeType; 7 | import org.hl7.fhir.dstu3.model.Period; 8 | import org.hl7.fhir.instance.model.api.IBase; 9 | import org.hl7.fhir.instance.model.api.IBaseResource; 10 | 11 | import ca.uhn.fhir.context.FhirContext; 12 | import ca.uhn.fhir.context.RuntimeSearchParam; 13 | import io.puntanegra.fhir.index.search.datatypes.AbstractSearchParam; 14 | import io.puntanegra.fhir.index.search.datatypes.SearchParamDates; 15 | import io.puntanegra.fhir.index.search.datatypes.SearchParamTypes; 16 | 17 | /** 18 | * Extracts information from dates attribute types. 19 | * 20 | * @author Jorge L. Middleton {@literal } 21 | * 22 | */ 23 | public class DatesSearchParameterExtractor extends AbstractSearchParameterExtractor { 24 | 25 | public DatesSearchParameterExtractor(FhirContext ctx) { 26 | super(ctx); 27 | } 28 | 29 | @Override 30 | public Set extractValues(IBaseResource instance, RuntimeSearchParam searchParam) { 31 | 32 | Set values = new HashSet(); 33 | 34 | String path = searchParam.getPath(); 35 | String resourceName = searchParam.getName(); 36 | String paramType = getParamType(searchParam); 37 | 38 | for (Object obj : extractValues(path, instance)) { 39 | if (obj == null || ((IBase) obj).isEmpty()) { 40 | continue; 41 | } 42 | 43 | boolean multiType = false; 44 | if (path.endsWith("[x]")) { 45 | multiType = true; 46 | } 47 | 48 | if (obj instanceof BaseDateTimeType) { 49 | BaseDateTimeType datetime = (BaseDateTimeType) obj; 50 | if (datetime.isEmpty()) { 51 | continue; 52 | } 53 | SearchParamDates defq = new SearchParamDates(resourceName, path, SearchParamTypes.valueOf(paramType), 54 | datetime.getValue(), null); 55 | values.add(defq); 56 | } else if (obj instanceof Period) { 57 | Period period = (Period) obj; 58 | if (period.isEmpty()) { 59 | continue; 60 | } 61 | SearchParamDates defq = new SearchParamDates(resourceName, path, SearchParamTypes.valueOf(paramType), 62 | period.getStart(), period.getEnd()); 63 | values.add(defq); 64 | 65 | } else { 66 | if (!multiType) { 67 | throw new SearchParameterException( 68 | "Search param " + resourceName + " is of unexpected datatype: " + obj.getClass()); 69 | } else { 70 | continue; 71 | } 72 | } 73 | } 74 | return values; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/search/extractor/UriSearchParameterExtractor.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.search.extractor; 2 | 3 | import static org.apache.commons.lang3.StringUtils.isBlank; 4 | 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | 8 | import org.hl7.fhir.dstu3.model.Reference; 9 | import org.hl7.fhir.dstu3.model.UriType; 10 | import org.hl7.fhir.instance.model.api.IBase; 11 | import org.hl7.fhir.instance.model.api.IBaseResource; 12 | 13 | import ca.uhn.fhir.context.FhirContext; 14 | import ca.uhn.fhir.context.RuntimeSearchParam; 15 | import io.puntanegra.fhir.index.search.datatypes.AbstractSearchParam; 16 | import io.puntanegra.fhir.index.search.datatypes.SearchParamString; 17 | import io.puntanegra.fhir.index.search.datatypes.SearchParamTypes; 18 | 19 | /** 20 | * Extracts information from generic attribute types. 21 | * 22 | * @author Jorge L. Middleton {@literal } 23 | * 24 | */ 25 | public class UriSearchParameterExtractor extends AbstractSearchParameterExtractor { 26 | 27 | public UriSearchParameterExtractor(FhirContext ctx) { 28 | super(ctx); 29 | } 30 | 31 | @Override 32 | public Set extractValues(IBaseResource instance, RuntimeSearchParam searchParam) { 33 | 34 | Set values = new HashSet(); 35 | 36 | String path = searchParam.getPath(); 37 | String resourceName = searchParam.getName(); 38 | String paramType = getParamType(searchParam); 39 | 40 | for (Object obj : extractValues(path, instance)) { 41 | if (obj == null || ((IBase) obj).isEmpty()) { 42 | continue; 43 | } 44 | 45 | boolean multiType = false; 46 | if (path.endsWith("[x]")) { 47 | multiType = true; 48 | } 49 | 50 | if (obj instanceof UriType) { 51 | UriType uri = (UriType) obj; 52 | if (isBlank(uri.getValue())) { 53 | continue; 54 | } 55 | 56 | logger.trace("Adding param: {}, {}", resourceName, uri.getValue()); 57 | 58 | SearchParamString def = new SearchParamString(resourceName, path, SearchParamTypes.valueOf(paramType), 59 | uri.getValue()); 60 | values.add(def); 61 | } else if (obj instanceof Reference) { 62 | Reference ref = (Reference) obj; 63 | SearchParamString def = new SearchParamString(resourceName, path, SearchParamTypes.valueOf(paramType), 64 | ref.getReference()); 65 | values.add(def); 66 | } else { 67 | if (!multiType) { 68 | throw new SearchParameterException( 69 | "Search param " + resourceName + " is of unexpected datatype: " + obj.getClass()); 70 | } else { 71 | continue; 72 | } 73 | } 74 | } 75 | return values; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /test-data/src/test/java/io/puntanegra/fhir/index/FhirIndexServiceTest.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.util.List; 6 | 7 | import org.junit.After; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | 11 | import com.datastax.driver.core.Cluster; 12 | import com.datastax.driver.core.ProtocolVersion; 13 | import com.datastax.driver.core.ResultSet; 14 | import com.datastax.driver.core.Row; 15 | import com.datastax.driver.core.Session; 16 | 17 | public class FhirIndexServiceTest extends InitCassandraKeyspace { 18 | private Session session; 19 | 20 | //@formatter:off 21 | private static final String patientJson= "{"+ 22 | "\"resourceType\":\"Patient\","+ 23 | "\"id\":\"34a4e1c6-57c2-4217-8478-9c3f10b7aaaa\","+ 24 | "\"name\":["+ 25 | " {"+ 26 | " \"family\":["+ 27 | " \"Peroni\""+ 28 | " ],"+ 29 | " \"given\":["+ 30 | " \"Marcelo\""+ 31 | " ]"+ 32 | " }"+ 33 | "],"+ 34 | "\"active\": true," + 35 | "\"telecom\": ["+ 36 | " {"+ 37 | " \"system\": \"phone\","+ 38 | " \"value\": \"0648352638\","+ 39 | " \"use\": \"mobile\""+ 40 | " },"+ 41 | " {"+ 42 | " \"system\": \"email\","+ 43 | " \"value\": \"juan.perez@gmail.com\","+ 44 | " \"use\": \"home\""+ 45 | " }"+ 46 | "],"+ 47 | "\"gender\":\"male\","+ 48 | "\"birthDate\":\"1977-04-16\""+ 49 | "}"; 50 | //@formatter:on 51 | 52 | @Before 53 | public void init() throws Exception { 54 | initCassandraFS(); 55 | 56 | Cluster cluster = Cluster.builder().addContactPoints("localhost").withProtocolVersion(ProtocolVersion.V4) 57 | .build(); 58 | session = cluster.connect(); 59 | 60 | session.execute("USE test;"); 61 | } 62 | 63 | @Test 64 | public void test() throws Exception { 65 | 66 | session.execute( 67 | "INSERT INTO test.FHIR_RESOURCES (resource_id, version, resource_type, state, lastupdated, format, author, content)" 68 | + " VALUES ('pat556eb333', 1, 'Patient', 'active', 1442959315019, 'json', 'dr who'," + "'" 69 | + patientJson + "')"); 70 | 71 | Thread.sleep(1000); 72 | ResultSet r = session.execute( 73 | "SELECT * FROM test.FHIR_RESOURCES" + " WHERE expr(idx_fhir_resources, 'active:true')" + " LIMIT 100;"); 74 | //@formatter:on 75 | 76 | List l = r.all(); 77 | assertEquals(1, l.size()); 78 | 79 | for (Row row : l) { 80 | System.out.println(">>>>>>>>>>>>>>>>" + row.toString()); 81 | // assertEquals("556ebd54", row.getString("resource_id")); 82 | } 83 | 84 | } 85 | 86 | @After 87 | public void tearDown() { 88 | // session.close(); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/search/extractor/AbstractSearchParameterExtractor.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.search.extractor; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.hl7.fhir.dstu3.model.Enumeration; 7 | import org.hl7.fhir.instance.model.api.IBaseResource; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import ca.uhn.fhir.context.FhirContext; 12 | import ca.uhn.fhir.context.RuntimeResourceDefinition; 13 | import ca.uhn.fhir.context.RuntimeSearchParam; 14 | import ca.uhn.fhir.util.FhirTerser; 15 | import io.puntanegra.fhir.index.search.SearchParamExtractor; 16 | import io.puntanegra.fhir.index.search.datatypes.SearchParamString; 17 | import io.puntanegra.fhir.index.search.datatypes.SearchParamTypes; 18 | 19 | public abstract class AbstractSearchParameterExtractor implements SearchParamExtractor { 20 | 21 | protected static final Logger logger = LoggerFactory.getLogger(SearchParamExtractor.class); 22 | protected static final int MAX_LENGTH = 200; 23 | 24 | protected FhirContext ctx; 25 | 26 | public AbstractSearchParameterExtractor(FhirContext ctx) { 27 | this.ctx = ctx; 28 | } 29 | 30 | protected List extractValues(String paths, IBaseResource instance) { 31 | List values = new ArrayList(); 32 | String[] nextPathsSplit = paths.split("\\|"); 33 | FhirTerser t = this.ctx.newTerser(); 34 | for (String nextPath : nextPathsSplit) { 35 | String nextPathTrimmed = nextPath.trim(); 36 | try { 37 | values.addAll(t.getValues(instance, nextPathTrimmed)); 38 | } catch (Exception e) { 39 | RuntimeResourceDefinition def = this.ctx.getResourceDefinition(instance); 40 | logger.warn("Failed to index values from path[{}] in resource type[{}]: {}", 41 | new Object[] { nextPathTrimmed, def.getName(), e.toString(), e }); 42 | } 43 | } 44 | return values; 45 | } 46 | 47 | protected static > String extractSystem(Enumeration theBoundCode) { 48 | if (theBoundCode.getValue() != null) { 49 | return theBoundCode.getEnumFactory().toSystem(theBoundCode.getValue()); 50 | } 51 | return null; 52 | } 53 | 54 | /** 55 | * Create a {@link SearchParamString} value. 56 | * 57 | * @param searchParam 58 | * @param value 59 | * @return 60 | */ 61 | protected SearchParamString addStringParam(RuntimeSearchParam searchParam, String value) { 62 | String path = searchParam.getPath(); 63 | String resourceName = searchParam.getName(); 64 | 65 | return new SearchParamString(resourceName, path, SearchParamTypes.STRING, value); 66 | } 67 | 68 | protected String getParamType(RuntimeSearchParam searchParam) { 69 | String paramType = searchParam.getParamType().getCode().toUpperCase(); 70 | return paramType; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/cache/SearchCacheUpdater.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to STRATIO (C) under one or more contributor license agreements. 3 | * See the NOTICE file distributed with this work for additional information 4 | * regarding copyright ownership. The STRATIO (C) licenses this file 5 | * to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | package io.puntanegra.fhir.index.cache; 20 | 21 | import org.apache.cassandra.db.Clustering; 22 | import org.apache.cassandra.db.DecoratedKey; 23 | import org.apache.cassandra.db.PartitionRangeReadCommand; 24 | import org.apache.cassandra.db.ReadCommand; 25 | import org.apache.lucene.search.Query; 26 | import org.apache.lucene.search.ScoreDoc; 27 | 28 | import java.util.Optional; 29 | import java.util.UUID; 30 | 31 | /** 32 | * A cache updater to update a cache entry. 33 | * 34 | * @author Andres de la Pena {@literal } 35 | */ 36 | public class SearchCacheUpdater { 37 | 38 | private final UUID id; 39 | private final SearchCache cache; 40 | private final String search; 41 | private final PartitionRangeReadCommand command; 42 | private final Query query; 43 | 44 | SearchCacheUpdater(SearchCache cache, String search, UUID id, ReadCommand command, Query query) { 45 | this.cache = cache; 46 | this.search = search; 47 | this.id = id; 48 | this.command = command instanceof PartitionRangeReadCommand ? (PartitionRangeReadCommand) command : null; 49 | this.query = query; 50 | } 51 | 52 | /** 53 | * Updates the cached entry with the specified pointer to a search result. 54 | * 55 | * @param key the row partition key 56 | * @param clustering the row clustering key 57 | * @param scoreDoc the row score for the query 58 | */ 59 | public void put(DecoratedKey key, Clustering clustering, ScoreDoc scoreDoc) { 60 | if (command != null) { 61 | cache.put(id, new SearchCacheEntry(cache, search, command, key, Optional.of(clustering), scoreDoc, query)); 62 | } 63 | } 64 | 65 | /** 66 | * Updates the cached entry with the specified pointer to a search result. 67 | * 68 | * @param key the row partition key 69 | * @param scoreDoc the row score for the query 70 | */ 71 | public void put(DecoratedKey key, ScoreDoc scoreDoc) { 72 | if (command != null) { 73 | cache.put(id, new SearchCacheEntry(cache, search, command, key, Optional.empty(), scoreDoc, query)); 74 | } 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/util/JsonSerializer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to STRATIO (C) under one or more contributor license agreements. 3 | * See the NOTICE file distributed with this work for additional information 4 | * regarding copyright ownership. The STRATIO (C) licenses this file 5 | * to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | package io.puntanegra.fhir.index.util; 20 | 21 | import java.io.IOException; 22 | 23 | import org.codehaus.jackson.JsonGenerator; 24 | import org.codehaus.jackson.JsonParser; 25 | import org.codehaus.jackson.map.DeserializationConfig; 26 | import org.codehaus.jackson.map.ObjectMapper; 27 | import org.codehaus.jackson.map.SerializationConfig; 28 | import org.codehaus.jackson.map.annotate.JsonSerialize; 29 | 30 | /** 31 | * A JSON mapper based on Codehaus {@link ObjectMapper} annotations. 32 | * 33 | * @author Andres de la Pena {@literal } 34 | */ 35 | public final class JsonSerializer { 36 | 37 | private static final JsonSerializer INSTANCE = new JsonSerializer(); 38 | 39 | /** The embedded JSON serializer. */ 40 | private final ObjectMapper jsonMapper = new ObjectMapper(); 41 | 42 | /** Private constructor to hide the implicit public one. */ 43 | private JsonSerializer() { 44 | jsonMapper.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, false); 45 | jsonMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); 46 | jsonMapper.configure(SerializationConfig.Feature.AUTO_DETECT_IS_GETTERS, false); 47 | jsonMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false); 48 | jsonMapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL); 49 | } 50 | 51 | /** 52 | * Returns the JSON {@code String} representation of the specified object. 53 | * 54 | * @param value 55 | * the object to be serialized. 56 | * @return the JSON {@code String} representation of {@code value} 57 | * @throws IOException 58 | * if there are serialization problems 59 | */ 60 | public static String toString(Object value) throws IOException { 61 | return INSTANCE.jsonMapper.writeValueAsString(value); 62 | } 63 | 64 | /** 65 | * Returns the object of the specified class represented by the specified 66 | * JSON {@code String}. 67 | * 68 | * @param value 69 | * the JSON {@code String} to be parsed 70 | * @param valueType 71 | * the class of the object to be parsed 72 | * @param 73 | * the type of the object to be parsed 74 | * @return an object of the specified class represented by {@code value} 75 | * @throws IOException 76 | * if there are parsing problems 77 | */ 78 | public static T fromString(String value, Class valueType) throws IOException { 79 | return INSTANCE.jsonMapper.readValue(value, valueType); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/mapper/KeyEntry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to STRATIO (C) under one or more contributor license agreements. 3 | * See the NOTICE file distributed with this work for additional information 4 | * regarding copyright ownership. The STRATIO (C) licenses this file 5 | * to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | package io.puntanegra.fhir.index.mapper; 20 | 21 | import java.nio.ByteBuffer; 22 | import java.util.Arrays; 23 | 24 | import org.apache.cassandra.config.DatabaseDescriptor; 25 | import org.apache.cassandra.db.Clustering; 26 | import org.apache.cassandra.db.DecoratedKey; 27 | import org.apache.cassandra.dht.Murmur3Partitioner; 28 | import org.apache.cassandra.dht.Token; 29 | 30 | /** 31 | * Class representing a Cassandra's wide table primary key. This is composed by 32 | * token, partition key and clustering key. 33 | * 34 | * @author Andres de la Pena {@literal } 35 | */ 36 | public class KeyEntry implements Comparable { 37 | 38 | private final KeyMapper mapper; 39 | private final ByteBuffer[] components; 40 | 41 | /** 42 | * Constructor using a {@link KeyMapper} and an array of binary components 43 | * 44 | * @param mapper 45 | * the mapper 46 | * @param components 47 | * the binary components 48 | */ 49 | public KeyEntry(KeyMapper mapper, ByteBuffer[] components) { 50 | this.mapper = mapper; 51 | this.components = Arrays.copyOf(components, components.length); 52 | } 53 | 54 | /** 55 | * Returns the partitioning token. 56 | * 57 | * @return the token 58 | */ 59 | public Token getToken() { 60 | return Murmur3Partitioner.instance.getTokenFactory().fromByteArray(components[0]); 61 | } 62 | 63 | /** 64 | * Returns the raw partition key. 65 | * 66 | * @return the partition key 67 | */ 68 | public ByteBuffer getKey() { 69 | return components[1]; 70 | } 71 | 72 | /** 73 | * Returns the decorated partition key. 74 | * 75 | * @return the partition key 76 | */ 77 | public DecoratedKey getDecoratedKey() { 78 | return DatabaseDescriptor.getPartitioner().decorateKey(getKey()); 79 | } 80 | 81 | /** 82 | * Returns the clustering key. 83 | * 84 | * @return the clustering key 85 | */ 86 | public Clustering getClustering() { 87 | return new Clustering(mapper.clusteringType().split(components[2])); 88 | } 89 | 90 | /** {@inheritDoc} */ 91 | @Override 92 | public int compareTo(KeyEntry other) { 93 | int comp = getToken().compareTo(other.getToken()); 94 | if (comp == 0) { 95 | comp = getDecoratedKey().compareTo(other.getDecoratedKey()); 96 | } 97 | if (comp == 0) { 98 | comp = mapper.clusteringComparator().compare(getClustering(), other.getClustering()); 99 | } 100 | return comp; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test-data/src/test/java/io/puntanegra/fhir/index/InitCassandraKeyspace.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index; 2 | 3 | import org.apache.cassandra.service.EmbeddedCassandraService; 4 | import org.apache.commons.io.FileUtils; 5 | import org.junit.Test; 6 | 7 | import com.datastax.driver.core.Cluster; 8 | import com.datastax.driver.core.ProtocolVersion; 9 | import com.datastax.driver.core.Session; 10 | 11 | public class InitCassandraKeyspace { 12 | private static final String RESOURCE_COMMITLOG = "/tmp/commitlog"; 13 | private static final String RESOURCE_DATA = "/tmp/data"; 14 | protected Session session; 15 | 16 | @Test 17 | public void initCassandraFS() throws Exception { 18 | FileUtils.forceDeleteOnExit(FileUtils.getFile(RESOURCE_DATA)); 19 | FileUtils.forceDeleteOnExit(FileUtils.getFile(RESOURCE_COMMITLOG)); 20 | 21 | System.setProperty("CASSANDRA_HOME", "~/Apps/apache-cassandra-3.0.4"); 22 | System.setProperty("cassandra.config", "file:///Users/jmiddleton/Apps/apache-cassandra-3.0.4/conf/cassandra.yaml"); 23 | System.setProperty("storage-config", RESOURCE_DATA); 24 | 25 | EmbeddedCassandraService cassandraService = new EmbeddedCassandraService(); 26 | 27 | cassandraService.start(); 28 | 29 | Cluster cluster = Cluster.builder().addContactPoints("localhost").withProtocolVersion(ProtocolVersion.V4) 30 | .build(); 31 | session = cluster.connect(); 32 | 33 | //@formatter:off 34 | //session.execute("DROP KEYSPACE test"); 35 | session.execute("CREATE KEYSPACE IF NOT EXISTS test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};"); 36 | session.execute("USE test;"); 37 | 38 | //session.execute("DROP TABLE test.FHIR_RESOURCES;"); 39 | session.execute("CREATE TABLE IF NOT EXISTS test.FHIR_RESOURCES ("+ 40 | "resource_id text,"+ 41 | "version int,"+ 42 | "resource_type text,"+ 43 | "state text,"+ 44 | "lastupdated timestamp,"+ 45 | "format text,"+ 46 | "author text,"+ 47 | "content text,"+ 48 | "PRIMARY KEY (resource_id, version, lastupdated));"); 49 | 50 | //session.execute("DROP INDEX idx_fhir_resources"); 51 | session.execute("CREATE CUSTOM INDEX IF NOT EXISTS idx_fhir_resources ON test.FHIR_RESOURCES (content)"+ 52 | "USING 'io.puntanegra.fhir.index.FhirIndex'"+ 53 | "WITH OPTIONS = {"+ 54 | " 'refresh_seconds' : '5',"+ 55 | " 'search' : '{"+ 56 | " resources : {"+ 57 | " Patient : [\"family\", \"email\", \"active\"],"+ 58 | " Observation : [\"subject\", \"date\"]"+ 59 | " }"+ 60 | " }'"+ 61 | "};"); 62 | 63 | // session.execute("CREATE CUSTOM INDEX IF NOT EXISTS idx_patient ON test.FHIR_RESOURCES (content)"+ 64 | // "USING 'io.puntanegra.fhir.index.FhirIndex'"+ 65 | // "WITH OPTIONS = {"+ 66 | // " 'refresh_seconds' : '5',"+ 67 | // " 'resource_type' : 'Patient',"+ 68 | // " 'resource_type_column' : 'resource_type',"+ 69 | // " 'search' : '{"+ 70 | // " parameters : [" + 71 | // " \"active\", " + 72 | // " \"family\", " + 73 | // " \"email\", " + 74 | // " \"identifier\"" + 75 | // " ]" + 76 | // " }'"+ 77 | // "};"); 78 | 79 | //@formatter:off 80 | // session.execute("CREATE CUSTOM INDEX IF NOT EXISTS idx_observation ON test.FHIR_RESOURCES (content)"+ 81 | // "USING 'io.puntanegra.fhir.index.FhirIndex'"+ 82 | // "WITH OPTIONS = {"+ 83 | // " 'refresh_seconds' : '5',"+ 84 | // " 'resource_type' : 'Observation',"+ 85 | // " 'resource_type_column' : 'resource_type',"+ 86 | // " 'search' : '{"+ 87 | // " parameters : [" + 88 | // " \"code\", " + 89 | // " \"value-quantity\"" + 90 | // " ]" + 91 | // " }'"+ 92 | // "};"); 93 | //@formatter:on 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/util/TimeCounter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to STRATIO (C) under one or more contributor license agreements. 3 | * See the NOTICE file distributed with this work for additional information 4 | * regarding copyright ownership. The STRATIO (C) licenses this file 5 | * to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | package io.puntanegra.fhir.index.util; 20 | 21 | import org.apache.commons.lang3.time.StopWatch; 22 | 23 | /** 24 | * Class for measuring time durations. 25 | * 26 | * @author Andres de la Pena {@literal } 27 | */ 28 | public final class TimeCounter { 29 | 30 | private enum State { 31 | UNSTARTED, RUNNING, STOPPED 32 | } 33 | 34 | private final StopWatch watch; 35 | private State state; 36 | 37 | /** 38 | * Returns a new stopped {@link TimeCounter}. 39 | * 40 | * @return a new stopped counter 41 | */ 42 | public static TimeCounter create() { 43 | return new TimeCounter(); 44 | } 45 | 46 | /** 47 | * Builds a new stopped {@link TimeCounter}. 48 | */ 49 | private TimeCounter() { 50 | this.watch = new StopWatch(); 51 | this.state = State.UNSTARTED; 52 | } 53 | 54 | /** 55 | * Starts or resumes the time count. 56 | * 57 | * @return this 58 | */ 59 | public TimeCounter start() { 60 | switch (state) { 61 | case UNSTARTED: 62 | watch.start(); 63 | break; 64 | case RUNNING: 65 | throw new IllegalStateException("Already started"); 66 | case STOPPED: 67 | watch.resume(); 68 | break; 69 | default: 70 | throw new IllegalStateException("Unrecognized state " + state); 71 | } 72 | state = State.RUNNING; 73 | return this; 74 | } 75 | 76 | /** 77 | * Stops or suspends the time count. 78 | * 79 | * @return this 80 | */ 81 | public TimeCounter stop() { 82 | switch (state) { 83 | case UNSTARTED: 84 | throw new IllegalStateException("Not started. "); 85 | case STOPPED: 86 | throw new IllegalStateException("Already stopped. "); 87 | case RUNNING: 88 | watch.suspend(); 89 | default: 90 | state = State.STOPPED; 91 | return this; 92 | } 93 | } 94 | 95 | /** 96 | * Returns a summary of the time that the stopwatch has recorded as a string. 97 | * 98 | * @return a summary of the time that the stopwatch has recorded 99 | */ 100 | public String toString() { 101 | return watch.toString(); 102 | } 103 | 104 | /** 105 | * Returns the counted time in milliseconds. 106 | * 107 | * @return the counted time in milliseconds 108 | */ 109 | public long getTime() { 110 | return watch.getTime(); 111 | } 112 | 113 | /** 114 | * Returns the counted time in nanoseconds. 115 | * 116 | * @return the counted time in nanoseconds 117 | */ 118 | public long getNanoTime() { 119 | return watch.getNanoTime(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /test-data/src/test/java/io/puntanegra/fhir/index/NameGenerator.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | 7 | public class NameGenerator { 8 | 9 | private List vocals = new ArrayList(); 10 | private List startConsonants = new ArrayList(); 11 | private List endConsonants = new ArrayList(); 12 | private List nameInstructions = new ArrayList(); 13 | 14 | public NameGenerator() { 15 | String demoVocals[] = { "a", "e", "i", "o", "u", "ei", "ai", "ou", "j", "ji", "y", "oi", "au", "oo" }; 16 | 17 | String demoStartConsonants[] = { "b", "c", "d", "f", "g", "h", "k", "l", "m", "n", "p", "q", "r", "s", "t", "v", 18 | "w", "x", "z", "ch", "bl", "br", "fl", "gl", "gr", "kl", "pr", "st", "sh", "th" }; 19 | 20 | String demoEndConsonants[] = { "b", "d", "f", "g", "h", "k", "l", "m", "n", "p", "r", "s", "t", "v", "w", "z", 21 | "ch", "gh", "nn", "st", "sh", "th", "tt", "ss", "pf", "nt" }; 22 | 23 | String nameInstructions[] = { "vd", "cvdvd", "cvd", "vdvd" }; 24 | 25 | this.vocals.addAll(Arrays.asList(demoVocals)); 26 | this.startConsonants.addAll(Arrays.asList(demoStartConsonants)); 27 | this.endConsonants.addAll(Arrays.asList(demoEndConsonants)); 28 | this.nameInstructions.addAll(Arrays.asList(nameInstructions)); 29 | } 30 | 31 | /** 32 | * 33 | * The names will look like this 34 | * (v=vocal,c=startConsonsonant,d=endConsonants): vd, cvdvd, cvd, vdvd 35 | * 36 | * @param vocals 37 | * pass something like {"a","e","ou",..} 38 | * @param startConsonants 39 | * pass something like {"s","f","kl",..} 40 | * @param endConsonants 41 | * pass something like {"th","sh","f",..} 42 | */ 43 | public NameGenerator(String[] vocals, String[] startConsonants, String[] endConsonants) { 44 | this.vocals.addAll(Arrays.asList(vocals)); 45 | this.startConsonants.addAll(Arrays.asList(startConsonants)); 46 | this.endConsonants.addAll(Arrays.asList(endConsonants)); 47 | } 48 | 49 | /** 50 | * see {@link NameGenerator#NameGenerator(String[], String[], String[])} 51 | * 52 | * @param vocals 53 | * @param startConsonants 54 | * @param endConsonants 55 | * @param nameInstructions 56 | * Use only the following letters: 57 | * (v=vocal,c=startConsonsonant,d=endConsonants)! Pass something 58 | * like {"vd", "cvdvd", "cvd", "vdvd"} 59 | */ 60 | public NameGenerator(String[] vocals, String[] startConsonants, String[] endConsonants, String[] nameInstructions) { 61 | this(vocals, startConsonants, endConsonants); 62 | this.nameInstructions.addAll(Arrays.asList(nameInstructions)); 63 | } 64 | 65 | public String getName() { 66 | return firstCharUppercase(getNameByInstructions(getRandomElementFrom(nameInstructions))); 67 | } 68 | 69 | private int randomInt(int min, int max) { 70 | return (int) (min + (Math.random() * (max + 1 - min))); 71 | } 72 | 73 | private String getNameByInstructions(String nameInstructions) { 74 | String name = ""; 75 | int l = nameInstructions.length(); 76 | 77 | for (int i = 0; i < l; i++) { 78 | char x = nameInstructions.charAt(0); 79 | switch (x) { 80 | case 'v': 81 | name += getRandomElementFrom(vocals); 82 | break; 83 | case 'c': 84 | name += getRandomElementFrom(startConsonants); 85 | break; 86 | case 'd': 87 | name += getRandomElementFrom(endConsonants); 88 | break; 89 | } 90 | nameInstructions = nameInstructions.substring(1); 91 | } 92 | return name; 93 | } 94 | 95 | private String firstCharUppercase(String name) { 96 | return Character.toString(name.charAt(0)).toUpperCase() + name.substring(1); 97 | } 98 | 99 | private String getRandomElementFrom(List v) { 100 | return v.get(randomInt(0, v.size() - 1)); 101 | } 102 | } -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/search/extractor/NumberSearchParameterExtractor.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.search.extractor; 2 | 3 | import static org.apache.commons.lang3.StringUtils.isNotBlank; 4 | 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | 8 | import javax.measure.unit.NonSI; 9 | import javax.measure.unit.Unit; 10 | 11 | import org.hl7.fhir.dstu3.model.Duration; 12 | import org.hl7.fhir.dstu3.model.IntegerType; 13 | import org.hl7.fhir.dstu3.model.Quantity; 14 | import org.hl7.fhir.instance.model.api.IBase; 15 | import org.hl7.fhir.instance.model.api.IBaseResource; 16 | 17 | import ca.uhn.fhir.context.FhirContext; 18 | import ca.uhn.fhir.context.RuntimeSearchParam; 19 | import io.puntanegra.fhir.index.search.datatypes.AbstractSearchParam; 20 | import io.puntanegra.fhir.index.search.datatypes.SearchParamNumber; 21 | import io.puntanegra.fhir.index.search.datatypes.SearchParamTypes; 22 | 23 | /** 24 | * Extracts information from generic attribute types. 25 | * 26 | * @author Jorge L. Middleton {@literal } 27 | * 28 | */ 29 | public class NumberSearchParameterExtractor extends AbstractSearchParameterExtractor { 30 | 31 | public static final String UCUM_NS = "http://unitsofmeasure.org"; 32 | 33 | public NumberSearchParameterExtractor(FhirContext ctx) { 34 | super(ctx); 35 | } 36 | 37 | @Override 38 | public Set extractValues(IBaseResource instance, RuntimeSearchParam searchParam) { 39 | 40 | Set values = new HashSet(); 41 | 42 | String path = searchParam.getPath(); 43 | String resourceName = searchParam.getName(); 44 | String paramType = getParamType(searchParam); 45 | 46 | for (Object obj : extractValues(path, instance)) { 47 | if (obj == null || ((IBase) obj).isEmpty()) { 48 | continue; 49 | } 50 | 51 | boolean multiType = false; 52 | if (path.endsWith("[x]")) { 53 | multiType = true; 54 | } 55 | 56 | if (obj instanceof Duration) { 57 | Duration nextValue = (Duration) obj; 58 | if (nextValue.getValueElement().isEmpty()) { 59 | continue; 60 | } 61 | 62 | if (UCUM_NS.equals(nextValue.getSystem())) { 63 | if (isNotBlank(nextValue.getCode())) { 64 | Unit unit = Unit.valueOf(nextValue.getCode()); 65 | javax.measure.converter.UnitConverter dayConverter = unit.getConverterTo(NonSI.DAY); 66 | double dayValue = dayConverter.convert(nextValue.getValue().doubleValue()); 67 | Duration newValue = new Duration(); 68 | newValue.setSystem(UCUM_NS); 69 | newValue.setCode(NonSI.DAY.toString()); 70 | newValue.setValue(dayValue); 71 | nextValue = newValue; 72 | } 73 | } 74 | 75 | SearchParamNumber defq = new SearchParamNumber(resourceName, path, SearchParamTypes.valueOf(paramType), 76 | nextValue.getValue().doubleValue()); 77 | values.add(defq); 78 | } else if (obj instanceof Quantity) { 79 | Quantity nextValue = (Quantity) obj; 80 | if (nextValue.getValueElement().isEmpty()) { 81 | continue; 82 | } 83 | 84 | SearchParamNumber defq = new SearchParamNumber(resourceName, path, SearchParamTypes.valueOf(paramType), 85 | nextValue.getValue().doubleValue()); 86 | values.add(defq); 87 | } else if (obj instanceof IntegerType) { 88 | IntegerType nextValue = (IntegerType) obj; 89 | if (nextValue.getValue() == null) { 90 | continue; 91 | } 92 | 93 | SearchParamNumber defq = new SearchParamNumber(resourceName, path, SearchParamTypes.valueOf(paramType), 94 | nextValue.getValue().doubleValue()); 95 | values.add(defq); 96 | } else { 97 | if (!multiType) { 98 | throw new SearchParameterException( 99 | "Search param " + resourceName + " is of unexpected datatype: " + obj.getClass()); 100 | } else { 101 | continue; 102 | } 103 | } 104 | } 105 | return values; 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/mapper/FhirMapper.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.mapper; 2 | 3 | import java.util.List; 4 | import java.util.Set; 5 | 6 | import org.apache.lucene.document.Document; 7 | import org.apache.lucene.document.Field; 8 | import org.hl7.fhir.instance.model.api.IBaseResource; 9 | 10 | import ca.uhn.fhir.context.FhirVersionEnum; 11 | import io.puntanegra.fhir.index.config.ResourceOptions; 12 | import io.puntanegra.fhir.index.search.SearchParamExtractorHelper; 13 | import io.puntanegra.fhir.index.search.datatypes.AbstractSearchParam; 14 | import io.puntanegra.fhir.index.search.datatypes.SearchParamString; 15 | import io.puntanegra.fhir.index.search.datatypes.SearchParamTypes; 16 | 17 | /** 18 | * Mapper class used to create Lucene {@link Document} index from a FHIR 19 | * {@link IBaseResource}.
20 | * Based on the index configuration, it extracts search parameter values from a 21 | * FHIR {@link IBaseResource} and creates the appropiate Lucene {@link Field}. 22 | * The fields are indexed and can be sorted based on the configuration. 23 | * 24 | * @author Jorge L. Middleton {@literal } 25 | * 26 | */ 27 | public class FhirMapper { 28 | 29 | private ResourceOptions searchOptions; 30 | 31 | private SearchParamExtractorHelper fhirExtractor; 32 | 33 | public FhirMapper(ResourceOptions searchOptions) { 34 | this.searchOptions = searchOptions; 35 | this.fhirExtractor = new SearchParamExtractorHelper(FhirVersionEnum.DSTU3); 36 | } 37 | 38 | /** 39 | * Parses Json FHIR Resource and converts to Lucene {@link Field}s. 40 | * 41 | * @param document, 42 | * the Lucene {@link Document}. 43 | * @param json, 44 | * the FHIR resource as JSON format. 45 | */ 46 | public void addFields(Document document, String json) { 47 | 48 | IBaseResource resourceInstance = this.fhirExtractor.parseResource(json); 49 | String resourceName = resourceInstance.getClass().getSimpleName(); 50 | 51 | Set parameters = this.searchOptions.resources.get(resourceName); 52 | Set values = this.fhirExtractor.extractParametersValues(resourceInstance, parameters); 53 | for (AbstractSearchParam entry : values) { 54 | doAddFields(document, entry, false); 55 | } 56 | 57 | doAddFields(document, new SearchParamString("resource_type", "", SearchParamTypes.STRING, resourceName), false); 58 | 59 | } 60 | 61 | /** 62 | * Adds the specified column name and value to a Lucene {@link Document}. 63 | * The added fields are indexed and sorted (if the parameter sorted is 64 | * true). 65 | * 66 | * @param document 67 | * a {@link Document} 68 | * @param value 69 | * the parameter to add to a {@link Document} 70 | * @param sorted 71 | * sort the value or not 72 | */ 73 | private void doAddFields(Document document, AbstractSearchParam value, boolean sorted) { 74 | if (value != null && value.hasValue()) { 75 | doAddIndexedFields(document, value); 76 | 77 | if (sorted) { 78 | doAddSortedFields(document, value); 79 | } 80 | } 81 | } 82 | 83 | /** 84 | * Add a {@link Field} to be sorted by. 85 | * 86 | * @param document 87 | * @param param 88 | */ 89 | private void doAddSortedFields(Document document, AbstractSearchParam param) { 90 | // BytesRef bytes = new BytesRef(value); 91 | // document.add(new SortedDocValuesField(value.getName(), bytes)); 92 | 93 | // TODO: implement sort 94 | List fields = param.createSortedFields(); 95 | for (Field field : fields) { 96 | if (field != null) { 97 | document.add(field); 98 | } 99 | } 100 | } 101 | 102 | /** 103 | * Add an indexed {@link Field}. 104 | * 105 | * @param document 106 | * @param param 107 | */ 108 | private void doAddIndexedFields(Document document, AbstractSearchParam param) { 109 | List fields = param.createIndexedFields(); 110 | for (Field field : fields) { 111 | if (field != null) { 112 | document.add(field); 113 | } 114 | } 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/search/SearchParamExtractorHelper.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.search; 2 | 3 | import static org.apache.commons.lang3.StringUtils.isBlank; 4 | 5 | import java.util.Collections; 6 | import java.util.HashSet; 7 | import java.util.List; 8 | import java.util.Set; 9 | 10 | import org.hl7.fhir.instance.model.api.IBaseResource; 11 | 12 | import ca.uhn.fhir.context.FhirContext; 13 | import ca.uhn.fhir.context.FhirVersionEnum; 14 | import ca.uhn.fhir.context.RuntimeResourceDefinition; 15 | import ca.uhn.fhir.context.RuntimeSearchParam; 16 | import io.puntanegra.fhir.index.search.datatypes.AbstractSearchParam; 17 | import io.puntanegra.fhir.index.search.datatypes.SearchParamTypes; 18 | import io.puntanegra.fhir.index.search.extractor.SearchParameterExtractorFactory; 19 | 20 | /** 21 | * Helper class to extract information from FHIR {@link IBaseResource}.
22 | * Based on the search parameters metadata defined in the resource, it extracts 23 | * instance information which is later used to index a particular resource.
24 | * If no parameter is defined, then all the search parameters will be used for 25 | * indexing. 26 | * 27 | * @author Jorge L. Middleton {@literal } 28 | * 29 | */ 30 | public class SearchParamExtractorHelper { 31 | 32 | private FhirVersionEnum fhirVersion = FhirVersionEnum.DSTU3; 33 | private FhirContext ctx; 34 | private SearchParameterExtractorFactory extractorFactory; 35 | 36 | public SearchParamExtractorHelper(FhirVersionEnum version) { 37 | this.fhirVersion = version; 38 | this.ctx = FhirContextHelper.getContext(this.fhirVersion); 39 | this.extractorFactory = SearchParameterExtractorFactory.getInstance(this.ctx); 40 | } 41 | 42 | /** 43 | * Extracts search parameter metadata defined in a resource. 44 | * 45 | * @param instance, 46 | * the FHIR {@link IBaseResource} 47 | * @param parameters, 48 | * parameters to index for this resource. This are defined during 49 | * index creation.
50 | * If no parameter is defined, then all the FHIR search 51 | * parameters will be extracted. 52 | * @return 53 | */ 54 | public Set extractParametersValues(IBaseResource instance, Set parameters) { 55 | Set values = new HashSet(); 56 | 57 | RuntimeResourceDefinition def = this.ctx.getResourceDefinition(instance); 58 | 59 | // si no hay parametros definidos, indexamos todos los atributos 60 | // definidos en el recurso FHIR. 61 | if (parameters == null || parameters.isEmpty()) { 62 | List params = def.getSearchParams(); 63 | for (RuntimeSearchParam searchParam : params) { 64 | values.addAll(doCreateSearchParam(instance, searchParam)); 65 | } 66 | } else { 67 | for (String param : parameters) { 68 | RuntimeSearchParam searchParam = def.getSearchParam(param); 69 | values.addAll(doCreateSearchParam(instance, searchParam)); 70 | 71 | } 72 | } 73 | return values; 74 | } 75 | 76 | private Set doCreateSearchParam(IBaseResource instance, RuntimeSearchParam searchParam) { 77 | String nextPath = searchParam.getPath(); 78 | if (isBlank(nextPath)) { 79 | return Collections.emptySet(); 80 | } 81 | 82 | String strType = searchParam.getParamType().getCode().toUpperCase(); 83 | 84 | SearchParamExtractor extractor = extractorFactory.getParameterExtractor(SearchParamTypes.valueOf(strType)); 85 | return extractor.extractValues(instance, searchParam); 86 | } 87 | 88 | @SuppressWarnings("unchecked") 89 | public IBaseResource parseResource(String resourceType, String json) throws ClassNotFoundException { 90 | Class type = (Class) Class.forName(resourceType).asSubclass(IBaseResource.class); 91 | return ctx.newJsonParser().parseResource(type, json); 92 | } 93 | 94 | /** 95 | * Parse a JSON document to a FHIR {@link IBaseResource}. 96 | * 97 | * @param json 98 | * @return 99 | */ 100 | public IBaseResource parseResource(String json) { 101 | return ctx.newJsonParser().parseResource(json); 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/test/resources/fhir/patient_f001.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Patient", 3 | "id": "f001", 4 | "text": { 5 | "status": "generated", 6 | "div": "

Generated Narrative with Details

id: f001

identifier: 738472983 (USUAL), ?? (USUAL)

active: true

name: Pieter van de Heuvel

telecom: ph: 0648352638(MOBILE), p.heuvel@gmail.com(HOME)

gender: male

birthDate: 17/11/1944

deceased: false

address: Van Egmondkade 23 Amsterdam 1024 RJ NLD (HOME)

maritalStatus: Getrouwd

multipleBirth: true

Contacts

-RelationshipNameTelecom
*Partner (Details : {http://hl7.org/fhir/patient-contact-relationship code partner = Partner)Sarah Abels ph: 0690383372(MOBILE)

Communications

-LanguagePreferred
*Nederlands (Details : {urn:ietf:bcp:47 code nl = ??, given as Dutch})true

managingOrganization: Burgers University Medical Centre

" 7 | }, 8 | "identifier": [ 9 | { 10 | "use": "usual", 11 | "system": "urn:oid:2.16.840.1.113883.2.4.6.3", 12 | "value": "738472983", 13 | "_value": { 14 | "fhir_comments": [ 15 | " BSN identification system " 16 | ] 17 | } 18 | }, 19 | { 20 | "fhir_comments": [ 21 | " BSN identification system " 22 | ], 23 | "use": "usual", 24 | "system": "urn:oid:2.16.840.1.113883.2.4.6.3" 25 | } 26 | ], 27 | "active": false, 28 | "name": [ 29 | { 30 | "use": "usual", 31 | "family": [ 32 | "van de Heuvel" 33 | ], 34 | "given": [ 35 | "Pieter" 36 | ], 37 | "suffix": [ 38 | "MSc" 39 | ] 40 | } 41 | ], 42 | "telecom": [ 43 | { 44 | "system": "phone", 45 | "value": "0648352638", 46 | "use": "mobile" 47 | }, 48 | { 49 | "system": "email", 50 | "value": "p.heuvel@gmail.com", 51 | "use": "home" 52 | } 53 | ], 54 | "gender": "male", 55 | "birthDate": "1944-11-17", 56 | "deceasedBoolean": false, 57 | "address": [ 58 | { 59 | "fhir_comments": [ 60 | " ISO 3166 Codes (Countries) " 61 | ], 62 | "use": "home", 63 | "line": [ 64 | "Van Egmondkade 23" 65 | ], 66 | "city": "Amsterdam", 67 | "postalCode": "1024 RJ", 68 | "country": "NLD" 69 | } 70 | ], 71 | "maritalStatus": { 72 | "coding": [ 73 | { 74 | "system": "http://hl7.org/fhir/v3/MaritalStatus", 75 | "code": "M", 76 | "display": "Married" 77 | } 78 | ], 79 | "text": "Getrouwd" 80 | }, 81 | "multipleBirthBoolean": true, 82 | "contact": [ 83 | { 84 | "relationship": [ 85 | { 86 | "coding": [ 87 | { 88 | "system": "http://hl7.org/fhir/patient-contact-relationship", 89 | "code": "partner" 90 | } 91 | ] 92 | } 93 | ], 94 | "name": { 95 | "use": "usual", 96 | "family": [ 97 | "Abels" 98 | ], 99 | "given": [ 100 | "Sarah" 101 | ] 102 | }, 103 | "telecom": [ 104 | { 105 | "system": "phone", 106 | "value": "0690383372", 107 | "use": "mobile" 108 | } 109 | ] 110 | } 111 | ], 112 | "communication": [ 113 | { 114 | "language": { 115 | "coding": [ 116 | { 117 | "system": "urn:ietf:bcp:47", 118 | "code": "nl", 119 | "_code": { 120 | "fhir_comments": [ 121 | " IETF language tag " 122 | ] 123 | }, 124 | "display": "Dutch" 125 | } 126 | ], 127 | "text": "Nederlands" 128 | }, 129 | "preferred": true 130 | } 131 | ], 132 | "managingOrganization": { 133 | "reference": "Organization/f001", 134 | "display": "Burgers University Medical Centre" 135 | } 136 | } -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/cache/SearchCache.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to STRATIO (C) under one or more contributor license agreements. 3 | * See the NOTICE file distributed with this work for additional information 4 | * regarding copyright ownership. The STRATIO (C) licenses this file 5 | * to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | package io.puntanegra.fhir.index.cache; 20 | 21 | import java.util.Optional; 22 | import java.util.UUID; 23 | 24 | import org.apache.cassandra.config.CFMetaData; 25 | import org.apache.cassandra.db.ClusteringComparator; 26 | import org.apache.cassandra.db.PartitionRangeReadCommand; 27 | import org.apache.cassandra.db.ReadCommand; 28 | import org.apache.lucene.search.Query; 29 | 30 | import com.google.common.cache.Cache; 31 | import com.google.common.cache.CacheBuilder; 32 | 33 | /** 34 | * Search cache to take advantage of Lucene's query cache. 35 | * 36 | * @author Andres de la Pena {@literal } 37 | */ 38 | public class SearchCache { 39 | 40 | private final ClusteringComparator comparator; 41 | private final Cache cache; 42 | 43 | /** 44 | * Constructor taking the base table metadata and the max number of cache entries. 45 | * 46 | * @param metadata the base table metadata 47 | * @param cacheSize the max number of cache entries 48 | */ 49 | public SearchCache(CFMetaData metadata, int cacheSize) { 50 | this.comparator = metadata.comparator; 51 | this.cache = CacheBuilder.newBuilder().maximumSize(cacheSize).build(); 52 | } 53 | 54 | void put(UUID key, SearchCacheEntry entry) { 55 | cache.put(key, entry); 56 | } 57 | 58 | /** 59 | * Puts a cache entry associating the specified search and {@link ReadCommand} with the specified {@link Query}. 60 | * 61 | * @param search the search 62 | * @param command the read command 63 | * @param query the cached query 64 | */ 65 | public void put(String search, ReadCommand command, Query query) { 66 | if (command instanceof PartitionRangeReadCommand) { 67 | PartitionRangeReadCommand rangeCommand = (PartitionRangeReadCommand) command; 68 | put(UUID.randomUUID(), new SearchCacheEntry(this, search, rangeCommand, query)); 69 | } 70 | } 71 | 72 | /** 73 | * Returns a {@link SearchCacheUpdater} for updating an entry associating the specified search and {@link 74 | * ReadCommand} with the specified {@link Query}. 75 | * 76 | * @param search the search 77 | * @param command the read command 78 | * @param query the cached query 79 | * @return the cache updater 80 | */ 81 | public SearchCacheUpdater updater(String search, ReadCommand command, Query query) { 82 | return new SearchCacheUpdater(this, search, UUID.randomUUID(), command, query); 83 | } 84 | 85 | /** 86 | * Discards all cached entries. 87 | */ 88 | public void invalidate() { 89 | cache.invalidateAll(); 90 | } 91 | 92 | /** 93 | * Gets the optional {@link SearchCacheEntry} associated to the specified search and {@link ReadCommand}. 94 | * 95 | * @param search the search 96 | * @param command the read command 97 | * @return the cache entry, maybe empty 98 | */ 99 | public Optional get(String search, ReadCommand command) { 100 | if (command instanceof PartitionRangeReadCommand) { 101 | PartitionRangeReadCommand rangeCommand = (PartitionRangeReadCommand) command; 102 | return cache.asMap().values().stream().filter(e -> e.isValid(comparator, search, rangeCommand)).findAny(); 103 | } 104 | return Optional.empty(); 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/search/extractor/StringsSearchParameterExtractor.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.search.extractor; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.hl7.fhir.dstu3.model.Address; 9 | import org.hl7.fhir.dstu3.model.ContactPoint; 10 | import org.hl7.fhir.dstu3.model.DateType; 11 | import org.hl7.fhir.dstu3.model.HumanName; 12 | import org.hl7.fhir.instance.model.api.IBase; 13 | import org.hl7.fhir.instance.model.api.IBaseResource; 14 | import org.hl7.fhir.instance.model.api.IPrimitiveType; 15 | 16 | import ca.uhn.fhir.context.FhirContext; 17 | import ca.uhn.fhir.context.RuntimeSearchParam; 18 | import io.puntanegra.fhir.index.search.datatypes.AbstractSearchParam; 19 | import io.puntanegra.fhir.index.search.datatypes.SearchParamDates; 20 | import io.puntanegra.fhir.index.search.datatypes.SearchParamString; 21 | import io.puntanegra.fhir.index.search.datatypes.SearchParamTypes; 22 | 23 | /** 24 | * Extracts information from string attribute types. 25 | * 26 | * @author Jorge L. Middleton {@literal } 27 | * 28 | */ 29 | public class StringsSearchParameterExtractor extends AbstractSearchParameterExtractor { 30 | 31 | public StringsSearchParameterExtractor(FhirContext ctx) { 32 | super(ctx); 33 | } 34 | 35 | @Override 36 | public Set extractValues(IBaseResource instance, RuntimeSearchParam searchParam) { 37 | 38 | Set values = new HashSet(); 39 | 40 | String path = searchParam.getPath(); 41 | String resourceName = searchParam.getName(); 42 | String paramType = getParamType(searchParam); 43 | 44 | for (Object obj : extractValues(path, instance)) { 45 | if (obj == null || ((IBase) obj).isEmpty()) { 46 | continue; 47 | } 48 | 49 | boolean multiType = false; 50 | if (path.endsWith("[x]")) { 51 | multiType = true; 52 | } 53 | 54 | // TODO: check if it is better to return the original value instead 55 | // of the String representation 56 | if (obj instanceof IPrimitiveType) { 57 | IPrimitiveType primitive = (IPrimitiveType) obj; 58 | String searchTerm = primitive.getValueAsString(); 59 | 60 | AbstractSearchParam def = null; 61 | if (primitive instanceof DateType) { 62 | def = new SearchParamDates(resourceName, path, SearchParamTypes.valueOf(paramType), 63 | ((DateType) primitive).getValue(), null); 64 | } else { 65 | def = new SearchParamString(resourceName, path, SearchParamTypes.valueOf(paramType), searchTerm); 66 | } 67 | values.add(def); 68 | 69 | } else { 70 | if (obj instanceof HumanName) { 71 | ArrayList allNames = new ArrayList(); 72 | HumanName nextHumanName = (HumanName) obj; 73 | allNames.add(nextHumanName.getFamilyAsSingleString()); 74 | allNames.add(nextHumanName.getGivenAsSingleString()); 75 | 76 | SearchParamString def = new SearchParamString(resourceName, path, 77 | SearchParamTypes.valueOf(paramType), StringUtils.join(allNames, ' ')); 78 | values.add(def); 79 | 80 | } else if (obj instanceof Address) { 81 | ArrayList allNames = new ArrayList(); 82 | Address nextAddress = (Address) obj; 83 | // TODO: allNames.addAll(nextAddress.getLine()); 84 | allNames.add(nextAddress.getCityElement().asStringValue()); 85 | allNames.add(nextAddress.getStateElement().asStringValue()); 86 | allNames.add(nextAddress.getCountryElement().asStringValue()); 87 | allNames.add(nextAddress.getPostalCodeElement().asStringValue()); 88 | 89 | SearchParamString def = new SearchParamString(resourceName, path, 90 | SearchParamTypes.valueOf(paramType), StringUtils.join(allNames, ' ')); 91 | values.add(def); 92 | 93 | } else if (obj instanceof ContactPoint) { 94 | ContactPoint contact = (ContactPoint) obj; 95 | if (contact.getValueElement().isEmpty() == false) { 96 | 97 | SearchParamString def = new SearchParamString(resourceName, path, 98 | SearchParamTypes.valueOf(paramType), contact.getValue()); 99 | values.add(def); 100 | } 101 | } else { 102 | if (!multiType) { 103 | // throw new SearchParameterException("Search param " + 104 | // resourceName 105 | // + " is of unexpected datatype: " + 106 | // obj.getClass()); 107 | } 108 | } 109 | } 110 | } 111 | return values; 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/lucene/LuceneRAMIndex.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to STRATIO (C) under one or more contributor license agreements. 3 | * See the NOTICE file distributed with this work for additional information 4 | * regarding copyright ownership. The STRATIO (C) licenses this file 5 | * to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | package io.puntanegra.fhir.index.lucene; 20 | 21 | import java.io.IOException; 22 | import java.util.LinkedList; 23 | import java.util.List; 24 | import java.util.Set; 25 | 26 | import org.apache.lucene.analysis.Analyzer; 27 | import org.apache.lucene.document.Document; 28 | import org.apache.lucene.index.DirectoryReader; 29 | import org.apache.lucene.index.IndexReader; 30 | import org.apache.lucene.index.IndexWriter; 31 | import org.apache.lucene.index.IndexWriterConfig; 32 | import org.apache.lucene.search.IndexSearcher; 33 | import org.apache.lucene.search.Query; 34 | import org.apache.lucene.search.ScoreDoc; 35 | import org.apache.lucene.search.Sort; 36 | import org.apache.lucene.search.TopDocs; 37 | import org.apache.lucene.store.Directory; 38 | import org.apache.lucene.store.RAMDirectory; 39 | 40 | import io.puntanegra.fhir.index.FhirIndexException; 41 | 42 | /** 43 | * Class wrapping a Lucene RAM directory and its readers, writers and searchers 44 | * for NRT. 45 | * 46 | * @author Andres de la Pena {@literal } 47 | */ 48 | public class LuceneRAMIndex { 49 | 50 | private final Directory directory; 51 | private final IndexWriter indexWriter; 52 | 53 | /** 54 | * Builds a new {@link LuceneRAMIndex}. 55 | * 56 | * @param analyzer 57 | * the index writer analyzer 58 | */ 59 | public LuceneRAMIndex(Analyzer analyzer) { 60 | try { 61 | directory = new RAMDirectory(); 62 | indexWriter = new IndexWriter(directory, new IndexWriterConfig(analyzer)); 63 | } catch (Exception e) { 64 | throw new FhirIndexException(e, "Error while creating index"); 65 | } 66 | } 67 | 68 | /** 69 | * Adds the specified {@link Document} 70 | * 71 | * @param document 72 | * the {@link Document} to be added 73 | */ 74 | public void add(Document document) { 75 | try { 76 | indexWriter.addDocument(document); 77 | } catch (Exception e) { 78 | throw new FhirIndexException(e, "Error while indexing %s", document); 79 | } 80 | } 81 | 82 | /** 83 | * Commits all changes to the index, waits for pending merges to complete, 84 | * and closes all associated resources. 85 | */ 86 | public void close() { 87 | try { 88 | indexWriter.close(); 89 | directory.close(); 90 | } catch (Exception e) { 91 | throw new FhirIndexException(e, "Error while closing"); 92 | } 93 | } 94 | 95 | /** 96 | * Finds the top {@code count} hits for {@code query} and sorting the hits 97 | * by {@code sort}. 98 | * 99 | * @param query 100 | * the {@link Query} to search for 101 | * @param sort 102 | * the {@link Sort} to be applied 103 | * @param count 104 | * the max number of results to be collected 105 | * @param fields 106 | * the names of the fields to be loaded 107 | * @return the found documents 108 | */ 109 | public List search(Query query, Sort sort, Integer count, Set fields) { 110 | try { 111 | indexWriter.commit(); 112 | IndexReader reader = DirectoryReader.open(directory); 113 | IndexSearcher searcher = new IndexSearcher(reader); 114 | sort = sort.rewrite(searcher); 115 | TopDocs topDocs = searcher.search(query, count, sort); 116 | ScoreDoc[] scoreDocs = topDocs.scoreDocs; 117 | List documents = new LinkedList<>(); 118 | for (ScoreDoc scoreDoc : scoreDocs) { 119 | Document document = searcher.doc(scoreDoc.doc, fields); 120 | documents.add(document); 121 | } 122 | searcher.getIndexReader().close(); 123 | return documents; 124 | } catch (IOException e) { 125 | throw new FhirIndexException(e, "Error while searching"); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /test-data/src/test/java/io/puntanegra/fhir/index/FhirTestDataTest.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index; 2 | 3 | import java.io.File; 4 | import java.io.FileReader; 5 | import java.util.Calendar; 6 | 7 | import org.hl7.fhir.dstu3.model.ContactPoint; 8 | import org.hl7.fhir.dstu3.model.ContactPoint.ContactPointSystem; 9 | import org.hl7.fhir.dstu3.model.Enumerations.AdministrativeGender; 10 | import org.hl7.fhir.dstu3.model.HumanName; 11 | import org.hl7.fhir.dstu3.model.Observation; 12 | import org.hl7.fhir.dstu3.model.Patient; 13 | import org.hl7.fhir.instance.model.api.IBaseResource; 14 | import org.junit.Before; 15 | import org.junit.Test; 16 | 17 | import com.datastax.driver.core.Cluster; 18 | import com.datastax.driver.core.ProtocolVersion; 19 | import com.datastax.driver.core.Session; 20 | 21 | import ca.uhn.fhir.context.FhirContext; 22 | import ca.uhn.fhir.context.FhirVersionEnum; 23 | import ca.uhn.fhir.parser.IParser; 24 | 25 | public class FhirTestDataTest { 26 | private FhirVersionEnum fhirVersion = FhirVersionEnum.DSTU3; 27 | private FhirContext ctx = new FhirContext(fhirVersion); 28 | 29 | private Session session; 30 | 31 | @Before 32 | public void init() throws Exception { 33 | 34 | String cassandraServer = System.getProperty("CassandraNode"); 35 | 36 | if (cassandraServer == null || cassandraServer.length() == 0) { 37 | cassandraServer = "localhost"; 38 | } 39 | 40 | Cluster cluster = Cluster.builder().addContactPoints(cassandraServer).withProtocolVersion(ProtocolVersion.V4) 41 | .build(); 42 | session = cluster.connect(); 43 | session.execute("USE test;"); 44 | } 45 | 46 | @Test 47 | public void loadTestData() throws Exception { 48 | loadObservationData(); 49 | //loadPatientData(); 50 | } 51 | 52 | public void loadObservationData() throws Exception { 53 | IParser parser = ctx.newJsonParser(); 54 | 55 | FileReader fileReader = new FileReader( 56 | new File(this.getClass().getClassLoader().getResource("fhir/observation_example001.json").getPath())); 57 | IBaseResource resource = parser.parseResource(fileReader); 58 | 59 | for (int i = 0; i < 1; i++) { 60 | 61 | resource.getIdElement().setValue("obs_" + i); 62 | ((Observation) resource).getIdentifier().get(0).setValue("urn:uuid:187e0c12-8dd2-67e2-99b2-bf273c1111" + i); 63 | 64 | String json = parser.encodeResourceToString(resource); 65 | 66 | long timestamp = Calendar.getInstance().getTimeInMillis(); 67 | session.execute( 68 | "INSERT INTO test.FHIR_RESOURCES (resource_id, version, resource_type, state, lastupdated, format, author, content)" 69 | + " VALUES ('" + resource.getIdElement().getValue() + "', 1, '" 70 | + resource.getClass().getSimpleName() + "', 'active', " + timestamp + ", 'json', 'dr who'," 71 | + "'" + json + "')"); 72 | 73 | System.out.println(resource.getClass().getSimpleName() + ": " + resource.getIdElement().getValue()); 74 | } 75 | } 76 | 77 | public void loadPatientData() throws Exception { 78 | IParser parser = ctx.newJsonParser(); 79 | NameGenerator nameGenerator = new NameGenerator(); 80 | 81 | FileReader fileReader = new FileReader( 82 | new File(this.getClass().getClassLoader().getResource("fhir/patient_f001.json").getPath())); 83 | IBaseResource resource = parser.parseResource(fileReader); 84 | 85 | for (int i = 0; i < 100; i++) { 86 | String family = nameGenerator.getName(); 87 | String given1 = nameGenerator.getName(); 88 | String email = given1.toLowerCase() + "." + family.toLowerCase() + "@gmail.com"; 89 | 90 | resource.getIdElement().setValue("pat_" + i); 91 | Patient patient = (Patient) resource; 92 | patient.getIdentifier().get(0).setValue(resource.getIdElement().getValue()); 93 | 94 | HumanName name = patient.getName().get(0); 95 | name.getFamily().clear(); 96 | name.getGiven().clear(); 97 | name.addFamily(family); 98 | name.addGiven(given1).addGiven(nameGenerator.getName()); 99 | 100 | patient.setGender(i % 2 == 0 ? AdministrativeGender.MALE : AdministrativeGender.FEMALE); 101 | 102 | patient.getTelecom().clear(); 103 | ContactPoint cp = new ContactPoint(); 104 | cp.setSystem(ContactPointSystem.EMAIL); 105 | cp.setValue(email); 106 | patient.addTelecom(cp); 107 | 108 | String json = parser.encodeResourceToString(resource); 109 | 110 | long timestamp = Calendar.getInstance().getTimeInMillis(); 111 | session.execute( 112 | "INSERT INTO test.FHIR_RESOURCES (resource_id, version, resource_type, state, lastupdated, format, author, content)" 113 | + " VALUES ('" + resource.getIdElement().getValue() + "', 1, '" 114 | + resource.getClass().getSimpleName() + "', 'active', " + timestamp + ", 'json', 'dr who'," 115 | + "'" + json + "')"); 116 | System.out.println(resource.getClass().getSimpleName() + ": " + resource.getIdElement().getValue() 117 | + ", family:" + family + ", email:" + email); 118 | } 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /fhir-index-plugin/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | 6 | io.puntanegra.fhir.cassandra 7 | cassandra-fhir-index 8 | 0.1.1 9 | 10 | 11 | fhir-index-plugin 12 | Cassandra FHIR Index Plugin 13 | Cassandra custom implementation to indexes FHIR Resources 14 | 15 | 16 | 3.0.4 17 | 3.0.0 18 | 5.3.0 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ca.uhn.hapi.fhir 27 | hapi-fhir-base 28 | ${hapi.version} 29 | 30 | 31 | 32 | ca.uhn.hapi.fhir 33 | hapi-fhir-structures-dstu3 34 | ${hapi.version} 35 | 36 | 37 | 38 | ca.uhn.hapi.fhir 39 | hapi-fhir-structures-hl7org-dstu2 40 | ${hapi.version} 41 | 42 | 43 | 44 | 45 | org.jscience 46 | jscience 47 | 4.3.1 48 | 49 | 50 | 51 | org.apache.cassandra 52 | cassandra-all 53 | ${cassandra.version} 54 | provided 55 | 56 | 57 | jcl-over-slf4j 58 | org.slf4j 59 | 60 | 61 | commons-lang3 62 | org.apache.commons 63 | 64 | 65 | 66 | 67 | org.apache.lucene 68 | lucene-core 69 | ${lucene.version} 70 | 71 | 72 | org.apache.lucene 73 | lucene-analyzers-common 74 | ${lucene.version} 75 | 76 | 77 | org.apache.lucene 78 | lucene-spatial 79 | ${lucene.version} 80 | 81 | 82 | org.apache.lucene 83 | lucene-queryparser 84 | ${lucene.version} 85 | 86 | 87 | com.vividsolutions 88 | jts-core 89 | 1.14.0 90 | provided 91 | 92 | 93 | 94 | org.mockito 95 | mockito-all 96 | 1.10.19 97 | test 98 | 99 | 100 | 101 | 102 | 103 | 104 | org.apache.maven.plugins 105 | maven-shade-plugin 106 | 2.3 107 | 108 | 109 | 110 | org.apache.commons.lang3 111 | org.shaded.commons.lang3 112 | 113 | 114 | 115 | 116 | ca.uhn.hapi.fhir:* 117 | org.apache.lucene:* 118 | org.codehaus.jackson:* 119 | org.jscience:* 120 | org.apache.commons:* 121 | org.glassfish:* 122 | org.codehaus.woodstox:* 123 | com.spatial4j:* 124 | net.sf.saxon:* 125 | javax.json.stream:* 126 | 127 | 128 | org.apache.cassandra:* 129 | 130 | 131 | 132 | 133 | *:* 134 | 135 | META-INF/*.SF 136 | META-INF/*.DSA 137 | META-INF/*.RSA 138 | 139 | 140 | 141 | false 142 | 143 | 144 | 145 | package 146 | 147 | shade 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /test-data/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | io.puntanegra.fhir.cassandra 5 | cassandra-fhir-index-test-data 6 | 0.0.1-SNAPSHOT 7 | Cassandra FHIR Index Test Data 8 | 9 | 10 | 11 | Apache License, Version 2.0 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | 15 | 16 | 17 | 18 | jorge.middleton 19 | Jorge L. Middleton 20 | jorge.middleton at gmail dot com 21 | 22 | 23 | 24 | 25 | UTF-8 26 | 1.8 27 | 1.8 28 | 1.8 29 | 3.0.4 30 | 3.0.0 31 | 5.3.0 32 | 1.9.2 33 | 1.5 34 | 4.12 35 | false 36 | false 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ca.uhn.hapi.fhir 45 | hapi-fhir-base 46 | ${hapi.version} 47 | 48 | 49 | 50 | 52 | 53 | 54 | ca.uhn.hapi.fhir 55 | hapi-fhir-structures-dstu3 56 | ${hapi.version} 57 | 58 | 59 | 60 | ca.uhn.hapi.fhir 61 | hapi-fhir-structures-hl7org-dstu2 62 | ${hapi.version} 63 | 64 | 65 | 66 | 67 | org.jscience 68 | jscience 69 | 4.3.1 70 | 71 | 72 | 73 | org.apache.cassandra 74 | cassandra-all 75 | ${cassandra.version} 76 | provided 77 | 78 | 79 | jcl-over-slf4j 80 | org.slf4j 81 | 82 | 83 | 84 | 85 | org.apache.lucene 86 | lucene-core 87 | ${lucene.version} 88 | 89 | 90 | org.apache.lucene 91 | lucene-analyzers-common 92 | ${lucene.version} 93 | 94 | 95 | org.apache.lucene 96 | lucene-spatial 97 | ${lucene.version} 98 | 99 | 100 | org.apache.lucene 101 | lucene-queryparser 102 | ${lucene.version} 103 | 104 | 105 | com.vividsolutions 106 | jts-core 107 | 1.14.0 108 | provided 109 | 110 | 111 | 112 | org.mockito 113 | mockito-all 114 | 1.10.19 115 | test 116 | 117 | 118 | org.cassandraunit 119 | cassandra-unit 120 | 3.0.0.1 121 | test 122 | 123 | 124 | org.apache.cassandra 125 | cassandra-all 126 | 127 | 128 | 129 | 130 | org.hectorclient 131 | hector-core 132 | 2.0-0 133 | test 134 | 135 | 136 | com.datastax.cassandra 137 | cassandra-driver-core 138 | ${cassandra.driver.version} 139 | test 140 | 141 | 142 | 143 | com.datastax.cassandra 144 | cassandra-driver-mapping 145 | ${cassandra.driver.version} 146 | test 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /test-data/src/test/resources/fhir/observation_example001.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "blood-pressure", 4 | "meta": { 5 | "lastUpdated": "2014-01-30T22:35:23+11:00" 6 | }, 7 | "text": { 8 | "fhir_comments": [ 9 | " \tEH Narrative created by build\n\t\n\t\t\n\t\t
Sept 17, 2012: Systolic Blood pressure 107/60 mmHg (low)
\n\t
\n " 10 | ], 11 | "status": "generated", 12 | "div": "

Generated Narrative with Details

id: blood-pressure

meta:

identifier: urn:uuid:187e0c12-8dd2-67e2-99b2-bf273c878281

status: final

code: Blood pressure systolic & diastolic (Details : {LOINC code 55284-4 = Blood pressure systolic and diastolic, given as Blood pressure systolic & diastolic})

subject: Patient/example

effective: 17/09/2012

performer: Practitioner/example

interpretation: low (Details : {http://hl7.org/fhir/v2/0078 code L = Low, given as Below low normal})

bodySite: Right arm (Details : {SNOMED CT code 368209003 = 368209003, given as Right arm})

component

code: Systolic blood pressure (Details : {LOINC code 8480-6 = Systolic blood pressure, given as Systolic blood pressure}; {SNOMED CT code 271649006 = 271649006, given as Systolic blood pressure}; {http://acme.org/devices/clinical-codes code bp-s = ??, given as Systolic Blood pressure})

value: 107 mm[Hg]

component

code: Diastolic blood pressure (Details : {LOINC code 8462-4 = Diastolic blood pressure, given as Diastolic blood pressure})

value: 60 mm[Hg]

" 13 | }, 14 | "identifier": [ 15 | { 16 | "system": "urn:ietf:rfc:3986", 17 | "value": "urn:uuid:187e0c12-8dd2-67e2-99b2-bf273c878281" 18 | } 19 | ], 20 | "status": "final", 21 | "code": { 22 | "coding": [ 23 | { 24 | "system": "http://loinc.org", 25 | "code": "2112345-51", 26 | "display": "Blood pressure systolic & diastolic" 27 | } 28 | ] 29 | }, 30 | "subject": { 31 | "reference": "Patient/example23" 32 | }, 33 | "effectiveDateTime": "2016-08-17T07:14:35+10:00", 34 | "performer": [ 35 | { 36 | "reference": "Practitioner/example" 37 | } 38 | ], 39 | "interpretation": { 40 | "fhir_comments": [ 41 | " an interpretation offered to the combination observation\n generally, it would only be appropriate to offer an interpretation\n of an observation that has no value if it has \"COMP\" (component)\n observations " 42 | ], 43 | "coding": [ 44 | { 45 | "system": "http://hl7.org/fhir/v2/0078", 46 | "code": "L", 47 | "display": "Below low normal" 48 | } 49 | ], 50 | "text": "low" 51 | }, 52 | "bodySite": { 53 | "coding": [ 54 | { 55 | "system": "http://snomed.info/sct", 56 | "code": "368209003", 57 | "display": "Right arm" 58 | } 59 | ] 60 | }, 61 | "component": [ 62 | { 63 | "code": { 64 | "fhir_comments": [ 65 | " \n Observations are often coded in multiple code systems.\n - LOINC provides a very specific code (though not more specific in this particular case)\n - snomed provides a clinically relevant code that is usually less granular than LOINC\n - the source system provides its own code, which may be less or more granular than LOINC\n ", 66 | " that shows the concept. The next two codes only have a LOINC code " 67 | ], 68 | "coding": [ 69 | { 70 | "fhir_comments": [ 71 | " LOINC -code " 72 | ], 73 | "system": "http://loinc.org", 74 | "code": "8480-6", 75 | "display": "Systolic blood pressure" 76 | }, 77 | { 78 | "fhir_comments": [ 79 | " SNOMED CT Codes " 80 | ], 81 | "system": "http://snomed.info/sct", 82 | "code": "271649006", 83 | "display": "Systolic blood pressure" 84 | }, 85 | { 86 | "fhir_comments": [ 87 | " Also, a local code specific to the source system " 88 | ], 89 | "system": "http://acme.org/devices/clinical-codes", 90 | "code": "bp-s", 91 | "display": "Systolic Blood pressure" 92 | } 93 | ] 94 | }, 95 | "valueQuantity": { 96 | "fhir_comments": [ 97 | " no standard units used in this example " 98 | ], 99 | "value": 107, 100 | "unit": "mm[Hg]" 101 | } 102 | }, 103 | { 104 | "code": { 105 | "coding": [ 106 | { 107 | "system": "http://loinc.org", 108 | "code": "8462-4", 109 | "display": "Diastolic blood pressure" 110 | } 111 | ] 112 | }, 113 | "valueQuantity": { 114 | "fhir_comments": [ 115 | " no formal units in this example " 116 | ], 117 | "value": 60, 118 | "unit": "mm[Hg]" 119 | } 120 | } 121 | ] 122 | } -------------------------------------------------------------------------------- /fhir-index-plugin/src/test/resources/fhir/observation_example001.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "blood-pressure", 4 | "meta": { 5 | "lastUpdated": "2014-01-30T22:35:23+11:00" 6 | }, 7 | "text": { 8 | "fhir_comments": [ 9 | " \tEH Narrative created by build\n\t\n\t\t\n\t\t
Sept 17, 2012: Systolic Blood pressure 107/60 mmHg (low)
\n\t
\n " 10 | ], 11 | "status": "generated", 12 | "div": "

Generated Narrative with Details

id: blood-pressure

meta:

identifier: urn:uuid:187e0c12-8dd2-67e2-99b2-bf273c878281

status: final

code: Blood pressure systolic & diastolic (Details : {LOINC code 55284-4 = Blood pressure systolic and diastolic, given as Blood pressure systolic & diastolic})

subject: Patient/example

effective: 17/09/2012

performer: Practitioner/example

interpretation: low (Details : {http://hl7.org/fhir/v2/0078 code L = Low, given as Below low normal})

bodySite: Right arm (Details : {SNOMED CT code 368209003 = 368209003, given as Right arm})

component

code: Systolic blood pressure (Details : {LOINC code 8480-6 = Systolic blood pressure, given as Systolic blood pressure}; {SNOMED CT code 271649006 = 271649006, given as Systolic blood pressure}; {http://acme.org/devices/clinical-codes code bp-s = ??, given as Systolic Blood pressure})

value: 107 mm[Hg]

component

code: Diastolic blood pressure (Details : {LOINC code 8462-4 = Diastolic blood pressure, given as Diastolic blood pressure})

value: 60 mm[Hg]

" 13 | }, 14 | "identifier": [ 15 | { 16 | "system": "urn:ietf:rfc:3986", 17 | "value": "urn:uuid:187e0c12-8dd2-67e2-99b2-bf273c878281" 18 | } 19 | ], 20 | "status": "final", 21 | "code": { 22 | "coding": [ 23 | { 24 | "system": "http://loinc.org", 25 | "code": "12345-5", 26 | "display": "Blood pressure systolic & diastolic" 27 | } 28 | ] 29 | }, 30 | "subject": { 31 | "reference": "Patient/example" 32 | }, 33 | "effectiveDateTime": "2015-05-14T18:05:19", 34 | "performer": [ 35 | { 36 | "reference": "Practitioner/example" 37 | } 38 | ], 39 | "interpretation": { 40 | "fhir_comments": [ 41 | " an interpretation offered to the combination observation\n generally, it would only be appropriate to offer an interpretation\n of an observation that has no value if it has \"COMP\" (component)\n observations " 42 | ], 43 | "coding": [ 44 | { 45 | "system": "http://hl7.org/fhir/v2/0078", 46 | "code": "L", 47 | "display": "Below low normal" 48 | } 49 | ], 50 | "text": "low" 51 | }, 52 | "bodySite": { 53 | "coding": [ 54 | { 55 | "system": "http://snomed.info/sct", 56 | "code": "368209003", 57 | "display": "Right arm" 58 | } 59 | ] 60 | }, 61 | "component": [ 62 | { 63 | "code": { 64 | "fhir_comments": [ 65 | " \n Observations are often coded in multiple code systems.\n - LOINC provides a very specific code (though not more specific in this particular case)\n - snomed provides a clinically relevant code that is usually less granular than LOINC\n - the source system provides its own code, which may be less or more granular than LOINC\n ", 66 | " that shows the concept. The next two codes only have a LOINC code " 67 | ], 68 | "coding": [ 69 | { 70 | "fhir_comments": [ 71 | " LOINC -code " 72 | ], 73 | "system": "http://loinc.org", 74 | "code": "8480-6", 75 | "display": "Systolic blood pressure" 76 | }, 77 | { 78 | "fhir_comments": [ 79 | " SNOMED CT Codes " 80 | ], 81 | "system": "http://snomed.info/sct", 82 | "code": "271649006", 83 | "display": "Systolic blood pressure" 84 | }, 85 | { 86 | "fhir_comments": [ 87 | " Also, a local code specific to the source system " 88 | ], 89 | "system": "http://acme.org/devices/clinical-codes", 90 | "code": "bp-s", 91 | "display": "Systolic Blood pressure" 92 | } 93 | ] 94 | }, 95 | "valueQuantity": { 96 | "fhir_comments": [ 97 | " no standard units used in this example " 98 | ], 99 | "value": 107, 100 | "unit": "mm[Hg]" 101 | } 102 | }, 103 | { 104 | "code": { 105 | "coding": [ 106 | { 107 | "system": "http://loinc.org", 108 | "code": "8462-4", 109 | "display": "Diastolic blood pressure" 110 | } 111 | ] 112 | }, 113 | "valueQuantity": { 114 | "fhir_comments": [ 115 | " no formal units in this example " 116 | ], 117 | "value": 60, 118 | "unit": "mm[Hg]" 119 | } 120 | } 121 | ] 122 | } -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/mapper/TokenMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to STRATIO (C) under one or more contributor license agreements. 3 | * See the NOTICE file distributed with this work for additional information 4 | * regarding copyright ownership. The STRATIO (C) licenses this file 5 | * to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | package io.puntanegra.fhir.index.mapper; 20 | 21 | import java.nio.ByteBuffer; 22 | 23 | import org.apache.cassandra.db.DecoratedKey; 24 | import org.apache.cassandra.db.PartitionPosition; 25 | import org.apache.cassandra.db.marshal.LongType; 26 | import org.apache.cassandra.dht.Token; 27 | import org.apache.lucene.document.Document; 28 | import org.apache.lucene.document.Field; 29 | import org.apache.lucene.document.FieldType; 30 | import org.apache.lucene.document.LongField; 31 | import org.apache.lucene.index.DocValuesType; 32 | import org.apache.lucene.index.IndexOptions; 33 | import org.apache.lucene.index.Term; 34 | import org.apache.lucene.search.Query; 35 | import org.apache.lucene.search.SortField; 36 | import org.apache.lucene.search.TermQuery; 37 | import org.apache.lucene.util.BytesRef; 38 | import org.apache.lucene.util.BytesRefBuilder; 39 | import org.apache.lucene.util.NumericUtils; 40 | 41 | /** 42 | * Class for several token mappings between Cassandra and Lucene. 43 | * 44 | * @author Andres de la Pena {@literal } 45 | */ 46 | public final class TokenMapper { 47 | 48 | /** The Lucene field name */ 49 | static final String FIELD_NAME = "_token"; 50 | 51 | /** The Lucene field type */ 52 | static final FieldType FIELD_TYPE = new FieldType(); 53 | 54 | static { 55 | FIELD_TYPE.setTokenized(true); 56 | FIELD_TYPE.setOmitNorms(true); 57 | FIELD_TYPE.setIndexOptions(IndexOptions.DOCS); 58 | FIELD_TYPE.setNumericType(FieldType.NumericType.LONG); 59 | FIELD_TYPE.setDocValuesType(DocValuesType.NUMERIC); 60 | FIELD_TYPE.freeze(); 61 | } 62 | 63 | /** 64 | * Adds to the specified {@link Document} the {@link Field}s associated to 65 | * the token of the specified row key. 66 | * 67 | * @param document 68 | * a {@link Document} 69 | * @param key 70 | * the raw partition key to be added 71 | */ 72 | public void addFields(Document document, DecoratedKey key) { 73 | Token token = key.getToken(); 74 | Long value = value(token); 75 | Field field = new LongField(FIELD_NAME, value, FIELD_TYPE); 76 | document.add(field); 77 | } 78 | 79 | /** 80 | * Returns the {code Long} value of the specified Murmur3 partitioning 81 | * {@link Token}. 82 | * 83 | * @param token 84 | * a Murmur3 token 85 | * @return the {@code token}'s {code Long} value 86 | */ 87 | public static Long value(Token token) { 88 | return (Long) token.getTokenValue(); 89 | } 90 | 91 | /** 92 | * Returns the {code ByteBuffer} value of the specified Murmur3 partitioning 93 | * {@link Token}. 94 | * 95 | * @param token 96 | * a Murmur3 token 97 | * @return the {@code token}'s {code ByteBuffer} value 98 | */ 99 | public static ByteBuffer byteBuffer(Token token) { 100 | return LongType.instance.decompose(value(token)); 101 | } 102 | 103 | /** 104 | * Returns the {@link BytesRef} indexing value of the specified Murmur3 105 | * partitioning {@link Token}. 106 | * 107 | * @param token 108 | * a Murmur3 token 109 | * @return the {@code token}'s indexing value 110 | */ 111 | private static BytesRef bytesRef(Token token) { 112 | Long value = value(token); 113 | BytesRefBuilder bytesRef = new BytesRefBuilder(); 114 | NumericUtils.longToPrefixCoded(value, 0, bytesRef); 115 | return bytesRef.get(); 116 | } 117 | 118 | /** 119 | * Returns a Lucene {@link SortField} for sorting documents/rows according 120 | * to the partitioner's order. 121 | * 122 | * @return a sort field for sorting by token 123 | */ 124 | public SortField sortField() { 125 | return new SortField(FIELD_NAME, SortField.Type.LONG); 126 | } 127 | 128 | /** 129 | * Returns if the specified lower partition position must be included in a 130 | * filtered range. 131 | * 132 | * @param position 133 | * a {@link PartitionPosition} 134 | * @return {@code true} if {@code position} must be included, {@code false} 135 | * otherwise 136 | */ 137 | public boolean includeStart(PartitionPosition position) { 138 | return position.kind() == PartitionPosition.Kind.MIN_BOUND; 139 | } 140 | 141 | /** 142 | * Returns if the specified upper partition position must be included in a 143 | * filtered range. 144 | * 145 | * @param position 146 | * a {@link PartitionPosition} 147 | * @return {@code true} if {@code position} must be included, {@code false} 148 | * otherwise 149 | */ 150 | public boolean includeStop(PartitionPosition position) { 151 | return position.kind() == PartitionPosition.Kind.MAX_BOUND; 152 | } 153 | 154 | /** 155 | * Returns a Lucene {@link Query} to find the {@link Document}s containing 156 | * the specified {@link Token}. 157 | * 158 | * @param token 159 | * the token 160 | * @return the query to find the documents containing {@code token} 161 | */ 162 | public Query query(Token token) { 163 | return new TermQuery(new Term(FIELD_NAME, bytesRef(token))); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/FhirIndexIndexer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to STRATIO (C) under one or more contributor license agreements. 3 | * See the NOTICE file distributed with this work for additional information 4 | * regarding copyright ownership. The STRATIO (C) licenses this file 5 | * to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | package io.puntanegra.fhir.index; 20 | 21 | import java.util.LinkedHashMap; 22 | import java.util.Map; 23 | import java.util.Optional; 24 | 25 | import org.apache.cassandra.db.Clustering; 26 | import org.apache.cassandra.db.DecoratedKey; 27 | import org.apache.cassandra.db.DeletionTime; 28 | import org.apache.cassandra.db.RangeTombstone; 29 | import org.apache.cassandra.db.marshal.UTF8Type; 30 | import org.apache.cassandra.db.rows.Cell; 31 | import org.apache.cassandra.db.rows.Row; 32 | import org.apache.cassandra.index.Index; 33 | import org.apache.cassandra.index.transactions.IndexTransaction; 34 | import org.apache.cassandra.utils.concurrent.OpOrder; 35 | import org.slf4j.Logger; 36 | import org.slf4j.LoggerFactory; 37 | 38 | import io.puntanegra.fhir.index.util.ByteBufferUtils; 39 | 40 | /** 41 | * Lucene-based {@link Index.Indexer} that processes events emitted during partition updates. 42 | * 43 | * @author Jorge L. Middleton {@literal } 44 | * 45 | */ 46 | public class FhirIndexIndexer implements Index.Indexer { 47 | 48 | private static final Logger logger = LoggerFactory.getLogger(FhirIndexIndexer.class); 49 | 50 | private final FhirIndexService service; 51 | private final DecoratedKey key; 52 | private final int nowInSec; 53 | private final OpOrder.Group opGroup; 54 | private final IndexTransaction.Type transactionType; 55 | private final Map> rows; 56 | 57 | /** 58 | * Builds a new {@link FhirIndexIndexer} for tables with wide rows. 59 | * 60 | * @param service 61 | * the service to perform the indexing operation 62 | * @param key 63 | * key of the partition being modified 64 | * @param nowInSec 65 | * current time of the update operation 66 | * @param opGroup 67 | * operation group spanning the update operation 68 | * @param transactionType 69 | * what kind of update is being performed on the base data 70 | */ 71 | public FhirIndexIndexer(FhirIndexService service, DecoratedKey key, int nowInSec, OpOrder.Group opGroup, 72 | IndexTransaction.Type transactionType) { 73 | this.service = service; 74 | this.key = key; 75 | this.nowInSec = nowInSec; 76 | this.opGroup = opGroup; 77 | this.transactionType = transactionType; 78 | rows = new LinkedHashMap<>(); 79 | } 80 | 81 | @Override 82 | public void begin() { 83 | } 84 | 85 | @Override 86 | public void partitionDelete(DeletionTime deletionTime) { 87 | logger.trace("Delete partition inoked {}: {}", this.transactionType, deletionTime); 88 | delete(); 89 | } 90 | 91 | @Override 92 | public void rangeTombstone(RangeTombstone tombstone) { 93 | // nothing to do here 94 | } 95 | 96 | @Override 97 | public void insertRow(Row row) { 98 | logger.trace("Inserting row {}: {}", this.transactionType, row); 99 | index(row); 100 | } 101 | 102 | @Override 103 | public void updateRow(Row oldRowData, Row newRowData) { 104 | logger.trace("Updating row {}: {} to {}", this.transactionType, oldRowData, newRowData); 105 | index(newRowData); 106 | } 107 | 108 | @Override 109 | public void removeRow(Row row) { 110 | logger.trace("Removing row {}: {}", this.transactionType, row); 111 | index(row); 112 | } 113 | 114 | /** 115 | * Temporally store the row to be indexed in a map. 116 | * 117 | * @param row 118 | * the row to be indexed. 119 | */ 120 | public void index(Row row) { 121 | if (!row.isStatic()) { 122 | Clustering clustering = row.clustering(); 123 | if (service.needsReadBeforeWrite(key, row)) { 124 | rows.put(clustering, Optional.empty()); 125 | } else { 126 | rows.put(clustering, Optional.of(row)); 127 | } 128 | } 129 | } 130 | 131 | public void finish() { 132 | // Read required rows from storage engine 133 | service.read(key, nowInSec, opGroup).forEachRemaining(unfiltered -> { 134 | Row row = (Row) unfiltered; 135 | rows.put(row.clustering(), Optional.of(row)); 136 | }); 137 | 138 | // Write rows to Lucene index 139 | rows.forEach((clustering, optional) -> optional.ifPresent(row -> { 140 | if (row.hasLiveData(nowInSec)) { 141 | service.upsert(key, row); 142 | } else { 143 | service.delete(key, row); 144 | } 145 | })); 146 | } 147 | 148 | /** 149 | * Delete the partition. 150 | */ 151 | private void delete() { 152 | service.delete(key); 153 | rows.clear(); 154 | } 155 | 156 | @SuppressWarnings("unused") 157 | private boolean insertInIndex(final String indexResourceType, Row row) { 158 | // only index in the correct lucene index. 159 | // this code checks if the resourceType is the same as the index. 160 | // TODO: externalizar resource_type 161 | 162 | for (Cell cell : row.cells()) { 163 | String columnname = cell.column().name.toString(); 164 | if ("resource_type".equals(columnname)) { 165 | String resourceType = ByteBufferUtils.toString(cell.value(), UTF8Type.instance); 166 | 167 | if (indexResourceType.equals(resourceType)) { 168 | return true; 169 | } 170 | return false; 171 | } 172 | } 173 | return false; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/cache/SearchCacheEntry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to STRATIO (C) under one or more contributor license agreements. 3 | * See the NOTICE file distributed with this work for additional information 4 | * regarding copyright ownership. The STRATIO (C) licenses this file 5 | * to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | package io.puntanegra.fhir.index.cache; 20 | 21 | import java.util.Optional; 22 | 23 | import org.apache.cassandra.db.Clustering; 24 | import org.apache.cassandra.db.ClusteringComparator; 25 | import org.apache.cassandra.db.ClusteringPrefix; 26 | import org.apache.cassandra.db.DataRange; 27 | import org.apache.cassandra.db.DecoratedKey; 28 | import org.apache.cassandra.db.PartitionPosition; 29 | import org.apache.cassandra.db.PartitionRangeReadCommand; 30 | import org.apache.cassandra.db.ReadCommand; 31 | import org.apache.lucene.search.Query; 32 | import org.apache.lucene.search.ScoreDoc; 33 | 34 | import io.puntanegra.fhir.index.mapper.KeyMapper; 35 | 36 | /** 37 | * A entry of the {@link SearchCache}. 38 | * 39 | * @author Andres de la Pena {@literal } 40 | */ 41 | public class SearchCacheEntry { 42 | 43 | private final SearchCache searchCache; 44 | private final String search; 45 | private final ReadCommand command; 46 | private final PartitionPosition currentPosition; 47 | private final Optional currentClustering; 48 | private final ScoreDoc scoreDoc; 49 | private final Query query; 50 | private final PartitionPosition startPosition; 51 | private final PartitionPosition stopPosition; 52 | private final Optional startPrefix; 53 | private final Optional stopPrefix; 54 | 55 | SearchCacheEntry(SearchCache searchCache, String search, PartitionRangeReadCommand command, Query query) { 56 | this.searchCache = searchCache; 57 | this.search = search; 58 | this.command = command; 59 | this.query = query; 60 | startPosition = command.dataRange().startKey(); 61 | stopPosition = command.dataRange().stopKey(); 62 | startPrefix = KeyMapper.startClusteringPrefix(command.dataRange()); 63 | stopPrefix = KeyMapper.stopClusteringPrefix(command.dataRange()); 64 | currentPosition = startPosition; 65 | currentClustering = startPrefix.isPresent() ? Optional.of(new Clustering(startPrefix.get().getRawValues())) 66 | : Optional.empty(); 67 | scoreDoc = null; 68 | } 69 | 70 | SearchCacheEntry(SearchCache searchCache, String search, PartitionRangeReadCommand command, 71 | DecoratedKey decoratedKey, Optional currentClustering, ScoreDoc scoreDoc, Query query) { 72 | this.searchCache = searchCache; 73 | this.search = search; 74 | this.command = command; 75 | this.currentPosition = decoratedKey; 76 | this.currentClustering = currentClustering; 77 | this.scoreDoc = scoreDoc; 78 | this.query = query; 79 | startPosition = command.dataRange().startKey(); 80 | stopPosition = command.dataRange().stopKey(); 81 | startPrefix = KeyMapper.startClusteringPrefix(command.dataRange()); 82 | stopPrefix = KeyMapper.stopClusteringPrefix(command.dataRange()); 83 | } 84 | 85 | boolean isValid(ClusteringComparator comparator, String search, PartitionRangeReadCommand command) { 86 | if (search.equals(this.search)) { 87 | DataRange dataRange = command.dataRange(); 88 | return validKey(dataRange) && validPrefix(comparator, dataRange); 89 | } 90 | return false; 91 | } 92 | 93 | private boolean validKey(DataRange dataRange) { 94 | PartitionPosition start = dataRange.startKey(); 95 | if (currentPosition.compareTo(start) == 0 && startPosition.compareTo(start) <= 0) { 96 | PartitionPosition stop = dataRange.stopKey(); 97 | return stopPosition.compareTo(stop) == 0; 98 | } 99 | return false; 100 | } 101 | 102 | private boolean validPrefix(ClusteringComparator comparator, DataRange dataRange) { 103 | 104 | // Discard start prefix 105 | Optional start = KeyMapper.startClusteringPrefix(dataRange); 106 | if (start.isPresent() && startPrefix.isPresent() && comparator.compare(startPrefix.get(), start.get()) > 0) { 107 | return false; 108 | } 109 | 110 | // Discard null clusterings 111 | if (start.isPresent() != currentClustering.isPresent()) { 112 | return false; 113 | } 114 | 115 | // Discard clustering 116 | if (start.isPresent() 117 | && comparator.compare(new Clustering(start.get().getRawValues()), currentClustering.get()) != 0) { 118 | return false; 119 | } 120 | 121 | // Discard stop prefix 122 | Optional stop = KeyMapper.stopClusteringPrefix(dataRange); 123 | if (stop.isPresent() && stopPrefix.isPresent() && comparator.compare(stopPrefix.get(), stop.get()) != 0) { 124 | return false; 125 | } 126 | 127 | return true; 128 | } 129 | 130 | /** 131 | * Returns a new {@link SearchCacheUpdater} for updating this entry. 132 | * 133 | * @return the cache updater 134 | */ 135 | public SearchCacheUpdater updater() { 136 | return searchCache.updater(search, command, query); 137 | } 138 | 139 | /** 140 | * Returns the cached {@link ScoreDoc}. 141 | * 142 | * @return the score of the last cached position 143 | */ 144 | public ScoreDoc getScoreDoc() { 145 | return scoreDoc; 146 | } 147 | 148 | /** 149 | * Returns the cached {@link Query}. 150 | * 151 | * @return the cached Lucene's query 152 | */ 153 | public Query getQuery() { 154 | return query; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/lucene/LuceneDocumentIterator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to STRATIO (C) under one or more contributor license agreements. 3 | * See the NOTICE file distributed with this work for additional information 4 | * regarding copyright ownership. The STRATIO (C) licenses this file 5 | * to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | package io.puntanegra.fhir.index.lucene; 19 | 20 | import java.util.Deque; 21 | import java.util.Iterator; 22 | import java.util.LinkedList; 23 | import java.util.NoSuchElementException; 24 | import java.util.Set; 25 | 26 | import org.apache.cassandra.utils.CloseableIterator; 27 | import org.apache.cassandra.utils.Pair; 28 | import org.apache.lucene.document.Document; 29 | import org.apache.lucene.search.IndexSearcher; 30 | import org.apache.lucene.search.Query; 31 | import org.apache.lucene.search.ScoreDoc; 32 | import org.apache.lucene.search.SearcherManager; 33 | import org.apache.lucene.search.Sort; 34 | import org.apache.lucene.search.TopDocs; 35 | import org.slf4j.Logger; 36 | import org.slf4j.LoggerFactory; 37 | 38 | import io.puntanegra.fhir.index.FhirIndexException; 39 | import io.puntanegra.fhir.index.util.TimeCounter; 40 | 41 | /** 42 | * {@link Iterator} for retrieving Lucene {@link Document}s satisfying a 43 | * {@link Query} from an {@link IndexSearcher}. 44 | * 45 | * @author Jorge L. Middleton {@literal } 46 | * 47 | */ 48 | public class LuceneDocumentIterator implements CloseableIterator> { 49 | 50 | private static final Logger logger = LoggerFactory.getLogger(LuceneDocumentIterator.class); 51 | 52 | private final SearcherManager manager; 53 | private final Query query; 54 | private final Integer page; 55 | private final Set fields; 56 | private final Deque> documents = new LinkedList<>(); 57 | private Sort sort; 58 | private ScoreDoc after; 59 | private boolean mayHaveMore = true; 60 | 61 | /** 62 | * Builds a new iterator over the {@link Document}s satisfying the specified 63 | * {@link Query}. 64 | * 65 | * @param manager 66 | * the index searcher manager 67 | * @param query 68 | * the query to be satisfied by the documents 69 | * @param sort 70 | * the sort in which the documents are going to be retrieved 71 | * @param after 72 | * a pointer to the start document (not included) 73 | * @param limit 74 | * the max number of documents to be retrieved 75 | * @param fields 76 | * the names of the fields to be loaded 77 | */ 78 | LuceneDocumentIterator(SearcherManager manager, Query query, Sort sort, ScoreDoc after, Integer limit, 79 | Set fields) { 80 | this.manager = manager; 81 | this.query = query; 82 | this.sort = sort; 83 | this.after = after; 84 | this.page = limit < Integer.MAX_VALUE ? limit + 1 : limit; 85 | this.fields = fields; 86 | } 87 | 88 | private void fetch() { 89 | try { 90 | IndexSearcher searcher = manager.acquire(); 91 | 92 | try { 93 | 94 | TimeCounter time = TimeCounter.create().start(); 95 | 96 | // Search for top documents 97 | TopDocs topDocs = null; 98 | if (this.sort != null) { 99 | sort = sort.rewrite(searcher); 100 | topDocs = searcher.searchAfter(after, query, page, sort); 101 | } else { 102 | topDocs = searcher.searchAfter(after, query, page); 103 | } 104 | ScoreDoc[] scoreDocs = topDocs.scoreDocs; 105 | 106 | // Check inf mayHaveMore 107 | mayHaveMore = scoreDocs.length == page; 108 | 109 | // Collect the documents from query result 110 | for (ScoreDoc scoreDoc : scoreDocs) { 111 | Document document = searcher.doc(scoreDoc.doc, fields); 112 | documents.add(Pair.create(document, scoreDoc)); 113 | after = scoreDoc; 114 | } 115 | 116 | logger.debug("Get page with {} documents in {}", scoreDocs.length, time.stop()); 117 | 118 | } finally { 119 | manager.release(searcher); 120 | } 121 | 122 | } catch (Exception e) { 123 | e.printStackTrace(); 124 | throw new FhirIndexException(e, "Error searching in with %s and %s", query, sort); 125 | } 126 | } 127 | 128 | /** 129 | * Returns {@code true} if the iteration has more {@link Document}s. (In 130 | * other words, returns {@code true} if {@link #next} would return an 131 | * {@link Document} rather than throwing an exception.) 132 | * 133 | * @return {@code true} if the iteration has more {@link Document}s 134 | */ 135 | @Override 136 | public boolean hasNext() { 137 | if (needsFetch()) { 138 | fetch(); 139 | } 140 | return !documents.isEmpty(); 141 | } 142 | 143 | /** 144 | * Returns if more {@link Document}s should be fetched from the Lucene 145 | * index. 146 | * 147 | * @return {@code true} if more documents should be fetched, {@code false} 148 | * otherwise 149 | */ 150 | public boolean needsFetch() { 151 | return mayHaveMore && documents.isEmpty(); 152 | } 153 | 154 | /** 155 | * Returns the next {@link Document} in the iteration. 156 | * 157 | * @return the next document 158 | * @throws NoSuchElementException 159 | * if the iteration has no more {@link Document}s 160 | */ 161 | @Override 162 | public Pair next() { 163 | if (hasNext()) { 164 | return documents.poll(); 165 | } else { 166 | throw new NoSuchElementException(); 167 | } 168 | } 169 | 170 | /** {@inheritDoc} */ 171 | @Override 172 | public void close() { 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/FhirIndexSearcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to STRATIO (C) under one or more contributor license agreements. 3 | * See the NOTICE file distributed with this work for additional information 4 | * regarding copyright ownership. The STRATIO (C) licenses this file 5 | * to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | package io.puntanegra.fhir.index; 20 | 21 | import java.util.NavigableSet; 22 | 23 | import org.apache.cassandra.config.CFMetaData; 24 | import org.apache.cassandra.db.Clustering; 25 | import org.apache.cassandra.db.ClusteringComparator; 26 | import org.apache.cassandra.db.ColumnFamilyStore; 27 | import org.apache.cassandra.db.DecoratedKey; 28 | import org.apache.cassandra.db.ReadCommand; 29 | import org.apache.cassandra.db.ReadOrderGroup; 30 | import org.apache.cassandra.db.SinglePartitionReadCommand; 31 | import org.apache.cassandra.db.filter.ClusteringIndexFilter; 32 | import org.apache.cassandra.db.filter.ClusteringIndexNamesFilter; 33 | import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; 34 | import org.apache.cassandra.db.rows.UnfilteredRowIterator; 35 | import org.apache.cassandra.utils.Pair; 36 | import org.apache.lucene.document.Document; 37 | import org.apache.lucene.search.ScoreDoc; 38 | 39 | import io.puntanegra.fhir.index.cache.SearchCacheUpdater; 40 | import io.puntanegra.fhir.index.lucene.LuceneDocumentIterator; 41 | 42 | /** 43 | * {@link UnfilteredPartitionIterator} for retrieving rows from Cassandra 44 | * partition table. 45 | * 46 | * @author Jorge L. Middleton {@literal } 47 | * 48 | */ 49 | public class FhirIndexSearcher implements UnfilteredPartitionIterator { 50 | 51 | private final ReadCommand command; 52 | private final ColumnFamilyStore table; 53 | private final ReadOrderGroup orderGroup; 54 | private final LuceneDocumentIterator documents; 55 | private UnfilteredRowIterator next; 56 | 57 | private final FhirIndexService service; 58 | private final ClusteringComparator comparator; 59 | private final SearchCacheUpdater cacheUpdater; 60 | private Pair nextDoc; 61 | 62 | /** 63 | * Constructor taking the Cassandra read data and the Lucene results 64 | * iterator. 65 | * 66 | * @param service 67 | * the index service 68 | * @param command 69 | * the read command 70 | * @param table 71 | * the base table 72 | * @param orderGroup 73 | * the order group of the read operation 74 | * @param documents 75 | * the documents iterator 76 | * @param cacheUpdater 77 | * the search cache updater 78 | */ 79 | public FhirIndexSearcher(FhirIndexService service, ReadCommand command, ColumnFamilyStore table, 80 | ReadOrderGroup orderGroup, LuceneDocumentIterator documents, SearchCacheUpdater cacheUpdater) { 81 | 82 | this.command = command; 83 | this.table = table; 84 | this.orderGroup = orderGroup; 85 | this.documents = documents; 86 | 87 | this.service = service; 88 | this.comparator = service.metadata.comparator; 89 | this.cacheUpdater = cacheUpdater; 90 | } 91 | 92 | @Override 93 | public boolean isForThrift() { 94 | return command.isForThrift(); 95 | } 96 | 97 | @Override 98 | public CFMetaData metadata() { 99 | return table.metadata; 100 | } 101 | 102 | @Override 103 | public boolean hasNext() { 104 | if (next != null) { 105 | return true; 106 | } 107 | 108 | if (nextDoc == null) { 109 | if (!documents.hasNext()) { 110 | return false; 111 | } 112 | nextDoc = documents.next(); 113 | } 114 | 115 | DecoratedKey key = service.decoratedKey(nextDoc.left); 116 | NavigableSet clusterings = clusterings(key); 117 | 118 | if (clusterings.isEmpty()) { 119 | return hasNext(); 120 | } 121 | 122 | ClusteringIndexFilter filter = new ClusteringIndexNamesFilter(clusterings, false); 123 | UnfilteredRowIterator data = read(key, filter); 124 | 125 | if (data.isEmpty()) { 126 | data.close(); 127 | return hasNext(); 128 | } 129 | 130 | next = data; 131 | return true; 132 | } 133 | 134 | @Override 135 | public UnfilteredRowIterator next() { 136 | if (next == null) { 137 | hasNext(); 138 | } 139 | UnfilteredRowIterator result = next; 140 | next = null; 141 | return result; 142 | } 143 | 144 | @Override 145 | public void remove() { 146 | throw new UnsupportedOperationException(); 147 | } 148 | 149 | @Override 150 | public void close() { 151 | try { 152 | if (next != null) { 153 | next.close(); 154 | } 155 | } finally { 156 | documents.close(); 157 | } 158 | } 159 | 160 | public UnfilteredRowIterator read(DecoratedKey key, ClusteringIndexFilter filter) { 161 | return SinglePartitionReadCommand.create(isForThrift(), table.metadata, command.nowInSec(), 162 | command.columnFilter(), command.rowFilter(), command.limits(), key, filter) 163 | .queryMemtableAndDisk(table, orderGroup.baseReadOpOrderGroup()); 164 | } 165 | 166 | private NavigableSet clusterings(DecoratedKey key) { 167 | 168 | NavigableSet clusterings = service.clusterings(); 169 | Clustering clustering = service.clustering(nextDoc.left); 170 | 171 | Clustering lastClustering = null; 172 | while (nextDoc != null && key.getKey().equals(service.decoratedKey(nextDoc.left).getKey()) 173 | && (lastClustering == null || comparator.compare(lastClustering, clustering) < 0)) { 174 | if (command.selectsKey(key) && command.selectsClustering(key, clustering)) { 175 | lastClustering = clustering; 176 | clusterings.add(clustering); 177 | cacheUpdater.put(key, clustering, nextDoc.right); 178 | } 179 | if (documents.hasNext()) { 180 | nextDoc = documents.next(); 181 | clustering = service.clustering(nextDoc.left); 182 | } else { 183 | nextDoc = null; 184 | } 185 | if (documents.needsFetch()) { 186 | break; 187 | } 188 | } 189 | return clusterings; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/util/TaskQueue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to STRATIO (C) under one or more contributor license agreements. 3 | * See the NOTICE file distributed with this work for additional information 4 | * regarding copyright ownership. The STRATIO (C) licenses this file 5 | * to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | package io.puntanegra.fhir.index.util; 20 | 21 | import java.util.concurrent.ExecutionException; 22 | import java.util.concurrent.Future; 23 | import java.util.concurrent.TimeUnit; 24 | import java.util.concurrent.locks.ReadWriteLock; 25 | import java.util.concurrent.locks.ReentrantReadWriteLock; 26 | 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | import io.puntanegra.fhir.index.FhirIndexException; 31 | 32 | /** 33 | * A queue that executes each submitted task using one of possibly several pooled threads. Tasks can be submitted with 34 | * an identifier, ensuring that all tasks with same identifier will be executed orderly in the same thread. Each thread 35 | * has its own task queue. 36 | * 37 | * @author Andres de la Pena {@literal } 38 | */ 39 | public class TaskQueue { 40 | 41 | private static final Logger logger = LoggerFactory.getLogger(TaskQueue.class); 42 | 43 | private BlockingExecutor[] pools; 44 | 45 | private final ReadWriteLock lock = new ReentrantReadWriteLock(); 46 | 47 | /** 48 | * Returns a new {@link TaskQueue}. 49 | * 50 | * @param numThreads the number of executor threads 51 | * @param queuesSize the max number of tasks in each thread queue before blocking 52 | */ 53 | public TaskQueue(int numThreads, int queuesSize) { 54 | if (numThreads > 0) { 55 | pools = new BlockingExecutor[numThreads]; 56 | for (int i = 0; i < numThreads; i++) { 57 | pools[i] = new BlockingExecutor(1, 58 | queuesSize, 59 | Long.MAX_VALUE, 60 | TimeUnit.DAYS, 61 | 0, 62 | TimeUnit.NANOSECONDS, 63 | null); 64 | pools[i].submit(new Runnable() { 65 | @Override 66 | public void run() { 67 | logger.debug("Task queue starts"); 68 | } 69 | }); 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * Submits a non value-returning task for asynchronous execution. 76 | * 77 | * The specified identifier is used to choose the thread executor where the task will be queued. The selection and 78 | * load balancing is based in the {@link #hashCode()} of this identifier. 79 | * 80 | * @param id the identifier of the task used to choose the thread executor where the task will be queued for 81 | * asynchronous execution 82 | * @param task the task to be queued for asynchronous execution 83 | * @return a future for the submitted task 84 | */ 85 | public Future submitAsynchronous(Object id, Runnable task) { 86 | if (pools == null) { 87 | task.run(); 88 | return null; 89 | } else { 90 | lock.readLock().lock(); 91 | try { 92 | int i = Math.abs(id.hashCode() % pools.length); 93 | return pools[i].submit(task); 94 | } catch (Exception e) { 95 | logger.error("Task queue submission failed", e); 96 | throw new FhirIndexException(e); 97 | } finally { 98 | lock.readLock().unlock(); 99 | } 100 | } 101 | } 102 | 103 | /** 104 | * Submits a non value-returning task for synchronous execution. It waits for all synchronous tasks to be 105 | * completed. 106 | * 107 | * @param task a task to be executed synchronously 108 | */ 109 | public void submitSynchronous(Runnable task) { 110 | if (pools == null) { 111 | task.run(); 112 | } else { 113 | lock.writeLock().lock(); 114 | try { 115 | await(); 116 | task.run(); 117 | } finally { 118 | lock.writeLock().unlock(); 119 | } 120 | } 121 | } 122 | 123 | /** 124 | * Await for task completion. 125 | */ 126 | public void await() { 127 | if (pools != null) { 128 | lock.writeLock().lock(); 129 | try { 130 | Future[] futures = new Future[pools.length]; 131 | for (int i = 0; i < pools.length; i++) { 132 | Future future = pools[i].submit(() -> { 133 | }); 134 | futures[i] = future; 135 | } 136 | for (Future future : futures) { 137 | future.get(); 138 | } 139 | } catch (InterruptedException e) { 140 | logger.error("Task queue await interrupted", e); 141 | throw new FhirIndexException(e); 142 | } catch (ExecutionException e) { 143 | logger.error("Task queue await failed", e); 144 | throw new FhirIndexException(e); 145 | } finally { 146 | lock.writeLock().unlock(); 147 | } 148 | } 149 | } 150 | 151 | /** 152 | * Shutdowns this task. 153 | */ 154 | public void shutdown() { 155 | if (pools != null) { 156 | lock.writeLock().lock(); 157 | try { 158 | for (BlockingExecutor pool : pools) { 159 | pool.shutdown(); 160 | } 161 | } finally { 162 | lock.writeLock().unlock(); 163 | } 164 | } 165 | } 166 | 167 | } -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/util/ByteBufferUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to STRATIO (C) under one or more contributor license agreements. 3 | * See the NOTICE file distributed with this work for additional information 4 | * regarding copyright ownership. The STRATIO (C) licenses this file 5 | * to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | package io.puntanegra.fhir.index.util; 20 | 21 | import java.nio.ByteBuffer; 22 | import java.util.List; 23 | 24 | import org.apache.cassandra.db.marshal.AbstractType; 25 | import org.apache.cassandra.db.marshal.CompositeType; 26 | import org.apache.cassandra.utils.ByteBufferUtil; 27 | import org.apache.cassandra.utils.Hex; 28 | import org.apache.lucene.util.BytesRef; 29 | 30 | /** 31 | * Utility class with some {@link ByteBuffer}/{@link AbstractType} utilities. 32 | * 33 | * @author Andres de la Pena {@literal } 34 | */ 35 | public final class ByteBufferUtils { 36 | 37 | /** Private constructor to hide the implicit public one. */ 38 | private ByteBufferUtils() { 39 | } 40 | 41 | /** 42 | * Returns the specified {@link ByteBuffer} as a byte array. 43 | * 44 | * @param byteBuffer a {@link ByteBuffer} to be converted to a byte array 45 | * @return the byte array representation of the {@code byteBuffer} 46 | */ 47 | public static byte[] asArray(ByteBuffer byteBuffer) { 48 | ByteBuffer bb = ByteBufferUtil.clone(byteBuffer); 49 | byte[] bytes = new byte[bb.remaining()]; 50 | bb.get(bytes); 51 | return bytes; 52 | } 53 | 54 | /** 55 | * Returns {@code true} if the specified {@link ByteBuffer} is empty, {@code false} otherwise. 56 | * 57 | * @param byteBuffer the byte buffer 58 | * @return {@code true} if the specified {@link ByteBuffer} is empty, {@code false} otherwise. 59 | */ 60 | public static boolean isEmpty(ByteBuffer byteBuffer) { 61 | return byteBuffer.remaining() == 0; 62 | } 63 | 64 | /** 65 | * Returns the {@link ByteBuffer}s contained in {@code byteBuffer} according to {@code type}. 66 | * 67 | * @param byteBuffer the byte buffer to be split 68 | * @param type the {@link AbstractType} of {@code byteBuffer} 69 | * @return the {@link ByteBuffer}s contained in {@code byteBuffer} according to {@code type} 70 | */ 71 | public static ByteBuffer[] split(ByteBuffer byteBuffer, AbstractType type) { 72 | if (type instanceof CompositeType) { 73 | return ((CompositeType) type).split(byteBuffer); 74 | } else { 75 | return new ByteBuffer[]{byteBuffer}; 76 | } 77 | } 78 | 79 | /** 80 | * Returns a {@code String} representation of {@code byteBuffer} validated by {@code type}. 81 | * 82 | * @param byteBuffer the {@link ByteBuffer} to be converted to {@code String} 83 | * @param type {@link AbstractType} of {@code byteBuffer} 84 | * @return a {@code String} representation of {@code byteBuffer} validated by {@code type} 85 | */ 86 | public static String toString(ByteBuffer byteBuffer, AbstractType type) { 87 | if (type instanceof CompositeType) { 88 | CompositeType composite = (CompositeType) type; 89 | List> types = composite.types; 90 | ByteBuffer[] components = composite.split(byteBuffer); 91 | StringBuilder sb = new StringBuilder(); 92 | for (int i = 0; i < components.length; i++) { 93 | AbstractType componentType = types.get(i); 94 | ByteBuffer component = components[i]; 95 | sb.append(componentType.compose(component)); 96 | if (i < types.size() - 1) { 97 | sb.append(':'); 98 | } 99 | } 100 | return sb.toString(); 101 | } else { 102 | return type.compose(byteBuffer).toString(); 103 | } 104 | } 105 | 106 | /** 107 | * Returns the hexadecimal {@code String} representation of the specified {@link ByteBuffer}. 108 | * 109 | * @param byteBuffer a {@link ByteBuffer} 110 | * @return the hexadecimal {@code String} representation of {@code byteBuffer} 111 | */ 112 | public static String toHex(ByteBuffer byteBuffer) { 113 | return ByteBufferUtil.bytesToHex(byteBuffer); 114 | } 115 | 116 | /** 117 | * Returns the hexadecimal {@code String} representation of the specified {@code byte} array. 118 | * 119 | * @param bytes the {@code byte} array 120 | * @return The hexadecimal {@code String} representation of {@code bytes} 121 | */ 122 | public static String toHex(byte[] bytes) { 123 | return Hex.bytesToHex(bytes); 124 | } 125 | 126 | /** 127 | * Returns the hexadecimal {@code String} representation of the specified {@code byte}. 128 | * 129 | * @param b the {@code byte} 130 | * @return the hexadecimal {@code String} representation of {@code b} 131 | */ 132 | public static String toHex(byte b) { 133 | return Hex.bytesToHex(b); 134 | } 135 | 136 | /** 137 | * Returns the {@link BytesRef} representation of the specified {@link ByteBuffer}. 138 | * 139 | * @param bb the byte buffer 140 | * @return the {@link BytesRef} representation of the byte buffer 141 | */ 142 | public static BytesRef bytesRef(ByteBuffer bb) { 143 | byte[] bytes = asArray(bb); 144 | return new BytesRef(bytes); 145 | } 146 | 147 | /** 148 | * Returns the {@link ByteBuffer} representation of the specified {@link BytesRef}. 149 | * 150 | * @param bytesRef the {@link BytesRef} 151 | * @return the {@link ByteBuffer} representation of {@code bytesRef} 152 | */ 153 | public static ByteBuffer byteBuffer(BytesRef bytesRef) { 154 | byte[] bytes = bytesRef.bytes; 155 | return ByteBuffer.wrap(bytes, bytesRef.offset, bytesRef.offset + bytesRef.length); 156 | } 157 | 158 | } -------------------------------------------------------------------------------- /test-data/src/test/resources/fhir/patient_f001.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Patient", 3 | "id": "f201", 4 | "text": { 5 | "status": "generated", 6 | "div": "

Generated Narrative with Details

id: f201

identifier: BSN = 123456789 (OFFICIAL), BSN = 123456789 (OFFICIAL)

active: true

name: Roel(OFFICIAL)

telecom: ph: +31612345678(MOBILE), ph: +31201234567(HOME)

gender: male

birthDate: 13/03/1960

deceased: false

address: Bos en Lommerplein 280 Amsterdam 1055RW NLD (HOME)

maritalStatus: Legally married (Details : {SNOMED CT code 36629006 = 36629006, given as Legally married}; {http://hl7.org/fhir/v3/MaritalStatus code M = Married)

multipleBirth: false

photo:

Contacts

-RelationshipNameTelecom
*Wife (Details : {SNOMED CT code 127850001 = 127850001, given as Wife}; {http://hl7.org/fhir/patient-contact-relationship code partner = Partner)Ariadne Bor-Jansmaph: +31201234567(HOME)

Communications

-LanguagePreferred
*Dutch (Details : {urn:ietf:bcp:47 code nl-NL = ??, given as Dutch})true

managingOrganization: AUMC

" 7 | }, 8 | "identifier": [ 9 | { 10 | "use": "official", 11 | "_use": { 12 | "fhir_comments": [ 13 | " The identifier for the person as this patient (fictive)" 14 | ] 15 | }, 16 | "type": { 17 | "text": "BSN" 18 | }, 19 | "system": "urn:oid:2.16.840.1.113883.2.4.6.3", 20 | "value": "123456789" 21 | }, 22 | { 23 | "fhir_comments": [ 24 | " Demographics " 25 | ], 26 | "use": "official", 27 | "_use": { 28 | "fhir_comments": [ 29 | " The identifier for this individual" 30 | ] 31 | }, 32 | "type": { 33 | "text": "BSN" 34 | }, 35 | "system": "urn:oid:2.16.840.1.113883.2.4.6.3", 36 | "value": "123456789" 37 | } 38 | ], 39 | "active": true, 40 | "_active": { 41 | "fhir_comments": [ 42 | " Indicates that the patient is not part of a multiple birth " 43 | ] 44 | }, 45 | "name": [ 46 | { 47 | "use": "official", 48 | "_use": { 49 | "fhir_comments": [ 50 | " The name associated with the individual (fictive) " 51 | ] 52 | }, 53 | "text": "Roel", 54 | "family": [ 55 | "Bor" 56 | ], 57 | "given": [ 58 | "Roelof Olaf" 59 | ], 60 | "prefix": [ 61 | "Drs." 62 | ], 63 | "suffix": [ 64 | "PDEng." 65 | ] 66 | } 67 | ], 68 | "telecom": [ 69 | { 70 | "system": "phone", 71 | "_system": { 72 | "fhir_comments": [ 73 | " The mobile contact detail for the individual " 74 | ] 75 | }, 76 | "value": "+31612345678", 77 | "use": "mobile" 78 | }, 79 | { 80 | "system": "phone", 81 | "_system": { 82 | "fhir_comments": [ 83 | " The home contact detail for the individual " 84 | ] 85 | }, 86 | "value": "+31201234567", 87 | "use": "home" 88 | } 89 | ], 90 | "gender": "male", 91 | "birthDate": "1960-03-13", 92 | "deceasedBoolean": false, 93 | "_deceasedBoolean": { 94 | "fhir_comments": [ 95 | " The date and time of birth for the individual " 96 | ] 97 | }, 98 | "address": [ 99 | { 100 | "fhir_comments": [ 101 | " Indicates that the individual is not deceased ", 102 | " ISO 3166 3 letter code " 103 | ], 104 | "use": "home", 105 | "_use": { 106 | "fhir_comments": [ 107 | " Home address for the individual " 108 | ] 109 | }, 110 | "line": [ 111 | "Bos en Lommerplein 280" 112 | ], 113 | "city": "Amsterdam", 114 | "postalCode": "1055RW", 115 | "country": "NLD" 116 | } 117 | ], 118 | "maritalStatus": { 119 | "coding": [ 120 | { 121 | "fhir_comments": [ 122 | " Marital status of the person " 123 | ], 124 | "system": "http://snomed.info/sct", 125 | "code": "36629006", 126 | "display": "Legally married" 127 | }, 128 | { 129 | "system": "http://hl7.org/fhir/v3/MaritalStatus", 130 | "code": "M" 131 | } 132 | ] 133 | }, 134 | "multipleBirthBoolean": false, 135 | "photo": [ 136 | { 137 | "contentType": "image/jpeg", 138 | "url": "Binary/f006" 139 | } 140 | ], 141 | "contact": [ 142 | { 143 | "relationship": [ 144 | { 145 | "fhir_comments": [ 146 | " Contact of the patient " 147 | ], 148 | "coding": [ 149 | { 150 | "fhir_comments": [ 151 | " Indicates that the contact is the patients wife " 152 | ], 153 | "system": "http://snomed.info/sct", 154 | "code": "127850001", 155 | "display": "Wife" 156 | }, 157 | { 158 | "system": "http://hl7.org/fhir/patient-contact-relationship", 159 | "code": "partner" 160 | } 161 | ] 162 | } 163 | ], 164 | "name": { 165 | "use": "usual", 166 | "_use": { 167 | "fhir_comments": [ 168 | " The name of the contact " 169 | ] 170 | }, 171 | "text": "Ariadne Bor-Jansma" 172 | }, 173 | "telecom": [ 174 | { 175 | "system": "phone", 176 | "_system": { 177 | "fhir_comments": [ 178 | " The home contact detail " 179 | ] 180 | }, 181 | "value": "+31201234567", 182 | "use": "home" 183 | } 184 | ] 185 | } 186 | ], 187 | "communication": [ 188 | { 189 | "language": { 190 | "coding": [ 191 | { 192 | "system": "urn:ietf:bcp:47", 193 | "code": "nl-NL", 194 | "display": "Dutch" 195 | } 196 | ] 197 | }, 198 | "preferred": true 199 | } 200 | ], 201 | "managingOrganization": { 202 | "reference": "Organization/f201", 203 | "display": "AUMC" 204 | } 205 | } -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/mapper/PartitionMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to STRATIO (C) under one or more contributor license agreements. 3 | * See the NOTICE file distributed with this work for additional information 4 | * regarding copyright ownership. The STRATIO (C) licenses this file 5 | * to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | package io.puntanegra.fhir.index.mapper; 20 | 21 | import java.io.IOException; 22 | import java.nio.ByteBuffer; 23 | 24 | import org.apache.cassandra.config.CFMetaData; 25 | import org.apache.cassandra.config.DatabaseDescriptor; 26 | import org.apache.cassandra.db.DecoratedKey; 27 | import org.apache.cassandra.db.marshal.AbstractType; 28 | import org.apache.cassandra.dht.IPartitioner; 29 | import org.apache.cassandra.utils.ByteBufferUtil; 30 | import org.apache.lucene.document.Document; 31 | import org.apache.lucene.document.Field; 32 | import org.apache.lucene.document.FieldType; 33 | import org.apache.lucene.index.DocValuesType; 34 | import org.apache.lucene.index.IndexOptions; 35 | import org.apache.lucene.index.Term; 36 | import org.apache.lucene.search.FieldComparator; 37 | import org.apache.lucene.search.FieldComparatorSource; 38 | import org.apache.lucene.search.Query; 39 | import org.apache.lucene.search.SortField; 40 | import org.apache.lucene.search.TermQuery; 41 | import org.apache.lucene.util.BytesRef; 42 | 43 | import io.puntanegra.fhir.index.util.ByteBufferUtils; 44 | 45 | /** 46 | * Class for several partition key mappings between Cassandra and Lucene. 47 | * 48 | * @author Andres de la Pena {@literal } 49 | */ 50 | public final class PartitionMapper { 51 | 52 | /** The Lucene field name. */ 53 | public static final String FIELD_NAME = "_partition_key"; 54 | 55 | /** The Lucene field type. */ 56 | public static final FieldType FIELD_TYPE = new FieldType(); 57 | 58 | static { 59 | FIELD_TYPE.setOmitNorms(true); 60 | FIELD_TYPE.setIndexOptions(IndexOptions.DOCS); 61 | FIELD_TYPE.setTokenized(false); 62 | FIELD_TYPE.setStored(true); 63 | FIELD_TYPE.setDocValuesType(DocValuesType.SORTED); 64 | FIELD_TYPE.freeze(); 65 | } 66 | 67 | private final CFMetaData metadata; 68 | private final IPartitioner partitioner; 69 | private final AbstractType type; 70 | 71 | /** 72 | * Constructor specifying the indexed table {@link CFMetaData}. 73 | * 74 | * @param metadata 75 | * the indexed table metadata 76 | */ 77 | public PartitionMapper(CFMetaData metadata) { 78 | this.metadata = metadata; 79 | partitioner = DatabaseDescriptor.getPartitioner(); 80 | type = metadata.getKeyValidator(); 81 | } 82 | 83 | /** 84 | * Returns the type of the partition key. 85 | * 86 | * @return the key's type 87 | */ 88 | public AbstractType getType() { 89 | return type; 90 | } 91 | 92 | /** 93 | * Adds to the specified {@link Document} the {@link Field}s associated to 94 | * the specified partition key. 95 | * 96 | * @param document 97 | * the document in which the fields are going to be added 98 | * @param partitionKey 99 | * the partition key to be converted 100 | */ 101 | public void addFields(Document document, DecoratedKey partitionKey) { 102 | ByteBuffer bb = partitionKey.getKey(); 103 | BytesRef bytesRef = ByteBufferUtils.bytesRef(bb); 104 | document.add(new Field(FIELD_NAME, bytesRef, FIELD_TYPE)); 105 | } 106 | 107 | /** 108 | * Returns the specified raw partition key as a Lucene {@link Term}. 109 | * 110 | * @param partitionKey 111 | * the raw partition key to be converted 112 | * @return a Lucene {@link Term} 113 | */ 114 | public Term term(DecoratedKey partitionKey) { 115 | ByteBuffer bb = partitionKey.getKey(); 116 | BytesRef bytesRef = ByteBufferUtils.bytesRef(bb); 117 | return new Term(FIELD_NAME, bytesRef); 118 | } 119 | 120 | /** 121 | * Returns the {@link Term} representing the partition key of the specified 122 | * {@link Document}. 123 | * 124 | * @param document 125 | * the document 126 | * @return the partition key term 127 | */ 128 | public Term term(Document document) { 129 | BytesRef bytesRef = document.getBinaryValue(FIELD_NAME); 130 | return new Term(FIELD_NAME, bytesRef); 131 | } 132 | 133 | /** 134 | * Returns the specified raw partition key as a Lucene {@link Query}. 135 | * 136 | * @param partitionKey 137 | * the raw partition key to be converted 138 | * @return the specified raw partition key as a Lucene {@link Query} 139 | */ 140 | public Query query(DecoratedKey partitionKey) { 141 | return new TermQuery(term(partitionKey)); 142 | } 143 | 144 | /** 145 | * Returns the {@link DecoratedKey} contained in the specified Lucene 146 | * {@link Document}. 147 | * 148 | * @param document 149 | * the {@link Document} containing the partition key to be get 150 | * @return the {@link DecoratedKey} contained in the specified Lucene 151 | * {@link Document} 152 | */ 153 | public DecoratedKey decoratedKey(Document document) { 154 | BytesRef bytesRef = document.getBinaryValue(FIELD_NAME); 155 | ByteBuffer bb = ByteBufferUtils.byteBuffer(bytesRef); 156 | return decoratedKey(bb); 157 | } 158 | 159 | /** 160 | * Returns the specified binary partition key as a {@link DecoratedKey}. 161 | * 162 | * @param partitionKey 163 | * the binary representation of a partition key 164 | * @return the specified partition key as a a {@link DecoratedKey} 165 | */ 166 | public DecoratedKey decoratedKey(ByteBuffer partitionKey) { 167 | return partitioner.decorateKey(partitionKey); 168 | } 169 | 170 | /** 171 | * Returns a Lucene {@link SortField} for sorting documents/rows according 172 | * to the partition key. 173 | * 174 | * @return a sort field for sorting by partition key 175 | */ 176 | public SortField sortField() { 177 | return new SortField(FIELD_NAME, new FieldComparatorSource() { 178 | @Override 179 | public FieldComparator newComparator(String field, int hits, int sort, boolean reversed) 180 | throws IOException { 181 | return new FieldComparator.TermValComparator(hits, field, false) { 182 | @Override 183 | public int compareValues(BytesRef val1, BytesRef val2) { 184 | ByteBuffer bb1 = ByteBufferUtils.byteBuffer(val1); 185 | ByteBuffer bb2 = ByteBufferUtils.byteBuffer(val2); 186 | return ByteBufferUtil.compareUnsigned(bb1, bb2); 187 | } 188 | }; 189 | } 190 | }); 191 | } 192 | 193 | } 194 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/FhirIndex.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index; 2 | 3 | import java.util.Optional; 4 | import java.util.concurrent.Callable; 5 | import java.util.function.BiFunction; 6 | 7 | import org.apache.cassandra.config.ColumnDefinition; 8 | import org.apache.cassandra.cql3.Operator; 9 | import org.apache.cassandra.db.ColumnFamilyStore; 10 | import org.apache.cassandra.db.DecoratedKey; 11 | import org.apache.cassandra.db.PartitionColumns; 12 | import org.apache.cassandra.db.ReadCommand; 13 | import org.apache.cassandra.db.SystemKeyspace; 14 | import org.apache.cassandra.db.filter.RowFilter; 15 | import org.apache.cassandra.db.marshal.AbstractType; 16 | import org.apache.cassandra.db.marshal.UTF8Type; 17 | import org.apache.cassandra.db.partitions.PartitionIterator; 18 | import org.apache.cassandra.db.partitions.PartitionUpdate; 19 | import org.apache.cassandra.exceptions.InvalidRequestException; 20 | import org.apache.cassandra.index.Index; 21 | import org.apache.cassandra.index.IndexRegistry; 22 | import org.apache.cassandra.index.transactions.IndexTransaction.Type; 23 | import org.apache.cassandra.schema.IndexMetadata; 24 | import org.apache.cassandra.utils.concurrent.OpOrder.Group; 25 | import org.apache.lucene.document.Document; 26 | import org.apache.lucene.queryparser.classic.QueryParser; 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | /** 31 | * Cassandra Secondary {@link Index} implementation that uses Lucene to index 32 | * FHIR Resources.
33 | * The index operates on a JSON column where the Resources are stored as JSON. 34 | * This index firstly parses the JSON, then based on the index's configuration 35 | * extracts the values from the Resource and finally creates a Lucene 36 | * {@link Document} with the indexed search parameters. 37 | *

38 | * This implementation uses HAPI library 39 | * (http://jamesagnew.github.io/hapi-fhir/) to parse and extract information 40 | * from the FHIR Resource. 41 | *

42 | * The index uses an extension of Lucene {@link QueryParser} to parse the query 43 | * expression.
44 | * 45 | * It allows the following type of search: 46 | *

  • Range: value-quantity:[1 TO 200]

  • 47 | *
  • Wildcard: family:Pero*

  • 48 | *
  • Full Text Search: name:Jonh

  • 49 | *
  • Boolean Search: family:Pero* AND active:true

  • 50 | *

    51 | * It also support sorting and top-k queries. 52 | * 53 | *

    54 | * NOTE: This implementation is based on Stratio cassandra-lucene index ( 55 | * https://github.com/Stratio/cassandra-lucene-index). 56 | * 57 | * @author Jorge L. Middleton {@literal } 58 | * 59 | */ 60 | public class FhirIndex implements Index { 61 | 62 | private static final Logger logger = LoggerFactory.getLogger(FhirIndex.class); 63 | 64 | private final ColumnFamilyStore table; 65 | private final IndexMetadata config; 66 | private FhirIndexService service; 67 | private String name; 68 | 69 | /** 70 | * Builds a new Lucene index for the specified {@link ColumnFamilyStore} 71 | * using the specified {@link IndexMetadata}. 72 | * 73 | * @param table 74 | * the indexed {@link ColumnFamilyStore} 75 | * @param indexDef 76 | * the index's metadata 77 | */ 78 | public FhirIndex(ColumnFamilyStore baseCfs, IndexMetadata indexDef) { 79 | logger.debug("Building Lucene index {} {}", baseCfs.metadata, indexDef); 80 | 81 | this.table = baseCfs; 82 | this.config = indexDef; 83 | 84 | this.service = new FhirIndexService(); 85 | service.build(this.table, this.config); 86 | 87 | this.name = service.getName(); 88 | 89 | } 90 | 91 | public AbstractType customExpressionValueType() { 92 | return UTF8Type.instance; 93 | } 94 | 95 | public boolean dependsOn(ColumnDefinition column) { 96 | return false; 97 | } 98 | 99 | public Optional getBackingTable() { 100 | return Optional.empty(); 101 | } 102 | 103 | public Callable getBlockingFlushTask() { 104 | return () -> { 105 | logger.info("Flushing Lucene index {}", name); 106 | service.commit(); 107 | return null; 108 | }; 109 | } 110 | 111 | public long getEstimatedResultRows() { 112 | logger.trace("Getting the estimated result rows"); 113 | return 1; 114 | } 115 | 116 | public IndexMetadata getIndexMetadata() { 117 | return this.config; 118 | } 119 | 120 | public Callable getInitializationTask() { 121 | logger.info("Getting initialization task of {}", name); 122 | if (table.isEmpty() || SystemKeyspace.isIndexBuilt(table.keyspace.getName(), config.name)) { 123 | logger.info("Index {} doesn't need (re)building", name); 124 | return null; 125 | } else { 126 | logger.info("Index {} needs (re)building", name); 127 | return () -> { 128 | table.forceBlockingFlush(); 129 | service.truncate(); 130 | table.indexManager.buildIndexBlocking(this); 131 | return null; 132 | }; 133 | } 134 | } 135 | 136 | public Callable getInvalidateTask() { 137 | return () -> { 138 | service.delete(); 139 | return null; 140 | }; 141 | } 142 | 143 | public Callable getMetadataReloadTask(IndexMetadata indexMetadata) { 144 | return () -> { 145 | logger.debug("Reloading Lucene index {} metadata: {}", name, indexMetadata); 146 | return null; 147 | }; 148 | } 149 | 150 | public RowFilter getPostIndexQueryFilter(RowFilter filter) { 151 | logger.trace("Getting the post index query filter for {}", filter); 152 | return filter; 153 | } 154 | 155 | public Callable getTruncateTask(long time) { 156 | logger.trace("Getting truncate task"); 157 | return () -> { 158 | logger.info("Truncating Lucene index {}...", name); 159 | service.truncate(); 160 | logger.info("Truncated Lucene index {}", name); 161 | return null; 162 | }; 163 | } 164 | 165 | /** 166 | * This method is invoked when a new row is inserted/updated to the table 167 | */ 168 | public Indexer indexerFor(DecoratedKey key, PartitionColumns columns, int nowInSec, Group opGroup, 169 | Type transactionType) { 170 | return service.indexWriter(key, columns, nowInSec, opGroup, transactionType); 171 | } 172 | 173 | public BiFunction postProcessorFor(ReadCommand arg0) { 174 | return (partitions, readCommand) -> service.postProcess(partitions, readCommand); 175 | } 176 | 177 | public void register(IndexRegistry indexRegistry) { 178 | indexRegistry.registerIndex(this); 179 | } 180 | 181 | /** 182 | * This method is invoked when a CQL query is executed. 183 | */ 184 | public Searcher searcherFor(ReadCommand command) { 185 | logger.trace("Getting searcher for {}", command); 186 | try { 187 | return service.searcher(command); 188 | } catch (Exception e) { 189 | logger.error("Error while searching", e); 190 | throw new InvalidRequestException(e.getMessage()); 191 | } 192 | } 193 | 194 | public boolean shouldBuildBlocking() { 195 | return true; 196 | } 197 | 198 | public boolean supportsExpression(ColumnDefinition column, Operator operator) { 199 | logger.trace("Asking if it supports the expression {} {}", column, operator); 200 | return false; 201 | } 202 | 203 | public void validate(PartitionUpdate update) throws InvalidRequestException { 204 | logger.trace("Validating {}...", update); 205 | try { 206 | service.validate(update); 207 | } catch (Exception e) { 208 | throw new InvalidRequestException(e.getMessage()); 209 | } 210 | 211 | } 212 | 213 | } 214 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/test/java/io/puntanegra/fhir/index/SearchParamExtractorTest.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertFalse; 5 | import static org.junit.Assert.assertNotNull; 6 | 7 | import java.io.File; 8 | import java.io.FileReader; 9 | import java.util.Calendar; 10 | import java.util.HashSet; 11 | import java.util.List; 12 | import java.util.Set; 13 | 14 | import org.hl7.fhir.dstu3.model.CodeableConcept; 15 | import org.hl7.fhir.dstu3.model.Coding; 16 | import org.hl7.fhir.dstu3.model.ContactPoint; 17 | import org.hl7.fhir.dstu3.model.ContactPoint.ContactPointSystem; 18 | import org.hl7.fhir.dstu3.model.Enumerations.AdministrativeGender; 19 | import org.hl7.fhir.dstu3.model.HumanName; 20 | import org.hl7.fhir.dstu3.model.Identifier; 21 | import org.hl7.fhir.dstu3.model.Patient; 22 | import org.hl7.fhir.instance.model.api.IBaseResource; 23 | import org.junit.Test; 24 | 25 | import ca.uhn.fhir.context.FhirContext; 26 | import ca.uhn.fhir.context.FhirVersionEnum; 27 | import io.puntanegra.fhir.index.search.SearchParamExtractorHelper; 28 | import io.puntanegra.fhir.index.search.datatypes.AbstractSearchParam; 29 | import io.puntanegra.fhir.index.search.datatypes.SearchParamDates; 30 | import io.puntanegra.fhir.index.search.datatypes.SearchParamToken; 31 | import io.puntanegra.fhir.index.search.datatypes.SearchParamTypes; 32 | 33 | public class SearchParamExtractorTest { 34 | private FhirVersionEnum fhirVersion = FhirVersionEnum.DSTU3; 35 | private FhirContext ctx = new FhirContext(fhirVersion); 36 | private SearchParamExtractorHelper helper = new SearchParamExtractorHelper(FhirVersionEnum.DSTU3); 37 | 38 | @Test 39 | public void testExtractParam() throws Exception { 40 | FileReader fileReader = new FileReader( 41 | new File(this.getClass().getClassLoader().getResource("fhir/observation_example001.json").getPath())); 42 | IBaseResource resource = ctx.newJsonParser().parseResource(fileReader); 43 | 44 | Set parameters = new HashSet(); 45 | parameters.add("code"); 46 | parameters.add("value-quantity"); 47 | parameters.add("date"); 48 | 49 | Set values = helper.extractParametersValues(resource, parameters); 50 | 51 | assertNotNull(values); 52 | assertFalse(values.isEmpty()); 53 | 54 | for (AbstractSearchParam entry : values) { 55 | String ename = entry.getName(); 56 | SearchParamTypes type = entry.getType(); 57 | 58 | if (SearchParamTypes.TOKEN == type) { 59 | SearchParamToken token = (SearchParamToken) entry; 60 | if (ename.equals("code") && "http://loinc.org".equals(token.getSystem())) { 61 | assertEquals("12345-5", token.getCode()); 62 | } 63 | }if (SearchParamTypes.DATE == type) { 64 | SearchParamDates dates = (SearchParamDates) entry; 65 | if (ename.equals("date")) { 66 | Calendar date= Calendar.getInstance(); 67 | date.setTime(dates.getValue()); 68 | assertEquals(4, date.get(Calendar.MONTH)); 69 | } 70 | } 71 | } 72 | } 73 | 74 | @Test 75 | public void testLoadPatient() throws Exception { 76 | FileReader fileReader = new FileReader( 77 | new File(this.getClass().getClassLoader().getResource("fhir/patient_f001.json").getPath())); 78 | IBaseResource resource = ctx.newJsonParser().parseResource(fileReader); 79 | 80 | Set parameters = new HashSet(); 81 | parameters.add("name"); 82 | parameters.add("email"); 83 | 84 | Set values = helper.extractParametersValues(resource, parameters); 85 | 86 | assertNotNull(values); 87 | assertFalse(values.isEmpty()); 88 | 89 | for (AbstractSearchParam entry : values) { 90 | String ename = entry.getName(); 91 | SearchParamTypes type = entry.getType(); 92 | 93 | if (SearchParamTypes.STRING == type) { 94 | if (ename.equals("name")) { 95 | assertEquals("van de Heuvel Pieter", entry.getValue()); 96 | } 97 | } 98 | 99 | if (SearchParamTypes.TOKEN == type) { 100 | SearchParamToken token = (SearchParamToken) entry; 101 | if (ename.equals("email")) { 102 | assertEquals("email", token.getSystem()); 103 | 104 | List contacts = ((Patient) resource).getTelecom(); 105 | for (ContactPoint contactPoint : contacts) { 106 | if (ContactPointSystem.EMAIL == contactPoint.getSystem()) { 107 | assertEquals(contactPoint.getValue(), token.getCode()); 108 | } 109 | } 110 | } 111 | } 112 | } 113 | } 114 | 115 | @Test 116 | public void testSearchParamValues() { 117 | 118 | Patient patient = new Patient(); 119 | patient.setId("253345"); 120 | 121 | Calendar c = Calendar.getInstance(); 122 | c.set(1998, 3, 3); 123 | patient.setBirthDate(c.getTime()); 124 | 125 | CodeableConcept language = patient.addCommunication().getLanguage(); 126 | language.setText("Nederlands"); 127 | 128 | Coding coding = language.addCoding(); 129 | coding.setSystem("urn:ietf:bcp:47"); 130 | coding.setCode("nl"); 131 | coding.setDisplay("Dutch"); 132 | patient.addCommunication().setLanguage(language); 133 | 134 | HumanName name = patient.addName(); 135 | name.addFamily("Smith"); 136 | name.addGiven("Rob").addGiven("Jon"); 137 | 138 | Identifier id = new Identifier(); 139 | id.setValue("253345"); 140 | id.setSystem("urn:mrns"); 141 | patient.addIdentifier(id); 142 | // patient.getManagingOrganization().setReference("Organization/124362"); 143 | 144 | patient.setGender(AdministrativeGender.MALE); 145 | ContactPoint cp = new ContactPoint(); 146 | cp.setSystem(ContactPointSystem.EMAIL); 147 | cp.setValue("rob@gmail.com"); 148 | patient.addTelecom(cp); 149 | 150 | // Coded types can naturally be set using plain strings 151 | Coding statusCoding = patient.getMaritalStatus().addCoding(); 152 | statusCoding.setSystem("http://hl7.org/fhir/v3/MaritalStatus"); 153 | statusCoding.setCode("M"); 154 | statusCoding.setDisplay("Married"); 155 | 156 | Set parameters = new HashSet(); 157 | parameters.add("family"); 158 | parameters.add("email"); 159 | parameters.add("identifier"); 160 | parameters.add("birthdate"); 161 | parameters.add("name"); 162 | parameters.add("language"); 163 | 164 | Set values = helper.extractParametersValues(patient, parameters); 165 | 166 | assertNotNull(values); 167 | assertFalse(values.isEmpty()); 168 | 169 | for (AbstractSearchParam entry : values) { 170 | String ename = entry.getName(); 171 | SearchParamTypes type = entry.getType(); 172 | 173 | if (SearchParamTypes.STRING == type) { 174 | if (ename.equals("family")) { 175 | assertEquals("Smith", entry.getValue()); 176 | } 177 | 178 | if (ename.equals("identifier")) { 179 | assertEquals("253345", entry.getValue()); 180 | } 181 | } 182 | 183 | if (SearchParamTypes.TOKEN == type) { 184 | SearchParamToken token = (SearchParamToken) entry; 185 | if (ename.equals("email")) { 186 | assertEquals("email", token.getSystem()); 187 | assertEquals("rob@gmail.com", token.getCode()); 188 | } 189 | 190 | if (ename.equals("identifier")) { 191 | assertEquals("urn:mrns", token.getSystem()); 192 | } 193 | 194 | if (ename.equals("language")) { 195 | assertEquals("nl", token.getCode()); 196 | assertEquals("urn:ietf:bcp:47", token.getSystem()); 197 | } 198 | } 199 | 200 | if (SearchParamTypes.DATE == type) { 201 | SearchParamDates dates = (SearchParamDates) entry; 202 | 203 | if (ename.equals("birthdate")) { 204 | assertEquals(c.getTime().getTime(), dates.getValue().getTime()); 205 | } 206 | } 207 | } 208 | } 209 | 210 | } 211 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/search/extractor/TokenSearchParameterExtractor.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.search.extractor; 2 | 3 | import static org.apache.commons.lang3.StringUtils.isBlank; 4 | import static org.apache.commons.lang3.StringUtils.isNotBlank; 5 | 6 | import java.util.ArrayList; 7 | import java.util.HashSet; 8 | import java.util.List; 9 | import java.util.Set; 10 | 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.hl7.fhir.dstu3.model.CodeableConcept; 13 | import org.hl7.fhir.dstu3.model.Coding; 14 | import org.hl7.fhir.dstu3.model.Conformance.ConformanceRestSecurityComponent; 15 | import org.hl7.fhir.dstu3.model.ContactPoint; 16 | import org.hl7.fhir.dstu3.model.Enumeration; 17 | import org.hl7.fhir.dstu3.model.Identifier; 18 | import org.hl7.fhir.dstu3.model.Location.LocationPositionComponent; 19 | import org.hl7.fhir.dstu3.model.Patient.PatientCommunicationComponent; 20 | import org.hl7.fhir.dstu3.model.ValueSet; 21 | import org.hl7.fhir.instance.model.api.IBaseResource; 22 | import org.hl7.fhir.instance.model.api.IPrimitiveType; 23 | 24 | import ca.uhn.fhir.context.FhirContext; 25 | import ca.uhn.fhir.context.RuntimeSearchParam; 26 | import io.puntanegra.fhir.index.search.datatypes.AbstractSearchParam; 27 | import io.puntanegra.fhir.index.search.datatypes.SearchParamToken; 28 | import io.puntanegra.fhir.index.search.datatypes.SearchParamTypes; 29 | 30 | /** 31 | * Extracts information from a parameter of type Token. 32 | * 33 | * @author Jorge L. Middleton {@literal } 34 | * 35 | */ 36 | public class TokenSearchParameterExtractor extends AbstractSearchParameterExtractor { 37 | 38 | public TokenSearchParameterExtractor(FhirContext ctx) { 39 | super(ctx); 40 | } 41 | 42 | @Override 43 | public Set extractValues(IBaseResource instance, RuntimeSearchParam searchParam) { 44 | 45 | Set values = new HashSet(); 46 | 47 | String path = searchParam.getPath(); 48 | String resourceName = searchParam.getName(); 49 | String paramType = getParamType(searchParam); 50 | 51 | boolean multiType = false; 52 | if (path.endsWith("[x]")) { 53 | multiType = true; 54 | } 55 | 56 | List systems = new ArrayList(); 57 | List codes = new ArrayList(); 58 | 59 | String needContactPointSystem = null; 60 | if (path.endsWith(".where(system='phone')")) { 61 | path = path.substring(0, path.length() - ".where(system='phone')".length()); 62 | needContactPointSystem = "phone"; 63 | } 64 | if (path.endsWith(".where(system='email')")) { 65 | path = path.substring(0, path.length() - ".where(system='email')".length()); 66 | needContactPointSystem = "email"; 67 | } 68 | 69 | for (Object obj : extractValues(path, instance)) { 70 | 71 | // Patient:language 72 | if (obj instanceof PatientCommunicationComponent) { 73 | PatientCommunicationComponent nextValue = (PatientCommunicationComponent) obj; 74 | obj = nextValue.getLanguage(); 75 | } 76 | 77 | if (obj instanceof Identifier) { 78 | Identifier identifier = (Identifier) obj; 79 | if (identifier.isEmpty()) { 80 | continue; 81 | } 82 | String system = StringUtils.defaultIfBlank(identifier.getSystemElement().getValueAsString(), null); 83 | String value = identifier.getValueElement().getValue(); 84 | if (isNotBlank(value)) { 85 | systems.add(system); 86 | codes.add(value); 87 | } 88 | 89 | if (isNotBlank(identifier.getType().getText())) { 90 | values.add(addStringParam(searchParam, identifier.getType().getText())); 91 | } 92 | 93 | } else if (obj instanceof ContactPoint) { 94 | ContactPoint nextValue = (ContactPoint) obj; 95 | if (nextValue.isEmpty()) { 96 | continue; 97 | } 98 | if (isNotBlank(needContactPointSystem)) { 99 | if (!needContactPointSystem.equals(nextValue.getSystemElement().getValueAsString())) { 100 | continue; 101 | } 102 | } 103 | systems.add(nextValue.getSystemElement().getValueAsString()); 104 | codes.add(nextValue.getValueElement().getValue()); 105 | } else if (obj instanceof Enumeration) { 106 | Enumeration en = (Enumeration) obj; 107 | String system = extractSystem(en); 108 | String code = en.getValueAsString(); 109 | if (isNotBlank(code)) { 110 | systems.add(system); 111 | codes.add(code); 112 | } 113 | } else if (obj instanceof IPrimitiveType) { 114 | IPrimitiveType nextValue = (IPrimitiveType) obj; 115 | if (nextValue.isEmpty()) { 116 | continue; 117 | } 118 | if ("ValueSet.codeSystem.concept.code".equals(path)) { 119 | String useSystem = null; 120 | if (instance instanceof ValueSet) { 121 | ValueSet vs = (ValueSet) instance; 122 | //useSystem = "" //TODO: review vs.getCodeSystem().getSystem(); 123 | } 124 | systems.add(useSystem); 125 | } else { 126 | systems.add(null); 127 | } 128 | codes.add(nextValue.getValueAsString()); 129 | } else if (obj instanceof Coding) { 130 | Coding coding = (Coding) obj; 131 | extractTokensFromCoding(systems, codes, searchParam, coding, values); 132 | } else if (obj instanceof CodeableConcept) { 133 | CodeableConcept codeable = (CodeableConcept) obj; 134 | if (!codeable.getTextElement().isEmpty()) { 135 | values.add(addStringParam(searchParam, codeable.getTextElement().getValue())); 136 | } 137 | 138 | for (Coding coding : codeable.getCoding()) { 139 | extractTokensFromCoding(systems, codes, searchParam, coding, values); 140 | } 141 | } else if (obj instanceof ConformanceRestSecurityComponent) { 142 | // Conformance.security search param points to something kind of 143 | // useless right now - This should probably 144 | // be fixed. 145 | ConformanceRestSecurityComponent sec = (ConformanceRestSecurityComponent) obj; 146 | for (CodeableConcept nextCC : sec.getService()) { 147 | if (!nextCC.getTextElement().isEmpty()) { 148 | values.add(addStringParam(searchParam, nextCC.getTextElement().getValue())); 149 | } 150 | } 151 | } else if (obj instanceof LocationPositionComponent) { 152 | logger.warn("Position search not currently supported, not indexing location"); 153 | continue; 154 | } else { 155 | if (!multiType) { 156 | throw new SearchParameterException( 157 | "Search param " + resourceName + " is of unexpected datatype: " + obj.getClass()); 158 | } else { 159 | continue; 160 | } 161 | } 162 | } 163 | 164 | assert systems.size() == codes.size() : "Systems contains " + systems + ", codes contains: " + codes; 165 | 166 | for (int i = 0; i < systems.size(); i++) { 167 | String system = systems.get(i); 168 | String code = codes.get(i); 169 | if (isBlank(system) && isBlank(code)) { 170 | continue; 171 | } 172 | 173 | if (system != null && system.length() > MAX_LENGTH) { 174 | system = system.substring(0, MAX_LENGTH); 175 | } 176 | 177 | if (code != null && code.length() > MAX_LENGTH) { 178 | code = code.substring(0, MAX_LENGTH); 179 | } 180 | 181 | SearchParamToken token = new SearchParamToken(resourceName, path, SearchParamTypes.valueOf(paramType), 182 | system, code); 183 | values.add(token); 184 | 185 | } 186 | 187 | return values; 188 | } 189 | 190 | private void extractTokensFromCoding(List systems, List codes, RuntimeSearchParam searchParam, 191 | Coding coding, Set values) { 192 | if (coding != null && !coding.isEmpty()) { 193 | 194 | String nextSystem = coding.getSystemElement().getValueAsString(); 195 | String nextCode = coding.getCodeElement().getValue(); 196 | if (isNotBlank(nextSystem) || isNotBlank(nextCode)) { 197 | systems.add(nextSystem); 198 | codes.add(nextCode); 199 | } 200 | } 201 | } 202 | 203 | } 204 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/mapper/KeyMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to STRATIO (C) under one or more contributor license agreements. 3 | * See the NOTICE file distributed with this work for additional information 4 | * regarding copyright ownership. The STRATIO (C) licenses this file 5 | * to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | package io.puntanegra.fhir.index.mapper; 20 | 21 | import java.io.IOException; 22 | import java.nio.ByteBuffer; 23 | import java.util.Optional; 24 | 25 | import org.apache.cassandra.config.CFMetaData; 26 | import org.apache.cassandra.db.Clustering; 27 | import org.apache.cassandra.db.ClusteringComparator; 28 | import org.apache.cassandra.db.ClusteringPrefix; 29 | import org.apache.cassandra.db.DataRange; 30 | import org.apache.cassandra.db.DecoratedKey; 31 | import org.apache.cassandra.db.PartitionPosition; 32 | import org.apache.cassandra.db.Slices; 33 | import org.apache.cassandra.db.filter.ClusteringIndexFilter; 34 | import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter; 35 | import org.apache.cassandra.db.marshal.CompositeType; 36 | import org.apache.cassandra.db.marshal.LongType; 37 | import org.apache.lucene.document.Document; 38 | import org.apache.lucene.document.Field; 39 | import org.apache.lucene.document.FieldType; 40 | import org.apache.lucene.index.DocValuesType; 41 | import org.apache.lucene.index.IndexOptions; 42 | import org.apache.lucene.index.Term; 43 | import org.apache.lucene.search.FieldComparator; 44 | import org.apache.lucene.search.FieldComparatorSource; 45 | import org.apache.lucene.search.SortField; 46 | import org.apache.lucene.util.BytesRef; 47 | import org.slf4j.Logger; 48 | import org.slf4j.LoggerFactory; 49 | 50 | import io.puntanegra.fhir.index.util.ByteBufferUtils; 51 | 52 | /** 53 | * Class for several clustering key mappings between Cassandra and Lucene. 54 | * 55 | * @author Andres de la Pena {@literal } 56 | */ 57 | public final class KeyMapper { 58 | 59 | public static final Logger logger = LoggerFactory.getLogger(KeyMapper.class); 60 | 61 | /** The Lucene field name. */ 62 | public static final String FIELD_NAME = "_primary_key"; 63 | 64 | /** The Lucene field type. */ 65 | public static final FieldType FIELD_TYPE = new FieldType(); 66 | 67 | static { 68 | FIELD_TYPE.setOmitNorms(true); 69 | FIELD_TYPE.setIndexOptions(IndexOptions.DOCS); 70 | FIELD_TYPE.setTokenized(false); 71 | FIELD_TYPE.setStored(true); 72 | FIELD_TYPE.setDocValuesType(DocValuesType.SORTED); 73 | FIELD_TYPE.freeze(); 74 | } 75 | 76 | /** The indexed table metadata */ 77 | private final CFMetaData metadata; 78 | 79 | /** The clustering key comparator */ 80 | private final ClusteringComparator clusteringComparator; 81 | 82 | /** A composite type composed by the types of the clustering key */ 83 | private final CompositeType clusteringType; 84 | 85 | /** 86 | * The type of the primary key, which is composed by token, partition key 87 | * and clustering key types. 88 | */ 89 | private final CompositeType type; 90 | 91 | /** 92 | * Constructor specifying the partition and clustering key mappers. 93 | * 94 | * @param metadata 95 | * the indexed table metadata 96 | */ 97 | public KeyMapper(CFMetaData metadata) { 98 | this.metadata = metadata; 99 | clusteringComparator = metadata.comparator; 100 | clusteringType = CompositeType.getInstance(clusteringComparator.subtypes()); 101 | type = CompositeType.getInstance(LongType.instance, metadata.getKeyValidator(), clusteringType); 102 | } 103 | 104 | /** 105 | * Returns the clustering key comparator. 106 | * 107 | * @return the comparator 108 | */ 109 | public ClusteringComparator clusteringComparator() { 110 | return clusteringComparator; 111 | } 112 | 113 | /** 114 | * The type of the primary key, which is composed by token, partition key 115 | * and clustering key types. 116 | * 117 | * @return the composite type 118 | */ 119 | public CompositeType clusteringType() { 120 | return clusteringType; 121 | } 122 | 123 | /** 124 | * Returns the {@link KeyEntry} represented by the specified Lucene 125 | * {@link BytesRef}. 126 | * 127 | * @param bytesRef 128 | * the Lucene field binary value 129 | * @return the represented key entry 130 | */ 131 | public KeyEntry entry(BytesRef bytesRef) { 132 | ByteBuffer bb = ByteBufferUtils.byteBuffer(bytesRef); 133 | ByteBuffer[] components = type.split(bb); 134 | return new KeyEntry(this, components); 135 | } 136 | 137 | /** 138 | * Returns the {@code String} human-readable representation of the specified 139 | * {@link ClusteringPrefix}. 140 | * 141 | * @param prefix 142 | * the clustering prefix 143 | * @return a {@code String} representing {@code prefix} 144 | */ 145 | public String toString(ClusteringPrefix prefix) { 146 | return prefix.toString(metadata); 147 | } 148 | 149 | /** 150 | * Returns the {@link ByteBuffer} representation of the primary key formed 151 | * by the specified partition key and the clustering key. 152 | * 153 | * @param key 154 | * the partition key 155 | * @param clustering 156 | * the clustering key 157 | * @return the {@link ByteBuffer} representation of the primary key 158 | */ 159 | public ByteBuffer byteBuffer(DecoratedKey key, Clustering clustering) { 160 | return type.builder().add(TokenMapper.byteBuffer(key.getToken())).add(key.getKey()).add(byteBuffer(clustering)) 161 | .build(); 162 | } 163 | 164 | /** 165 | * Returns a {@link ByteBuffer} representing the specified clustering key 166 | * 167 | * @param clustering 168 | * the clustering key 169 | * @return the byte buffer representing {@code clustering} 170 | */ 171 | public ByteBuffer byteBuffer(Clustering clustering) { 172 | CompositeType.Builder builder = type.builder(); 173 | for (ByteBuffer component : clustering.getRawValues()) { 174 | builder.add(component); 175 | } 176 | return builder.build(); 177 | } 178 | 179 | /** 180 | * Adds to the specified Lucene {@link Document} the primary key formed by 181 | * the specified partition key and the clustering key. 182 | * 183 | * @param document 184 | * the Lucene {@link Document} in which the key is going to be 185 | * added 186 | * @param key 187 | * the partition key 188 | * @param clustering 189 | * the clustering key 190 | */ 191 | public void addFields(Document document, DecoratedKey key, Clustering clustering) { 192 | ByteBuffer bb = byteBuffer(key, clustering); 193 | BytesRef bytesRef = ByteBufferUtils.bytesRef(bb); 194 | Field field = new Field(FIELD_NAME, bytesRef, FIELD_TYPE); 195 | document.add(field); 196 | } 197 | 198 | /** 199 | * Returns the Lucene {@link Term} representing the primary key formed by 200 | * the specified partition key and the clustering key. 201 | * 202 | * @param key 203 | * the partition key 204 | * @param clustering 205 | * the clustering key 206 | * @return the Lucene {@link Term} representing the primary key 207 | */ 208 | public Term term(DecoratedKey key, Clustering clustering) { 209 | return new Term(FIELD_NAME, bytesRef(key, clustering)); 210 | } 211 | 212 | /** 213 | * Returns the {@link BytesRef} representation of the specified primary key. 214 | * 215 | * @param key 216 | * the partition key 217 | * @param clustering 218 | * the clustering key 219 | * @return the Lucene field binary value 220 | */ 221 | public BytesRef bytesRef(DecoratedKey key, Clustering clustering) { 222 | ByteBuffer bb = byteBuffer(key, clustering); 223 | return ByteBufferUtils.bytesRef(bb); 224 | } 225 | 226 | /** 227 | * Returns the clustering key contained in the specified {@link Document}. 228 | * 229 | * @param document 230 | * a {@link Document} containing the clustering key to be get 231 | * @return the clustering key contained in {@code document} 232 | */ 233 | public Clustering clustering(Document document) { 234 | BytesRef bytesRef = document.getBinaryValue(FIELD_NAME); 235 | return entry(bytesRef).getClustering(); 236 | } 237 | 238 | /** 239 | * Returns the start {@link ClusteringPrefix} of the first partition of the 240 | * specified {@link DataRange}. 241 | * 242 | * @param dataRange 243 | * the data range 244 | * @return the start clustering prefix of {@code dataRange}, or {@code null} 245 | * if there is no such start 246 | */ 247 | public static Optional startClusteringPrefix(DataRange dataRange) { 248 | PartitionPosition startPosition = dataRange.startKey(); 249 | if (startPosition instanceof DecoratedKey) { 250 | DecoratedKey startKey = (DecoratedKey) startPosition; 251 | ClusteringIndexFilter filter = dataRange.clusteringIndexFilter(startKey); 252 | if (filter instanceof ClusteringIndexSliceFilter) { 253 | ClusteringIndexSliceFilter sliceFilter = (ClusteringIndexSliceFilter) filter; 254 | Slices slices = sliceFilter.requestedSlices(); 255 | return Optional.of(slices.get(0).start()); 256 | } 257 | } 258 | return Optional.empty(); 259 | } 260 | 261 | /** 262 | * Returns the stop {@link ClusteringPrefix} of the last partition of the 263 | * specified {@link DataRange}. 264 | * 265 | * @param dataRange 266 | * the data range 267 | * @return the stop clustering prefix of {@code dataRange}, or {@code null} 268 | * if there is no such start 269 | */ 270 | public static Optional stopClusteringPrefix(DataRange dataRange) { 271 | PartitionPosition stopPosition = dataRange.stopKey(); 272 | if (stopPosition instanceof DecoratedKey) { 273 | DecoratedKey stopKey = (DecoratedKey) stopPosition; 274 | ClusteringIndexFilter filter = dataRange.clusteringIndexFilter(stopKey); 275 | if (filter instanceof ClusteringIndexSliceFilter) { 276 | ClusteringIndexSliceFilter sliceFilter = (ClusteringIndexSliceFilter) filter; 277 | Slices slices = sliceFilter.requestedSlices(); 278 | return Optional.of(slices.get(slices.size() - 1).end()); 279 | } 280 | } 281 | return Optional.empty(); 282 | } 283 | 284 | /** 285 | * Returns the {@link Term} representing the primary key of the specified 286 | * {@link Document}. 287 | * 288 | * @param document 289 | * the document 290 | * @return the clustering key term 291 | */ 292 | public static Term term(Document document) { 293 | BytesRef bytesRef = document.getBinaryValue(FIELD_NAME); 294 | return new Term(FIELD_NAME, bytesRef); 295 | } 296 | 297 | /** 298 | * Returns a Lucene {@link SortField} to sort documents by primary key 299 | * according to Cassandra's natural order. 300 | * 301 | * @return the sort field 302 | */ 303 | public SortField sortField() { 304 | return new SortField(FIELD_NAME, new FieldComparatorSource() { 305 | @Override 306 | public FieldComparator newComparator(String field, int hits, int sort, boolean reversed) 307 | throws IOException { 308 | return new FieldComparator.TermValComparator(hits, field, false) { 309 | @Override 310 | public int compareValues(BytesRef val1, BytesRef val2) { 311 | return entry(val1).compareTo(entry(val2)); 312 | } 313 | }; 314 | } 315 | }); 316 | } 317 | 318 | } 319 | -------------------------------------------------------------------------------- /fhir-index-plugin/src/main/java/io/puntanegra/fhir/index/config/IndexOptions.java: -------------------------------------------------------------------------------- 1 | package io.puntanegra.fhir.index.config; 2 | 3 | import java.io.File; 4 | import java.nio.file.Path; 5 | import java.nio.file.Paths; 6 | import java.util.Map; 7 | 8 | import org.apache.cassandra.config.CFMetaData; 9 | import org.apache.cassandra.config.ColumnDefinition; 10 | import org.apache.cassandra.db.Directories; 11 | import org.apache.cassandra.schema.IndexMetadata; 12 | import org.apache.cassandra.utils.ByteBufferUtil; 13 | 14 | import io.puntanegra.fhir.index.FhirIndexException; 15 | import io.puntanegra.fhir.index.util.JsonSerializer; 16 | 17 | /** 18 | * FHIR Index configuration options parser. 19 | * 20 | * @author Jorge L. Middleton {@literal } 21 | * 22 | */ 23 | public class IndexOptions { 24 | 25 | public static final String REFRESH_SECONDS_OPTION = "refresh_seconds"; 26 | public static final double DEFAULT_REFRESH_SECONDS = 60; 27 | 28 | public static final String RAM_BUFFER_MB_OPTION = "ram_buffer_mb"; 29 | public static final int DEFAULT_RAM_BUFFER_MB = 64; 30 | 31 | public static final String MAX_MERGE_MB_OPTION = "max_merge_mb"; 32 | public static final int DEFAULT_MAX_MERGE_MB = 5; 33 | 34 | public static final String MAX_CACHED_MB_OPTION = "max_cached_mb"; 35 | public static final int DEFAULT_MAX_CACHED_MB = 30; 36 | 37 | public static final String INDEXING_THREADS_OPTION = "indexing_threads"; 38 | public static final int DEFAULT_INDEXING_THREADS = 0; 39 | 40 | public static final String INDEXING_QUEUES_SIZE_OPTION = "indexing_queues_size"; 41 | public static final int DEFAULT_INDEXING_QUEUES_SIZE = 50; 42 | 43 | public static final String SEARCH_CACHE_SIZE_OPTION = "search_cache_size"; 44 | public static final int DEFAULT_SEARCH_CACHE_SIZE = 16; 45 | 46 | public static final String DIRECTORY_PATH_OPTION = "directory_path"; 47 | public static final String INDEXES_DIR_NAME = "lucene"; 48 | 49 | public static final String SEARCH_OPTION = "search"; 50 | 51 | public static final String RESOURCE_TYPE_COLUMN = "resource_type_column"; 52 | 53 | public final ResourceOptions search; 54 | 55 | /** The path of the directory where the index files will be stored */ 56 | public final Path path; 57 | 58 | public final String resourceTypeColumn; 59 | 60 | /** The Lucene index searcher refresh frequency, in seconds */ 61 | public final double refreshSeconds; 62 | 63 | /** The Lucene's max RAM buffer size, in MB */ 64 | public final int ramBufferMB; 65 | 66 | /** The Lucene's max segments merge size size, in MB */ 67 | public final int maxMergeMB; 68 | 69 | /** The Lucene's max cache size, in MB */ 70 | public final int maxCachedMB; 71 | 72 | /** The number of asynchronous indexing threads */ 73 | public final int indexingThreads; 74 | 75 | /** The size of the asynchronous indexing queues */ 76 | public final int indexingQueuesSize; 77 | 78 | /** The max size of the search cache */ 79 | public final int searchCacheSize; 80 | 81 | /** ColumnDefinition of the target column associated with the index **/ 82 | public final ColumnDefinition targetColumn; 83 | 84 | /** 85 | * Builds a new {@link IndexOptions} for the column family and index 86 | * metadata. 87 | * 88 | * @param tableMetadata 89 | * the indexed table metadata 90 | * @param indexMetadata 91 | * the index metadata 92 | */ 93 | public IndexOptions(CFMetaData tableMetadata, IndexMetadata indexMetadata) { 94 | Map options = indexMetadata.options; 95 | refreshSeconds = parseRefresh(options); 96 | resourceTypeColumn = parseResourceTypeColumn(options); 97 | ramBufferMB = parseRamBufferMB(options); 98 | maxMergeMB = parseMaxMergeMB(options); 99 | maxCachedMB = parseMaxCachedMB(options); 100 | indexingThreads = parseIndexingThreads(options); 101 | indexingQueuesSize = parseIndexingQueuesSize(options); 102 | searchCacheSize = parseSearchCacheSize(options); 103 | path = parsePath(options, tableMetadata, indexMetadata); 104 | search = parseSearchOptions(options); 105 | 106 | String targetColumnName = indexMetadata.options.get("target"); 107 | targetColumn = tableMetadata.getColumnDefinition(ByteBufferUtil.bytes(targetColumnName)); 108 | 109 | } 110 | 111 | private String parseResourceTypeColumn(Map options) { 112 | return options.get(RESOURCE_TYPE_COLUMN); 113 | } 114 | 115 | /** 116 | * Validates the specified index options. 117 | * 118 | * @param options 119 | * the options to be validated 120 | * @param metadata 121 | * the indexed table metadata 122 | */ 123 | public static void validateOptions(Map options, CFMetaData metadata) { 124 | parseRefresh(options); 125 | parseRamBufferMB(options); 126 | parseMaxMergeMB(options); 127 | parseMaxCachedMB(options); 128 | parseIndexingThreads(options); 129 | parseIndexingQueuesSize(options); 130 | parseSearchCacheSize(options); 131 | parseSearchOptions(options); 132 | parsePath(options, metadata, null); 133 | } 134 | 135 | private static double parseRefresh(Map options) { 136 | String refreshOption = options.get(REFRESH_SECONDS_OPTION); 137 | if (refreshOption != null) { 138 | double refreshSeconds; 139 | try { 140 | refreshSeconds = Double.parseDouble(refreshOption); 141 | } catch (NumberFormatException e) { 142 | throw new FhirIndexException("'%s' must be a strictly positive double", REFRESH_SECONDS_OPTION); 143 | } 144 | if (refreshSeconds <= 0) { 145 | throw new FhirIndexException("'%s' must be strictly positive", REFRESH_SECONDS_OPTION); 146 | } 147 | return refreshSeconds; 148 | } else { 149 | return DEFAULT_REFRESH_SECONDS; 150 | } 151 | } 152 | 153 | private static int parseRamBufferMB(Map options) { 154 | String ramBufferSizeOption = options.get(RAM_BUFFER_MB_OPTION); 155 | if (ramBufferSizeOption != null) { 156 | int ramBufferMB; 157 | try { 158 | ramBufferMB = Integer.parseInt(ramBufferSizeOption); 159 | } catch (NumberFormatException e) { 160 | throw new FhirIndexException("'%s' must be a strictly positive integer", RAM_BUFFER_MB_OPTION); 161 | } 162 | if (ramBufferMB <= 0) { 163 | throw new FhirIndexException("'%s' must be strictly positive", RAM_BUFFER_MB_OPTION); 164 | } 165 | return ramBufferMB; 166 | } else { 167 | return DEFAULT_RAM_BUFFER_MB; 168 | } 169 | } 170 | 171 | private static int parseMaxMergeMB(Map options) { 172 | String maxMergeSizeMBOption = options.get(MAX_MERGE_MB_OPTION); 173 | if (maxMergeSizeMBOption != null) { 174 | int maxMergeMB; 175 | try { 176 | maxMergeMB = Integer.parseInt(maxMergeSizeMBOption); 177 | } catch (NumberFormatException e) { 178 | throw new FhirIndexException("'%s' must be a strictly positive integer", MAX_MERGE_MB_OPTION); 179 | } 180 | if (maxMergeMB <= 0) { 181 | throw new FhirIndexException("'%s' must be strictly positive", MAX_MERGE_MB_OPTION); 182 | } 183 | return maxMergeMB; 184 | } else { 185 | return DEFAULT_MAX_MERGE_MB; 186 | } 187 | } 188 | 189 | private static int parseMaxCachedMB(Map options) { 190 | String maxCachedMBOption = options.get(MAX_CACHED_MB_OPTION); 191 | if (maxCachedMBOption != null) { 192 | int maxCachedMB; 193 | try { 194 | maxCachedMB = Integer.parseInt(maxCachedMBOption); 195 | } catch (NumberFormatException e) { 196 | throw new FhirIndexException("'%s' must be a strictly positive integer", MAX_CACHED_MB_OPTION); 197 | } 198 | if (maxCachedMB <= 0) { 199 | throw new FhirIndexException("'%s' must be strictly positive", MAX_CACHED_MB_OPTION); 200 | } 201 | return maxCachedMB; 202 | } else { 203 | return DEFAULT_MAX_CACHED_MB; 204 | } 205 | } 206 | 207 | private static int parseIndexingThreads(Map options) { 208 | String indexPoolNumQueuesOption = options.get(INDEXING_THREADS_OPTION); 209 | if (indexPoolNumQueuesOption != null) { 210 | try { 211 | return Integer.parseInt(indexPoolNumQueuesOption); 212 | } catch (NumberFormatException e) { 213 | throw new FhirIndexException("'%s' must be a positive integer", INDEXING_THREADS_OPTION); 214 | } 215 | } else { 216 | return DEFAULT_INDEXING_THREADS; 217 | } 218 | } 219 | 220 | private static int parseIndexingQueuesSize(Map options) { 221 | String indexPoolQueuesSizeOption = options.get(INDEXING_QUEUES_SIZE_OPTION); 222 | if (indexPoolQueuesSizeOption != null) { 223 | int indexingQueuesSize; 224 | try { 225 | indexingQueuesSize = Integer.parseInt(indexPoolQueuesSizeOption); 226 | } catch (NumberFormatException e) { 227 | throw new FhirIndexException("'%s' must be a strictly positive integer", INDEXING_QUEUES_SIZE_OPTION); 228 | } 229 | if (indexingQueuesSize <= 0) { 230 | throw new FhirIndexException("'%s' must be strictly positive", INDEXING_QUEUES_SIZE_OPTION); 231 | } 232 | return indexingQueuesSize; 233 | } else { 234 | return DEFAULT_INDEXING_QUEUES_SIZE; 235 | } 236 | } 237 | 238 | private static int parseSearchCacheSize(Map options) { 239 | String searchCacheSizeOption = options.get(SEARCH_CACHE_SIZE_OPTION); 240 | if (searchCacheSizeOption != null) { 241 | int searchCacheSize; 242 | try { 243 | searchCacheSize = Integer.parseInt(searchCacheSizeOption); 244 | } catch (NumberFormatException e) { 245 | throw new FhirIndexException("'%s' must be a positive integer", SEARCH_CACHE_SIZE_OPTION); 246 | } 247 | if (searchCacheSize < 0) { 248 | throw new FhirIndexException("'%s' must be positive", SEARCH_CACHE_SIZE_OPTION); 249 | } 250 | return searchCacheSize; 251 | } else { 252 | return DEFAULT_SEARCH_CACHE_SIZE; 253 | } 254 | } 255 | 256 | private static Path parsePath(Map options, CFMetaData tableMetadata, IndexMetadata indexMetadata) { 257 | String pathOption = options.get(DIRECTORY_PATH_OPTION); 258 | if (pathOption != null) { 259 | return Paths.get(pathOption); 260 | } else if (indexMetadata != null) { 261 | Directories directories = new Directories(tableMetadata); 262 | String basePath = directories.getDirectoryForNewSSTables().getAbsolutePath(); 263 | return Paths.get(basePath + File.separator + INDEXES_DIR_NAME + File.separator + indexMetadata.name); 264 | } 265 | return null; 266 | } 267 | 268 | private static ResourceOptions parseSearchOptions(Map options) { 269 | String searchOption = options.get(SEARCH_OPTION); 270 | if (searchOption != null && !searchOption.trim().isEmpty()) { 271 | ResourceOptions searchOptions; 272 | try { 273 | searchOptions = JsonSerializer.fromString(searchOption, ResourceOptions.class); 274 | return searchOptions; 275 | } catch (Exception e) { 276 | throw new FhirIndexException(e, "'%s' is invalid : %s", SEARCH_OPTION, e.getMessage()); 277 | } 278 | } else { 279 | throw new FhirIndexException("'%s' required", SEARCH_OPTION); 280 | } 281 | } 282 | 283 | @Override 284 | public String toString() { 285 | StringBuilder builder = new StringBuilder(); 286 | builder.append("IndexOptions [path="); 287 | builder.append(path); 288 | builder.append(", resourceTypeColumn="); 289 | builder.append(resourceTypeColumn); 290 | builder.append(", refreshSeconds="); 291 | builder.append(refreshSeconds); 292 | builder.append(", ramBufferMB="); 293 | builder.append(ramBufferMB); 294 | builder.append(", maxMergeMB="); 295 | builder.append(maxMergeMB); 296 | builder.append(", maxCachedMB="); 297 | builder.append(maxCachedMB); 298 | builder.append(", indexingThreads="); 299 | builder.append(indexingThreads); 300 | builder.append(", indexingQueuesSize="); 301 | builder.append(indexingQueuesSize); 302 | builder.append(", searchCacheSize="); 303 | builder.append(searchCacheSize); 304 | builder.append(", targetColumn="); 305 | builder.append(targetColumn); 306 | builder.append("]"); 307 | return builder.toString(); 308 | } 309 | 310 | } 311 | --------------------------------------------------------------------------------