Skip to content

Commit 99d6a9d

Browse files
feat: Manual setting of disjunctive facets (#150)
* feat: add possibility to manually add disjunctive facets in addition to derived from filter groups ones
1 parent dfa1b4a commit 99d6a9d

File tree

8 files changed

+159
-68
lines changed

8 files changed

+159
-68
lines changed

Sources/InstantSearchCore/QueryBuilder/QueryBuilder+DisjunctiveFaceting.swift

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -92,31 +92,25 @@ extension QueryBuilder {
9292
/// - returns: list of "or" queries for disjunctive faceting
9393

9494
func buildDisjunctiveFacetingQueries(query: Query, filterGroups: [FilterGroupType], disjunctiveFacets: Set<Attribute>) -> [Query] {
95-
96-
return disjunctiveFacets.map { attribute in
97-
98-
let groups = filterGroups.map { (group) -> FilterGroupType in
99-
guard let disjunctiveFacetGroup = group as? FilterGroup.Or<Filter.Facet> else {
100-
return group
101-
}
102-
let filtersMinusDisjunctiveFacet = disjunctiveFacetGroup.typedFilters.filter { $0.attribute != attribute }
103-
return FilterGroup.Or(filters: filtersMinusDisjunctiveFacet, name: group.name)
104-
}.filter { !$0.filters.isEmpty }
105-
106-
var query = query
107-
query.facets = [attribute]
108-
query.requestOnlyFacets()
109-
query.filters = FilterGroupConverter().sql(groups)
110-
return query
111-
112-
}
113-
95+
return disjunctiveFacets.map { query.disjunctiveFacetingQuery(disjunctiveFacetAttribute: $0, with: filterGroups) }
11496
}
11597

11698
}
11799

118100
extension Query {
119101

102+
func disjunctiveFacetingQuery(disjunctiveFacetAttribute: Attribute, with filterGroups: [FilterGroupType]) -> Query {
103+
let filterGroups = filterGroups.droppingDisjunctiveFacetFilters(with: disjunctiveFacetAttribute)
104+
var output = self
105+
output.facets = [disjunctiveFacetAttribute]
106+
output.filters = FilterGroupConverter().sql(filterGroups)
107+
output.attributesToRetrieve = []
108+
output.attributesToHighlight = []
109+
output.hitsPerPage = 0
110+
output.analytics = false
111+
return output
112+
}
113+
120114
mutating func requestOnlyFacets() {
121115
attributesToRetrieve = []
122116
attributesToHighlight = []
@@ -126,6 +120,21 @@ extension Query {
126120

127121
}
128122

123+
extension Array where Element == FilterGroupType {
124+
125+
func droppingDisjunctiveFacetFilters(with attribute: Attribute) -> Self {
126+
map { group in
127+
guard let disjunctiveFacetGroup = group as? FilterGroup.Or<Filter.Facet> else {
128+
return group
129+
}
130+
let filtersMinusDisjunctiveFacet = disjunctiveFacetGroup.typedFilters.filter { $0.attribute != attribute }
131+
return FilterGroup.Or(filters: filtersMinusDisjunctiveFacet, name: group.name)
132+
}
133+
.filter { !$0.filters.isEmpty }
134+
}
135+
136+
}
137+
129138
extension Collection {
130139

131140
func partition(by predicate: (Element) -> Bool) -> (satisfying: [Element], rest: [Element]) {

Sources/InstantSearchCore/QueryBuilder/QueryBuilder.swift

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,19 @@ public struct QueryBuilder {
3838
}
3939

4040
public init(query: Query,
41+
disjunctiveFacets: Set<Attribute> = [],
4142
filterGroups: [FilterGroupType] = [],
4243
hierarchicalAttributes: [Attribute] = [],
4344
hierachicalFilters: [Filter.Facet] = []) {
44-
self.query = query
45-
self.keepSelectedEmptyFacets = false
46-
self.filterGroups = filterGroups
47-
self.disjunctiveFacets = Set(filterGroups
45+
let disjunctiveFacetsFromFilters = filterGroups
4846
.compactMap { $0 as? FilterGroup.Or<Filter.Facet> }
4947
.map { $0.filters }
5048
.flatMap { $0 }
51-
.map { $0.attribute })
49+
.map { $0.attribute }
50+
self.query = query
51+
self.keepSelectedEmptyFacets = false
52+
self.filterGroups = filterGroups
53+
self.disjunctiveFacets = disjunctiveFacets.union(disjunctiveFacetsFromFilters)
5254
self.hierarchicalAttributes = hierarchicalAttributes
5355
self.hierachicalFilters = hierachicalFilters
5456
}
@@ -84,11 +86,9 @@ public struct QueryBuilder {
8486
let resultsForDisjuncitveFaceting = resultsForFaceting[...disjunctiveFacetingQueriesCount]
8587
let resultsForHierarchicalFaceting = resultsForFaceting.dropFirst(disjunctiveFacetingQueriesCount)[..<totalQueriesCount]
8688

87-
let facetStats = results.aggregateFacetStats()
88-
aggregatedResult.facetStats = facetStats.isEmpty ? nil : facetStats
89-
90-
aggregatedResult = update(aggregatedResult, withResultsForDisjuncitveFaceting: resultsForDisjuncitveFaceting)
91-
aggregatedResult = update(aggregatedResult, withResultsForHierarchicalFaceting: resultsForHierarchicalFaceting)
89+
update(&aggregatedResult, withResults: results)
90+
update(&aggregatedResult, withResultsForDisjuncitveFaceting: resultsForDisjuncitveFaceting)
91+
update(&aggregatedResult, withResultsForHierarchicalFaceting: resultsForHierarchicalFaceting)
9292

9393
if keepSelectedEmptyFacets {
9494
let filters = filterGroups.flatMap { $0.filters }
@@ -99,18 +99,19 @@ public struct QueryBuilder {
9999

100100
}
101101

102-
func update<C: Collection>(_ results: SearchResponse, withResultsForDisjuncitveFaceting resultsForDisjuncitveFaceting: C) -> SearchResponse where C.Element == SearchResponse {
103-
var output = results
104-
output.disjunctiveFacets = resultsForDisjuncitveFaceting.aggregateFacets()
105-
output.exhaustiveFacetsCount = resultsForDisjuncitveFaceting.allSatisfy { $0.exhaustiveFacetsCount == true }
106-
return output
102+
func update<C: Collection>(_ result: inout SearchResponse, withResults results: C) where C.Element == SearchResponse {
103+
let facetStats = results.aggregateFacetStats()
104+
result.facetStats = facetStats.isEmpty ? nil : facetStats
105+
}
106+
107+
func update<C: Collection>(_ result: inout SearchResponse, withResultsForDisjuncitveFaceting resultsForDisjuncitveFaceting: C) where C.Element == SearchResponse {
108+
result.disjunctiveFacets = resultsForDisjuncitveFaceting.aggregateFacets()
109+
result.exhaustiveFacetsCount = resultsForDisjuncitveFaceting.allSatisfy { $0.exhaustiveFacetsCount == true }
107110
}
108111

109-
func update<C: Collection>(_ results: SearchResponse, withResultsForHierarchicalFaceting resultsForHierarchicalFaceting: C) -> SearchResponse where C.Element == SearchResponse {
110-
var output = results
112+
func update<C: Collection>(_ result: inout SearchResponse, withResultsForHierarchicalFaceting resultsForHierarchicalFaceting: C) where C.Element == SearchResponse {
111113
let hierarchicalFacets = resultsForHierarchicalFaceting.aggregateFacets()
112-
output.hierarchicalFacets = hierarchicalFacets.isEmpty ? nil : hierarchicalFacets
113-
return output
114+
result.hierarchicalFacets = hierarchicalFacets.isEmpty ? nil : hierarchicalFacets
114115
}
115116

116117
public enum Error: Swift.Error {

Sources/InstantSearchCore/Searcher/SingleIndex/SingleIndexSearcher.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ public class SingleIndexSearcher: Searcher, SequencerDelegate, SearchResultObser
6767
/// Delegate providing a necessary information for hierarchical faceting
6868
public weak var hierarchicalFacetingDelegate: HierarchicalFacetingDelegate?
6969

70+
/// Manually set attributes for disjunctive faceting
71+
///
72+
/// These attributes are merged with disjunctiveFacetsAttributes provided by DisjunctiveFacetingDelegate to create the necessary queries for disjunctive faceting
73+
public var disjunctiveFacetsAttributes: Set<Attribute>
74+
7075
/// Flag defining if disjunctive faceting is enabled
7176
/// - Default value: true
7277
public var isDisjunctiveFacetingEnabled = true
@@ -127,6 +132,7 @@ public class SingleIndexSearcher: Searcher, SequencerDelegate, SearchResultObser
127132
onIndexChanged = .init()
128133
processingQueue = .init()
129134
onSearch = .init()
135+
disjunctiveFacetsAttributes = []
130136
sequencer.delegate = self
131137
onResults.retainLastData = true
132138
onError.retainLastData = false
@@ -163,10 +169,12 @@ public class SingleIndexSearcher: Searcher, SequencerDelegate, SearchResultObser
163169
let operation: Operation
164170

165171
if isDisjunctiveFacetingEnabled {
172+
let disjunctiveFacets = disjunctiveFacetsAttributes.union(disjunctiveFacetingDelegate?.disjunctiveFacetsAttributes ?? [])
166173
let filterGroups = disjunctiveFacetingDelegate?.toFilterGroups() ?? []
167174
let hierarchicalAttributes = hierarchicalFacetingDelegate?.hierarchicalAttributes ?? []
168175
let hierarchicalFilters = hierarchicalFacetingDelegate?.hierarchicalFilters ?? []
169176
var queriesBuilder = QueryBuilder(query: query,
177+
disjunctiveFacets: disjunctiveFacets,
170178
filterGroups: filterGroups,
171179
hierarchicalAttributes: hierarchicalAttributes,
172180
hierachicalFilters: hierarchicalFilters)

Tests/InstantSearchCoreTests/Integration/DisjuncitveAndHierarchicalIntegrationTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class DisjuncitveAndHierarchicalIntegrationTests: OnlineTestCase {
9898
let query = Query("").set(\.facets, to: Set(facetAttributes))
9999

100100
let queryBuilder = QueryBuilder(query: query,
101+
disjunctiveFacets: [],
101102
filterGroups: filterGroups,
102103
hierarchicalAttributes: hierarchicalAttributes,
103104
hierachicalFilters: hierarchicalFilters)

Tests/InstantSearchCoreTests/Integration/DisjunctiveFacetingIntegrationTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class DisjunctiveFacetingIntegrationTests: OnlineTestCase {
5959
let query = Query().set(\.facets, to: Set(disjunctiveAttributes))
6060
let colorFilter = Filter.Facet(attribute: "color", stringValue: "blue")
6161
let disjunctiveGroup = FilterGroup.Or(filters: [colorFilter], name: "colors")
62-
let queryBuilder = QueryBuilder(query: query, filterGroups: [disjunctiveGroup])
62+
let queryBuilder = QueryBuilder(query: query, disjunctiveFacets: [], filterGroups: [disjunctiveGroup])
6363

6464
let queries = queryBuilder.build().map { IndexedQuery(indexName: index.name, query: $0) }
6565

@@ -111,7 +111,7 @@ class DisjunctiveFacetingIntegrationTests: OnlineTestCase {
111111
let disjunctiveGroup = FilterGroup.Or(filters: [colorFilter], name: "colors")
112112
let promotionsFilter = Filter.Facet(attribute: "promotions", stringValue: "coupon")
113113
let conjunctiveGroup = FilterGroup.And(filters: [promotionsFilter], name: "promotions")
114-
let queryBuilder = QueryBuilder(query: query, filterGroups: [disjunctiveGroup, conjunctiveGroup])
114+
let queryBuilder = QueryBuilder(query: query, disjunctiveFacets: [], filterGroups: [disjunctiveGroup, conjunctiveGroup])
115115

116116
let queries = queryBuilder.build().map { IndexedQuery(indexName: index.name, query: $0) }
117117

Tests/InstantSearchCoreTests/Integration/HierarchicalIntegrationTests.swift

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,10 @@ class HierarchicalTests: OnlineTestCase {
7676
let query = Query("").set(\.facets, to: Set(hierarchicalAttributes))
7777

7878
let queryBuilder = QueryBuilder(query: query,
79-
filterGroups: filterGroups,
80-
hierarchicalAttributes: hierarchicalAttributes,
81-
hierachicalFilters: hierarchicalFilters)
79+
disjunctiveFacets: [],
80+
filterGroups: filterGroups,
81+
hierarchicalAttributes: hierarchicalAttributes,
82+
hierachicalFilters: hierarchicalFilters)
8283
let queries = queryBuilder.build().map { IndexedQuery(indexName: self.index.name, query: $0) }
8384

8485
XCTAssertEqual(queryBuilder.hierarchicalFacetingQueriesCount, 3)
@@ -111,9 +112,10 @@ class HierarchicalTests: OnlineTestCase {
111112
let query = Query("").set(\.facets, to: Set(hierarchicalAttributes))
112113

113114
let queryBuilder = QueryBuilder(query: query,
114-
filterGroups: filterGroups,
115-
hierarchicalAttributes: hierarchicalAttributes,
116-
hierachicalFilters: hierarchicalFilters)
115+
disjunctiveFacets: [],
116+
filterGroups: filterGroups,
117+
hierarchicalAttributes: hierarchicalAttributes,
118+
hierachicalFilters: hierarchicalFilters)
117119
let queries = queryBuilder.build().map { IndexedQuery(indexName: self.index.name, query: $0) }
118120

119121
XCTAssertEqual(queryBuilder.hierarchicalFacetingQueriesCount, 0)
@@ -167,9 +169,10 @@ class HierarchicalTests: OnlineTestCase {
167169
let query = Query("").set(\.facets, to: Set(hierarchicalAttributes))
168170

169171
let queryBuilder = QueryBuilder(query: query,
170-
filterGroups: filterGroups,
171-
hierarchicalAttributes: hierarchicalAttributes,
172-
hierachicalFilters: hierarchicalFilters)
172+
disjunctiveFacets: [],
173+
filterGroups: filterGroups,
174+
hierarchicalAttributes: hierarchicalAttributes,
175+
hierachicalFilters: hierarchicalFilters)
173176
let queries = queryBuilder.build().map { IndexedQuery(indexName: self.index.name, query: $0) }
174177

175178
XCTAssertEqual(queryBuilder.hierarchicalFacetingQueriesCount, 3)

Tests/InstantSearchCoreTests/Unit/DisjunctiveFacetingsTests.swift

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,11 @@ import AlgoliaSearchClient
1313

1414
class DisjunctiveFacetingTests: XCTestCase {
1515

16-
class TestDelegate: DisjunctiveFacetingDelegate {
17-
18-
let disjunctiveFacetsAttributes: Set<Attribute>
19-
let filterGroups: [FilterGroupType]
20-
21-
init(disjunctiveFacetsAttributes: Set<Attribute>, filterGroups: [FilterGroupType]) {
22-
self.disjunctiveFacetsAttributes = disjunctiveFacetsAttributes
23-
self.filterGroups = filterGroups
24-
}
25-
26-
func toFilterGroups() -> [FilterGroupType] {
27-
return filterGroups
28-
}
29-
30-
}
31-
3216
func testMergeResults() {
3317

3418
let query = Query()
3519

36-
let queryBuilder = QueryBuilder(query: query, filterGroups: [
20+
let queryBuilder = QueryBuilder(query: query, disjunctiveFacets: [], filterGroups: [
3721
FilterGroup.Or(filters: [Filter.Facet(attribute: "price", floatValue: 100)], name: "price"),
3822
FilterGroup.Or(filters: [Filter.Facet(attribute: "pubYear", floatValue: 2000)], name: "pubYear")
3923
])
@@ -65,7 +49,7 @@ class DisjunctiveFacetingTests: XCTestCase {
6549

6650
let filterGroups: [FilterGroupType] = [colorGroup, sizeGroup]
6751

68-
let queryBuilder = QueryBuilder(query: query, filterGroups: filterGroups)
52+
let queryBuilder = QueryBuilder(query: query, disjunctiveFacets: [], filterGroups: filterGroups)
6953

7054
let queries = queryBuilder.build()
7155

@@ -114,7 +98,11 @@ class DisjunctiveFacetingTests: XCTestCase {
11498
.init(attribute: "category.lvl2", stringValue: "a > b > c")
11599
]
116100

117-
let queryBuilder = QueryBuilder(query: query, filterGroups: filterGroups, hierarchicalAttributes: hierarchicalAttributes, hierachicalFilters: hierarchicalFilters)
101+
let queryBuilder = QueryBuilder(query: query,
102+
disjunctiveFacets: [],
103+
filterGroups: filterGroups,
104+
hierarchicalAttributes: hierarchicalAttributes,
105+
hierachicalFilters: hierarchicalFilters)
118106

119107
let queries = queryBuilder.build()
120108

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//
2+
// QueryBuilderTests.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 16/12/2020.
6+
//
7+
8+
import Foundation
9+
import XCTest
10+
@testable import InstantSearchCore
11+
12+
13+
class QueryBuilderTests: XCTestCase {
14+
15+
func testDisjunctiveFacetingQueriesGeneration() {
16+
17+
let query = Query("phone")
18+
let disjunctiveFacets: Set<Attribute> = ["price", "color"]
19+
20+
let queryBuilder = QueryBuilder(query: query, disjunctiveFacets: disjunctiveFacets)
21+
22+
let queries = queryBuilder.build()
23+
24+
XCTAssertNotNil(queries.first)
25+
26+
let disjunctiveFacetingQueries = Array(queries[1...])
27+
XCTAssertEqual(disjunctiveFacetingQueries.count, 2)
28+
29+
disjunctiveFacetingQueries.forEach { XCTAssertEqual($0.facets?.count, 1) }
30+
let disjunctiveFacetingQueriesFacetSet: Set<Attribute> = disjunctiveFacetingQueries.compactMap { $0.facets ?? [] }.reduce([]) { $0.union($1) }
31+
XCTAssertEqual(disjunctiveFacets, disjunctiveFacetingQueriesFacetSet)
32+
}
33+
34+
func testDisjunctiveFacetingWithFiltersQueriesGeneration() {
35+
36+
let query = Query("phone")
37+
let disjunctiveFacets: Set<Attribute> = ["price", "color", "brand"]
38+
let filterGroups: [FilterGroupType] = [
39+
FilterGroup.Or(filters: [Filter.Facet(attribute: "price", value: 100), Filter.Facet(attribute: "color", value: "green"), Filter.Facet(attribute: "size", value: "44")], name: "g1"),
40+
FilterGroup.Or(filters: [Filter.Facet(attribute: "type", value: "phone")], name: "g2"),
41+
FilterGroup.And(filters: [Filter.Facet(attribute: "color", value: "red"), Filter.Numeric(attribute: "price", range: 20...50), Filter.Tag(value: "On sale")] as [FilterType], name: "g3")
42+
]
43+
44+
let queryBuilder = QueryBuilder(query: query, disjunctiveFacets: disjunctiveFacets, filterGroups: filterGroups)
45+
46+
let queries = queryBuilder.build()
47+
48+
XCTAssertNotNil(queries.first)
49+
50+
let disjunctiveFacetingQueries = Array(queries[1...])
51+
XCTAssertEqual(disjunctiveFacetingQueries.count, 5)
52+
53+
disjunctiveFacetingQueries.forEach { XCTAssertEqual($0.facets?.count, 1) }
54+
55+
let priceFilterString = "\"price\":\"100.0\""
56+
let sizeFilterString = "\"size\":\"44\""
57+
let colorFilterString = "\"color\":\"green\""
58+
let singletonOrGroupString = "( \"type\":\"phone\" )"
59+
let intactAndGroupString = "( \"color\":\"red\" AND \"price\":20.0 TO 50.0 AND \"_tags\":\"On sale\" )"
60+
61+
for query in disjunctiveFacetingQueries {
62+
let disjunctiveFacetingAttribute = query.facets!.first!
63+
switch disjunctiveFacetingAttribute {
64+
case "price":
65+
XCTAssertEqual(query.filters, "( \(colorFilterString) OR \(sizeFilterString) ) AND \(singletonOrGroupString) AND \(intactAndGroupString)")
66+
case "color":
67+
XCTAssertEqual(query.filters, "( \(priceFilterString) OR \(sizeFilterString) ) AND \(singletonOrGroupString) AND \(intactAndGroupString)")
68+
case "size":
69+
XCTAssertEqual(query.filters, "( \(priceFilterString) OR \(colorFilterString) ) AND \(singletonOrGroupString) AND \(intactAndGroupString)")
70+
case "type":
71+
XCTAssertEqual(query.filters, "( \(priceFilterString) OR \(colorFilterString) OR \(sizeFilterString) ) AND \(intactAndGroupString)")
72+
case "brand":
73+
XCTAssertEqual(query.filters, "( \(priceFilterString) OR \(colorFilterString) OR \(sizeFilterString) ) AND \(singletonOrGroupString) AND \(intactAndGroupString)")
74+
case let unexpectedAttribute:
75+
XCTFail("Unexpected disjunctive faceting attribute \"\(unexpectedAttribute)\" ")
76+
}
77+
}
78+
79+
}
80+
81+
}

0 commit comments

Comments
 (0)