From ec89b9f6574f3f2589f271507dc551b49499c2d0 Mon Sep 17 00:00:00 2001 From: Strift Date: Mon, 13 Oct 2025 16:25:15 +0800 Subject: [PATCH 01/12] Add _insidePolygon support to geosearch --- .../__tests__/assets/utils.ts | 21 +++++ .../__tests__/geosearch.test.ts | 94 ++++++++++++++++++- .../__tests__/geo-rules.test.ts | 53 +++++++++++ .../geo-rules-adapter.ts | 19 ++++ .../search-params-adapter.ts | 2 + 5 files changed, 188 insertions(+), 1 deletion(-) diff --git a/packages/instant-meilisearch/__tests__/assets/utils.ts b/packages/instant-meilisearch/__tests__/assets/utils.ts index 84fe3398..184637b7 100644 --- a/packages/instant-meilisearch/__tests__/assets/utils.ts +++ b/packages/instant-meilisearch/__tests__/assets/utils.ts @@ -104,101 +104,121 @@ 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] }, }, ] @@ -206,6 +226,7 @@ export type City = { id: string city: string _geo: { lat: number; lng: number } + _geojson?: { type: 'Point'; coordinates: [number, number] } } export type Movies = { diff --git a/packages/instant-meilisearch/__tests__/geosearch.test.ts b/packages/instant-meilisearch/__tests__/geosearch.test.ts index c8e8216c..075c9a30 100644 --- a/packages/instant-meilisearch/__tests__/geosearch.test.ts +++ b/packages/instant-meilisearch/__tests__/geosearch.test.ts @@ -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') @@ -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([ + { + 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([ + { + 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([ + { + 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() + }) }) diff --git a/packages/instant-meilisearch/src/adapter/search-request-adapter/__tests__/geo-rules.test.ts b/packages/instant-meilisearch/src/adapter/search-request-adapter/__tests__/geo-rules.test.ts index 3e21d266..66c5cf6e 100644 --- a/packages/instant-meilisearch/src/adapter/search-request-adapter/__tests__/geo-rules.test.ts +++ b/packages/instant-meilisearch/src/adapter/search-request-adapter/__tests__/geo-rules.test.ts @@ -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() +}) diff --git a/packages/instant-meilisearch/src/adapter/search-request-adapter/geo-rules-adapter.ts b/packages/instant-meilisearch/src/adapter/search-request-adapter/geo-rules-adapter.ts index 16c1edf3..d12fa4f4 100644 --- a/packages/instant-meilisearch/src/adapter/search-request-adapter/geo-rules-adapter.ts +++ b/packages/instant-meilisearch/src/adapter/search-request-adapter/geo-rules-adapter.ts @@ -1,6 +1,7 @@ import type { InstantSearchGeoParams } from '../../types/index.js' export function adaptGeoSearch({ + insidePolygon, insideBoundingBox, aroundLatLng, aroundRadius, @@ -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 + 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(',') diff --git a/packages/instant-meilisearch/src/adapter/search-request-adapter/search-params-adapter.ts b/packages/instant-meilisearch/src/adapter/search-request-adapter/search-params-adapter.ts index ebd96748..b1cd1b4c 100644 --- a/packages/instant-meilisearch/src/adapter/search-request-adapter/search-params-adapter.ts +++ b/packages/instant-meilisearch/src/adapter/search-request-adapter/search-params-adapter.ts @@ -206,6 +206,7 @@ export function MeiliParamsCreator(searchContext: SearchContext) { }, addGeoSearchFilter() { const { + insidePolygon, insideBoundingBox, aroundLatLng, aroundRadius, @@ -213,6 +214,7 @@ export function MeiliParamsCreator(searchContext: SearchContext) { } = searchContext const filter = adaptGeoSearch({ + insidePolygon, insideBoundingBox, aroundLatLng, aroundRadius, From 1e84be51e2de4025873097f3b3ca1eecc6e6c7c3 Mon Sep 17 00:00:00 2001 From: Strift Date: Mon, 13 Oct 2025 16:28:45 +0800 Subject: [PATCH 02/12] Format --- .../__tests__/assets/utils.ts | 100 ++++++++++++++---- .../__tests__/geosearch.test.ts | 11 +- 2 files changed, 87 insertions(+), 24 deletions(-) diff --git a/packages/instant-meilisearch/__tests__/assets/utils.ts b/packages/instant-meilisearch/__tests__/assets/utils.ts index 184637b7..d05a7072 100644 --- a/packages/instant-meilisearch/__tests__/assets/utils.ts +++ b/packages/instant-meilisearch/__tests__/assets/utils.ts @@ -104,121 +104,181 @@ const geoDataset = [ id: '1', city: 'Lille', _geo: { lat: 50.629973371633746, lng: 3.056944739941957 }, - _geojson: { type: 'Point', coordinates: [3.056944739941957, 50.629973371633746] }, + _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] }, + _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] }, + _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] }, + _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] }, + _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] }, + _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] }, + _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] }, + _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] }, + _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] }, + _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] }, + _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] }, + _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] }, + _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] }, + _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] }, + _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] }, + _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] }, + _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] }, + _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] }, + _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] }, + _geojson: { + type: 'Point', + coordinates: [2.370840086740693, 48.90210006089548], + }, }, ] diff --git a/packages/instant-meilisearch/__tests__/geosearch.test.ts b/packages/instant-meilisearch/__tests__/geosearch.test.ts index 075c9a30..67cb0f77 100644 --- a/packages/instant-meilisearch/__tests__/geosearch.test.ts +++ b/packages/instant-meilisearch/__tests__/geosearch.test.ts @@ -119,7 +119,7 @@ describe('Instant Meilisearch Browser test', () => { insidePolygon: [ [50.95, 4.1], [50.75, 4.6], - [50.70, 4.2], + [50.7, 4.2], ], }, }, @@ -140,7 +140,7 @@ describe('Instant Meilisearch Browser test', () => { { id: 'geo-only', city: 'GeoOnly', - _geo: { lat: 50.80, lng: 4.35 }, + _geo: { lat: 50.8, lng: 4.35 }, }, ]) .waitTask() @@ -153,7 +153,7 @@ describe('Instant Meilisearch Browser test', () => { insidePolygon: [ [50.95, 4.1], [50.75, 4.6], - [50.70, 4.2], + [50.7, 4.2], ], }, }, @@ -164,7 +164,10 @@ describe('Instant Meilisearch Browser test', () => { expect(hits.find((h: any) => h.city === 'GeoOnly')).toBeFalsy() // Cleanup - await meilisearchClient.index('geotest').deleteDocument('geo-only').waitTask() + await meilisearchClient + .index('geotest') + .deleteDocument('geo-only') + .waitTask() }) test('aroundRadius matches _geojson-only documents', async () => { From 8672d579b6743c152770df780deee705a2f4fa5b Mon Sep 17 00:00:00 2001 From: Strift Date: Mon, 13 Oct 2025 17:37:00 +0800 Subject: [PATCH 03/12] Update README --- packages/instant-meilisearch/README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/instant-meilisearch/README.md b/packages/instant-meilisearch/README.md index 5e457d78..6e5b36da 100644 --- a/packages/instant-meilisearch/README.md +++ b/packages/instant-meilisearch/README.md @@ -791,6 +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. **Note**: This feature requires `_geojson` to be added to your Meilisearch index's `filterableAttributes` setting. For exemple, by adding `boundingBox` in the [`instantSearch`](#-instantsearch) widget parameters, the parameter will be used as a search parameter for the first request. @@ -817,6 +818,26 @@ Alternatively, the parameters can be passed through the [`searchFunction`](https }, ``` +You can also filter results within a polygon using `insidePolygon` (requires `_geojson` in your Meilisearch `filterableAttributes`). + +```js +// First, ensure your Meilisearch index has the correct settings: +// { +// "filterableAttributes": ["_geojson"] +// } + +// Then use insidePolygon in your search configuration: +search.addWidgets([ + instantsearch.widgets.configure({ + insidePolygon: [ + [50.8466, 4.35], + [50.75, 4.1], + [50.65, 4.5], + ], + }), +]) +``` + [Read the guide on how GeoSearch works in Meilisearch](https://www.meilisearch.com/docs/learn/getting_started/filtering_and_sorting#geosearch). ### ❌ Answers From ba24fb2e3ca9cac24a56801bb39e148fdb84c077 Mon Sep 17 00:00:00 2001 From: Strift Date: Mon, 13 Oct 2025 17:55:08 +0800 Subject: [PATCH 04/12] Update geosearch docs --- packages/instant-meilisearch/README.md | 13 +++-------- playgrounds/geo-javascript/index.html | 2 +- playgrounds/geo-javascript/src/app.js | 30 +++++++++++++------------- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/packages/instant-meilisearch/README.md b/packages/instant-meilisearch/README.md index 6e5b36da..acec89df 100644 --- a/packages/instant-meilisearch/README.md +++ b/packages/instant-meilisearch/README.md @@ -791,8 +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. **Note**: This feature requires `_geojson` to be added to your Meilisearch index's `filterableAttributes` setting. - +- `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. @@ -818,15 +817,9 @@ Alternatively, the parameters can be passed through the [`searchFunction`](https }, ``` -You can also filter results within a polygon using `insidePolygon` (requires `_geojson` in your Meilisearch `filterableAttributes`). +You can also filter results within a polygon using `insidePolygon`. ```js -// First, ensure your Meilisearch index has the correct settings: -// { -// "filterableAttributes": ["_geojson"] -// } - -// Then use insidePolygon in your search configuration: search.addWidgets([ instantsearch.widgets.configure({ insidePolygon: [ @@ -838,7 +831,7 @@ search.addWidgets([ ]) ``` -[Read the guide on how GeoSearch works in Meilisearch](https://www.meilisearch.com/docs/learn/getting_started/filtering_and_sorting#geosearch). +For more information, read the [geosearch documentation](https://www.meilisearch.com/docs/learn/filtering_and_sorting/geosearch). ### ❌ Answers diff --git a/playgrounds/geo-javascript/index.html b/playgrounds/geo-javascript/index.html index cb952b2d..d3ebf7fd 100644 --- a/playgrounds/geo-javascript/index.html +++ b/playgrounds/geo-javascript/index.html @@ -33,7 +33,7 @@

Search in world cities

-
+
diff --git a/playgrounds/geo-javascript/src/app.js b/playgrounds/geo-javascript/src/app.js index 07dd53e2..07f38a08 100644 --- a/playgrounds/geo-javascript/src/app.js +++ b/playgrounds/geo-javascript/src/app.js @@ -2,15 +2,15 @@ import { instantMeiliSearch } from '@meilisearch/instant-meilisearch' import injectScript from 'scriptjs' -const GOOGLE_API = process.env.GOOGLE_API +const GOOGLE_API = import.meta.env.GOOGLE_API injectScript( `https://maps.googleapis.com/maps/api/js?v=quarterly&key=${GOOGLE_API}`, () => { const search = instantsearch({ - indexName: 'world_cities', + indexName: 'world_cities_geojson', searchClient: instantMeiliSearch( - 'https://ms-adf78ae33284-106.lon.meilisearch.io', + 'https://edge.meilisearch.com', 'a63da4928426f12639e19d62886f621130f3fa9ff3c7534c5d179f0f51c4f303', {} ).searchClient, @@ -20,13 +20,13 @@ injectScript( instantsearch.widgets.sortBy({ container: '#sort-by', items: [ - { value: 'world_cities', label: 'Relevant' }, + { value: 'world_cities_geojson', label: 'Relevant' }, { - value: 'world_cities:population:desc', + value: 'world_cities_geojson:population:desc', label: 'Most Populated', }, { - value: 'world_cities:population:asc', + value: 'world_cities_geojson:population:asc', label: 'Least Populated', }, ], @@ -37,15 +37,15 @@ injectScript( instantsearch.widgets.configure({ hitsPerPage: 20, }), - instantsearch.widgets.geoSearch({ - container: '#maps', - googleReference: window.google, - initialZoom: 7, - initialPosition: { - lat: 50.655250871381355, - lng: 4.843585698860502, - }, - }), + // instantsearch.widgets.geoSearch({ + // container: '#maps', + // googleReference: window.google, + // initialZoom: 7, + // initialPosition: { + // lat: 50.655250871381355, + // lng: 4.843585698860502, + // }, + // }), instantsearch.widgets.infiniteHits({ container: '#hits', templates: { From 169e4b13b8d34e8b8f5e7927187c95f6202d55ca Mon Sep 17 00:00:00 2001 From: Strift Date: Tue, 21 Oct 2025 14:50:38 +0400 Subject: [PATCH 05/12] Fix google maps display --- playgrounds/geo-javascript/.env | 2 +- playgrounds/geo-javascript/index.html | 5 +- playgrounds/geo-javascript/package.json | 4 +- playgrounds/geo-javascript/src/app.js | 130 ++++++++++++------------ yarn.lock | 14 +-- 5 files changed, 80 insertions(+), 75 deletions(-) diff --git a/playgrounds/geo-javascript/.env b/playgrounds/geo-javascript/.env index 9e8ef78a..31c4fa20 100644 --- a/playgrounds/geo-javascript/.env +++ b/playgrounds/geo-javascript/.env @@ -1 +1 @@ -GOOGLE_API=AIzaSyDOaUaar4GL0i99LpN2zQHzfWXL1wu_JQo +VITE_GOOGLE_MAPS_KEY=AIzaSyAdTxSaQG1L1kMborkwbUOM-gnHLYr7vGk diff --git a/playgrounds/geo-javascript/index.html b/playgrounds/geo-javascript/index.html index d3ebf7fd..08d29b19 100644 --- a/playgrounds/geo-javascript/index.html +++ b/playgrounds/geo-javascript/index.html @@ -32,8 +32,8 @@

Search in world cities

- - + +

@@ -41,6 +41,7 @@

Search in world cities

+ diff --git a/playgrounds/geo-javascript/package.json b/playgrounds/geo-javascript/package.json index 8e7e8890..370dd0d4 100644 --- a/playgrounds/geo-javascript/package.json +++ b/playgrounds/geo-javascript/package.json @@ -12,7 +12,7 @@ ], "license": "MIT", "dependencies": { - "@meilisearch/instant-meilisearch": "0.28.0", - "scriptjs": "^2.5.9" + "@googlemaps/js-api-loader": "^2.0.1", + "@meilisearch/instant-meilisearch": "*" } } diff --git a/playgrounds/geo-javascript/src/app.js b/playgrounds/geo-javascript/src/app.js index 07f38a08..336c4da0 100644 --- a/playgrounds/geo-javascript/src/app.js +++ b/playgrounds/geo-javascript/src/app.js @@ -1,71 +1,73 @@ /* eslint-disable no-undef */ import { instantMeiliSearch } from '@meilisearch/instant-meilisearch' -import injectScript from 'scriptjs' +import { setOptions, importLibrary } from '@googlemaps/js-api-loader' -const GOOGLE_API = import.meta.env.GOOGLE_API +const GOOGLE_API_KEY = import.meta.env.VITE_GOOGLE_MAPS_KEY -injectScript( - `https://maps.googleapis.com/maps/api/js?v=quarterly&key=${GOOGLE_API}`, - () => { - const search = instantsearch({ - indexName: 'world_cities_geojson', - searchClient: instantMeiliSearch( - 'https://edge.meilisearch.com', - 'a63da4928426f12639e19d62886f621130f3fa9ff3c7534c5d179f0f51c4f303', - {} - ).searchClient, - }) +setOptions({ + apiKey: GOOGLE_API_KEY, + version: 'weekly', +}) - search.addWidgets([ - instantsearch.widgets.sortBy({ - container: '#sort-by', - items: [ - { value: 'world_cities_geojson', label: 'Relevant' }, - { - value: 'world_cities_geojson:population:desc', - label: 'Most Populated', - }, - { - value: 'world_cities_geojson:population:asc', - label: 'Least Populated', - }, - ], - }), - instantsearch.widgets.searchBox({ - container: '#searchbox', - }), - instantsearch.widgets.configure({ - hitsPerPage: 20, - }), - // instantsearch.widgets.geoSearch({ - // container: '#maps', - // googleReference: window.google, - // initialZoom: 7, - // initialPosition: { - // lat: 50.655250871381355, - // lng: 4.843585698860502, - // }, - // }), - instantsearch.widgets.infiniteHits({ - container: '#hits', - templates: { - item: ` -
-
- City: {{#helpers.highlight}}{ "attribute": "name" }{{/helpers.highlight}} -
-
- Country: {{#helpers.highlight}}{ "attribute": "country" }{{/helpers.highlight}} -
-
- Population: {{#helpers.highlight}}{ "attribute": "population" }{{/helpers.highlight}} -
-
- `, +importLibrary('maps').then(() => { + const search = instantsearch({ + indexName: 'world_cities_geojson', + searchClient: instantMeiliSearch( + 'https://edge.meilisearch.com', + 'a63da4928426f12639e19d62886f621130f3fa9ff3c7534c5d179f0f51c4f303', + {} + ).searchClient, + }) + + search.addWidgets([ + instantsearch.widgets.sortBy({ + container: '#sort-by', + items: [ + { value: 'world_cities_geojson', label: 'Relevant' }, + { + value: 'world_cities_geojson:population:desc', + label: 'Most Populated', + }, + { + value: 'world_cities_geojson:population:asc', + label: 'Least Populated', }, - }), - ]) + ], + }), + instantsearch.widgets.searchBox({ + container: '#searchbox', + }), + instantsearch.widgets.configure({ + hitsPerPage: 20, + }), + instantsearch.widgets.geoSearch({ + container: '#maps', + googleReference: window.google, + initialZoom: 7, + initialPosition: { + lat: 50.655250871381355, + lng: 4.843585698860502, + }, + }), + instantsearch.widgets.infiniteHits({ + container: '#hits', + templates: { + item: ` +
+
+ City: {{#helpers.highlight}}{ "attribute": "name" }{{/helpers.highlight}} +
+
+ Country: {{#helpers.highlight}}{ "attribute": "country" }{{/helpers.highlight}} +
+
+ Population: {{#helpers.highlight}}{ "attribute": "population" }{{/helpers.highlight}} +
+
+ `, + }, + }), + ]) - search.start() - } -) + search.start() +}) diff --git a/yarn.lock b/yarn.lock index bcc1e92e..28749361 100644 --- a/yarn.lock +++ b/yarn.lock @@ -909,6 +909,13 @@ "@eslint/core" "^0.10.0" levn "^0.4.1" +"@googlemaps/js-api-loader@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@googlemaps/js-api-loader/-/js-api-loader-2.0.1.tgz#98ee96786a00de2d100f8a103738b3644a1cbb25" + integrity sha512-YUtKJSWH6FiE/6Ii+NP+WfkvoZ55DLwyqthKmVYxZZwn/ny310dvz9Zw3cO1besEhGxA9lW7Ctjmzae1putjXQ== + dependencies: + "@types/google.maps" "^3.53.1" + "@humanfs/core@^0.19.1": version "0.19.1" resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" @@ -1202,7 +1209,7 @@ resolved "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.45.6.tgz" integrity sha512-BzGzxs8UXFxeP8uN/0nRgGbsbpYQxSCKsv/7S8OitU7wwhfFcqQSm5aAcL1nbwueMiJ/VVmIZKPq69s0kX5W+Q== -"@types/google.maps@^3.55.12": +"@types/google.maps@^3.53.1", "@types/google.maps@^3.55.12": version "3.58.1" resolved "https://registry.yarnpkg.com/@types/google.maps/-/google.maps-3.58.1.tgz#71ce3dec44de1452f56641d2c87c7dd8ea964b4d" integrity sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ== @@ -6162,11 +6169,6 @@ scheduler@^0.23.2: dependencies: loose-envify "^1.1.0" -scriptjs@^2.5.9: - version "2.5.9" - resolved "https://registry.npmjs.org/scriptjs/-/scriptjs-2.5.9.tgz" - integrity sha512-qGVDoreyYiP1pkQnbnFAUIS5AjenNwwQBdl7zeos9etl+hYKWahjRTfzAZZYBv5xNHx7vNKCmaLDQZ6Fr2AEXg== - search-insights@^2.17.2, search-insights@^2.17.3: version "2.17.3" resolved "https://registry.yarnpkg.com/search-insights/-/search-insights-2.17.3.tgz#8faea5d20507bf348caba0724e5386862847b661" From 922b3b1769f334a7c56b2c32d931faf7efa0d0be Mon Sep 17 00:00:00 2001 From: Strift Date: Tue, 21 Oct 2025 15:11:51 +0400 Subject: [PATCH 06/12] Disable non-working feature --- playgrounds/geo-javascript/.env | 2 +- playgrounds/geo-javascript/src/app.js | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/playgrounds/geo-javascript/.env b/playgrounds/geo-javascript/.env index 31c4fa20..c4ecd289 100644 --- a/playgrounds/geo-javascript/.env +++ b/playgrounds/geo-javascript/.env @@ -1 +1 @@ -VITE_GOOGLE_MAPS_KEY=AIzaSyAdTxSaQG1L1kMborkwbUOM-gnHLYr7vGk +VITE_GOOGLE_MAPS_API_KEY=AIzaSyAdTxSaQG1L1kMborkwbUOM-gnHLYr7vGk diff --git a/playgrounds/geo-javascript/src/app.js b/playgrounds/geo-javascript/src/app.js index 336c4da0..4f75ba3d 100644 --- a/playgrounds/geo-javascript/src/app.js +++ b/playgrounds/geo-javascript/src/app.js @@ -2,10 +2,10 @@ import { instantMeiliSearch } from '@meilisearch/instant-meilisearch' import { setOptions, importLibrary } from '@googlemaps/js-api-loader' -const GOOGLE_API_KEY = import.meta.env.VITE_GOOGLE_MAPS_KEY +const GOOGLE_MAP_API_KEY = import.meta.env.VITE_GOOGLE_MAPS_KEY setOptions({ - apiKey: GOOGLE_API_KEY, + apiKey: GOOGLE_MAP_API_KEY, version: 'weekly', }) @@ -48,6 +48,9 @@ importLibrary('maps').then(() => { lat: 50.655250871381355, lng: 4.843585698860502, }, + enableRefineOnMapMove: false, + enableClearMapRefinement: false, + enableRefineControl: false, }), instantsearch.widgets.infiniteHits({ container: '#hits', From 2a7dd4e86a04c07c48c03833e2793c6a84f8475f Mon Sep 17 00:00:00 2001 From: Strift Date: Mon, 27 Oct 2025 16:33:07 +0400 Subject: [PATCH 07/12] Update env var name --- playgrounds/geo-javascript/src/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playgrounds/geo-javascript/src/app.js b/playgrounds/geo-javascript/src/app.js index 4f75ba3d..80329503 100644 --- a/playgrounds/geo-javascript/src/app.js +++ b/playgrounds/geo-javascript/src/app.js @@ -2,7 +2,7 @@ import { instantMeiliSearch } from '@meilisearch/instant-meilisearch' import { setOptions, importLibrary } from '@googlemaps/js-api-loader' -const GOOGLE_MAP_API_KEY = import.meta.env.VITE_GOOGLE_MAPS_KEY +const GOOGLE_MAP_API_KEY = import.meta.env.VITE_GOOGLE_MAPS_API_KEY setOptions({ apiKey: GOOGLE_MAP_API_KEY, From a85f7a54f9efc1eb21e2f778caa0c672cd676c5a Mon Sep 17 00:00:00 2001 From: Strift Date: Mon, 27 Oct 2025 16:35:36 +0400 Subject: [PATCH 08/12] Remove extra closing tag --- playgrounds/geo-javascript/index.html | 1 - 1 file changed, 1 deletion(-) diff --git a/playgrounds/geo-javascript/index.html b/playgrounds/geo-javascript/index.html index 08d29b19..4a9adbcd 100644 --- a/playgrounds/geo-javascript/index.html +++ b/playgrounds/geo-javascript/index.html @@ -41,7 +41,6 @@

Search in world cities

- From 22597f84dcadc992c0992136fcd448ccfad7de4c Mon Sep 17 00:00:00 2001 From: Strift Date: Tue, 28 Oct 2025 11:24:04 +0400 Subject: [PATCH 09/12] Remove .env --- playgrounds/geo-javascript/.env | 1 - playgrounds/geo-javascript/.env.example | 1 + playgrounds/geo-javascript/.gitignore | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 playgrounds/geo-javascript/.env create mode 100644 playgrounds/geo-javascript/.env.example create mode 100644 playgrounds/geo-javascript/.gitignore diff --git a/playgrounds/geo-javascript/.env b/playgrounds/geo-javascript/.env deleted file mode 100644 index c4ecd289..00000000 --- a/playgrounds/geo-javascript/.env +++ /dev/null @@ -1 +0,0 @@ -VITE_GOOGLE_MAPS_API_KEY=AIzaSyAdTxSaQG1L1kMborkwbUOM-gnHLYr7vGk diff --git a/playgrounds/geo-javascript/.env.example b/playgrounds/geo-javascript/.env.example new file mode 100644 index 00000000..aa0d283e --- /dev/null +++ b/playgrounds/geo-javascript/.env.example @@ -0,0 +1 @@ +VITE_GOOGLE_MAPS_API_KEY="insert your Google Maps API key here" diff --git a/playgrounds/geo-javascript/.gitignore b/playgrounds/geo-javascript/.gitignore new file mode 100644 index 00000000..4c49bd78 --- /dev/null +++ b/playgrounds/geo-javascript/.gitignore @@ -0,0 +1 @@ +.env From 16a4c9d5a0394df5093c492de74697e5149b8261 Mon Sep 17 00:00:00 2001 From: Strift Date: Tue, 28 Oct 2025 11:29:25 +0400 Subject: [PATCH 10/12] Add changeset --- .changeset/mean-weeks-admire.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/mean-weeks-admire.md diff --git a/.changeset/mean-weeks-admire.md b/.changeset/mean-weeks-admire.md new file mode 100644 index 00000000..6e9824ee --- /dev/null +++ b/.changeset/mean-weeks-admire.md @@ -0,0 +1,5 @@ +--- +"@meilisearch/instant-meilisearch": minor +--- + +Add support for insidePolygon filter to geosearch From ab4af7371a5d6147db8ab658e590006300704e36 Mon Sep 17 00:00:00 2001 From: Strift Date: Tue, 28 Oct 2025 11:46:19 +0400 Subject: [PATCH 11/12] Update instant-meilisearch readme --- packages/instant-meilisearch/README.md | 33 +++++++++++++++----------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/instant-meilisearch/README.md b/packages/instant-meilisearch/README.md index acec89df..f7feb7d7 100644 --- a/packages/instant-meilisearch/README.md +++ b/packages/instant-meilisearch/README.md @@ -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) From a305a0d0f50b26c70e974179f64a2061455b147e Mon Sep 17 00:00:00 2001 From: Strift Date: Thu, 30 Oct 2025 15:16:36 +0400 Subject: [PATCH 12/12] Add warnings for invalid pairs --- .../geo-rules-adapter.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/instant-meilisearch/src/adapter/search-request-adapter/geo-rules-adapter.ts b/packages/instant-meilisearch/src/adapter/search-request-adapter/geo-rules-adapter.ts index d12fa4f4..76fef1e4 100644 --- a/packages/instant-meilisearch/src/adapter/search-request-adapter/geo-rules-adapter.ts +++ b/packages/instant-meilisearch/src/adapter/search-request-adapter/geo-rules-adapter.ts @@ -13,16 +13,31 @@ export function adaptGeoSearch({ // Highest precedence: insidePolygon if (Array.isArray(insidePolygon) && insidePolygon.length >= 3) { + const invalidPairs: unknown[] = [] + const formattedPoints = insidePolygon .map((pair) => { - if (!Array.isArray(pair) || pair.length < 2) return null + if (!Array.isArray(pair) || pair.length < 2) { + invalidPairs.push(pair) + return null + } const lat = Number.parseFloat(String(pair[0])) const lng = Number.parseFloat(String(pair[1])) - if (Number.isNaN(lat) || Number.isNaN(lng)) return null + if (Number.isNaN(lat) || Number.isNaN(lng)) { + invalidPairs.push(pair) + return null + } return `[${lat}, ${lng}]` }) .filter((pt): pt is string => pt !== null) + if (invalidPairs.length > 0) { + console.warn( + 'instant-meilisearch: insidePolygon contains invalid coordinate pairs that were ignored:', + invalidPairs + ) + } + if (formattedPoints.length >= 3) { filter = `_geoPolygon(${formattedPoints.join(', ')})` return filter