typesense-instantsearch-adapter
Version:
Adapter to use InstantSearch UI widgets with Typesense Search
368 lines (354 loc) • 19 kB
JavaScript
"use strict";
import _typeof from "@babel/runtime/helpers/typeof";
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _createClass from "@babel/runtime/helpers/createClass";
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
import { utils } from "./support/utils";
export var SearchResponseAdapter = /*#__PURE__*/function () {
function SearchResponseAdapter(typesenseResponse, instantsearchRequest, configuration) {
var allTypesenseResults = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : [];
var fullTypesenseResponse = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {};
_classCallCheck(this, SearchResponseAdapter);
this.typesenseResponse = typesenseResponse;
this.instantsearchRequest = instantsearchRequest;
this.configuration = configuration;
this.allTypesenseResults = allTypesenseResults;
this.fullTypesenseResponse = fullTypesenseResponse;
}
return _createClass(SearchResponseAdapter, [{
key: "_adaptGroupedHits",
value: function _adaptGroupedHits(typesenseGroupedHits) {
var _this = this;
var adaptedResult = [];
adaptedResult = typesenseGroupedHits.map(function (groupedHit) {
var adaptedHits = _this._adaptHits(groupedHit.hits);
adaptedHits.forEach(function (hit) {
hit["group_key"] = hit["_group_key"] = groupedHit.group_key;
if (groupedHit.found) {
hit["_group_found"] = groupedHit.found;
}
});
return adaptedHits;
});
// adaptedResult is now in the form of [[{}, {}], [{}, {}], ...]
// where each element in the outermost array corresponds to a group.
if (this.configuration.flattenGroupedHits) {
// We flatten it to [{}, {}, {}]
adaptedResult = adaptedResult.flat();
} else {
// We flatten it to [{ ..., grouped_hits: [{}, {}] }, {}, {}]
// We set the first element in the group as the hit, and then add a new key called grouped_hits with the other hits
adaptedResult = adaptedResult.map(function (adaptedGroupedHit) {
return _objectSpread(_objectSpread({}, adaptedGroupedHit[0]), {}, {
_grouped_hits: adaptedGroupedHit
});
});
}
return adaptedResult;
}
}, {
key: "_adaptHits",
value: function _adaptHits(typesenseHits) {
var _this2 = this;
var adaptedResult = [];
adaptedResult = typesenseHits.map(function (typesenseHit) {
var adaptedHit = _objectSpread({}, typesenseHit.document);
adaptedHit.objectID = typesenseHit.document.id;
adaptedHit._snippetResult = _this2._adaptHighlightResult(typesenseHit, "snippet");
adaptedHit._highlightResult = _this2._adaptHighlightResult(typesenseHit, "value");
adaptedHit._rawTypesenseHit = typesenseHit;
// We're adding `conversation` into each hit, since there doesn't seem to be any other way to pass this up to Instantsearch outside of hits
if (_this2.fullTypesenseResponse.conversation) {
adaptedHit._rawTypesenseConversation = _this2.fullTypesenseResponse.conversation;
}
// Add metadata fields to result, if a field with that name doesn't already exist
["text_match", "geo_distance_meters", "curated", "text_match_info", "hybrid_search_info", "vector_distance", "collection",
// Union search specific
"search_index" // Union search specific
].forEach(function (metadataField) {
if (Object.keys(typesenseHit).includes(metadataField) && !Object.keys(adaptedHit).includes(metadataField)) {
adaptedHit[metadataField] = typesenseHit[metadataField];
}
});
var geoLocationValue = adaptedHit[_this2.configuration.geoLocationField];
if (geoLocationValue) {
adaptedHit._geoloc = {
lat: geoLocationValue[0],
lng: geoLocationValue[1]
};
}
return adaptedHit;
});
return adaptedResult;
}
}, {
key: "_adaptHighlightResult",
value: function _adaptHighlightResult(typesenseHit, snippetOrValue) {
var result = {};
// If there is a highlight object available (as of v0.24.0),
// And the highlight object uses the highlight format available in v0.24.0.rcn32 and above
// use that instead of the highlights array, since it supports highlights of nested fields
if (typesenseHit.highlight != null && this.isHighlightPost0240RCN32Format(typesenseHit.highlight)) {
this.adaptHighlightObject(typesenseHit, result, snippetOrValue);
} else {
this.adaptHighlightsArray(typesenseHit, result, snippetOrValue);
}
return result;
}
}, {
key: "isHighlightPost0240RCN32Format",
value: function isHighlightPost0240RCN32Format(highlight) {
return highlight.full == null && highlight.snippet == null;
}
}, {
key: "adaptHighlightsArray",
value: function adaptHighlightsArray(typesenseHit, result, snippetOrValue) {
var _this3 = this;
// Algolia lists all searchable attributes in this key, even if there are no matches
// So do the same and then override highlights
Object.assign.apply(Object, [result].concat(_toConsumableArray(Object.entries(typesenseHit.document).map(function (_ref) {
var _ref2 = _slicedToArray(_ref, 2),
attribute = _ref2[0],
value = _ref2[1];
return _defineProperty({}, attribute, {
value: value,
matchLevel: "none",
matchedWords: []
});
}))));
typesenseHit.highlights.forEach(function (highlight) {
result[highlight.field] = {
value: highlight[snippetOrValue] || highlight["".concat(snippetOrValue, "s")],
matchLevel: "full",
matchedWords: highlight.matched_tokens
};
if (highlight.indices) {
result[highlight.field]["matchedIndices"] = highlight.indices;
}
});
// Now convert any values that have an array value
// Also, replace highlight tag
Object.entries(result).forEach(function (_ref4) {
var _ref5 = _slicedToArray(_ref4, 2),
k = _ref5[0],
v = _ref5[1];
var attribute = k;
var value = v.value,
matchLevel = v.matchLevel,
matchedWords = v.matchedWords,
matchedIndices = v.matchedIndices;
if (value == null) {
result[attribute] = _this3._adaptHighlightNullValue();
} else if (Array.isArray(value)) {
// Algolia lists all values of the key in highlights, even those that don't have any highlights
// So add all values of the array field, including highlights
result[attribute] = [];
typesenseHit.document[attribute].forEach(function (unhighlightedValueFromArray, index) {
if (matchedIndices && matchedIndices.includes(index)) {
result[attribute].push({
value: _this3._adaptHighlightTag("".concat(value[matchedIndices.indexOf(index)]), _this3.instantsearchRequest.params.highlightPreTag, _this3.instantsearchRequest.params.highlightPostTag),
matchLevel: matchLevel,
matchedWords: matchedWords[index]
});
} else if (_typeof(unhighlightedValueFromArray) === "object") {
// Handle arrays of objects
// Side note: Typesense does not support highlights for nested objects in this `highlights` array,
// so we pass in an empty object below
result[attribute].push(_this3._adaptHighlightInObjectValue(unhighlightedValueFromArray, {}, snippetOrValue));
} else {
result[attribute].push({
value: "".concat(unhighlightedValueFromArray),
matchLevel: "none",
matchedWords: []
});
}
});
} else if (_typeof(value) === "object") {
// Handle nested objects
// Side note: Typesense does not support highlights for nested objects in this `highlights` array,
// so we pass in an empty object below
result[attribute] = _this3._adaptHighlightInObjectValue(value, {}, snippetOrValue);
} else {
// Convert all values to strings
result[attribute].value = _this3._adaptHighlightTag("".concat(value), _this3.instantsearchRequest.params.highlightPreTag, _this3.instantsearchRequest.params.highlightPostTag);
}
});
}
}, {
key: "adaptHighlightObject",
value: function adaptHighlightObject(typesenseHit, result, snippetOrValue) {
Object.assign(result, this._adaptHighlightInObjectValue(typesenseHit.document, typesenseHit.highlight, snippetOrValue));
}
}, {
key: "_adaptHighlightInObjectValue",
value: function _adaptHighlightInObjectValue(objectValue, highlightObjectValue, snippetOrValue) {
var _this4 = this;
return Object.assign.apply(Object, [{}].concat(_toConsumableArray(Object.entries(objectValue).map(function (_ref6) {
var _ref7 = _slicedToArray(_ref6, 2),
attribute = _ref7[0],
value = _ref7[1];
var adaptedValue;
if (value == null) {
adaptedValue = _this4._adaptHighlightNullValue();
} else if (Array.isArray(value)) {
var _highlightObjectValue;
adaptedValue = _this4._adaptHighlightInArrayValue(value, (_highlightObjectValue = highlightObjectValue === null || highlightObjectValue === void 0 ? void 0 : highlightObjectValue[attribute]) !== null && _highlightObjectValue !== void 0 ? _highlightObjectValue : [], snippetOrValue);
} else if (_typeof(value) === "object") {
var _highlightObjectValue2;
adaptedValue = _this4._adaptHighlightInObjectValue(value, (_highlightObjectValue2 = highlightObjectValue === null || highlightObjectValue === void 0 ? void 0 : highlightObjectValue[attribute]) !== null && _highlightObjectValue2 !== void 0 ? _highlightObjectValue2 : {}, snippetOrValue);
} else {
adaptedValue = _this4._adaptHighlightInPrimitiveValue(value, highlightObjectValue === null || highlightObjectValue === void 0 ? void 0 : highlightObjectValue[attribute], snippetOrValue);
}
return _defineProperty({}, attribute, adaptedValue);
}))));
}
}, {
key: "_adaptHighlightInArrayValue",
value: function _adaptHighlightInArrayValue(arrayValue, highlightArrayValue, snippetOrValue) {
var _this5 = this;
return arrayValue.map(function (value, index) {
var adaptedValue;
if (value == null) {
adaptedValue = _this5._adaptHighlightNullValue();
} else if (Array.isArray(value)) {
var _highlightArrayValue$;
adaptedValue = _this5._adaptHighlightInArrayValue(value, (_highlightArrayValue$ = highlightArrayValue === null || highlightArrayValue === void 0 ? void 0 : highlightArrayValue[index]) !== null && _highlightArrayValue$ !== void 0 ? _highlightArrayValue$ : [], snippetOrValue);
} else if (_typeof(value) === "object") {
var _highlightArrayValue$2;
adaptedValue = _this5._adaptHighlightInObjectValue(value, (_highlightArrayValue$2 = highlightArrayValue === null || highlightArrayValue === void 0 ? void 0 : highlightArrayValue[index]) !== null && _highlightArrayValue$2 !== void 0 ? _highlightArrayValue$2 : {}, snippetOrValue);
} else {
adaptedValue = _this5._adaptHighlightInPrimitiveValue(value, highlightArrayValue === null || highlightArrayValue === void 0 ? void 0 : highlightArrayValue[index], snippetOrValue);
}
return adaptedValue;
});
}
}, {
key: "_adaptHighlightInPrimitiveValue",
value: function _adaptHighlightInPrimitiveValue(primitiveValue, highlightPrimitiveValue, snippetOrValue) {
if (highlightPrimitiveValue != null) {
var _ref9, _ref10, _highlightPrimitiveVa;
var highlightedValue = (_ref9 = (_ref10 = (_highlightPrimitiveVa = highlightPrimitiveValue[snippetOrValue]) !== null && _highlightPrimitiveVa !== void 0 ? _highlightPrimitiveVa : highlightPrimitiveValue["highlight"]) !== null && _ref10 !== void 0 ? _ref10 : highlightPrimitiveValue["snippet"]) !== null && _ref9 !== void 0 ? _ref9 : primitiveValue;
return {
value: this._adaptHighlightTag("".concat(highlightedValue), this.instantsearchRequest.params.highlightPreTag, this.instantsearchRequest.params.highlightPostTag),
matchLevel: (highlightPrimitiveValue.matched_tokens || []).length > 0 ? "full" : "none",
matchedWords: highlightPrimitiveValue.matched_tokens || []
};
} else {
return {
// Convert all values to strings
value: this._adaptHighlightTag("".concat(primitiveValue), this.instantsearchRequest.params.highlightPreTag, this.instantsearchRequest.params.highlightPostTag),
matchLevel: "none",
matchedWords: []
};
}
}
}, {
key: "_adaptHighlightNullValue",
value: function _adaptHighlightNullValue() {
return {
value: "",
matchLevel: "none",
matchedWords: []
};
}
}, {
key: "_adaptFacets",
value: function _adaptFacets(typesenseFacetCounts) {
var adaptedResult = {};
if (Array.isArray(typesenseFacetCounts)) {
typesenseFacetCounts.forEach(function (facet) {
Object.assign(adaptedResult, _defineProperty({}, facet.field_name, Object.assign.apply(Object, [{}].concat(_toConsumableArray(facet.counts.map(function (count) {
return _defineProperty({}, count.value, count.count);
}))))));
});
}
return adaptedResult;
}
}, {
key: "_adaptFacetStats",
value: function _adaptFacetStats(typesenseFacetCounts) {
var adaptedResult = {};
if (Array.isArray(typesenseFacetCounts)) {
typesenseFacetCounts.forEach(function (facet) {
if (facet.stats && Object.keys(facet.stats).length > 0) {
Object.assign(adaptedResult, _defineProperty({}, facet.field_name, facet.stats));
}
});
}
return adaptedResult;
}
}, {
key: "_adaptRenderingContent",
value: function _adaptRenderingContent(typesenseFacetCounts) {
var _adaptedResult$facetO;
var adaptedResult = Object.assign({}, this.configuration.renderingContent);
// Only set facet ordering if the user has not set one
if (((_adaptedResult$facetO = adaptedResult.facetOrdering) === null || _adaptedResult$facetO === void 0 || (_adaptedResult$facetO = _adaptedResult$facetO.facets) === null || _adaptedResult$facetO === void 0 ? void 0 : _adaptedResult$facetO.order) == null) {
adaptedResult.facetOrdering = adaptedResult.facetOrdering || {};
adaptedResult.facetOrdering.facets = adaptedResult.facetOrdering.facets || {};
adaptedResult.facetOrdering.facets.order = _toConsumableArray(new Set((Array.isArray(typesenseFacetCounts) ? typesenseFacetCounts : []).map(function (fc) {
return fc["field_name"];
}).concat(this.allTypesenseResults.map(function (r) {
return r.facet_counts || [];
}).flat().map(function (fc) {
return fc["field_name"];
}).filter(function (f) {
return f;
}))));
}
return adaptedResult;
}
}, {
key: "_adaptUserData",
value: function _adaptUserData(metadata) {
if (!metadata) return [];
return Array.isArray(metadata) ? metadata : [metadata];
}
}, {
key: "adapt",
value: function adapt() {
var adaptedRenderingContent = this._adaptRenderingContent(this.typesenseResponse.facet_counts || []);
// For union search, use union_request_params, otherwise use request_params
var requestParams = this.typesenseResponse.union_request_params ? this.typesenseResponse.union_request_params[0] : this.typesenseResponse.request_params;
var adaptedResult = _objectSpread({
hits: this.typesenseResponse.grouped_hits ? this._adaptGroupedHits(this.typesenseResponse.grouped_hits) : this._adaptHits(this.typesenseResponse.hits),
nbHits: this.typesenseResponse.found,
page: this.typesenseResponse.union_request_params ? this.typesenseResponse.page // Union search already uses 0-based page
: this.typesenseResponse.page - 1,
// Regular search uses 1-based page, convert to 0-based
nbPages: this._adaptNumberOfPages(),
hitsPerPage: (requestParams === null || requestParams === void 0 ? void 0 : requestParams.per_page) || 10,
facets: this._adaptFacets(this.typesenseResponse.facet_counts || []),
facets_stats: this._adaptFacetStats(this.typesenseResponse.facet_counts || []),
query: (requestParams === null || requestParams === void 0 ? void 0 : requestParams.q) || "",
processingTimeMS: this.typesenseResponse.search_time_ms
}, Object.keys(adaptedRenderingContent).length > 0 ? {
renderingContent: adaptedRenderingContent
} : null);
// Add appliedRules if metadata is present
if (this.typesenseResponse.metadata) {
adaptedResult.appliedRules = ["typesense-override"];
adaptedResult.userData = this._adaptUserData(this.typesenseResponse.metadata);
}
// Add parsed_nl_query if natural language search was used
if (this.typesenseResponse.parsed_nl_query) {
adaptedResult.parsed_nl_query = this.typesenseResponse.parsed_nl_query;
}
// If no results were found for the search, but there is still a conversation response,
// still send that as a hit so the conversation is accessible via Instantsearch
if (this.fullTypesenseResponse.conversation && adaptedResult.hits.length === 0) {
adaptedResult.hits = [{
_rawTypesenseConversation: this.fullTypesenseResponse.conversation
}];
}
// console.log(adaptedResult);
return adaptedResult;
}
}]);
}();
Object.assign(SearchResponseAdapter.prototype, utils);
//# sourceMappingURL=SearchResponseAdapter.js.map