All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.algolia.search.endpoint.internal.EndpointSearch.kt Maven / Gradle / Ivy

@file:Suppress("FunctionName")

package com.algolia.search.endpoint.internal

import com.algolia.search.configuration.CallType
import com.algolia.search.dsl.filters
import com.algolia.search.endpoint.EndpointSearch
import com.algolia.search.model.Attribute
import com.algolia.search.model.IndexName
import com.algolia.search.model.filter.Filter
import com.algolia.search.model.filter.FilterGroup
import com.algolia.search.model.filter.FilterGroupsConverter
import com.algolia.search.model.internal.request.RequestCursor
import com.algolia.search.model.internal.request.RequestParams
import com.algolia.search.model.multipleindex.IndexQuery
import com.algolia.search.model.response.ResponseHitWithPosition
import com.algolia.search.model.response.ResponseSearch
import com.algolia.search.model.response.ResponseSearchForFacets
import com.algolia.search.model.response.ResponseSearches
import com.algolia.search.model.search.Cursor
import com.algolia.search.model.search.Facet
import com.algolia.search.model.search.FacetStats
import com.algolia.search.model.search.Query
import com.algolia.search.serialize.internal.JsonNoDefaults
import com.algolia.search.serialize.internal.Key
import com.algolia.search.serialize.internal.merge
import com.algolia.search.serialize.internal.toBody
import com.algolia.search.serialize.internal.toJsonNoDefaults
import com.algolia.search.serialize.internal.urlEncode
import com.algolia.search.transport.RequestOptions
import com.algolia.search.transport.internal.Transport
import io.ktor.http.HttpMethod
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put

