packages/search-ui-elasticsearch-connector/src/queryBuilders/SearchQueryBuilder.ts (226 lines of code) (raw):
import type { QueryConfig, RequestState } from "@elastic/search-ui";
import { BaseQueryBuilder } from "./BaseQueryBuilder";
import {
transformFacet,
transformFacetToAggs,
transformFilter
} from "../transformer/filterTransformer";
import { SearchRequest } from "../types";
import { getQueryFields } from "../utils";
export class SearchQueryBuilder extends BaseQueryBuilder {
constructor(state: RequestState, private readonly queryConfig: QueryConfig) {
super(state);
}
build() {
this.setPagination(this.state.current, this.state.resultsPerPage);
this.setSourceFields(Object.keys(this.queryConfig.result_fields || {}));
this.setSort(this.buildSort());
this.setHighlight(this.buildHighlight());
this.setAggregations(this.buildAggregations());
this.setPostFilter(this.buildPostFilter());
this.setQuery(this.buildQuery());
return this.query;
}
private buildSort(): SearchRequest["sort"] {
if (this.state.sortList?.length) {
return this.state.sortList
.filter((s) => s.direction)
.map(({ field, direction }) => ({
[field]: direction || "desc"
}));
}
if (this.state.sortField && this.state.sortDirection) {
return { [this.state.sortField]: this.state.sortDirection };
}
return "_score";
}
private buildHighlight() {
const highlightFields = Object.entries(
this.queryConfig.result_fields || {}
).reduce((acc, [fieldKey, fieldConfiguration]) => {
if (fieldConfiguration.snippet) {
acc[fieldKey] = {};
}
return acc;
}, {});
return Object.keys(highlightFields).length > 0
? { fields: highlightFields }
: null;
}
private buildAggregations() {
if (
!this.queryConfig.facets ||
!Object.keys(this.queryConfig.facets).length
) {
return null;
}
const hasSelectedFilters = this.state.filters?.some(
(selectedFilter) =>
!this.queryConfig.filters?.find(
(baseFilter) => baseFilter.field === selectedFilter.field
)
);
return Object.entries(this.queryConfig.facets).reduce(
(acc, [facetKey, facetConfiguration]) => {
const isDisjunctive =
this.queryConfig.disjunctiveFacets?.includes(facetKey);
if (isDisjunctive && hasSelectedFilters) {
acc[`facet_bucket_${facetKey}`] = {
aggs: {
[facetKey]: transformFacetToAggs(facetKey, facetConfiguration)
},
filter: {
bool: {
must: this.state.filters
.filter(
(filter) =>
filter.field !== facetKey &&
this.queryConfig.facets[filter.field]
)
.map((filter) =>
transformFacet(
filter,
this.queryConfig.facets[filter.field],
this.queryConfig.disjunctiveFacets?.includes(filter.field)
)
)
}
}
};
} else {
acc.facet_bucket_all.aggs = {
...acc.facet_bucket_all.aggs,
[facetKey]: transformFacetToAggs(facetKey, facetConfiguration)
};
}
return acc;
},
{
facet_bucket_all: {
aggs: {},
filter: {
bool: {
must: (this.state.filters || [])
.filter((filter) => this.queryConfig.facets[filter.field])
.map((filter) =>
transformFacet(
filter,
this.queryConfig.facets[filter.field],
this.queryConfig.disjunctiveFacets?.includes(filter.field)
)
)
}
}
}
}
);
}
private buildPostFilter() {
const postFilter = this.state.filters
?.filter((filter) => this.queryConfig.facets[filter.field])
.map((filter) =>
transformFacet(
filter,
this.queryConfig.facets[filter.field],
this.queryConfig.disjunctiveFacets?.includes(filter.field)
)
);
return postFilter?.length ? { bool: { must: postFilter } } : null;
}
private buildQuery(): SearchRequest["query"] | null {
const filters = (this.state.filters || [])
.filter((filter) => !this.queryConfig.facets[filter.field]) // remove filters that are also facets
.concat(this.queryConfig.filters || []) // add filters from the config and do filter even if they are facets
.map(transformFilter);
const searchQuery = this.state.searchTerm;
if (!searchQuery && !filters?.length) {
return null;
}
const fields = getQueryFields(this.queryConfig.search_fields);
return {
bool: {
...(filters?.length && { filter: filters }),
...(searchQuery && {
must: [
{
bool: {
minimum_should_match: 1,
should: [
{
multi_match: {
query: searchQuery,
fields: fields,
type: "best_fields",
operator: "and"
}
},
{
multi_match: {
query: searchQuery,
fields: fields,
type: "cross_fields"
}
},
{
multi_match: {
query: searchQuery,
fields: fields,
type: "phrase"
}
},
{
multi_match: {
query: searchQuery,
fields: fields,
type: "phrase_prefix"
}
}
]
}
}
]
})
}
};
return {
bool: {
...(filters?.length && { filter: filters }),
...(searchQuery
? {
should: [
{
multi_match: {
query: searchQuery,
fields: fields,
type: "best_fields",
operator: "and"
}
},
{
multi_match: {
query: searchQuery,
fields: fields,
type: "cross_fields"
}
},
{
multi_match: {
query: searchQuery,
fields: fields,
type: "phrase"
}
},
{
multi_match: {
query: searchQuery,
fields: fields,
type: "phrase_prefix"
}
}
]
}
: {})
}
};
}
}