diff --git a/elasticmagic-query-filters/api/elasticmagic-query-filters.api b/elasticmagic-query-filters/api/elasticmagic-query-filters.api index 797da1911c..65caacbf2c 100644 --- a/elasticmagic-query-filters/api/elasticmagic-query-filters.api +++ b/elasticmagic-query-filters/api/elasticmagic-query-filters.api @@ -295,6 +295,38 @@ public final class dev/evo/elasticmagic/qf/ExpressionValue { public final fun getName ()Ljava/lang/String; } +public final class dev/evo/elasticmagic/qf/FacetExpressionFilterResult : dev/evo/elasticmagic/qf/FilterResult { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ldev/evo/elasticmagic/qf/FilterMode;)V + public final fun getMode ()Ldev/evo/elasticmagic/qf/FilterMode; + public fun getName ()Ljava/lang/String; + public fun getParamName ()Ljava/lang/String; + public final fun getResults ()Ljava/util/List; +} + +public final class dev/evo/elasticmagic/qf/FacetExpressionValue { + public fun (Ljava/lang/String;ZJ)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Z + public final fun component3 ()J + public final fun copy (Ljava/lang/String;ZJ)Ldev/evo/elasticmagic/qf/FacetExpressionValue; + public static synthetic fun copy$default (Ldev/evo/elasticmagic/qf/FacetExpressionValue;Ljava/lang/String;ZJILjava/lang/Object;)Ldev/evo/elasticmagic/qf/FacetExpressionValue; + public fun equals (Ljava/lang/Object;)Z + public final fun getDocCount ()J + public final fun getName ()Ljava/lang/String; + public final fun getSelected ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public class dev/evo/elasticmagic/qf/FacetExpressionsFilter : dev/evo/elasticmagic/qf/Filter { + public fun ()V + public fun (Ljava/lang/String;Ljava/util/List;Ldev/evo/elasticmagic/qf/FilterMode;)V + public synthetic fun (Ljava/lang/String;Ljava/util/List;Ldev/evo/elasticmagic/qf/FilterMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getMode ()Ldev/evo/elasticmagic/qf/FilterMode; + public fun prepare (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Ldev/evo/elasticmagic/qf/PreparedFacetExpressionFilter; + public synthetic fun prepare (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Ldev/evo/elasticmagic/qf/PreparedFilter; +} + public final class dev/evo/elasticmagic/qf/FacetFilter : dev/evo/elasticmagic/qf/Filter { public static final field Companion Ldev/evo/elasticmagic/qf/FacetFilter$Companion; public fun (Ldev/evo/elasticmagic/query/FieldOperations;Ljava/lang/String;Ldev/evo/elasticmagic/qf/FilterMode;Ldev/evo/elasticmagic/aggs/TermsAgg;)V @@ -400,6 +432,10 @@ public abstract interface class dev/evo/elasticmagic/qf/FilterResult { public abstract fun getParamName ()Ljava/lang/String; } +public final class dev/evo/elasticmagic/qf/HelperKt { + public static final fun getFacetFilterExpr (Ljava/util/List;Ldev/evo/elasticmagic/qf/FilterMode;)Ldev/evo/elasticmagic/query/QueryExpression; +} + public abstract class dev/evo/elasticmagic/qf/MatchKey { public abstract fun match (Ljava/lang/String;)Z } @@ -528,11 +564,20 @@ public final class dev/evo/elasticmagic/qf/PreparedAttrRangeFacetFilter : dev/ev public final class dev/evo/elasticmagic/qf/PreparedAttrRangeFacetFilter$Companion { } +public final class dev/evo/elasticmagic/qf/PreparedFacetExpressionFilter : dev/evo/elasticmagic/qf/PreparedFilter { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ldev/evo/elasticmagic/qf/FilterMode;)V + public fun apply (Ldev/evo/elasticmagic/SearchQuery;Ljava/util/List;)V + public final fun getMode ()Ldev/evo/elasticmagic/qf/FilterMode; + public fun processResult (Ldev/evo/elasticmagic/SearchQueryResult;)Ldev/evo/elasticmagic/qf/FacetExpressionFilterResult; + public synthetic fun processResult (Ldev/evo/elasticmagic/SearchQueryResult;)Ldev/evo/elasticmagic/qf/FilterResult; +} + public final class dev/evo/elasticmagic/qf/PreparedFacetFilter : dev/evo/elasticmagic/qf/PreparedFilter { public fun (Ldev/evo/elasticmagic/qf/FacetFilter;Ljava/lang/String;Ljava/lang/String;Ldev/evo/elasticmagic/query/QueryExpression;Ljava/util/List;)V public fun apply (Ldev/evo/elasticmagic/SearchQuery;Ljava/util/List;)V public final fun getFilter ()Ldev/evo/elasticmagic/qf/FacetFilter; public final fun getSelectedValues ()Ljava/util/List; + public final fun getTermsAggResult (Ldev/evo/elasticmagic/SearchQueryResult;)Ldev/evo/elasticmagic/aggs/TermsAggResult; public fun processResult (Ldev/evo/elasticmagic/SearchQueryResult;)Ldev/evo/elasticmagic/qf/FacetFilterResult; public synthetic fun processResult (Ldev/evo/elasticmagic/SearchQueryResult;)Ldev/evo/elasticmagic/qf/FilterResult; } diff --git a/elasticmagic-query-filters/src/commonMain/kotlin/dev/evo/elasticmagic/qf/EspressionsFilter.kt b/elasticmagic-query-filters/src/commonMain/kotlin/dev/evo/elasticmagic/qf/EspressionsFilter.kt index 63568992dc..2a7eca2492 100644 --- a/elasticmagic-query-filters/src/commonMain/kotlin/dev/evo/elasticmagic/qf/EspressionsFilter.kt +++ b/elasticmagic-query-filters/src/commonMain/kotlin/dev/evo/elasticmagic/qf/EspressionsFilter.kt @@ -1,6 +1,5 @@ package dev.evo.elasticmagic.qf -import dev.evo.elasticmagic.query.Bool import dev.evo.elasticmagic.query.QueryExpression class ExpressionValue(val name: String, val expr: QueryExpression) @@ -18,24 +17,12 @@ open class SimpleExpressionsFilter( null ) - val expression = mutableListOf() - - filterValues.map { value -> - values.firstOrNull { it.name == value }?.let { - expression.add(it.expr) - } - - } - val filterExpr = when (expression.size) { - 0 -> null - else -> { - when (mode) { - FilterMode.UNION -> maybeWrapBool(Bool::should, expression) - FilterMode.INTERSECT -> maybeWrapBool(Bool::must, expression) - } - } + val expression = filterValues.mapNotNull { value -> + values.find { it.name == value }?.expr } + val filterExpr = getFacetFilterExpr(expression, mode) + return PreparedSimpleFilter( name, paramName, diff --git a/elasticmagic-query-filters/src/commonMain/kotlin/dev/evo/elasticmagic/qf/FacetExpressionsFilter.kt b/elasticmagic-query-filters/src/commonMain/kotlin/dev/evo/elasticmagic/qf/FacetExpressionsFilter.kt new file mode 100644 index 0000000000..b504e42dba --- /dev/null +++ b/elasticmagic-query-filters/src/commonMain/kotlin/dev/evo/elasticmagic/qf/FacetExpressionsFilter.kt @@ -0,0 +1,111 @@ +package dev.evo.elasticmagic.qf + +import dev.evo.elasticmagic.SearchQuery +import dev.evo.elasticmagic.SearchQueryResult +import dev.evo.elasticmagic.aggs.FilterAgg +import dev.evo.elasticmagic.aggs.LongValueAggResult +import dev.evo.elasticmagic.aggs.SingleBucketAggResult +import dev.evo.elasticmagic.query.Bool +import dev.evo.elasticmagic.query.QueryExpression + +data class FacetExpressionValue(val name: String, val selected: Boolean, val docCount: Long) + +class FacetExpressionFilterResult( + override val name: String, + override val paramName: String, + val results: List, + val mode: FilterMode +) : FilterResult + +class PreparedFacetExpressionFilter( + name: String, + paramName: String, + private val selectedFilterExprs: List, + private val facetFilterExprs: List, + val mode: FilterMode, +) : PreparedFilter( + name, + paramName, + getFacetFilterExpr(selectedFilterExprs.map { it.expr }, mode) +) { + private val filterAggName = "qf.$name.filter" + + private fun makeAggName(value: String) = "qf.$name:$value" + + override fun apply( + searchQuery: SearchQuery<*>, + otherFacetFilterExpressions: List + ) { + + val filterAggs = facetFilterExprs.associate { makeAggName(it.name) to FilterAgg(it.expr) } + val filters = + if (mode == FilterMode.INTERSECT) (otherFacetFilterExpressions + facetFilterExpr).mapNotNull { it } + else otherFacetFilterExpressions + + val aggs = if (filters.isNotEmpty()) { + val filtered = filters.filter { f -> facetFilterExprs.all { it.name != f.name } } + mapOf( + filterAggName to FilterAgg( + maybeWrapBool(Bool::must, filtered), + aggs = filterAggs + ) + ) + } else filterAggs + + searchQuery.aggs(aggs) + if (facetFilterExpr != null) { + searchQuery.postFilter(facetFilterExpr) + } + } + + override fun processResult( + searchQueryResult: SearchQueryResult<*>, + ): FacetExpressionFilterResult { + val selectedNames = selectedFilterExprs.map { it.name } + val agg = searchQueryResult.aggs[filterAggName] + + val parsedAggs = if (agg != null) + (agg as SingleBucketAggResult).aggs + else + searchQueryResult.aggs + + val results = facetFilterExprs.map { facetFilterExpr -> + val filterAgg = parsedAggs[makeAggName(facetFilterExpr.name)] + val docCount = if (filterAgg is SingleBucketAggResult) { + filterAgg.docCount + } else { + (filterAgg as LongValueAggResult).value + } + FacetExpressionValue( + facetFilterExpr.name, + selectedNames.contains(facetFilterExpr.name), + docCount + ) + } + + return FacetExpressionFilterResult(name, paramName, results, mode) + } +} + +open class FacetExpressionsFilter( + name: String? = null, + private val allValues: List = emptyList(), + val mode: FilterMode = FilterMode.UNION, +) : Filter(name) { + + override fun prepare(name: String, paramName: String, params: QueryFilterParams): PreparedFacetExpressionFilter { + val filterValues = params.getOrElse(listOf(paramName), ::emptyList) + + val selectedValues = filterValues.mapNotNull { value -> + allValues.find { it.name == value } + } + + return PreparedFacetExpressionFilter( + name, + paramName, + selectedValues, + allValues, + mode, + ) + } +} diff --git a/elasticmagic-query-filters/src/commonMain/kotlin/dev/evo/elasticmagic/qf/FacetFilter.kt b/elasticmagic-query-filters/src/commonMain/kotlin/dev/evo/elasticmagic/qf/FacetFilter.kt index 35cd43676c..02a9c7f7cc 100644 --- a/elasticmagic-query-filters/src/commonMain/kotlin/dev/evo/elasticmagic/qf/FacetFilter.kt +++ b/elasticmagic-query-filters/src/commonMain/kotlin/dev/evo/elasticmagic/qf/FacetFilter.kt @@ -142,7 +142,7 @@ class PreparedFacetFilter( } mapOf( filterAggName to FilterAgg( - if (aggFilters.size == 1) aggFilters[0] else Bool(filter = aggFilters), + maybeWrapBool(Bool::filter, aggFilters), aggs = mapOf(termsAggName to filter.termsAgg) ) ) @@ -185,7 +185,7 @@ class PreparedFacetFilter( return FacetFilterResult(name, paramName, filter.mode, values, isSelected) } - private fun getTermsAggResult( + fun getTermsAggResult( searchQueryResult: SearchQueryResult<*> ): TermsAggResult { var aggResult: AggAwareResult = searchQueryResult diff --git a/elasticmagic-query-filters/src/commonMain/kotlin/dev/evo/elasticmagic/qf/Helper.kt b/elasticmagic-query-filters/src/commonMain/kotlin/dev/evo/elasticmagic/qf/Helper.kt new file mode 100644 index 0000000000..3892aad26c --- /dev/null +++ b/elasticmagic-query-filters/src/commonMain/kotlin/dev/evo/elasticmagic/qf/Helper.kt @@ -0,0 +1,15 @@ +package dev.evo.elasticmagic.qf + +import dev.evo.elasticmagic.query.Bool +import dev.evo.elasticmagic.query.QueryExpression + +fun getFacetFilterExpr(expression: List, mode: FilterMode) = when (expression.size) { + 0 -> null + else -> { + when (mode) { + FilterMode.UNION -> maybeWrapBool(Bool::should, expression) + FilterMode.INTERSECT -> maybeWrapBool(Bool::must, expression) + } + } +} + diff --git a/elasticmagic-query-filters/src/commonTest/kotlin/dev/evo/elasticmagic/qf/AttrSimpleFilterTests.kt b/elasticmagic-query-filters/src/commonTest/kotlin/dev/evo/elasticmagic/qf/AttrSimpleFilterTests.kt index 47938b24be..cf9297e1c0 100644 --- a/elasticmagic-query-filters/src/commonTest/kotlin/dev/evo/elasticmagic/qf/AttrSimpleFilterTests.kt +++ b/elasticmagic-query-filters/src/commonTest/kotlin/dev/evo/elasticmagic/qf/AttrSimpleFilterTests.kt @@ -10,11 +10,11 @@ import io.kotest.matchers.maps.shouldContainExactly import kotlin.test.Test class AttrSimpleFilterTests : BaseCompilerTest(::SearchQueryCompiler) { - object ProductDoc : Document() { + private object ProductDoc : Document() { val attrs by long() } - object AttrsQueryFilters : QueryFilters() { + private object AttrsQueryFilters : QueryFilters() { val attrsBool by AttrSimpleFilter(ProductDoc.attrs, "a") } diff --git a/elasticmagic-query-filters/src/commonTest/kotlin/dev/evo/elasticmagic/qf/FacetQueryFilterTests.kt b/elasticmagic-query-filters/src/commonTest/kotlin/dev/evo/elasticmagic/qf/FacetQueryFilterTests.kt new file mode 100644 index 0000000000..6a9e23b1e2 --- /dev/null +++ b/elasticmagic-query-filters/src/commonTest/kotlin/dev/evo/elasticmagic/qf/FacetQueryFilterTests.kt @@ -0,0 +1,390 @@ +package dev.evo.elasticmagic.qf + +import dev.evo.elasticmagic.SearchQuery +import dev.evo.elasticmagic.aggs.FilterAggResult +import dev.evo.elasticmagic.aggs.ValueCountAggResult +import dev.evo.elasticmagic.compile.BaseCompilerTest +import dev.evo.elasticmagic.compile.SearchQueryCompiler +import dev.evo.elasticmagic.doc.Document +import io.kotest.matchers.maps.shouldContainExactly +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import kotlin.test.Test + +class FacetQueryFilterTests : BaseCompilerTest(::SearchQueryCompiler) { + private object ProductDoc : Document() { + val isAvailable by boolean() + val sellingType by keyword() + } + + private object CarDoc : Document() { + val state by boolean() + val price by int() + val year by int() + } + + private object CarQueryFilter : QueryFilters() { + val isNew by FacetExpressionsFilter( + "new", + listOf(ExpressionValue("true", CarDoc.state.eq(true))) + ) + val price by FacetExpressionsFilter( + "price", + listOf( + ExpressionValue("*-10000", CarDoc.price.lte(10000)), + ExpressionValue("10000-20000", CarDoc.price.range(gt = 10000, lte = 20000)), + ExpressionValue("20000-30000", CarDoc.price.range(gt = 20000, lte = 30000)), + ExpressionValue("30000-*", CarDoc.price.range(30000)) + ), + + ) + } + + private object ItemQueryFilters : QueryFilters() { + val sellingType by FacetExpressionsFilter( + "sellingType", + listOf( + ExpressionValue("wholesale", ProductDoc.sellingType.oneOf(listOf("4", "5", "6"))), + ExpressionValue("retail", ProductDoc.sellingType.oneOf(listOf("1", "2", "3"))), + ), + mode = FilterMode.INTERSECT, + ) + + val available by FacetExpressionsFilter( + "available", + listOf( + ExpressionValue("true", ProductDoc.isAvailable.eq(true)), + ), + ) + } + + @Test + fun simpleTest() = testWithCompiler { + var sq = SearchQuery() + ItemQueryFilters.apply(sq, emptyMap()) + compile(sq).body shouldContainExactly mapOf( + "aggs" to mapOf( + "qf.sellingType:retail" to mapOf( + "filter" to mapOf( + "terms" to mapOf( + "sellingType" to listOf("1", "2", "3") + ) + ) + ), + "qf.sellingType:wholesale" to mapOf( + "filter" to mapOf( + "terms" to mapOf( + "sellingType" to listOf("4", "5", "6") + ) + ) + ), + + "qf.available:true" to mapOf( + "filter" to mapOf( + "term" to mapOf( + "isAvailable" to true + ) + ) + ), + ) + ) + + sq = SearchQuery() + ItemQueryFilters.apply(sq, mapOf(listOf("sellingType") to listOf("retail"))) + compile(sq).body shouldContainExactly mapOf( + "aggs" to mapOf( + "qf.sellingType.filter" to mapOf( + "filter" to mapOf( + "terms" to mapOf( + "sellingType" to listOf("1", "2", "3") + ) + ), + "aggs" to mapOf( + "qf.sellingType:retail" to mapOf( + "filter" to mapOf( + "terms" to mapOf( + "sellingType" to listOf("1", "2", "3") + ) + ) + ), + "qf.sellingType:wholesale" to mapOf( + "filter" to mapOf( + "terms" to mapOf( + "sellingType" to listOf("4", "5", "6") + ) + ) + ) + ) + ), + + "qf.available.filter" to mapOf( + "filter" to mapOf( + "terms" to mapOf( + "sellingType" to listOf("1", "2", "3") + ) + ), + "aggs" to mapOf( + "qf.available:true" to mapOf( + "filter" to mapOf( + "term" to mapOf( + "isAvailable" to true + ) + ) + ), + ) + ) + ), + "post_filter" to mapOf( + "terms" to mapOf( + "sellingType" to listOf("1", "2", "3") + ) + ) + ) + } + + @Test + fun testResult() = testWithCompiler { + var sq = SearchQuery() + + val f = CarQueryFilter.apply(sq, mapOf(listOf("new") to listOf("true"))) + + compile(sq).body shouldContainExactly mapOf( + "aggs" to mapOf( + "qf.isNew:true" to mapOf( + "filter" to mapOf( + "term" to mapOf( + "state" to true + ) + ) + ), + + "qf.price.filter" to mapOf( + "filter" to mapOf( + "term" to mapOf( + "state" to true + ) + ), + "aggs" to mapOf( + "qf.price:*-10000" to mapOf( + "filter" to mapOf( + "range" to mapOf( + "price" to mapOf( + "lte" to 10000 + ) + ) + ) + ), + "qf.price:10000-20000" to mapOf( + "filter" to mapOf( + "range" to mapOf( + "price" to mapOf( + "gt" to 10000, + "lte" to 20000, + ) + ) + ) + ), + "qf.price:20000-30000" to mapOf( + "filter" to mapOf( + "range" to mapOf( + "price" to mapOf( + "gt" to 20000, + "lte" to 30000, + ) + ) + ) + ), + "qf.price:30000-*" to mapOf( + "filter" to mapOf( + "range" to mapOf( + "price" to mapOf( + "gt" to 30000 + ) + ) + ) + ), + ) + ) + ), + "post_filter" to mapOf( + "term" to mapOf( + "state" to true + ) + ) + ) + val result = f.processResult( + searchResultWithAggs( + "qf.isNew:true" to FilterAggResult(82, mapOf()), + "qf.price.filter" to FilterAggResult( + 82, + mapOf( + "qf.price:*-10000" to ValueCountAggResult(11), + "qf.price:10000-20000" to ValueCountAggResult(16), + "qf.price:20000-30000" to ValueCountAggResult(23), + "qf.price:30000-*" to ValueCountAggResult(32), + ) + ) + ) + ) + val isNew = result[CarQueryFilter.isNew] + val price = result[CarQueryFilter.price] + isNew shouldNotBe null + price shouldNotBe null + isNew.name shouldBe "isNew" + isNew.results.size shouldBe 1 + isNew.results[0].name shouldBe "true" + isNew.results[0].selected shouldBe true + isNew.results[0].docCount shouldBe 82 + + price.name shouldBe "price" + price.results.size shouldBe 4 + price.results[0].name shouldBe "*-10000" + price.results[0].selected shouldBe false + price.results[0].docCount shouldBe 11 + price.results[1].name shouldBe "10000-20000" + price.results[1].selected shouldBe false + price.results[1].docCount shouldBe 16 + price.results[2].name shouldBe "20000-30000" + price.results[2].selected shouldBe false + price.results[2].docCount shouldBe 23 + price.results[3].name shouldBe "30000-*" + price.results[3].selected shouldBe false + price.results[3].docCount shouldBe 32 + + + sq = SearchQuery(CarDoc.year.eq(2014)) + val priceFilter = CarQueryFilter.apply(sq, mapOf(listOf("price") to listOf("*-10000", "10000-20000", "null"))) + compile(sq).body shouldContainExactly mapOf( + "query" to mapOf( + "term" to mapOf( + "year" to 2014 + ), + ), + "aggs" to mapOf( + "qf.isNew.filter" to mapOf( + "filter" to mapOf( + "bool" to mapOf( + "should" to listOf( + mapOf( + "range" to mapOf( + "price" to mapOf( + "lte" to 10000 + ) + ) + ), + mapOf( + "range" to mapOf( + "price" to mapOf( + "gt" to 10000, + "lte" to 20000, + ) + ) + ) + ) + ) + ), + "aggs" to mapOf( + "qf.isNew:true" to mapOf( + "filter" to mapOf( + "term" to mapOf( + "state" to true + ) + ) + ), + ) + ), + "qf.price:*-10000" to mapOf( + "filter" to mapOf( + "range" to mapOf( + "price" to mapOf( + "lte" to 10000 + ) + ) + ) + ), + "qf.price:10000-20000" to mapOf( + "filter" to mapOf( + "range" to mapOf( + "price" to mapOf( + "gt" to 10000, + "lte" to 20000, + ) + ) + ) + ), + "qf.price:20000-30000" to mapOf( + "filter" to mapOf( + "range" to mapOf( + "price" to mapOf( + "gt" to 20000, + "lte" to 30000, + ) + ) + ) + ), + "qf.price:30000-*" to mapOf( + "filter" to mapOf( + "range" to mapOf( + "price" to mapOf( + "gt" to 30000 + ) + ) + ) + ), + ), + "post_filter" to mapOf( + "bool" to mapOf( + "should" to listOf( + mapOf( + "range" to mapOf( + "price" to mapOf( + "lte" to 10000 + ) + ) + ), + mapOf( + "range" to mapOf( + "price" to mapOf( + "gt" to 10000, + "lte" to 20000, + ) + ) + ) + ) + ) + ) + ) + + val result1 = priceFilter.processResult( + searchResultWithAggs( + "qf.isNew:true" to FilterAggResult(32, mapOf()), + "qf.price.filter" to FilterAggResult( + 82, + mapOf( + "qf.price:*-10000" to ValueCountAggResult(7), + "qf.price:10000-20000" to ValueCountAggResult(11), + "qf.price:20000-30000" to ValueCountAggResult(6), + "qf.price:30000-*" to ValueCountAggResult(10), + ) + ) + ) + ) + val priceResult = result1[CarQueryFilter.price] + priceResult shouldNotBe null + priceResult.name shouldBe "price" + priceResult.results.size shouldBe 4 + priceResult.results[0].name shouldBe "*-10000" + priceResult.results[0].selected shouldBe true + priceResult.results[0].docCount shouldBe 7 + priceResult.results[1].name shouldBe "10000-20000" + priceResult.results[1].selected shouldBe true + priceResult.results[1].docCount shouldBe 11 + priceResult.results[2].name shouldBe "20000-30000" + priceResult.results[2].selected shouldBe false + priceResult.results[2].docCount shouldBe 6 + priceResult.results[3].name shouldBe "30000-*" + priceResult.results[3].selected shouldBe false + priceResult.results[3].docCount shouldBe 10 + + } +}