Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/instant-meilisearch/__tests__/assets/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,108 +104,129 @@ const geoDataset = [
id: '1',
city: 'Lille',
_geo: { lat: 50.629973371633746, lng: 3.056944739941957 },
_geojson: { type: 'Point', coordinates: [3.056944739941957, 50.629973371633746] },
},
{
id: '2',
city: 'Mons-en-Barœul',
_geo: { lat: 50.64158612012105, lng: 3.110659348034867 },
_geojson: { type: 'Point', coordinates: [3.110659348034867, 50.64158612012105] },
},
{
id: '3',
city: 'Hellemmes',
_geo: { lat: 50.63122096551808, lng: 3.1106399673339933 },
_geojson: { type: 'Point', coordinates: [3.1106399673339933, 50.63122096551808] },
},
{
id: '4',
city: "Villeneuve-d'Ascq",
_geo: { lat: 50.622468098014565, lng: 3.147642551343714 },
_geojson: { type: 'Point', coordinates: [3.147642551343714, 50.622468098014565] },
},
{
id: '5',
city: 'Hem',
_geo: { lat: 50.655250871381355, lng: 3.189729726624413 },
_geojson: { type: 'Point', coordinates: [3.189729726624413, 50.655250871381355] },
},
{
id: '6',
city: 'Roubaix',
_geo: { lat: 50.69247345189671, lng: 3.176332673774765 },
_geojson: { type: 'Point', coordinates: [3.176332673774765, 50.69247345189671] },
},
{
id: '7',
city: 'Tourcoing',
_geo: { lat: 50.72639746673648, lng: 3.154165365957867 },
_geojson: { type: 'Point', coordinates: [3.154165365957867, 50.72639746673648] },
},
{
id: '8',
city: 'Mouscron',
_geo: { lat: 50.74532555490861, lng: 3.2206407854429853 },
_geojson: { type: 'Point', coordinates: [3.2206407854429853, 50.74532555490861] },
},
{
id: '9',
city: 'Tournai',
_geo: { lat: 50.60534252860263, lng: 3.3758586941351414 },
_geojson: { type: 'Point', coordinates: [3.3758586941351414, 50.60534252860263] },
},
{
id: '10',
city: 'Ghent',
_geo: { lat: 51.053777403679035, lng: 3.695773311992693 },
_geojson: { type: 'Point', coordinates: [3.695773311992693, 51.053777403679035] },
},
{
id: '11',
city: 'Brussels',
_geo: { lat: 50.84664097454469, lng: 4.337066356428184 },
_geojson: { type: 'Point', coordinates: [4.337066356428184, 50.84664097454469] },
},
{
id: '12',
city: 'Charleroi',
_geo: { lat: 50.40957013888948, lng: 4.434735431508552 },
_geojson: { type: 'Point', coordinates: [4.434735431508552, 50.40957013888948] },
},
{
id: '13',
city: 'Mons',
_geo: { lat: 50.45029417885542, lng: 3.962372287090469 },
_geojson: { type: 'Point', coordinates: [3.962372287090469, 50.45029417885542] },
},
{
id: '14',
city: 'Valenciennes',
_geo: { lat: 50.351817774473545, lng: 3.53262836469288 },
_geojson: { type: 'Point', coordinates: [3.53262836469288, 50.351817774473545] },
},
{
id: '15',
city: 'Arras',
_geo: { lat: 50.28448752857995, lng: 2.763751584447816 },
_geojson: { type: 'Point', coordinates: [2.763751584447816, 50.28448752857995] },
},
{
id: '16',
city: 'Cambrai',
_geo: { lat: 50.1793405779067, lng: 3.218940995250293 },
_geojson: { type: 'Point', coordinates: [3.218940995250293, 50.1793405779067] },
},
{
id: '17',
city: 'Bapaume',
_geo: { lat: 50.1112761272364, lng: 2.854789466608312 },
_geojson: { type: 'Point', coordinates: [2.854789466608312, 50.1112761272364] },
},
{
id: '18',
city: 'Amiens',
_geo: { lat: 49.931472529669996, lng: 2.271049975831708 },
_geojson: { type: 'Point', coordinates: [2.271049975831708, 49.931472529669996] },
},
{
id: '19',
city: 'Compiègne',
_geo: { lat: 49.444980887725656, lng: 2.7913841281529015 },
_geojson: { type: 'Point', coordinates: [2.7913841281529015, 49.444980887725656] },
},
{
id: '20',
city: 'Paris',
_geo: { lat: 48.90210006089548, lng: 2.370840086740693 },
_geojson: { type: 'Point', coordinates: [2.370840086740693, 48.90210006089548] },
},
]

export type City = {
id: string
city: string
_geo: { lat: number; lng: number }
_geojson?: { type: 'Point'; coordinates: [number, number] }
}

