Skip to content

Commit 853786a

Browse files
algolia-botFluf22
andcommitted
feat(swift): add disjunctive faceting (#3778) (generated) [skip ci]
Co-authored-by: Thomas Raffray <[email protected]>
1 parent 92aeb27 commit 853786a

File tree

4 files changed

+338
-337
lines changed

4 files changed

+338
-337
lines changed

clients/algoliasearch-client-swift/Sources/Search/Extra/DisjunctiveFaceting.swift

Lines changed: 121 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -8,147 +8,143 @@
88
import Foundation
99

1010
public struct SearchDisjunctiveFacetingResponse<T: Codable> {
11-
let response: SearchResponse<T>;
12-
let disjunctiveFacets: [String: [String: Int]];
11+
let response: SearchResponse<T>
12+
let disjunctiveFacets: [String: [String: Int]]
1313
}
1414

1515
/// Helper making multiple queries for disjunctive faceting
1616
/// and merging the multiple search responses into a single one with
1717
/// combined facets information
18-
internal struct DisjunctiveFacetingHelper {
19-
20-
let query: SearchForHits
21-
let refinements: [String: [String]]
22-
let disjunctiveFacets: Set<String>
23-
24-
/// Build filters SQL string from the provided refinements and disjunctive facets set
25-
func buildFilters(excluding excludedAttribute: String?) -> String {
26-
String(
27-
refinements
28-
.sorted(by: { $0.key < $1.key })
29-
.filter { (name: String, values: [String]) in
30-
name != excludedAttribute && !values.isEmpty
31-
}.map { (name: String, values: [String]) in
32-
let facetOperator = disjunctiveFacets.contains(name) ? " OR " : " AND "
33-
let expression = values
34-
.map { value in """
35-
"\(name)":"\(value)"
36-
"""
37-
}
38-
.joined(separator: facetOperator)
39-
return "(\(expression))"
40-
}.joined(separator: " AND ")
41-
)
42-
}
43-
44-
/// Build search queries to fetch the necessary facets information for disjunctive faceting
45-
/// If the disjunctive facets set is empty, makes a single request with applied conjunctive filters
46-
func makeQueries() -> [SearchQuery] {
47-
var queries = [SearchQuery]()
48-
49-
var mainQuery = query
50-
mainQuery.filters = [
51-
mainQuery.filters,
52-
buildFilters(excluding: .none)
53-
]
54-
.compactMap { $0 }
55-
.filter { !$0.isEmpty }
56-
.joined(separator: " AND ")
57-
58-
queries.append(.searchForHits(mainQuery))
59-
60-
disjunctiveFacets
61-
.sorted(by: { $0 < $1 })
62-
.forEach { disjunctiveFacet in
63-
var disjunctiveQuery = query
64-
disjunctiveQuery.facets = [disjunctiveFacet]
65-
disjunctiveQuery.filters = [
66-
disjunctiveQuery.filters,
67-
buildFilters(excluding: disjunctiveFacet)
68-
]
69-
.compactMap { $0 }
70-
.filter { !$0.isEmpty }
71-
.joined(separator: " AND ")
72-
disjunctiveQuery.hitsPerPage = 0
73-
disjunctiveQuery.attributesToRetrieve = []
74-
disjunctiveQuery.attributesToHighlight = []
75-
disjunctiveQuery.attributesToSnippet = []
76-
disjunctiveQuery.analytics = false
77-
queries.append(.searchForHits(disjunctiveQuery))
18+
struct DisjunctiveFacetingHelper {
19+
let query: SearchForHits
20+
let refinements: [String: [String]]
21+
let disjunctiveFacets: Set<String>
22+
23+
/// Build filters SQL string from the provided refinements and disjunctive facets set
24+
func buildFilters(excluding excludedAttribute: String?) -> String {
25+
String(
26+
self.refinements
27+
.sorted(by: { $0.key < $1.key })
28+
.filter { (name: String, values: [String]) in
29+
name != excludedAttribute && !values.isEmpty
30+
}.map { (name: String, values: [String]) in
31+
let facetOperator = self.disjunctiveFacets.contains(name) ? " OR " : " AND "
32+
let expression = values
33+
.map { value in """
34+
"\(name)":"\(value)"
35+
"""
36+
}
37+
.joined(separator: facetOperator)
38+
return "(\(expression))"
39+
}.joined(separator: " AND ")
40+
)
7841
}
7942

80-
return queries
81-
}
82-
83-
/// Get applied disjunctive facet values for provided attribute
84-
func appliedDisjunctiveFacetValues(for attribute: String) -> Set<String> {
85-
guard disjunctiveFacets.contains(attribute) else {
86-
return []
87-
}
88-
return refinements[attribute].flatMap(Set.init) ?? []
89-
}
90-
91-
/// Merge received search responses into single one with combined facets information
92-
func mergeResponses<T: Codable>(
93-
_ responses: [SearchResponse<T>],
94-
keepSelectedEmptyFacets: Bool = true
95-
) throws -> SearchDisjunctiveFacetingResponse<T> {
96-
guard var mainResponse = responses.first else {
97-
throw DisjunctiveFacetingError.emptyResponses
43+
/// Build search queries to fetch the necessary facets information for disjunctive faceting
44+
/// If the disjunctive facets set is empty, makes a single request with applied conjunctive filters
45+
func makeQueries() -> [SearchQuery] {
46+
var queries = [SearchQuery]()
47+
48+
var mainQuery = self.query
49+
mainQuery.filters = [
50+
mainQuery.filters,
51+
self.buildFilters(excluding: .none),
52+
]
53+
.compactMap { $0 }
54+
.filter { !$0.isEmpty }
55+
.joined(separator: " AND ")
56+
57+
queries.append(.searchForHits(mainQuery))
58+
59+
self.disjunctiveFacets
60+
.sorted(by: { $0 < $1 })
61+
.forEach { disjunctiveFacet in
62+
var disjunctiveQuery = self.query
63+
disjunctiveQuery.facets = [disjunctiveFacet]
64+
disjunctiveQuery.filters = [
65+
disjunctiveQuery.filters,
66+
self.buildFilters(excluding: disjunctiveFacet),
67+
]
68+
.compactMap { $0 }
69+
.filter { !$0.isEmpty }
70+
.joined(separator: " AND ")
71+
disjunctiveQuery.hitsPerPage = 0
72+
disjunctiveQuery.attributesToRetrieve = []
73+
disjunctiveQuery.attributesToHighlight = []
74+
disjunctiveQuery.attributesToSnippet = []
75+
disjunctiveQuery.analytics = false
76+
queries.append(.searchForHits(disjunctiveQuery))
77+
}
78+
79+
return queries
9880
}
9981

100-
let responsesForDisjunctiveFaceting = responses.dropFirst()
101-
102-
var mergedDisjunctiveFacets = [String: [String: Int]]()
103-
var mergedFacetStats = mainResponse.facetsStats ?? [:]
104-
var mergedExhaustiveFacetsCount = mainResponse.exhaustive?.facetsCount ?? true
105-
106-
for result in responsesForDisjunctiveFaceting {
107-
// Merge facet values
108-
if let facetsPerAttribute = result.facets {
109-
for (attribute, facets) in facetsPerAttribute {
110-
// Complete facet values applied in the filters
111-
// but missed in the search response
112-
let missingFacets = appliedDisjunctiveFacetValues(for: attribute)
113-
.subtracting(facets.keys)
114-
.reduce(into: [String: Int]()) { acc, cur in acc[cur] = 0 }
115-
mergedDisjunctiveFacets[attribute] = facets.merging(missingFacets) { current, _ in current }
82+
/// Get applied disjunctive facet values for provided attribute
83+
func appliedDisjunctiveFacetValues(for attribute: String) -> Set<String> {
84+
guard self.disjunctiveFacets.contains(attribute) else {
85+
return []
11686
}
117-
}
118-
// Merge facets stats
119-
if let facetsStats = result.facetsStats {
120-
mergedFacetStats.merge(facetsStats) { _, last in last }
121-
}
122-
// If facet counts are not exhaustive, propagate this information to the main results.
123-
// Because disjunctive queries are less restrictive than the main query, it can happen that the main query
124-
// returns exhaustive facet counts, while the disjunctive queries do not.
125-
if let exhaustiveFacetsCount = result.exhaustive?.facetsCount {
126-
mergedExhaustiveFacetsCount = mergedExhaustiveFacetsCount && exhaustiveFacetsCount
127-
}
87+
return self.refinements[attribute].flatMap(Set.init) ?? []
12888
}
129-
mainResponse.facetsStats = mergedFacetStats
130-
if mainResponse.exhaustive == nil {
131-
mainResponse.exhaustive = SearchExhaustive()
132-
}
133-
mainResponse.exhaustive?.facetsCount = mergedExhaustiveFacetsCount
13489

135-
return SearchDisjunctiveFacetingResponse(
136-
response: mainResponse,
137-
disjunctiveFacets: mergedDisjunctiveFacets
138-
)
139-
}
90+
/// Merge received search responses into single one with combined facets information
91+
func mergeResponses<T: Codable>(
92+
_ responses: [SearchResponse<T>],
93+
keepSelectedEmptyFacets _: Bool = true
94+
) throws -> SearchDisjunctiveFacetingResponse<T> {
95+
guard var mainResponse = responses.first else {
96+
throw DisjunctiveFacetingError.emptyResponses
97+
}
14098

99+
let responsesForDisjunctiveFaceting = responses.dropFirst()
100+
101+
var mergedDisjunctiveFacets = [String: [String: Int]]()
102+
var mergedFacetStats = mainResponse.facetsStats ?? [:]
103+
var mergedExhaustiveFacetsCount = mainResponse.exhaustive?.facetsCount ?? true
104+
105+
for result in responsesForDisjunctiveFaceting {
106+
// Merge facet values
107+
if let facetsPerAttribute = result.facets {
108+
for (attribute, facets) in facetsPerAttribute {
109+
// Complete facet values applied in the filters
110+
// but missed in the search response
111+
let missingFacets = self.appliedDisjunctiveFacetValues(for: attribute)
112+
.subtracting(facets.keys)
113+
.reduce(into: [String: Int]()) { acc, cur in acc[cur] = 0 }
114+
mergedDisjunctiveFacets[attribute] = facets.merging(missingFacets) { current, _ in current }
115+
}
116+
}
117+
// Merge facets stats
118+
if let facetsStats = result.facetsStats {
119+
mergedFacetStats.merge(facetsStats) { _, last in last }
120+
}
121+
// If facet counts are not exhaustive, propagate this information to the main results.
122+
// Because disjunctive queries are less restrictive than the main query, it can happen that the main query
123+
// returns exhaustive facet counts, while the disjunctive queries do not.
124+
if let exhaustiveFacetsCount = result.exhaustive?.facetsCount {
125+
mergedExhaustiveFacetsCount = mergedExhaustiveFacetsCount && exhaustiveFacetsCount
126+
}
127+
}
128+
mainResponse.facetsStats = mergedFacetStats
129+
if mainResponse.exhaustive == nil {
130+
mainResponse.exhaustive = SearchExhaustive()
131+
}
132+
mainResponse.exhaustive?.facetsCount = mergedExhaustiveFacetsCount
133+
134+
return SearchDisjunctiveFacetingResponse(
135+
response: mainResponse,
136+
disjunctiveFacets: mergedDisjunctiveFacets
137+
)
138+
}
141139
}
142140

143141
public enum DisjunctiveFacetingError: Error, LocalizedError {
142+
case emptyResponses
144143

145-
case emptyResponses
146-
147-
var localizedDescription: String {
148-
switch self {
149-
case .emptyResponses:
150-
return "Unexpected empty search responses list. At least one search responses might be present."
144+
var localizedDescription: String {
145+
switch self {
146+
case .emptyResponses:
147+
"Unexpected empty search responses list. At least one search responses might be present."
148+
}
151149
}
152-
}
153-
154150
}

clients/algoliasearch-client-swift/Sources/Search/Extra/SearchClientExtension.swift

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -637,40 +637,39 @@ public extension SearchClient {
637637

638638
return true
639639
}
640-
641-
/**
642-
Method used for perform search with disjunctive facets.
643-
644-
- Parameter indexName: The name of the index in which the search queries should be performed
645-
- Parameter searchParamsObject: The search query params.
646-
- Parameter refinements: Refinements to apply to the search in form of dictionary with
647-
facet attribute as a key and a list of facet values for the designated attribute.
648-
Any facet in this list not present in the `disjunctiveFacets` set will be filtered conjunctively.
649-
- Parameter disjunctiveFacets: Set of facets attributes applied disjunctively (with OR operator)
650-
- Parameter keepSelectedEmptyFacets: Whether the selected facet values might be preserved even
651-
in case of their absence in the search response
652-
- Parameter requestOptions: Configure request locally with RequestOptions.
653-
- Returns: SearchDisjunctiveFacetingResponse<T> - a struct containing the merge response from all the disjunctive faceting search queries,
654-
and a list of disjunctive facets
655-
*/
640+
641+
/// Method used for perform search with disjunctive facets.
642+
///
643+
/// - Parameter indexName: The name of the index in which the search queries should be performed
644+
/// - Parameter searchParamsObject: The search query params.
645+
/// - Parameter refinements: Refinements to apply to the search in form of dictionary with
646+
/// facet attribute as a key and a list of facet values for the designated attribute.
647+
/// Any facet in this list not present in the `disjunctiveFacets` set will be filtered conjunctively.
648+
/// - Parameter disjunctiveFacets: Set of facets attributes applied disjunctively (with OR operator)
649+
/// - Parameter keepSelectedEmptyFacets: Whether the selected facet values might be preserved even
650+
/// in case of their absence in the search response
651+
/// - Parameter requestOptions: Configure request locally with RequestOptions.
652+
/// - Returns: SearchDisjunctiveFacetingResponse<T> - a struct containing the merge response from all the
653+
/// disjunctive faceting search queries,
654+
/// and a list of disjunctive facets
656655
func searchDisjunctiveFaceting<T: Codable>(
657-
indexName: String,
658-
searchParamsObject: SearchSearchParamsObject,
659-
refinements: [String: [String]],
660-
disjunctiveFacets: Set<String>,
661-
keepSelectedEmptyFacets: Bool = true,
662-
requestOptions: RequestOptions? = nil
656+
indexName: String,
657+
searchParamsObject: SearchSearchParamsObject,
658+
refinements: [String: [String]],
659+
disjunctiveFacets: Set<String>,
660+
keepSelectedEmptyFacets: Bool = true,
661+
requestOptions: RequestOptions? = nil
663662
) async throws -> SearchDisjunctiveFacetingResponse<T> {
664-
let helper = DisjunctiveFacetingHelper(
665-
query: SearchForHits(from: searchParamsObject, indexName: indexName),
666-
refinements: refinements,
667-
disjunctiveFacets: disjunctiveFacets
668-
)
669-
let queries = helper.makeQueries()
670-
let responses: [SearchResponse<T>] = try await self.searchForHitsWithResponse(
671-
searchMethodParams: SearchMethodParams(requests: queries),
672-
requestOptions: requestOptions
673-
)
674-
return try helper.mergeResponses(responses, keepSelectedEmptyFacets: keepSelectedEmptyFacets)
663+
let helper = DisjunctiveFacetingHelper(
664+
query: SearchForHits(from: searchParamsObject, indexName: indexName),
665+
refinements: refinements,
666+
disjunctiveFacets: disjunctiveFacets
667+
)
668+
let queries = helper.makeQueries()
669+
let responses: [SearchResponse<T>] = try await self.searchForHitsWithResponse(
670+
searchMethodParams: SearchMethodParams(requests: queries),
671+
requestOptions: requestOptions
672+
)
673+
return try helper.mergeResponses(responses, keepSelectedEmptyFacets: keepSelectedEmptyFacets)
675674
}
676675
}

0 commit comments

Comments
 (0)