Skip to content

Commit

Permalink
Merge pull request #2560 from SwiftPackageIndex/add-search-page-size
Browse files Browse the repository at this point in the history
Add pageSize parameter to search API
  • Loading branch information
finestructure authored Aug 18, 2023
2 parents d105e6f + dbb375a commit 532033e
Show file tree
Hide file tree
Showing 7 changed files with 44 additions and 48 deletions.
17 changes: 10 additions & 7 deletions Sources/App/Controllers/API/API+SearchController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,42 +21,45 @@ 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
}
}

static func get(req: Request) async throws -> Search.Response {
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)
}
}
}


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,
Expand All @@ -67,6 +70,6 @@ extension API {
return try await Search.fetch(database,
terms,
page: query.page,
pageSize: pageSize).get()
pageSize: query.pageSize).get()
}
}
23 changes: 17 additions & 6 deletions Sources/App/Controllers/KeywordController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -52,7 +63,7 @@ enum KeywordController {
let model = KeywordShow.Model(
keyword: keyword,
packages: packageInfo,
page: pageIndex,
page: query.page,
hasMoreResults: page.hasMoreResults
)

Expand Down
3 changes: 1 addition & 2 deletions Sources/App/Controllers/SearchController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
2 changes: 0 additions & 2 deletions Sources/App/Core/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion Sources/App/Core/Search.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down
24 changes: 8 additions & 16 deletions Tests/AppTests/QueryPerformanceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
21 changes: 7 additions & 14 deletions Tests/AppTests/SearchTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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"])
Expand Down

0 comments on commit 532033e

Please sign in to comment.