Skip to content

Commit 170fed7

Browse files
Merge #888
888: Add multi index and disjunctive facet search r=bidoubiwa a=bidoubiwa ⚠️ NOT working with `keepzerofacet` Using the `Index` widget, it is now possible to do mult-index search, see example bellow. By introducing the multi index search. we automatically introduce disjunctive facet search. Related to: #774 #789 # Multi index search ```jsx <InstantSearch indexName="movies" searchClient={searchClient}> <SearchBox /> <h2>Movies</h2> <Index indexName="movies"> <Hits hitComponent={Hit} /> </Index> <h2>Games</h2> <Index indexName="games"> <Hits hitComponent={Hit} /> </Index> </InstantSearch> ``` https://user-images.githubusercontent.com/33010418/203822998-8fc99a62-970f-42a9-95bb-ab09d3c67a9d.mp4 ## Disjunctive facet search https://user-images.githubusercontent.com/33010418/204564207-576bca0c-344b-427c-a1a1-e10ddd7a677f.mp4 # Multi index + disjunctive facet search https://user-images.githubusercontent.com/33010418/204565010-d4564fdb-88a0-46ad-8dc0-bb6761798e49.mp4 ## TODO - [x] Should work on simple search - [x] Should be compatible with following widgets: - [x] Pagination - [x] RefinementList - [x] Should be compatible with all Meilisearch parameters - [x] placeholder search - [x] finite pagination - [ ] keep zero facets ⚠️ Fixed in disjunctive facet search PR - [x] check state of cache Co-authored-by: Charlotte Vermandel <[email protected]> Co-authored-by: cvermand <[email protected]>
2 parents 92eb414 + a6ab3fa commit 170fed7

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)