Skip to content

Commit c28acbd

Browse files
authored
Fix error on non array/object/string being highlighted (#598)
* Fix error on non array/object/string being highlighted * Fix tests
1 parent f1522bd commit c28acbd

File tree

4 files changed

+245
-82
lines changed

4 files changed

+245
-82
lines changed

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

Lines changed: 95 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,39 @@ import { SearchContext } from '../../types'
1212
*/
1313
function replaceHighlightTags(
1414
value: any,
15-
highlightPreTag?: string,
16-
highlightPostTag?: string
15+
preTag?: string,
16+
postTag?: string
1717
): string {
18-
highlightPreTag = highlightPreTag || '__ais-highlight__'
19-
highlightPostTag = highlightPostTag || '__/ais-highlight__'
18+
preTag = preTag || '__ais-highlight__'
19+
postTag = postTag || '__/ais-highlight__'
2020
// Highlight is applied by MeiliSearch (<em> tags)
2121
// We replace the <em> by the expected tag for InstantSearch
2222
const stringifiedValue = isString(value) ? value : JSON.stringify(value)
2323

24-
return stringifiedValue
25-
.replace(/<em>/g, highlightPreTag)
26-
.replace(/<\/em>/g, highlightPostTag)
24+
return stringifiedValue.replace(/<em>/g, preTag).replace(/<\/em>/g, postTag)
25+
}
26+
27+
function addHighlightTags(value: string, preTag?: string, postTag?: string) {
28+
return {
29+
value: replaceHighlightTags(value, preTag, postTag),
30+
}
31+
}
32+
33+
function resolveHighlightValue(
34+
value: string,
35+
preTag?: string,
36+
postTag?: string
37+
) {
38+
if (typeof value === 'string') {
39+
// String
40+
return addHighlightTags(value, preTag, postTag)
41+
} else if (value === undefined) {
42+
// undefined
43+
return { value: JSON.stringify(null) }
44+
} else {
45+
// Other
46+
return { value: JSON.stringify(value) }
47+
}
2748
}
2849

2950
/**
@@ -33,98 +54,93 @@ function replaceHighlightTags(
3354
* @returns {Record}
3455
*/
3556
function adaptHighlight(
36-
formattedHit: Record<string, any>,
37-
highlightPreTag?: string,
38-
highlightPostTag?: string
57+
hit: Record<string, any>,
58+
preTag?: string,
59+
postTag?: string
3960
): Record<string, any> {
40-
// formattedHit is the `_formatted` object returned by MeiliSearch.
61+
// hit is the `_formatted` object returned by MeiliSearch.
4162
// It contains all the highlighted and croped attributes
42-
const toHighlightMatch = (value: any) => ({
43-
value: replaceHighlightTags(value, highlightPreTag, highlightPostTag),
44-
})
45-
return Object.keys(formattedHit).reduce((result, key) => {
46-
const value = formattedHit[key]
63+
64+
return Object.keys(hit).reduce((result, key) => {
65+
const value = hit[key]
66+
4767
if (Array.isArray(value)) {
48-
result[key] = value.map((val) => ({
49-
value: typeof val === 'object' ? JSON.stringify(val) : val,
50-
}))
51-
} else if (typeof value === 'object' && value !== null) {
52-
result[key] = { value: JSON.stringify(value) }
68+
// Array
69+
result[key] = value.map((elem) =>
70+
resolveHighlightValue(elem, preTag, postTag)
71+
)
5372
} else {
54-
result[key] = toHighlightMatch(value)
73+
result[key] = resolveHighlightValue(value, preTag, postTag)
5574
}
5675
return result
5776
}, {} as any)
5877
}
5978

6079
/**
6180
* @param {string} value
62-
* @param {string} snippetEllipsisText?
63-
* @param {string} highlightPreTag?
64-
* @param {string} highlightPostTag?
81+
* @param {string} preTag?
82+
* @param {string} postTag?
83+
* @param {string} ellipsis?
6584
* @returns {string}
6685
*/
67-
function snippetValue(
86+
function resolveSnippetValue(
6887
value: string,
69-
snippetEllipsisText?: string,
70-
highlightPreTag?: string,
71-
highlightPostTag?: string
72-
): string {
88+
preTag?: string,
89+
postTag?: string,
90+
ellipsis?: string
91+
): { value: string } {
7392
let newValue = value
74-
// manage a kind of `...` for the crop until this feature is implemented https://roadmap.meilisearch.com/c/69-policy-for-cropped-values?utm_medium=social&utm_source=portal_share
75-
// `...` is put if we are at the middle of a sentence (instead at the middle of the document field)
76-
if (snippetEllipsisText !== undefined && isString(newValue) && newValue) {
93+
94+
// 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
95+
if (newValue && ellipsis !== undefined && isString(newValue)) {
7796
if (
7897
newValue[0] === newValue[0].toLowerCase() && // beginning of a sentence
79-
newValue.startsWith('<em>') === false // beginning of the document field, otherwise MeiliSearch would crop around the highligh
98+
newValue.startsWith('<em>') === false // beginning of the document field, otherwise MeiliSearch would crop around the highlight
8099
) {
81-
newValue = `${snippetEllipsisText}${newValue}`
100+
newValue = `${ellipsis}${newValue}`
82101
}
83102
if (!!newValue.match(/[.!?]$/) === false) {
84103
// end of the sentence
85-
newValue = `${newValue}${snippetEllipsisText}`
104+
newValue = `${newValue}${ellipsis}`
86105
}
87106
}
88-
return replaceHighlightTags(newValue, highlightPreTag, highlightPostTag)
107+
return resolveHighlightValue(newValue, preTag, postTag)
89108
}
90109

91110
/**
92-
* @param {Record<string} formattedHit
93-
* @param {readonlystring[]|undefined} attributesToSnippet
94-
* @param {string|undefined} snippetEllipsisText
95-
* @param {string|undefined} highlightPreTag
96-
* @param {string|undefined} highlightPostTag
111+
* @param {Record<string} hit
112+
* @param {readonlystring[]|undefined} attributes
113+
* @param {string|undefined} ellipsis
114+
* @param {string|undefined} preTag
115+
* @param {string|undefined} postTage
97116
*/
98117
function adaptSnippet(
99-
formattedHit: Record<string, any>,
100-
attributesToSnippet: readonly string[] | undefined,
101-
snippetEllipsisText: string | undefined,
102-
highlightPreTag: string | undefined,
103-
highlightPostTag: string | undefined
118+
hit: Record<string, any>,
119+
attributes: readonly string[] | undefined,
120+
ellipsis: string | undefined,
121+
pretag: string | undefined,
122+
postTag: string | undefined
104123
) {
105-
if (attributesToSnippet === undefined) {
124+
if (attributes === undefined) {
106125
return null
107126
}
108-
attributesToSnippet = attributesToSnippet.map(
109-
(attribute) => attribute.split(':')[0]
110-
) as any[]
111-
const snippetAll = attributesToSnippet.includes('*')
112-
// formattedHit is the `_formatted` object returned by MeiliSearch.
127+
attributes = attributes.map((attribute) => attribute.split(':')[0]) as any[]
128+
const snippetAll = attributes.includes('*')
129+
130+
// hit is the `_formatted` object returned by MeiliSearch.
113131
// It contains all the highlighted and croped attributes
114-
const toSnippetMatch = (value: any) => ({
115-
value: snippetValue(
116-
value,
117-
snippetEllipsisText,
118-
highlightPreTag,
119-
highlightPostTag
120-
),
121-
})
122-
return (Object.keys(formattedHit) as any[]).reduce((result, key) => {
123-
if (snippetAll || attributesToSnippet?.includes(key)) {
124-
const value = formattedHit[key]
125-
result[key] = Array.isArray(value)
126-
? value.map(toSnippetMatch)
127-
: toSnippetMatch(value)
132+
133+
return (Object.keys(hit) as any[]).reduce((result, key) => {
134+
if (snippetAll || attributes?.includes(key)) {
135+
const value = hit[key]
136+
if (Array.isArray(value)) {
137+
// Array
138+
result[key] = value.map((elem) =>
139+
resolveSnippetValue(elem, pretag, postTag, ellipsis)
140+
)
141+
} else {
142+
result[key] = resolveSnippetValue(value, pretag, postTag, ellipsis)
143+
}
128144
}
129145
return result
130146
}, {} as any)
@@ -138,27 +154,24 @@ function adaptSnippet(
138154
* @returns {Record}
139155
*/
140156
export function adaptFormating(
141-
formattedHit: Record<string, any>,
157+
hit: Record<string, any>,
142158
searchContext: SearchContext
143159
): Record<string, any> {
144160
const attributesToSnippet = searchContext?.attributesToSnippet
145-
const snippetEllipsisText = searchContext?.snippetEllipsisText
146-
const highlightPreTag = searchContext?.highlightPreTag
147-
const highlightPostTag = searchContext?.highlightPostTag
161+
const ellipsis = searchContext?.snippetEllipsisText
162+
const preTag = searchContext?.highlightPreTag
163+
const postTag = searchContext?.highlightPostTag
148164

149-
if (!formattedHit || formattedHit.length) return {}
150-
return {
151-
_highlightResult: adaptHighlight(
152-
formattedHit,
153-
highlightPreTag,
154-
highlightPostTag
155-
),
165+
if (!hit || hit.length) return {}
166+
const highlightedHit = {
167+
_highlightResult: adaptHighlight(hit, preTag, postTag),
156168
_snippetResult: adaptSnippet(
157-
formattedHit,
169+
hit,
158170
attributesToSnippet,
159-
snippetEllipsisText,
160-
highlightPreTag,
161-
highlightPostTag
171+
ellipsis,
172+
preTag,
173+
postTag
162174
),
163175
}
176+
return highlightedHit
164177
}

tests/assets/utils.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ const dataset = [
99
genres: ['Drama', 'Crime', 'Comedy'],
1010
poster: 'https://image.tmdb.org/t/p/w500/ojDg0PGvs6R9xYFodRct2kdI6wC.jpg',
1111
release_date: 593395200,
12+
undefinedArray: [undefined, undefined, undefined],
13+
nullArray: [null, null, null],
14+
objectArray: [
15+
{ name: 'charlotte' },
16+
{ name: 'charlotte' },
17+
{ name: 'charlotte' },
18+
],
19+
object: {
20+
id: 1,
21+
name: 'Nader',
22+
},
23+
nullField: null,
1224
},
1325
{
1426
id: 5,
@@ -18,6 +30,18 @@ const dataset = [
1830
genres: ['Crime', 'Comedy'],
1931
poster: 'https://image.tmdb.org/t/p/w500/75aHn1NOYXh4M7L5shoeQ6NGykP.jpg',
2032
release_date: 818467200,
33+
undefinedArray: [undefined, undefined, undefined],
34+
nullArray: [null, null, null],
35+
objectArray: [
36+
{ name: 'charlotte' },
37+
{ name: 'charlotte' },
38+
{ name: 'charlotte' },
39+
],
40+
object: {
41+
id: 1,
42+
name: 'Nader',
43+
},
44+
nullField: null,
2145
},
2246
{
2347
id: 6,
@@ -27,6 +51,18 @@ const dataset = [
2751
genres: ['Action', 'Thriller', 'Crime'],
2852
poster: 'https://image.tmdb.org/t/p/w500/rYFAvSPlQUCebayLcxyK79yvtvV.jpg',
2953
release_date: 750643200,
54+
undefinedArray: [undefined, undefined, undefined],
55+
nullArray: [null, null, null],
56+
objectArray: [
57+
{ name: 'charlotte' },
58+
{ name: 'charlotte' },
59+
{ name: 'charlotte' },
60+
],
61+
object: {
62+
id: 1,
63+
name: 'Nader',
64+
},
65+
nullField: null,
3066
},
3167
{
3268
id: 11,
@@ -170,6 +206,14 @@ export type Movies = {
170206
overview?: string
171207
genres?: string[]
172208
release_date?: number // eslint-disable-line
209+
undefinedArray?: [undefined, undefined, undefined]
210+
nullArray?: [null]
211+
objectArray?: Array<{ name: string }>
212+
object?: {
213+
id?: number
214+
name?: string
215+
}
216+
nullField?: null
173217
_highlightResult?: Movies
174218
}
175219

tests/highlight.tests.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,4 +229,55 @@ describe('Highlight Browser test', () => {
229229
expect.stringMatching('<p>S</p>olo')
230230
)
231231
})
232+
233+
test('Test attributes to highlight on non-string-types', async () => {
234+
const response = await searchClient.search<Movies>([
235+
{
236+
indexName: 'movies',
237+
params: {
238+
query: 'Ariel',
239+
attributesToHighlight: ['*'],
240+
},
241+
},
242+
])
243+
const hit = response.results[0].hits[0]._highlightResult
244+
245+
if (hit?.genres) {
246+
expect(hit?.genres[0]?.value).toEqual('Drama')
247+
expect(hit?.genres[1]?.value).toEqual('Crime')
248+
}
249+
if (hit?.id) {
250+
expect(hit?.id.value).toEqual('2')
251+
}
252+
if (hit?.undefinedArray) {
253+
// @ts-ignore
254+
expect(hit?.undefinedArray[0]?.value).toEqual('null')
255+
// @ts-ignore
256+
expect(hit?.undefinedArray[1]?.value).toEqual('null')
257+
}
258+
259+
if (hit?.nullArray) {
260+
// @ts-ignore
261+
expect(hit?.nullArray[0]?.value).toEqual('null')
262+
// @ts-ignore
263+
expect(hit?.nullArray[1]?.value).toEqual('null')
264+
}
265+
266+
if (hit?.objectArray) {
267+
// @ts-ignore
268+
expect(hit?.objectArray[0]?.value).toEqual('{"name":"charlotte"}')
269+
// @ts-ignore
270+
expect(hit?.objectArray[1]?.value).toEqual('{"name":"charlotte"}')
271+
}
272+
273+
if (hit?.object) {
274+
// @ts-ignore
275+
expect(hit?.object?.value).toEqual('{"id":1,"name":"Nader"}')
276+
}
277+
278+
if (hit?.nullField) {
279+
// @ts-ignore
280+
expect(hit?.nullField?.value).toEqual('null')
281+
}
282+
})
232283
})

0 commit comments

Comments
 (0)