From 475de3ea0b0a46139d57dfcb97ed154a5fd6c618 Mon Sep 17 00:00:00 2001 From: programVeins Date: Sun, 16 Jan 2022 16:15:57 +0530 Subject: [PATCH 1/6] Update CI test script --- .circleci/config.yml | 25 +++++++++++++++---------- .gitignore | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6f98693..f96688c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,13 +1,18 @@ -# Use the latest 2.1 version of CircleCI pipeline process engine. See: https://circleci.com/docs/2.0/configuration-reference version: 2.1 -# Use a package of configuration called an orb. -orbs: - # Declare a dependency on the welcome-orb - welcome: circleci/welcome-orb@0.4.1 -# Orchestrate or schedule a set of jobs + +jobs: + test: + macos: + xcode: 13.2.1 # Using 13.2.1 to support backwards compatibility of Modern Concurrency + steps: + - checkout + - run: + # Build and Test the Typesense Package + name: Run Tests + command: swift test + workflows: - # Name the workflow "welcome" - welcome: - # Run the welcome/run job in its own container + version: 2 + test_build: jobs: - - welcome/run + - test diff --git a/.gitignore b/.gitignore index bb460e7..e284f64 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ /*.xcodeproj xcuserdata/ DerivedData/ -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.swiftpm From 642cabb5af7a7fb053ce5d69b74f61d183f72da8 Mon Sep 17 00:00:00 2001 From: programVeins Date: Sun, 16 Jan 2022 16:22:24 +0530 Subject: [PATCH 2/6] Update Package.swift to macOS 10.15 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index fdf39bd..18ba827 100644 --- a/Package.swift +++ b/Package.swift @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "Typesense", platforms: [ - .iOS(.v13), .macOS(.v12) + .iOS(.v13), .macOS(.v10_15) ], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. From 3204a673d34f3e46d67f49c962148b7fe190f540 Mon Sep 17 00:00:00 2001 From: programVeins Date: Fri, 11 Feb 2022 23:23:45 +0530 Subject: [PATCH 3/6] Add Multisearch --- Sources/Typesense/Client.swift | 4 + Sources/Typesense/MultiSearch.swift | 177 ++++++++++++++++++++ Tests/TypesenseTests/MultiSearchTests.swift | 90 ++++++++++ 3 files changed, 271 insertions(+) create mode 100644 Sources/Typesense/MultiSearch.swift create mode 100644 Tests/TypesenseTests/MultiSearchTests.swift diff --git a/Sources/Typesense/Client.swift b/Sources/Typesense/Client.swift index c09f056..f19e345 100644 --- a/Sources/Typesense/Client.swift +++ b/Sources/Typesense/Client.swift @@ -27,4 +27,8 @@ public struct Client { public func operations() -> Operations { return Operations(config: self.configuration) } + + public func multiSearch() -> MultiSearch { + return MultiSearch(config: self.configuration) + } } diff --git a/Sources/Typesense/MultiSearch.swift b/Sources/Typesense/MultiSearch.swift new file mode 100644 index 0000000..18558f2 --- /dev/null +++ b/Sources/Typesense/MultiSearch.swift @@ -0,0 +1,177 @@ +import Foundation + +public struct MultiSearch { + var apiCall: ApiCall + let RESOURCEPATH = "multi_search" + + public init(config: Configuration) { + apiCall = ApiCall(config: config) + } + + public func perform(searchRequests: [MultiSearchCollectionParameters], commonParameters: MultiSearchParameters, for: T.Type) async throws -> (MultiSearchResult?, URLResponse?) { + var searchQueryParams: [URLQueryItem] = [] + + if let query = commonParameters.q { + searchQueryParams.append(URLQueryItem(name: "q", value: query)) + } + + if let queryBy = commonParameters.queryBy { + searchQueryParams.append(URLQueryItem(name: "query_by", value: queryBy)) + } + + if let queryByWeights = commonParameters.queryByWeights { + searchQueryParams.append(URLQueryItem(name: "query_by_weights", value: queryByWeights)) + } + + if let maxHits = commonParameters.maxHits { + searchQueryParams.append(URLQueryItem(name: "max_hits", value: maxHits)) + } + + 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()))) + } + + if let filterBy = commonParameters.filterBy { + searchQueryParams.append(URLQueryItem(name: "filter_by", value: filterBy)) + } + + if let sortBy = commonParameters.sortBy { + searchQueryParams.append(URLQueryItem(name: "sort_by", value: sortBy)) + } + + if let facetBy = commonParameters.facetBy { + searchQueryParams.append(URLQueryItem(name: "facet_by", value: facetBy)) + } + + if let maxFacetValues = commonParameters.maxFacetValues { + searchQueryParams.append(URLQueryItem(name: "max_facet_values", value: String(maxFacetValues))) + } + + if let facetQuery = commonParameters.facetQuery { + searchQueryParams.append(URLQueryItem(name: "facet_query", value: facetQuery)) + } + + if let numTypos = commonParameters.numTypos { + searchQueryParams.append(URLQueryItem(name: "num_typos", value: String(numTypos))) + } + + if let page = commonParameters.page { + searchQueryParams.append(URLQueryItem(name: "page", value: String(page))) + } + + if let perPage = commonParameters.perPage { + searchQueryParams.append(URLQueryItem(name: "per_page", value: String(perPage))) + } + + if let groupBy = commonParameters.groupBy { + searchQueryParams.append(URLQueryItem(name: "group_by", value: groupBy)) + } + + if let groupLimit = commonParameters.groupLimit { + searchQueryParams.append(URLQueryItem(name: "group_limit", value: String(groupLimit))) + } + + if let includeFields = commonParameters.includeFields { + searchQueryParams.append(URLQueryItem(name: "include_fields", value: includeFields)) + } + + if let excludeFields = commonParameters.excludeFields { + searchQueryParams.append(URLQueryItem(name: "exclude_fields", value: excludeFields)) + } + + if let highlightFullFields = commonParameters.highlightFullFields { + searchQueryParams.append(URLQueryItem(name: "highlight_full_fields", value: highlightFullFields)) + } + + if let highlightAffixNumTokens = commonParameters.highlightAffixNumTokens { + searchQueryParams.append(URLQueryItem(name: "highlight_affix_num_tokens", value: String(highlightAffixNumTokens))) + } + + if let highlightStartTag = commonParameters.highlightStartTag { + searchQueryParams.append(URLQueryItem(name: "highlight_start_tag", value: highlightStartTag)) + } + + if let highlightEndTag = commonParameters.highlightEndTag { + searchQueryParams.append(URLQueryItem(name: "highlight_end_tag", value: highlightEndTag)) + } + + if let snippetThreshold = commonParameters.snippetThreshold { + searchQueryParams.append(URLQueryItem(name: "snippet_threshold", value: String(snippetThreshold))) + } + + if let dropTokensThreshold = commonParameters.dropTokensThreshold { + searchQueryParams.append(URLQueryItem(name: "drop_tokens_threshold", value: String(dropTokensThreshold))) + } + + if let typoTokensThreshold = commonParameters.typoTokensThreshold { + searchQueryParams.append(URLQueryItem(name: "typo_tokens_threshold", value: String(typoTokensThreshold))) + } + + if let pinnedHits = commonParameters.pinnedHits { + searchQueryParams.append(URLQueryItem(name: "pinned_hits", value: pinnedHits)) + } + + if let hiddenHits = commonParameters.hiddenHits { + searchQueryParams.append(URLQueryItem(name: "hidden_hits", value: hiddenHits)) + } + + if let highlightFields = commonParameters.highlightFields { + searchQueryParams.append(URLQueryItem(name: "highlight_fields", value: highlightFields)) + } + + if let preSegmentedQuery = commonParameters.preSegmentedQuery { + searchQueryParams.append(URLQueryItem(name: "pre_segmented_query", value: String(preSegmentedQuery))) + } + + if let enableOverrides = commonParameters.enableOverrides { + searchQueryParams.append(URLQueryItem(name: "enable_overrides", value: String(enableOverrides))) + } + + if let prioritizeExactMatch = commonParameters.prioritizeExactMatch { + searchQueryParams.append(URLQueryItem(name: "prioritize_exact_match", value: String(prioritizeExactMatch))) + } + + if let exhaustiveSearch = commonParameters.exhaustiveSearch { + searchQueryParams.append(URLQueryItem(name: "exhaustive_search", value: String(exhaustiveSearch))) + } + + if let searchCutoffMs = commonParameters.searchCutoffMs { + searchQueryParams.append(URLQueryItem(name: "search_cutoff_ms", value: String(searchCutoffMs))) + } + + if let useCache = commonParameters.useCache { + searchQueryParams.append(URLQueryItem(name: "use_cache", value: String(useCache))) + } + + if let cacheTtl = commonParameters.cacheTtl { + searchQueryParams.append(URLQueryItem(name: "cache_ttl", value: String(cacheTtl))) + } + + if let minLen1typo = commonParameters.minLen1typo { + searchQueryParams.append(URLQueryItem(name: "min_len1type", value: String(minLen1typo))) + } + + if let minLen2typo = commonParameters.minLen2typo { + searchQueryParams.append(URLQueryItem(name: "min_len2type", value: String(minLen2typo))) + } + + let searches = MultiSearchSearchesParameter(searches: searchRequests) + + let searchesData = try encoder.encode(searches) + + let (data, response) = try await apiCall.post(endPoint: "\(RESOURCEPATH)", body: searchesData, queryParameters: searchQueryParams) + + if let validData = data { + let searchRes = try decoder.decode(MultiSearchResult.self, from: validData) + return (searchRes, response) + } + + return (nil, response) + } +} diff --git a/Tests/TypesenseTests/MultiSearchTests.swift b/Tests/TypesenseTests/MultiSearchTests.swift new file mode 100644 index 0000000..e946970 --- /dev/null +++ b/Tests/TypesenseTests/MultiSearchTests.swift @@ -0,0 +1,90 @@ +import XCTest +@testable import Typesense + +final class MultiSearchTests: XCTestCase { + + struct Product: Codable, Equatable { + var name: String? + var price: Int? + var brand: String? + var desc: String? + + static func == (lhs: Product, rhs: Product) -> Bool { + return + lhs.name == rhs.name && + lhs.price == rhs.price && + lhs.brand == rhs.brand && + lhs.desc == rhs.desc + } + } + + struct Brand: Codable { + var name: String + } + + + func testMultiSearch() async { + let config = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true)) + + let client = Client(config: config) + + let productSchema = CollectionSchema(name: "products", fields: [ + Field(name: "name", type: "string"), + Field(name: "price", type: "int32"), + Field(name: "brand", type: "string"), + Field(name: "desc", type: "string"), + ]) + + let brandSchema = CollectionSchema(name: "brands", fields: [ + Field(name: "name", type: "string"), + ]) + + let searchRequests = [ + MultiSearchCollectionParameters(q: "shoe", filterBy: "price:=[50..120]", collection: "products"), + MultiSearchCollectionParameters(q: "Nike", collection: "brands"), + ] + + let brand1 = Brand(name: "Nike") + let product1 = Product(name: "Jordan", price: 70, brand: "Nike", desc: "High quality shoe") + + let commonParams = MultiSearchParameters(queryBy: "name") + + do { + let (_, _) = try await client.collections.create(schema: productSchema) //Creating test collection - Products + let (_,_) = try await client.collections.create(schema: brandSchema) + + let (_,_) = try await client.collection(name: "products").documents().create(document: encoder.encode(product1)) + + let (_,_) = try await client.collection(name: "brands").documents().create(document: encoder.encode(brand1)) + + let (data, _) = try await client.multiSearch().perform(searchRequests: searchRequests, commonParameters: commonParams, for: Product.self) + + let (_,_) = try await client.collection(name: "products").delete() //Deleting test collection + let (_,_) = try await client.collection(name: "brands").delete() //Deleting test collection + + XCTAssertNotNil(data) + guard let validResp = data else { + throw DataError.dataNotFound + } + + XCTAssertNotNil(validResp.results) + XCTAssertNotEqual(validResp.results.count, 0) + XCTAssertNotNil(validResp.results[0].hits) + XCTAssertNotNil(validResp.results[1].hits) + XCTAssertEqual(validResp.results[1].hits?.count, 1) + + print(validResp.results[1].hits as Any) + } 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) + } + + } + + + +} From 7791b352572edb319555f85735bd47ae3646e849 Mon Sep 17 00:00:00 2001 From: programVeins Date: Sat, 12 Feb 2022 11:14:44 +0530 Subject: [PATCH 4/6] Experiment: Try Docker CI --- .circleci/config.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index f96688c..520a907 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,6 +6,15 @@ jobs: xcode: 13.2.1 # Using 13.2.1 to support backwards compatibility of Modern Concurrency steps: - checkout + - run: + name: Install docker + command: | + curl -fsSL https://get.docker.com -o get-docker.sh + sh get-docker.sh + - run: + name: Docker hello-world + command: | + docker run hello-world - run: # Build and Test the Typesense Package name: Run Tests From 10a1759ff177860b6bc5664642a11de1cf3cb950 Mon Sep 17 00:00:00 2001 From: programVeins Date: Sat, 12 Feb 2022 11:26:09 +0530 Subject: [PATCH 5/6] Experiment: Remove Docker & Add Curl --- .circleci/config.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 520a907..0008293 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,14 +7,15 @@ jobs: steps: - checkout - run: - name: Install docker + name: Install Typesense command: | - curl -fsSL https://get.docker.com -o get-docker.sh - sh get-docker.sh + curl --output ts.tar.gz https://dl.typesense.org/releases/0.22.2/typesense-server-0.22.2-darwin-amd64.tar.gz + tar -xzf ts.tar.gz - run: - name: Docker hello-world + name: Run Typesense + background: true command: | - docker run hello-world + ./typesense-server --api-key=xyz --data-dir=/tmp - run: # Build and Test the Typesense Package name: Run Tests From 5bc7dbb1d21a34caa5256f682c81122d6ab42174 Mon Sep 17 00:00:00 2001 From: Sabesh Bharathi Date: Sat, 12 Feb 2022 19:51:08 +0530 Subject: [PATCH 6/6] Remove MultiSearch from TODO --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 67641ea..e9aa887 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,5 @@ The generated Models (inside the Models directory) are to be used inside the Mod ## TODO: Features - Curation API -- Multisearch - Dealing with Dirty Data - Scoped Search Key