Skip to content

Commit bc73484

Browse files
committed
Add disjunctive facet search
Add condition on filtering caching Add multi index search Add datasets in tests assets Improve filters Improve readme Update src/client/instant-meilisearch-client.ts Co-authored-by: Morgane Dubus <[email protected]> fix merge conflicts Remove console logs from client Remove comments Add tests on disjunctive facet search remove console log Update playground Add logging on react setup Improve README Roll back .gitignore Fix readme errors Fix refinement list typo error Fix end to end tests Fix linting error Update selectors in cypress to improve tests Add json module resolver for tests Add a wait in cypress method resolving to fast Remove useless wait in search-ui specs
1 parent 92eb414 commit bc73484

File tree

21 files changed

+1441
-416
lines changed

21 files changed

+1441
-416
lines changed

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ List of all the components that are available in [instantSearch](https://github.
270270
### Table Of Widgets
271271

272272
-[InstantSearch](#-instantsearch)
273-
- [index](#-index)
273+
- [index](#-index)
274274
-[SearchBox](#-searchbox)
275275
-[Configure](#-configure)
276276
-[ConfigureRelatedItems](#-configure-related-items)
@@ -339,15 +339,13 @@ const search = instantsearch({
339339
})
340340
```
341341

342-
### Index
342+
### Index
343343

344344
[Index references](https://www.algolia.com/doc/api-reference/widgets/index-widget/js/)
345345

346346
`Index` is the component that lets you apply widgets to a dedicated index. It’s useful if you want to build an interface that targets multiple indices.
347347

348-
Not compatible as Meilisearch does not support federated search on multiple indexes.
349-
350-
If you'd like to see federated search implemented please vote for it in the [roadmap](https://roadmap.meilisearch.com/c/74-multi-index-search?utm_medium=social&utm_source=portal_share).
348+
Using this component, instant-meilisearch does an http-request for each different `Index` widget added. More http requests are made when using the [`RefinementList`](#✅-refinementlist) widget.
351349

352350
### ✅ SearchBox
353351

@@ -672,6 +670,9 @@ The `refinementList` widget is one of the most common widgets you can find in a
672670
- ✅ templates: The templates to use for the widget.
673671
- ✅ cssClasses: The CSS classes to override.
674672

673+
The `RefinementList` widget uses the `disjunctive facet search` principle when using the `or` operator. For each different facet category used, an additional http call is made.
674+
For example, if I ask for `color=green` and `size=2`, three http requests are made. One for the hits, one for the `color` distribution and one for the `size` distribution. To provide any feedback on the subject, refer to [this discussion](https://github.com/meilisearch/product/issues/54).
675+
675676
The following example will create a UI component with the a list of genres on which you will be able to facet.
676677

677678
```js

cypress/integration/search-ui.spec.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,26 @@ describe(`${playground} playground test`, () => {
3838

3939
it('Sort by recommendationCound ascending', () => {
4040
const select = `.ais-SortBy-select`
41-
cy.get(select).select('steam-video-games:recommendationCount:asc')
41+
cy.get(select).select('games:recommendationCount:asc')
4242
cy.wait(1000)
4343
cy.get(HIT_ITEM_CLASS).eq(0).contains('Deathmatch Classic')
4444
})
4545

4646
it('Sort by default relevancy', () => {
4747
const select = `.ais-SortBy-select`
48-
cy.get(select).select('steam-video-games')
48+
cy.get(select).select('games')
4949
cy.wait(1000)
5050
cy.get(HIT_ITEM_CLASS).eq(0).contains('Counter-Strike')
5151
})
5252

53-
it('click on facets', () => {
54-
const checkbox = `.ais-RefinementList-list .ais-RefinementList-checkbox`
55-
cy.get(checkbox).eq(1).click()
53+
it('click on facets ensure disjunctive facet search', () => {
54+
const facet = `.ais-RefinementList-list`
55+
const checkbox = `.ais-RefinementList-checkbox`
56+
const facetCount = '.ais-RefinementList-count'
57+
cy.get(facet).eq(0).find(checkbox).eq(1).click() // genres > action
5658
cy.wait(1000)
57-
cy.get(HIT_ITEM_CLASS).eq(1).contains('Team Fortress Classic')
58-
cy.get(HIT_ITEM_CLASS).eq(1).contains('4.99 $')
59+
cy.get(facet).eq(0).find(facetCount).eq(0).contains('5') // genres > action count
60+
cy.get(facet).eq(1).find(facetCount).eq(0).contains('4') // players > multiplayer
5961
})
6062

6163
it('Search', () => {
@@ -78,6 +80,6 @@ describe(`${playground} playground test`, () => {
7880

7981
it('Paginate Search', () => {
8082
cy.get('.ais-InfiniteHits-loadMore').click()
81-
cy.get(HIT_ITEM_CLASS).should('have.length', 11)
83+
cy.get(HIT_ITEM_CLASS).should('have.length', 12)
8284
})
8385
})

jest.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ module.exports = {
3232
displayName: 'dom',
3333
testPathIgnorePatterns: [...ignoreFiles, '<rootDir>/tests/build*'],
3434
testMatch: ['**/*.tests.ts', '/tests/**/*.ts'],
35+
setupFilesAfterEnv: ['<rootDir>/scripts/jest_teardown.js'],
3536
},
3637
{
3738
globals: {
@@ -44,6 +45,7 @@ module.exports = {
4445
testEnvironment: 'node',
4546
testPathIgnorePatterns: [...ignoreFiles],
4647
testMatch: ['**/*.tests.ts', '/tests/**/*.ts'],
48+
setupFilesAfterEnv: ['<rootDir>/scripts/jest_teardown.js'],
4749
},
4850
],
4951
}

scripts/jest_teardown.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const { MeiliSearch } = require('meilisearch')
2+
3+
const HOST = 'http://localhost:7700'
4+
const API_KEY = 'masterKey'
5+
6+
afterAll(async () => {
7+
const client = new MeiliSearch({ host: HOST, apiKey: API_KEY })
8+
await client.deleteIndex('movies')
9+
const task = await client.deleteIndex('games')
10+
11+
await client.waitForTask(task.taskUid)
12+
})

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,10 @@ export function MeiliParamsCreator(searchContext: SearchContext) {
7676
return meiliSearchParams
7777
},
7878
addFacets() {
79-
if (facets?.length) {
79+
if (Array.isArray(facets)) {
8080
meiliSearchParams.facets = facets
81+
} else if (typeof facets === 'string') {
82+
meiliSearchParams.facets = [facets]
8183
}
8284
},
8385
addAttributesToCrop() {

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,20 @@ export function SearchResolver(
3939
// Check if specific request is already cached with its associated search response.
4040
if (cachedResponse) return cachedResponse
4141

42-
const cachedFacets = extractFacets(searchContext, searchParams)
43-
4442
// Make search request
4543
const searchResponse = await client
4644
.index(searchContext.indexUid)
4745
.search(searchContext.query, searchParams)
4846

49-
// Add missing facets back into facetDistribution
50-
searchResponse.facetDistribution = addMissingFacets(
51-
cachedFacets,
52-
searchResponse.facetDistribution
53-
)
47+
if (searchContext.keepZeroFacets) {
48+
const cachedFacets = extractFacets(searchContext, searchParams)
49+
50+
// Add missing facets back into facetDistribution
51+
searchResponse.facetDistribution = addMissingFacets(
52+
cachedFacets,
53+
searchResponse.facetDistribution
54+
)
55+
}
5456

5557
// query can be: empty string, undefined or null
5658
// all of them are falsy's

src/client/instant-meilisearch-client.ts

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -67,40 +67,49 @@ export function instantMeiliSearch(
6767
instantSearchRequests: readonly AlgoliaMultipleQueriesQuery[]
6868
): Promise<{ results: Array<AlgoliaSearchResponse<T>> }> {
6969
try {
70-
const searchRequest = instantSearchRequests[0]
71-
const searchContext: SearchContext = createSearchContext(
72-
searchRequest,
73-
instantMeiliSearchOptions,
74-
defaultFacetDistribution
75-
)
70+
const searchResponses: { results: Array<AlgoliaSearchResponse<T>> } = {
71+
results: [],
72+
}
7673

77-
// Adapt search request to Meilisearch compliant search request
78-
const adaptedSearchRequest = adaptSearchParams(searchContext)
74+
const requests = instantSearchRequests
7975

80-
// Cache first facets distribution of the instantMeilisearch instance
81-
// Needed to add in the facetDistribution the fields that were not returned
82-
// When the user sets `keepZeroFacets` to true.
83-
if (defaultFacetDistribution === undefined) {
84-
defaultFacetDistribution = await cacheFirstFacetDistribution(
85-
searchResolver,
86-
searchContext
76+
for (const searchRequest of requests) {
77+
const searchContext: SearchContext = createSearchContext(
78+
searchRequest,
79+
instantMeiliSearchOptions,
80+
defaultFacetDistribution
8781
)
88-
searchContext.defaultFacetDistribution = defaultFacetDistribution
89-
}
9082

91-
// Search response from Meilisearch
92-
const searchResponse = await searchResolver.searchResponse(
93-
searchContext,
94-
adaptedSearchRequest
95-
)
83+
// Adapt search request to Meilisearch compliant search request
84+
const adaptedSearchRequest = adaptSearchParams(searchContext)
85+
86+
// Cache first facets distribution of the instantMeilisearch instance
87+
// Needed to add in the facetDistribution the fields that were not returned
88+
// When the user sets `keepZeroFacets` to true.
89+
if (defaultFacetDistribution === undefined) {
90+
defaultFacetDistribution = await cacheFirstFacetDistribution(
91+
searchResolver,
92+
searchContext
93+
)
94+
searchContext.defaultFacetDistribution = defaultFacetDistribution
95+
}
96+
97+
// Search response from Meilisearch
98+
const searchResponse = await searchResolver.searchResponse(
99+
searchContext,
100+
adaptedSearchRequest
101+
)
96102

97-
// Adapt the Meilisearch responsne to a compliant instantsearch.js response
98-
const adaptedSearchResponse = adaptSearchResponse<T>(
99-
searchResponse,
100-
searchContext
101-
)
103+
// Adapt the Meilisearch response to a compliant instantsearch.js response
104+
const adaptedSearchResponse = adaptSearchResponse<T>(
105+
searchResponse,
106+
searchContext
107+
)
108+
109+
searchResponses.results.push(adaptedSearchResponse.results[0])
110+
}
102111

103-
return adaptedSearchResponse
112+
return searchResponses
104113
} catch (e: any) {
105114
console.error(e)
106115
throw new Error(e)

0 commit comments

Comments
 (0)