Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
18 changes: 16 additions & 2 deletions packages/instant-meilisearch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -791,7 +791,7 @@ The following parameters exist:
- `boundingBox`: The Google Map window box. It is used as parameter in a search request. It takes precedent on all the following parameters.
- `aroundLatLng`: The middle point of the Google Map. If `insideBoundingBox` or `boundingBox` is present, it is ignored.
- `aroundRadius`: The radius around a Geo Point, used for sorting in the search request. It only works if `aroundLatLng` is present as well. If `insideBoundingBox` or `boundingBox` is present, it is ignored.

- `insidePolygon`: Filters search results to only include documents whose coordinates fall within a specified polygon. This parameter accepts an array of coordinate pairs `[[lat, lng], [lat, lng], ...]` that define the polygon vertices (minimum 3 points required). When `insidePolygon` is specified, it takes precedence over `insideBoundingBox` and `around*` parameters. Polygon filters require documents to contain a valid `_geojson` field with [GeoJSON format](https://geojson.org/). Documents without `_geojson` will not be returned in polygon searches, even if they have `_geo` coordinates.

For exemple, by adding `boundingBox` in the [`instantSearch`](#-instantsearch) widget parameters, the parameter will be used as a search parameter for the first request.

Expand All @@ -817,7 +817,21 @@ Alternatively, the parameters can be passed through the [`searchFunction`](https
},
```

[Read the guide on how GeoSearch works in Meilisearch](https://www.meilisearch.com/docs/learn/getting_started/filtering_and_sorting#geosearch).
You can also filter results within a polygon using `insidePolygon`.

```js
search.addWidgets([
instantsearch.widgets.configure({
insidePolygon: [
[50.8466, 4.35],
[50.75, 4.1],
[50.65, 4.5],
],
}),
])
```

For more information, read the [geosearch documentation](https://www.meilisearch.com/docs/learn/filtering_and_sorting/geosearch).

### ❌ Answers

Expand Down
81 changes: 81 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,189 @@ 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
97 changes: 96 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,99 @@ 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.7, 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.8, lng: 4.35 },
},
])
.waitTask()

const response = await searchClient.search<City>([
{
indexName: 'geotest',
params: {
query: '',
insidePolygon: [
[50.95, 4.1],
[50.75, 4.6],
[50.7, 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()
})
Loading