elastic-composer

A high-level Elasticsearch query manager and executor. Filter fields, find search suggestions, and paginate query results for your indicies. Comes with addons for persisting and rehydrationg filter state from localStorage and the URL. Batteries included for optionally initializing via index introspection. Fully configurable. Very delightful. Try a slice 🍰!

Usage no npm install needed!

<script type="module">
  import elasticComposer from 'https://cdn.skypack.dev/elastic-composer';
</script>

README

elastic-composer

A high-level Elasticsearch query manager and executor. Filter fields, find search suggestions, and paginate query results for your indicies. Comes with addons for persisting and rehydrationg filter state from localStorage and the URL. Batteries included for optionally initializing via index introspection. Fully configurable. Very delightful. Try a slice 🍰!

Example:

const client = new AxiosESClient('my_url/my_index');
const crm = new Manager(client);

// set filters on the elasticsearch index fields 'age', 'isMarried', 'id', 'tags', and 'user_profile.location'
crm.filter.range.setFilter('age', {greaterThan: 20, lessThanEqual: 60})
crm.filter.boolean.setFilter('isMarried', {state: true})
crm.filter.exists.setFilter('id')
crm.filter.multiselect.setFilter('tags', { isHuman: { inclusion: 'include' }, hasBlueHair: { inclusion: 'exclude' }})
crm.filters.geo.addToFilter('user_profile.location', 'my_location_search', {
    inclusion: 'include',
    kind: 'should',
    points : [
        {"lat" : 40, "lon" : -70},
        {"lat" : 30, "lon" : -80},
        {"lat" : 20, "lon" : -90}
    ]
})

autorun(() => {
  console.log(crm.results) // results of the above compound query
})

Note: Internally all filter changes create sideEffectRequests that are put onto a processing queue. The above example will create 5 requests - one for each setFilter | addFilter action. However, because there is built in debouncing, if these actions occur within the debounce window, only the last request (fully compounded with all filters applied) will be executed.

Example with React:

export default observer(() => {
    const crm = useContext(Context.crm);
    return (
      <div>
        <div onClick={() => crm.filter.exists.setFilter('id')}/>
        <div onClick={() => crm.filter.exists.clearFilter('id')}/>
        <div>
          {crm.results}
        </div>
      </div>
    )
})

Install

npm install --save elastic-composer

Peer dependencies

This package requires that you also install:

{
        "await-timeout": "^1.1.1",
        "axios": "^0.19.1", <------- only needed if using the AxiosESClient
        "lodash.chunk": "^4.2.0",
        "mobx": "^5.14.2"
        "lodash.debounce": "^4.0.8", <------- only needed if using the History API
        "query-string": "^6.11.1", <------- only needed if using the History API
        "query-params-data": "^0.1.1", <------- only needed if using the History API
}

About

This library is a high level aid in querying an Elasticsearch index and building applications ontop of indexes. It is designed to bind directly to the view layer (React, Vue, Vanilla, etc..) of your app, and it handles the vast majority of associated business logic internally.

This library is written in MobX, which makes it reactive. If you don't want to use MobX, you can convert any attribute (see all attributes in the API) to an observable stream (RxJS, 😎) using the mobx-utils tool.

Paradigm

TL;DR: You describe how you want to filer each field via a Filter API customized to your index and the manager handles querying, state, and pagination.

The general paradigm is as follows:

There are 4 API's:

  • Filter API
  • Suggestion API
  • History API
  • Manager API

The flow is:

  1. You define all the fields of an ES index that you want to use via (A) configuration objects in either the Filter or Suggestion API or (B) introspection abilities in the Manager API.
  2. Once you have fields set that you can filter or find suggestions on, you use the Filter API to filter results and the Suggestion API to get suggestions (for parameters to use in filters - such as fuzzy or prefix search of values).
  3. The Manager API gives you access to results and allows you to paginate over the results.
  4. The History API records Filters and Suggestions that have been set, persists this state to the URL in a persistent store (like localStorage), and rehydrates from persisted state.

Everything in this library is reactive. So once you set a filter, the manager will react to the change, and submit a new query to Elasticsearch using all the filters that have been set across all the fields. The manager handles debouncing, throttling and batching queries.

