From dbb375a4a6a6ed1472b9f8fa0b86219404a34f97 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Fri, 18 Aug 2023 08:01:24 +0200 Subject: [PATCH] Add pageSize parameter to search API --- .../API/API+SearchController.swift | 17 +++++++------ .../App/Controllers/KeywordController.swift | 23 +++++++++++++----- .../App/Controllers/SearchController.swift | 3 +-- Sources/App/Core/Constants.swift | 2 -- Sources/App/Core/Search.swift | 2 +- Tests/AppTests/QueryPerformanceTests.swift | 24 +++++++------------ Tests/AppTests/SearchTests.swift | 21 ++++++---------- 7 files changed, 44 insertions(+), 48 deletions(-) diff --git a/Sources/App/Controllers/API/API+SearchController.swift b/Sources/App/Controllers/API/API+SearchController.swift index 815e908c5..13b3be6d5 100644 --- a/Sources/App/Controllers/API/API+SearchController.swift +++ b/Sources/App/Controllers/API/API+SearchController.swift @@ -21,24 +21,29 @@ extension API { struct Query: Codable { var query: String = Self.defaultQuery var page: Int = Self.defaultPage + var pageSize: Int = Self.defaultPageSize static let defaultQuery = "" static let defaultPage = 1 - + static let defaultPageSize = 20 + enum CodingKeys: CodingKey { case query case page + case pageSize } - init(query: String = Self.defaultQuery, page: Int = Self.defaultPage) { + init(query: String = Self.defaultQuery, page: Int = Self.defaultPage, pageSize: Int = Self.defaultPageSize) { self.query = query self.page = page + self.pageSize = pageSize } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.query = try container.decodeIfPresent(String.self, forKey: CodingKeys.query) ?? Self.defaultQuery self.page = try container.decodeIfPresent(Int.self, forKey: CodingKeys.page) ?? Self.defaultPage + self.pageSize = try container.decodeIfPresent(Int.self, forKey: CodingKeys.pageSize) ?? Self.defaultPageSize } } @@ -46,8 +51,7 @@ extension API { let query = try req.query.decode(Query.self) AppMetrics.apiSearchGetTotal?.inc() return try await search(database: req.db, - query: query, - pageSize: Constants.resultsPageSize) + query: query) } } } @@ -55,8 +59,7 @@ extension API { extension API { static func search(database: Database, - query: SearchController.Query, - pageSize: Int) async throws -> Search.Response { + query: SearchController.Query) async throws -> Search.Response { let terms = query.query.components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty } guard !terms.isEmpty else { return .init(hasMoreResults: false, @@ -67,6 +70,6 @@ extension API { return try await Search.fetch(database, terms, page: query.page, - pageSize: pageSize).get() + pageSize: query.pageSize).get() } } diff --git a/Sources/App/Controllers/KeywordController.swift b/Sources/App/Controllers/KeywordController.swift index 3a0b4d162..f789eaf7c 100644 --- a/Sources/App/Controllers/KeywordController.swift +++ b/Sources/App/Controllers/KeywordController.swift @@ -29,19 +29,30 @@ enum KeywordController { } struct Query: Codable { - var page: Int? + var page: Int + var pageSize: Int static let defaultPage = 1 + static let defaultPageSize = 20 + + enum CodingKeys: CodingKey { + case page + case pageSize + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.page = try container.decodeIfPresent(Int.self, forKey: CodingKeys.page) ?? Self.defaultPage + self.pageSize = try container.decodeIfPresent(Int.self, forKey: CodingKeys.pageSize) ?? Self.defaultPageSize + } } static func show(req: Request) async throws -> HTML { guard let keyword = req.parameters.get("keyword") else { throw Abort(.notFound) } - let pageIndex = try req.query.decode(Query.self).page ?? Query.defaultPage - let pageSize = Constants.resultsPageSize - - let page = try await Self.query(on: req.db, keyword: keyword, page: pageIndex, pageSize: pageSize).get() + let query = try req.query.decode(Query.self) + let page = try await Self.query(on: req.db, keyword: keyword, page: query.page, pageSize: query.pageSize).get() guard !page.results.isEmpty else { throw Abort(.notFound) @@ -52,7 +63,7 @@ enum KeywordController { let model = KeywordShow.Model( keyword: keyword, packages: packageInfo, - page: pageIndex, + page: query.page, hasMoreResults: page.hasMoreResults ) diff --git a/Sources/App/Controllers/SearchController.swift b/Sources/App/Controllers/SearchController.swift index e2237b56e..1727f022c 100644 --- a/Sources/App/Controllers/SearchController.swift +++ b/Sources/App/Controllers/SearchController.swift @@ -23,8 +23,7 @@ enum SearchController { let query = try req.query.decode(API.SearchController.Query.self) let response = try await API.search(database: req.db, - query: query, - pageSize: Constants.resultsPageSize) + query: query) let matchedKeywords = response.results.compactMap { $0.keywordResult?.keyword } diff --git a/Sources/App/Core/Constants.swift b/Sources/App/Core/Constants.swift index f09f9e842..d4993cfe7 100644 --- a/Sources/App/Core/Constants.swift +++ b/Sources/App/Core/Constants.swift @@ -36,8 +36,6 @@ enum Constants { static let rssFeedMaxItemCount = 500 static let rssTTL: TimeInterval = .minutes(60) - static let resultsPageSize = 20 - // analyzer settings static let gitCheckoutMaxAge: TimeInterval = .days(30) diff --git a/Sources/App/Core/Search.swift b/Sources/App/Core/Search.swift index 6c82e102f..44541e001 100644 --- a/Sources/App/Core/Search.swift +++ b/Sources/App/Core/Search.swift @@ -305,7 +305,7 @@ enum Search { _ sanitizedTerms: [String], filters: [SearchFilterProtocol] = [], page: Int, - pageSize: Int) -> SQLSelectBuilder? { + pageSize: Int = API.SearchController.Query.defaultPageSize) -> SQLSelectBuilder? { // This function assembles results from the different search types (packages, // keywords, ...) into a single query. // diff --git a/Tests/AppTests/QueryPerformanceTests.swift b/Tests/AppTests/QueryPerformanceTests.swift index d6ff4e505..0bcdb8383 100644 --- a/Tests/AppTests/QueryPerformanceTests.swift +++ b/Tests/AppTests/QueryPerformanceTests.swift @@ -52,64 +52,56 @@ class QueryPerformanceTests: XCTestCase { } func test_04_Search_query_noFilter() async throws { - let query = try Search.query(app.db, ["a"], - page: 1, pageSize: Constants.resultsPageSize) + let query = try Search.query(app.db, ["a"], page: 1) .unwrap() try await assertQueryPerformance(query, expectedCost: 6080, variation: 200) } func test_05_Search_query_authorFilter() async throws { let filter = try AuthorSearchFilter(expression: .init(operator: .is, value: "apple")) - let query = try Search.query(app.db, ["a"], filters: [filter], - page: 1, pageSize: Constants.resultsPageSize) + let query = try Search.query(app.db, ["a"], filters: [filter], page: 1) .unwrap() try await assertQueryPerformance(query, expectedCost: 5810, variation: 200) } func test_06_Search_query_keywordFilter() async throws { let filter = try KeywordSearchFilter(expression: .init(operator: .is, value: "apple")) - let query = try Search.query(app.db, ["a"], filters: [filter], - page: 1, pageSize: Constants.resultsPageSize) + let query = try Search.query(app.db, ["a"], filters: [filter], page: 1) .unwrap() try await assertQueryPerformance(query, expectedCost: 5880, variation: 200) } func test_07_Search_query_lastActicityFilter() async throws { let filter = try LastActivitySearchFilter(expression: .init(operator: .greaterThan, value: "2000-01-01")) - let query = try Search.query(app.db, ["a"], filters: [filter], - page: 1, pageSize: Constants.resultsPageSize) + let query = try Search.query(app.db, ["a"], filters: [filter], page: 1) .unwrap() try await assertQueryPerformance(query, expectedCost: 6100, variation: 200) } func test_08_Search_query_licenseFilter() async throws { let filter = try LicenseSearchFilter(expression: .init(operator: .is, value: "mit")) - let query = try Search.query(app.db, ["a"], filters: [filter], - page: 1, pageSize: Constants.resultsPageSize) + let query = try Search.query(app.db, ["a"], filters: [filter], page: 1) .unwrap() try await assertQueryPerformance(query, expectedCost: 6000, variation: 200) } func test_09_Search_query_platformFilter() async throws { let filter = try PlatformSearchFilter(expression: .init(operator: .is, value: "macos,ios")) - let query = try Search.query(app.db, ["a"], filters: [filter], - page: 1, pageSize: Constants.resultsPageSize) + let query = try Search.query(app.db, ["a"], filters: [filter], page: 1) .unwrap() try await assertQueryPerformance(query, expectedCost: 5910, variation: 200) } func test_10_Search_query_productTypeFilter() async throws { let filter = try ProductTypeSearchFilter(expression: .init(operator: .is, value: "plugin")) - let query = try Search.query(app.db, ["a"], filters: [filter], - page: 1, pageSize: Constants.resultsPageSize) + let query = try Search.query(app.db, ["a"], filters: [filter], page: 1) .unwrap() try await assertQueryPerformance(query, expectedCost: 5810, variation: 200) } func test_11_Search_query_starsFilter() async throws { let filter = try StarsSearchFilter(expression: .init(operator: .greaterThan, value: "5")) - let query = try Search.query(app.db, ["a"], filters: [filter], - page: 1, pageSize: Constants.resultsPageSize) + let query = try Search.query(app.db, ["a"], filters: [filter], page: 1) .unwrap() try await assertQueryPerformance(query, expectedCost: 6000, variation: 300) } diff --git a/Tests/AppTests/SearchTests.swift b/Tests/AppTests/SearchTests.swift index c0da49702..a641fd11d 100644 --- a/Tests/AppTests/SearchTests.swift +++ b/Tests/AppTests/SearchTests.swift @@ -361,8 +361,7 @@ class SearchTests: AppTestCase { do { // first page // MUT let res = try await API.search(database: app.db, - query: .init(query: "foo", page: 1), - pageSize: 3) + query: .init(query: "foo", page: 1, pageSize: 3)) // validate XCTAssertTrue(res.hasMoreResults) @@ -373,8 +372,7 @@ class SearchTests: AppTestCase { do { // second page // MUT let res = try await API.search(database: app.db, - query: .init(query: "foo", page: 2), - pageSize: 3) + query: .init(query: "foo", page: 2, pageSize: 3)) // validate XCTAssertTrue(res.hasMoreResults) @@ -385,8 +383,7 @@ class SearchTests: AppTestCase { do { // third page // MUT let res = try await API.search(database: app.db, - query: .init(query: "foo", page: 3), - pageSize: 3) + query: .init(query: "foo", page: 3, pageSize: 3)) // validate XCTAssertFalse(res.hasMoreResults) @@ -415,8 +412,7 @@ class SearchTests: AppTestCase { do { // first page // MUT let res = try await API.search(database: app.db, - query: .init(query: "foo", page: 1), - pageSize: 3) + query: .init(query: "foo", page: 1, pageSize: 3)) // validate XCTAssertTrue(res.hasMoreResults) @@ -427,8 +423,7 @@ class SearchTests: AppTestCase { do { // second page // MUT let res = try await API.search(database: app.db, - query: .init(query: "foo", page: 2), - pageSize: 3) + query: .init(query: "foo", page: 2, pageSize: 3)) // validate XCTAssertTrue(res.hasMoreResults) @@ -456,8 +451,7 @@ class SearchTests: AppTestCase { do { // page out of bounds (too large) // MUT let res = try await API.search(database: app.db, - query: .init(query: "foo", page: 4), - pageSize: 3) + query: .init(query: "foo", page: 4, pageSize: 3)) // validate XCTAssertFalse(res.hasMoreResults) @@ -468,8 +462,7 @@ class SearchTests: AppTestCase { do { // page out of bounds (too small - will be clamped to page 1) // MUT let res = try await API.search(database: app.db, - query: .init(query: "foo", page: 0), - pageSize: 3) + query: .init(query: "foo", page: 0, pageSize: 3)) XCTAssertTrue(res.hasMoreResults) XCTAssertEqual(res.results.map(\.testDescription), ["a:foobar", "p:0", "p:1", "p:2"])