Skip to content

Commit

Permalink
Add disjunctive facet search
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bidoubiwa committed Dec 8, 2022
1 parent 92eb414 commit bc73484
Show file tree
Hide file tree
Showing 21 changed files with 1,441 additions and 416 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ List of all the components that are available in [instantSearch](https://github.
### Table Of Widgets

-[InstantSearch](#-instantsearch)
- [index](#-index)
- [index](#-index)
-[SearchBox](#-searchbox)
-[Configure](#-configure)
-[ConfigureRelatedItems](#-configure-related-items)
Expand Down Expand Up @@ -339,15 +339,13 @@ const search = instantsearch({
})
```

### Index
### Index

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

`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.

Not compatible as Meilisearch does not support federated search on multiple indexes.

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).
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.

### ✅ SearchBox

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

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.
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).

The following example will create a UI component with the a list of genres on which you will be able to facet.

```js
Expand Down
18 changes: 10 additions & 8 deletions cypress/integration/search-ui.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,26 @@ describe(`${playground} playground test`, () => {

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

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

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

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

it('Paginate Search', () => {
cy.get('.ais-InfiniteHits-loadMore').click()
cy.get(HIT_ITEM_CLASS).should('have.length', 11)
cy.get(HIT_ITEM_CLASS).should('have.length', 12)
})
})
2 changes: 2 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ module.exports = {
displayName: 'dom',
testPathIgnorePatterns: [...ignoreFiles, '<rootDir>/tests/build*'],
testMatch: ['**/*.tests.ts', '/tests/**/*.ts'],
setupFilesAfterEnv: ['<rootDir>/scripts/jest_teardown.js'],
},
{
globals: {
Expand All @@ -44,6 +45,7 @@ module.exports = {
testEnvironment: 'node',
testPathIgnorePatterns: [...ignoreFiles],
testMatch: ['**/*.tests.ts', '/tests/**/*.ts'],
setupFilesAfterEnv: ['<rootDir>/scripts/jest_teardown.js'],
},
],
}
12 changes: 12 additions & 0 deletions scripts/jest_teardown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const { MeiliSearch } = require('meilisearch')

const HOST = 'http://localhost:7700'
const API_KEY = 'masterKey'

afterAll(async () => {
const client = new MeiliSearch({ host: HOST, apiKey: API_KEY })
await client.deleteIndex('movies')
const task = await client.deleteIndex('games')

await client.waitForTask(task.taskUid)
})
4 changes: 3 additions & 1 deletion src/adapter/search-request-adapter/search-params-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ export function MeiliParamsCreator(searchContext: SearchContext) {
return meiliSearchParams
},
addFacets() {
if (facets?.length) {
if (Array.isArray(facets)) {
meiliSearchParams.facets = facets
} else if (typeof facets === 'string') {
meiliSearchParams.facets = [facets]
}
},
addAttributesToCrop() {
Expand Down
16 changes: 9 additions & 7 deletions src/adapter/search-request-adapter/search-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,20 @@ export function SearchResolver(
// Check if specific request is already cached with its associated search response.
if (cachedResponse) return cachedResponse

const cachedFacets = extractFacets(searchContext, searchParams)

// Make search request
const searchResponse = await client
.index(searchContext.indexUid)
.search(searchContext.query, searchParams)

// Add missing facets back into facetDistribution
searchResponse.facetDistribution = addMissingFacets(
cachedFacets,
searchResponse.facetDistribution
)
if (searchContext.keepZeroFacets) {
const cachedFacets = extractFacets(searchContext, searchParams)

// Add missing facets back into facetDistribution
searchResponse.facetDistribution = addMissingFacets(
cachedFacets,
searchResponse.facetDistribution
)
}

// query can be: empty string, undefined or null
// all of them are falsy's
Expand Down
65 changes: 37 additions & 28 deletions src/client/instant-meilisearch-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,40 +67,49 @@ export function instantMeiliSearch(
instantSearchRequests: readonly AlgoliaMultipleQueriesQuery[]
): Promise<{ results: Array<AlgoliaSearchResponse<T>> }> {
try {
const searchRequest = instantSearchRequests[0]
const searchContext: SearchContext = createSearchContext(
searchRequest,
instantMeiliSearchOptions,
defaultFacetDistribution
)
const searchResponses: { results: Array<AlgoliaSearchResponse<T>> } = {
results: [],
}

// Adapt search request to Meilisearch compliant search request
const adaptedSearchRequest = adaptSearchParams(searchContext)
const requests = instantSearchRequests

// Cache first facets distribution of the instantMeilisearch instance
// Needed to add in the facetDistribution the fields that were not returned
// When the user sets `keepZeroFacets` to true.
if (defaultFacetDistribution === undefined) {
defaultFacetDistribution = await cacheFirstFacetDistribution(
searchResolver,
searchContext
for (const searchRequest of requests) {
const searchContext: SearchContext = createSearchContext(
searchRequest,
instantMeiliSearchOptions,
defaultFacetDistribution
)
searchContext.defaultFacetDistribution = defaultFacetDistribution
}

// Search response from Meilisearch
const searchResponse = await searchResolver.searchResponse(
searchContext,
adaptedSearchRequest
)
// Adapt search request to Meilisearch compliant search request
const adaptedSearchRequest = adaptSearchParams(searchContext)

// Cache first facets distribution of the instantMeilisearch instance
// Needed to add in the facetDistribution the fields that were not returned
// When the user sets `keepZeroFacets` to true.
if (defaultFacetDistribution === undefined) {
defaultFacetDistribution = await cacheFirstFacetDistribution(
searchResolver,
searchContext
)
searchContext.defaultFacetDistribution = defaultFacetDistribution
}

// Search response from Meilisearch
const searchResponse = await searchResolver.searchResponse(
searchContext,
adaptedSearchRequest
)

// Adapt the Meilisearch responsne to a compliant instantsearch.js response
const adaptedSearchResponse = adaptSearchResponse<T>(
searchResponse,
searchContext
)
// Adapt the Meilisearch response to a compliant instantsearch.js response
const adaptedSearchResponse = adaptSearchResponse<T>(
searchResponse,
searchContext
)

searchResponses.results.push(adaptedSearchResponse.results[0])
}

return adaptedSearchResponse
return searchResponses
} catch (e: any) {
console.error(e)
throw new Error(e)
Expand Down
Loading

0 comments on commit bc73484

Please sign in to comment.