searchkit
Version:
Search UI for Elasticsearch.
1,374 lines (1,361 loc) • 45.7 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
ESTransporter: () => ESTransporter,
MatchFilter: () => MatchFilter,
TermFilter: () => TermFilter,
default: () => Searchkit
});
module.exports = __toCommonJS(src_exports);
// src/transformRequest.ts
var import_deepmerge = __toESM(require("deepmerge"));
// src/filterUtils.ts
var TermFilter = (field, value) => {
return { term: { [field]: value } };
};
var MatchFilter = (field, value) => {
return { match: { [field]: value } };
};
// src/utils.ts
var getFacet = (facet_attributes, attributeName) => {
const f = facet_attributes.find((a) => {
if (typeof a === "string") {
return a === attributeName;
}
return a.attribute === attributeName;
});
return f || null;
};
var isNestedFacet = (facet) => {
return typeof facet !== "string" && !!facet.nestedPath;
};
var getFacetFieldType = (facet_attributes, attribute) => {
var _a;
const attributeKey = typeof attribute === "string" ? attribute : attribute.attribute;
if (facet_attributes.includes(attributeKey)) {
return "string";
}
return ((_a = facet_attributes.find((a) => (a == null ? void 0 : a.attribute) === attributeKey)) == null ? void 0 : _a.type) || "string";
};
var getFacetFieldConfig = (facet_attributes, attribute) => {
return facet_attributes.find((a) => {
if (typeof a === "string") {
return false;
}
return a.attribute === attribute;
});
};
var createElasticsearchQueryFromRequest = (requests) => {
return requests.reduce(
(sum, request) => [
...sum,
JSON.stringify({ index: request.indexName }),
"\n",
JSON.stringify(request.body),
"\n"
],
[]
).join("");
};
// src/filters.ts
var transformNumericFilters = (request, config) => {
const { params = {} } = request;
const { numericFilters } = params;
if (!Array.isArray(numericFilters)) {
return [];
}
return numericFilters.reduce((sum, filter) => {
let match, field, operator, value, maxValue = "";
let groups = filter.match(
/([\w\.\_\-]+)\s*(\=|\!\=|\>|\>\=|\<|\<\=)\s*(-?(?:\d+(?:\.\d*)?|\.\d+))/
);
if (groups) {
;
[match, field, operator, value] = groups;
} else {
groups = filter.match(
/([\w\.\_\-]+):\s*(-?(?:\d+(?:\.\d*)?|\.\d+))\s*([Tt][Oo])\s*(-?(?:\d+(?:\.\d*)?|\.\d+))/
);
if (!groups) {
throw new Error(
`Numeric filter "${filter}" could not be parsed. It should either be in the format "attributeName operator operand" or "attributeName: lowerBound TO upperBound"`
);
}
;
[match, field, value, operator, maxValue] = groups;
}
const facetFilterMap = getFacetFilterMap(
config.facet_attributes || [],
config.filter_attributes || []
);
const facetFilterConfig = facetFilterMap[field];
const getFilter = (field2, operator2, value2) => {
if (operator2 === "=") {
return {
term: {
[field2]: value2
}
};
} else if (operator2 === "!=") {
return {
bool: {
must_not: {
term: {
[field2]: value2
}
}
}
};
} else if (operator2 === ">") {
return {
range: {
[field2]: {
gt: value2
}
}
};
} else if (operator2 === ">=") {
return {
range: {
[field2]: {
gte: value2
}
}
};
} else if (operator2 === "<") {
return {
range: {
[field2]: {
lt: value2
}
}
};
} else if (operator2 === "<=") {
return {
range: {
[field2]: {
lte: value2
}
}
};
} else if (operator2.toUpperCase() === "TO") {
return {
range: {
[field2]: {
gte: value2,
lte: maxValue
}
}
};
}
};
const esFilter = [];
if (facetFilterConfig.nestedPath) {
const nestedPathPresent = sum.find((filter2) => {
return filter2.nested.path === facetFilterConfig.nestedPath;
});
if (nestedPathPresent) {
nestedPathPresent.nested.query.bool.filter.push(
getFilter(facetFilterConfig.nestedPath + "." + facetFilterConfig.field, operator, value)
);
} else {
esFilter.push({
nested: {
path: facetFilterConfig.nestedPath,
inner_hits: {},
query: {
bool: {
filter: [
getFilter(
facetFilterConfig.nestedPath + "." + facetFilterConfig.field,
operator,
value
)
]
}
}
}
});
}
} else {
esFilter.push(getFilter(facetFilterConfig.field, operator, value));
}
return [...sum, ...esFilter];
}, []);
};
var getFacetFilterMap = (facets, filters) => {
return [...filters, ...facets].reduce(
(sum, filter) => {
let f = typeof filter === "string" ? { attribute: filter, field: filter, type: "string" } : filter;
return {
...sum,
[f.attribute]: f
};
},
{}
);
};
var transformFacetFilters = (request, config) => {
const { params = {} } = request;
const { facetFilters } = params;
if (!Array.isArray(facetFilters)) {
return [];
}
const facetFilterMap = getFacetFilterMap(
config.facet_attributes || [],
config.filter_attributes || []
);
return facetFilters.reduce((sum, filter) => {
if (Array.isArray(filter)) {
return [
...sum,
{
bool: {
should: filter.reduce((sum2, filter2) => {
const [facet, value] = filter2.split(/:(.*)/);
const facetFilterConfig = facetFilterMap[facet];
if (!facetFilterConfig)
throw new Error(
`Facet "${facet}" not found in configuration. Add configuration to either facet_attributes or filter_attributes.`
);
const field = facetFilterConfig.field;
const filterClauseFn = "filterQuery" in facetFilterConfig && facetFilterConfig.filterQuery ? facetFilterConfig.filterQuery : TermFilter;
if (isNestedFacet(facetFilterConfig)) {
const nestedFilter = sum2.find((filter3) => {
return filter3.nested && filter3.nested.path === facetFilterConfig.nestedPath;
});
if (nestedFilter) {
nestedFilter.nested.query.bool.should.push(
filterClauseFn(
`${facetFilterConfig.nestedPath}.${facetFilterConfig.field}`,
value
)
);
return sum2;
} else {
return [
...sum2,
{
nested: {
inner_hits: {},
path: facetFilterConfig.nestedPath,
query: {
bool: {
should: [
filterClauseFn(
`${facetFilterConfig.nestedPath}.${facetFilterConfig.field}`,
value
)
]
}
}
}
}
];
}
}
return [...sum2, filterClauseFn(field, value)];
}, [])
}
}
];
} else if (typeof filter === "string") {
const [facet, value] = filter.split(/:(.*)/);
const facetFilterConfig = facetFilterMap[facet];
if (!facetFilterConfig)
throw new Error(
`Facet "${facet}" not found in configuration. Add configuration to either facet_attributes or filter_attributes.`
);
const filterClauseFn = "filterQuery" in facetFilterConfig && facetFilterConfig.filterQuery ? facetFilterConfig.filterQuery : TermFilter;
if (isNestedFacet(facetFilterConfig) && facetFilterConfig.nestedPath) {
const nestedFilter = sum.find((filter2) => {
return filter2.nested && filter2.nested.path === facetFilterConfig.nestedPath + ".";
});
if (nestedFilter) {
nestedFilter.nested.query.bool.should.push(
filterClauseFn(`${facetFilterConfig.nestedPath}.${facetFilterConfig.field}`, value)
);
return sum;
} else {
return [
...sum,
{
nested: {
inner_hits: {},
path: facetFilterConfig.nestedPath,
query: {
bool: {
should: [
filterClauseFn(
`${facetFilterConfig.nestedPath}.${facetFilterConfig.field}`,
value
)
]
}
}
}
}
];
}
}
return [...sum, filterClauseFn(facetFilterConfig.field, value)];
}
}, []);
};
var transformQueryString = (facets = [], filters = [], queryString) => {
const regex = /([\w\.\-]+)\:/gi;
const filterMap = getFacetFilterMap(facets, filters);
return queryString.replace(regex, (match, word) => {
if (!filterMap[word]) {
throw new Error(
`Attribute "${word}" is not defined as an attribute in the facet or filter search settings`
);
}
if (!!filterMap[word].nestedPath) {
throw new Error(
`Attribute "${word}" is a nested field and cannot be used as a filter. Nested fields are supported in facetFilers or numericFilters.`
);
}
return filterMap[word].field + ":";
});
};
var transformBaseFilters = (request, config) => {
const { params = {} } = request;
const { filters } = params;
if (!filters || filters === "") {
return [];
}
const queryString = transformQueryString(
config.facet_attributes,
config.filter_attributes,
filters
);
return [
{
query_string: {
query: queryString
}
}
];
};
var transformGeoFilters = (request, config) => {
if (!config.geo_attribute) {
return [];
}
const { params = {} } = request;
const { aroundLatLng, aroundRadius, insideBoundingBox } = params;
if (insideBoundingBox) {
return [insideBoundingBoxFilter(insideBoundingBox, config.geo_attribute)];
}
if (aroundLatLng) {
const geoPoint = aroundLatLng.split(",");
return [
{
geo_distance: {
distance: aroundRadius || "1000m",
[config.geo_attribute]: {
lat: geoPoint[0],
lon: geoPoint[1]
}
}
}
];
}
return [];
};
function insideBoundingBoxFilter(insideBoundingBox, field) {
const geoBoundingboxFilter = (top, left, bottom, right) => {
return {
geo_bounding_box: {
[field]: {
top_right: {
lat: top,
lon: left
},
bottom_left: {
lat: bottom,
lon: right
}
}
}
};
};
if (typeof insideBoundingBox === "string") {
const [top, left, bottom, right] = insideBoundingBox.split(",");
return geoBoundingboxFilter(
parseFloat(top),
parseFloat(left),
parseFloat(bottom),
parseFloat(right)
);
} else if (Array.isArray(insideBoundingBox)) {
const geoBoundingboxes = insideBoundingBox.map((boundingBox) => {
const [top, left, bottom, right] = boundingBox;
return geoBoundingboxFilter(
parseFloat(top),
parseFloat(left),
parseFloat(bottom),
parseFloat(right)
);
});
return {
bool: {
should: geoBoundingboxes
}
};
}
}
// src/sorting.ts
function getSorting(request, config) {
if (config.sorting && Object.keys(config.sorting).length > 0) {
const selectedSorting = Object.keys(config.sorting).find((key) => {
if (request.indexName.endsWith(key)) {
return true;
}
});
if (!selectedSorting && !config.sorting.default)
return {};
const sortOption = selectedSorting ? config.sorting[selectedSorting] : config.sorting.default;
if (Array.isArray(sortOption)) {
return {
sort: sortOption.map((sorting) => {
return {
[sorting.field]: sorting.order
};
})
};
} else {
return {
sort: {
[sortOption.field]: sortOption.order
}
};
}
}
return {};
}
function getIndexName(indexName, config) {
if (config.sorting && Object.keys(config.sorting).length > 0) {
const selectedSorting = Object.keys(config.sorting).find((key) => {
if (indexName.endsWith(key)) {
return true;
}
});
if (selectedSorting) {
return indexName.replace(selectedSorting, "");
}
}
return indexName;
}
// src/transformRequest.ts
var createRegexQuery = (queryString) => {
let query = queryString.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
query = query.split("").map((char) => {
if (/[a-z]/.test(char)) {
return `[${char}${char.toUpperCase()}]`;
}
return char;
}).join("");
query = `${query}.*`;
if (queryString.length > 2) {
query = `([a-zA-Z]+ )+?${query}`;
}
return query;
};
var TermAggregation = (field, size, search) => {
const searchInclude = search && search.length > 0 ? { include: createRegexQuery(search) } : {};
return {
terms: {
field,
size,
...searchInclude
}
};
};
var getTermAggregation = (facet, size, search) => {
let aggEntries = {};
const AggregationFn = typeof facet !== "string" && "facetQuery" in facet ? facet.facetQuery : TermAggregation;
const getInnerAggs = (facetName, field) => {
if (typeof facet === "string" || facet.type === "string") {
aggEntries = {
[facetName]: AggregationFn(field, size, search)
};
} else if (facet.type === "numeric") {
aggEntries = {
[facetName + "$_stats"]: {
stats: {
field
}
},
[facetName + "$_entries"]: AggregationFn(field, size, search)
};
}
return aggEntries;
};
if (typeof facet === "string") {
return getInnerAggs(facet, facet);
} else if (isNestedFacet(facet)) {
return {
[`${facet.nestedPath}.`]: {
nested: {
path: facet.nestedPath
},
aggs: getInnerAggs(facet.attribute, `${facet.nestedPath}.${facet.field}`)
}
};
} else {
return getInnerAggs(facet.attribute, facet.field);
}
};
var getAggs = (request, config, queryRuleActions) => {
const { params = {}, type } = request;
const { facets, maxValuesPerFacet, facetName, facetQuery } = params;
const maxFacetSize = maxValuesPerFacet || 10;
const facetAttributes = config.facet_attributes || [];
if (facetName) {
const facet = getFacet(facetAttributes, facetName);
if (!facet)
return null;
return getTermAggregation(facet, maxFacetSize, facetQuery);
} else if (Array.isArray(facets)) {
let facetAttibutes = config.facet_attributes || [];
if (queryRuleActions.facetAttributesOrder) {
facetAttibutes = queryRuleActions.facetAttributesOrder.map((attribute) => {
return getFacet(config.facet_attributes || [], attribute);
}).filter((x) => x !== null);
}
const facetAttributes2 = facets[0] === "*" ? facetAttibutes : facets.map((facetAttribute) => {
return getFacet(config.facet_attributes || [], facetAttribute);
}).filter((x) => x !== null);
return facetAttributes2.reduce((sum, facet) => {
return (0, import_deepmerge.default)(sum, getTermAggregation(facet, maxFacetSize, ""));
}, {}) || {};
} else if (typeof facets === "string") {
const field = getFacet(config.facet_attributes || [], facets);
if (!field)
return {};
return getTermAggregation(field, maxFacetSize, "");
}
};
function queryRulesWrapper(organicQuery, queryRuleActions) {
if (queryRuleActions.touched) {
return {
function_score: {
query: {
pinned: {
ids: queryRuleActions.pinnedDocs,
organic: organicQuery
}
},
functions: queryRuleActions.boostFunctions
}
};
}
return organicQuery;
}
function RelevanceQueryMatch(query, search_attributes) {
const getFieldsMap = (boostMultiplier) => {
return search_attributes.map((attribute) => {
return typeof attribute === "string" ? attribute : `${attribute.field}^${(attribute.weight || 1) * boostMultiplier}`;
});
};
return {
bool: {
should: [
{
bool: {
should: [
{
multi_match: {
query,
fields: getFieldsMap(1),
fuzziness: "AUTO:4,8"
}
},
{
multi_match: {
query,
fields: getFieldsMap(0.5),
type: "bool_prefix"
}
}
]
}
},
{
multi_match: {
query,
type: "phrase",
fields: getFieldsMap(2)
}
}
]
}
};
}
var getQuery = (request, config, queryRuleActions, requestOptions) => {
var _a, _b, _c;
const query = queryRuleActions.query;
const searchAttributes = config.search_attributes;
const filters = [
...transformFacetFilters(request, config),
...transformNumericFilters(request, config),
...transformBaseFilters(request, config),
...transformGeoFilters(request, config),
...((_a = requestOptions == null ? void 0 : requestOptions.getBaseFilters) == null ? void 0 : _a.call(requestOptions)) || [],
...queryRuleActions.baseFilters
];
let organicQuery = typeof query === "string" && query !== "" ? (requestOptions == null ? void 0 : requestOptions.getQuery) ? requestOptions.getQuery(query, searchAttributes, config) : RelevanceQueryMatch(query, searchAttributes) : {
match_all: {}
};
const hasKnn = typeof (requestOptions == null ? void 0 : requestOptions.getKnnQuery) === "function";
const hasNoQuery = ((_b = requestOptions == null ? void 0 : requestOptions.getQuery) == null ? void 0 : _b.call(requestOptions, query, searchAttributes, config)) === false;
if (hasNoQuery || hasKnn && query === "") {
organicQuery = {
match_all: {}
};
}
const queryDsl = {
bool: {
filter: filters,
must: queryRuleActions.touched ? queryRulesWrapper(organicQuery, queryRuleActions) : organicQuery
}
};
let knnQueryDsl = null;
if (hasKnn && query !== "") {
knnQueryDsl = {
filter: filters,
...((_c = requestOptions == null ? void 0 : requestOptions.getKnnQuery) == null ? void 0 : _c.call(requestOptions, query, searchAttributes, config)) || {}
};
}
if (query !== "" && hasNoQuery && hasKnn && knnQueryDsl) {
return {
knn: knnQueryDsl
};
}
const size = getHitsPerPage(request);
return {
query: queryDsl,
knn: knnQueryDsl ? knnQueryDsl : void 0,
// in hybrid mode (knn + keyword query), is displaying results and query is not empty
rank: hasKnn && !hasNoQuery && size > 0 && query !== "" ? { rrf: { window_size: size } } : void 0
};
};
var getHitsPerPage = (request) => {
const { params = {} } = request;
return params.hitsPerPage == null ? 20 : params.hitsPerPage;
};
var getResultsSize = (request, config) => {
const { params = {} } = request;
const hitsPerPage = getHitsPerPage(request);
return {
size: hitsPerPage,
from: (params.page || 0) * hitsPerPage
};
};
var getHitFields = (request, config) => {
const { params = {} } = request;
const { attributesToRetrieve } = params;
const sourceFields = /* @__PURE__ */ new Set([
...config.result_attributes || [],
...config.highlight_attributes || [],
...config.geo_attribute ? [config.geo_attribute] : []
]);
const runtimeFields = Object.keys(config.runtime_mappings || {});
const fields = runtimeFields.reduce((sum, field) => {
var _a;
if ((_a = config.result_attributes) == null ? void 0 : _a.includes(field)) {
return [field, ...sum];
}
return sum;
}, []);
return {
_source: {
includes: Array.from(sourceFields)
},
...fields.length > 0 ? { fields } : {}
};
};
var getSnippetFieldLength = (attribute) => {
const defaultMatch = {
attribute,
length: 100
};
if (!attribute.includes(":")) {
return defaultMatch;
}
const match = attribute.match(/(.+)\:(\d+)/);
if (!match)
return defaultMatch;
return {
attribute: match[1],
length: parseInt(match[2])
};
};
var getHighlightFields = (request, config) => {
var _a, _b;
const { params = {} } = request;
const { attributesToHighlight } = params;
const highlightFields = ((_a = config.highlight_attributes) == null ? void 0 : _a.reduce(
(sum, field) => ({
...sum,
[field]: {
number_of_fragments: 0
}
}),
{}
)) || {};
const snippetFields = ((_b = config.snippet_attributes) == null ? void 0 : _b.reduce(
(sum, attribute) => ({
...sum,
[getSnippetFieldLength(attribute).attribute]: {
number_of_fragments: 5,
fragment_size: getSnippetFieldLength(attribute).length
}
}),
{}
)) || {};
if (Object.keys(highlightFields).length === 0 && Object.keys(snippetFields).length === 0) {
return {};
}
return {
highlight: {
pre_tags: ["<em>"],
post_tags: ["</em>"],
fields: {
...highlightFields,
...snippetFields
}
}
};
};
var getRuntimeMappings = (request, config) => {
if (!config.runtime_mappings) {
return {};
}
return {
runtime_mappings: config.runtime_mappings
};
};
function transformRequest(request, config, queryRuleActions, requestOptions) {
const body = {
aggs: getAggs(request, config, queryRuleActions),
...getQuery(request, config, queryRuleActions, requestOptions),
...getResultsSize(request, config),
...getHitFields(request, config),
...getHighlightFields(request, config),
...getSorting(request, config),
...getRuntimeMappings(request, config)
};
return body;
}
// src/highlightUtils.ts
function highlightTerm(value, query) {
const regex = new RegExp(query, "gi");
return value.replace(regex, (match) => `<em>${match}</em>`);
}
function isAllowableHighlightField(fieldKey, highlightFields) {
return highlightFields.findIndex((highlightField) => {
if (highlightField.indexOf("*") < 0) {
return highlightField === fieldKey;
}
const safeHighlightField = highlightField.replace(/[.+?^$|\{\}\(\)\[\]\\]/g, "\\$&");
const regex = new RegExp(`^${safeHighlightField.replace(/\*/g, ".*")}$`);
return regex.test(fieldKey);
}) >= 0;
}
function transformObject(input) {
const result = {};
for (const key in input) {
const keys = key.split(".");
let currentObj = result;
for (let i = 0; i < keys.length - 1; i++) {
const currentKey = keys[i];
if (!currentObj[currentKey]) {
currentObj[currentKey] = {};
}
currentObj = currentObj[currentKey];
}
currentObj[keys[keys.length - 1]] = input[key];
}
return result;
}
function getFieldValue(obj, path) {
return path.split(".").reduce((acc, key) => {
if (Array.isArray(acc)) {
return acc.map((item) => item[key]);
}
return acc && Object.prototype.hasOwnProperty.call(acc, key) ? acc[key] : void 0;
}, obj);
}
function getHighlightFields2(hit, preTag = "<ais-highlight-0000000000>", postTag = "<ais-highlight-0000000000/>", fields = []) {
const { _source = {}, highlight = {} } = hit;
const combinedKeys = {
..._source,
...highlight
};
const highlightFields = fields.map((field) => getSnippetFieldLength(field).attribute);
const hitHighlights = Object.keys(combinedKeys).reduce((sum, fieldKey) => {
const fieldValue = getFieldValue(_source, fieldKey);
const highlightedMatch = highlight[fieldKey] || null;
if (!isAllowableHighlightField(fieldKey, highlightFields)) {
return sum;
}
if (Array.isArray(fieldValue) && !highlightedMatch) {
return {
...sum,
[fieldKey]: fieldValue.map((value) => ({
matchLevel: "none",
matchedWords: [],
value: value.toString()
}))
};
} else if (Array.isArray(fieldValue) && highlightedMatch && Array.isArray(highlightedMatch)) {
return {
...sum,
[fieldKey]: highlightedMatch.map((highlightedMatch2) => {
const matchWords = Array.from(highlightedMatch2.matchAll(/\<em\>(.*?)\<\/em\>/g)).map(
(match) => match[1]
);
return {
fullyHighlighted: false,
matchLevel: "full",
matchedWords: matchWords,
value: highlightedMatch2.toString().replace(/\<em\>/g, preTag).replace(/\<\/em\>/g, postTag)
};
})
};
} else if (!Array.isArray(fieldValue) && highlightedMatch && Array.isArray(highlightedMatch) || !fieldValue && Array.isArray(highlightedMatch) && highlightedMatch.length > 0) {
const singleMatch = highlightedMatch[0];
const matchWords = Array.from(singleMatch.matchAll(/\<em\>(.*?)\<\/em\>/g)).map(
(match) => match[1]
);
const x = {
fullyHighlighted: false,
matchLevel: "full",
matchedWords: matchWords,
value: singleMatch.toString().replace(/\<em\>/g, preTag).replace(/\<\/em\>/g, postTag)
};
return {
...sum,
[fieldKey]: x
};
}
return {
...sum,
[fieldKey]: {
matchLevel: "none",
matchedWords: [],
value: fieldValue != void 0 ? fieldValue.toString() : ""
}
};
}, {});
return transformObject(hitHighlights);
}
// src/transformResponse.ts
var getHits = (response, config, instantsearchRequest) => {
const { hits } = response;
const { highlight_attributes = [], snippet_attributes = [] } = config;
return hits.hits.map((hit) => {
var _a, _b, _c, _d, _e, _f;
return {
objectID: hit._id,
_index: hit._index,
_score: hit._score,
...hit._source || {},
...hit.fields || {},
// for runtime_mapping fields
...hit.inner_hits ? { inner_hits: hit.inner_hits } : {},
...highlight_attributes.length > 0 ? {
_highlightResult: getHighlightFields2(
hit,
(_a = instantsearchRequest == null ? void 0 : instantsearchRequest.params) == null ? void 0 : _a.highlightPreTag,
(_b = instantsearchRequest == null ? void 0 : instantsearchRequest.params) == null ? void 0 : _b.highlightPostTag,
highlight_attributes
)
} : {},
...snippet_attributes.length > 0 ? {
_snippetResult: getHighlightFields2(
hit,
(_c = instantsearchRequest == null ? void 0 : instantsearchRequest.params) == null ? void 0 : _c.highlightPreTag,
(_d = instantsearchRequest == null ? void 0 : instantsearchRequest.params) == null ? void 0 : _d.highlightPostTag,
config.snippet_attributes
)
} : {},
...config.geo_attribute && ((_e = hit._source) == null ? void 0 : _e[config.geo_attribute]) ? { _geoloc: convertLatLng((_f = hit._source) == null ? void 0 : _f[config.geo_attribute]) } : {}
};
});
};
function convertLatLng(value) {
if (typeof value === "string") {
const [lat, lng] = value.split(",").map((v) => parseFloat(v));
return { lat, lng };
} else if (Array.isArray(value)) {
return { lat: value[0], lng: value[1] };
} else if (typeof value === "object") {
if ("lat" in value && "lon" in value) {
return {
lat: parseFloat(value.lat),
lng: parseFloat(value.lon)
};
}
}
return null;
}
var TermFacetResponse = (aggregation) => {
return aggregation.buckets.reduce(
(sum, bucket) => ({
...sum,
[bucket.key]: bucket.doc_count
}),
{}
);
};
var getFacets = (response, config) => {
if (!(response == null ? void 0 : response.aggregations)) {
return {};
}
const aggregations = Object.keys(response.aggregations).reduce(
(sum, key) => {
const value = (response.aggregations || {})[key];
if (key.endsWith(".")) {
const { doc_count, ...nestedAggregations } = value;
return {
...sum,
...nestedAggregations
};
}
return {
...sum,
[key]: value
};
},
{}
);
return Object.keys(aggregations).reduce(
(sum, f) => {
const facet = f.split("$")[0];
const fieldType = getFacetFieldType(config.facet_attributes || [], facet);
const facetConfig = getFacetFieldConfig(config.facet_attributes || [], facet);
const facetResponse = facetConfig && "facetResponse" in facetConfig && (facetConfig == null ? void 0 : facetConfig.facetResponse) || TermFacetResponse;
if (fieldType === "numeric") {
const facetValues = aggregations[facet + "$_stats"];
const { buckets: buckets2 } = aggregations[facet + "$_entries"];
return {
...sum,
facets: {
...sum.facets,
[facet]: facetResponse({ buckets: buckets2 })
},
facets_stats: {
...sum.facets_stats,
[facet]: {
min: facetValues.min,
avg: facetValues.avg,
max: facetValues.max,
sum: facetValues.sum
}
}
};
}
const { buckets } = aggregations[facet];
return {
...sum,
facets: {
...sum.facets,
[facet]: facetResponse({ buckets })
}
};
},
{
facets: {},
facets_stats: {}
}
);
};
var getRenderingContent = (config, queryRuleActions) => {
var _a, _b;
const defaultOrder = (_a = config.facet_attributes) == null ? void 0 : _a.map(
(facet) => typeof facet === "string" ? facet : facet.attribute
);
return {
renderingContent: {
facetOrdering: {
facets: {
order: queryRuleActions.facetAttributesOrder || defaultOrder || []
},
values: (_b = config.facet_attributes) == null ? void 0 : _b.reduce(
(sum, facet) => {
const facetName = typeof facet === "string" ? facet : facet.attribute;
if (queryRuleActions.facetAttributesOrder && !queryRuleActions.facetAttributesOrder.includes(facetName)) {
return sum;
}
return {
...sum,
[facetName]: {
sortRemainingBy: "count"
}
};
},
{}
)
}
}
};
};
var getPageDetails = (response, request, queryRuleActions) => {
const { params = {} } = request;
const { hitsPerPage = 20, page = 0 } = params;
const { total } = response.hits;
const totalHits = typeof total === "number" ? total : total == null ? void 0 : total.value;
const nbPages = hitsPerPage <= 0 ? 0 : Math.ceil((typeof total === "number" ? total : (total == null ? void 0 : total.value) || 0) / hitsPerPage);
return {
hitsPerPage,
processingTimeMS: response.took,
nbHits: totalHits,
page,
nbPages,
query: queryRuleActions.query
};
};
function transformResponse(response, instantsearchRequest, config, queryRuleActions) {
try {
return {
appliedRules: queryRuleActions.ruleIds,
exhaustiveNbHits: true,
exhaustiveFacetsCount: true,
exhaustiveTypo: true,
exhaustive: { facetsCount: true, nbHits: true, typo: true },
...getPageDetails(response, instantsearchRequest, queryRuleActions),
...getRenderingContent(config, queryRuleActions),
...getFacets(response, config),
hits: getHits(response, config, instantsearchRequest),
index: instantsearchRequest.indexName,
params: new URLSearchParams(instantsearchRequest.params).toString(),
...queryRuleActions.userData.length > 0 ? { userData: queryRuleActions.userData } : {}
};
} catch (e) {
throw new Error(`Error transforming Elasticsearch response for index`);
}
}
var transformFacetValuesResponse = (response, instantsearchRequest) => {
var _a, _b, _c;
const aggregations = response.aggregations || {};
const facetName = (_a = instantsearchRequest == null ? void 0 : instantsearchRequest.params) == null ? void 0 : _a.facetName;
const preTag = ((_b = instantsearchRequest.params) == null ? void 0 : _b.highlightPreTag) || "<ais-highlight-0000000000>";
const postTag = ((_c = instantsearchRequest.params) == null ? void 0 : _c.highlightPostTag) || "<ais-highlight-0000000000/>";
let agg = aggregations[Object.keys(aggregations)[0]];
if (agg && agg[facetName]) {
agg = agg[facetName];
}
return {
facetHits: agg.buckets.map((entry) => ({
value: entry.key,
highlighted: highlightTerm(
entry.key,
// @ts-ignore
instantsearchRequest.params.facetQuery || ""
).replace(/<\em>/g, preTag).replace(/<\/\em>/g, postTag),
count: entry.doc_count
})),
exhaustiveFacetsCount: true,
processingTimeMS: response.took
};
};
// src/Transporter.ts
var authString = (auth) => {
if (typeof btoa === "undefined") {
return Buffer.from(auth.username + ":" + auth.password).toString("base64");
} else {
return btoa(auth.username + ":" + auth.password);
}
};
function getHostFromCloud(cloudId) {
let cloudUrls;
if (typeof atob === "undefined") {
cloudUrls = Buffer.from(cloudId.split(":")[1], "base64").toString();
} else {
cloudUrls = atob(cloudId.split(":")[1]).split("$");
}
return `https://${cloudUrls[1]}.${cloudUrls[0]}`;
}
var ESTransporter = class {
constructor(config, settings) {
this.config = config;
this.settings = settings;
}
createElasticsearchQueryFromRequest(requests) {
return createElasticsearchQueryFromRequest(requests);
}
async performNetworkRequest(requests) {
if (this.config.host === void 0 && this.config.cloud_id === void 0) {
throw new Error(
"No Elasticsearch host or cloud_id specified. Please provide a host or cloud id in your Searchkit configuration."
);
}
const host = this.config.cloud_id ? getHostFromCloud(this.config.cloud_id) : this.config.host;
return fetch(`${host}/_msearch`, {
headers: {
...this.config.apiKey ? { authorization: `ApiKey ${this.config.apiKey}` } : {},
"content-type": "application/json",
...this.config.headers || {},
...this.config.auth ? {
Authorization: "Basic " + authString(this.config.auth)
} : {}
},
body: this.createElasticsearchQueryFromRequest(requests),
method: "POST",
...this.config.withCredentials ? { credentials: "include" } : {}
});
}
async msearch(requests) {
var _a, _b, _c, _d, _e, _f;
try {
const response = await this.performNetworkRequest(requests);
const responses = await response.json();
if (this.settings.debug) {
console.log("Elasticsearch response:");
console.log(JSON.stringify(responses));
}
if (responses.status >= 500) {
console.error(
"Elasticsearch Internal Error: Check your elasticsearch instance to make sure it can recieve requests."
);
throw new Error(JSON.stringify(responses));
} else if (responses.status === 401) {
console.error(
"Cannot connect to Elasticsearch. Check your connection host and auth details (username/password or API Key required). You can also provide a custom Elasticsearch transporter to the API Client. See https://www.searchkit.co/docs/guides/setup-elasticsearch#connecting-with-usernamepassword for more details."
);
throw new Error(JSON.stringify(responses));
} else if (((_b = (_a = responses.responses) == null ? void 0 : _a[0]) == null ? void 0 : _b.status) === 403) {
console.error(
"Auth Error: You do not have permission to access this index. Check you are calling the right index (specified in frontend) and your API Key permissions has access to the index."
);
throw new Error(JSON.stringify(responses));
} else if (responses.status === 404 || ((_d = (_c = responses.responses) == null ? void 0 : _c[0]) == null ? void 0 : _d.status) === 404) {
console.error("Elasticsearch index not found. Check your index name and make sure it exists.");
throw new Error(JSON.stringify(responses));
} else if (responses.status === 400 || ((_f = (_e = responses.responses) == null ? void 0 : _e[0]) == null ? void 0 : _f.status) === 400) {
console.error(
`Elasticsearch Bad Request.
1. Check your query and make sure it is valid.
2. Check the field mapping. See documentation to make sure you are using text types for searching and keyword fields for faceting
3. Turn on debug mode to see the Elasticsearch query and the error response.
`
);
throw new Error(JSON.stringify(responses));
}
return responses.responses;
} catch (error) {
throw error;
}
}
};
// src/queryRules.ts
var getFacetFilters = (facetFilters) => {
if (!facetFilters) {
return [];
}
if (typeof facetFilters === "string") {
const [attribute, value] = facetFilters.split(/:(.*)/);
return [{ attribute, value }];
} else {
return facetFilters.reduce((sum, filter) => {
if (typeof filter === "string") {
const [attribute, value] = filter.split(/:(.*)/);
return [...sum, { attribute, value }];
}
return [...sum, ...getFacetFilters(filter)];
}, []);
}
};
var getQueryRulesActionsFromRequest = (queryRules, request, config) => {
var _a, _b, _c;
const queryContext = {
query: ((_a = request.params) == null ? void 0 : _a.query) || "",
context: ((_b = request.params) == null ? void 0 : _b.ruleContexts) || [],
filters: getFacetFilters((_c = request.params) == null ? void 0 : _c.facetFilters)
};
const satisfiedRules = getSatisfiedRules(queryContext, queryRules || []);
const actions = satisfiedRules.reduce(
(sum, rule) => {
rule.actions.map((action) => {
sum.touched = true;
if (action.action === "PinnedResult") {
sum.pinnedDocs.push(...action.documentIds);
} else if (action.action === "QueryRewrite") {
sum.query = action.query;
} else if (action.action === "QueryBoost") {
sum.boostFunctions.push({
filter: {
query_string: {
query: transformQueryString(
config.facet_attributes,
config.filter_attributes,
action.query
)
}
},
weight: action.weight
});
} else if (action.action === "RenderUserData") {
sum.userData.push(JSON.parse(action.userData));
} else if (action.action === "RenderFacetsOrder") {
sum.facetAttributesOrder = action.facetAttributesOrder;
} else if (action.action === "QueryFilter") {
sum.baseFilters.push({
query_string: {
query: transformQueryString(
config.facet_attributes,
config.filter_attributes,
action.query
)
}
});
}
});
return sum;
},
{
ruleIds: satisfiedRules.map((rule) => rule.id),
pinnedDocs: [],
boostFunctions: [],
query: queryContext.query,
userData: [],
facetAttributesOrder: void 0,
touched: false,
baseFilters: []
}
);
return actions;
};
var getSatisfiedRules = (queryContext, rules) => rules.filter(
(ruleOrs) => ruleOrs.conditions.find(
(rule) => rule.filter((condition) => {
if (condition.context === "query" && condition.match_type === "exact") {
return condition.value === queryContext.query;
}
if (condition.context === "query" && condition.match_type === "contains") {
return queryContext.query.includes(condition.value);
}
if (condition.context === "query" && condition.match_type === "prefix") {
return queryContext.query.startsWith(condition.value);
}
if (condition.context === "context") {
return condition.value.some((value) => queryContext.context.includes(value));
}
if (condition.context === "filterPresent") {
return condition.values.every(
(value) => queryContext.filters.find(
(filter) => filter.attribute === value.attribute && filter.value === value.value
) !== void 0
);
}
return false;
}).length === rule.length
) !== void 0
);
// src/index.ts
var Searchkit = class {
constructor(config, settings = { debug: false }) {
this.config = config;
this.settings = settings;
this.transporter = "msearch" in config.connection ? config.connection : new ESTransporter(config.connection, settings);
}
async performSearch(requests) {
if (this.settings.debug) {
console.log("Performing search with requests:");
console.log("POST /_msearch");
console.log(createElasticsearchQueryFromRequest(requests));
}
const responses = await this.transporter.msearch(requests);
return responses;
}
async handleInstantSearchRequests(instantsearchRequests, requestOptions) {
var _a, _b;
if (!instantsearchRequests || Array.isArray(instantsearchRequests) === false) {
console.log({ instantsearchRequests });
throw new Error(
"No instantsearch requests provided. Check that the data you are providing from API request is correct. Likely you are not passing the request body correctly, its still a JSON string or the API is not a POST request."
);
}
const queryRules = this.config.search_settings.query_rules || [];
const requestQueryRuleActions = instantsearchRequests.map((request) => {
return getQueryRulesActionsFromRequest(queryRules, request, this.config.search_settings);
});
let esRequests = instantsearchRequests.map((request, i) => ({
body: transformRequest(
request,
this.config.search_settings,
requestQueryRuleActions[i],
requestOptions
),
request,
indexName: getIndexName(request.indexName, this.config.search_settings)
}));
if ((_a = requestOptions == null ? void 0 : requestOptions.hooks) == null ? void 0 : _a.beforeSearch) {
esRequests = await requestOptions.hooks.beforeSearch(esRequests);
}
let esResponses = await this.performSearch(esRequests);
if ((_b = requestOptions == null ? void 0 : requestOptions.hooks) == null ? void 0 : _b.afterSearch) {
esResponses = await requestOptions.hooks.afterSearch(esRequests, esResponses);
}
try {
const instantsearchResponses = esResponses.map((response, i) => {
var _a2;
if ((_a2 = instantsearchRequests[i].params) == null ? void 0 : _a2.facetName) {
return transformFacetValuesResponse(response, instantsearchRequests[i]);
}
return transformResponse(
response,
instantsearchRequests[i],
this.config.search_settings,
requestQueryRuleActions[i]
);
});
return {
results: instantsearchResponses
};
} catch (err) {
console.error(err);
throw new Error(
"Error transforming response. Check the afterSearch hook function is correct. Likely you are not returning the correct response object."
);
}
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ESTransporter,
MatchFilter,
TermFilter
});
//# sourceMappingURL=index.js.map