From 4636a9c696bfbc3f0f038d6787f425868b6ffcff Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 8 Sep 2026 18:38:12 +0200 Subject: [PATCH] fix(search): index a resource with out-of-range coordinates without a geopoint Corrupt EXIF (latitude beyond 90, longitude beyond 180) made OpenSearch reject the whole document on the geo_point sibling while bleve indexed it at the pole. Both engines now skip the sibling and keep the location object; the parity suite pins it. --- services/search/pkg/mapping/geo.go | 5 ++++ services/search/pkg/mapping/geo_test.go | 21 ++++++++++++++++ services/search/pkg/parity/README.md | 2 ++ .../pkg/parity/response_metadata_test.go | 24 ++++++++++++++++++- 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/services/search/pkg/mapping/geo.go b/services/search/pkg/mapping/geo.go index 3d0adf61a7..4aa7f79de6 100644 --- a/services/search/pkg/mapping/geo.go +++ b/services/search/pkg/mapping/geo.go @@ -47,5 +47,10 @@ func addGeopointSibling(m map[string]any, dottedPath string) { if !hasLon || !hasLat { return } + // corrupt EXIF: OpenSearch rejects the whole document on an out-of-range + // geo_point, bleve would index it at the pole; both index it without one + if lat < -90 || lat > 90 || lon < -180 || lon > 180 { + return + } parent[leaf+GeopointSuffix] = map[string]any{"lat": lat, "lon": lon} } diff --git a/services/search/pkg/mapping/geo_test.go b/services/search/pkg/mapping/geo_test.go index c0eac54fef..369c3a2c8d 100644 --- a/services/search/pkg/mapping/geo_test.go +++ b/services/search/pkg/mapping/geo_test.go @@ -64,6 +64,27 @@ var _ = Describe("PrepareForIndex geopoint", func() { Expect(gp["lon"]).To(Equal(lon)) }) + It("skips out-of-range geopoints but keeps the object", func() { + type geoDoc struct { + Location *struct { + Longitude *float64 `json:"longitude,omitempty"` + Latitude *float64 `json:"latitude,omitempty"` + } `json:"location,omitempty"` + } + for _, c := range [][2]float64{{11.1, 100}, {11.1, -90.5}, {180.5, 49.4}, {-181, 49.4}} { + lon, lat := c[0], c[1] + doc := geoDoc{Location: &struct { + Longitude *float64 `json:"longitude,omitempty"` + Latitude *float64 `json:"latitude,omitempty"` + }{Longitude: &lon, Latitude: &lat}} + + m, err := PrepareForIndex(doc, map[string]FieldOpts{"location": {Type: TypeGeopoint}}) + Expect(err).ToNot(HaveOccurred()) + Expect(m).To(HaveKey("location"), "lon=%v lat=%v", lon, lat) + Expect(m).ToNot(HaveKey("location"+GeopointSuffix), "lon=%v lat=%v", lon, lat) + } + }) + It("skips incomplete geopoints", func() { type geoDoc struct { Location *struct { diff --git a/services/search/pkg/parity/README.md b/services/search/pkg/parity/README.md index 8b3691c710..55ad864b0e 100644 --- a/services/search/pkg/parity/README.md +++ b/services/search/pkg/parity/README.md @@ -687,9 +687,11 @@ Fixtures: - `some_song.mp3`, ID = 1$1!5, MimeType = audio/mpeg - `team.jpg`, ID = 1$1!6, MimeType = image/jpeg +- `lost.jpg`, ID = 1$1!7, MimeType = image/jpeg | Case | Query | expected | bleve | OpenSearch | same? | |---|---|---|---|---|---| | METADATA-01 | `*song*` reads `Audio` | all 16 fields unchanged | all 16 fields unchanged | all 16 fields unchanged | ✅ | | METADATA-02 | `*team*` reads `Location` | all 3 fields unchanged | all 3 fields unchanged | all 3 fields unchanged | ✅ | +| METADATA-04 | `*lost*` reads `Location` | Latitude=100, Longitude=11.1 | Latitude=100, Longitude=11.1 | Latitude=100, Longitude=11.1 | ✅ | | METADATA-03 | `*team*` reads `Audio` | none | none | none | ✅ | diff --git a/services/search/pkg/parity/response_metadata_test.go b/services/search/pkg/parity/response_metadata_test.go index e4bbca5e34..4b79e0034d 100644 --- a/services/search/pkg/parity/response_metadata_test.go +++ b/services/search/pkg/parity/response_metadata_test.go @@ -43,6 +43,17 @@ func metadataGroup() responseGroup { }), ) + // corrupt EXIF: latitude beyond 90 must not lose the file from the index + lost := fixtureDoc("lost.jpg", + withID("1$1!7"), + withMime("image/jpeg"), + withLocation(&libregraph.GeoCoordinates{ + Altitude: libregraph.PtrFloat64(0), + Latitude: libregraph.PtrFloat64(100), + Longitude: libregraph.PtrFloat64(11.1), + }), + ) + indexed := []string{ "Album=Some Album", "AlbumArtist=Some AlbumArtist", @@ -70,7 +81,7 @@ func metadataGroup() responseGroup { return responseGroup{ name: "metadata", - fixtures: []search.Resource{song, team}, + fixtures: []search.Resource{song, team, lost}, cases: []responseCase{ { id: 1, query: `*song*`, reads: "Audio", want: unchanged(indexed), @@ -109,6 +120,17 @@ func metadataGroup() responseGroup { }) }), }, + { + id: 4, query: `*lost*`, reads: "Location", want: []string{"Latitude=100", "Longitude=11.1"}, + read: readsMany(func(m *searchMessage.Match) []string { + location := m.GetEntity().GetLocation() + + return []string{ + fmt.Sprintf("Latitude=%v", location.GetLatitude()), + fmt.Sprintf("Longitude=%v", location.GetLongitude()), + } + }), + }, { id: 3, query: `*team*`, reads: "Audio", want: []string{"none"}, read: reads(func(m *searchMessage.Match) string {