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

com.couchbase.client.kotlin.search.SearchQuery.kt Maven / Gradle / Ivy

There is a newer version: 1.4.7
Show newest version
/*
 * Copyright 2022 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
 *
 * https://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.search

import com.couchbase.client.core.api.search.CoreSearchQuery
import com.couchbase.client.core.api.search.queries.CoreBooleanFieldQuery
import com.couchbase.client.core.api.search.queries.CoreBooleanQuery
import com.couchbase.client.core.api.search.queries.CoreConjunctionQuery
import com.couchbase.client.core.api.search.queries.CoreCustomQuery
import com.couchbase.client.core.api.search.queries.CoreDateRangeQuery
import com.couchbase.client.core.api.search.queries.CoreDisjunctionQuery
import com.couchbase.client.core.api.search.queries.CoreDocIdQuery
import com.couchbase.client.core.api.search.queries.CoreGeoBoundingBoxQuery
import com.couchbase.client.core.api.search.queries.CoreGeoDistanceQuery
import com.couchbase.client.core.api.search.queries.CoreGeoPolygonQuery
import com.couchbase.client.core.api.search.queries.CoreMatchAllQuery
import com.couchbase.client.core.api.search.queries.CoreMatchNoneQuery
import com.couchbase.client.core.api.search.queries.CoreMatchOperator
import com.couchbase.client.core.api.search.queries.CoreMatchPhraseQuery
import com.couchbase.client.core.api.search.queries.CoreMatchQuery
import com.couchbase.client.core.api.search.queries.CoreNumericRangeQuery
import com.couchbase.client.core.api.search.queries.CorePhraseQuery
import com.couchbase.client.core.api.search.queries.CorePrefixQuery
import com.couchbase.client.core.api.search.queries.CoreQueryStringQuery
import com.couchbase.client.core.api.search.queries.CoreRegexpQuery
import com.couchbase.client.core.api.search.queries.CoreSearchRequest
import com.couchbase.client.core.api.search.queries.CoreTermQuery
import com.couchbase.client.core.api.search.queries.CoreTermRangeQuery
import com.couchbase.client.core.api.search.queries.CoreWildcardQuery
import com.couchbase.client.kotlin.search.GeoShape.Companion.circle
import com.couchbase.client.kotlin.search.SearchQuery.Companion.matchPhrase
import com.couchbase.client.kotlin.search.SearchQuery.Companion.term
import java.time.Instant

/**
 * A non-vector [Full-Text Search query](https://docs.couchbase.com/server/current/fts/fts-supported-queries.html)
 *
 * See also: [SearchSpec].
 */
public sealed class SearchQuery : SearchSpec() {
    internal abstract val core: CoreSearchQuery
    internal abstract fun withBoost(boost: Double?): SearchQuery

    override val coreRequest: CoreSearchRequest
        get() = CoreSearchRequest(this.core, null)

    /**
     * Returns a new query that decorates this one with the given boost multiplier.
     * Has no effect unless this query is used in a disjunction or conjunction.
     */
    public infix fun boost(boost: Number): SearchQuery = this.withBoost(boost.toDouble())

    /**
     * Returns the JSON representation of this query condition.
     */
    public override fun toString(): String = core.export().toString()

