Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/mean-weeks-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@meilisearch/instant-meilisearch": minor
---

Add support for insidePolygon filter to geosearch
51 changes: 35 additions & 16 deletions packages/instant-meilisearch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -747,29 +747,34 @@ The `geoSearch` widget displays search results on a Google Map. It lets you sear
- ✅ templates: The templates to use for the widget.
- ✅ cssClasses: The CSS classes to override.

[See our playground for a working exemple](./playgrounds/geo-javascript/src/app.js) and this section in our [contributing guide](./CONTRIBUTING.md#-geo-search-playground) to set up your `Meilisearch`.
[See our playground for a working example](./playgrounds/geo-javascript/src/app.js) and this section in our [contributing guide](./CONTRIBUTING.md#-geo-search-playground) to set up your `Meilisearch`.

#### Requirements

The Geosearch widgey only works with a valid Google API key.
The Geosearch widget only works with a valid Google API key.

In order to communicate your Google API key, your `instantSearch` widget should be surrounded by the following function:
The example below uses the `@googlemaps/js-api-loader` package to load the Google Maps library before initializing `instantSearch`:

```js
import injectScript from 'scriptjs'

injectScript(
`https://maps.googleapis.com/maps/api/js?v=quarterly&key=${GOOGLE_API}`,
() => {
const search = instantsearch({
indexName: 'geo',
// ...
})
// ...
import { setOptions, importLibrary } from '@googlemaps/js-api-loader'

const GOOGLE_MAP_API_KEY = 'YOUR_GOOGLE_MAPS_API_KEY'

setOptions({
apiKey: GOOGLE_MAP_API_KEY,
version: 'weekly',
})

importLibrary('maps').then(() => {
const search = instantsearch({
indexName: 'geo',
// ...
})
// ...
})
```

Replace `${GOOGLE_API}` with you google api key.
Replace `YOUR_GOOGLE_MAPS_API_KEY` with your Google API key.

See [code example in the playground](./playgrounds/geo-javascript/src/app.js)

Expand All @@ -791,7 +796,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 +822,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()
})
})
Loading