feat(medusa): Rollout index engine behind feature flag (#11431)
**What** - Add index engine feature flag - apply it to the `store/products` end point as well as `admin/products` - Query builder various fixes - search capabilities on full data of every entities. The `q` search will be applied to all involved joined table for selection/where clauses Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
3b69f5a105
commit
448dbcb596
@@ -4,6 +4,7 @@ import {
|
||||
isObject,
|
||||
isPresent,
|
||||
isString,
|
||||
unflattenObjectKeys,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { Knex } from "@mikro-orm/knex"
|
||||
import { OrderBy, QueryFormat, QueryOptions, Select } from "@types"
|
||||
@@ -22,6 +23,8 @@ export const OPERATOR_MAP = {
|
||||
}
|
||||
|
||||
export class QueryBuilder {
|
||||
#searchVectorColumnName = "document_tsv"
|
||||
|
||||
private readonly structure: Select
|
||||
private readonly entityMap: Record<string, any>
|
||||
private readonly knex: Knex
|
||||
@@ -82,6 +85,7 @@ export class QueryBuilder {
|
||||
private getGraphQLType(path, field) {
|
||||
const entity = this.getEntity(path)?.ref?.entity!
|
||||
const fieldRef = this.entityMap[entity]._fields[field]
|
||||
|
||||
if (!fieldRef) {
|
||||
throw new Error(`Field ${field} is not indexed.`)
|
||||
}
|
||||
@@ -111,6 +115,7 @@ export class QueryBuilder {
|
||||
Boolean: (val) => Boolean(val),
|
||||
ID: (val) => String(val),
|
||||
Date: (val) => new Date(val).toISOString(),
|
||||
DateTime: (val) => new Date(val).toISOString(),
|
||||
Time: (val) => new Date(`1970-01-01T${val}Z`).toISOString(),
|
||||
}
|
||||
|
||||
@@ -132,6 +137,7 @@ export class QueryBuilder {
|
||||
Float: "::double precision",
|
||||
Boolean: "::boolean",
|
||||
Date: "::timestamp",
|
||||
DateTime: "::timestamp",
|
||||
Time: "::time",
|
||||
"": "",
|
||||
}
|
||||
@@ -141,6 +147,7 @@ export class QueryBuilder {
|
||||
Float: "0",
|
||||
Boolean: "false",
|
||||
Date: "1970-01-01 00:00:00",
|
||||
DateTime: "1970-01-01 00:00:00",
|
||||
Time: "00:00:00",
|
||||
"": "",
|
||||
}
|
||||
@@ -560,9 +567,10 @@ export class QueryBuilder {
|
||||
hasPagination?: boolean
|
||||
hasCount?: boolean
|
||||
returnIdOnly?: boolean
|
||||
}): [string, string | null] {
|
||||
}): string {
|
||||
const queryBuilder = this.knex.queryBuilder()
|
||||
|
||||
const selectOnlyStructure = this.selector.select
|
||||
const structure = this.requestedFields
|
||||
const filter = this.selector.where ?? {}
|
||||
|
||||
@@ -579,6 +587,16 @@ export class QueryBuilder {
|
||||
const rootEntity = entity.toLowerCase()
|
||||
const aliasMapping: { [path: string]: string } = {}
|
||||
|
||||
let hasTextSearch: boolean = false
|
||||
let textSearchQuery: string | null = null
|
||||
const searchQueryFilterProp = `${rootEntity}.q`
|
||||
|
||||
if (filter[searchQueryFilterProp]) {
|
||||
hasTextSearch = true
|
||||
textSearchQuery = filter[searchQueryFilterProp]
|
||||
delete filter[searchQueryFilterProp]
|
||||
}
|
||||
|
||||
const joinParts = this.buildQueryParts(
|
||||
rootStructure,
|
||||
"",
|
||||
@@ -591,7 +609,11 @@ export class QueryBuilder {
|
||||
|
||||
const rootAlias = aliasMapping[rootKey]
|
||||
const selectParts = !returnIdOnly
|
||||
? this.buildSelectParts(rootStructure, rootKey, aliasMapping)
|
||||
? this.buildSelectParts(
|
||||
selectOnlyStructure[rootKey] as Select,
|
||||
rootKey,
|
||||
aliasMapping
|
||||
)
|
||||
: { [rootKey + ".id"]: `${rootAlias}.id` }
|
||||
|
||||
queryBuilder.select(selectParts)
|
||||
@@ -604,6 +626,36 @@ export class QueryBuilder {
|
||||
queryBuilder.joinRaw(joinPart)
|
||||
})
|
||||
|
||||
let searchWhereParts: string[] = []
|
||||
if (hasTextSearch) {
|
||||
/**
|
||||
* Build the search where parts for the query,.
|
||||
* Apply the search query to the search vector column for every joined tabled except
|
||||
* the pivot joined table.
|
||||
*/
|
||||
searchWhereParts = [
|
||||
`${this.getShortAlias(aliasMapping, rootEntity)}.${
|
||||
this.#searchVectorColumnName
|
||||
} @@ plainto_tsquery('simple', '${textSearchQuery}')`,
|
||||
...joinParts.flatMap((part) => {
|
||||
const aliases = part
|
||||
.split(" as ")
|
||||
.flatMap((chunk) => chunk.split(" on "))
|
||||
.filter(
|
||||
(alias) => alias.startsWith('"t_') && !alias.includes("_ref")
|
||||
)
|
||||
return aliases.map(
|
||||
(alias) =>
|
||||
`${alias}.${
|
||||
this.#searchVectorColumnName
|
||||
} @@ plainto_tsquery('simple', '${textSearchQuery}')`
|
||||
)
|
||||
}),
|
||||
]
|
||||
|
||||
queryBuilder.whereRaw(`(${searchWhereParts.join(" OR ")})`)
|
||||
}
|
||||
|
||||
// WHERE clause
|
||||
this.parseWhere(aliasMapping, filter, queryBuilder)
|
||||
|
||||
@@ -618,49 +670,60 @@ export class QueryBuilder {
|
||||
const direction = orderBy[aliasPath]
|
||||
|
||||
queryBuilder.orderByRaw(
|
||||
pgType.coalesce(`${alias}.data->>'${field}'`) + " " + direction
|
||||
`(${alias}.data->>'${field}')${pgType.cast}` + " " + direction
|
||||
)
|
||||
}
|
||||
|
||||
let distinctQueryBuilder = queryBuilder.clone()
|
||||
|
||||
let take_ = !isNaN(+take!) ? +take! : 15
|
||||
let skip_ = !isNaN(+skip!) ? +skip! : 0
|
||||
let sql = ""
|
||||
|
||||
let cte = ""
|
||||
if (hasPagination) {
|
||||
const idColumn = `${this.getShortAlias(aliasMapping, rootEntity)}.id`
|
||||
distinctQueryBuilder.clearSelect()
|
||||
distinctQueryBuilder.select(
|
||||
this.knex.raw(`DISTINCT ON (${idColumn}) ${idColumn} as "id"`)
|
||||
)
|
||||
distinctQueryBuilder.limit(take_)
|
||||
distinctQueryBuilder.offset(skip_)
|
||||
cte = this.buildCTEData({
|
||||
hasCount,
|
||||
searchWhereParts,
|
||||
take: take_,
|
||||
skip: skip_,
|
||||
orderBy,
|
||||
})
|
||||
|
||||
sql += `WITH paginated_data AS (${distinctQueryBuilder.toQuery()}),`
|
||||
if (hasCount) {
|
||||
queryBuilder.select(this.knex.raw("pd.count_total"))
|
||||
}
|
||||
|
||||
queryBuilder.andWhere(
|
||||
this.knex.raw(`${idColumn} IN (SELECT id FROM "paginated_data")`)
|
||||
queryBuilder.joinRaw(
|
||||
`JOIN paginated_data AS pd ON ${rootAlias}.id = pd.id`
|
||||
)
|
||||
}
|
||||
|
||||
sql += `${hasPagination ? " " : "WITH"} data AS (${queryBuilder.toQuery()})
|
||||
SELECT *
|
||||
FROM data`
|
||||
|
||||
let sqlCount = ""
|
||||
if (hasCount) {
|
||||
sqlCount = this.buildQueryCount()
|
||||
}
|
||||
|
||||
return [sql, hasCount ? sqlCount : null]
|
||||
return cte + queryBuilder.toQuery()
|
||||
}
|
||||
|
||||
public buildQueryCount(): string {
|
||||
public buildCTEData({
|
||||
hasCount,
|
||||
searchWhereParts = [],
|
||||
skip,
|
||||
take,
|
||||
orderBy,
|
||||
}: {
|
||||
hasCount: boolean
|
||||
searchWhereParts: string[]
|
||||
skip?: number
|
||||
take: number
|
||||
orderBy: OrderBy
|
||||
}): string {
|
||||
const queryBuilder = this.knex.queryBuilder()
|
||||
|
||||
const hasWhere = isPresent(this.rawConfig?.filters)
|
||||
const structure = hasWhere ? this.rawConfig?.filters! : this.requestedFields
|
||||
const hasWhere = isPresent(this.rawConfig?.filters) || isPresent(orderBy)
|
||||
const structure =
|
||||
hasWhere && !searchWhereParts.length
|
||||
? unflattenObjectKeys({
|
||||
...(this.rawConfig?.filters
|
||||
? unflattenObjectKeys(this.rawConfig?.filters)
|
||||
: {}),
|
||||
...orderBy,
|
||||
})
|
||||
: this.requestedFields
|
||||
|
||||
const rootKey = this.getStructureKeys(structure)[0]
|
||||
|
||||
@@ -682,9 +745,7 @@ export class QueryBuilder {
|
||||
|
||||
const rootAlias = aliasMapping[rootKey]
|
||||
|
||||
queryBuilder.select(
|
||||
this.knex.raw(`COUNT(DISTINCT ${rootAlias}.id) as count`)
|
||||
)
|
||||
queryBuilder.select(this.knex.raw(`${rootAlias}.id as id`))
|
||||
|
||||
queryBuilder.from(
|
||||
`cat_${rootEntity} AS ${this.getShortAlias(aliasMapping, rootEntity)}`
|
||||
@@ -695,10 +756,58 @@ export class QueryBuilder {
|
||||
queryBuilder.joinRaw(joinPart)
|
||||
})
|
||||
|
||||
if (searchWhereParts.length) {
|
||||
queryBuilder.whereRaw(`(${searchWhereParts.join(" OR ")})`)
|
||||
}
|
||||
|
||||
this.parseWhere(aliasMapping, this.selector.where!, queryBuilder)
|
||||
}
|
||||
|
||||
return queryBuilder.toQuery()
|
||||
// ORDER BY clause
|
||||
const orderAliases: string[] = []
|
||||
for (const aliasPath in orderBy) {
|
||||
const path = aliasPath.split(".")
|
||||
const field = path.pop()
|
||||
const attr = path.join(".")
|
||||
|
||||
const pgType = this.getPostgresCastType(attr, [field])
|
||||
|
||||
const alias = aliasMapping[attr]
|
||||
const direction = orderBy[aliasPath]
|
||||
|
||||
const orderAlias = `"${alias}.data->>'${field}'"`
|
||||
orderAliases.push(orderAlias + " " + direction)
|
||||
|
||||
// transform the order by clause to a select MIN/MAX
|
||||
queryBuilder.select(
|
||||
direction === "ASC"
|
||||
? this.knex.raw(
|
||||
`MIN((${alias}.data->>'${field}')${pgType.cast}) as ${orderAlias}`
|
||||
)
|
||||
: this.knex.raw(
|
||||
`MAX((${alias}.data->>'${field}')${pgType.cast}) as ${orderAlias}`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
queryBuilder.groupByRaw(`${rootAlias}.id`)
|
||||
|
||||
const countSubQuery = hasCount
|
||||
? `, (SELECT count(id) FROM data_select) as count_total`
|
||||
: ""
|
||||
|
||||
return `
|
||||
WITH data_select AS (
|
||||
${queryBuilder.toQuery()}
|
||||
),
|
||||
paginated_data AS (
|
||||
SELECT id ${countSubQuery}
|
||||
FROM data_select
|
||||
${orderAliases.length ? "ORDER BY " + orderAliases.join(", ") : ""}
|
||||
LIMIT ${take}
|
||||
${skip ? `OFFSET ${skip}` : ""}
|
||||
)
|
||||
`
|
||||
}
|
||||
|
||||
// NOTE: We are keeping the bellow code for now as reference to alternative implementation for us. DO NOT REMOVE
|
||||
|
||||
Reference in New Issue
Block a user