In addition to simply running new queries, Filters and Suggestions provide opinionated aggregates that help inform how well the filter is doing. For example, the Range Filter gives you aggregates that show you a histogram of documents with the filter applied and without it applied. By default, aggregates are turned off by default. The paradigm with aggregates is to turn them on when a user is accessing a UI element that allows seeing aggregate data and turn them off when the UI element is no long visible. Because aggregates will respond to all filter changes, if you don't turn them off when not in use, you will submit meaningless queries to Elasticsearch.

Available filters and suggestions

The currently available Filters are:

  • range: Filter documents by fields that fit within a LT (<), LTE(<=), GT(>), GTE(>=) range
  • boolean: Filter documents by fields that have a value of either true or false
  • exists: Filter documents by fields that have any value existing for that field
  • multiselect: Filter documents that have fields matching certain values (includes or excludes)
  • geo: Filter documents that have fields with geo_point data in them

The currently available suggestions are:

  • prefix: Get suggestions for fields based on matches with the same prefix
  • fuzzy: Get suggestions for fields based on fuzzy matching

Enabling filters and suggestions

All Filters affect both the query and aggs part of an Elasticsearch request object. The query part is how the Filter impacts which documents match the filters. The aggs part provides information about how successful the filter is - showing things like histogram of range results, count of exists and not exists, etc... By default, the aggs part is disabled for every Filter. You should use setAggsEnabledToTrue and setAggsEnabledToFalse to toggle aggs for a Filter. The idea is to only run aggs queries when you want to show this data to the user.

Similarly, Suggestions are disabled by default. For the same reason above, suggestions shouldn't run unless you explicitly are showing suggestion data to a user. To toggle Suggestion enabled state use the methods setEnabledToTrue and setEnabledToFalse.

How filters and suggestions affect one another

The interplay between Suggestions and Filters is such:

  • Suggestions don't affect Filters, but they will react to every Filter change
  • Filters affect Suggestions and don't react to Suggestion changes

Extending and customizing filters

Extending and overriding the set of usable Filters or Suggestions is both possible, and easy. See Extending Filters and Suggestions for a complete guide. The basic idea is that you extend a base Filter or base Suggestion and fill out methods that tell: (A) when the manager should react to changes, (B) how to mutate a Elasticsearch request object to add Filter or Suggestion specific query and aggs, (C) how to parse an Elasticsearch response object to extract aggs.

Quick Examples

Various use cases are described below. Be sure to check out the API for the full range of attributes and methods available on the Manager, Filters, and Suggestions.

Instantiate a manager

import {AxiosESClient, Manager} from 'elastic-composer';

// instantiate an elasticsearch axios client made for this lib
const client = new AxiosESClient('my_url/my_index');

// instantiate a manager
const manager = new Manager(client, {
    pageSize: 100,
    queryThrottleInMS: 350,
    fieldBlackList: ['id']
});

Instantiate a manager with specific config options for a range filter

import {AxiosESClient, Manager, RangeFilter} from 'elastic-composer';

// set the default config all filters will have if not explicitly set
// by default we don't want aggs enabled unless we know the filter is being shown in the UI. So,
// we use lifecycle methods in react to toggle this config attribute and set the default to `false`.
const defaultRangeFilterConfig = {
    aggsEnabled: false,
    defaultFilterKind: 'should',
    getDistribution: true,
    getRangeBounds: true,
    rangeInterval: 1
};

// explicitly set the config for certain fields
const customRangeFilterConfig = {
    age: {
        field: 'user.age',
        rangeInterval: 10
    },
    invites: {
        field: 'user.invites',
        getDistribution: false
    }
};

// instantiate a range filter
const rangeFilter = new RangeFilter(defaultRangeFilterConfig, customRangeFilterConfig);

const options = {
    pageSize: 100,
    queryThrottleInMS: 350,
    fieldBlackList: ['id'],
    filters: {range: rangeFilter}
};

const manager = new Manager(client, options);

Setting the fieldNameModifier for all fields in a filter

The fieldNameModifier can be used to modify what the field name sent to Elasticsearch looks like. This is useful if you want to take a field name such as tags and turn it into tags.keyword for matching purposes.

The modifier is a function with the signature (fieldName: string) => string

import {AxiosESClient, Manager, MultiSelectFilter} from 'elastic-composer';

// set the default config all filters will have if not explicitly set
// by default we don't want aggs enabled unless we know the filter is being shown in the UI. So,
// we use lifecycle methods in react to toggle this config attribute and set the default to `false`.

