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

com.couchbase.client.kotlin.Scope.kt Maven / Gradle / Ivy

There is a newer version: 1.4.7
Show newest version
/*
 * Copyright 2021 Couchbase, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.couchbase.client.kotlin

import com.couchbase.client.core.annotation.SinceCouchbase
import com.couchbase.client.core.api.CoreCouchbaseOps
import com.couchbase.client.core.api.manager.CoreBucketAndScope
import com.couchbase.client.core.api.query.CoreQueryContext
import com.couchbase.client.core.api.search.CoreHighlightStyle
import com.couchbase.client.core.api.search.CoreSearchKeyset
import com.couchbase.client.core.api.search.CoreSearchOps
import com.couchbase.client.core.api.search.CoreSearchOptions
import com.couchbase.client.core.api.search.CoreSearchScanConsistency
import com.couchbase.client.core.api.search.facet.CoreSearchFacet
import com.couchbase.client.core.api.search.sort.CoreSearchSort
import com.couchbase.client.core.api.shared.CoreMutationState
import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.JsonNode
import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.node.ObjectNode
import com.couchbase.client.core.endpoint.http.CoreCommonOptions
import com.couchbase.client.core.error.FeatureNotAvailableException
import com.couchbase.client.core.io.CollectionIdentifier
import com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_COLLECTION
import com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_SCOPE
import com.couchbase.client.core.json.Mapper
import com.couchbase.client.kotlin.analytics.AnalyticsFlowItem
import com.couchbase.client.kotlin.analytics.AnalyticsParameters
import com.couchbase.client.kotlin.analytics.AnalyticsPriority
import com.couchbase.client.kotlin.analytics.AnalyticsScanConsistency
import com.couchbase.client.kotlin.analytics.internal.AnalyticsExecutor
import com.couchbase.client.kotlin.codec.JsonSerializer
import com.couchbase.client.kotlin.internal.requireUnique
import com.couchbase.client.kotlin.internal.toOptional
import com.couchbase.client.kotlin.manager.search.ScopeSearchIndexManager
import com.couchbase.client.kotlin.query.QueryFlowItem
import com.couchbase.client.kotlin.query.QueryMetadata
import com.couchbase.client.kotlin.query.QueryParameters
import com.couchbase.client.kotlin.query.QueryProfile
import com.couchbase.client.kotlin.query.QueryResult
import com.couchbase.client.kotlin.query.QueryRow
import com.couchbase.client.kotlin.query.QueryScanConsistency
import com.couchbase.client.kotlin.query.internal.QueryExecutor
import com.couchbase.client.kotlin.search.Direction
import com.couchbase.client.kotlin.search.Highlight
import com.couchbase.client.kotlin.search.Score
import com.couchbase.client.kotlin.search.SearchFacet
import com.couchbase.client.kotlin.search.SearchFlowItem
import com.couchbase.client.kotlin.search.SearchMetadata
import com.couchbase.client.kotlin.search.SearchPage
import com.couchbase.client.kotlin.search.SearchResult
import com.couchbase.client.kotlin.search.SearchRow
import com.couchbase.client.kotlin.search.SearchScanConsistency
import com.couchbase.client.kotlin.search.SearchSort
import com.couchbase.client.kotlin.search.SearchSpec
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.reactive.asFlow
import kotlinx.coroutines.reactive.awaitSingle
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap

public class Scope(
    public val name: String,
    public val bucket: Bucket,
) {
    internal val queryContext = CoreQueryContext.of(bucket.name, name)
    internal val couchbaseOps: CoreCouchbaseOps = bucket.couchbaseOps
    private val searchOps = couchbaseOps.searchOps(CoreBucketAndScope(bucket.name, name))

    private val collectionCache = ConcurrentHashMap()

    private val queryExecutor = QueryExecutor(
        couchbaseOps.queryOps(),
        queryContext,
        bucket.env.jsonSerializer,
    )

    private val analyticsExecutor
        get() = AnalyticsExecutor(couchbaseOps.asCore(), this)

    /**
     * A manager for administering scope-level Full-Text Search indexes.
     */
    @SinceCouchbase("7.6")
    public val searchIndexes: ScopeSearchIndexManager = ScopeSearchIndexManager(
        couchbaseOps.scopeSearchIndexManager(CoreBucketAndScope(bucket.name, name))
    )

    /**
     * Opens a collection for this scope.
     *
     * @param name the collection name.
     */
    public fun collection(name: String): Collection {
        val collectionName = name
        val scopeName = this.name
        return collectionCache.computeIfAbsent(name) {
            val defaultScopeAndCollection = collectionName == DEFAULT_COLLECTION && scopeName == DEFAULT_SCOPE
            if (!defaultScopeAndCollection) {
                couchbaseOps.ifCore {
                    configurationProvider().refreshCollectionId(
                        CollectionIdentifier(bucket.name, scopeName.toOptional(), collectionName.toOptional())
                    )
                }
            }
            Collection(collectionName, this)
        }
    }

    /**
     * Returns a Flow which may be collected to execute a scope-level
     * SQL++ query and process the results.
     *
     * The returned Flow is cold, meaning the query is not executed unless
     * the Flow is collected. If you collect the flow multiple times,
     * the query is executed each time.
     *
     * The extension function `Flow.execute()` may be used when
     * when the results are known to fit in memory. It simply collects the flow
     * into a [QueryResult].
     *
     * For larger query results, prefer the streaming version which takes a
     * lambda to invoke when each row is received from the server:
     * `Flow.execute { row -> ... }`.
     *
     * @param statement the SQL++ statement to execute.
     *
     * @param common options common to all requests.
     *
     * @param parameters parameters to the SQL++ statement.
     *
     * @param preserveExpiry pass true if you want the query engine to preserve
     * existing expiration times for any documents modified by this query.
     * *Requires Couchbase Server 7.1 or later.*
     *
     * @param serializer the serializer to use for converting parameters to JSON,
     * and the default serializer for parsing [QueryRow] content.
     * Defaults to the serializer configured on the cluster environment.
     *
     * @param consistency required if you want to read your own writes.
     * Values other than [QueryScanConsistency.notBounded]
     * tell the server to wait for the indexer to catch up with a certain
     * state of the K/V service before executing the query.
     *
     * @param readonly pass true if the SQL++ statement does not modify documents.
     * This allows the client to retry the query if necessary.
     *
     * @param adhoc pass false if this is a commonly used query that should be
     * turned into a prepared statement for faster execution.
     *
     * @param flexIndex pass true to use a full-text index instead of a query index.
     *
     * @param metrics pass true to include metrics in the response (access via
     * [QueryMetadata.metrics]). Relatively inexpensive, and may be enabled
     * in production with minimal impact.
     *
     * @param profile specifies how much profiling information to include in
     * the response (access via [QueryMetadata.profile]). Profiling is
     * relatively expensive, and can impact the performance of the server
     * query engine. Not recommended for use in production, unless you're
     * diagnosing a specific issue.  Note this is an Enterprise Edition feature.
     * On Community Edition the parameter will be accepted, but no profiling
     * information returned.
     *
     * @param maxParallelism Specifies the maximum parallelism for the query.
     *
     * @param scanCap Maximum buffered channel size between the indexer client
     * and the query service for index scans. This parameter controls when to use
     * scan backfill. Use 0 or a negative number to disable. Smaller values
     * reduce GC, while larger values reduce indexer backfill.
     *
     * @param pipelineBatch Controls the number of items execution operators
     * can batch for Fetch from the KV.
     *
     * @param pipelineCap Maximum number of items each execution operator
     * can buffer between various operators.
     *
     * @param clientContextId an arbitrary string that identifies this query
     * for diagnostic purposes.
     *
     * @param raw an "escape hatch" for passing arbitrary query options that
     * aren't otherwise exposed by this method.
     */
    public fun query(
        statement: String,
        common: CommonOptions = CommonOptions.Default,
        parameters: QueryParameters = QueryParameters.None,
        @SinceCouchbase("7.1") preserveExpiry: Boolean = false,

        serializer: JsonSerializer? = null,

        consistency: QueryScanConsistency = QueryScanConsistency.notBounded(),
        readonly: Boolean = false,
        adhoc: Boolean = true,
        flexIndex: Boolean = false,

        metrics: Boolean = false,
        profile: QueryProfile = QueryProfile.OFF,

        maxParallelism: Int? = null,
        scanCap: Int? = null,
        pipelineBatch: Int? = null,
        pipelineCap: Int? = null,

        clientContextId: String? = UUID.randomUUID().toString(),
        raw: Map = emptyMap(),
        @SinceCouchbase("7.6") useReplica: Boolean? = null,
    ): Flow {

        return queryExecutor.query(
            statement,
            common,
            parameters,
            preserveExpiry,
            serializer,
            consistency,
            readonly,
            adhoc,
            flexIndex,
            metrics,
            profile,
            maxParallelism,
            scanCap,
            pipelineBatch,
            pipelineCap,
            clientContextId,
            raw,
            useReplica
        )
    }

    @Deprecated(level = DeprecationLevel.HIDDEN, message = "Use similar method with additional useReplica parameter.")
    public fun query(
        statement: String,
        common: CommonOptions = CommonOptions.Default,
        parameters: QueryParameters = QueryParameters.None,
        @SinceCouchbase("7.1") preserveExpiry: Boolean = false,

        serializer: JsonSerializer? = null,

        consistency: QueryScanConsistency = QueryScanConsistency.notBounded(),
        readonly: Boolean = false,
        adhoc: Boolean = true,
        flexIndex: Boolean = false,

        metrics: Boolean = false,
        profile: QueryProfile = QueryProfile.OFF,

        maxParallelism: Int? = null,
        scanCap: Int? = null,
        pipelineBatch: Int? = null,
        pipelineCap: Int? = null,

        clientContextId: String? = UUID.randomUUID().toString(),
        raw: Map = emptyMap()

    ): Flow {

        return queryExecutor.query(
            statement,
            common,
            parameters,
            preserveExpiry,
            serializer,
            consistency,
            readonly,
            adhoc,
            flexIndex,
            metrics,
            profile,
            maxParallelism,
            scanCap,
            pipelineBatch,
            pipelineCap,
            clientContextId,
            raw,
            null
        )
    }

    public fun analyticsQuery(
        statement: String,
        common: CommonOptions = CommonOptions.Default,
        parameters: AnalyticsParameters = AnalyticsParameters.None,

        serializer: JsonSerializer? = null,

        consistency: AnalyticsScanConsistency = AnalyticsScanConsistency.notBounded(),
        @SinceCouchbase("6.5") readonly: Boolean = false,
        priority: AnalyticsPriority = AnalyticsPriority.normal(),

        clientContextId: String? = UUID.randomUUID().toString(),
        raw: Map = emptyMap(),

        ): Flow {

        return analyticsExecutor.query(
            statement,
            common,
            parameters,
            serializer,
            consistency,
            readonly,
            priority,
            clientContextId,
            raw
        )
    }

    /**
     * Returns a Flow which can be collected to execute a Full-Text Search
     * (vector, non-vector, or mixed-mode) against a scope-level index.
     *
     * The returned Flow is cold, meaning the query is not executed unless
     * the Flow is collected. If you collect the flow multiple times,
     * the query is executed each time.
     *
     * The extension function `Flow.execute()` may be used
     * when the results are known to fit in memory. It simply collects the flow
     * into a [SearchResult].
     *
     * For larger query results, prefer the streaming version which takes a
     * lambda to invoke when each row is received from the server:
     * `Flow.execute { row -> ... }`.
     *
     * @param indexName Index to search.
     *
     * @param spec Condition a document must match in order to be included in the search results.
     *
     * @param page Specifies which rows of the search result to return.
     *
     * @param limit Number of rows to return (page size).
     *
     * @param sort Specifies how the results should be sorted.
     * For tiered sort (sort by X then by Y) see [SearchSort.then].
     *
     * @param fields Stored fields to include in the result rows, or `listOf("*")`
     * to include all stored fields.
     *
     * @param facets Specifies the facets to include in the search results.
     * A facet counts the number of documents in the full, unpaginated search results
     * that meet certain criteria. Facet results may be retrieved from either
     * [SearchResult] or [SearchMetadata], whichever is more convenient.
     *
     * @param highlight Specifies whether to include fragments of text
     * with the matching search term highlighted. If highlighting is requested,
     * the result also includes location information for matched terms.
     *
     * @param includeLocations If true, triggers the inclusion of location information
     * for matched terms. If highlighting is requested, locations are always included
     * and this parameter has no effect.
     *
     * @param score The scoring algorithm to use.
     *
     * @param explain If true, [SearchRow.explanation] holds the bytes of a JSON Object
     * describing how the score was calculated.
     *
     * @param collections If not empty, only search within the named collections.
     * Requires an index defined on a non-default scope containing the collections.
     *
     * @param consistency Specifies whether to wait for the index to catch up with the
     * latest versions of certain documents. The default (unbounded) is fast, but means
     * the results might not reflect the latest document mutations.
     *
     * @param serializer Default serializer to use for [SearchRow.fieldsAs].
     * If not specified, defaults to the cluster environment's default serializer.
     *
     * @param raw The keys and values in this map are added to the query specification JSON.
     * This is an "escape hatch" for sending arguments supported by Couchbase Server, but not
     * by this version of the SDK.
     *
     * @sample com.couchbase.client.kotlin.samples.searchSimple
     * @sample com.couchbase.client.kotlin.samples.checkSearchResultForPartialFailure
     * @sample com.couchbase.client.kotlin.samples.searchQueryWithFacets
     * @sample com.couchbase.client.kotlin.samples.searchSimpleVector
     * @sample com.couchbase.client.kotlin.samples.searchSpecMixedMode
     */
    @SinceCouchbase("7.6")
    public fun search(
        indexName: String,
        spec: SearchSpec,
        common: CommonOptions = CommonOptions.Default,
        page: SearchPage = SearchPage.startAt(offset = 0),
        limit: Int? = null,
        sort: SearchSort = SearchSort.byScore(Direction.DESCENDING),
        fields: List = emptyList(),
        facets: List = emptyList(),
        highlight: Highlight = Highlight.none(),
        includeLocations: Boolean = false,
        score: Score = Score.default(),
        explain: Boolean = false,
        @SinceCouchbase("7.0") collections: List = emptyList(),
        consistency: SearchScanConsistency = SearchScanConsistency.notBounded(),
        serializer: JsonSerializer? = null,
        raw: Map = emptyMap(),
    ): Flow = searchOps.search(
        indexName = indexName,
        spec = spec,
        common = common,
        page = page,
        limit = limit,
        sort = sort,
        fields = fields,
        facets = facets,
        highlight = highlight,
        includeLocations = includeLocations,
        score = score,
        explain = explain,
        collections = collections,
        consistency = consistency,
        serializer = serializer ?: bucket.env.jsonSerializer,
        raw = raw,
    )
}

