Skip to content

Commit a349586

Browse files
authored
Merge branch 'main' into fix/python-pydantic-warnings
2 parents b3c60e7 + 34c78b0 commit a349586

File tree

9 files changed

+816
-25
lines changed

9 files changed

+816
-25
lines changed

clients/algoliasearch-client-kotlin/client/build.gradle.kts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
21
import org.jetbrains.kotlin.konan.target.HostManager
32

43
plugins {
@@ -9,7 +8,6 @@ plugins {
98
id("binary-compatibility-validator")
109
}
1110

12-
@OptIn(ExperimentalKotlinGradlePluginApi::class)
1311
kotlin {
1412
applyDefaultHierarchyTemplate()
1513

clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/extensions/SearchClient.kt

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.algolia.client.extensions
22

33
import com.algolia.client.api.SearchClient
44
import com.algolia.client.exception.AlgoliaApiException
5+
import com.algolia.client.extensions.internal.DisjunctiveFaceting
56
import com.algolia.client.extensions.internal.buildRestrictionString
67
import com.algolia.client.extensions.internal.encodeKeySHA256
78
import com.algolia.client.extensions.internal.retryUntil
@@ -105,7 +106,10 @@ public suspend fun SearchClient.waitForTask(
105106
)
106107
}
107108

108-
@Deprecated("Please use waitForTask instead", ReplaceWith("waitForTask(indexName, taskID, maxRetries, timeout, initialDelay, maxDelay, requestOptions)"))
109+
@Deprecated(
110+
"Please use waitForTask instead",
111+
ReplaceWith("waitForTask(indexName, taskID, maxRetries, timeout, initialDelay, maxDelay, requestOptions)"),
112+
)
109113
public suspend fun SearchClient.waitTask(
110114
indexName: String,
111115
taskID: Long,
@@ -154,7 +158,10 @@ public suspend fun SearchClient.waitForAppTask(
154158
)
155159
}
156160

157-
@Deprecated("Please use waitForAppTask instead", ReplaceWith("waitForAppTask(taskID, maxRetries, timeout, initialDelay, maxDelay, requestOptions)"))
161+
@Deprecated(
162+
"Please use waitForAppTask instead",
163+
ReplaceWith("waitForAppTask(taskID, maxRetries, timeout, initialDelay, maxDelay, requestOptions)"),
164+
)
158165
public suspend fun SearchClient.waitAppTask(
159166
taskID: Long,
160167
maxRetries: Int = 50,
@@ -409,7 +416,7 @@ public suspend fun SearchClient.deleteObjects(
409416
* Helper: Replaces object content of all the given objects according to their respective `objectID` field. The `chunkedBatch` helper is used under the hood, which creates a `batch` requests with at most 1000 objects in it.
410417
*
411418
* @param indexName The index in which to perform the request.
412-
* @param objectIDs The list of objects to update in the index.
419+
* @param objects The list of objects to update in the index.
413420
* @param createIfNotExists To be provided if non-existing objects are passed, otherwise, the call will fail..
414421
* @param requestOptions The requestOptions to send along with the query, they will be merged with the transporter requestOptions.
415422
* @return The list of responses from the batch requests.
@@ -498,7 +505,7 @@ public suspend fun SearchClient.replaceAllObjects(
498505
* Generate a virtual API Key without any call to the server.
499506
*
500507
* @param parentApiKey API key to generate from.
501-
* @param restriction Restriction to add the key
508+
* @param restrictions Restriction to add the key
502509
* @throws Exception if an error occurs during the encoding
503510
*/
504511
public fun SearchClient.generateSecuredApiKey(parentApiKey: String, restrictions: SecuredApiKeyRestrictions): String {
@@ -534,3 +541,39 @@ public suspend fun SearchClient.indexExists(indexName: String): Boolean {
534541

535542
return true
536543
}
544+
545+
public data class SearchDisjunctiveFacetingResponse(
546+
val response: SearchResponse,
547+
val disjunctiveFacets: Map<String, Map<String, Int>>,
548+
)
549+
550+
/**
551+
* Method used for perform search with disjunctive facets.
552+
*
553+
* @param indexName The name of the index in which the search queries should be performed
554+
* @param searchParamsObject The search query params.
555+
* @param refinements Refinements to apply to the search in form of dictionary with
556+
* facet attribute as a key and a list of facet values for the designated attribute.
557+
* Any facet in this list not present in the `disjunctiveFacets` set will be filtered conjunctively (with AND operator).
558+
* @param disjunctiveFacets Set of facets attributes applied disjunctively (with OR operator)
559+
* @param requestOptions Configure request locally with RequestOptions.
560+
* @return SearchDisjunctiveFacetingResponse - a struct containing the merge response from all the
561+
* disjunctive faceting search queries, and a list of disjunctive facets
562+
* @throws NoSuchElementException if there are no search response from the multi-query search
563+
*/
564+
public suspend fun SearchClient.searchDisjunctiveFaceting(
565+
indexName: String,
566+
searchParamsObject: SearchParamsObject,
567+
refinements: Map<String, List<String>>,
568+
disjunctiveFacets: Set<String>,
569+
requestOptions: RequestOptions? = null
570+
): SearchDisjunctiveFacetingResponse {
571+
val helper = DisjunctiveFaceting(
572+
query = SearchForHits.from(searchParamsObject, indexName),
573+
refinements = refinements,
574+
disjunctiveFacets = disjunctiveFacets,
575+
)
576+
val queries = helper.buildQueries()
577+
val responses = searchForHits(queries, requestOptions = requestOptions)
578+
return helper.mergeResponses(responses)
579+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package com.algolia.client.extensions
2+
3+
import com.algolia.client.model.search.SearchForHits
4+
import com.algolia.client.model.search.SearchParamsObject
5+
import com.algolia.client.model.search.SearchTypeDefault
6+
7+
/**
8+
* Create a new [SearchForHits] from a [SearchParamsObject] and an [indexName] string.
9+
* Can take an optional [params] string.
10+
*/
11+
public fun SearchForHits.Companion.from(
12+
searchParamsObject: SearchParamsObject,
13+
indexName: String,
14+
params: String? = null
15+
): SearchForHits {
16+
return SearchForHits(
17+
params = params,
18+
query = searchParamsObject.query,
19+
similarQuery = searchParamsObject.similarQuery,
20+
filters = searchParamsObject.filters,
21+
facetFilters = searchParamsObject.facetFilters,
22+
optionalFilters = searchParamsObject.optionalFilters,
23+
numericFilters = searchParamsObject.numericFilters,
24+
tagFilters = searchParamsObject.tagFilters,
25+
sumOrFiltersScores = searchParamsObject.sumOrFiltersScores,
26+
restrictSearchableAttributes = searchParamsObject.restrictSearchableAttributes,
27+
facets = searchParamsObject.facets,
28+
facetingAfterDistinct = searchParamsObject.facetingAfterDistinct,
29+
page = searchParamsObject.page,
30+
offset = searchParamsObject.offset,
31+
length = searchParamsObject.length,
32+
aroundLatLng = searchParamsObject.aroundLatLng,
33+
aroundLatLngViaIP = searchParamsObject.aroundLatLngViaIP,
34+
aroundRadius = searchParamsObject.aroundRadius,
35+
aroundPrecision = searchParamsObject.aroundPrecision,
36+
minimumAroundRadius = searchParamsObject.minimumAroundRadius,
37+
insideBoundingBox = searchParamsObject.insideBoundingBox,
38+
insidePolygon = searchParamsObject.insidePolygon,
39+
naturalLanguages = searchParamsObject.naturalLanguages,
40+
ruleContexts = searchParamsObject.ruleContexts,
41+
personalizationImpact = searchParamsObject.personalizationImpact,
42+
userToken = searchParamsObject.userToken,
43+
getRankingInfo = searchParamsObject.getRankingInfo,
44+
synonyms = searchParamsObject.synonyms,
45+
clickAnalytics = searchParamsObject.clickAnalytics,
46+
analytics = searchParamsObject.analytics,
47+
analyticsTags = searchParamsObject.analyticsTags,
48+
percentileComputation = searchParamsObject.percentileComputation,
49+
enableABTest = searchParamsObject.enableABTest,
50+
attributesToRetrieve = searchParamsObject.attributesToRetrieve,
51+
ranking = searchParamsObject.ranking,
52+
customRanking = searchParamsObject.customRanking,
53+
relevancyStrictness = searchParamsObject.relevancyStrictness,
54+
attributesToHighlight = searchParamsObject.attributesToHighlight,
55+
attributesToSnippet = searchParamsObject.attributesToSnippet,
56+
highlightPreTag = searchParamsObject.highlightPreTag,
57+
highlightPostTag = searchParamsObject.highlightPostTag,
58+
snippetEllipsisText = searchParamsObject.snippetEllipsisText,
59+
restrictHighlightAndSnippetArrays = searchParamsObject.restrictHighlightAndSnippetArrays,
60+
hitsPerPage = searchParamsObject.hitsPerPage,
61+
minWordSizefor1Typo = searchParamsObject.minWordSizefor1Typo,
62+
minWordSizefor2Typos = searchParamsObject.minWordSizefor2Typos,
63+
typoTolerance = searchParamsObject.typoTolerance,
64+
allowTyposOnNumericTokens = searchParamsObject.allowTyposOnNumericTokens,
65+
disableTypoToleranceOnAttributes = searchParamsObject.disableTypoToleranceOnAttributes,
66+
ignorePlurals = searchParamsObject.ignorePlurals,
67+
removeStopWords = searchParamsObject.removeStopWords,
68+
keepDiacriticsOnCharacters = searchParamsObject.keepDiacriticsOnCharacters,
69+
queryLanguages = searchParamsObject.queryLanguages,
70+
decompoundQuery = searchParamsObject.decompoundQuery,
71+
enableRules = searchParamsObject.enableRules,
72+
enablePersonalization = searchParamsObject.enablePersonalization,
73+
queryType = searchParamsObject.queryType,
74+
removeWordsIfNoResults = searchParamsObject.removeWordsIfNoResults,
75+
mode = searchParamsObject.mode,
76+
semanticSearch = searchParamsObject.semanticSearch,
77+
advancedSyntax = searchParamsObject.advancedSyntax,
78+
optionalWords = searchParamsObject.optionalWords,
79+
disableExactOnAttributes = searchParamsObject.disableExactOnAttributes,
80+
exactOnSingleWordQuery = searchParamsObject.exactOnSingleWordQuery,
81+
alternativesAsExact = searchParamsObject.alternativesAsExact,
82+
advancedSyntaxFeatures = searchParamsObject.advancedSyntaxFeatures,
83+
distinct = searchParamsObject.distinct,
84+
replaceSynonymsInHighlight = searchParamsObject.replaceSynonymsInHighlight,
85+
minProximity = searchParamsObject.minProximity,
86+
responseFields = searchParamsObject.responseFields,
87+
maxFacetHits = searchParamsObject.maxFacetHits,
88+
maxValuesPerFacet = searchParamsObject.maxValuesPerFacet,
89+
sortFacetValuesBy = searchParamsObject.sortFacetValuesBy,
90+
attributeCriteriaComputedByMinProximity = searchParamsObject.attributeCriteriaComputedByMinProximity,
91+
renderingContent = searchParamsObject.renderingContent,
92+
enableReRanking = searchParamsObject.enableReRanking,
93+
reRankingApplyFilter = searchParamsObject.reRankingApplyFilter,
94+
indexName = indexName,
95+
type = SearchTypeDefault.Default,
96+
)
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package com.algolia.client.extensions.internal
2+
3+
import com.algolia.client.extensions.SearchDisjunctiveFacetingResponse
4+
import com.algolia.client.model.search.Exhaustive
5+
import com.algolia.client.model.search.SearchForHits
6+
import com.algolia.client.model.search.SearchResponse
7+
8+
/**
9+
* Helper making multiple queries for disjunctive faceting
10+
* and merging the multiple search responses into a single one with
11+
* combined facets information
12+
*/
13+
internal data class DisjunctiveFaceting(
14+
val query: SearchForHits,
15+
val refinements: Map<String, List<String>>,
16+
val disjunctiveFacets: Set<String>,
17+
) {
18+
// Build filters SQL string from the provided refinements and disjunctive facets set
19+
internal fun buildFilters(excludedAttribute: String?): String {
20+
val filters = refinements.entries.sortedBy { it.key }.filter {
21+
it.key != excludedAttribute && it.value.isNotEmpty()
22+
}.map {
23+
val facetOperator = if (disjunctiveFacets.contains(it.key)) " OR " else " AND "
24+
val expression = it.value.joinToString(facetOperator) { value -> """"${it.key}":"$value"""" }
25+
return@map "($expression)"
26+
}
27+
return filters.joinToString(" AND ")
28+
}
29+
30+
/*
31+
* Build search queries to fetch the necessary facets information for disjunctive faceting
32+
* If the disjunctive facets set is empty, makes a single request with applied conjunctive filters
33+
*/
34+
fun buildQueries(): List<SearchForHits> {
35+
val queries = mutableListOf<SearchForHits>()
36+
37+
val mainQueryFilters = listOf(
38+
query.filters,
39+
buildFilters(null),
40+
).mapNotNull { it }
41+
.filter { it.isNotEmpty() }
42+
.joinToString(" AND ")
43+
44+
queries.add(query.copy(filters = mainQueryFilters))
45+
46+
disjunctiveFacets.sortedWith(compareBy { it }).forEach { facet ->
47+
val disjunctiveQuery = query.copy(
48+
facets = listOf(facet),
49+
filters = listOf(
50+
query.filters,
51+
buildFilters(facet),
52+
).mapNotNull { it }
53+
.filter { it.isNotEmpty() }
54+
.joinToString(" AND "),
55+
hitsPerPage = 0,
56+
attributesToRetrieve = emptyList(),
57+
attributesToHighlight = emptyList(),
58+
attributesToSnippet = emptyList(),
59+
analytics = false,
60+
)
61+
62+
queries.add(disjunctiveQuery)
63+
}
64+
65+
return queries
66+
}
67+
68+
// Get applied disjunctive facet values for provided attribute
69+
internal fun appliedDisjunctiveFacetValues(attribute: String): Set<String> {
70+
if (!disjunctiveFacets.contains(attribute)) {
71+
return emptySet()
72+
}
73+
74+
return refinements[attribute]?.toSet() ?: emptySet()
75+
}
76+
77+
// Merge received search responses into single one with combined facets information
78+
fun mergeResponses(
79+
responses: List<SearchResponse>,
80+
): SearchDisjunctiveFacetingResponse {
81+
val mainResponse = responses.first()
82+
val responsesForDisjunctiveFaceting = responses.drop(1)
83+
84+
val mergedDisjunctiveFacets = mutableMapOf<String, Map<String, Int>>()
85+
val mergedFacetStats = mainResponse.facetsStats?.toMutableMap() ?: mutableMapOf()
86+
var mergedExhaustiveFacetsCount = mainResponse.exhaustive?.facetsCount ?: false
87+
88+
for (result in responsesForDisjunctiveFaceting) {
89+
// Merge facet values
90+
for ((attribute, facets) in result.facets ?: emptyMap()) {
91+
// Complete facet values applied in the filters
92+
// but missed in the search response
93+
val missingFacets = appliedDisjunctiveFacetValues(attribute).subtract(facets.keys).associateWith { 0 }
94+
mergedDisjunctiveFacets[attribute] = facets + missingFacets
95+
}
96+
97+
// Merge facets stats
98+
mergedFacetStats.putAll(result.facetsStats ?: emptyMap())
99+
100+
// If facet counts are not exhaustive, propagate this information to the main results.
101+
// Because disjunctive queries are less restrictive than the main query, it can happen that the main query
102+
// returns exhaustive facet counts, while the disjunctive queries do not.
103+
if (result.exhaustive?.facetsCount != null) {
104+
mergedExhaustiveFacetsCount = mergedExhaustiveFacetsCount && (result.exhaustive.facetsCount)
105+
}
106+
}
107+
108+
return SearchDisjunctiveFacetingResponse(
109+
response = mainResponse.copy(
110+
facetsStats = mergedFacetStats,
111+
exhaustive = mainResponse.exhaustive?.copy(
112+
facetsCount = mergedExhaustiveFacetsCount,
113+
) ?: Exhaustive(facetsCount = mergedExhaustiveFacetsCount),
114+
),
115+
disjunctiveFacets = mergedDisjunctiveFacets,
116+
)
117+
}
118+
}

0 commit comments

Comments
 (0)