internal class EndpointSearchImpl(
    private val transport: Transport,
    override val indexName: IndexName,
) : EndpointSearch {

    override suspend fun search(query: Query, requestOptions: RequestOptions?): ResponseSearch {
        val body = query.toBody()

        return transport.request(HttpMethod.Post, CallType.Read, indexName.toPath("/query"), requestOptions, body)
    }

    override tailrec suspend fun findObject(
        match: (ResponseSearch.Hit) -> Boolean,
        query: Query,
        paginate: Boolean,
        requestOptions: RequestOptions?,
    ): ResponseHitWithPosition? {
        val response = search(query, requestOptions)
        val hit = response.hits.find(match)
        val hasNextPage = response.page + 1 < response.nbPages

        return if (hit != null) {
            ResponseHitWithPosition(hit, response.hits.indexOf(hit), response.page)
        } else if (paginate && hasNextPage) {
            findObject(match, query.copy(page = (query.page ?: 0) + 1), paginate, requestOptions)
        } else null
    }

    override suspend fun browse(query: Query, requestOptions: RequestOptions?): ResponseSearch {
        val params = RequestParams(query.toJsonNoDefaults().urlEncode())
        val body = JsonNoDefaults.encodeToString(RequestParams.serializer(), params)

        return transport.request(HttpMethod.Post, CallType.Read, indexName.toPath("/browse"), requestOptions, body)
    }

    override suspend fun browse(cursor: Cursor, requestOptions: RequestOptions?): ResponseSearch {
        val params = RequestCursor(cursor.toString())
        val body = JsonNoDefaults.encodeToString(RequestCursor.serializer(), params)

        return transport.request(HttpMethod.Post, CallType.Read, indexName.toPath("/browse"), requestOptions, body)
    }

    override suspend fun searchForFacets(
        attribute: Attribute,
        facetQuery: String?,
        query: Query,
        requestOptions: RequestOptions?,
    ): ResponseSearchForFacets {
        val path = indexName.toPath("/facets/$attribute/query")
        val extraParams = buildJsonObject {
            facetQuery?.let { put(Key.FacetQuery, it) }
        }
        val body = query.toJsonNoDefaults().merge(extraParams).toString()

        return transport.request(HttpMethod.Post, CallType.Read, path, requestOptions, body)
    }

    override suspend fun advancedSearch(
        query: Query,
        filterGroups: Set>,
        requestOptions: RequestOptions?,
    ): ResponseSearch {
        val filtersAnd = filterGroups.filterIsInstance>().flatten()
        val filtersOr = filterGroups.filterIsInstance>().flatten()
        val disjunctiveFacets = filtersOr.map { it.attribute }.toSet()
        val filtersOrFacet = filtersOr.filterIsInstance()
        val filtersOrTag = filtersOr.filterIsInstance()
        val filtersOrNumeric = filtersOr.filterIsInstance()
        val queryForResults = query.toIndexQuery().setFilters(filterGroups)
        val queriesForDisjunctiveFacets = disjunctiveFacets.map { attribute ->
            val groups = filterGroups.map { group ->
                if (group is FilterGroup.Or.Facet) {
                    FilterGroup.Or.Facet(group.filter { filter -> filter.attribute != attribute }.toSet())
                } else group
            }

            query
                .toIndexQuery()
                .setFacets(attribute)
                .optimize()
                .setFilters(groups.toSet())
        }
        val queriesForHierarchicalFacets = filterGroups.filterIsInstance().flatMap {
            it.attributes
                .take(it.path.size + 1)
                .mapIndexed { index, attribute ->
                    query
                        .toIndexQuery()
                        .filters(
                            filtersAnd.combine(it.path.getOrNull(index - 1)).minus(it.path.last()),
                            filtersOrFacet,
                            filtersOrTag,
                            filtersOrNumeric
                        )
                        .setFacets(attribute)
                        .optimize()
                }
        }
        val queries = listOf(queryForResults) + queriesForDisjunctiveFacets + queriesForHierarchicalFacets
        val response = EndpointMultipleIndex(transport).multipleQueries(queries, requestOptions = requestOptions)

        return response.aggregateResult(disjunctiveFacets.size)
    }

    private fun IndexQuery.setFilters(groups: Set>): IndexQuery {
        query.filters = FilterGroupsConverter.SQL(groups)
        return this
    }

    private fun List.aggregateFacets(): Map> {
        return fold(mapOf()) { acc, result ->
            result.facetsOrNull?.let { acc + it } ?: acc
        }
    }

    private fun List.aggregateFacetStats(): Map {
        return fold(mapOf()) { acc, result ->
            result.facetStatsOrNull?.let { acc + it } ?: acc
        }
    }

    private fun List.combine(hierarchicalFilter: Filter.Facet?): List {
        return hierarchicalFilter?.let { this + it } ?: this
    }

    private fun ResponseSearches.aggregateResult(disjunctiveFacetCount: Int): ResponseSearch {
        val resultsDisjunctiveFacets = results.subList(1, 1 + disjunctiveFacetCount)
        val resultHierarchicalFacets = results.subList(1 + disjunctiveFacetCount, results.size)
        val facets = resultsDisjunctiveFacets.aggregateFacets()
        val facetStats = results.aggregateFacetStats()
        val hierarchicalFacets = resultHierarchicalFacets.aggregateFacets()

        return results.first().copy(
            facetStatsOrNull = if (facetStats.isEmpty()) null else facetStats,
            disjunctiveFacetsOrNull = facets,
            hierarchicalFacetsOrNull = if (hierarchicalFacets.isEmpty()) null else hierarchicalFacets,
            exhaustiveFacetsCountOrNull = resultsDisjunctiveFacets.all { it.exhaustiveFacetsCountOrNull == true }
        )
    }

    private fun IndexQuery.optimize(): IndexQuery {
        query.apply {
            attributesToRetrieve = listOf()
            attributesToHighlight = listOf()
            hitsPerPage = 0
            analytics = false
        }
        return this
    }

    private fun Query.toIndexQuery(): IndexQuery {
        return IndexQuery(indexName, copy())
    }

    private fun IndexQuery.filters(
        filtersAnd: List,
        filtersOrFacet: List,
        filtersOrTag: List,
        filtersOrNumeric: List,
    ): IndexQuery {
        query.apply {
            filters {
                and { +filtersAnd }
                orFacet { +filtersOrFacet }
                orTag { +filtersOrTag }
                orNumeric { +filtersOrNumeric }
            }
        }
        return this
    }

    private fun IndexQuery.setFacets(facet: Attribute?): IndexQuery {
        if (facet != null) query.facets = setOf(facet)
        return this
    }
}

/**
 * Create an [EndpointSearch] instance.
 */
internal fun EndpointSearch(
    transport: Transport,
    indexName: IndexName,
): EndpointSearch = EndpointSearchImpl(transport, indexName)




© 2015 - 2025 Weber Informatics LLC | Privacy Policy