Skip to content

Commit fe107bb

Browse files
bors[bot]bidoubiwa
andauthored
Merge #378
378: Change instantMeiliSearch parameters r=bidoubiwa a=bidoubiwa fixes: #379 fixes: #380 # Summary Change `instantMeiliSearch` to make integration tests not dependent on creating an instance of MeiliSearch. Thus making integrations possible. Refactor of files to create an environment for easy testing but also for a very maintainable code. ## Breaking changes This PR breaks the repository in two different ways: ### 1. `client` object inside instantMeiliSearch instance is now called `MeiliSearchClient` Probably the only important breaking change that could potentially affect some users. Naming had to change as it was very confusing for users ```js const searchClient = instantMeiliSearch( 'https://demos.meilisearch.com', 'dc3fedaf922de8937fdea01f0a7d59557f1fd31832cb8440ce94231cfdde7f25', { paginationTotalHits: 60, primaryKey: 'id', } ) ``` to access MeiliSearch client this is the way: Now: ```js searchClient.client.MeiliSearchClient ``` Before: ```js searchClient.client.client ``` ### 2. the `instantMeiliSearch` class instance has less parameters than before: Before: ```js export type InstantMeiliSearchInstance = { page: number paginationTotalHits: number hitsPerPage: number primaryKey: string | undefined client: MStypes.MeiliSearch placeholderSearch: boolean transformToISResponse: ( indexUid: string, meiliSearchResponse: MStypes.SearchResponse<any, any>, instantSearchParams: ISSearchParams ) => { results: SearchResponse[] } transformToMeiliSearchParams: ( instantSearchParams: ISSearchParams ) => Record<string, any> transformToISHits: ( meiliSearchHits: Array<Record<string, any>>, instantSearchParams: ISSearchParams ) => ISHits[] getNumberPages: (hitsLength: number) => number paginateHits: ( meiliSearchHits: Array<Record<string, any>> ) => Array<Record<string, any>> search: ( requests: IStypes.SearchRequest[] ) => Promise<{ results: SearchResponse[] }> } ``` after ```js export type InstantMeiliSearchInstance = { MeiliSearchClient: MStypes.MeiliSearch search: ( requests: IStypes.SearchRequest[] ) => Promise<{ results: SearchResponse[] }> } ``` This refactor was done to make testing independent and not need a instantMeiliSearch instance context to be usable. Before that, we wouldn't have been able to perform integration tests. Only e2e tests. The other reason is that we want to have a searchClient as close to what `instantSearch` is requesting which is the following: ```js export declare type SearchClient = { addAlgoliaAgent?: (agent: string) => void; search: (requests: SearchRequest[]) => Promise<{ results: SearchResponse[]; }>; searchForFacetValues?: (requests: SearchForFacetValuesRequest[]) => Promise<{ facetHits: SearchForFacetValuesResponse[]; }[]>; }; ``` 🚨 It is probably not going to affect any user as it has no purpose for users. ### 3. MeiliSearch types and InstantSearch types are not exported anymore These types were exported because I needed them in other files. But this is not a good way to do it since typing should happen in types files and not a bit in every other file. 🚨 It's probably not going to affect any user as it has no purpose for users. Co-authored-by: Charlotte Vermandel <[email protected]> Co-authored-by: cvermand <[email protected]>
2 parents b7ae607 + 3b8376a commit fe107bb

File tree

17 files changed

+346
-265
lines changed

17 files changed

+346
-265
lines changed

