-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Attribute range and bool facet filters
- Loading branch information
1 parent
c064c83
commit 635789d
Showing
25 changed files
with
2,690 additions
and
333 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
338 changes: 279 additions & 59 deletions
338
elasticmagic-query-filters/api/elasticmagic-query-filters.api
Large diffs are not rendered by default.
Oops, something went wrong.
255 changes: 255 additions & 0 deletions
255
...cmagic-query-filters/src/commonMain/kotlin/dev/evo/elasticmagic/qf/AttrBoolFacetFilter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,255 @@ | ||
package dev.evo.elasticmagic.qf | ||
|
||
import dev.evo.elasticmagic.SearchQuery | ||
import dev.evo.elasticmagic.SearchQueryResult | ||
import dev.evo.elasticmagic.aggs.Aggregation | ||
import dev.evo.elasticmagic.aggs.FilterAgg | ||
import dev.evo.elasticmagic.aggs.ScriptedMetricAgg | ||
import dev.evo.elasticmagic.aggs.ScriptedMetricAggResult | ||
import dev.evo.elasticmagic.aggs.TermsAgg | ||
import dev.evo.elasticmagic.aggs.TermsAggResult | ||
import dev.evo.elasticmagic.query.Bool | ||
import dev.evo.elasticmagic.query.FieldOperations | ||
import dev.evo.elasticmagic.query.QueryExpression | ||
import dev.evo.elasticmagic.query.Script | ||
import dev.evo.elasticmagic.types.BooleanType | ||
import dev.evo.elasticmagic.types.IntType | ||
import dev.evo.elasticmagic.types.RequiredListType | ||
|
||
fun encodeBoolAttrWithValue(attrId: Int, value: Boolean): Long { | ||
return (attrId.toLong() shl 1) or (if (value) 1L else 0L) | ||
} | ||
|
||
fun decodeBoolAttrAndValue(attrValue: Long): Pair<Int, Boolean> { | ||
return (attrValue ushr 1).toInt() to (attrValue and 1L == 1L) | ||
} | ||
|
||
/** | ||
* Facet fiter for attribute values. An attribute value is a pair of 2 | ||
* 32-bit values attribute id and value id combined as a single 64-bit field. | ||
*/ | ||
class AttrBoolFacetFilter( | ||
val field: FieldOperations<Long>, | ||
name: String? = null | ||
) : Filter<PreparedAttrBoolFacetFilter, AttrBoolFacetFilterResult>(name) { | ||
|
||
data class SelectedValues(val attrId: Int, val values: List<Boolean>) { | ||
fun filterExpression(field: FieldOperations<Long>): QueryExpression { | ||
return if (values.size == 1) { | ||
field eq encodeBoolAttrWithValue(attrId, values[0]) | ||
} else { | ||
field oneOf values.map { v -> encodeBoolAttrWithValue(attrId, v) } | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Parses [params] and prepares the [AttrFacetFilter] for applying. | ||
* | ||
* @param name - name of the filter | ||
* @param params - parameters that should be applied to a search query. | ||
* Examples: | ||
* - `mapOf(listOf("attrs", "1") to listOf("12", "13"))` | ||
* - `mapOf(listOf("attrs", "2", "all") to listOf("101", "102")) | ||
*/ | ||
override fun prepare(name: String, paramName: String, params: QueryFilterParams): PreparedAttrBoolFacetFilter { | ||
val selectedValues = params.asSequence() | ||
.mapNotNull { (keys, values) -> | ||
@Suppress("MagicNumber") | ||
when { | ||
keys.isEmpty() -> null | ||
keys[0] != paramName -> null | ||
keys.size == 2 -> { | ||
val attrId = IntType.deserializeTermOrNull(keys[1]) | ||
if (attrId != null) { | ||
val parsedValues = values.mapNotNull(BooleanType::deserializeTermOrNull) | ||
if (parsedValues.isNotEmpty()) { | ||
attrId to SelectedValues(attrId, parsedValues) | ||
} else { | ||
null | ||
} | ||
} else { | ||
null | ||
} | ||
} | ||
else -> null | ||
} | ||
} | ||
.toMap() | ||
val facetFilters = selectedValues.values.map { w -> | ||
w.filterExpression(field) | ||
} | ||
val facetFilterExpr = when (facetFilters.size) { | ||
0 -> null | ||
1 -> facetFilters[0] | ||
else -> Bool.filter(facetFilters) | ||
} | ||
|
||
return PreparedAttrBoolFacetFilter(this, name, paramName, facetFilterExpr, selectedValues) | ||
} | ||
} | ||
|
||
class PreparedAttrBoolFacetFilter( | ||
val filter: AttrBoolFacetFilter, | ||
name: String, | ||
paramName: String, | ||
facetFilterExpr: QueryExpression?, | ||
val selectedValues: Map<Int, AttrBoolFacetFilter.SelectedValues>, | ||
) : PreparedFilter<AttrBoolFacetFilterResult>(name, paramName, facetFilterExpr) { | ||
private val otherFilterAggName = "qf:$name.filter" | ||
private val fullAggName = "qf:$name.full" | ||
private val filterFullAggName = "qf:$name.full.filter" | ||
private fun attrAggName(attrId: Int) = "qf:$name.$attrId" | ||
private fun filterAttrAggName(attrId: Int) = "qf:$name.$attrId.filter" | ||
|
||
companion object { | ||
const val DEFAULT_FULL_AGG_SIZE = 100 | ||
internal val SELECTED_ATTR_INIT_SCRIPT = """ | ||
state.buckets = new int[2]; | ||
""".trimIndent() | ||
internal val SELECTED_ATTR_MAP_SCRIPT = """ | ||
if (doc[params.attrsField].size() == 0) { | ||
return; | ||
} | ||
for (v in doc[params.attrsField]) { | ||
def attrId = (int) (v >>> 1); | ||
if (attrId != params.attrId) { | ||
continue; | ||
} | ||
def value = (int) (v & 1); | ||
state.buckets[value]++; | ||
} | ||
""".trimIndent() | ||
internal val SELECTED_ATTR_COMBINE_SCRIPT = """ | ||
return state.buckets; | ||
""".trimIndent() | ||
internal val SELECTED_ATTR_REDUCE_SCRIPT = """ | ||
def buckets = new int[2]; | ||
for (state in states) { | ||
buckets[0] += state[0]; | ||
buckets[1] += state[1]; | ||
} | ||
return buckets; | ||
""".trimIndent() | ||
} | ||
|
||
override fun apply( | ||
searchQuery: SearchQuery<*>, | ||
otherFacetFilterExpressions: List<QueryExpression> | ||
) { | ||
val attrAggs = mutableMapOf<String, Aggregation<*>>() | ||
val fullAgg = TermsAgg(filter.field, size = DEFAULT_FULL_AGG_SIZE) | ||
if (facetFilterExpr != null) { | ||
attrAggs[filterFullAggName] = FilterAgg(facetFilterExpr, aggs = mapOf(fullAggName to fullAgg)) | ||
} else { | ||
attrAggs[fullAggName] = fullAgg | ||
} | ||
|
||
for (attrId in selectedValues.keys) { | ||
val otherAttrFacetFilterExpressions = selectedValues | ||
.mapNotNull { (a, w) -> | ||
if (a != attrId) { | ||
w.filterExpression(filter.field) | ||
} else { | ||
null | ||
} | ||
} | ||
val attrAgg = ScriptedMetricAgg( | ||
RequiredListType(IntType), | ||
initScript = Script.Source(SELECTED_ATTR_INIT_SCRIPT), | ||
mapScript = Script.Source(SELECTED_ATTR_MAP_SCRIPT), | ||
combineScript = Script.Source(SELECTED_ATTR_COMBINE_SCRIPT), | ||
reduceScript = Script.Source(SELECTED_ATTR_REDUCE_SCRIPT), | ||
params = mapOf( | ||
"attrId" to attrId, | ||
"attrsField" to filter.field, | ||
) | ||
) | ||
if (otherAttrFacetFilterExpressions.isNotEmpty()) { | ||
attrAggs[filterAttrAggName(attrId)] = FilterAgg( | ||
maybeWrapBool(Bool::filter, otherAttrFacetFilterExpressions), | ||
aggs = mapOf( | ||
attrAggName(attrId) to attrAgg | ||
) | ||
) | ||
} else { | ||
attrAggs[attrAggName(attrId)] = attrAgg | ||
} | ||
} | ||
|
||
val aggs = if (otherFacetFilterExpressions.isNotEmpty()) { | ||
mutableMapOf( | ||
otherFilterAggName to FilterAgg( | ||
maybeWrapBool(Bool::filter, otherFacetFilterExpressions), | ||
aggs = attrAggs | ||
) | ||
) | ||
} else { | ||
attrAggs | ||
} | ||
|
||
searchQuery.aggs(aggs) | ||
|
||
if (facetFilterExpr != null) { | ||
searchQuery.postFilter(facetFilterExpr) | ||
} | ||
} | ||
|
||
override fun processResult(searchQueryResult: SearchQueryResult<*>): AttrBoolFacetFilterResult { | ||
val facets = mutableMapOf<Int, MutableList<AttrBoolFacetValue>>() | ||
|
||
val aggsResult = searchQueryResult.unwrapFilterAgg(otherFilterAggName) | ||
|
||
val fullAgg = aggsResult.facetAgg<TermsAggResult<Long>>(fullAggName) | ||
for (bucket in fullAgg.buckets) { | ||
val (attrId, value) = decodeBoolAttrAndValue(bucket.key) | ||
facets.getOrPut(attrId, ::mutableListOf) | ||
.add(AttrBoolFacetValue(value, bucket.docCount)) | ||
} | ||
|
||
for ((attrId, selectedAttrValues) in selectedValues) { | ||
val attrAgg = aggsResult.facetAgg<ScriptedMetricAggResult<*>>(attrAggName(attrId)) | ||
val counts = attrAgg.value as List<*> | ||
val facetValues = buildList { | ||
val falseCount = counts[0] as Int | ||
if (falseCount > 0 || false in selectedAttrValues.values) { | ||
add(AttrBoolFacetValue(false, falseCount.toLong())) | ||
} | ||
val trueCount = counts[1] as Int | ||
if (trueCount > 0 || true in selectedAttrValues.values) { | ||
add(AttrBoolFacetValue(true, trueCount.toLong())) | ||
} | ||
}.sortedByDescending { fv -> | ||
// Sort by count descending and then by value ascending | ||
fv.count shl 1 or (if (fv.value) 0 else 1) | ||
} | ||
facets[attrId] = facetValues.toMutableList() | ||
} | ||
|
||
return AttrBoolFacetFilterResult( | ||
name, | ||
paramName, | ||
facets = facets.mapValues { (attrId, values) -> | ||
AttrBoolFacet(attrId, values) | ||
} | ||
) | ||
} | ||
} | ||
|
||
|
||
data class AttrBoolFacetFilterResult( | ||
override val name: String, | ||
override val paramName: String, | ||
val facets: Map<Int, AttrBoolFacet> | ||
) : FilterResult, Iterable<Map.Entry<Int, AttrBoolFacet>> by facets.entries | ||
|
||
data class AttrBoolFacet( | ||
val attrId: Int, | ||
val values: List<AttrBoolFacetValue> | ||
) : Iterable<AttrBoolFacetValue> by values | ||
|
||
data class AttrBoolFacetValue( | ||
val value: Boolean, | ||
val count: Long, | ||
) |
Oops, something went wrong.