Skip to content

Commit e8d3975

Browse files
meili-bors[bot]Haye de Witbidoubiwa
authored
Merge #329
329: Improve settings implementation feature r=bidoubiwa a=bidoubiwa # Pull Request ## What does this PR do? Fixes #327 ## PR checklist Please check if your PR fulfills the following requirements: - [ ] #327 Co-authored-by: Haye de Wit <[email protected]> Co-authored-by: Charlotte Vermandel <[email protected]>
2 parents b7374fd + 4711231 commit e8d3975

File tree

13 files changed

+277
-6
lines changed

13 files changed

+277
-6
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,30 @@ Resulting in `categories` being transformed like this in a `restaurant` entry.
287287

288288
By transforming the `categories` into an array of names, it is now compatible with the [`filtering` feature](https://docs.meilisearch.com/reference/features/filtering_and_faceted_search.html#configuring-filters) in MeiliSearch.
289289

290+
#### 🏗 Add MeiliSearch Settings
290291

292+
Each index in MeiliSearch can be customized with specific settings. It is possible to add your [MeiliSearch settings](https://docs.meilisearch.com/reference/features/settings.html#settings) configuration to the indexes you create using `settings` field in your model's config.
293+
294+
The settings are added when either: adding a collection to MeiliSearch or when updating a collection in MeiliSearch. The settings are not updated when documents are added through the [`listeners`](-apply-hooks).
295+
296+
**For example**
297+
```js
298+
module.exports = {
299+
meilisearch: {
300+
settings: {
301+
filterableAttributes: ['genres'],
302+
distinctAttribute: null,
303+
searchableAttributes: ['title', 'description', 'genres'],
304+
synonyms: {
305+
wolverine: ['xmen', 'logan'],
306+
logan: ['wolverine', 'xmen']
307+
}
308+
}
309+
},
310+
}
311+
```
312+
313+
[See resources](./resources/meilisearch-settings) for more settings examples.
291314

292315
### 🕵️‍♀️ Start Searching <!-- omit in toc -->
293316

connectors/__tests__/custom-index-name.tests.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ const createCollectionConnector = require('../collection')
66
jest.mock('meilisearch')
77

88
const addDocumentsMock = jest.fn(() => 10)
9+
const updateSettingsMock = jest.fn(() => 10)
10+
911
const mockIndex = jest.fn(() => ({
1012
addDocuments: addDocumentsMock,
13+
updateSettings: updateSettingsMock,
1114
}))
1215

1316
MeiliSearch.mockImplementation(() => {
@@ -50,7 +53,6 @@ const loggerMock = {
5053
describe('Test custom index names', () => {
5154
let storeConnector
5255
beforeEach(async () => {
53-
jest.resetAllMocks()
5456
jest.clearAllMocks()
5557
jest.restoreAllMocks()
5658
storeConnector = createStoreConnector({

connectors/__tests__/entry-transformer.tests.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ const createCollectionConnector = require('../collection')
66
jest.mock('meilisearch')
77

88
const addDocumentsMock = jest.fn(() => 10)
9+
const updateSettingsMock = jest.fn(() => 10)
10+
911
const mockIndex = jest.fn(() => ({
1012
addDocuments: addDocumentsMock,
13+
updateSettings: updateSettingsMock,
1114
}))
1215

1316
MeiliSearch.mockImplementation(() => {
@@ -76,7 +79,6 @@ describe('Entry transformation', () => {
7679
})
7780

7881
afterEach(() => {
79-
jest.resetAllMocks()
8082
jest.clearAllMocks()
8183
jest.restoreAllMocks()
8284
})
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
const { MeiliSearch } = require('meilisearch')
2+
const createMeiliSearchConnector = require('../meilisearch')
3+
const createStoreConnector = require('../store')
4+
const createCollectionConnector = require('../collection')
5+
6+
jest.mock('meilisearch')
7+
8+
const addDocumentsMock = jest.fn(() => 10)
9+
const updateSettingsMock = jest.fn(() => 10)
10+
11+
const mockIndex = jest.fn(() => ({
12+
addDocuments: addDocumentsMock,
13+
updateSettings: updateSettingsMock,
14+
}))
15+
16+
MeiliSearch.mockImplementation(() => {
17+
return {
18+
getOrCreateIndex: () => {
19+
return mockIndex
20+
},
21+
index: mockIndex,
22+
}
23+
})
24+
25+
const storeClientMock = {
26+
set: jest.fn(() => 'test'),
27+
get: jest.fn(() => 'test'),
28+
}
29+
30+
const servicesMock = {
31+
restaurant: {
32+
count: jest.fn(() => {
33+
return 11
34+
}),
35+
find: jest.fn(() => {
36+
return [{ id: '1', collection: [{ name: 'one' }, { name: 'two' }] }]
37+
}),
38+
},
39+
}
40+
41+
const transformEntryMock = jest.fn(function ({ entry }) {
42+
const transformedEntry = {
43+
...entry,
44+
collection: entry.collection.map(cat => cat.name),
45+
}
46+
return transformedEntry
47+
})
48+
49+
const loggerMock = {
50+
warn: jest.fn(() => 'test'),
51+
}
52+
53+
describe('Test MeiliSearch settings', () => {
54+
let storeConnector
55+
beforeEach(async () => {
56+
jest.clearAllMocks()
57+
jest.restoreAllMocks()
58+
storeConnector = createStoreConnector({
59+
storeClient: storeClientMock,
60+
})
61+
})
62+
63+
test('Test not settings field in configuration', async () => {
64+
const modelMock = {
65+
restaurant: {
66+
meilisearch: {
67+
indexName: 'my_restaurant',
68+
transformEntry: transformEntryMock,
69+
},
70+
},
71+
}
72+
const collectionConnector = createCollectionConnector({
73+
logger: loggerMock,
74+
models: modelMock,
75+
services: servicesMock,
76+
})
77+
const meilisearchConnector = await createMeiliSearchConnector({
78+
collectionConnector,
79+
storeConnector,
80+
})
81+
const getIndexNameSpy = jest.spyOn(collectionConnector, 'getIndexName')
82+
const getSettingsSpy = jest.spyOn(collectionConnector, 'getSettings')
83+
84+
await meilisearchConnector.addCollectionInMeiliSearch('restaurant')
85+
86+
expect(servicesMock.restaurant.count).toHaveBeenCalledTimes(1)
87+
88+
expect(getIndexNameSpy).toHaveBeenCalledWith('restaurant')
89+
expect(getIndexNameSpy).toHaveReturnedWith('my_restaurant')
90+
expect(getSettingsSpy).toHaveBeenCalledWith('restaurant')
91+
expect(getSettingsSpy).toHaveReturnedWith({})
92+
})
93+
94+
test('Test a empty setting object in configuration', async () => {
95+
const modelMock = {
96+
restaurant: {
97+
meilisearch: {
98+
indexName: 'my_restaurant',
99+
transformEntry: transformEntryMock,
100+
settings: {},
101+
},
102+
},
103+
}
104+
const collectionConnector = createCollectionConnector({
105+
logger: loggerMock,
106+
models: modelMock,
107+
services: servicesMock,
108+
})
109+
const meilisearchConnector = await createMeiliSearchConnector({
110+
collectionConnector,
111+
storeConnector,
112+
})
113+
const getIndexNameSpy = jest.spyOn(collectionConnector, 'getIndexName')
114+
const getSettingsSpy = jest.spyOn(collectionConnector, 'getSettings')
115+
116+
await meilisearchConnector.addCollectionInMeiliSearch('restaurant')
117+
118+
expect(servicesMock.restaurant.count).toHaveBeenCalledTimes(1)
119+
120+
expect(getIndexNameSpy).toHaveBeenCalledWith('restaurant')
121+
expect(getIndexNameSpy).toHaveReturnedWith('my_restaurant')
122+
expect(getSettingsSpy).toHaveBeenCalledWith('restaurant')
123+
expect(getSettingsSpy).toHaveReturnedWith({})
124+
})
125+
126+
test('Test a setting object with one field in configuration', async () => {
127+
const modelMock = {
128+
restaurant: {
129+
meilisearch: {
130+
indexName: 'my_restaurant',
131+
transformEntry: transformEntryMock,
132+
settings: {
133+
searchableAttributes: ['*'],
134+
},
135+
},
136+
},
137+
}
138+
const collectionConnector = createCollectionConnector({
139+
logger: loggerMock,
140+
models: modelMock,
141+
services: servicesMock,
142+
})
143+
const meilisearchConnector = await createMeiliSearchConnector({
144+
collectionConnector,
145+
storeConnector,
146+
})
147+
const getIndexNameSpy = jest.spyOn(collectionConnector, 'getIndexName')
148+
const getSettingsSpy = jest.spyOn(collectionConnector, 'getSettings')
149+
150+
await meilisearchConnector.addCollectionInMeiliSearch('restaurant')
151+
152+
expect(servicesMock.restaurant.count).toHaveBeenCalledTimes(1)
153+
154+
expect(getIndexNameSpy).toHaveBeenCalledWith('restaurant')
155+
expect(getIndexNameSpy).toHaveReturnedWith('my_restaurant')
156+
expect(getSettingsSpy).toHaveBeenCalledWith('restaurant')
157+
expect(getSettingsSpy).toHaveReturnedWith({ searchableAttributes: ['*'] })
158+
})
159+
})

connectors/collection.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,5 +152,26 @@ module.exports = ({ services, models, logger }) => {
152152
}
153153
return entries
154154
},
155+
156+
/**
157+
* Returns MeiliSearch index settings from model definition.
158+
* @param collection - Name of the Collection.
159+
* @typedef Settings
160+
* @type {import('meilisearch').Settings}
161+
* @return {Settings} - MeiliSearch index settings
162+
*/
163+
getSettings: function (collection) {
164+
const model = models[collection].meilisearch || {}
165+
const settings = model.settings || {}
166+
167+
if (typeof settings !== 'object') {
168+
logger.warn(
169+
`[MEILISEARCH]: "settings" provided in the model of the ${collection} must be an object.`
170+
)
171+
return {}
172+
}
173+
174+
return settings
175+
},
155176
}
156177
}

connectors/meilisearch/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,10 @@ module.exports = async ({ storeConnector, collectionConnector }) => {
264264
const client = MeiliSearch({ apiKey, host })
265265
const indexUid = collectionConnector.getIndexName(collection)
266266

267+
// Get MeiliSearch Index settings from model
268+
const settings = collectionConnector.getSettings(collection)
269+
await client.index(indexUid).updateSettings(settings)
270+
267271
// Callback function for batching action
268272
const addDocuments = async (entries, collection) => {
269273
if (entries.length === 0) {

cypress/integration/ui_spec.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ describe('Strapi Login flow', () => {
5858
})
5959

6060
it('Add credentials', () => {
61+
cy.removeNotifications()
6162
cy.get('input[name="MSHost"]').clear().type(host)
6263
cy.get('input[name="MSApiKey"]').clear().type(apiKey)
6364
cy.get('.credentials_button').click()
@@ -98,6 +99,7 @@ describe('Strapi Login flow', () => {
9899
})
99100
cy.contains('Reload needed', { timeout: 10000 })
100101
cy.reloadServer()
102+
cy.removeNotifications()
101103
})
102104

103105
it('Check for successfull listened in develop mode', () => {
@@ -205,6 +207,7 @@ describe('Strapi Login flow', () => {
205207
cy.contains('Reload needed', { timeout: 10000 })
206208
}
207209
cy.reloadServer()
210+
cy.removeNotifications()
208211
})
209212

210213
it('Check that collections are not in MeiliSearch anymore', () => {

playground/api/restaurant/models/restaurant.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ module.exports = {
1313
};
1414
return transformed;
1515
},
16-
indexName: "my_restaurant"
16+
indexName: "my_restaurant",
17+
settings: {
18+
"searchableAttributes": ["*"]
19+
}
1720
}
1821
}

resources/entries-transformers/dates-transformer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ function dateToTimeStamp(date) {
1313

1414
module.exports = {
1515
meilisearch: {
16-
transformEntry(entry) {
16+
transformEntry({ entry }) {
1717
const transformedEntry = {
1818
...entry,
1919
// transform date format to timestamp

resources/entries-transformers/filter-compatibility.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
module.exports = {
99
meilisearch: {
10-
transformEntry(entry) {
10+
transformEntry({ entry }) {
1111
const transformedEntry = {
1212
...entry,
1313
categories: entry.categories.map(cat => cat.name), // map to only have categories name

0 commit comments

Comments
 (0)