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 diff --git a/packages/instant-meilisearch/README.md b/packages/instant-meilisearch/README.md index 5e457d78..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) @@ -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. @@ -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 diff --git a/packages/instant-meilisearch/__tests__/assets/utils.ts b/packages/instant-meilisearch/__tests__/assets/utils.ts index 84fe3398..d05a7072 100644 --- a/packages/instant-meilisearch/__tests__/assets/utils.ts +++ b/packages/instant-meilisearch/__tests__/assets/utils.ts @@ -104,101 +104,181 @@ 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 +286,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..67cb0f77 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,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([ + { + 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([ + { + 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([ + { + 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..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 @@ -1,6 +1,7 @@ import type { InstantSearchGeoParams } from '../../types/index.js' export function adaptGeoSearch({ + insidePolygon, insideBoundingBox, aroundLatLng, aroundRadius, @@ -10,6 +11,39 @@ export function adaptGeoSearch({ let radius: number | undefined let filter: string | undefined + // 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) { + 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)) { + 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 + } + } + 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, diff --git a/playgrounds/geo-javascript/.env b/playgrounds/geo-javascript/.env deleted file mode 100644 index 9e8ef78a..00000000 --- a/playgrounds/geo-javascript/.env +++ /dev/null @@ -1 +0,0 @@ -GOOGLE_API=AIzaSyDOaUaar4GL0i99LpN2zQHzfWXL1wu_JQo 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 diff --git a/playgrounds/geo-javascript/index.html b/playgrounds/geo-javascript/index.html index cb952b2d..4a9adbcd 100644 --- a/playgrounds/geo-javascript/index.html +++ b/playgrounds/geo-javascript/index.html @@ -32,7 +32,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 07dd53e2..80329503 100644 --- a/playgrounds/geo-javascript/src/app.js +++ b/playgrounds/geo-javascript/src/app.js @@ -1,71 +1,76 @@ /* 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 = process.env.GOOGLE_API +const GOOGLE_MAP_API_KEY = import.meta.env.VITE_GOOGLE_MAPS_API_KEY -injectScript( - `https://maps.googleapis.com/maps/api/js?v=quarterly&key=${GOOGLE_API}`, - () => { - const search = instantsearch({ - indexName: 'world_cities', - searchClient: instantMeiliSearch( - 'https://ms-adf78ae33284-106.lon.meilisearch.io', - 'a63da4928426f12639e19d62886f621130f3fa9ff3c7534c5d179f0f51c4f303', - {} - ).searchClient, - }) +setOptions({ + apiKey: GOOGLE_MAP_API_KEY, + version: 'weekly', +}) - search.addWidgets([ - instantsearch.widgets.sortBy({ - container: '#sort-by', - items: [ - { value: 'world_cities', label: 'Relevant' }, - { - value: 'world_cities:population:desc', - label: 'Most Populated', - }, - { - value: 'world_cities: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, +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', }, - }), - 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}} -
-
- `, + { + 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, + }, + enableRefineOnMapMove: false, + enableClearMapRefinement: false, + enableRefineControl: false, + }), + 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"