Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { adaptFilters } from './filter-adapter'
*
* @returns {MeiliSearchParams}
*/
function MeiliParamsCreator(searchContext: SearchContext) {
export function MeiliParamsCreator(searchContext: SearchContext) {
const meiliSearchParams: Record<string, any> = {}
const {
facets,
Expand Down
30 changes: 11 additions & 19 deletions src/adapter/search-request-adapter/search-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,13 @@ import {
} from '../../types'
import { addMissingFacets, extractFacets } from './filters'

const emptySearch: MeiliSearchResponse<Record<string, any>> = {
hits: [],
query: '',
facetDistribution: {},
limit: 0,
offset: 0,
estimatedTotalHits: 0,
processingTimeMs: 0,
}

/**
* @param {ResponseCacher} cache
*/
export function SearchResolver(cache: SearchCacheInterface) {
export function SearchResolver(
client: MeiliSearch,
cache: SearchCacheInterface
) {
return {
/**
* @param {SearchContext} searchContext
Expand All @@ -30,17 +23,10 @@ export function SearchResolver(cache: SearchCacheInterface) {
*/
searchResponse: async function (
searchContext: SearchContext,
searchParams: MeiliSearchParams,
client: MeiliSearch
searchParams: MeiliSearchParams
): Promise<MeiliSearchResponse<Record<string, any>>> {
const { placeholderSearch, query } = searchContext

// query can be: empty string, undefined or null
// all of them are falsy's
if (!placeholderSearch && !query) {
return emptySearch
}

const { pagination } = searchContext

// In case we are in a `finitePagination`, only one big request is made
Expand Down Expand Up @@ -74,6 +60,12 @@ export function SearchResolver(cache: SearchCacheInterface) {
searchResponse.facetDistribution
)

// query can be: empty string, undefined or null
// all of them are falsy's
if (!placeholderSearch && !query) {
searchResponse.hits = []
searchResponse.estimatedTotalHits = 0
}
// Cache response
cache.setEntry<MeiliSearchResponse>(key, searchResponse)
return searchResponse
Expand Down
36 changes: 25 additions & 11 deletions src/cache/first-facets-distribution.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
import { FacetDistribution } from '../types'
import { FacetDistribution, SearchContext } from '../types'
import { MeiliParamsCreator } from '../adapter'

export function cacheFirstFacetDistribution(
defaultFacetDistribution: FacetDistribution,
searchResponse: any
): FacetDistribution {
if (
searchResponse.query === '' &&
Object.keys(defaultFacetDistribution).length === 0
) {
return searchResponse.facetDistribution
export async function cacheFirstFacetDistribution(
searchResolver: any,
searchContext: SearchContext
): Promise<FacetDistribution> {
const defaultSearchContext = {
...searchContext,
// placeholdersearch true to ensure a request is made
placeholderSearch: true,
// Set paginationTotalHits to ensure limit is set to 0
// in order to retrieve 0 documents during the default search request
pagination: { ...searchContext.pagination, paginationTotalHits: 0 },
// query set to empty to ensure retrieving the default facetdistribution
query: '',
}
return defaultFacetDistribution
const meilisearchParams = MeiliParamsCreator(defaultSearchContext)
meilisearchParams.addFacets()
meilisearchParams.addPagination()

// Search response from Meilisearch
const searchResponse = await searchResolver.searchResponse(
defaultSearchContext,
meilisearchParams.getParams()
)
return searchResponse.facetDistribution || {}
}
35 changes: 20 additions & 15 deletions src/client/instant-meilisearch-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
AlgoliaSearchResponse,
AlgoliaMultipleQueriesQuery,
SearchContext,
FacetDistribution,
} from '../types'
import {
adaptSearchResponse,
Expand All @@ -28,11 +29,6 @@ export function instantMeiliSearch(
apiKey = '',
instantMeiliSearchOptions: InstantMeiliSearchOptions = {}
): InstantMeiliSearchInstance {
const searchCache = SearchCache()
// create search resolver with included cache
const searchResolver = SearchResolver(searchCache)
// paginationTotalHits can be 0 as it is a valid number
let defaultFacetDistribution: any = {}
const clientAgents = constructClientAgents(
instantMeiliSearchOptions.clientAgents
)
Expand All @@ -43,6 +39,12 @@ export function instantMeiliSearch(
clientAgents,
})

const searchCache = SearchCache()
// create search resolver with included cache
const searchResolver = SearchResolver(meilisearchClient, searchCache)

let defaultFacetDistribution: FacetDistribution

return {
clearCache: () => searchCache.clearCache(),
/**
Expand All @@ -63,26 +65,29 @@ export function instantMeiliSearch(
// Adapt search request to Meilisearch compliant search request
const adaptedSearchRequest = adaptSearchParams(searchContext)

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

// 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.
defaultFacetDistribution = cacheFirstFacetDistribution(
defaultFacetDistribution,
searchResponse
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
)

return adaptedSearchResponse
} catch (e: any) {
console.error(e)
Expand Down
2 changes: 1 addition & 1 deletion src/contexts/search-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function createSearchContext(
sort: sortByArray.join(':') || '',
indexUid,
pagination,
defaultFacetDistribution,
defaultFacetDistribution: defaultFacetDistribution || {},
placeholderSearch: options.placeholderSearch !== false, // true by default
keepZeroFacets: !!options.keepZeroFacets, // false by default
finitePagination: !!options.finitePagination, // false by default
Expand Down
9 changes: 7 additions & 2 deletions tests/assets/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { instantMeiliSearch } from '../../src'
import { MeiliSearch } from 'meilisearch'

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

const dataset = [
{
id: 2,
Expand Down Expand Up @@ -225,8 +228,8 @@ const wrongSearchClient = instantMeiliSearch(
'masterKey'
)
const meilisearchClient = new MeiliSearch({
host: 'http://localhost:7700',
apiKey: 'masterKey',
host: HOST,
apiKey: API_KEY,
})

export {
Expand All @@ -235,4 +238,6 @@ export {
wrongSearchClient,
geoDataset,
meilisearchClient,
HOST,
API_KEY,
}
148 changes: 148 additions & 0 deletions tests/first-facets-distribution.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { dataset, meilisearchClient, HOST, API_KEY } from './assets/utils'
import { instantMeiliSearch } from '../src'

describe('First facet distribution', () => {
beforeAll(async () => {
const deleteTask = await meilisearchClient.deleteIndex('movies')
await meilisearchClient.waitForTask(deleteTask.taskUid)
await meilisearchClient
.index('movies')
.updateFilterableAttributes(['genres'])
const documentsTask = await meilisearchClient
.index('movies')
.addDocuments(dataset)
await meilisearchClient.index('movies').waitForTask(documentsTask.taskUid)
})

test('creation of facet distribution without facets', async () => {
const searchClient = instantMeiliSearch(HOST, API_KEY)
const response = await searchClient.search([
{
indexName: 'movies',
params: {
facets: [],
query: '',
},
},
])
expect(response.results[0].facets).toEqual({})
})

test('creation of facet distribution without facets and with keepZeroFacets to true', async () => {
const searchClient = instantMeiliSearch(HOST, API_KEY, {
keepZeroFacets: false,
})
const response = await searchClient.search([
{
indexName: 'movies',
params: {
facets: [],
query: '',
},
},
])
expect(response.results[0].facets).toEqual({})
})

test('creation of facet distribution with facets', async () => {
const searchClient = instantMeiliSearch(HOST, API_KEY)
const response = await searchClient.search([
{
indexName: 'movies',
params: {
facets: ['genres'],
query: '',
},
},
])
expect(response.results[0].facets).toEqual({
genres: {
Action: 3,
Adventure: 1,
Animation: 1,
Comedy: 2,
Crime: 4,
Drama: 1,
'Science Fiction': 2,
Thriller: 1,
},
})
})

test('creation of facet distribution with facets and keepZeroFacets to true', async () => {
const searchClient = instantMeiliSearch(HOST, API_KEY, {
keepZeroFacets: true,
})
const response = await searchClient.search([
{
indexName: 'movies',
params: {
facets: ['genres'],
query: '',
},
},
])
expect(response.results[0].facets).toEqual({
genres: {
Action: 3,
Adventure: 1,
Animation: 1,
Comedy: 2,
Crime: 4,
Drama: 1,
'Science Fiction': 2,
Thriller: 1,
},
})

const response2 = await searchClient.search([
{
indexName: 'movies',
params: {
facets: ['genres'],
query: 'no results',
},
},
])

expect(response2.results[0].facets).toEqual({
genres: {
Action: 0,
Adventure: 0,
Animation: 0,
Comedy: 0,
Crime: 0,
Drama: 0,
'Science Fiction': 0,
Thriller: 0,
},
})
})

Comment on lines +234 to +257
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can remove this, because you have the same setup/exercise in the test below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is actually a difference for me!
The first tests:

  • Makes a search with no query
  • Should cache the default distribution correctly
  • Makes a search with the same searchClient as before but with a query that would return 0 documents
  • Checks if every facet is correctly set to 0

Second test:

  • Does a search with a query that would return 0 documents
  • Should cache the default distribution when none has been set before
  • Checks if every facet is correctly set to 0

There is a cache system between the multiple calls on the same search client. These two tests ensure they are helping each other.
The tests that ensure that when doing two call on my search client only triggers the default facetdistribution call only once is tested here: #830 (comment). In this test we are calling two time searchClient.search. During the first search we make two requests to meilisearch, one for the default facet distribution, one for the actual search. During the second search on the same searchClient the default facet distribution should already be cached and only the actual search request should be made, thus 3 calls.

test('creation of facet distribution with facets, keepZeroFacets to true, and query', async () => {
const searchClient = instantMeiliSearch(HOST, API_KEY, {
keepZeroFacets: true,
})
const response = await searchClient.search([
{
indexName: 'movies',
params: {
facets: ['genres'],
query: 'no results',
},
},
])
expect(response.results[0].facets).toEqual({
genres: {
Action: 0,
Adventure: 0,
Animation: 0,
Comedy: 0,
Crime: 0,
Drama: 0,
'Science Fiction': 0,
Thriller: 0,
},
})
})
})
8 changes: 4 additions & 4 deletions tests/search-resolver.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('Pagination browser test', () => {
apiKey: '',
clientAgents: [`Meilisearch instant-meilisearch (v${PACKAGE_VERSION})`],
})
expect(mockedSearch).toHaveBeenCalledTimes(1)
expect(mockedSearch).toHaveBeenCalledTimes(2)
})

test('two different search parameters', async () => {
Expand All @@ -77,7 +77,7 @@ describe('Pagination browser test', () => {
apiKey: '',
clientAgents: [`Meilisearch instant-meilisearch (v${PACKAGE_VERSION})`],
})
expect(mockedSearch).toHaveBeenCalledTimes(2)
expect(mockedSearch).toHaveBeenCalledTimes(3)
})

test('two identical and one different search parameters', async () => {
Expand All @@ -104,7 +104,7 @@ describe('Pagination browser test', () => {
apiKey: '',
clientAgents: [`Meilisearch instant-meilisearch (v${PACKAGE_VERSION})`],
})
expect(mockedSearch).toHaveBeenCalledTimes(2)
expect(mockedSearch).toHaveBeenCalledTimes(3)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have +1 calls now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the first call always sets up the default facet distribution by making a call with zero filters (except for the facet field) to ensure we have the facet distribution of an empty search not impacted by any filter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

})

test('two same and two different search parameter', async () => {
Expand Down Expand Up @@ -132,6 +132,6 @@ describe('Pagination browser test', () => {
apiKey: '',
clientAgents: [`Meilisearch instant-meilisearch (v${PACKAGE_VERSION})`],
})
expect(mockedSearch).toHaveBeenCalledTimes(2)
expect(mockedSearch).toHaveBeenCalledTimes(3)
})
})