Skip to content

Commit baef2e4

Browse files
authored
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
1 parent d506846 commit baef2e4

File tree

9 files changed

+114
-117
lines changed

9 files changed

+114
-117
lines changed

src/adapter/search-request-adapter/search-params-adapter.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,22 @@ export function adaptSearchParams(
6363
'*',
6464
]
6565

66+
// Highlight pre tag
67+
const highlightPreTag = searchContext?.highlightPreTag
68+
if (highlightPreTag) {
69+
meiliSearchParams.highlightPreTag = highlightPreTag
70+
} else {
71+
meiliSearchParams.highlightPreTag = '__ais-highlight__'
72+
}
73+
74+
// Highlight post tag
75+
const highlightPostTag = searchContext?.highlightPostTag
76+
if (highlightPostTag) {
77+
meiliSearchParams.highlightPostTag = highlightPostTag
78+
} else {
79+
meiliSearchParams.highlightPostTag = '__/ais-highlight__'
80+
}
81+
6682
const placeholderSearch = searchContext.placeholderSearch
6783
const query = searchContext.query
6884

src/adapter/search-response-adapter/format-adapter/format-adapter.ts

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,70 @@
1-
import { adaptHighlight } from './highlight-adapter'
2-
import { SearchContext } from '../../../types'
1+
import { isPureObject } from '../../../utils'
32

