diff --git a/src/facets.js b/src/facets.js index 4ef3513..22f0684 100644 --- a/src/facets.js +++ b/src/facets.js @@ -1,4 +1,4 @@ -import { map, mapValues, clone, keys } from 'lodash-es'; +import { map, keys } from 'lodash-es'; import FastBitSet from 'fastbitset'; import { facets_ids, @@ -71,64 +71,50 @@ export class Facets { return this._items_map[_id]; } - /* - * - * ids is optional only when there is query - */ - search(input, data) { + search(input, data = {}) { const config = this.config; - data = data || Object.create(null); - - // consider removing clone - const temp_facet = clone(this.facets); + const temp_facet = {}; temp_facet.not_ids = facets_ids( - temp_facet['bits_data'], + this.facets.bits_data, input.not_filters ); - let temp_data; - const filters = input_to_facet_filters(input, config); - temp_data = matrix(this.facets, filters); + let temp_data = matrix(this.facets, filters); if (input.filters_query) { - const filters = parse_boolean_query(input.filters_query); - temp_data = filters_matrix(temp_data, filters); + const filtersQuery = parse_boolean_query(input.filters_query); + temp_data = filters_matrix(temp_data, filtersQuery); } - temp_facet['bits_data_temp'] = temp_data['bits_data_temp']; - - mapValues(temp_facet['bits_data_temp'], function (values, key) { - mapValues( - temp_facet['bits_data_temp'][key], - function (facet_indexes, key2) { - if (data.query_ids) { - temp_facet['bits_data_temp'][key][key2] = - data.query_ids.new_intersection( - temp_facet['bits_data_temp'][key][key2] - ); - } - - if (data.test) { - temp_facet['data'][key][key2] = - temp_facet['bits_data_temp'][key][key2].array(); - } + temp_facet.bits_data_temp = temp_data.bits_data_temp; + const bitsDataTemp = temp_facet.bits_data_temp; + + if (data.query_ids) { + for (const key in bitsDataTemp) { + for (const key2 in bitsDataTemp[key]) { + bitsDataTemp[key][key2] = data.query_ids.new_intersection( + bitsDataTemp[key][key2] + ); } - ); - }); + } + } - /** - * calculating ids (for a list of items) - * facets ids is faster and filter ids because filter ids makes union each to each filters - * filter ids needs to be used if there is filters query - */ - if (input.filters_query) { - temp_facet.ids = filters_ids(temp_facet['bits_data_temp']); - } else { - temp_facet.ids = facets_ids(temp_facet['bits_data_temp'], input.filters); + if (data.test) { + temp_facet.data = {}; + for (const key in bitsDataTemp) { + temp_facet.data[key] = {}; + for (const key2 in bitsDataTemp[key]) { + temp_facet.data[key][key2] = bitsDataTemp[key][key2].array(); + } + } } + temp_facet.ids = input.filters_query + ? filters_ids(bitsDataTemp) + : facets_ids(bitsDataTemp, input.filters); + return temp_facet; } } diff --git a/src/helpers.js b/src/helpers.js index 026a916..aecc87b 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -1,7 +1,6 @@ import { mapValues, clone as _clone, - sortBy, isArray, orderBy, minBy, @@ -12,7 +11,7 @@ import { import FastBitSet from 'fastbitset'; import booleanParser from 'boolean-parser'; -export const clone = function (val) { +export const clone = function(val) { try { return structuredClone(val); } catch (e) { @@ -24,32 +23,32 @@ export const clone = function (val) { } }; -export const humanize = function (str) { +export const humanize = function(str) { return str .replace(/^[\s_]+|[\s_]+$/g, '') .replace(/[_\s]+/g, ' ') - .replace(/^[a-z]/, function (m) { + .replace(/^[a-z]/, function(m) { return m.toUpperCase(); }); }; -export const combination_indexes = function (facets, filters) { - const indexes = Object.create(null); +export const combination_indexes = function(facets, filters) { + const indexes = {}; - mapValues(filters, function (filter) { - // filter is still array so disjunctive + filters.forEach(filter => { if (Array.isArray(filter[0])) { - let facet_union = new FastBitSet([]); - const filter_keys = []; - - mapValues(filter, function (disjunctive_filter) { - const filter_key = disjunctive_filter[0]; - const filter_val = disjunctive_filter[1]; + let facet_union = new FastBitSet(); + const filter_keys = new Set(); + + filter.forEach(disjunctive_filter => { + const [filter_key, filter_val] = disjunctive_filter; + filter_keys.add(filter_key); + const bits = + facets.bits_data[filter_key]?.[filter_val] || new FastBitSet(); + facet_union = facet_union.new_union(bits); + }); - filter_keys.push(filter_key); - facet_union = facet_union.new_union( - facets['bits_data'][filter_key][filter_val] || new FastBitSet([]) - ); + filter_keys.forEach(filter_key => { indexes[filter_key] = facet_union; }); } @@ -58,251 +57,200 @@ export const combination_indexes = function (facets, filters) { return indexes; }; -export const filters_matrix = function (facets, query_filters) { +export const filters_matrix = function(facets, query_filters) { + // We use cloneDeepWith with a custom cloning function + // const temp_facet = _.cloneDeepWith(facets, customClone); const temp_facet = _clone(facets); - if (!temp_facet['is_temp_copied']) { - mapValues(temp_facet['bits_data'], function (values, key) { - mapValues(temp_facet['bits_data'][key], function (facet_indexes, key2) { - temp_facet['bits_data_temp'][key][key2] = - temp_facet['bits_data'][key][key2]; - }); - }); + // Initialize bits_data_temp if it does not exist + if (!temp_facet.bits_data_temp) { + temp_facet.bits_data_temp = {}; + } + + if (!temp_facet.is_temp_copied) { + for (const key in temp_facet.bits_data) { + temp_facet.bits_data_temp[key] = {}; + for (const key2 in temp_facet.bits_data[key]) { + temp_facet.bits_data_temp[key][key2] = temp_facet.bits_data[key][key2]; + } + } } let union = null; - /** - * process only conjunctive filters - */ - mapValues(query_filters, function (conjunction) { - let conjunctive_index = null; + // Ensure that query_filters is an array + if (Array.isArray(query_filters)) { + // Processing conjunctive filters + query_filters.forEach(conjunction => { + let conjunctive_index = null; - mapValues(conjunction, function (filter) { - const filter_key = filter[0]; - const filter_val = filter[1]; + conjunction.forEach(filter => { + const [filter_key, filter_val] = filter; - if (!temp_facet['bits_data_temp'][filter_key]) { - throw new Error('Panic. The key does not exist in facets lists.'); - } + if (!temp_facet.bits_data_temp[filter_key]) { + throw new Error('Panic. The key does not exist in facets lists.'); + } - if ( - conjunctive_index && - temp_facet['bits_data_temp'][filter_key][filter_val] - ) { - conjunctive_index = - temp_facet['bits_data_temp'][filter_key][filter_val].new_intersection( - conjunctive_index - ); - } else if ( - conjunctive_index && - !temp_facet['bits_data_temp'][filter_key][filter_val] - ) { - conjunctive_index = new FastBitSet([]); - } else { - conjunctive_index = - temp_facet['bits_data_temp'][filter_key][filter_val]; - } - }); + const facetData = temp_facet.bits_data_temp[filter_key][filter_val]; - union = (union || new FastBitSet([])).new_union( - conjunctive_index || new FastBitSet([]) - ); - }); + if (facetData) { + if (conjunctive_index) { + conjunctive_index = conjunctive_index.new_intersection(facetData); + } else { + conjunctive_index = facetData; + } + } else { + conjunctive_index = new FastBitSet(); + } + }); - if (union !== null) { - mapValues(temp_facet['bits_data_temp'], function (values, key) { - mapValues( - temp_facet['bits_data_temp'][key], - function (facet_indexes, key2) { - temp_facet['bits_data_temp'][key][key2] = - temp_facet['bits_data_temp'][key][key2].new_intersection(union); + if (conjunctive_index) { + if (union) { + union = union.new_union(conjunctive_index); + } else { + union = conjunctive_index; } - ); + } }); } + if (union !== null) { + for (const key in temp_facet.bits_data_temp) { + for (const key2 in temp_facet.bits_data_temp[key]) { + temp_facet.bits_data_temp[key][key2] = + temp_facet.bits_data_temp[key][key2].new_intersection(union); + } + } + } + return temp_facet; }; -/* - * returns facets and ids - */ -export const matrix = function (facets, filters) { +export const matrix = function(facets, filters = []) { const temp_facet = _clone(facets); - filters = filters || []; - - mapValues(temp_facet['bits_data'], function (values, key) { - mapValues(temp_facet['bits_data'][key], function (facet_indexes, key2) { - temp_facet['bits_data_temp'][key][key2] = - temp_facet['bits_data'][key][key2]; - }); - }); + // Kopiujemy bits_data do bits_data_temp + temp_facet.bits_data_temp = {}; + for (const key in temp_facet.bits_data) { + temp_facet.bits_data_temp[key] = {}; + for (const key2 in temp_facet.bits_data[key]) { + temp_facet.bits_data_temp[key][key2] = temp_facet.bits_data[key][key2]; + } + } - temp_facet['is_temp_copied'] = true; + temp_facet.is_temp_copied = true; let conjunctive_index; const disjunctive_indexes = combination_indexes(facets, filters); - /** - * process only conjunctive filters - */ - mapValues(filters, function (filter) { + // Processing conjunctive filters + filters.forEach(filter => { if (!Array.isArray(filter[0])) { - const filter_key = filter[0]; - const filter_val = filter[1]; - - if ( - conjunctive_index && - temp_facet['bits_data_temp'][filter_key][filter_val] - ) { - conjunctive_index = - temp_facet['bits_data_temp'][filter_key][filter_val].new_intersection( - conjunctive_index - ); - } else if ( - conjunctive_index && - !temp_facet['bits_data_temp'][filter_key][filter_val] - ) { + const [filter_key, filter_val] = filter; + const facetData = temp_facet.bits_data_temp[filter_key]?.[filter_val]; + + if (conjunctive_index && facetData) { + conjunctive_index = facetData.new_intersection(conjunctive_index); + } else if (conjunctive_index && !facetData) { conjunctive_index = new FastBitSet([]); } else { - conjunctive_index = - temp_facet['bits_data_temp'][filter_key][filter_val]; + conjunctive_index = facetData; } } }); - // cross all facets with conjunctive index + // Intersect all facets with conjunctive indexes if (conjunctive_index) { - mapValues(temp_facet['bits_data_temp'], function (values, key) { - mapValues( - temp_facet['bits_data_temp'][key], - function (facet_indexes, key2) { - temp_facet['bits_data_temp'][key][key2] = - temp_facet['bits_data_temp'][key][key2].new_intersection( - conjunctive_index - ); - } - ); - }); + for (const key in temp_facet.bits_data_temp) { + for (const key2 in temp_facet.bits_data_temp[key]) { + temp_facet.bits_data_temp[key][key2] = temp_facet.bits_data_temp[key][key2].new_intersection(conjunctive_index); + } + } } - /** - * process only negative filters - */ - mapValues(filters, function (filter) { + // Processing negative filters + filters.forEach(filter => { if (filter.length === 3 && filter[1] === '-') { - const filter_key = filter[0]; - const filter_val = filter[2]; - - const negative_bits = - temp_facet['bits_data_temp'][filter_key][filter_val].clone(); - - mapValues(temp_facet['bits_data_temp'], function (values, key) { - mapValues( - temp_facet['bits_data_temp'][key], - function (facet_indexes, key2) { - temp_facet['bits_data_temp'][key][key2] = - temp_facet['bits_data_temp'][key][key2].new_difference( - negative_bits - ); - } - ); - }); + const [filter_key, , filter_val] = filter; + const negative_bits = temp_facet.bits_data_temp[filter_key][filter_val].clone(); + + for (const key in temp_facet.bits_data_temp) { + for (const key2 in temp_facet.bits_data_temp[key]) { + temp_facet.bits_data_temp[key][key2] = temp_facet.bits_data_temp[key][key2].new_difference(negative_bits); + } + } } }); - // cross all facets with disjunctive index - mapValues(temp_facet['bits_data_temp'], function (values, key) { - mapValues( - temp_facet['bits_data_temp'][key], - function (facet_indexes, key2) { - mapValues( - disjunctive_indexes, - function (disjunctive_index, disjunctive_key) { - if (disjunctive_key !== key) { - temp_facet['bits_data_temp'][key][key2] = - temp_facet['bits_data_temp'][key][key2].new_intersection( - disjunctive_index - ); - } - } - ); + // Intersect all facets with disjunctive indexes + for (const key in temp_facet.bits_data_temp) { + for (const key2 in temp_facet.bits_data_temp[key]) { + for (const disjunctive_key in disjunctive_indexes) { + if (disjunctive_key !== key) { + temp_facet.bits_data_temp[key][key2] = temp_facet.bits_data_temp[key][key2].new_intersection(disjunctive_indexes[disjunctive_key]); + } } - ); - }); + } + } return temp_facet; }; -export const index = function (items, fields) { - fields = fields || []; - +export const index = function(items = [], fields = []) { const facets = { data: Object.create(null), bits_data: Object.create(null), bits_data_temp: Object.create(null), }; - let i = 1; + let nextId = 1; - fields.forEach((field) => { - facets['data'][field] = Object.create(null); + // Initialize facets.data for each field + fields.forEach(field => { + facets.data[field] = Object.create(null); }); - items && items.map((item) => { - if (!item['_id']) { - item['_id'] = i; - ++i; + // Assign _id to items without this identifier + items.forEach(item => { + if (!item._id) { + item._id = nextId++; } - - return item; }); - items && items.map((item) => { - fields.forEach((field) => { - if (!item) { - return; - } + // Build facets.data + items.forEach(item => { + fields.forEach(field => { + const fieldValue = item[field]; - if (Array.isArray(item[field])) { - item[field].forEach((v) => { - if (!item[field]) { - return; + if (Array.isArray(fieldValue)) { + fieldValue.forEach(value => { + if (!facets.data[field][value]) { + facets.data[field][value] = []; } - - if (!facets['data'][field][v]) { - facets['data'][field][v] = []; - } - - facets['data'][field][v].push(parseInt(item._id)); + facets.data[field][value].push(item._id); }); - } else if (typeof item[field] !== 'undefined') { - const v = item[field]; - - if (!facets['data'][field][v]) { - facets['data'][field][v] = []; + } else if (typeof fieldValue !== 'undefined') { + const value = fieldValue; + if (!facets.data[field][value]) { + facets.data[field][value] = []; } - - facets['data'][field][v].push(parseInt(item._id)); + facets.data[field][value].push(item._id); } }); - - return item; }); - facets['data'] = mapValues(facets['data'], function (values, field) { - if (!facets['bits_data'][field]) { - facets['bits_data'][field] = Object.create(null); - facets['bits_data_temp'][field] = Object.create(null); - } - - //console.log(values); - return mapValues(values, function (indexes, filter) { - const sorted_indexes = sortBy(indexes); - facets['bits_data'][field][filter] = new FastBitSet(sorted_indexes); - return sorted_indexes; + // Building facets.bits_data and facets.bits_data_temp + Object.keys(facets.data).forEach(field => { + facets.bits_data[field] = Object.create(null); + facets.bits_data_temp[field] = Object.create(null); + + const values = facets.data[field]; + Object.keys(values).forEach(filter => { + const indexes = values[filter]; + const sorted_indexes = indexes.sort((a, b) => a - b); + facets.bits_data[field][filter] = new FastBitSet(sorted_indexes); + // Updating facets.data with sorted indexes + facets.data[field][filter] = sorted_indexes; }); }); @@ -312,44 +260,49 @@ export const index = function (items, fields) { /** * calculates ids for filters */ -export const filters_ids = function (facets_data) { - let output = new FastBitSet([]); - - mapValues(facets_data, function (values, key) { - mapValues(facets_data[key], function (facet_indexes, key2) { - output = output.new_union(facets_data[key][key2]); +export const filters_ids = function(facets_data) { + return Object.values(facets_data).reduce((output, values) => { + Object.values(values).forEach(facet_indexes => { + output = output.new_union(facet_indexes); }); - }); - - return output; + return output; + }, new FastBitSet([])); }; /** - * calculates ids for facets - * if there is no facet input then return null to not save resources for OR calculation - * null means facets haven't matched searched items + * Calculates identifiers for facets + * If there are no filters, returns null to save resources during OR calculations + * null means that the facets did not match any of the searched items */ -export const facets_ids = function (facets_data, filters) { - let output = new FastBitSet([]); - let i = 0; - - mapValues(filters, function (filters, field) { - filters.forEach((filter) => { - ++i; - output = output.new_union( - facets_data[field][filter] || new FastBitSet([]) - ); - }); - }); +export const facets_ids = function(facets_data, filters) { + if (!facets_data || typeof facets_data !== 'object') { + throw new Error('Invalid facets_data provided.'); + } + + if (!filters || typeof filters !== 'object') { + return null; + } + + const allFilters = Object.entries(filters).flatMap( + ([field, filterArray]) => + Array.isArray(filterArray) + ? filterArray.map(filter => ({ field, filter })) + : [] + ); - if (i === 0) { + if (allFilters.length === 0) { return null; } + const output = allFilters.reduce((acc, { field, filter }) => { + const bitset = facets_data[field]?.[filter] || new FastBitSet([]); + return acc.new_union(bitset); + }, new FastBitSet([])); + return output; }; -export const getBuckets = function (data, input, aggregations) { +export const getBuckets = function(data, input, aggregations) { let position = 1; return mapValues(data['bits_data_temp'], (v, k) => { @@ -460,66 +413,57 @@ export const getBuckets = function (data, input, aggregations) { }); }; -export const mergeAggregations = function (aggregations, input) { - return mapValues(clone(aggregations), (val, key) => { - if (!val.field) { - val.field = key; - } +export const mergeAggregations = function(aggregations, input) { + const result = {}; - let filters = []; - if (input.filters && input.filters[key]) { - filters = input.filters[key]; - } + for (const key in aggregations) { + const val = { ...aggregations[key] }; - val.filters = filters; - - let not_filters = []; - if (input.not_filters && input.not_filters[key]) { - not_filters = input.not_filters[key]; - } + val.field = val.field || key; + val.filters = (input.filters && input.filters[key]) || []; + val.not_filters = + (input.exclude_filters && input.exclude_filters[key]) || + (input.not_filters && input.not_filters[key]) || + []; - if (input.exclude_filters && input.exclude_filters[key]) { - not_filters = input.exclude_filters[key]; - } - - val.not_filters = not_filters; + result[key] = val; + } - return val; - }); + return result; }; -export const input_to_facet_filters = function (input, config) { +export const input_to_facet_filters = function(input, config) { const filters = []; - mapValues(input.filters, function (values, key) { + // Processing positive filters + for (const key in input.filters) { + const values = input.filters[key]; if (values && values.length) { - if (config[key].conjunction !== false) { - mapValues(values, function (values2) { - filters.push([key, values2]); + if (config[key]?.conjunction !== false) { + values.forEach(value => { + filters.push([key, value]); }); } else { - const temp = []; - mapValues(values, function (values2) { - temp.push([key, values2]); - }); - + const temp = values.map(value => [key, value]); filters.push(temp); } } - }); + } - mapValues(input.not_filters, function (values, key) { +// Processing negative filters + for (const key in input.not_filters) { + const values = input.not_filters[key]; if (values && values.length) { - mapValues(values, function (values2) { - filters.push([key, '-', values2]); + values.forEach(value => { + filters.push([key, '-', value]); }); } - }); + } return filters; }; -export const parse_boolean_query = function (query) { +export const parse_boolean_query = function(query) { const result = booleanParser.parseBooleanQuery(query); return result.map((v1) => {