rollup.config.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ function getOutputFileName(fileName, isProd = false) {
1313

1414
const env = process.env.NODE_ENV || 'development'
1515
const ROOT = resolve(__dirname, '.')
16+
const INPUT = 'src/index.ts'
1617

1718
const PLUGINS = [
1819
typescript({
@@ -27,7 +28,7 @@ const PLUGINS = [
2728
module.exports = [
2829
// browser-friendly IIFE build
2930
{
30-
input: 'src/index.ts', // directory to transpilation of typescript
31+
input: INPUT, // directory to transpilation of typescript
3132
output: {
3233
name: 'window',
3334
extend: true,
@@ -56,7 +57,7 @@ module.exports = [
5657
],
5758
},
5859
{
59-
input: 'src/index.ts',
60+
input: INPUT,
6061
external: ['meilisearch'],
6162
output: [
6263
{

src/client/index.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { MeiliSearch } from 'meilisearch'
2+
import { InstantMeiliSearchOptions, InstantMeiliSearchInstance } from '../types'
3+
4+
import {
5+
transformToMeiliSearchParams,
6+
transformToISResponse,
7+
} from '../transformers'
8+
9+
export function instantMeiliSearch(
10+
hostUrl: string,
11+
apiKey: string,
12+
options: InstantMeiliSearchOptions = {}
13+
): InstantMeiliSearchInstance {
14+
return {
15+
MeiliSearchClient: new MeiliSearch({ host: hostUrl, apiKey: apiKey }),
16+
search: async function ([isSearchRequest]) {
17+
try {
18+
// Params got from InstantSearch
19+
const {
20+
params: instantSearchParams,
21+
indexName: indexUid,
22+
} = isSearchRequest
23+
24+
const { paginationTotalHits, primaryKey, placeholderSearch } = options
25+
const { page, hitsPerPage } = instantSearchParams
26+
const client = this.MeiliSearchClient
27+
const context = {
28+
client,
29+
paginationTotalHits: paginationTotalHits || 200,
30+
primaryKey: primaryKey || undefined,
31+
placeholderSearch: placeholderSearch !== false, // true by default
32+
hitsPerPage: hitsPerPage || 20, // 20 is the MeiliSearch's default limit value. `hitsPerPage` can be changed with `InsantSearch.configure`.
33+
page: page || 0, // default page is 0 if none is provided
34+
}
35+
36+
// Transform IS params to MeiliSearch params
37+
const msSearchParams = transformToMeiliSearchParams(
38+
instantSearchParams,
39+
context
40+
)
41+
42+
// Executes the search with MeiliSearch
43+
const searchResponse = await client
44+
.index(indexUid)
45+
.search(msSearchParams.q, msSearchParams)
46+
47+
// Parses the MeiliSearch response and returns it for InstantSearch
48+
return transformToISResponse(
49+
indexUid,
50+
searchResponse,
51+
instantSearchParams,
52+
context
53+
)
54+
} catch (e) {
55+
console.error(e)
56+
throw new Error(e)
57+
}
58+
},
59+
}
60+
}

src/index.ts

Lines changed: 3 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,162 +1,3 @@
1-
import { MeiliSearch } from 'meilisearch'
2-
import { createHighlighResult, createSnippetResult } from './format'
3-
import {
4-
InstantMeiliSearchOptions,
5-
InstantMeiliSearchInstance,
6-
InstantSearchTypes,
7-
} from './types'
8-
9-
export function instantMeiliSearch(
10-
hostUrl: string,
11-
apiKey: string,
12-
options: InstantMeiliSearchOptions = {}
13-
): InstantMeiliSearchInstance {
14-
return {
15-
client: new MeiliSearch({ host: hostUrl, apiKey: apiKey }),
16-
paginationTotalHits: options.paginationTotalHits || 200,
17-
primaryKey: options.primaryKey || undefined,
18-
placeholderSearch: options.placeholderSearch !== false, // true by default
19-
hitsPerPage: 20,
20-
page: 0,
21-
22-
/*
23-
REQUEST
24-
*/
25-
26-
transformToMeiliSearchParams: function ({
27-
query,
28-
facets,
29-
facetFilters,
30-
attributesToSnippet: attributesToCrop,
31-
attributesToRetrieve,
32-
attributesToHighlight,
33-
filters = '',
34-
numericFilters = [],
35-
}) {
36-
const limit = this.paginationTotalHits
37-
38-
const filter = [numericFilters.join(' AND '), filters.trim()]
39-
.filter((x) => x)
40-
.join(' AND ')
41-
.trim()
42-
43-
// Creates search params object compliant with MeiliSearch
44-
return {
45-
q: query,
46-
...(facets?.length && { facetsDistribution: facets }),
47-
...(facetFilters && { facetFilters }),
48-
...(attributesToCrop && { attributesToCrop }),
49-
...(attributesToRetrieve && { attributesToRetrieve }),
50-
...(filter && { filters: filter }),
51-
attributesToHighlight: attributesToHighlight || ['*'],
52-
limit: (!this.placeholderSearch && query === '') || !limit ? 0 : limit,
53-
}
54-
},
55-
56-
/*
57-
RESPONSE
58-
*/
59-
60-
getNumberPages: function (hitsLength) {
61-
const adjust = hitsLength % this.hitsPerPage! === 0 ? 0 : 1
62-
return Math.floor(hitsLength / this.hitsPerPage!) + adjust // total number of pages
63-
},
64-
65-
paginateHits: function (hits) {
66-
const start = this.page * this.hitsPerPage!
67-
return hits.splice(start, this.hitsPerPage)
68-
},
69-
70-
transformToISHits: function (meiliSearchHits, instantSearchParams) {
71-
const paginatedHits = this.paginateHits(meiliSearchHits)
72-
73-
return paginatedHits.map((hit: Record<string, any>) => {
74-
const { _formatted: formattedHit, ...restOfHit } = hit
75-
76-
// Creates Hit object compliant with InstantSearch
77-
return {
78-
...restOfHit,
79-
_highlightResult: createHighlighResult({
80-
formattedHit,
81-
...instantSearchParams,
82-
}),
83-
_snippetResult: createSnippetResult({
84-
formattedHit,
85-
...instantSearchParams,
86-
}),
87-
...(this.primaryKey && { objectID: hit[this.primaryKey] }),
88-
}
89-
})
90-
},
91-
92-
transformToISResponse: function (
93-
indexUid,
94-
{
95-
exhaustiveFacetsCount,
96-
exhaustiveNbHits,
97-
facetsDistribution: facets,
98-
nbHits,
99-
processingTimeMs,
100-
query,
101-
hits,
102-
},
103-
instantSearchParams
104-
) {
105-
// Create response object compliant with InstantSearch
106-
const ISResponse = {
107-
index: indexUid,
108-
hitsPerPage: this.hitsPerPage,
109-
...(facets && { facets }),
110-
...(exhaustiveFacetsCount && { exhaustiveFacetsCount }),
111-
page: this.page,
112-
nbPages: this.getNumberPages(hits.length),
113-
exhaustiveNbHits,
114-
nbHits,
115-
processingTimeMS: processingTimeMs,
116-
query,
117-
hits: this.transformToISHits(hits, instantSearchParams),
118-
}
119-
return {
120-
results: [ISResponse],
121-
}
122-
},
123-
124-
/*
125-
SEARCH
126-
*/
127-
search: async function ([
128-
isSearchRequest,
129-
]: InstantSearchTypes.SearchRequest[]) {
130-
try {
131-
// Params got from InstantSearch
132-
const {
133-
params: instantSearchParams,
134-
indexName: indexUid,
135-
} = isSearchRequest
136-
const { page, hitsPerPage } = instantSearchParams
137-
138-
this.page = page || 0 // default page is 0 if none is provided
139-
this.hitsPerPage = hitsPerPage || 20 // 20 is the MeiliSearch's default limit value. `hitsPerPage` can be changed with `InsantSearch.configure`.
140-
141-
// Transform IS params to MeiliSearch params
142-
const msSearchParams = this.transformToMeiliSearchParams(
143-
instantSearchParams
144-
)
145-
// Executes the search with MeiliSearch
146-
const searchResponse = await this.client
147-
.index(indexUid)
148-
.search(msSearchParams.q, msSearchParams)
149-
150-
// Parses the MeiliSearch response and returns it for InstantSearch
151-
return this.transformToISResponse(
152-
indexUid,
153-
searchResponse,
154-
instantSearchParams
155-
)
156-
} catch (e) {
157-
console.error(e)
158-
throw new Error(e)
159-
}
160-
},
161-
}
162-
}
1+
export * from './client'
2+
export * from './transformers'
3+
export * from './types'

src/transformers/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from './to-meilisearch-params'
2+
export * from './to-instantsearch-response'
3+
export * from './to-instantsearch-hits'
4+
export * from './to-instantsearch-highlight'
5+
export * from './pagination'

src/transformers/pagination.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { PaginateHits, GetNumberPages } from '../types'
2+
3+
export const getNumberPages: GetNumberPages = function (
4+
hitsLength,
5+
{ hitsPerPage }
6+
) {
7+
const adjust = hitsLength % hitsPerPage! === 0 ? 0 : 1
8+
return Math.floor(hitsLength / hitsPerPage!) + adjust // total number of pages
9+
}
10+
11+
export const paginateHits: PaginateHits = function (
12+
hits,
13+
{ page, hitsPerPage }
14+
) {
15+
const start = page * hitsPerPage!
16+
return hits.splice(start, hitsPerPage)
17+
}

src/format.ts renamed to src/transformers/to-instantsearch-highlight.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
import { ISSearchParams } from './types'
2-
import { isString } from './utils'
1+
import {
2+
CreateHighlighResult,
3+
ReplaceHighlightTags,
4+
SnippetValue,
5+
CreateSnippetResult,
6+
isString,
7+
} from '../types'
38

4-
function replaceHighlightTags(
9+
export const replaceHighlightTags: ReplaceHighlightTags = (
510
value: string,
611
highlightPreTag?: string,
712
highlightPostTag?: string
8-
): string {
13+
): string => {
914
// Value has to be a string to have highlight.
1015
// Highlight is applied by MeiliSearch (<em> tags)
1116
// We replace the <em> by the expected tag for InstantSearch
@@ -20,11 +25,11 @@ function replaceHighlightTags(
2025
return JSON.stringify(value)
2126
}
2227

23-
function createHighlighResult<T extends Record<string, any>>({
28+
export const createHighlighResult: CreateHighlighResult = ({
2429
formattedHit,
2530
highlightPreTag,
2631
highlightPostTag,
27-
}: { formattedHit: T } & ISSearchParams) {
32+
}) => {
2833
// formattedHit is the `_formatted` object returned by MeiliSearch.
2934
// It contains all the highlighted and croped attributes
3035
return Object.keys(formattedHit).reduce((result, key) => {
@@ -36,15 +41,15 @@ function createHighlighResult<T extends Record<string, any>>({
3641
),
3742
}
3843
return result
39-
}, {} as T)
44+
}, {} as any)
4045
}
4146

42-
function snippetFinalValue(
47+
export const snippetValue: SnippetValue = (
4348
value: string,
4449
snippetEllipsisText?: string,
4550
highlightPreTag?: string,
4651
highlightPostTag?: string
47-
) {
52+
) => {
4853
let newValue = value
4954
// manage a kind of `...` for the crop until this issue is solved: https://github.com/meilisearch/MeiliSearch/issues/923
5055
// `...` is put if we are at the middle of a sentence (instead at the middle of the document field)
@@ -63,28 +68,25 @@ function snippetFinalValue(
6368
return replaceHighlightTags(newValue, highlightPreTag, highlightPostTag)
6469
}
6570

66-
function createSnippetResult<
67-
T extends Record<string, any>,
68-
K extends keyof T & string
69-
>({
71+
export const createSnippetResult: CreateSnippetResult = ({
7072
formattedHit,
7173
attributesToSnippet,
7274
snippetEllipsisText,
7375
highlightPreTag,
7476
highlightPostTag,
75-
}: { formattedHit: T } & ISSearchParams) {
77+
}) => {
7678
if (attributesToSnippet === undefined) {
7779
return null
7880
}
7981
attributesToSnippet = attributesToSnippet.map(
8082
(attribute) => attribute.split(':')[0]
81-
) as K[]
83+
) as any[]
8284
// formattedHit is the `_formatted` object returned by MeiliSearch.
8385
// It contains all the highlighted and croped attributes
84-
return (Object.keys(formattedHit) as K[]).reduce((result, key) => {
86+
return (Object.keys(formattedHit) as any[]).reduce((result, key) => {
8587
if (attributesToSnippet!.includes(key)) {
8688
;(result[key] as any) = {
87-
value: snippetFinalValue(
89+
value: snippetValue(
8890
formattedHit[key],
8991
snippetEllipsisText,
9092
highlightPreTag,
@@ -93,7 +95,5 @@ function createSnippetResult<
9395
}
9496
}
9597
return result
96-
}, {} as T)
98+
}, {} as any)
9799
}
98-
99-
export { createHighlighResult, createSnippetResult }

0 commit comments

Comments
 (0)