export type Movies = {
Expand Down
94 changes: 93 additions & 1 deletion packages/instant-meilisearch/__tests__/geosearch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('Instant Meilisearch Browser test', () => {
await meilisearchClient.deleteIndex('geotest').waitTask()
await meilisearchClient
.index('geotest')
.updateFilterableAttributes(['_geo'])
.updateFilterableAttributes(['_geo', '_geojson'])
.waitTask()
await meilisearchClient
.index('geotest')
Expand Down Expand Up @@ -108,4 +108,96 @@ describe('Instant Meilisearch Browser test', () => {
expect(hits.length).toEqual(2)
expect(hits[0].city).toEqual('Brussels')
})

test('insidePolygon in geo search', async () => {
const response = await searchClient.search<City>([
{
indexName: 'geotest',
params: {
query: '',
// Simple triangle roughly around Brussels area
insidePolygon: [
[50.95, 4.1],
[50.75, 4.6],
[50.70, 4.2],
],
},
},
])

const hits = response.results[0].hits
// Expect Brussels to be included
expect(hits.find((h: City) => h.city === 'Brussels')).toBeTruthy()
// Expect far cities like Paris to be excluded
expect(hits.find((h: City) => h.city === 'Paris')).toBeFalsy()
})

test('insidePolygon ignores documents without _geojson', async () => {
// Add a document inside the polygon but only with _geo (no _geojson)
await meilisearchClient
.index('geotest')
.addDocuments([
{
id: 'geo-only',
city: 'GeoOnly',
_geo: { lat: 50.80, lng: 4.35 },
},
])
.waitTask()

const response = await searchClient.search<City>([
{
indexName: 'geotest',
params: {
query: '',
insidePolygon: [
[50.95, 4.1],
[50.75, 4.6],
[50.70, 4.2],
],
},
},
])

const hits = response.results[0].hits
// Should not include the _geo-only document
expect(hits.find((h: any) => h.city === 'GeoOnly')).toBeFalsy()

// Cleanup
await meilisearchClient.index('geotest').deleteDocument('geo-only').waitTask()
})

test('aroundRadius matches _geojson-only documents', async () => {
// Add a document only with _geojson near Brussels
await meilisearchClient
.index('geotest')
.addDocuments([
{
id: 'geojson-only',
city: 'GeoJSONOnly',
_geojson: { type: 'Point', coordinates: [4.35, 50.8467] },
},
])
.waitTask()

const response = await searchClient.search<City>([
{
indexName: 'geotest',
params: {
query: '',
aroundRadius: 5000,
aroundLatLng: '50.8466, 4.35',
},
},
])

const hits = response.results[0].hits
expect(hits.find((h: any) => h.city === 'GeoJSONOnly')).toBeTruthy()

// Cleanup
await meilisearchClient
.index('geotest')
.deleteDocument('geojson-only')
.waitTask()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,56 @@ test('Adapt instantsearch geo parameters to meilisearch filters with aroundLatLn

expect(filter).toBe('_geoBoundingBox([1, 2], [3, 4])')
})

test('Adapt instantsearch geo parameters to meilisearch filters with insidePolygon (triangle)', () => {
const filter = adaptGeoSearch({
insidePolygon: [
[50.0, 3.0],
[50.7, 3.2],
[50.6, 2.9],
],
})

expect(filter).toBe('_geoPolygon([50, 3], [50.7, 3.2], [50.6, 2.9])')
})

test('Adapt instantsearch geo parameters to meilisearch filters with insidePolygon (quadrilateral)', () => {
const filter = adaptGeoSearch({
insidePolygon: [
[50.9, 4.1],
[50.9, 4.6],
[50.7, 4.6],
[50.7, 4.1],
],
})

expect(filter).toBe(
'_geoPolygon([50.9, 4.1], [50.9, 4.6], [50.7, 4.6], [50.7, 4.1])'
)
})

test('insidePolygon takes precedence over insideBoundingBox and around*', () => {
const filter = adaptGeoSearch({
insidePolygon: [
[1, 1],
[2, 2],
[3, 3],
],
insideBoundingBox: '1,2,3,4',
aroundLatLng: '51.1241999, 9.662499900000057',
aroundRadius: 10,
})

expect(filter).toBe('_geoPolygon([1, 1], [2, 2], [3, 3])')
})

test('Invalid insidePolygon (<3 points) gracefully ignored', () => {
const filter = adaptGeoSearch({
insidePolygon: [
[1, 1],
[2, 2],
],
})

expect(filter).toBeUndefined()
})
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { InstantSearchGeoParams } from '../../types/index.js'

export function adaptGeoSearch({
insidePolygon,
insideBoundingBox,
aroundLatLng,
aroundRadius,
Expand All @@ -10,6 +11,24 @@ export function adaptGeoSearch({
let radius: number | undefined
let filter: string | undefined

// Highest precedence: insidePolygon
if (Array.isArray(insidePolygon) && insidePolygon.length >= 3) {
const formattedPoints = insidePolygon
.map((pair) => {
if (!Array.isArray(pair) || pair.length < 2) return null

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. what happens when we return null? Is that ignoring the error silently?
  2. are we silently ignoring extra coordinates rather than returning an error?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, errors were silently ignored.

I updated the code to log warnings when coordinate pairs are malformed.

I chose this approach because this file already contains warnings for other ignored parameters:

      console.warn(
        'instant-meilisearch is not compatible with the `all` value on the aroundRadius parameter'
      )

const lat = Number.parseFloat(String(pair[0]))
const lng = Number.parseFloat(String(pair[1]))
if (Number.isNaN(lat) || Number.isNaN(lng)) return null
return `[${lat}, ${lng}]`
})
.filter((pt): pt is string => pt !== null)

if (formattedPoints.length >= 3) {
filter = `_geoPolygon(${formattedPoints.join(', ')})`
return filter
}
}

if (aroundLatLng) {
const [lat, lng] = aroundLatLng
.split(',')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,13 +206,15 @@ export function MeiliParamsCreator(searchContext: SearchContext) {
},
addGeoSearchFilter() {
const {
insidePolygon,
insideBoundingBox,
aroundLatLng,
aroundRadius,
minimumAroundRadius,
} = searchContext

const filter = adaptGeoSearch({
insidePolygon,
insideBoundingBox,
aroundLatLng,
aroundRadius,
Expand Down
Loading