From e273f507220924325f7f2f7b89a34467aeea2d14 Mon Sep 17 00:00:00 2001 From: Hayden Date: Thu, 8 Aug 2024 08:24:17 +0700 Subject: [PATCH 1/7] feat: retrieve debug info --- .../Models/DebugRetrieveSchema.swift | 16 ++++++++ Sources/Typesense/Operations.swift | 9 ++++ Tests/TypesenseTests/OperationTests.swift | 41 ++++++++++++++----- 3 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 Sources/Typesense/Models/DebugRetrieveSchema.swift diff --git a/Sources/Typesense/Models/DebugRetrieveSchema.swift b/Sources/Typesense/Models/DebugRetrieveSchema.swift new file mode 100644 index 0000000..6c25698 --- /dev/null +++ b/Sources/Typesense/Models/DebugRetrieveSchema.swift @@ -0,0 +1,16 @@ +import Foundation + + + +public struct DebugRetrieveSchema: Codable { + + public var state: Int + public var version: String + + public init(state: Int, version: String) { + self.state = state + self.version = version + } + + +} diff --git a/Sources/Typesense/Operations.swift b/Sources/Typesense/Operations.swift index 2f733a5..18a7b79 100644 --- a/Sources/Typesense/Operations.swift +++ b/Sources/Typesense/Operations.swift @@ -31,6 +31,15 @@ public struct Operations { return (data, response) } + public func getDebug() async throws -> (DebugRetrieveSchema?, URLResponse?) { + let (data, response) = try await apiCall.get(endPoint: "debug") + if let result = data { + let decodedData = try decoder.decode(DebugRetrieveSchema.self, from: result) + return (decodedData, response) + } + return (nil, response) + } + public func vote() async throws -> (SuccessStatus?, URLResponse?) { let (data, response) = try await apiCall.post(endPoint: "\(RESOURCEPATH)/vote", body: Data()) if let result = data { diff --git a/Tests/TypesenseTests/OperationTests.swift b/Tests/TypesenseTests/OperationTests.swift index 8c88a79..5ff0861 100644 --- a/Tests/TypesenseTests/OperationTests.swift +++ b/Tests/TypesenseTests/OperationTests.swift @@ -5,7 +5,7 @@ final class OperationTests: XCTestCase { func testGetHealth() async { let newConfig = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true)) let myClient = Client(config: newConfig) - + do { let (data, _) = try await myClient.operations().getHealth() XCTAssertNotNil(data) @@ -23,11 +23,11 @@ final class OperationTests: XCTestCase { XCTAssertTrue(false) //To prevent this, check availability of Typesense Server and retry } } - + func testGetStats() async { let newConfig = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true)) let myClient = Client(config: newConfig) - + do { let (data, _) = try await myClient.operations().getStats() XCTAssertNotNil(data) @@ -44,11 +44,11 @@ final class OperationTests: XCTestCase { XCTAssertTrue(false) } } - + func testGetMetrics() async { let newConfig = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true)) let myClient = Client(config: newConfig) - + do { let (data, _) = try await myClient.operations().getMetrics() XCTAssertNotNil(data) @@ -65,11 +65,30 @@ final class OperationTests: XCTestCase { XCTAssertTrue(false) } } - + + func testGetDebug() async { + do { + let (data, _) = try await client.operations().getDebug() + XCTAssertNotNil(data) + guard let validData = data else { + throw DataError.dataNotFound + } + print(validData) + XCTAssertEqual(1, validData.state) + } catch HTTPError.serverError(let code, let desc) { + print(desc) + print("The response status code is \(code)") + XCTAssertTrue(false) + } catch (let error) { + print(error.localizedDescription) + XCTAssertTrue(false) + } + } + func testVote() async { let newConfig = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true)) let myClient = Client(config: newConfig) - + do { let (data, _) = try await myClient.operations().vote() XCTAssertNotNil(data) @@ -87,11 +106,11 @@ final class OperationTests: XCTestCase { XCTAssertTrue(false) } } - + func testSnapshot() async { let newConfig = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true)) let myClient = Client(config: newConfig) - + do { let (data, _) = try await myClient.operations().snapshot(path: "/tmp/typesense-data-snapshot") XCTAssertNotNil(data) @@ -109,11 +128,11 @@ final class OperationTests: XCTestCase { XCTAssertTrue(false) } } - + func testSlowRequestLog() async { let newConfig = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true)) let myClient = Client(config: newConfig) - + do { let (data, _) = try await myClient.operations().toggleSlowRequestLog(seconds: 2) XCTAssertNotNil(data) From 7074950346645d3f256ad814fb74ce3e7906f6cf Mon Sep 17 00:00:00 2001 From: Hayden Date: Thu, 8 Aug 2024 08:25:48 +0700 Subject: [PATCH 2/7] update README: getDebug --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f29717e..29f5732 100644 --- a/README.md +++ b/README.md @@ -181,12 +181,18 @@ let (data, response) = try await client.stopwords().retrieve() let (data, response) = try await client.stopword("stopword_set1").retrieve() ``` -### Delete a preset +### Delete a stopwords set ```swift let (data, response) = try await client.stopword("stopword_set1").delete() ``` +### Retrieve debug information + +```swift +let (data, response) = try await client.operations().getDebug() +``` + ## Contributing Issues and pull requests are welcome on GitHub at [Typesense Swift](https://github.com/typesense/typesense-swift). Do note that the Models used in the Swift client are generated by [Swagger-Codegen](https://github.com/swagger-api/swagger-codegen) and are automated to be modified in order to prevent major errors. So please do use the shell script that is provided in the repo to generate the models: From a3ef4572b4ec4fb61237d342eb4145b0501c0ec8 Mon Sep 17 00:00:00 2001 From: Hayden Date: Thu, 8 Aug 2024 21:04:41 +0700 Subject: [PATCH 3/7] feat: add voice query --- .../Typesense/Models/CollectionSchema.swift | 9 ++++--- .../Models/MultiSearchParameters.swift | 6 ++++- .../Typesense/Models/SearchParameters.swift | 6 ++++- .../Models/SearchResultRequestParams.swift | 7 ++++-- .../SearchResultRequestParamsVoiceQuery.swift | 24 ++++++++++++++++++ .../VoiceQueryModelCollectionConfig.swift | 25 +++++++++++++++++++ 6 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 Sources/Typesense/Models/SearchResultRequestParamsVoiceQuery.swift create mode 100644 Sources/Typesense/Models/VoiceQueryModelCollectionConfig.swift diff --git a/Sources/Typesense/Models/CollectionSchema.swift b/Sources/Typesense/Models/CollectionSchema.swift index 4e511d5..0366a24 100644 --- a/Sources/Typesense/Models/CollectionSchema.swift +++ b/Sources/Typesense/Models/CollectionSchema.swift @@ -17,29 +17,32 @@ public struct CollectionSchema: Codable { public var fields: [Field] /** The name of an int32 / float field that determines the order in which the search results are ranked when a sort_by clause is not provided during searching. This field must indicate some kind of popularity. */ public var defaultSortingField: String? - /** List of symbols or special characters to be used for splitting the text into individual words in addition to space and new-line characters. */ + /** List of symbols or special characters to be used for splitting the text into individual words in addition to space and new-line characters. */ public var tokenSeparators: [String]? /** Enables experimental support at a collection level for nested object or object array fields. This field is only available if the Typesense server is version `0.24.0.rcn34` or later. */ public var enableNestedFields: Bool? /** List of symbols or special characters to be indexed. */ public var symbolsToIndex: [String]? + public var voiceQueryModel: VoiceQueryModelCollectionConfig? - public init(name: String, fields: [Field], defaultSortingField: String? = nil, tokenSeparators: [String]? = nil, enableNestedFields: Bool? = nil, symbolsToIndex: [String]? = nil) { + public init(name: String, fields: [Field], defaultSortingField: String? = nil, tokenSeparators: [String]? = nil, enableNestedFields: Bool? = nil, symbolsToIndex: [String]? = nil, voiceQueryModel: VoiceQueryModelCollectionConfig? = nil) { self.name = name self.fields = fields self.defaultSortingField = defaultSortingField self.tokenSeparators = tokenSeparators self.enableNestedFields = enableNestedFields self.symbolsToIndex = symbolsToIndex + self.voiceQueryModel = voiceQueryModel } - public enum CodingKeys: String, CodingKey { + public enum CodingKeys: String, CodingKey { case name case fields case defaultSortingField = "default_sorting_field" case tokenSeparators = "token_separators" case enableNestedFields = "enable_nested_fields" case symbolsToIndex = "symbols_to_index" + case voiceQueryModel = "voice_query_model" } } diff --git a/Sources/Typesense/Models/MultiSearchParameters.swift b/Sources/Typesense/Models/MultiSearchParameters.swift index 77ade31..d55d2fe 100644 --- a/Sources/Typesense/Models/MultiSearchParameters.swift +++ b/Sources/Typesense/Models/MultiSearchParameters.swift @@ -116,8 +116,10 @@ public struct MultiSearchParameters: Codable { public var stopwords: String? /** Comma separated string of nested facet fields whose parent object should be returned in facet response. */ public var facetReturnParent: String? + /** The base64 encoded audio file in 16 khz 16-bit WAV format. */ + public var voiceQuery: String? - public init(q: String? = nil, queryBy: String? = nil, queryByWeights: String? = nil, textMatchType: String? = nil, _prefix: String? = nil, _infix: String? = nil, maxExtraPrefix: Int? = nil, maxExtraSuffix: Int? = nil, filterBy: String? = nil, sortBy: String? = nil, facetBy: String? = nil, maxFacetValues: Int? = nil, facetQuery: String? = nil, numTypos: String? = nil, page: Int? = nil, perPage: Int? = nil, limit: Int? = nil, offset: Int? = nil, groupBy: String? = nil, groupLimit: Int? = nil, includeFields: String? = nil, excludeFields: String? = nil, highlightFullFields: String? = nil, highlightAffixNumTokens: Int? = nil, highlightStartTag: String? = nil, highlightEndTag: String? = nil, snippetThreshold: Int? = nil, dropTokensThreshold: Int? = nil, typoTokensThreshold: Int? = nil, pinnedHits: String? = nil, hiddenHits: String? = nil, overrideTags: String? = nil, highlightFields: String? = nil, preSegmentedQuery: Bool? = nil, preset: String? = nil, enableOverrides: Bool? = nil, prioritizeExactMatch: Bool? = nil, prioritizeTokenPosition: Bool? = nil, prioritizeNumMatchingFields: Bool? = nil, enableTyposForNumericalTokens: Bool? = nil, exhaustiveSearch: Bool? = nil, searchCutoffMs: Int? = nil, useCache: Bool? = nil, cacheTtl: Int? = nil, minLen1typo: Int? = nil, minLen2typo: Int? = nil, vectorQuery: String? = nil, remoteEmbeddingTimeoutMs: Int? = nil, remoteEmbeddingNumTries: Int? = nil, facetStrategy: String? = nil, stopwords: String? = nil, facetReturnParent: String? = nil) { + public init(q: String? = nil, queryBy: String? = nil, queryByWeights: String? = nil, textMatchType: String? = nil, _prefix: String? = nil, _infix: String? = nil, maxExtraPrefix: Int? = nil, maxExtraSuffix: Int? = nil, filterBy: String? = nil, sortBy: String? = nil, facetBy: String? = nil, maxFacetValues: Int? = nil, facetQuery: String? = nil, numTypos: String? = nil, page: Int? = nil, perPage: Int? = nil, limit: Int? = nil, offset: Int? = nil, groupBy: String? = nil, groupLimit: Int? = nil, includeFields: String? = nil, excludeFields: String? = nil, highlightFullFields: String? = nil, highlightAffixNumTokens: Int? = nil, highlightStartTag: String? = nil, highlightEndTag: String? = nil, snippetThreshold: Int? = nil, dropTokensThreshold: Int? = nil, typoTokensThreshold: Int? = nil, pinnedHits: String? = nil, hiddenHits: String? = nil, overrideTags: String? = nil, highlightFields: String? = nil, preSegmentedQuery: Bool? = nil, preset: String? = nil, enableOverrides: Bool? = nil, prioritizeExactMatch: Bool? = nil, prioritizeTokenPosition: Bool? = nil, prioritizeNumMatchingFields: Bool? = nil, enableTyposForNumericalTokens: Bool? = nil, exhaustiveSearch: Bool? = nil, searchCutoffMs: Int? = nil, useCache: Bool? = nil, cacheTtl: Int? = nil, minLen1typo: Int? = nil, minLen2typo: Int? = nil, vectorQuery: String? = nil, remoteEmbeddingTimeoutMs: Int? = nil, remoteEmbeddingNumTries: Int? = nil, facetStrategy: String? = nil, stopwords: String? = nil, facetReturnParent: String? = nil, voiceQuery: String? = nil) { self.q = q self.queryBy = queryBy self.queryByWeights = queryByWeights @@ -170,6 +172,7 @@ public struct MultiSearchParameters: Codable { self.facetStrategy = facetStrategy self.stopwords = stopwords self.facetReturnParent = facetReturnParent + self.voiceQuery = voiceQuery } public enum CodingKeys: String, CodingKey { @@ -225,6 +228,7 @@ public struct MultiSearchParameters: Codable { case facetStrategy = "facet_strategy" case stopwords case facetReturnParent = "facet_return_parent" + case voiceQuery = "voice_query" } } diff --git a/Sources/Typesense/Models/SearchParameters.swift b/Sources/Typesense/Models/SearchParameters.swift index 6abbf49..f79f1f2 100644 --- a/Sources/Typesense/Models/SearchParameters.swift +++ b/Sources/Typesense/Models/SearchParameters.swift @@ -121,8 +121,10 @@ public struct SearchParameters: Codable { public var stopwords: String? /** Comma separated string of nested facet fields whose parent object should be returned in facet response. */ public var facetReturnParent: String? + /** The base64 encoded audio file in 16 khz 16-bit WAV format. */ + public var voiceQuery: String? - public init(q: String? = nil, queryBy: String? = nil, queryByWeights: String? = nil, textMatchType: String? = nil, _prefix: String? = nil, _infix: String? = nil, maxExtraPrefix: Int? = nil, maxExtraSuffix: Int? = nil, filterBy: String? = nil, sortBy: String? = nil, facetBy: String? = nil, maxFacetValues: Int? = nil, facetQuery: String? = nil, numTypos: String? = nil, page: Int? = nil, perPage: Int? = nil, limit: Int? = nil, offset: Int? = nil, groupBy: String? = nil, groupLimit: Int? = nil, includeFields: String? = nil, excludeFields: String? = nil, highlightFullFields: String? = nil, highlightAffixNumTokens: Int? = nil, highlightStartTag: String? = nil, highlightEndTag: String? = nil, enableHighlightV1: Bool? = nil, snippetThreshold: Int? = nil, dropTokensThreshold: Int? = nil, typoTokensThreshold: Int? = nil, pinnedHits: String? = nil, hiddenHits: String? = nil, overrideTags: String? = nil, highlightFields: String? = nil, splitJoinTokens: String? = nil, preSegmentedQuery: Bool? = nil, preset: String? = nil, enableOverrides: Bool? = nil, prioritizeExactMatch: Bool? = nil, maxCandidates: Int? = nil, prioritizeTokenPosition: Bool? = nil, prioritizeNumMatchingFields: Bool? = nil, enableTyposForNumericalTokens: Bool? = nil, exhaustiveSearch: Bool? = nil, searchCutoffMs: Int? = nil, useCache: Bool? = nil, cacheTtl: Int? = nil, minLen1typo: Int? = nil, minLen2typo: Int? = nil, vectorQuery: String? = nil, remoteEmbeddingTimeoutMs: Int? = nil, remoteEmbeddingNumTries: Int? = nil, facetStrategy: String? = nil, stopwords: String? = nil, facetReturnParent: String? = nil) { + public init(q: String? = nil, queryBy: String? = nil, queryByWeights: String? = nil, textMatchType: String? = nil, _prefix: String? = nil, _infix: String? = nil, maxExtraPrefix: Int? = nil, maxExtraSuffix: Int? = nil, filterBy: String? = nil, sortBy: String? = nil, facetBy: String? = nil, maxFacetValues: Int? = nil, facetQuery: String? = nil, numTypos: String? = nil, page: Int? = nil, perPage: Int? = nil, limit: Int? = nil, offset: Int? = nil, groupBy: String? = nil, groupLimit: Int? = nil, includeFields: String? = nil, excludeFields: String? = nil, highlightFullFields: String? = nil, highlightAffixNumTokens: Int? = nil, highlightStartTag: String? = nil, highlightEndTag: String? = nil, enableHighlightV1: Bool? = nil, snippetThreshold: Int? = nil, dropTokensThreshold: Int? = nil, typoTokensThreshold: Int? = nil, pinnedHits: String? = nil, hiddenHits: String? = nil, overrideTags: String? = nil, highlightFields: String? = nil, splitJoinTokens: String? = nil, preSegmentedQuery: Bool? = nil, preset: String? = nil, enableOverrides: Bool? = nil, prioritizeExactMatch: Bool? = nil, maxCandidates: Int? = nil, prioritizeTokenPosition: Bool? = nil, prioritizeNumMatchingFields: Bool? = nil, enableTyposForNumericalTokens: Bool? = nil, exhaustiveSearch: Bool? = nil, searchCutoffMs: Int? = nil, useCache: Bool? = nil, cacheTtl: Int? = nil, minLen1typo: Int? = nil, minLen2typo: Int? = nil, vectorQuery: String? = nil, remoteEmbeddingTimeoutMs: Int? = nil, remoteEmbeddingNumTries: Int? = nil, facetStrategy: String? = nil, stopwords: String? = nil, facetReturnParent: String? = nil, voiceQuery: String? = nil) { self.q = q self.queryBy = queryBy self.queryByWeights = queryByWeights @@ -178,6 +180,7 @@ public struct SearchParameters: Codable { self.facetStrategy = facetStrategy self.stopwords = stopwords self.facetReturnParent = facetReturnParent + self.voiceQuery = voiceQuery } public enum CodingKeys: String, CodingKey { @@ -236,6 +239,7 @@ public struct SearchParameters: Codable { case facetStrategy = "facet_strategy" case stopwords case facetReturnParent = "facet_return_parent" + case voiceQuery = "voice_query" } } diff --git a/Sources/Typesense/Models/SearchResultRequestParams.swift b/Sources/Typesense/Models/SearchResultRequestParams.swift index 1dc060a..8a666a7 100644 --- a/Sources/Typesense/Models/SearchResultRequestParams.swift +++ b/Sources/Typesense/Models/SearchResultRequestParams.swift @@ -14,17 +14,20 @@ public struct SearchResultRequestParams: Codable { public var collectionName: String public var q: String public var perPage: Int + public var voiceQuery: SearchResultRequestParamsVoiceQuery? - public init(collectionName: String, q: String, perPage: Int) { + public init(collectionName: String, q: String, perPage: Int, voiceQuery: SearchResultRequestParamsVoiceQuery? = nil) { self.collectionName = collectionName self.q = q self.perPage = perPage + self.voiceQuery = voiceQuery } - public enum CodingKeys: String, CodingKey { + public enum CodingKeys: String, CodingKey { case collectionName = "collection_name" case q case perPage = "per_page" + case voiceQuery = "voice_query" } } diff --git a/Sources/Typesense/Models/SearchResultRequestParamsVoiceQuery.swift b/Sources/Typesense/Models/SearchResultRequestParamsVoiceQuery.swift new file mode 100644 index 0000000..1a6ba50 --- /dev/null +++ b/Sources/Typesense/Models/SearchResultRequestParamsVoiceQuery.swift @@ -0,0 +1,24 @@ +// +// SearchResultRequestParamsVoiceQuery.swift +// +// Generated by swagger-codegen +// https://github.com/swagger-api/swagger-codegen +// + +import Foundation + + + +public struct SearchResultRequestParamsVoiceQuery: Codable { + + public var transcribedQuery: String? + + public init(transcribedQuery: String? = nil) { + self.transcribedQuery = transcribedQuery + } + + public enum CodingKeys: String, CodingKey { + case transcribedQuery = "transcribed_query" + } + +} diff --git a/Sources/Typesense/Models/VoiceQueryModelCollectionConfig.swift b/Sources/Typesense/Models/VoiceQueryModelCollectionConfig.swift new file mode 100644 index 0000000..fa2f97e --- /dev/null +++ b/Sources/Typesense/Models/VoiceQueryModelCollectionConfig.swift @@ -0,0 +1,25 @@ +// +// VoiceQueryModelCollectionConfig.swift +// +// Generated by swagger-codegen +// https://github.com/swagger-api/swagger-codegen +// + +import Foundation + + +/** Configuration for the voice query model */ + +public struct VoiceQueryModelCollectionConfig: Codable { + + public var modelName: String? + + public init(modelName: String? = nil) { + self.modelName = modelName + } + + public enum CodingKeys: String, CodingKey { + case modelName = "model_name" + } + +} From 2b9ca3e5ff6c2f10c3584daa044c7efc343488f4 Mon Sep 17 00:00:00 2001 From: Hayden Date: Thu, 8 Aug 2024 21:05:20 +0700 Subject: [PATCH 4/7] update: collection `Field` schema --- Sources/Typesense/Models/Field.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Sources/Typesense/Models/Field.swift b/Sources/Typesense/Models/Field.swift index 8a2c717..f0d7ac1 100644 --- a/Sources/Typesense/Models/Field.swift +++ b/Sources/Typesense/Models/Field.swift @@ -19,11 +19,14 @@ public struct Field: Codable { public var locale: String? public var sort: Bool? public var _infix: Bool? + public var reference: String? public var numDim: Int? public var drop: Bool? + /** Whether to store the image on disk. */ + public var store: Bool? public var embed: FieldEmbed? - public init(name: String, type: String, _optional: Bool? = nil, facet: Bool? = nil, index: Bool? = nil, locale: String? = nil, sort: Bool? = nil, _infix: Bool? = nil, numDim: Int? = nil, drop: Bool? = nil, embed: FieldEmbed? = nil) { + public init(name: String, type: String, _optional: Bool? = nil, facet: Bool? = nil, index: Bool? = nil, locale: String? = nil, sort: Bool? = nil, _infix: Bool? = nil, reference: String? = nil, numDim: Int? = nil, drop: Bool? = nil, store: Bool? = nil, embed: FieldEmbed? = nil) { self.name = name self.type = type self._optional = _optional @@ -32,8 +35,10 @@ public struct Field: Codable { self.locale = locale self.sort = sort self._infix = _infix + self.reference = reference self.numDim = numDim self.drop = drop + self.store = store self.embed = embed } @@ -46,8 +51,10 @@ public struct Field: Codable { case locale case sort case _infix = "infix" + case reference case numDim = "num_dim" case drop + case store case embed } From 1a3cc2e82b84a5eeee6b4eec6061035aa69bcde0 Mon Sep 17 00:00:00 2001 From: Hayden Date: Sat, 17 Aug 2024 09:17:16 +0700 Subject: [PATCH 5/7] fix: do not split the prefix string parameter --- Sources/Typesense/Documents.swift | 7 +------ Sources/Typesense/MultiSearch.swift | 8 +------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/Sources/Typesense/Documents.swift b/Sources/Typesense/Documents.swift index 0df9106..b8a30b6 100644 --- a/Sources/Typesense/Documents.swift +++ b/Sources/Typesense/Documents.swift @@ -75,12 +75,7 @@ public struct Documents { } if let _prefix = searchParameters._prefix { - var fullString = "" - for item in _prefix { - fullString.append(String(item)) - fullString.append(",") - } - searchQueryParams.append(URLQueryItem(name: "prefix", value: String(fullString.dropLast()))) + searchQueryParams.append(URLQueryItem(name: "prefix", value: _prefix)) } if let _infix = searchParameters._infix { diff --git a/Sources/Typesense/MultiSearch.swift b/Sources/Typesense/MultiSearch.swift index 8f59743..321c027 100644 --- a/Sources/Typesense/MultiSearch.swift +++ b/Sources/Typesense/MultiSearch.swift @@ -31,13 +31,7 @@ public struct MultiSearch { } if let _prefix = commonParameters._prefix { - var fullString = "" - for item in _prefix { - fullString.append(String(item)) - fullString.append(",") - } - - searchQueryParams.append(URLQueryItem(name: "prefix", value: String(fullString.dropLast()))) + searchQueryParams.append(URLQueryItem(name: "prefix", value: _prefix)) } if let _infix = commonParameters._infix { From f6c5ac3e7f1ccc4626f4f8617bb7bc2a1876ffde Mon Sep 17 00:00:00 2001 From: Hayden Date: Sat, 17 Aug 2024 17:07:17 +0700 Subject: [PATCH 6/7] feat: support node URL --- Sources/Typesense/ApiCall.swift | 5 +- Sources/Typesense/Node.swift | 24 +++++--- Tests/TypesenseTests/ApiCallTests.swift | 77 +++++++++++++++++-------- 3 files changed, 72 insertions(+), 34 deletions(-) diff --git a/Sources/Typesense/ApiCall.swift b/Sources/Typesense/ApiCall.swift index 1793833..fded9e1 100644 --- a/Sources/Typesense/ApiCall.swift +++ b/Sources/Typesense/ApiCall.swift @@ -148,7 +148,10 @@ struct ApiCall { //Get URL for a node combined with it's end point func uriFor(endpoint: String, node: Node) -> String { - return "\(node.nodeProtocol)://\(node.host):\(node.port)/\(endpoint)" + if let url = node.url{ + return "\(url)/\(endpoint)" + } + return "\(node.nodeProtocol!)://\(node.host!):\(node.port!)/\(endpoint)" } //Get the next healthy node from the given nodes diff --git a/Sources/Typesense/Node.swift b/Sources/Typesense/Node.swift index 317f907..55e0a4f 100644 --- a/Sources/Typesense/Node.swift +++ b/Sources/Typesense/Node.swift @@ -1,20 +1,28 @@ public struct Node: CustomStringConvertible { - var host: String - var port: String - var nodeProtocol: String + var host: String? + var port: String? + var nodeProtocol: String? + var url: String? var isHealthy: Bool = false var lastAccessTimeStamp: Int64 = 0 - - public init(host: String, port: String, nodeProtocol: String) { + + public init(host: String? = nil, port: String? = nil, nodeProtocol: String? = nil, url: String? = nil) { + if url == nil && (host == nil || port == nil || nodeProtocol == nil) { + fatalError("Node `url` or `nodeProtocol` and `host` and `port` must be set!") + } self.host = host self.port = port self.nodeProtocol = nodeProtocol + self.url = url } - + public var description: String { - return "Node: \(nodeProtocol)://\(host):\(port)" + if let url = self.url { + return "Node: \(url)" + } + return "Node: \(nodeProtocol!)://\(host!):\(port!)" } - + public var healthStatus: String { return isHealthy ? "Healthy" : "Unhealthy" } diff --git a/Tests/TypesenseTests/ApiCallTests.swift b/Tests/TypesenseTests/ApiCallTests.swift index ed0bf2e..da3a037 100644 --- a/Tests/TypesenseTests/ApiCallTests.swift +++ b/Tests/TypesenseTests/ApiCallTests.swift @@ -3,10 +3,10 @@ import XCTest @testable import Typesense final class A_ApiCallTests: XCTestCase { - + func testDefaultConfiguration() { let apiCall = ApiCall(config: Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz")) - + XCTAssertNotNil(apiCall) XCTAssertNotNil(apiCall.nodes) XCTAssertEqual(apiCall.apiKey, "xyz") @@ -17,7 +17,7 @@ final class A_ApiCallTests: XCTestCase { XCTAssertEqual(apiCall.retryIntervalSeconds, 0.1) XCTAssertEqual(apiCall.sendApiKeyAsQueryParam, false) } - + func testCustomConfiguration() { let apiCall = ApiCall(config: Configuration( nodes: @@ -32,7 +32,7 @@ final class A_ApiCallTests: XCTestCase { retryIntervalSeconds: 0.2, sendApiKeyAsQueryParam: true) ) - + XCTAssertNotNil(apiCall) XCTAssertNotNil(apiCall.nodes) XCTAssertNotNil(apiCall.nearestNode) @@ -45,39 +45,49 @@ final class A_ApiCallTests: XCTestCase { XCTAssertEqual(apiCall.retryIntervalSeconds, 0.2) XCTAssertEqual(apiCall.sendApiKeyAsQueryParam, true) } - - - + + + func testNodes() { let apiCall = ApiCall(config: Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz")) - + XCTAssertNotNil(apiCall) XCTAssertNotNil(apiCall.nodes) XCTAssertNotEqual(0, apiCall.nodes.count) XCTAssertEqual(apiCall.nodes[0].host, "localhost") XCTAssertEqual(apiCall.nodes[0].port, "8108") XCTAssertEqual(apiCall.nodes[0].nodeProtocol, "http") - + + } + + func testNodesWithURL() { + let apiCall = ApiCall(config: Configuration(nodes: [Node(url: "http://localhost:8108")], apiKey: "xyz")) + + XCTAssertNotNil(apiCall) + XCTAssertNotNil(apiCall.nodes) + XCTAssertNotEqual(0, apiCall.nodes.count) + XCTAssertEqual(apiCall.nodes[0].url, "http://localhost:8108") + } - + func testNearestNode() { let apiCall = ApiCall(config: Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", nearestNode: Node(host: "nearest.host", port: "8109", nodeProtocol: "https"))) - + XCTAssertNotNil(apiCall) XCTAssertNotNil(apiCall.nodes) XCTAssertNotNil(apiCall.nearestNode) XCTAssertEqual(apiCall.nearestNode?.host, "nearest.host") XCTAssertEqual(apiCall.nearestNode?.port, "8109") XCTAssertEqual(apiCall.nearestNode?.nodeProtocol, "https") - + } - + func testRequest() { let apiCall = ApiCall(config: Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz")) - + do { let request = try apiCall.prepareRequest(requestType: RequestType.get, endpoint: "health", body: nil, selectedNode: apiCall.nodes[0]) - + XCTAssertEqual(request.httpMethod, "GET") XCTAssertEqual(request.url?.absoluteString, "http://localhost:8108/health") XCTAssertNil(request.httpBody) @@ -86,31 +96,48 @@ final class A_ApiCallTests: XCTestCase { } catch (let error) { print(error.localizedDescription) } - + } - + + func testRequestWithNodeURL() { + let apiCall = ApiCall(config: Configuration(nodes: [Node(url: "http://localhost:8108")], apiKey: "xyz")) + + do { + let request = try apiCall.prepareRequest(requestType: RequestType.get, endpoint: "health", body: nil, selectedNode: apiCall.nodes[0]) + + XCTAssertEqual(request.httpMethod, "GET") + XCTAssertEqual(request.url?.absoluteString, "http://localhost:8108/health") + XCTAssertNil(request.httpBody) + XCTAssertTrue(request.allHTTPHeaderFields?.isEmpty != nil) + XCTAssertEqual(request.allHTTPHeaderFields?[APIKEYHEADERNAME], apiCall.apiKey) + } catch (let error) { + print(error.localizedDescription) + } + + } + func testSetNodeHealthCheck() { let apiCall = ApiCall(config: Configuration(nodes: [ Node(host: "localhost", port: "8108", nodeProtocol: "http"), Node(host: "localhost", port: "8109", nodeProtocol: "http"), ], apiKey: "xyz")) - + let node1 = apiCall.setNodeHealthCheck(node: apiCall.nodes[0], isHealthy: HEALTHY) let node2 = apiCall.setNodeHealthCheck(node: apiCall.nodes[1], isHealthy: UNHEALTHY) - + XCTAssertEqual(node1.isHealthy, HEALTHY) XCTAssertEqual(node2.isHealthy, UNHEALTHY) XCTAssertNotNil(node1.lastAccessTimeStamp) XCTAssertNotNil(node2.lastAccessTimeStamp) - + } - + func testGetNextNode() { let apiCall = ApiCall(config: Configuration(nodes: [ Node(host: "localhost", port: "8108", nodeProtocol: "http"), Node(host: "localhost", port: "8109", nodeProtocol: "http"), ], apiKey: "xyz")) - + var node = apiCall.getNextNode() XCTAssertEqual(node.isHealthy, HEALTHY) XCTAssertEqual(node.port, "8108") @@ -120,11 +147,11 @@ final class A_ApiCallTests: XCTestCase { XCTAssertEqual(node.isHealthy, HEALTHY) XCTAssertEqual(node.port, "8109") } - + //Integration Test - Requires Typesense Server func testServerHealth() async { let apiCall = ApiCall(config: Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true))) - + do { let (data, response) = try await apiCall.get(endPoint: "health") XCTAssertNotNil(data) @@ -138,5 +165,5 @@ final class A_ApiCallTests: XCTestCase { print(error.localizedDescription) } } - + } From 07dfb9f2c4c206c0e456acb6af82dbebf2d91de7 Mon Sep 17 00:00:00 2001 From: Hayden Date: Sat, 17 Aug 2024 17:11:40 +0700 Subject: [PATCH 7/7] update README: node URL --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 29f5732..3fe67af 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ import Typesense Declare the Typesense nodes that are available as `Node` members: ```swift -let node1 = Node(host: "localhost", port: "8108", nodeProtocol: "http") -let node2 = Node(host: "super-awesome.search", port: "8080", nodeProtocol: "https") //and so on +let node1 = Node(url: "http://localhost:8108") // or +let node2 = Node(host: "xxx-1.a1.typesense.net", port: "443", nodeProtocol: "https") ``` Create a configuration and hence a client with the Nodes mentioned: