Skip to content

Commit d27bca5

Browse files
committed
Add near Search Param for Location Resource
Signed-off-by: Jonas Wagner <jwagner@knoppiks.de>
1 parent 5dcda54 commit d27bca5

17 files changed

Lines changed: 655 additions & 5 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/bin/bash -e
2+
3+
script_dir="$(dirname "$(readlink -f "$0")")"
4+
. "$script_dir/util.sh"
5+
6+
base="http://localhost:8080/fhir"
7+
8+
location() {
9+
cat <<END
10+
{
11+
"resourceType": "Location",
12+
"name": "Leipzig",
13+
"position": {
14+
"latitude": 51.3397,
15+
"longitude": 12.3731
16+
}
17+
}
18+
END
19+
}
20+
21+
create() {
22+
curl -s -f -H "Content-Type: application/fhir+json" -H 'Accept: application/fhir+json' -d @- -o /dev/null "$base/Location"
23+
}
24+
25+
location | create
26+
27+
search() {
28+
curl -s -H "Content-Type: application/fhir+json" "$base/Location?near=$1&summary=count" | jq -r .total
29+
}
30+
31+
test "Location 900km from Florence finds Leipzig" "$(search "43.77925|11.24626|900|km")" "1"

.github/workflows/build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,6 +1318,9 @@ jobs:
13181318
- name: Control Character Handling
13191319
run: .github/scripts/control-character-handling.sh
13201320

1321+
- name: Control Character Handling
1322+
run: .github/scripts/search-location-near.sh
1323+
13211324
- name: Prometheus Metrics
13221325
run: .github/scripts/test-metrics.sh
13231326
if: ${{ matrix.variant == 'standalone' }}

docs/api/interaction/search-type.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ Search for `Resource.meta.profile` is supported using the `_profile` search para
4040

4141
When searching for date/time with a search parameter value without timezone like `2024` or `2024-02-16`, Blaze calculates the range of the search parameter values based on [UTC][2]. That means that a resource with a date/time value staring at `2024-01-01T00:00:00+01:00` will be not found by a search with `2024`. Please comment on [issue #1498](https://github.com/samply/blaze/issues/1498) if you like to have this situation improved.
4242

43+
## Geopositional Search
44+
45+
Blaze implements the [positional](https://hl7.org/fhir/R4B/location.html#positional) search parameter `near` for resources with a geospatial position (i.e., Location). The search parameter takes a latitude, longitude, distance and unit as search parameter values in the form `longitude|latitude[|distance[|unit]]`. Defaults for `distance` and `unit` are `1` and `km`. The [Haversine formula](https://en.wikipedia.org/wiki/Haversine_formula) is used to calculate the distance between the search parameter value and the resource's location, which simplifies calculation by assuming a spherical earth and has an error of less than approximately 0.5%.
46+
4347
## Sorting
4448

4549
The special search parameter `_sort` supports the values `_id`, `_lastUpdated` and `-_lastUpdated`.

modules/db/src/blaze/db/impl/index/resource_as_of.clj

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,3 +410,22 @@
410410
(rf result handle)
411411
result)
412412
result)))))))
413+
414+
(defn- encode-seek-key [tid]
415+
(-> (bb/allocate codec/tid-size)
416+
(bb/put-int! tid)
417+
bb/flip!
418+
(bs/from-byte-buffer!)))
419+
420+
(defn estimated-scan-size
421+
"Returns a relative estimation for the amount of work to do while scanning the
422+
ResourceAsOf index with the prefix consisting of `tid`.
423+
424+
The metric is relative and unitless. It can be only used to compare the amount
425+
of scan work between different prefixes.
426+
427+
Returns an anomaly if estimating the scan size isn't supported by `kv-store`."
428+
[kv-store tid]
429+
(let [seek-key (encode-seek-key tid)
430+
key-range [seek-key (bs/concat seek-key (bs/from-hex "FF"))]]
431+
(kv/estimate-scan-size kv-store :resource-as-of-index key-range)))