    public companion object {
        public enum class MatchOperator(internal val core: CoreMatchOperator) {
            AND(CoreMatchOperator.AND),
            OR(CoreMatchOperator.OR),
            ;
        }

        /**
         * A [Match query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-match.html).
         *
         * Analyzes [match] and uses the result to query the index.
         *
         * Analysis means if the field is indexed with an appropriate language-specific
         * analyzer, then "beauty" will match "beautiful", "swim" will match "swimming", etc.
         *
         * The [match] input may contain multiple terms. By default, at least one of the terms
         * must be present in the document. To require all terms be present, set [operator]
         * to [MatchOperator.AND]. The order and position of the terms do not
         * matter for this query.
         *
         * ## Similar queries
         *
         * If you want exact matches without analysis, use [term].
         *
         * If the order and position of the terms are significant, use [matchPhrase].
         *
         * @param match The input string to analyze and match against.
         *
         * @param analyzer Analyzer to apply to [match]. Defaults to
         * the analyzer set for [field] during index creation.
         *
         * @param field The field to search. Defaults to `_all`, whose content
         * is specified during index creation.
         *
         * @param operator Determines whether a [match] value of "location hostel"
         * means "location AND hostel" or "location OR hostel".
         *
         * @param fuzziness Maximum allowable Levenshtein distance for a match against an analyzed term.
         *
         * @param prefixLength To be considered a match, an input term and the matched text
         * must have a common prefix of this length.
         */
        public fun match(
            match: String,
            field: String = "_all",
            analyzer: String? = null,
            operator: MatchOperator = MatchOperator.OR,
            fuzziness: Int = 0,
            prefixLength: Int = 0,
        ): SearchQuery = MatchQuery(match, field, analyzer, operator, fuzziness, prefixLength)

        internal data class MatchQuery(
            val match: String,
            val field: String,
            val analyzer: String?,
            val operator: MatchOperator,
            val fuzziness: Int,
            val prefixLength: Int,
            val boost: Double? = null,
        ) : SearchQuery() {
            override val core = CoreMatchQuery(match, field, analyzer, prefixLength, fuzziness, operator.core, boost)
            override fun withBoost(boost: Double?) = copy(boost = boost)
        }

        /**
         * A [Match Phrase query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-match-phrase.html)
         *
         * Analyzes [matchPhrase] and uses the result to search for terms in the target
         * that occur in the same order and position.
         *
         * The target field must be indexed with "include term vectors".
         *
         * ## Similar queries
         *
         * If you want exact matches without analysis, use [phrase].
         *
         * If the order and proximity of the terms are not significant, use [match].
         *
         * @param matchPhrase The input string to analyze and match against.
         *
         * @param analyzer Analyzer to apply to [matchPhrase]. Defaults to
         * the analyzer set for [field] during index creation.
         *
         * @param field The field to search. Defaults to `_all`, whose content
         * is specified during index creation.
         */
        public fun matchPhrase(
            matchPhrase: String,
            field: String = "_all",
            analyzer: String? = null,
        ): SearchQuery = MatchPhraseQuery(matchPhrase, field, analyzer)

        internal data class MatchPhraseQuery(
            val matchPhrase: String,
            val field: String = "_all",
            val analyzer: String? = null,
            val boost: Double? = null,
        ) : SearchQuery() {
            override val core = CoreMatchPhraseQuery(matchPhrase, field, analyzer, boost)
            override fun withBoost(boost: Double?) = copy(boost = boost)
        }

        /**
         * A [Term query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-term.html)
         *
         * Searches for a single term, without using an analyzer.
         *
         * NOTE: This is a non-analytic query, meaning it won’t perform any text analysis on the query text.
         *
         * ## Similar queries
         *
         * If you want the terms to be analyzed for linguistic similarity,
         * use [match].
         *
         * To search for multiple terms that must appear in a certain order,
         * use [phrase]
         *
         * @param term The exact term to search for, without using an analyzer.
         *
         * @param field The field to search. Defaults to `_all`, whose content
         * is specified during index creation.
         *
         * @param fuzziness Maximum allowable Levenshtein distance for a match.
         *
         * @param prefixLength To be considered a match, an input term and the matched text
         * must have a common prefix of this length.
         */
        public fun term(
            term: String,
            field: String = "_all",
            fuzziness: Int = 0,
            prefixLength: Int = 0,
        ): SearchQuery = TermQuery(term, field, fuzziness, prefixLength)

        internal data class TermQuery(
            val term: String,
            val field: String,
            val fuzziness: Int,
            val prefixLength: Int,
            val boost: Double? = null,
        ) : SearchQuery() {
            override val core = CoreTermQuery(term, field, fuzziness, prefixLength, boost)
            override fun withBoost(boost: Double?) = copy(boost = boost)
        }

        /**
         * A [Phrase query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-phrase.html)
         *
         * Searches for the [terms] in the same order and position, without analyzing
         * them.
         *
         * The target field must be indexed with "include term vectors".
         *
         * NOTE: This is a non-analytic query, meaning it won’t perform any text analysis on the query text.
         *
         * ## Similar queries
         *
         * If you want to analyze the terms for linguistic similarity, use [matchPhrase].
         *
         * To search for a single term without analysis, use [term]
         *
         * @param terms The input terms to search for in, the same order and position.
         *
         * @param field The field to search. Defaults to `_all`, whose content
         * is specified during index creation.
         */
        public fun phrase(
            terms: List,
            field: String = "_all",
        ): SearchQuery = PhraseQuery(terms, field)

        internal data class PhraseQuery(
            val terms: List,
            val field: String,
            val boost: Double? = null,
        ) : SearchQuery() {
            override val core = CorePhraseQuery(terms, field, boost)
            override fun withBoost(boost: Double?) = copy(boost = boost)
        }

        /**
         * A [Prefix query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-prefix-query.html)
         *
         * Searches for terms that start with [prefix].
         *
         * NOTE: This is a non-analytic query, meaning it won’t perform any text analysis on the query text.
         *
         * @see regexp
         * @see wildcard
         */
        public fun prefix(
            prefix: String,
            field: String = "_all",
        ): SearchQuery = PrefixQuery(prefix, field)

        internal data class PrefixQuery(
            val prefix: String,
            val field: String,
            val boost: Double? = null,
        ) : SearchQuery() {
            override val core = CorePrefixQuery(prefix, field, boost)
            override fun withBoost(boost: Double?) = copy(boost = boost)
        }

        /**
         * A [Regexp query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-regexp.html)
         *
         * Searches for terms that match [regexp].
         *
         * NOTE: This is a non-analytic query, meaning it won’t perform any text analysis on the query text.
         *
         * @param regexp A Go regular expression
         *
         * @see prefix
         * @see wildcard
         */
        public fun regexp(
            regexp: String,
            field: String = "_all",
        ): SearchQuery = RegexpQuery(regexp, field)

        internal data class RegexpQuery(
            val regexp: String,
            val field: String,
            val boost: Double? = null,
        ) : SearchQuery() {
            override val core = CoreRegexpQuery(regexp, field, boost)
            override fun withBoost(boost: Double?) = copy(boost = boost)
        }

        /**
         * A [Wildcard query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-wildcard.html)
         *
         * A wildcard query uses a wildcard expression to search within individual terms for matches.
         * Wildcard expressions can be any single character (?) or zero to many characters (*).
         * Wildcard expressions can appear in the middle or end of a term, but not at the beginning.
         *
         * NOTE: This is a non-analytic query, meaning it won’t perform any text analysis on the query text.
         *
         * @see regexp
         */
        public fun wildcard(
            term: String,
            field: String = "_all",
        ): SearchQuery = WildcardQuery(term, field)

        internal data class WildcardQuery(
            val term: String,
            val field: String,
            val boost: Double? = null,
        ) : SearchQuery() {
            override val core = CoreWildcardQuery(term, field, boost)
            override fun withBoost(boost: Double?) = copy(boost = boost)
        }

        /**
         * A [Query String query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-query-string-query.html).
         *
         * Search using the [FTS query string syntax](https://docs.couchbase.com/server/current/fts/fts-query-string-syntax.html)
         * supported by the Couchbase admin console UI.
         *
         * @param queryString Example: `"+name:info* +country:france"`
         */
        public fun queryString(
            queryString: String,
        ): SearchQuery = QueryStringQuery(queryString)

        internal data class QueryStringQuery(
            val queryString: String,
            val boost: Double? = null,
        ) : SearchQuery() {
            override val core = CoreQueryStringQuery(queryString, boost)
            override fun withBoost(boost: Double?) = copy(boost = boost)
        }

        /**
         * A [Boolean Field query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-boolean-field-query.html).
         * Searches for documents where [field] has the value [bool].
         */
        public fun booleanField(
            bool: Boolean,
            field: String,
        ): SearchQuery = BooleanFieldQuery(bool, field)

        internal data class BooleanFieldQuery(
            val bool: Boolean,
            val field: String,
            val boost: Double? = null,
        ) : SearchQuery() {
            override val core = CoreBooleanFieldQuery(bool, field, boost)
            override fun withBoost(boost: Double?) = copy(boost = boost)
        }

        /**
         * A [DocId query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-DocID-query.html).
         * Searches for documents whose ID is one of [ids].
         */
        public fun documentId(
            ids: List,
        ): SearchQuery = DocumentIdQuery(ids)

        internal data class DocumentIdQuery(
            val ids: List,
            val boost: Double? = null,
        ) : SearchQuery() {
            override val core = CoreDocIdQuery(boost, ids)
            override fun withBoost(boost: Double?) = copy(boost = boost)
        }

        /**
         * A [Term Range query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-term-range.html)
         */
        public fun termRange(
            field: String = "_all",
            min: String? = null,
            inclusiveMin: Boolean = true,
            max: String? = null,
            inclusiveMax: Boolean = false,
        ): SearchQuery = TermRangeQuery(field, min, inclusiveMin, max, inclusiveMax)

        internal data class TermRangeQuery(
            val field: String,
            val min: String?,
            val inclusiveMin: Boolean,
            val max: String?,
            val inclusiveMax: Boolean,
            val boost: Double? = null,
        ) : SearchQuery() {
            override val core = CoreTermRangeQuery(min, max, inclusiveMin, inclusiveMax, field, boost)
            override fun withBoost(boost: Double?) = copy(boost = boost)
        }

        /**
         * A [Numeric Range query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-numeric-range.html)
         */
        public fun numericRange(
            field: String = "_all",
            min: Number? = null,
            inclusiveMin: Boolean = true,
            max: Number? = null,
            inclusiveMax: Boolean = false,
        ): SearchQuery = NumericRangeQuery(field, min, inclusiveMin, max, inclusiveMax)

        internal data class NumericRangeQuery(
            val field: String,
            val min: Number?,
            val inclusiveMin: Boolean,
            val max: Number?,
            val inclusiveMax: Boolean,
            val boost: Double? = null,
        ) : SearchQuery() {
            override val core = CoreNumericRangeQuery(min?.toDouble(), max?.toDouble(), inclusiveMin, inclusiveMax, field, boost)
            override fun withBoost(boost: Double?) = copy(boost = boost)
        }

        /**
         * A [Date Range query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-date-range.html)
         */
        public fun dateRange(
            field: String = "_all",
            start: Instant? = null,
            inclusiveStart: Boolean = true,
            end: Instant? = null,
            inclusiveEnd: Boolean = false,
        ): SearchQuery = DateRangeQuery(field, start, inclusiveStart, end, inclusiveEnd)

        internal data class DateRangeQuery(
            val field: String,
            val start: Instant?,
            val inclusiveStart: Boolean,
            val end: Instant?,
            val inclusiveEnd: Boolean,
            val boost: Double? = null,
        ) : SearchQuery() {
            override val core = CoreDateRangeQuery(start?.toString(), end?.toString(), inclusiveStart, inclusiveEnd, null, field, boost)
            override fun withBoost(boost: Double?) = copy(boost = boost)
        }

        /**
         * A [Geo Point Distance query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-geo-point-distance.html)
         *
         * Alias for [geoShape] using a circle centered at [location] with a radius of [distance].
         */
        public fun geoDistance(
            location: GeoPoint,
            distance: GeoDistance,
            field: String = "_all",
        ): SearchQuery = geoShape(circle(location, distance), field)

        /**
         * A [Geo Bounded Rectangle query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-geo-bounded-rectangle.html)
         * or [Geo Bounded Polygon query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-geo-bounded-polygon.html).
         * May also be used with [GeoCircle].
         *
         * Searches for documents where [field] is bounded by [shape].
         */
        public fun geoShape(
            shape: GeoShape,
            field: String = "_all",
        ): SearchQuery = GeoShapeQuery(shape, field)

        internal data class GeoShapeQuery(
            val shape: GeoShape,
            val field: String,
            val boost: Double? = null,
        ) : SearchQuery() {
            override val core = when (shape) {
                is GeoCircle -> CoreGeoDistanceQuery(shape.center.core, shape.radius.serialize(), field, boost)
                is GeoPolygon -> CoreGeoPolygonQuery(shape.vertices.map { it.core }, field, boost)
                is GeoRectangle -> CoreGeoBoundingBoxQuery(shape.topLeft.core, shape.bottomRight.core, field, boost)
            }

            override fun withBoost(boost: Double?) = copy(boost = boost)
        }

        /**
         * Searches for documents that don't match [query].
         */
        public fun negation(query: SearchQuery): SearchQuery = boolean(mustNot = disjunction(query))

        /**
         * A [Conjunction query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-conjuncts-disjuncts.html)
         *
         * Searches for documents that match all the conjuncts (child queries joined by AND).
         */
        public fun conjunction(
            firstConjunct: SearchQuery,
            vararg remainingConjuncts: SearchQuery,
        ): ConjunctionQuery = conjunction(listOf(firstConjunct, *remainingConjuncts))

        /**
         * A [Conjunction query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-conjuncts-disjuncts.html)
         *
         * Searches for documents that match all the [conjuncts] (child queries joined by AND).
         */
        public fun conjunction(conjuncts: List): ConjunctionQuery = ConjunctionQuery(conjuncts)

        /**
         * A [Disjunction query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-conjuncts-disjuncts.html)
         *
         * Searches for documents that match at least [min] of the disjuncts
         * (child queries joined by OR).
         */
        public fun disjunction(
            firstDisjunct: SearchQuery,
            vararg remainingDisjuncts: SearchQuery,
            min: Int = 1,
        ): DisjunctionQuery = disjunction(listOf(firstDisjunct, *remainingDisjuncts), min)

        /**
         * A [Disjunction query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-conjuncts-disjuncts.html)
         *
         * Searches for documents that match at least [min] of the [disjuncts]
         * (child queries joined by OR).
         */
        public fun disjunction(
            disjuncts: List,
            min: Int = 1,
        ): DisjunctionQuery = DisjunctionQuery(disjuncts, min)

        /**
         * A [Boolean query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-boolean-query.html)
         *
         * Searches for documents that match all of the [must] conditions and
         * none of the [mustNot] conditions, giving precedence to documents
         * matching the [should] conditions.
         */
        public fun boolean(
            must: ConjunctionQuery? = null,
            should: DisjunctionQuery? = null,
            mustNot: DisjunctionQuery? = null,
        ): SearchQuery = BooleanQuery(must, should, mustNot)

        internal data class BooleanQuery(
            val must: ConjunctionQuery?,
            val should: DisjunctionQuery?,
            val mustNot: DisjunctionQuery?,
            val boost: Double? = null,
        ) : SearchQuery() {
            override val core = CoreBooleanQuery(must?.core, mustNot?.core, should?.core, boost)
            override fun withBoost(boost: Double?) = copy(boost = boost)
        }

        /**
         * [A Match All query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-match-all.html)
         *
         * A query that matches all indexed documents.
         */
        public fun matchAll(): SearchQuery = MatchAllQuery()

        internal data class MatchAllQuery(
            val boost: Double? = null,
        ) : SearchQuery() {
            override val core = CoreMatchAllQuery(boost)
            override fun withBoost(boost: Double?) = copy(boost = boost)
        }

        /**
         * [A Match None query](https://docs.couchbase.com/server/current/fts/fts-supported-queries-match-none.html)
         *
         * A query that matches nothing.
         */
        public fun matchNone(): SearchQuery = MatchNoneQuery()

        internal data class MatchNoneQuery(
            val boost: Double? = null,
        ) : SearchQuery() {
            override val core = CoreMatchNoneQuery(boost)
            override fun withBoost(boost: Double?) = copy(boost = boost)
        }

        /**
         * Escape hatch for specifying a custom query condition supported
         * by Couchbase Server but not by this version of the SDK.
         *
         * The [customizer] lambda populates a map that gets converted to JSON.
         *
         * Example:
         * ```
         * val query = custom {
         *     put("wildcard", "foo?ball)
         *     put("field", "sport")
         * }
         * ```
         * yields the query JSON:
         * ```
         * {
         *     "wildcard": "foo?ball",
         *     "field": "sport"
         * }
         * ```
         */
        public fun custom(customizer: MutableMap.() -> Unit): SearchQuery {
            val params: MutableMap = mutableMapOf()
            params.customizer()
            return CustomQuery(params)
        }

        internal data class CustomQuery(
            val params: Map,
            val boost: Double? = null,
        ) : SearchQuery() {
            override val core = CoreCustomQuery(params, boost)
            override fun withBoost(boost: Double?) = copy(boost = boost)
        }
    }
}

/**
 * Create an instance using [SearchQuery.conjunction].
 */
public class ConjunctionQuery internal constructor(
    private val conjuncts: List,
    boost: Double? = null,
) : SearchQuery() {
    override val core = CoreConjunctionQuery(conjuncts.map { it.core }, boost)
    override fun withBoost(boost: Double?) = ConjunctionQuery(conjuncts, boost)
}

/**
 * Create an instance using [SearchQuery.disjunction].
 */
public class DisjunctionQuery internal constructor(
    private val disjuncts: List,
    private val min: Int = 1,
    boost: Double? = null,
) : SearchQuery() {
    override val core = CoreDisjunctionQuery(disjuncts.map { it.core }, min, boost)
    override fun withBoost(boost: Double?) = DisjunctionQuery(disjuncts, min, boost)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy