Skip to content

Commit 815f639

Browse files
authored
Merge Add MultiSearch and Fix CI/DC #14
2 parents d54544c + 5bc7dbb commit 815f639

File tree

7 files changed

+298
-13
lines changed

7 files changed

+298
-13
lines changed

.circleci/config.yml

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
1-
# Use the latest 2.1 version of CircleCI pipeline process engine. See: https://circleci.com/docs/2.0/configuration-reference
21
version: 2.1
3-
# Use a package of configuration called an orb.
4-
orbs:
5-
# Declare a dependency on the welcome-orb
6-
welcome: circleci/[email protected]
7-
# Orchestrate or schedule a set of jobs
2+
3+
jobs:
4+
test:
5+
macos:
6+
xcode: 13.2.1 # Using 13.2.1 to support backwards compatibility of Modern Concurrency
7+
steps:
8+
- checkout
9+
- run:
10+
name: Install Typesense
11+
command: |
12+
curl --output ts.tar.gz https://dl.typesense.org/releases/0.22.2/typesense-server-0.22.2-darwin-amd64.tar.gz
13+
tar -xzf ts.tar.gz
14+
- run:
15+
name: Run Typesense
16+
background: true
17+
command: |
18+
./typesense-server --api-key=xyz --data-dir=/tmp
19+
- run:
20+
# Build and Test the Typesense Package
21+
name: Run Tests
22+
command: swift test
23+
824
workflows:
9-
# Name the workflow "welcome"
10-
welcome:
11-
# Run the welcome/run job in its own container
25+
version: 2
26+
test_build:
1227
jobs:
13-
- welcome/run
28+
- test

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
/*.xcodeproj
55
xcuserdata/
66
DerivedData/
7-
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
7+
.swiftpm

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import PackageDescription
66
let package = Package(
77
name: "Typesense",
88
platforms: [
9-
.iOS(.v13), .macOS(.v12)
9+
.iOS(.v13), .macOS(.v10_15)
1010
],
1111
products: [
1212
// Products define the executables and libraries a package produces, and make them visible to other packages.

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,5 @@ The generated Models (inside the Models directory) are to be used inside the Mod
9696
## TODO: Features
9797

9898
- Curation API
99-
- Multisearch
10099
- Dealing with Dirty Data
101100
- Scoped Search Key

Sources/Typesense/Client.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,8 @@ public struct Client {
2727
public func operations() -> Operations {
2828
return Operations(config: self.configuration)
2929
}
30+
31+
public func multiSearch() -> MultiSearch {
32+
return MultiSearch(config: self.configuration)
33+
}
3034
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import Foundation
2+
3+
public struct MultiSearch {
4+
var apiCall: ApiCall
5+
let RESOURCEPATH = "multi_search"
6+
7+
public init(config: Configuration) {
8+
apiCall = ApiCall(config: config)
9+
}
10+
11+
public func perform<T>(searchRequests: [MultiSearchCollectionParameters], commonParameters: MultiSearchParameters, for: T.Type) async throws -> (MultiSearchResult<T>?, URLResponse?) {
12+
var searchQueryParams: [URLQueryItem] = []
13+
14+
if let query = commonParameters.q {
15+
searchQueryParams.append(URLQueryItem(name: "q", value: query))
16+
}
17+
18+
if let queryBy = commonParameters.queryBy {
19+
searchQueryParams.append(URLQueryItem(name: "query_by", value: queryBy))
20+
}
21+
22+
if let queryByWeights = commonParameters.queryByWeights {
23+
searchQueryParams.append(URLQueryItem(name: "query_by_weights", value: queryByWeights))
24+
}
25+
26+
if let maxHits = commonParameters.maxHits {
27+
searchQueryParams.append(URLQueryItem(name: "max_hits", value: maxHits))
28+
}
29+
30+
if let _prefix = commonParameters._prefix {
31+
var fullString = ""
32+
for item in _prefix {
33+
fullString.append(String(item))
34+
fullString.append(",")
35+
}
36+
37+
searchQueryParams.append(URLQueryItem(name: "prefix", value: String(fullString.dropLast())))
38+
}
39+
40+
if let filterBy = commonParameters.filterBy {
41+
searchQueryParams.append(URLQueryItem(name: "filter_by", value: filterBy))
42+
}
43+
44+
if let sortBy = commonParameters.sortBy {
45+
searchQueryParams.append(URLQueryItem(name: "sort_by", value: sortBy))
46+
}
47+
48+
if let facetBy = commonParameters.facetBy {
49+
searchQueryParams.append(URLQueryItem(name: "facet_by", value: facetBy))
50+
}
51+
52+
if let maxFacetValues = commonParameters.maxFacetValues {
53+
searchQueryParams.append(URLQueryItem(name: "max_facet_values", value: String(maxFacetValues)))
54+
}
55+
56+
if let facetQuery = commonParameters.facetQuery {
57+
searchQueryParams.append(URLQueryItem(name: "facet_query", value: facetQuery))
58+
}
59+
60+
if let numTypos = commonParameters.numTypos {
61+
searchQueryParams.append(URLQueryItem(name: "num_typos", value: String(numTypos)))
62+
}
63+
64+
if let page = commonParameters.page {
65+
searchQueryParams.append(URLQueryItem(name: "page", value: String(page)))
66+
}
67+
68+
if let perPage = commonParameters.perPage {
69+
searchQueryParams.append(URLQueryItem(name: "per_page", value: String(perPage)))
70+
}
71+
72+
if let groupBy = commonParameters.groupBy {
73+
searchQueryParams.append(URLQueryItem(name: "group_by", value: groupBy))
74+
}
75+
76+
if let groupLimit = commonParameters.groupLimit {
77+
searchQueryParams.append(URLQueryItem(name: "group_limit", value: String(groupLimit)))
78+
}
79+
80+
if let includeFields = commonParameters.includeFields {
81+
searchQueryParams.append(URLQueryItem(name: "include_fields", value: includeFields))
82+
}
83+
84+
if let excludeFields = commonParameters.excludeFields {
85+
searchQueryParams.append(URLQueryItem(name: "exclude_fields", value: excludeFields))
86+
}
87+
88+
if let highlightFullFields = commonParameters.highlightFullFields {
89+
searchQueryParams.append(URLQueryItem(name: "highlight_full_fields", value: highlightFullFields))
90+
}
91+
92+
if let highlightAffixNumTokens = commonParameters.highlightAffixNumTokens {
93+
searchQueryParams.append(URLQueryItem(name: "highlight_affix_num_tokens", value: String(highlightAffixNumTokens)))
94+
}
95+
96+
if let highlightStartTag = commonParameters.highlightStartTag {
97+
searchQueryParams.append(URLQueryItem(name: "highlight_start_tag", value: highlightStartTag))
98+
}
99+
100+
if let highlightEndTag = commonParameters.highlightEndTag {
101+
searchQueryParams.append(URLQueryItem(name: "highlight_end_tag", value: highlightEndTag))
102+
}
103+
104+
if let snippetThreshold = commonParameters.snippetThreshold {
105+
searchQueryParams.append(URLQueryItem(name: "snippet_threshold", value: String(snippetThreshold)))
106+
}
107+
108+
if let dropTokensThreshold = commonParameters.dropTokensThreshold {
109+
searchQueryParams.append(URLQueryItem(name: "drop_tokens_threshold", value: String(dropTokensThreshold)))
110+
}
111+
112+
if let typoTokensThreshold = commonParameters.typoTokensThreshold {
113+
searchQueryParams.append(URLQueryItem(name: "typo_tokens_threshold", value: String(typoTokensThreshold)))
114+
}
115+
116+
if let pinnedHits = commonParameters.pinnedHits {
117+
searchQueryParams.append(URLQueryItem(name: "pinned_hits", value: pinnedHits))
118+
}
119+
120+
if let hiddenHits = commonParameters.hiddenHits {
121+
searchQueryParams.append(URLQueryItem(name: "hidden_hits", value: hiddenHits))
122+
}
123+
124+
if let highlightFields = commonParameters.highlightFields {
125+
searchQueryParams.append(URLQueryItem(name: "highlight_fields", value: highlightFields))
126+
}
127+
128+
if let preSegmentedQuery = commonParameters.preSegmentedQuery {
129+
searchQueryParams.append(URLQueryItem(name: "pre_segmented_query", value: String(preSegmentedQuery)))
130+
}
131+
132+
if let enableOverrides = commonParameters.enableOverrides {
133+
searchQueryParams.append(URLQueryItem(name: "enable_overrides", value: String(enableOverrides)))
134+
}
135+
136+
if let prioritizeExactMatch = commonParameters.prioritizeExactMatch {
137+
searchQueryParams.append(URLQueryItem(name: "prioritize_exact_match", value: String(prioritizeExactMatch)))
138+
}
139+
140+
if let exhaustiveSearch = commonParameters.exhaustiveSearch {
141+
searchQueryParams.append(URLQueryItem(name: "exhaustive_search", value: String(exhaustiveSearch)))
142+
}
143+
144+
if let searchCutoffMs = commonParameters.searchCutoffMs {
145+
searchQueryParams.append(URLQueryItem(name: "search_cutoff_ms", value: String(searchCutoffMs)))
146+
}
147+
148+
if let useCache = commonParameters.useCache {
149+
searchQueryParams.append(URLQueryItem(name: "use_cache", value: String(useCache)))
150+
}
151+
152+
if let cacheTtl = commonParameters.cacheTtl {
153+
searchQueryParams.append(URLQueryItem(name: "cache_ttl", value: String(cacheTtl)))
154+
}
155+
156+
if let minLen1typo = commonParameters.minLen1typo {
157+
searchQueryParams.append(URLQueryItem(name: "min_len1type", value: String(minLen1typo)))
158+
}
159+
160+
if let minLen2typo = commonParameters.minLen2typo {
161+
searchQueryParams.append(URLQueryItem(name: "min_len2type", value: String(minLen2typo)))
162+
}
163+
164+
let searches = MultiSearchSearchesParameter(searches: searchRequests)
165+
166+
let searchesData = try encoder.encode(searches)
167+
168+
let (data, response) = try await apiCall.post(endPoint: "\(RESOURCEPATH)", body: searchesData, queryParameters: searchQueryParams)
169+
170+
if let validData = data {
171+
let searchRes = try decoder.decode(MultiSearchResult<T>.self, from: validData)
172+
return (searchRes, response)
173+
}
174+
175+
return (nil, response)
176+
}
177+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import XCTest
2+
@testable import Typesense
3+
4+
final class MultiSearchTests: XCTestCase {
5+
6+
struct Product: Codable, Equatable {
7+
var name: String?
8+
var price: Int?
9+
var brand: String?
10+
var desc: String?
11+
12+
static func == (lhs: Product, rhs: Product) -> Bool {
13+
return
14+
lhs.name == rhs.name &&
15+
lhs.price == rhs.price &&
16+
lhs.brand == rhs.brand &&
17+
lhs.desc == rhs.desc
18+
}
19+
}
20+
21+
struct Brand: Codable {
22+
var name: String
23+
}
24+
25+
26+
func testMultiSearch() async {
27+
let config = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true))
28+
29+
let client = Client(config: config)
30+
31+
let productSchema = CollectionSchema(name: "products", fields: [
32+
Field(name: "name", type: "string"),
33+
Field(name: "price", type: "int32"),
34+
Field(name: "brand", type: "string"),
35+
Field(name: "desc", type: "string"),
36+
])
37+
38+
let brandSchema = CollectionSchema(name: "brands", fields: [
39+
Field(name: "name", type: "string"),
40+
])
41+
42+
let searchRequests = [
43+
MultiSearchCollectionParameters(q: "shoe", filterBy: "price:=[50..120]", collection: "products"),
44+
MultiSearchCollectionParameters(q: "Nike", collection: "brands"),
45+
]
46+
47+
let brand1 = Brand(name: "Nike")
48+
let product1 = Product(name: "Jordan", price: 70, brand: "Nike", desc: "High quality shoe")
49+
50+
let commonParams = MultiSearchParameters(queryBy: "name")
51+
52+
do {
53+
let (_, _) = try await client.collections.create(schema: productSchema) //Creating test collection - Products
54+
let (_,_) = try await client.collections.create(schema: brandSchema)
55+
56+
let (_,_) = try await client.collection(name: "products").documents().create(document: encoder.encode(product1))
57+
58+
let (_,_) = try await client.collection(name: "brands").documents().create(document: encoder.encode(brand1))
59+
60+
let (data, _) = try await client.multiSearch().perform(searchRequests: searchRequests, commonParameters: commonParams, for: Product.self)
61+
62+
let (_,_) = try await client.collection(name: "products").delete() //Deleting test collection
63+
let (_,_) = try await client.collection(name: "brands").delete() //Deleting test collection
64+
65+
XCTAssertNotNil(data)
66+
guard let validResp = data else {
67+
throw DataError.dataNotFound
68+
}
69+
70+
XCTAssertNotNil(validResp.results)
71+
XCTAssertNotEqual(validResp.results.count, 0)
72+
XCTAssertNotNil(validResp.results[0].hits)
73+
XCTAssertNotNil(validResp.results[1].hits)
74+
XCTAssertEqual(validResp.results[1].hits?.count, 1)
75+
76+
print(validResp.results[1].hits as Any)
77+
} catch HTTPError.serverError(let code, let desc) {
78+
print(desc)
79+
print("The response status code is \(code)")
80+
XCTAssertTrue(false)
81+
} catch (let error) {
82+
print(error.localizedDescription)
83+
XCTAssertTrue(false)
84+
}
85+
86+
}
87+
88+
89+
90+
}

0 commit comments

Comments
 (0)