43
/**
5-
* Adapt Meilisearch formating to formating compliant with instantsearch.js.
4+
* Stringify values following instantsearch practices.
5+
*
6+
* @param {any} value - value that needs to be stringified
7+
*/
8+
function stringifyValue(value: any) {
9+
if (typeof value === 'string') {
10+
// String
11+
return value
12+
} else if (value === undefined) {
13+
// undefined
14+
return JSON.stringify(null)
15+
} else {
16+
return JSON.stringify(value)
17+
}
18+
}
19+
20+
/**
21+
* Recursif function wrap the deepest possible value
22+
* the following way: { value: "xx" }.
23+
*
24+
* For example:
25+
*
26+
* {
27+
* "rootField": { "value": "x" }
28+
* "nestedField": { child: { value: "y" } }
29+
* }
30+
*
31+
* recursivity continues until the value is not an array or an object.
32+
*
33+
* @param {any} value - value of a field
34+
*
35+
* @returns Record<string, any>
36+
*/
37+
function wrapValue(value: any): Record<string, any> {
38+
if (Array.isArray(value)) {
39+
// Array
40+
return value.map((elem) => wrapValue(elem))
41+
} else if (isPureObject(value)) {
42+
// Object
43+
return Object.keys(value).reduce<Record<string, any>>(
44+
(nested: Record<string, any>, key: string) => {
45+
nested[key] = wrapValue(value[key])
46+
47+
return nested
48+
},
49+
{}
50+
)
51+
} else {
52+
return { value: stringifyValue(value) }
53+
}
54+
}
55+
56+
/**
57+
* Adapt Meilisearch formatted fields to a format compliant to instantsearch.js.
658
*
759
* @param {Record<string} formattedHit
860
* @param {SearchContext} searchContext
961
* @returns {Record}
1062
*/
11-
export function adaptFormating(
12-
hit: Record<string, any>,
13-
searchContext: SearchContext
63+
export function adaptFormattedFields(
64+
hit: Record<string, any>
1465
): Record<string, any> {
15-
const preTag = searchContext?.highlightPreTag
16-
const postTag = searchContext?.highlightPostTag
17-
18-
if (!hit._formatted) return {}
19-
const _formattedResult = adaptHighlight(hit, preTag, postTag)
66+
if (!hit) return {}
67+
const _formattedResult = wrapValue(hit)
2068

2169
const highlightedHit = {
2270
// We could not determine what the differences are between those two fields.

src/adapter/search-response-adapter/format-adapter/highlight-adapter.ts

Lines changed: 0 additions & 76 deletions
This file was deleted.

src/adapter/search-response-adapter/hits-adapter.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { PaginationContext, SearchContext } from '../../types'
22
import { adaptPagination } from './pagination-adapter'
3-
import { adaptFormating } from './format-adapter'
3+
import { adaptFormattedFields } from './format-adapter'
44
import { adaptGeoResponse } from './geo-reponse-adapter'
55

66
/**
@@ -18,19 +18,23 @@ export function adaptHits(
1818
const { hitsPerPage, page } = paginationContext
1919
const paginatedHits = adaptPagination(hits, page, hitsPerPage)
2020

21-
let formattedHits = paginatedHits.map((hit: Record<string, any>) => {
21+
let adaptedHits = paginatedHits.map((hit: Record<string, any>) => {
2222
// Creates Hit object compliant with InstantSearch
2323
if (Object.keys(hit).length > 0) {
24-
const { _formatted: formattedHit, _matchesInfo, ...restOfHit } = hit
24+
const { _formatted: formattedHit, _matchesInfo, ...documentFields } = hit
2525

26-
return {
27-
...restOfHit,
28-
...adaptFormating(hit, searchContext),
29-
...(primaryKey && { objectID: hit[primaryKey] }),
26+
const adaptedHit: Record<string, any> = Object.assign(
27+
documentFields,
28+
adaptFormattedFields(formattedHit)
29+
)
30+
31+
if (primaryKey) {
32+
adaptedHit.objectID = hit[primaryKey]
3033
}
34+
return adaptedHit
3135
}
3236
return hit
3337
})
34-
formattedHits = adaptGeoResponse(formattedHits)
35-
return formattedHits
38+
adaptedHits = adaptGeoResponse(adaptedHits)
39+
return adaptedHits
3640
}

src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './array'
22
export * from './string'
33
export * from './number'
4+
export * from './object'

src/utils/object.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function isPureObject(data: any) {
2+
return typeof data === 'object' && !Array.isArray(data) && data !== null
3+
}

tests/assets/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,8 @@ export type Movies = {
208208
poster?: string
209209
genres?: string[]
210210
release_date?: number // eslint-disable-line
211-
undefinedArray?: [undefined, undefined, undefined]
212-
nullArray?: [null]
211+
undefinedArray?: undefined[]
212+
nullArray?: null[]
213213
objectArray?: Array<{ name: string }>
214214
object?: {
215215
id?: number

tests/highlight.tests.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -231,13 +231,18 @@ describe('Highlight Browser test', () => {
231231
{
232232
indexName: 'movies',
233233
params: {
234-
query: 'Ariel',
234+
query: 'hello',
235235
attributesToHighlight: ['*'],
236236
},
237237
},
238238
])
239+
239240
const hit = response.results[0].hits[0]._highlightResult
240241

242+
if (hit?.title) {
243+
expect(hit?.title?.value).toEqual('Ariel')
244+
}
245+
241246
if (hit?.genres) {
242247
expect(hit?.genres[0]?.value).toEqual('Drama')
243248
expect(hit?.genres[1]?.value).toEqual('Crime')
@@ -260,15 +265,15 @@ describe('Highlight Browser test', () => {
260265
}
261266

262267
if (hit?.objectArray) {
263-
// @ts-ignore
264-
expect(hit?.objectArray[0]?.value).toEqual('{"name":"hello world"}')
265-
// @ts-ignore
266-
expect(hit?.objectArray[1]?.value).toEqual('{"name":"hello world"}')
268+
expect(hit?.objectArray[0]?.name.value).toEqual(
269+
'__ais-highlight__hello__/ais-highlight__ world'
270+
)
267271
}
268272

269273
if (hit?.object) {
270-
// @ts-ignore
271-
expect(hit?.object?.value).toEqual('{"id":"1","name":"One two"}')
274+
expect(hit?.object?.id?.value).toEqual('1')
275+
276+
expect(hit?.object?.name?.value).toEqual('One two')
272277
}
273278

274279
if (hit?.nullField) {

tests/snippets.tests.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -297,15 +297,13 @@ describe('Snippet Browser test', () => {
297297
}
298298

299299
if (hit?.objectArray) {
300-
// @ts-ignore
301-
expect(hit?.objectArray[0]?.value).toEqual('{"name":"hello…"}')
302-
// @ts-ignore
303-
expect(hit?.objectArray[1]?.value).toEqual('{"name":"hello…"}')
300+
expect(hit?.objectArray[0]?.name?.value).toEqual('hello…')
301+
expect(hit?.objectArray[1]?.name?.value).toEqual('hello…')
304302
}
305303

306304
if (hit?.object) {
307-
// @ts-ignore
308-
expect(hit?.object?.value).toEqual('{"id":"1","name":"One…"}')
305+
expect(hit?.object?.name?.value).toEqual('One…')
306+
expect(hit?.object?.id?.value).toEqual('1')
309307
}
310308

311309
if (hit?.nullField) {
@@ -357,15 +355,13 @@ describe('Snippet Browser test', () => {
357355
}
358356

359357
if (hit?.objectArray) {
360-
// @ts-ignore
361-
expect(hit?.objectArray[0]?.value).toEqual('{"name":"hello( •_•)"}')
362-
// @ts-ignore
363-
expect(hit?.objectArray[1]?.value).toEqual('{"name":"hello( •_•)"}')
358+
expect(hit?.objectArray[0]?.name?.value).toEqual('hello( •_•)')
359+
expect(hit?.objectArray[1]?.name?.value).toEqual('hello( •_•)')
364360
}
365361

366362
if (hit?.object) {
367-
// @ts-ignore
368-
expect(hit?.object?.value).toEqual('{"id":"1","name":"One( •_•)"}')
363+
expect(hit?.object?.id?.value).toEqual('1')
364+
expect(hit?.object?.name?.value).toEqual('One( •_•)')
369365
}
370366

371367
if (hit?.nullField) {

0 commit comments

Comments
 (0)