const defaultMultiSelectFilterConfig = {
    defaultFilterKind: 'should',
    defaultFilterInclusion: 'include',
    getCount: true,
    aggsEnabled: false,
    fieldNameModifierQuery: (fieldName: string) => `${fieldName}`
    fieldNameModifierAggs: (fieldName: string) => `${fieldName}.keyword`
};


// instantiate a range filter
const multiselectFilter = new MultiSelectFilter(defaultMultiSelectFilterConfig);

const options = {
    pageSize: 100,
    queryThrottleInMS: 350,
    fieldBlackList: ['id'],
    filters: {multiselect: multiselectFilter}
};

const manager = new Manager(client, options);

Add a custom filter during manager instantiation

import MyCustomFilter from 'my_custom_filter';
import {AxiosESClient, Manager} from 'elastic-composer';

const client = new AxiosESClient('my_url/my_index');
const newCustomFilter = new MyCustomFilter();

const manager = new Manager(client, {
    pageSize: 100,
    queryThrottleInMS: 350,
    fieldBlackList: ['id'],
    filters: {myNewFilterName: newCustomFilter}
});

Add a custom suggestion during manager instantiation

import MyCustomSuggestion from 'my_custom_suggestion';
import {AxiosESClient, Manager} from 'elastic-composer';

const client = new AxiosESClient('my_url/my_index');
const newCustomSuggestion = new MyCustomSuggestion();

const manager = new Manager(client, {
    pageSize: 100,
    queryThrottleInMS: 350,
    fieldBlackList: ['id'],
    suggestions: {myNewSuggestionName: newCustomSuggestion}
});

Adding a custom client to the manager

If you don't have permissions set up on your Elasticsearch cluster, you will most likely want to create a custom client that uses your backend as a pass through layer for making Elasticsearch calls.

An example could look like this:

import {Manager, IClient, ESRequest, ESResponse, ESMappingType} from 'elastic-composer';

/**
 * Create a custom client that works on a specific through backend graphql nodes
 * In this case, the client uses the nodes 'creatorCRMSearch' and 'creatorCRMFields'
 */
class CreatorIndexGQLClient<Source extends object = object> implements IClient {
    public graphqlClient: GqlClient;

    constructor(graphqlClient: GqlClient) {
        if (graphqlClient === undefined) {
            throw new Error(
                'GraphqlQL client is undefined. Please instantiate this class with a GqlClient instance'
            );
        }
        this.graphqlClient = graphqlClient;
    }

    public search = async (search: ESRequest): Promise<ESResponse<Source>> => {
        const {data} = await this.graphqlClient.client.query({
            query: gql`
                query CreatorCRMSearch($search: JSON) {
                    creatorCRMSearch(search: $search)
                }
            `,
            fetchPolicy: 'no-cache',
            variables: {search: JSON.stringify(search)}
        });
        return JSON.parse(data.creatorCRMSearch);
    };

    public mapping = async (): Promise<Record<string, ESMappingType>> => {
        const {data} = (await this.graphqlClient.client.query({
            query: gql`
                query CreatorCRMFields {
                    creatorCRMFields
                }
            `,
            fetchPolicy: 'no-cache'
        })) as any;
        return JSON.parse(data.creatorCRMFields);
    };
}

const customClient = new CreatorIndexGQLClient(gqlClient);
const creatorCRM = new Manager(customClient);

Set middleware

import {Middleware} from 'elastic-composer';

const logRequestObj: Middleware = (
    _effectRequest: EffectRequest<EffectKinds>,
    request: ESRequest
) => {
    console.log(request);
    return request;
};

manager.setMiddleware([logRequestObj]);

Get the initial results for a manager

All queries are treated as requests and added to an internal queue. Thus, you don't await this method but, react to the manager.results attribute.

manager.runStartQuery();

Run a custom elasticsearch query using the current filters

If you wanted to bulk export a subset of the filtered results without having to paginate programmatically, you could request the results for a much larger page size this way over a reduced field list:

const results = await manager.runCustomFilterQuery({whiteList: ['id'], pageSize: 10000});

Get the raw elasticsearch query derived from the current filters

const rawEsQuery = await manager.getCurrentEsQuery();

Setting a range filter

manager.filters.range.setFilter('age', {greaterThanEqual: 20, lessThan: 40});

