From 30ffd772a97192baa79d8121fba97d9ceb820c5f Mon Sep 17 00:00:00 2001 From: meili-bot <74670311+meili-bot@users.noreply.github.com> Date: Mon, 11 Apr 2022 16:00:28 +0200 Subject: [PATCH 1/6] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 23cd71e9..edc3e543 100644 --- a/README.md +++ b/README.md @@ -253,7 +253,7 @@ This package only guarantees the compatibility with the [version v4 of InstantSe **Supported Meilisearch versions**: -This package only guarantees the compatibility with the [version v0.26.0 of Meilisearch](https://github.com/meilisearch/meilisearch/releases/tag/v0.26.0). +This package only guarantees the compatibility with the [version v0.27.0 of Meilisearch](https://github.com/meilisearch/meilisearch/releases/tag/v0.27.0). **Node / NPM versions**: From 4ebc6e534f3ad801ac8d9e969216abe0117e1023 Mon Sep 17 00:00:00 2001 From: meili-bot <74670311+meili-bot@users.noreply.github.com> Date: Tue, 12 Apr 2022 18:09:52 +0200 Subject: [PATCH 2/6] Replace docker command ./meilisearch with meilisearch (#734) * Update .github/workflows/tests.yml * Update .github/workflows/pre-release-tests.yml * Update meilisearch cmd in contributing guide Co-authored-by: Charlotte Vermandel --- .github/workflows/pre-release-tests.yml | 2 +- .github/workflows/tests.yml | 2 +- CONTRIBUTING.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pre-release-tests.yml b/.github/workflows/pre-release-tests.yml index 72b8f188..a9a4ad83 100644 --- a/.github/workflows/pre-release-tests.yml +++ b/.github/workflows/pre-release-tests.yml @@ -75,7 +75,7 @@ jobs: - name: Get the latest Meilisearch RC run: echo "MEILISEARCH_VERSION=$(curl https://raw.githubusercontent.com/meilisearch/integration-guides/main/scripts/get-latest-meilisearch-rc.sh | bash)" >> $GITHUB_ENV - name: Meilisearch (${{ env.MEILISEARCH_VERSION }}) setup with Docker - run: docker run -d -p 7700:7700 getmeili/meilisearch:${{ env.MEILISEARCH_VERSION }} ./meilisearch --master-key=masterKey --no-analytics + run: docker run -d -p 7700:7700 getmeili/meilisearch:${{ env.MEILISEARCH_VERSION }} meilisearch --master-key=masterKey --no-analytics - name: Install dependencies run: yarn install - name: Run tests diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e73ba30f..78dbceb6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -76,7 +76,7 @@ jobs: ./node_modules key: ${{ hashFiles('yarn.lock') }} - name: Docker setup - run: docker run -d -p 7700:7700 getmeili/meilisearch:latest ./meilisearch --no-analytics --master-key='masterKey' + run: docker run -d -p 7700:7700 getmeili/meilisearch:latest meilisearch --no-analytics --master-key='masterKey' - name: Install dependencies run: yarn install - name: Run tests diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 693e2ea6..0ed8b067 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,7 +39,7 @@ Each PR should pass the tests and the linter to be accepted. ```bash # Tests with Jest docker pull getmeili/meilisearch:latest # Fetch the latest version of Meilisearch image from Docker Hub -docker run -p 7700:7700 getmeili/meilisearch:latest ./meilisearch --master-key=masterKey --no-analytics +docker run -p 7700:7700 getmeili/meilisearch:latest meilisearch --master-key=masterKey --no-analytics # Integration tests yarn test # End-to-end tests From 7c49bda56724616332be02bfe267b8f277387ab2 Mon Sep 17 00:00:00 2001 From: cvermand <33010418+bidoubiwa@users.noreply.github.com> Date: Thu, 21 Apr 2022 14:13:39 +0200 Subject: [PATCH 3/6] Fix tests related to changes in placeholder hits order resolver (#737) * Fix tests related to changes in Meilisearch * Update tests based on placeholder search resolution --- tests/filter.tests.ts | 5 +++-- tests/highlight.tests.ts | 4 ++-- tests/pagination.tests.ts | 3 --- tests/snippets.tests.ts | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/filter.tests.ts b/tests/filter.tests.ts index 7c39ce8b..78138eea 100644 --- a/tests/filter.tests.ts +++ b/tests/filter.tests.ts @@ -104,6 +104,7 @@ describe('Instant Meilisearch Browser test', () => { }, ]) const hits = response.results[0].hits + expect(hits.length).toEqual(2) expect(hits[0].title).toEqual('Ariel') }) @@ -121,7 +122,7 @@ describe('Instant Meilisearch Browser test', () => { expect(hits[0].title).toEqual('Judgment Night') }) - test('Test multiple nested on filter without a query', async () => { + test('Test multiple nested array in filter without a query', async () => { const params = { indexName: 'movies', params: { @@ -131,7 +132,7 @@ describe('Instant Meilisearch Browser test', () => { } const response = await searchClient.search([params]) const hits = response.results[0].hits - expect(hits[0].title).toEqual('Kill Bill: Vol. 1') + expect(hits[0].title).toEqual('Judgment Night') }) test('Test multiple nested arrays on filter with a query', async () => { diff --git a/tests/highlight.tests.ts b/tests/highlight.tests.ts index de2886a1..f7c02fd7 100644 --- a/tests/highlight.tests.ts +++ b/tests/highlight.tests.ts @@ -86,8 +86,8 @@ describe('Highlight Browser test', () => { const highlightedHit = response.results[0].hits[0]._highlightResult if (highlightedHit?.genres) { - expect(highlightedHit?.genres[0]?.value).toEqual('Adventure') - expect(highlightedHit?.genres[1]?.value).toEqual('Action') + expect(highlightedHit?.genres[0]?.value).toEqual('Drama') + expect(highlightedHit?.genres[1]?.value).toEqual('Crime') } }) diff --git a/tests/pagination.tests.ts b/tests/pagination.tests.ts index 22a0adeb..aaa1a2e7 100644 --- a/tests/pagination.tests.ts +++ b/tests/pagination.tests.ts @@ -31,7 +31,6 @@ describe('Pagination browser test', () => { ]) const hits = response.results[0].hits expect(hits.length).toBe(1) - expect(hits[0]?.title).toBe('Star Wars') }) test('Test 1 hitsPerPage w/ page 0 ', async () => { @@ -48,7 +47,6 @@ describe('Pagination browser test', () => { const hits = response.results[0].hits expect(hits.length).toBe(1) - expect(hits[0].title).toBe('Star Wars') }) test('Test 1 hitsPerPage w/ page 1 ', async () => { @@ -64,7 +62,6 @@ describe('Pagination browser test', () => { ]) const hits = response.results[0].hits expect(hits.length).toBe(1) - expect(hits[0].title).toBe('Ariel') }) test('Test 100 hitsPerPage w/ page 1 ', async () => { diff --git a/tests/snippets.tests.ts b/tests/snippets.tests.ts index 89126f8f..e804aad8 100644 --- a/tests/snippets.tests.ts +++ b/tests/snippets.tests.ts @@ -324,6 +324,6 @@ test('Test attributes to snippet on value smaller than the snippet size', async const hit = response.results[0].hits[0]._snippetResult if (hit?.overview) { - expect(hit?.title?.value).toEqual('Star Wars') + expect(hit?.title?.value).toEqual('Ariel') } }) From d506846bf7e6793be2a23b46763f22bed3651ca4 Mon Sep 17 00:00:00 2001 From: cvermand <33010418+bidoubiwa@users.noreply.github.com> Date: Thu, 21 Apr 2022 16:03:11 +0200 Subject: [PATCH 4/6] Add crop marker support (#738) * Fix tests related to changes in Meilisearch * Update tests based on placeholder search resolution * Add crop marker support --- package.json | 2 +- .../search-params-adapter.ts | 6 + .../format-adapter/format-adapter.ts | 17 +- .../format-adapter/snippet-adapter.ts | 95 -------- src/types/types.ts | 1 + tests/assets/utils.ts | 24 +- tests/highlight.tests.ts | 6 +- tests/snippets.tests.ts | 207 ++++++++++++------ yarn.lock | 8 +- 9 files changed, 167 insertions(+), 199 deletions(-) delete mode 100644 src/adapter/search-response-adapter/format-adapter/snippet-adapter.ts diff --git a/package.json b/package.json index 677b7778..9b59774c 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "url": "https://github.com/meilisearch/instant-meilisearch.git" }, "dependencies": { - "meilisearch": "0.25.0" + "meilisearch": "^0.26.0-beta.0" }, "devDependencies": { "@babel/cli": "^7.17.6", diff --git a/src/adapter/search-request-adapter/search-params-adapter.ts b/src/adapter/search-request-adapter/search-params-adapter.ts index 606c43fe..30ee9aa8 100644 --- a/src/adapter/search-request-adapter/search-params-adapter.ts +++ b/src/adapter/search-request-adapter/search-params-adapter.ts @@ -31,6 +31,12 @@ export function adaptSearchParams( meiliSearchParams.attributesToCrop = attributesToCrop } + // Attributes To Crop marker + const cropMarker = searchContext?.snippetEllipsisText + if (cropMarker != null) { + meiliSearchParams.cropMarker = cropMarker + } + // Attributes To Retrieve const attributesToRetrieve = searchContext?.attributesToRetrieve if (attributesToRetrieve) { diff --git a/src/adapter/search-response-adapter/format-adapter/format-adapter.ts b/src/adapter/search-response-adapter/format-adapter/format-adapter.ts index 7f3e7c61..1d9e225f 100644 --- a/src/adapter/search-response-adapter/format-adapter/format-adapter.ts +++ b/src/adapter/search-response-adapter/format-adapter/format-adapter.ts @@ -1,5 +1,4 @@ import { adaptHighlight } from './highlight-adapter' -import { adaptSnippet } from './snippet-adapter' import { SearchContext } from '../../../types' /** @@ -13,24 +12,16 @@ export function adaptFormating( hit: Record, searchContext: SearchContext ): Record { - const attributesToSnippet = searchContext?.attributesToSnippet - const ellipsis = searchContext?.snippetEllipsisText const preTag = searchContext?.highlightPreTag const postTag = searchContext?.highlightPostTag if (!hit._formatted) return {} - const _highlightResult = adaptHighlight(hit, preTag, postTag) - - // what is ellipsis by default - const _snippetResult = adaptHighlight( - adaptSnippet(hit, attributesToSnippet, ellipsis), - preTag, - postTag - ) + const _formattedResult = adaptHighlight(hit, preTag, postTag) const highlightedHit = { - _highlightResult, - _snippetResult, + // We could not determine what the differences are between those two fields. + _highlightResult: _formattedResult, + _snippetResult: _formattedResult, } return highlightedHit diff --git a/src/adapter/search-response-adapter/format-adapter/snippet-adapter.ts b/src/adapter/search-response-adapter/format-adapter/snippet-adapter.ts deleted file mode 100644 index dcc4e99a..00000000 --- a/src/adapter/search-response-adapter/format-adapter/snippet-adapter.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { isString } from '../../../utils' - -function nakedOfTags(str: string) { - return str.replace(//g, '').replace(/<\/em>/g, '') -} - -function addEllipsis(value: any, formatValue: string, ellipsis: string): any { - // Manage ellpsis on cropped values until this feature is implemented https://roadmap.meilisearch.com/c/69-policy-for-cropped-values?utm_medium=social&utm_source=portal_share in Meilisearch - - let ellipsedValue = formatValue - - if ( - isString(formatValue) && - value.toString().length > nakedOfTags(formatValue).length - ) { - if ( - formatValue[0] === formatValue[0].toLowerCase() && // beginning of a sentence - formatValue.startsWith('') === false // beginning of the document field, otherwise Meilisearch would crop around the highlight - ) { - ellipsedValue = `${ellipsis}${formatValue.trim()}` - } - if (!!formatValue.match(/[.!?]$/) === false) { - // end of the sentence - ellipsedValue = `${formatValue.trim()}${ellipsis}` - } - } - return ellipsedValue -} - -/** - * @param {string} value - * @param {string} ellipsis? - * @returns {string} - */ -function resolveSnippet(value: any, formatValue: any, ellipsis?: string): any { - if (!ellipsis || !(typeof formatValue === 'string')) { - return formatValue - } else if (Array.isArray(value)) { - // Array - return value.map((elem) => addEllipsis(elem, formatValue, ellipsis)) - } - return addEllipsis(value, formatValue, ellipsis) -} - -/** - * @param {Record, - attributes: readonly string[] | undefined, - ellipsis: string | undefined -): Record { - // hit is the `_formatted` object returned by Meilisearch. - // It contains all the highlighted and croped attributes - - const formattedHit = hit._formatted - const newHit = hit._formatted - - if (attributes === undefined) { - return hit - } - - // All attributes that should be snippeted and their snippet size - const snippets = attributes.map( - (attribute) => attribute.split(':')[0] - ) as any[] - - // Find presence of a wildcard * - const wildCard = snippets.includes('*') - - if (wildCard) { - // In case of * - for (const attribute in formattedHit) { - newHit[attribute] = resolveSnippet( - hit[attribute], - formattedHit[attribute], - ellipsis - ) - } - } else { - // Itterate on all attributes that needs snippeting - for (const attribute of snippets) { - newHit[attribute] = resolveSnippet( - hit[attribute], - formattedHit[attribute], - ellipsis - ) - } - } - hit._formatted = newHit - - return hit -} diff --git a/src/types/types.ts b/src/types/types.ts index a54d7a78..4a2692b2 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -81,6 +81,7 @@ export type SearchContext = Omit< > & { insideBoundingBox?: InsideBoundingBox keepZeroFacets?: boolean + cropMarker?: string defaultFacetDistribution: FacetsDistribution pagination: PaginationContext } diff --git a/tests/assets/utils.ts b/tests/assets/utils.ts index 5a476277..4e53a334 100644 --- a/tests/assets/utils.ts +++ b/tests/assets/utils.ts @@ -13,13 +13,13 @@ const dataset = [ undefinedArray: [undefined, undefined, undefined], nullArray: [null, null, null], objectArray: [ - { name: 'charlotte' }, - { name: 'charlotte' }, - { name: 'charlotte' }, + { name: 'hello world' }, + { name: 'hello world' }, + { name: 'hello world' }, ], object: { id: 1, - name: 'Nader', + name: 'One two', }, nullField: null, }, @@ -34,13 +34,13 @@ const dataset = [ undefinedArray: [undefined, undefined, undefined], nullArray: [null, null, null], objectArray: [ - { name: 'charlotte' }, - { name: 'charlotte' }, - { name: 'charlotte' }, + { name: 'hello world' }, + { name: 'hello world' }, + { name: 'hello world' }, ], object: { id: 1, - name: 'Nader', + name: 'One two', }, nullField: null, }, @@ -55,13 +55,13 @@ const dataset = [ undefinedArray: [undefined, undefined, undefined], nullArray: [null, null, null], objectArray: [ - { name: 'charlotte' }, - { name: 'charlotte' }, - { name: 'charlotte' }, + { name: 'hello world' }, + { name: 'hello world' }, + { name: 'hello world' }, ], object: { id: 1, - name: 'Nader', + name: 'One two', }, nullField: null, }, diff --git a/tests/highlight.tests.ts b/tests/highlight.tests.ts index f7c02fd7..1bdc7c4a 100644 --- a/tests/highlight.tests.ts +++ b/tests/highlight.tests.ts @@ -261,14 +261,14 @@ describe('Highlight Browser test', () => { if (hit?.objectArray) { // @ts-ignore - expect(hit?.objectArray[0]?.value).toEqual('{"name":"charlotte"}') + expect(hit?.objectArray[0]?.value).toEqual('{"name":"hello world"}') // @ts-ignore - expect(hit?.objectArray[1]?.value).toEqual('{"name":"charlotte"}') + expect(hit?.objectArray[1]?.value).toEqual('{"name":"hello world"}') } if (hit?.object) { // @ts-ignore - expect(hit?.object?.value).toEqual('{"id":"1","name":"Nader"}') + expect(hit?.object?.value).toEqual('{"id":"1","name":"One two"}') } if (hit?.nullField) { diff --git a/tests/snippets.tests.ts b/tests/snippets.tests.ts index e804aad8..9d9f7f32 100644 --- a/tests/snippets.tests.ts +++ b/tests/snippets.tests.ts @@ -18,6 +18,42 @@ describe('Snippet Browser test', () => { await meilisearchClient.index('movies').waitForTask(documentsTask.uid) }) + test('Test one attributesToSnippet on placeholder without a snippetEllipsisText', async () => { + const response = await searchClient.search([ + { + indexName: 'movies', + params: { + query: '', + attributesToSnippet: ['overview:2'], + }, + }, + ]) + const snippeted = response.results[0]?.hits[0]?._snippetResult + + // Default cropMarker `...` value is used + expect(snippeted).toHaveProperty('overview', { + value: 'Taisto Kasurinen…', + }) + }) + + test('Test one attributesToSnippet on placeholder with empty string snippetEllipsisText', async () => { + const response = await searchClient.search([ + { + indexName: 'movies', + params: { + query: '', + attributesToSnippet: ['overview:2'], + snippetEllipsisText: '', + }, + }, + ]) + const snippeted = response.results[0]?.hits[0]?._snippetResult + + expect(snippeted).toHaveProperty('overview', { + value: 'Taisto Kasurinen', + }) + }) + test('Test one attributesToSnippet on placeholder', async () => { const response = await searchClient.search([ { @@ -25,12 +61,14 @@ describe('Snippet Browser test', () => { params: { query: '', attributesToSnippet: ['overview:2'], - snippetEllipsisText: '...', + snippetEllipsisText: '…', }, }, ]) const snippeted = response.results[0]?.hits[0]?._snippetResult - expect(snippeted).toHaveProperty('overview', { value: 'Princess...' }) + expect(snippeted).toHaveProperty('overview', { + value: 'Taisto Kasurinen…', + }) }) test('Test one attributesToSnippet on specific query', async () => { @@ -40,18 +78,14 @@ describe('Snippet Browser test', () => { params: { query: 'judg', attributesToSnippet: ['overview:2'], - snippetEllipsisText: '...', + snippetEllipsisText: '…', }, }, ]) - const highlighted = response.results[0]?.hits[0]?._highlightResult const snippeted = response.results[0].hits[0]._snippetResult - expect(highlighted).toHaveProperty('overview', { - value: 'While', - }) expect(snippeted).toHaveProperty('overview', { - value: 'While...', + value: 'While racing…', }) }) @@ -62,45 +96,28 @@ describe('Snippet Browser test', () => { params: { query: 'judg', attributesToSnippet: ['*:2'], - snippetEllipsisText: '...', + snippetEllipsisText: '…', }, }, ]) - const highlighted = response.results[0]?.hits[0]?._highlightResult const snippeted = response.results[0].hits[0]._snippetResult - expect(highlighted).toHaveProperty('id', { - value: '6', - }) expect(snippeted).toHaveProperty('id', { value: '6', }) - expect(highlighted).toHaveProperty('title', { - value: '__ais-highlight__Judg__/ais-highlight__ment Night', - }) + expect(snippeted).toHaveProperty('title', { value: '__ais-highlight__Judg__/ais-highlight__ment Night', }) - expect(highlighted).toHaveProperty('overview', { - value: 'While', - }) + expect(snippeted).toHaveProperty('overview', { - value: 'While...', - }) - expect(highlighted).toHaveProperty('release_date', { - value: '750643200', + value: 'While racing…', }) + expect(snippeted).toHaveProperty('release_date', { value: '750643200', }) - expect(highlighted?.genres).toBeTruthy() - if (highlighted?.genres) { - expect(highlighted?.genres[0].value).toEqual('Action') - expect(highlighted?.genres[1].value).toEqual('Thriller') - expect(highlighted?.genres[2].value).toEqual('Crime') - } - expect(snippeted?.genres).toBeTruthy() if (snippeted?.genres) { expect(snippeted?.genres[0].value).toEqual('Action') @@ -115,41 +132,28 @@ describe('Snippet Browser test', () => { indexName: 'movies', params: { query: 's', - attributesToSnippet: ['overview:2', 'title:2'], + attributesToSnippet: ['overview:2', 'title:1'], highlightPreTag: '

', highlightPostTag: '

', - snippetEllipsisText: '...', + snippetEllipsisText: '…', }, }, ]) - const firstHitHighlight = response.results[0]?.hits[0]?._highlightResult const firstHitSnippet = response.results[0].hits[0]._snippetResult - - expect(firstHitHighlight).toHaveProperty('title', { - value: '

S

tar Wars', - }) - expect(firstHitHighlight).toHaveProperty('overview', { - value: 'Luke

S

kywalker and', - }) + const secondHitSnippet = response.results[0]?.hits[1]?._snippetResult expect(firstHitSnippet).toHaveProperty('title', { - value: '

S

tar Wars', + value: '

S

tar…', }) expect(firstHitSnippet).toHaveProperty('overview', { - value: 'Luke

S

kywalker and...', + value: '…Luke

S

kywalker…', }) - - const secondHitHighlight = response.results[0]?.hits[1]?._highlightResult - const secondHitSnippet = response.results[0]?.hits[1]?._snippetResult - expect(secondHitHighlight).toHaveProperty('title', { value: 'Four' }) - expect(secondHitHighlight?.overview?.value).toEqual("It'

s

Ted") - expect(secondHitSnippet).toHaveProperty('title', { - value: 'Four...', + value: 'Four…', }) expect(secondHitSnippet).toHaveProperty('overview', { - value: "It'

s

Ted...", + value: "It'

s

…", }) }) @@ -160,7 +164,7 @@ describe('Snippet Browser test', () => { params: { query: 'Kill', attributesToSnippet: ['overview:2'], - snippetEllipsisText: '...', + snippetEllipsisText: '…', }, }, ]) @@ -176,14 +180,14 @@ describe('Snippet Browser test', () => { params: { query: '', attributesToSnippet: ['overview:2'], - snippetEllipsisText: '...', + snippetEllipsisText: '…', }, }, ]) const snippeted = response.results[0]?.hits[0]?._snippetResult - expect(snippeted).toHaveProperty('overview', { value: 'Princess...' }) - expect(snippeted).toHaveProperty('title', { value: 'Star Wars' }) + + expect(snippeted).toHaveProperty('overview', { value: 'Taisto Kasurinen…' }) }) test('Test one attributesToSnippet on specific query w/ snippetEllipsisText', async () => { @@ -193,12 +197,12 @@ describe('Snippet Browser test', () => { params: { query: 'judg', attributesToSnippet: ['overview:2'], - snippetEllipsisText: '...', + snippetEllipsisText: '…', }, }, ]) const snippeted = response.results[0]?.hits[0]?._snippetResult - expect(snippeted).toHaveProperty('overview', { value: 'While...' }) + expect(snippeted).toHaveProperty('overview', { value: 'While racing…' }) }) test('Test two attributesToSnippet on specific query with one hit empty string w/ snippetEllipsisText', async () => { @@ -207,24 +211,24 @@ describe('Snippet Browser test', () => { indexName: 'movies', params: { query: 'm', - attributesToSnippet: ['overview:2', 'title:5'], - snippetEllipsisText: '...', + attributesToSnippet: ['overview:1', 'title:1'], + snippetEllipsisText: '…', }, }, ]) const firstHit = response.results[0]?.hits[0]?._snippetResult expect(firstHit).toHaveProperty('title', { - value: '__ais-highlight__M__/ais-highlight__agnetic Rose', + value: '__ais-highlight__M__/ais-highlight__agnetic…', }) expect(firstHit).toHaveProperty('overview', { value: '' }) const secondHit = response.results[0].hits[1]._snippetResult expect(secondHit).toHaveProperty('title', { - value: 'Judgment...', + value: 'Judgment…', }) expect(secondHit).toHaveProperty('overview', { - value: 'boxing __ais-highlight__m__/ais-highlight__atch,...', + value: '…__ais-highlight__m__/ais-highlight__atch…', }) }) @@ -235,7 +239,7 @@ describe('Snippet Browser test', () => { params: { query: 'Kill', attributesToSnippet: ['overview:2'], - snippetEllipsisText: '...', + snippetEllipsisText: '…', }, }, ]) @@ -255,19 +259,80 @@ describe('Snippet Browser test', () => { { indexName: 'movies', params: { - query: 'Jud', - attributesToSnippet: ['*:2'], - snippetEllipsisText: '...', + query: 'Judgment', + attributesToSnippet: ['*:1'], + snippetEllipsisText: '…', + }, + }, + ]) + const hit = response.results[0].hits[0]._snippetResult + + if (hit?.overview) { + expect(hit?.overview.value).toEqual('While…') + } + if (hit?.poster) { + // Considered to be 2 words because of special char + expect(hit?.poster.value).toEqual('https…') + } + + if (hit?.genres) { + expect(hit?.genres[0]?.value).toEqual('Action') + expect(hit?.genres[1]?.value).toEqual('Thriller') + } + if (hit?.id) { + expect(hit?.id.value).toEqual('6') + } + if (hit?.undefinedArray) { + // @ts-ignore + expect(hit?.undefinedArray[0]?.value).toEqual('null') + // @ts-ignore + expect(hit?.undefinedArray[1]?.value).toEqual('null') + } + + if (hit?.nullArray) { + // @ts-ignore + expect(hit?.nullArray[0]?.value).toEqual('null') + // @ts-ignore + expect(hit?.nullArray[1]?.value).toEqual('null') + } + + if (hit?.objectArray) { + // @ts-ignore + expect(hit?.objectArray[0]?.value).toEqual('{"name":"hello…"}') + // @ts-ignore + expect(hit?.objectArray[1]?.value).toEqual('{"name":"hello…"}') + } + + if (hit?.object) { + // @ts-ignore + expect(hit?.object?.value).toEqual('{"id":"1","name":"One…"}') + } + + if (hit?.nullField) { + // @ts-ignore + expect(hit?.nullField?.value).toEqual('null') + } + }) + + test('Test custom crop marker', async () => { + const response = await searchClient.search([ + { + indexName: 'movies', + params: { + query: 'Judgment', + attributesToSnippet: ['*:1'], + snippetEllipsisText: '( •_•)', }, }, ]) const hit = response.results[0].hits[0]._snippetResult if (hit?.overview) { - expect(hit?.overview.value).toEqual('While...') + expect(hit?.overview.value).toEqual('While( •_•)') } if (hit?.poster) { - expect(hit?.poster.value).toEqual('https...') + // Considered to be 2 words because of special char + expect(hit?.poster.value).toEqual('https( •_•)') } if (hit?.genres) { @@ -293,14 +358,14 @@ describe('Snippet Browser test', () => { if (hit?.objectArray) { // @ts-ignore - expect(hit?.objectArray[0]?.value).toEqual('{"name":"charlotte"}') + expect(hit?.objectArray[0]?.value).toEqual('{"name":"hello( •_•)"}') // @ts-ignore - expect(hit?.objectArray[1]?.value).toEqual('{"name":"charlotte"}') + expect(hit?.objectArray[1]?.value).toEqual('{"name":"hello( •_•)"}') } if (hit?.object) { // @ts-ignore - expect(hit?.object?.value).toEqual('{"id":"1","name":"Nader"}') + expect(hit?.object?.value).toEqual('{"id":"1","name":"One( •_•)"}') } if (hit?.nullField) { @@ -317,7 +382,7 @@ test('Test attributes to snippet on value smaller than the snippet size', async params: { query: '', attributesToSnippet: ['*:20'], - snippetEllipsisText: '...', + snippetEllipsisText: '…', }, }, ]) diff --git a/yarn.lock b/yarn.lock index 8e17fa3b..f10f3233 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5138,10 +5138,10 @@ mdn-data@2.0.4: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== -meilisearch@0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/meilisearch/-/meilisearch-0.25.0.tgz#8e980fbdd36b9fe6ed606205e262418f21e64d84" - integrity sha512-TSIJTh5lva7WHBaoG3arNYQXuIAQkcD3BY09h2nHhjHS/wzxWKJM45x5bEC67Grw8zXihVqqmWty4a4ps4S+tg== +meilisearch@^0.26.0-beta.0: + version "0.26.0-beta.0" + resolved "https://registry.yarnpkg.com/meilisearch/-/meilisearch-0.26.0-beta.0.tgz#80b02e1a0e7c06316d3b4addf087dabf26c85af1" + integrity sha512-qJ0d045plX54gsOnxprWlOO99ShRm/+26tX5DJIysSafij6lFh2wAB9yFDWZ+Jq8fT0hDkkwsI3jqzx2wa61gg== dependencies: cross-fetch "^3.1.5" From baef2e4d1095f2fa03e57cebefc1a78a5e1b1820 Mon Sep 17 00:00:00 2001 From: cvermand <33010418+bidoubiwa@users.noreply.github.com> Date: Mon, 25 Apr 2022 16:25:39 +0200 Subject: [PATCH 5/6] Add highlight prefix suffix (#739) * Fix tests related to changes in Meilisearch * Update tests based on placeholder search resolution * Remove custom highlight tag handler * Remove unecessary highlight formating functions * Improve highliht tests * Make highlight work on nested objects * Update snippet tests * Test on correct nested object value --- .../search-params-adapter.ts | 16 ++++ .../format-adapter/format-adapter.ts | 70 ++++++++++++++--- .../format-adapter/highlight-adapter.ts | 76 ------------------- .../search-response-adapter/hits-adapter.ts | 22 +++--- src/utils/index.ts | 1 + src/utils/object.ts | 3 + tests/assets/utils.ts | 4 +- tests/highlight.tests.ts | 19 +++-- tests/snippets.tests.ts | 20 ++--- 9 files changed, 114 insertions(+), 117 deletions(-) delete mode 100644 src/adapter/search-response-adapter/format-adapter/highlight-adapter.ts create mode 100644 src/utils/object.ts diff --git a/src/adapter/search-request-adapter/search-params-adapter.ts b/src/adapter/search-request-adapter/search-params-adapter.ts index 30ee9aa8..6d144ecd 100644 --- a/src/adapter/search-request-adapter/search-params-adapter.ts +++ b/src/adapter/search-request-adapter/search-params-adapter.ts @@ -63,6 +63,22 @@ export function adaptSearchParams( '*', ] + // Highlight pre tag + const highlightPreTag = searchContext?.highlightPreTag + if (highlightPreTag) { + meiliSearchParams.highlightPreTag = highlightPreTag + } else { + meiliSearchParams.highlightPreTag = '__ais-highlight__' + } + + // Highlight post tag + const highlightPostTag = searchContext?.highlightPostTag + if (highlightPostTag) { + meiliSearchParams.highlightPostTag = highlightPostTag + } else { + meiliSearchParams.highlightPostTag = '__/ais-highlight__' + } + const placeholderSearch = searchContext.placeholderSearch const query = searchContext.query diff --git a/src/adapter/search-response-adapter/format-adapter/format-adapter.ts b/src/adapter/search-response-adapter/format-adapter/format-adapter.ts index 1d9e225f..7d3c656d 100644 --- a/src/adapter/search-response-adapter/format-adapter/format-adapter.ts +++ b/src/adapter/search-response-adapter/format-adapter/format-adapter.ts @@ -1,22 +1,70 @@ -import { adaptHighlight } from './highlight-adapter' -import { SearchContext } from '../../../types' +import { isPureObject } from '../../../utils' /** - * Adapt Meilisearch formating to formating compliant with instantsearch.js. + * Stringify values following instantsearch practices. + * + * @param {any} value - value that needs to be stringified + */ +function stringifyValue(value: any) { + if (typeof value === 'string') { + // String + return value + } else if (value === undefined) { + // undefined + return JSON.stringify(null) + } else { + return JSON.stringify(value) + } +} + +/** + * Recursif function wrap the deepest possible value + * the following way: { value: "xx" }. + * + * For example: + * + * { + * "rootField": { "value": "x" } + * "nestedField": { child: { value: "y" } } + * } + * + * recursivity continues until the value is not an array or an object. + * + * @param {any} value - value of a field + * + * @returns Record + */ +function wrapValue(value: any): Record { + if (Array.isArray(value)) { + // Array + return value.map((elem) => wrapValue(elem)) + } else if (isPureObject(value)) { + // Object + return Object.keys(value).reduce>( + (nested: Record, key: string) => { + nested[key] = wrapValue(value[key]) + + return nested + }, + {} + ) + } else { + return { value: stringifyValue(value) } + } +} + +/** + * Adapt Meilisearch formatted fields to a format compliant to instantsearch.js. * * @param {Record, - searchContext: SearchContext +export function adaptFormattedFields( + hit: Record ): Record { - const preTag = searchContext?.highlightPreTag - const postTag = searchContext?.highlightPostTag - - if (!hit._formatted) return {} - const _formattedResult = adaptHighlight(hit, preTag, postTag) + if (!hit) return {} + const _formattedResult = wrapValue(hit) const highlightedHit = { // We could not determine what the differences are between those two fields. diff --git a/src/adapter/search-response-adapter/format-adapter/highlight-adapter.ts b/src/adapter/search-response-adapter/format-adapter/highlight-adapter.ts deleted file mode 100644 index b6101b8c..00000000 --- a/src/adapter/search-response-adapter/format-adapter/highlight-adapter.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { isString } from '../../../utils' -/** - * Replace `em` tags in highlighted Meilisearch hits to - * provided tags by instantsearch.js. - * - * @param {string} value - * @param {string} highlightPreTag? - * @param {string} highlightPostTag? - * @returns {string} - */ -function replaceDefaultEMTag( - value: any, - preTag = '__ais-highlight__', - postTag = '__/ais-highlight__' -): string { - // Highlight is applied by Meilisearch ( tags) - // We replace the by the expected tag for InstantSearch - const stringifiedValue = isString(value) ? value : JSON.stringify(value) - - return stringifiedValue.replace(//g, preTag).replace(/<\/em>/g, postTag) -} - -function addHighlightTags( - value: any, - preTag?: string, - postTag?: string -): string { - if (typeof value === 'string') { - // String - return replaceDefaultEMTag(value, preTag, postTag) - } else if (value === undefined) { - // undefined - return JSON.stringify(null) - } else { - // Other - return JSON.stringify(value) - } -} - -export function resolveHighlightValue( - value: any, - preTag?: string, - postTag?: string -): { value: string } | Array<{ value: string }> { - if (Array.isArray(value)) { - // Array - return value.map((elem) => ({ - value: addHighlightTags(elem, preTag, postTag), - })) - } else { - return { value: addHighlightTags(value, preTag, postTag) } - } -} - -/** - * @param {Record, - preTag?: string, - postTag?: string -): Record { - // hit is the `_formatted` object returned by Meilisearch. - // It contains all the highlighted and croped attributes - - if (!hit._formatted) return hit._formatted - return Object.keys(hit._formatted).reduce((result, key) => { - const value = hit._formatted[key] - - result[key] = resolveHighlightValue(value, preTag, postTag) - return result - }, {} as any) -} diff --git a/src/adapter/search-response-adapter/hits-adapter.ts b/src/adapter/search-response-adapter/hits-adapter.ts index 2c0b88a1..9899c8d1 100644 --- a/src/adapter/search-response-adapter/hits-adapter.ts +++ b/src/adapter/search-response-adapter/hits-adapter.ts @@ -1,6 +1,6 @@ import type { PaginationContext, SearchContext } from '../../types' import { adaptPagination } from './pagination-adapter' -import { adaptFormating } from './format-adapter' +import { adaptFormattedFields } from './format-adapter' import { adaptGeoResponse } from './geo-reponse-adapter' /** @@ -18,19 +18,23 @@ export function adaptHits( const { hitsPerPage, page } = paginationContext const paginatedHits = adaptPagination(hits, page, hitsPerPage) - let formattedHits = paginatedHits.map((hit: Record) => { + let adaptedHits = paginatedHits.map((hit: Record) => { // Creates Hit object compliant with InstantSearch if (Object.keys(hit).length > 0) { - const { _formatted: formattedHit, _matchesInfo, ...restOfHit } = hit + const { _formatted: formattedHit, _matchesInfo, ...documentFields } = hit - return { - ...restOfHit, - ...adaptFormating(hit, searchContext), - ...(primaryKey && { objectID: hit[primaryKey] }), + const adaptedHit: Record = Object.assign( + documentFields, + adaptFormattedFields(formattedHit) + ) + + if (primaryKey) { + adaptedHit.objectID = hit[primaryKey] } + return adaptedHit } return hit }) - formattedHits = adaptGeoResponse(formattedHits) - return formattedHits + adaptedHits = adaptGeoResponse(adaptedHits) + return adaptedHits } diff --git a/src/utils/index.ts b/src/utils/index.ts index d81c4c9a..75968a85 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,4 @@ export * from './array' export * from './string' export * from './number' +export * from './object' diff --git a/src/utils/object.ts b/src/utils/object.ts new file mode 100644 index 00000000..b5a8afe3 --- /dev/null +++ b/src/utils/object.ts @@ -0,0 +1,3 @@ +export function isPureObject(data: any) { + return typeof data === 'object' && !Array.isArray(data) && data !== null +} diff --git a/tests/assets/utils.ts b/tests/assets/utils.ts index 4e53a334..dbb638b2 100644 --- a/tests/assets/utils.ts +++ b/tests/assets/utils.ts @@ -208,8 +208,8 @@ export type Movies = { poster?: string genres?: string[] release_date?: number // eslint-disable-line - undefinedArray?: [undefined, undefined, undefined] - nullArray?: [null] + undefinedArray?: undefined[] + nullArray?: null[] objectArray?: Array<{ name: string }> object?: { id?: number diff --git a/tests/highlight.tests.ts b/tests/highlight.tests.ts index 1bdc7c4a..3da94601 100644 --- a/tests/highlight.tests.ts +++ b/tests/highlight.tests.ts @@ -231,13 +231,18 @@ describe('Highlight Browser test', () => { { indexName: 'movies', params: { - query: 'Ariel', + query: 'hello', attributesToHighlight: ['*'], }, }, ]) + const hit = response.results[0].hits[0]._highlightResult + if (hit?.title) { + expect(hit?.title?.value).toEqual('Ariel') + } + if (hit?.genres) { expect(hit?.genres[0]?.value).toEqual('Drama') expect(hit?.genres[1]?.value).toEqual('Crime') @@ -260,15 +265,15 @@ describe('Highlight Browser test', () => { } if (hit?.objectArray) { - // @ts-ignore - expect(hit?.objectArray[0]?.value).toEqual('{"name":"hello world"}') - // @ts-ignore - expect(hit?.objectArray[1]?.value).toEqual('{"name":"hello world"}') + expect(hit?.objectArray[0]?.name.value).toEqual( + '__ais-highlight__hello__/ais-highlight__ world' + ) } if (hit?.object) { - // @ts-ignore - expect(hit?.object?.value).toEqual('{"id":"1","name":"One two"}') + expect(hit?.object?.id?.value).toEqual('1') + + expect(hit?.object?.name?.value).toEqual('One two') } if (hit?.nullField) { diff --git a/tests/snippets.tests.ts b/tests/snippets.tests.ts index 9d9f7f32..adbea30a 100644 --- a/tests/snippets.tests.ts +++ b/tests/snippets.tests.ts @@ -297,15 +297,13 @@ describe('Snippet Browser test', () => { } if (hit?.objectArray) { - // @ts-ignore - expect(hit?.objectArray[0]?.value).toEqual('{"name":"hello…"}') - // @ts-ignore - expect(hit?.objectArray[1]?.value).toEqual('{"name":"hello…"}') + expect(hit?.objectArray[0]?.name?.value).toEqual('hello…') + expect(hit?.objectArray[1]?.name?.value).toEqual('hello…') } if (hit?.object) { - // @ts-ignore - expect(hit?.object?.value).toEqual('{"id":"1","name":"One…"}') + expect(hit?.object?.name?.value).toEqual('One…') + expect(hit?.object?.id?.value).toEqual('1') } if (hit?.nullField) { @@ -357,15 +355,13 @@ describe('Snippet Browser test', () => { } if (hit?.objectArray) { - // @ts-ignore - expect(hit?.objectArray[0]?.value).toEqual('{"name":"hello( •_•)"}') - // @ts-ignore - expect(hit?.objectArray[1]?.value).toEqual('{"name":"hello( •_•)"}') + expect(hit?.objectArray[0]?.name?.value).toEqual('hello( •_•)') + expect(hit?.objectArray[1]?.name?.value).toEqual('hello( •_•)') } if (hit?.object) { - // @ts-ignore - expect(hit?.object?.value).toEqual('{"id":"1","name":"One( •_•)"}') + expect(hit?.object?.id?.value).toEqual('1') + expect(hit?.object?.name?.value).toEqual('One( •_•)') } if (hit?.nullField) { From 6c7e6e61c30bcc1874dad648e637aaacab2d8724 Mon Sep 17 00:00:00 2001 From: Bruno Casali Date: Mon, 9 May 2022 15:51:57 +0200 Subject: [PATCH 6/6] Update meilisearch dependency to latest stable version 0.25.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9b59774c..6b77e6fe 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "url": "https://github.com/meilisearch/instant-meilisearch.git" }, "dependencies": { - "meilisearch": "^0.26.0-beta.0" + "meilisearch": "0.25.1" }, "devDependencies": { "@babel/cli": "^7.17.6",