modules/db/src/blaze/db/impl/search_param.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
[blaze.db.impl.search-param.date]
1414
[blaze.db.impl.search-param.has]
1515
[blaze.db.impl.search-param.list]
16+
[blaze.db.impl.search-param.near]
1617
[blaze.db.impl.search-param.number]
1718
[blaze.db.impl.search-param.quantity]
1819
[blaze.db.impl.search-param.string]
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
(ns blaze.db.impl.search-param.near
2+
(:require
3+
[blaze.anomaly :as ba :refer [when-ok]]
4+
[blaze.coll.core :as coll]
5+
[blaze.db.api :as d]
6+
[blaze.db.impl.index.index-handle :as ih]
7+
[blaze.db.impl.index.resource-as-of :as rao]
8+
[blaze.db.impl.index.single-version-id :as svi]
9+
[blaze.db.impl.protocols :as p]
10+
[blaze.db.impl.search-param.near.geo :as spng]
11+
[blaze.db.impl.search-param.special :as special]
12+
[blaze.db.impl.search-param.util :as u]
13+
[blaze.fhir.spec.type :as type]
14+
[blaze.fhir.spec.type.system :as system]
15+
[clojure.string :as str]
16+
[cognitect.anomalies :as anom]))
17+
18+
(set! *warn-on-reflection* true)
19+
20+
(defn- near? [{:keys [longitude latitude]} {:keys [distance] :as compiled-value}]
21+
(let [coordinates {:latitude (type/value latitude)
22+
:longitude (type/value longitude)}
23+
actual-dist (spng/haversine-distance coordinates compiled-value)]
24+
(<= actual-dist distance)))
25+
26+
(defn- matches? [batch-db resource-handle compiled-values]
27+
(let [{:keys [position]} @(d/pull batch-db resource-handle)]
28+
(some #(near? position %) compiled-values)))
29+
30+
(defn- missing-param-msg [arg code]
31+
(format "Missing argument `%s` in search parameter `%s`." arg code))
32+
33+
(defn- parsing-err-msg [arg code]
34+
(format "Error parsing argument `%s` in search parameter `%s`." arg code))
35+
36+
(defn- update-msg [msg]
37+
(fn [anomaly]
38+
(update anomaly ::anom/message #(str msg " " %))))
39+
40+
(defn- parse-decimal-arg [coord name code]
41+
(if (str/blank? coord)
42+
(ba/incorrect (missing-param-msg name code))
43+
(-> (system/parse-decimal coord)
44+
(ba/exceptionally (update-msg (parsing-err-msg name code))))))
45+
46+
(defn- unsupported-unit-msg [unit code]
47+
(format "Unsupported unit `%s` in search parameter `%s`. Supported are 'km', 'm'." unit code))
48+
49+
(defn- parse-distance [dist-str unit code]
50+
(cond
51+
(str/blank? dist-str)
52+
1000M
53+
54+
(or (nil? unit) (= unit "km"))
55+
(when-ok [dist (parse-decimal-arg dist-str "distance" code)]
56+
(* dist 1000))
57+
58+
(= unit "m")
59+
(parse-decimal-arg dist-str "distance" code)
60+
61+
:else
62+
(ba/incorrect (unsupported-unit-msg unit code))))
63+
64+
(def ^:private ^:const min-latitude -90.0)
65+
66+
(def ^:private ^:const max-latitude 90.0)
67+
68+
(defn invalid-latitude-msg [arg code]
69+
(format
70+
"Invalid argument `%s` for latitude in search parameter `%s`, must be between %s and %s."
71+
arg code min-latitude max-latitude))
72+
73+
(defn- parse-latitude [val code]
74+
(when-ok [latitude (parse-decimal-arg val "latitude" code)]
75+
(if (<= min-latitude latitude max-latitude)
76+
latitude
77+
(ba/incorrect (invalid-latitude-msg val code)))))
78+
79+
(def ^:private ^:const min-longitude -180.0)
80+
81+
(def ^:private ^:const max-longitude 180.0)
82+
83+
(defn invalid-longitude-msg [val code]
84+
(format
85+
"Invalid argument `%s` for longitude in search parameter `%s`, must be between %s and %s."
86+
val code min-longitude max-longitude))
87+
88+
(defn- parse-longitude [val code]
89+
(when-ok [longitude (parse-decimal-arg val "longitude" code)]
90+
(if (<= min-longitude longitude max-longitude)
91+
longitude
92+
(ba/incorrect (invalid-longitude-msg val code)))))
93+
94+
(defrecord SearchParamNear [index name type code]
95+
p/SearchParam
96+
(-validate-modifier [_ modifier]
97+
(some->> modifier (u/unknown-modifier-anom code)))
98+
99+
(-compile-value [_ _ value]
100+
(let [[lat long dist unit] (str/split value #"\|" 4)]
101+
(when-ok [parsed-lat (parse-latitude lat code)
102+
parsed-long (parse-longitude long code)
103+
parsed-dist (parse-distance dist unit code)]
104+
{:latitude parsed-lat
105+
:longitude parsed-long
106+
:distance parsed-dist})))
107+
108+
(-estimated-scan-size [_ batch-db tid _ _]
109+
(rao/estimated-scan-size (:kv-store batch-db) tid))
110+
111+
(-supports-ordered-index-handles [_ _ _ _ _]
112+
true)
113+
114+
(-ordered-index-handles [search-param batch-db tid modifier compiled-values]
115+
(if (= 1 (count compiled-values))
116+
(p/-index-handles search-param batch-db tid modifier (first compiled-values))
117+
(let [index-handles #(p/-index-handles search-param batch-db tid modifier %)]
118+
(u/union-index-handles (map index-handles compiled-values)))))
119+
120+
(-ordered-index-handles [search-param batch-db tid modifier compiled-values start-id]
121+
(if (= 1 (count compiled-values))
122+
(p/-index-handles search-param batch-db tid modifier (first compiled-values) start-id)
123+
(let [index-handles #(p/-index-handles search-param batch-db tid modifier % start-id)]
124+
(u/union-index-handles (map index-handles compiled-values)))))
125+
126+
(-index-handles [search-param batch-db tid modifier compiled-value]
127+
(coll/eduction
128+
(comp (p/-matcher search-param batch-db modifier [compiled-value])
129+
(map ih/from-resource-handle))
130+
(p/-type-list batch-db tid)))
131+
132+
(-index-handles [search-param batch-db tid modifier compiled-value start-id]
133+
(coll/eduction
134+
(comp (p/-matcher search-param batch-db modifier [compiled-value])
135+
(map ih/from-resource-handle))
136+
(p/-type-list batch-db tid start-id)))
137+
138+
(-supports-ordered-compartment-index-handles [_ _]
139+
false)
140+
141+
(-ordered-compartment-index-handles [_ _ _ _ _]
142+
(ba/unsupported))
143+
144+
(-ordered-compartment-index-handles [_ _ _ _ _ _]
145+
(ba/unsupported))
146+
147+
(-matcher [_ batch-db _ compiled-values]
148+
(filter #(matches? batch-db % compiled-values)))
149+
150+
(-single-version-id-matcher [search-param batch-db tid modifier compiled-values]
151+
(comp (map ih/from-single-version-id)
152+
(u/resource-handle-xf batch-db tid)
153+
(p/-matcher search-param batch-db modifier compiled-values)
154+
(map svi/from-resource-handle)))
155+
156+
(-second-pass-filter [_ _ _])
157+
158+
(-index-values [_ _ _]
159+
[]))
160+
161+
(defmethod special/special-search-param "near"
162+
[index _]
163+
(->SearchParamNear index "near" "special" "near"))
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
(ns blaze.db.impl.search-param.near.geo)
2+
3+
(set! *warn-on-reflection* true)
4+
5+
(def ^:private ^:const earth-radius
6+
"Earths mean radius in meters, see https://en.wikipedia.org/wiki/Earth_radius."
7+
6371008.7714)
8+
9+
(defn- hav
10+
"Haversed sine of `theta`."
11+
[theta]
12+
(let [half-angle (/ theta 2)]
13+
(* (Math/sin half-angle) (Math/sin half-angle))))
14+
15+
(defn haversine-distance
16+
"Calculate the distance between two geographic coordinates (`location-1` and
17+
`location-2`) using the Haversine formula, which simplifies by assuming a
18+
spherical earth (error ≤ 0.5%). Returns distance in meters. See
19+
https://en.wikipedia.org/wiki/Haversine_formula."
20+
{:arglists '([location-1 location-2])}
21+
[{lat-1 :latitude lon-1 :longitude} {lat-2 :latitude lon-2 :longitude}]
22+
(let [delta-lat (Math/toRadians (- lat-2 lat-1))
23+
delta-long (Math/toRadians (- lon-2 lon-1))
24+
alpha (+ (hav delta-lat)
25+
(* (Math/cos (Math/toRadians lat-1))
26+
(Math/cos (Math/toRadians lat-2))
27+
(hav delta-long)))]
28+
(* earth-radius (Math/asin (Math/sqrt alpha)) 2)))

modules/db/src/blaze/db/search_param_registry.clj

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,13 +221,18 @@
221221
{:type "special"
222222
:name "_has"})
223223

224+
(def ^:private near-search-param
225+
{:type "special"
226+
:name "near"})
227+
224228
(defn- add-special
225229
"Add special search params to `index`.
226230
227231
See: https://www.hl7.org/fhir/search.html#special"
228232
[index]
229233
(-> (assoc-in index ["Resource" "_list"] (sc/search-param nil list-search-param))
230-
(assoc-in ["Resource" "_has"] (sc/search-param index has-search-param))))
234+
(assoc-in ["Resource" "_has"] (sc/search-param index has-search-param))
235+
(assoc-in ["Location" "near"] (sc/search-param index near-search-param))))
231236

232237
(defn- build-url-index* [index filter entries]
233238
(transduce

0 commit comments

Comments
 (0)