Note: This triggers a query to rerun with all the existing filters plus the range filter for age will be updated to only include people between the ages of 20-40 (inclusive to exclusive).

Setting a boolean filter

manager.filters.boolean.setFilter('isActive', {state: true});

Setting a exists filter

For example, this will filter all documents so only the ones with a facebook.id are shown

manager.filters.boolean.setFilter('facebook.id', {exists: true});

Setting a multi-select filter

A multi select filter can be set in two ways: (1) all selections at once, or (2) one selection at a time.

To set all selections at once, you would do something like:

manager.filters.multiselect.setFilter('tags', {
    is_good_user: {inclusion: 'include'},
    has_green_hair: {inclusion: 'exclude', kind: 'must'},
    likes_ham: {inclusion: 'include', kind: 'should'}
});

Notice how kind is optional. If its not specified, it will default to whatever defaultFilterKind is set to for the filter (aka manager.filter.multiselect.fieldConfigs['tags].defaultFilterKind)

To set one selection at a time, you would do:

manager.filters.multiselect.addToFilter('tags', 'has_green_hair', {
    inclusion: 'exclude',
    kind: 'must'
});

Setting a geo filter

Geo filters implement geo bounding box, geo distance, and geo polygon queries.

Like a multiselect filter, you can add all filters at once for a field using setFilter or add them one by one using addFilter.

crm.filters.geo.addToFilter('user_profile.location', 'my_first_loc', {
    'kind': 'should',
    'inclusion': 'exclude',
    'distance': '100mi',
    'lat': 34.7850143,
    'lon': -92.3912103
})

crm.filters.geo.addToFilter('user_profile.location', 'my_second_loc', {
    'kind': 'must',
    'inclusion': 'include',
    "top_left" : {
        "lat" : 40.73,
        "lon" : -74.1
    },
    "bottom_right" : {
        "lat" : 40.01,
        "lon" : -71.12
    }
})

crm.filters.geo.addToFilter('user_profile.location', 'my_third_loc', {
    "points" : [
        {"lat" : 40, "lon" : -70},
        {"lat" : 30, "lon" : -80},
        {"lat" : 20, "lon" : -90}
    ]
})

Clearing a single selection from a multi-select filter

manager.filters.multiselect.removeFromFilter('tags', 'has_green_hair');

Clearing a filter

For example, to clear the isActive field on a boolean filter, we would do:

manager.filters.boolean.clearFilter('tags');

Setting a prefix suggestion

manager.suggestions.prefix.setSearch('tags', 'blu');

Setting a fuzzy suggestion

manager.suggestions.fuzzy.setSearch('tags', 'ca');

Access suggestion results

All suggestions have the same interface (currently). For both prefix and fuzzy would get the suggestions for a search like:

manager.suggestions.fuzzy.fieldSuggestions['tags'];

// => [{ suggestion: 'car', count: 120}, { suggestion: 'can', count: 9 }]

Access the results of a query

manager.results; // Array<ESHit>

Results are an array where each object in the array has the type:

type ESHit<Source extends object = object> = {
    _index: string;
    _type: string;
    _id: string;
    _score: number;
    _source: Source;
    sort: ESRequestSortField;
};

_source will be the document result from the index.

Thus, you would likely use the results like:

manager.results.map(r => r._source);

Access the raw response object of the current query

manager.rawESResponse

// => 
// {"took":1,"timed_out":false,"_shards":{"total":5,"successful":5,"skipped":0,"failed":0},"hits":{"total":2178389,"max_score":0.0,"hits":[]}}
//

Paginating through the results set

manager.nextPage();
manager.prevPage();

manager.currentPage; // number
// # => 0 when no results exist
// # => 1 for the first page of results

Checking if there is another page to paginate to

manager.hasNextPage

Enabling aggregation data for a filter

By default, aggregation data is turned off for all filter. This data shows things like count of exists field, histogram of range data, etc..

manager.filters.boolean.setAggsEnabledToTrue('tags');

The idea with enabling and disabling aggregation data is that these aggregations only need to run when a filter is visible to the user in the UI. Thus, enabling and disabling should mirror filter visibility in the UI.

Disabling aggregation data for a filter

manager.filters.boolean.setAggsEnabledToFalse('tags');

Enabling suggestions

Similar to filters, suggestions are disabled by default because they rely on elasticsearch aggregations to run, and there is no point in collecting the data unless the user cares about it.

manager.suggestions.fuzzy.setEnabledToTrue('tags');

Disabling suggestions

manager.suggestions.fuzzy.setEnabledToFalse('tags');

Setting filter 'should' or 'must' kind

All filters can be use in should or must mode. By default, all filters are should filters unless explicitly changed to must filters. Read this for more info on the difference between should and must

manager.filters.boolean.setKind('facebook.id', 'must');

// or to go back to should:

manager.filters.boolean.setKind('facebook.id', 'should');

Clearing all filters

manager.clearAllFilters()

Clearing all suggestions

manager.clearAllSuggestions()

Looking at all active suggestions

manager.activeSuggestions

// => 
// { tags: [PrefixSuggestion, FuzzySuggestion], location: [PrefixSuggestion]}

Looking at all active filters

manager.activeFilters

// => 
// { tags: [MultiSelectFilter, ExistsFilter], location: [RangeFilter, ExistsFilter]}

Looking at all the Filter and Suggestion instances available for a filed

manager.fieldsWithFiltersAndSuggestions

// =>
// { tags: { filters: [MultiSelectFilter, ExistsFilter] suggestions: [PrefixSuggestion, FuzzySuggestion]} }

Using the history API

const userHistory = new History(manager, 'user', { // set the url's query param key to `user`
    historyPersister: localStorageHistoryPersister('user'), // set the local storage suffix key to `user`
    historySize: 4
});

API

Manager

Initialization

The manager constructor has the signature (client, options) => ManagerInstance

Client

client is an object than handles submitting query responses. It has the signature:

interface IClient<Source extends object = object> {
    search: (request: ESRequest) => Promise<ESResponse<Source>>;
    mapping: () => Promise<Record<string, ESMappingType>>;
    // With ESMappingType equal to https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html
}

At the moment there only exists an AxiosESClient client. This can be imported via a named import:

import {AxiosESClient} from 'elastic-composer';

const axiosESClient = new AxiosESClient(endpoint);

// endpoint is in the form: blah2lalkdjhgak.us-east-1.es.amazonaws.com/myindex1
Options

options are used to configure the manager. There currently exist these options:

type ManagerOptions = {
    pageSize?: number;
    queryThrottleInMS?: number;
    fieldWhiteList?: string[];
    fieldBlackList?: string[];
    middleware?: Middleware[];
    filters?: IFilters;
    suggestions?: ISuggestions;
};
  • pageSize: the number of results to expect when calling manager.results. The default size is 10.
  • queryThrottleInMS: the amount of time to wait before executing an Elasticsearch query. The default time is 1000.
  • fieldWhiteList: A list of elasticsearch fields that you only want to allow filtering on. This can't be used with fieldBlackList. Only white list fields will be returned in an elasticsearch query response.
  • fieldBlackList: A list of elasticsearch fields that you don't want to allow filtering on. This can't be used with fieldWhiteList. Black list fields will be excluded from an elasticsearch query response.
  • middleware: An array of custom middleware to run during elasticsearch request object construction. See below for the type.
  • filters: An object of filter instances. Default filters will be instantiate if none are specified in this options field. This options field however can be used to override existing filters or specify a custom one.
  • suggestions: An object of suggestion instances. Default suggestions will be instantiated if none are specified in this options field. This options field however can be used to override existing suggestions or specify a custom one.

The middleware function type signature is:

Middleware = (effectRequest: EffectRequest<EffectKinds>, request: ESRequest) => ESRequest;

Example of overriding the range filter:

const options = {filters: {range: rangeFilterInstance}};

Example of overriding the fuzzy suggestion:

const options = {suggestions: {fuzzy: fuzzyFilterInstance}};

Methods

method description type
nextPage paginates forward (): void
prevPage paginates backward (): void
clearAllFilters clears all active filters (): void
clearAllSuggestions clears all active suggestions (): void
getFieldNamesAndTypes runs an introspection query on the index mapping and generates an object of elasticsearch fields and the filter type they correspond to async (): void
runStartQuery runs the initial elasticsearch query that fetches unfiltered data (): void
runCustomFilterQuery runs a custom query using the existing applied filters outside the side effect queue flow. white lists and black lists control which data is returned in the elasticsearch response source object async (options?: {fieldBlackList?: string[], fieldWhiteList?: string[], pageSize?: number }): Promise<ESResponse>
setMiddleware adds middleware to run during construction of the elasticsearch query request object (middlewares: Middleware): void. Middleware has the type (effectRequest: EffectRequest<EffectKinds>, request: ESRequest) => ESRequest
getCurrentEsQuery gets the raw Elasicsearch query that is derived from the current state () => ESRequest

Attributes

attribute description notes
isSideEffectRunning a flag telling if a query is running boolean
currentPage the page number 0 if there are no results. 1 for the first page. etc...
fieldWhiteList the white list of fields that filters can exist for
fieldBlackList the black list of fields that filters can not exist for
pageSize the page size The default size is 10. This can be changed by setting manager options during init.
queryThrottleInMS the throttle time for queries The default is 1000 ms. This can be changed by setting manager options during init.
filters the filter instances that the manager controls
indexFieldNamesAndTypes A list of fields that can be filtered over and the filter name that this field uses. This is populated by the method getFieldNamesAndTypes.
results the results of the most recent query The results type is Array. See the results quick example doc for the type
rawESResponse The response object from the client from the query ESResponse
activeSuggestions the object of fields with active suggestions { fieldName: SuggestionInstance[] }
activeFilters the object of fields with active filters { fieldName: FilterInstance[] }
fieldsWithFiltersAndSuggestions the object of fields and the filters and suggestions that are available for them { fieldName: { suggestions: SuggestionInstance[], filters: FilterInstance[] } }
hasNextPage Whether another page is available via the nextPage method boolean
### Common Among All Filters

Initialization

All filter constructors have the signature (defaultConfig, specificConfig) => FilterTypeInstance

defaultConfig and specificConfig are specific to each filter class type.

Methods

method description type
setFilter sets the filter for a field (field: <name of field>, filter: <filter specific to filter class type>): void
clearFilter clears the filter for a field (field: <name of field>): void
setKind sets the kind for a field (field: <name of field>, kind: should or must): void
setAggsEnabledToTrue enables fetching of aggs for this filter field (field: <name of field>): void
setAggsEnabledToFalse disables fetching of aggs for this filter field (field: <name of field>): void

Attributes

attribute description type
fieldConfigs the config for a field, keyed by field name { [<names of fields>]: <config specific to filter class type> }
fieldFilters the filters for a field, keyed by field name { [<names of fields>]: Filter }
fieldKinds the kind (should or must) for a field, keyed by field name { [<names of fields>]: 'should' or 'must' }

Boolean Specific

Initialization

The boolean constructor has the signature (defaultConfig, specificConfig) => BooleanFilterInstance

defaultConfig

The configuration that each field will acquire if an override is not specifically set in specificConfig

type DefaultConfig = {
    defaultFilterKind: 'should',
    getCount: true,
    aggsEnabled: false
};
specificConfig

The explicit configuration set on a per field level. If a config isn't specified or only partially specified for a field, the defaultConfig will be used to fill in the gaps.

type SpecificConfig = Record<string, BooleanConfig>;

type BooleanConfig = {
    field: string;
    defaultFilterKind?: 'should' or 'must';
    getCount?: boolean;
    aggsEnabled?: boolean;
};

Methods

method description type
setFilter sets the filter for a field (field: <name of boolean field>, filter: {state: true or false}): void

Attributes

attribute description type
filteredCount the count of boolean values of all filtered documents, keyed by field name { [<names of boolean fields>]: { true: number; false: number;} }
unfilteredCount the count of boolean values of all unfiltered documents, keyed by field name { [<names of boolean fields>]: { true: number; false: number;} }

Range Specific

Initialization

The range constructor has the signature (defaultConfig, specificConfig) => RangeFilterInstance

defaultConfig

The configuration that each field will acquire if an override is not specifically set in specificConfig

type RangeConfig = {
    defaultFilterKind: 'should',
    getDistribution: true,
    getRangeBounds: true,
    rangeInterval: 1,
    aggsEnabled: false
};
specificConfig

The explicit configuration set on a per field level. If a config isn't specified or only partially specified for a field, the defaultConfig will be used to fill in the gaps.

type SpecificConfig = Record<string, RangeConfig>;

type RangeConfig = {
    field: string;
    defaultFilterKind?: 'should' or 'must';
    getDistribution?: boolean;
    getRangeBounds?: boolean;
    rangeInterval?: number;
    aggsEnabled?: boolean;
};

Methods

method description type
setFilter sets the filter for a field (field: <name of range field>, filter: {lessThan?: number, greaterThan?: number, lessThanEqual?: number, greaterThanEqual?: number): void

Attributes

attribute description type
filteredRangeBounds the bounds of all filtered ranges (ex: 20 - 75), keyed by field name { [<names of range fields>]: { min: { value: number; value_as_string?: string; }; max: { value: number; value_as_string?: string; };} }
unfilteredRangeBounds the bounds of all unfiltered ranges (ex: 0 - 100), keyed by field name { [<names of range fields>]: { min: { value: number; value_as_string?: string; }; max: { value: number; value_as_string?: string; };} }
filteredDistribution the distribution of all filtered ranges, keyed by field name {[<names of range fields>]: Array<{ key: number; doc_count: number; }>}
unfilteredDistribution the distribution of all filtered ranges, keyed by field name {[<names of range fields>]: Array<{ key: number; doc_count: number; }>}

Exists Specific

Initialization

The exists constructor has the signature (defaultConfig, specificConfig) => ExistsFilterInstance

defaultConfig

The configuration that each field will acquire if an override is not specifically set in specificConfig

type DefaultConfig = {
   defaultFilterKind: 'should',
   getCount: true,
   aggsEnabled: false
};
specificConfig

The explicit configuration set on a per field level. If a config isn't specified or only partially specified for a field, the defaultConfig will be used to fill in the gaps.

type SpecificConfig = Record<string, ExistsConfig>;

type ExistsConfig = {
    field: string;
    defaultFilterKind?: 'should' or 'must';
    getCount?: boolean;
    aggsEnabled?: boolean;
};

Methods

method description type
setFilter sets the filter for a field (field: <name of exists field>, filter: {exists: true or false}): void

Attributes

attribute description type
filteredCount the count of exists values of all filtered documents, keyed by field name { [<names of exists fields>]: { exists: number; doesntExist: number;} }
unfilteredCount the count of exists values of all unfiltered documents, keyed by field name { [<names of exists fields>]: { exists: number; doesntExist: number;} }

Multi-Select Specific

Initialization

The multiselect constructor has the signature (defaultConfig, specificConfig) => MultiSelectFilterInstance

defaultConfig

The configuration that each field will acquire if an override is not specifically set in specificConfig

type DefaultConfig = {
    defaultFilterKind: 'should',
    defaultFilterInclusion: 'include',
    getCount: true,
    aggsEnabled: false,
    fieldNameModifierQuery: (fieldName: string) => fieldName
    fieldNameModifierAggs: (fieldName: string) => fieldName
};
specificConfig

The explicit configuration set on a per field level. If a config isn't specified or only partially specified for a field, the defaultConfig will be used to fill in the gaps.

type SpecificConfig = Record<string, MultiSelectConfig>;

type MultiSelectConfig = {
    field: string;
    defaultFilterKind?: 'should' or 'must';
    defaultFilterInclusion?: 'include' | 'exclude';
    getCount?: boolean;
    aggsEnabled?: boolean;
    fieldNameModifierQuery?: (fieldName: string) => string
    fieldNameModifierAggs?: (fieldName: string) => string
};

Geo Specific

NOTE: Go filters do not have any aggs enabled! Do not try to use aggs with geo filters.

Examples of GeoFilter actions and the queries they generate can be found at src/filters/geo_filter_README.md

Initialization

The geoFilter constructor has the signature (defaultConfig, specificConfig) => GeoFilterInstance

defaultConfig

The configuration that each field will acquire if an override is not specifically set in specificConfig

type DefaultConfig = {
    defaultFilterKind: 'should',
    defaultFilterInclusion: 'include',
    getCount: true,
    aggsEnabled: false,
    fieldNameModifierQuery: (fieldName: string) => fieldName
    fieldNameModifierAggs: (fieldName: string) => fieldName
};
specificConfig

The explicit configuration set on a per field level. If a config isn't specified or only partially specified for a field, the defaultConfig will be used to fill in the gaps.

type SpecificConfig = Record<string, GeoConfig>;

type GeoConfig = {
    field: string;
    defaultFilterKind?: 'should' or 'must';
    defaultFilterInclusion?: 'include' | 'exclude';
    getCount?: boolean;
    aggsEnabled?: boolean;
    fieldNameModifierQuery?: (fieldName: string) => string
    fieldNameModifierAggs?: (fieldName: string) => string
};

Methods

A filter selection has the type:

{
  inclusion: 'include' | 'exclude';
  kind?: 'should' | 'must';
}
method description type
setFilter sets the filter for a field (field: <name of geo field>, filter: {[geoSubFilterReferenceName]: {inclusion: 'include' or 'exclude', kind?: 'should' or 'must'}}): void
addToFilter adds a single selection to a filter addToFilter(field: <name of geo field>, geoSubFilterReferenceName: string, selectionFilter: {inclusion: 'include' or 'exclude', kind?: 'should' or 'must'}): void
removeFromFilter removes a single selection from a filter removeFromFilter(field: <name of geo field>, geoSubFilterReferenceName: string): void

Attributes

attribute description type

Note: No aggregates are implemented, thus there are no attributes specific to this filter type.

Common Among All Suggestions

The suggestions that ship with this package all have the same public interface (for the moment). Thus, you can rely on this section for API documentation on each suggestion type.

Initialization

All filter constructors have the signature (defaultConfig, specificConfig) => SuggestionTypeInstance

defaultConfig and specificConfig are specific to each suggestion class type.

The defaultConfig looks like:

{
    defaultSuggestionKind: 'should',
    enabled: false,
    fieldNameModifierQuery: (fieldName: string) => fieldName,
    fieldNameModifierAggs: (fieldName: string) => fieldName
}

The typings for the specific config object looks like:

{
    field: string;
    defaultSuggestionKind?: 'should' | 'must';
    enabled?: boolean;
    fieldNameModifierQuery?: FieldNameModifier;
    fieldNameModifierAggs?: FieldNameModifier;
}

Methods

method description type
setSearch sets the search term for a field to get suggestions for (field: <name of field>, searchTerm: string): void
clearSearch clears the search for a field (field: <name of field>): void
setKind sets the kind for a field (field: <name of field>, kind: should or must): void
setEnabledToTrue enables fetching of suggestions for this suggestion field (field: <name of field>): void
setEnabledToFalse disables fetching of suggestions for this suggestion field (field: <name of field>): void

Attributes

attribute description type
fieldConfigs the config for a field, keyed by field name { [<names of fields>]: <config specific to filter class type> }
fieldSuggestions the suggestions for a field, keyed by field name { [<names of fields>]: Array<{suggestion: string; count: number}> }
fieldSearches the searches for a field, keyed by field name { [<names of fields>]: string }
fieldKinds the kind (should or must) for a field, keyed by field name { [<names of fields>]: 'should' or 'must' }

History API

The history API allows you to:

  • record history of user interactions with filters and suggestions
  • allow you to serialize the current state to a URL query param
  • allow you to save the history to local storage and rehydrate from this storage.

Initialization

All filter constructors have the signature (manager: Manager, queryParamKey: string, options?: IHistoryOptions<HistoryLocation>) => SuggestionTypeInstance

The options looks like:

{
    historySize?: number;
    currentLocationStore?: UrlStore<State>;
    historyPersister?: IHistoryPersister;
    rehydrateOnStart?: boolean; // whether to run the `rehydrate` method in the constructor
}

In turn, the historyPersister has the type:

IHistoryPersister {
    setHistory: (location: Array<HistoryLocation | undefined>) => void;
    getHistory: () => HistoryLocation[];
}

Methods

method description type
setCurrentState sets the current state of filters and suggestgions (location: HistoryLocation): void
back goes back in the history (): void
forward goes forward in the history (): void
rehydrate rehydrates from URL or persistent storage (): void

Attributes

attribute description type
history the recorded history Array<HistoryLocation | undefined>
currentLocationInHistoryCursor the location in history, changed by going 'back' or 'forward' number
hasRehydratedLocation flag to tell if any location was rehydrated from when the rehydrate method was called

Verbose Examples

See ./dev/app/ for examples used in the development environment.

Usage with React

import {AxiosESClient, Manager} from 'elastic-composer';

const client = new AxiosESClient(process.env.ELASTIC_SEARCH_ENDPOINT);
const creatorCRM = new Manager(client);

creatorCRM.getFieldNamesAndTypes().then(() => {
    creatorCRM.runStartQuery();
});

export default {
    exampleForm: React.createContext(exampleFormInstance),
    creatorCRM: React.createContext(creatorCRM)
};

Extending Filters and Suggestions

TODO