internal fun CoreSearchOps.search(
    indexName: String,
    spec: SearchSpec,
    common: CommonOptions,
    page: SearchPage,
    limit: Int?,
    sort: SearchSort,
    fields: List,
    facets: List,
    highlight: Highlight,
    includeLocations: Boolean,
    score: Score,
    explain: Boolean,
    @SinceCouchbase("7.0") collections: List,
    consistency: SearchScanConsistency,
    serializer: JsonSerializer,
    raw: Map,
): Flow {

    requireUnique(facets.map { it.name }) { duplicate ->
        "Each facet used in a request must have a unique name, but got multiple facts named '$duplicate'."
    }

    val opts = object : CoreSearchOptions {
        override fun collections() = collections

        override fun consistency(): CoreSearchScanConsistency? =
            when (consistency) {
                is SearchScanConsistency.NotBounded -> CoreSearchScanConsistency.NOT_BOUNDED
                is SearchScanConsistency.ConsistentWith -> null
            }

        override fun consistentWith(): CoreMutationState? =
            when (consistency) {
                is SearchScanConsistency.NotBounded -> null
                is SearchScanConsistency.ConsistentWith -> CoreMutationState(consistency.tokens)
            }

        override fun disableScoring(): Boolean = score is Score.None

        override fun explain(): Boolean? = explain

        override fun facets(): Map {
            if (facets.isEmpty()) return emptyMap()
            val map = mutableMapOf()
            facets.forEach { map[it.name] = it.core }
            return map
        }

        override fun fields(): List = fields

        override fun highlightFields(): List = when (highlight) {
            is Highlight.Companion.None -> emptyList()
            is Highlight.Companion.Specific -> highlight.fields
        }

        override fun highlightStyle(): CoreHighlightStyle? = when (highlight) {
            is Highlight.Companion.None -> null
            is Highlight.Companion.Specific -> {
                when (highlight.style?.name?.uppercase()) {
                    null -> CoreHighlightStyle.SERVER_DEFAULT
                    "HTML" -> CoreHighlightStyle.HTML
                    "ANSI" -> CoreHighlightStyle.ANSI
                    else -> throw FeatureNotAvailableException("Highlight style '${highlight.style}' not supported")
                }
            }
        }

        override fun limit(): Int? = limit

        override fun raw(): JsonNode? {
            return if (raw.isEmpty()) null else Mapper.convertValue(raw, ObjectNode::class.java)
        }

        override fun skip(): Int? = (page as? SearchPage.StartAt)?.offset
        override fun searchBefore(): CoreSearchKeyset? = (page as? SearchPage.SearchBefore)?.keyset?.core
        override fun searchAfter(): CoreSearchKeyset? = (page as? SearchPage.SearchAfter)?.keyset?.core

        override fun sort(): List = sort.core

        override fun includeLocations(): Boolean? = includeLocations

        override fun commonOptions(): CoreCommonOptions = common.toCore()
    }

    return flow {
        val response = searchReactive(
            indexName,
            spec.coreRequest,
            opts,
        ).awaitSingle()

        emitAll(response.rows().asFlow()
            .map { SearchRow.from(it, serializer) })

        val coreMetadata = response.metaData().awaitSingle()
        val coreFacets = response.facets().awaitSingle()

        emit(SearchMetadata(coreMetadata, coreFacets))
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy