diff --git a/packages/orama/src/methods/search-fulltext.ts b/packages/orama/src/methods/search-fulltext.ts index 5ab06fe27..a32bc32a6 100644 --- a/packages/orama/src/methods/search-fulltext.ts +++ b/packages/orama/src/methods/search-fulltext.ts @@ -15,7 +15,7 @@ import type { TokenScore, TypedDocument } from '../types.js' -import { getNanosecondsTime, removeVectorsFromHits, safeArrayPush, sortTokenScorePredicate } from '../utils.js' +import { filterAndReduceDocuments, getNanosecondsTime, removeVectorsFromHits, safeArrayPush, sortTokenScorePredicate } from '../utils.js' import { createSearchContext, defaultBM25Params, fetchDocuments, fetchDocumentsWithDistinct } from './search.js' export async function fullTextSearch>( @@ -34,7 +34,7 @@ export async function fullTextSearch 0 - const { limit = 10, offset = 0, term, properties, threshold = 1, distinctOn, includeVectors = false } = params + const { limit = 10, offset = 0, term, properties, returning, threshold = 1, distinctOn, includeVectors = false } = params const isPreflight = params.preflight === true const { index, docs } = orama.data @@ -182,10 +182,10 @@ export async function fullTextSearch 0 const [fullTextIDs, vectorIDs] = await Promise.all([ @@ -99,7 +99,8 @@ export async function hybridSearch(orama, uniqueTokenScores, params.groupBy) } - const results = (await fetchDocuments(orama, uniqueTokenScores, offset, limit)).filter(Boolean) + const documents = await fetchDocuments(orama, uniqueTokenScores, offset, limit) + const results = filterAndReduceDocuments(documents, returning) if (orama.afterSearch) { await runAfterSearch(orama.afterSearch, orama, params, language, results as any) @@ -118,7 +119,7 @@ export async function hybridSearch = Partial = Array | FlattenSchemaProperty> + export type ReduceFunction = (values: ScalarSearchableValue[], acc: T, value: R, index: number) => T export type Reduce = { reducer: ReduceFunction @@ -291,6 +293,23 @@ export interface SearchParamsFullText[] + /** + * The properties of the document to be returned. + * Supports nested objects, allowing root to deepest field extraction while maintaining the original structure. + * If provided, only the fields listed in this array will be included in the result. + * + * NOTE: This functionality is recommended primarily for server-side use. While it reduces the payload of the response + * by including only the specified fields, it can slow down the search. + * + * @example + * const results = await search(db, { + * term: 'Personal Computer', + * returning: ['title', 'meta.rating'], + * }) + * + */ + returning?: ReturningParams + /** * The number of matched documents to return. */ @@ -481,6 +500,8 @@ export interface SearchParamsFullText[] + /** + * The properties of the document to be returned. + * Supports nested objects, allowing root to deepest field extraction while maintaining the original structure. + * If provided, only the fields listed in this array will be included in the result. + * + * NOTE: This functionality is recommended primarily for server-side use. While it reduces the payload of the response + * by including only the specified fields, it can slow down the search. + * + * @example + * const results = await search(db, { + * term: 'Personal Computer', + * returning: ['title', 'meta.rating'], + * }) + * + */ + returning?: ReturningParams + /** * The BM25 parameters to use. * @@ -567,6 +605,8 @@ export interface SearchParamsHybrid + /** * The minimum similarity score between the vector and the document. * By default, Orama will use 0.8. @@ -715,6 +772,8 @@ export interface SearchParamsVector, vector } })) } + +/** + * Selects and returns only the specified fields from a document. + * Supports nested objects, allowing root to deepest field extraction while maintaining the original structure. + * + * @example + * const doc = { + * firstname: 'John', + * lastname: 'Doe', + * age: 30, + * address: { street: 'Main St', city: 'New York' }, + * details: { + * hair: 'Brown', + * sizes: { + * weight: 80, + * height: 180 + * }, + * }, + * }; + * + * const fields = ['firstname', 'address', 'details.sizes.height']; + * + * console.log(pickDocumentProperties(doc, fields)); + * { + * firstname: 'John', + * address: { street: 'Main St', city: 'New York' }, + * details: { sizes: { height: 180 }} + * } + * + * @param doc The document to process. + * @param returning The list of fields to extract, including nested fields (e.g., 'address.street'). + * @returns The document with only the selected fields, preserving the original nested structure. + * If fields are missing in a document, the resulting document will be empty. + */ +export function pickDocumentProperties>( + doc: ResultDocument, + returning: ReturningParams +): ResultDocument { + const result = {} as ResultDocument; + // Iterate over each properties to map the returned item + for (const field of returning) { + // Splits field into its parts (e.g., 'address.street' -> ['address', 'street']) + const parts = (field as string).split('.'); + let source = doc; + let target = result; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (source && source[part]) { + if (i === parts.length - 1) { + // Set the value at the deepest level + target[part] = source[part]; + } else { + // Ensure the target object has the correct structure + if (!target[part]) target[part] = {}; + // Move deeper into the object structure + source = source[part]; + target = target[part]; + } + } else { + // If any part of the path is undefined, break out of the loop + break; + } + } + } + return result +} + +/** + * Cleans an array of documents by removing falsy items. + * An optional array of fields can be provided in order to selects and returns only the + * specified fields from each document, supporting nested objects and maintaining the original structure. + * + * @param results The results of a fetch documents to process. + * @param returning An optional list of fields to extract, including nested fields. + * @returns Cleaned array of documents + */ +export function filterAndReduceDocuments>( + results: Result[], + returning?: ReturningParams +): Result[] { + if (returning?.length) { + return results.reduce(( + acc: Result[], + item: Result + ) => { + // Removes falsy documents + if (item) { + const result = pickDocumentProperties(item.document, returning); + // Remove empty object + if (Object.keys(result as any).length) { + item.document = result + acc.push(item); + } + } + return acc; + }, []); + } + // Removes falsy documents + return results.filter(Boolean) +} \ No newline at end of file