import dataloader from 'dataloader' ; import { merge } from 'lodash' ; import { Client } from '@elastic/elasticsearch' ; import HttpAgent , { HttpsAgent } from 'agentkeepalive' ; import { SearchkitConfig } from '../resolvers' ; import ESQueryError from '../utils/ESQueryError' ; import { BaseFacet } from '../facets' ; import QueryManager from './QueryManager' ; import { filterTransform } from './FacetsFns' ; export interface SearchResponse < T > { took : number ; timed_out : boolean ; hits : { total : { value : number ; } ; max_score : number ; hits : Array < { _id : number ; _source : any ; fields ? : any ; highlight ? : any ; inner_hits ? : any ; matched_queries ? : string [ ] ; sort ? : string [ ] ; } > ; } ; aggregations ? : any ; } export type Query = { bool ? : { must ? : Array < Record < string , unknown > > ; should ? : Array < Record < string , unknown > > ; filter ? : Array < Record < string , unknown > > ; } ; } ; type BaseQuery = { size : number ; query ? : Query ; post_filter ? : Query ; } ; export const mergeESQueries = ( queries ) => merge ( { aggs : { } } , ... queries ) ; const keepaliveHttpsAgent = new HttpsAgent ( ) ; const keepaliveAgent = new HttpAgent ( ) ; export default class SearchkitRequest { private dataloader : any ; private client : Client ; constructor ( private queryManager , private config , private baseFilters , private facets ) { this . client = new Client ( { node : this . config . host , agent : ( ) => new URL ( this . config . host ) . protocol === 'http:' ? keepaliveAgent : keepaliveHttpsAgent } ) ; this . dataloader = new dataloader ( async ( partialQueries ) => { const query = this . buildQuery ( partialQueries ) ; const esQuery = this . config . postProcessRequest ? this . config . postProcessRequest ( query ) : query ; const response = await this . executeQuery ( esQuery ) ; return partialQueries . map ( ( ) => response ) ; } ) ; } public async search ( partialQuery ) { return this . dataloader . load ( partialQuery ) ; } private buildQuery ( partialQueries ) { const queryFilter = this . queryManager . hasQuery ( ) && this . config . query ? this . config . query . getFilter ( this . queryManager ) : null ; const baseFiltersQuery = filterTransform ( this . queryManager , this . config . filters ) ; const combinedBaseFilters = [ ] . concat ( this . baseFilters , baseFiltersQuery ?. bool ?. must || [ ] ) ; const query = queryFilter || ( combinedBaseFilters . length > 0 ? { } : null ) ; if ( combinedBaseFilters . length ) { if ( query . bool ) { Object . assign ( query . bool , { filter : query . bool . filter ?. length ? [ ] . concat ( query . bool . filter , combinedBaseFilters ) : combinedBaseFilters } ) ; } else { Object . assign ( query , { bool : { filter : combinedBaseFilters } } ) ; } } const postFilter = filterTransform ( this . queryManager , this . facets ) ; let highlight ; this . config . hits . highlightedFields ?. forEach ( ( field ) => { if ( ! highlight ) { highlight = { fields : { } } ; } if ( typeof field == 'string' ) { highlight . fields [ field ] = { } ; } else { highlight . fields [ field . field ] = field . config ; } } ) ; const baseQuery = { size : 0 } ; return mergeESQueries ( [ baseQuery , query && { query } , postFilter && { post_filter : postFilter } , highlight && { highlight } , ... ( partialQueries as any [ ] ) , ] . filter ( Boolean ) ) ; } private async executeQuery ( esQuery ) { const debugMode = process . env . DEBUG_MODE === "true" ; try { if ( debugMode ) console . log ( JSON . stringify ( esQuery , null , 2 ) ) ; const response = await this . client . search < SearchResponse < any > > ( { index : this . config . index , body : esQuery } ) ; return response . body ; } catch ( e ) { if ( debugMode ) console . log ( JSON . stringify ( e , null , 2 ) ) ; if ( e . meta ?. statusCode === 400 ) { throw new ESQueryError ( `Elasticsearch query failed. Check your custom filters or configuration. Below is the ES Query` , esQuery ) ; } else { throw e ; } } } }