Skip to content

Commit

Permalink
Optimizations for Search, also search for followed content
Browse files Browse the repository at this point in the history
  • Loading branch information
livid committed Feb 22, 2024
1 parent f945312 commit a32cb67
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 50 deletions.
6 changes: 6 additions & 0 deletions Planet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@
6A373A3C28E8178500750256 /* MyPlanetCustomCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A373A3B28E8178500750256 /* MyPlanetCustomCodeView.swift */; };
6A373A4428E8263800750256 /* CodeMirror-SwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 6A373A4328E8263800750256 /* CodeMirror-SwiftUI */; };
6A373A4528E8279B00750256 /* CodeMirror-SwiftUI in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 6A373A4328E8263800750256 /* CodeMirror-SwiftUI */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
6A43E7D12B873F4900316F81 /* SearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A43E7D02B873F4900316F81 /* SearchResult.swift */; };
6A43E7D22B873F4900316F81 /* SearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A43E7D02B873F4900316F81 /* SearchResult.swift */; };
6A4825CA28F22DAC004CE2C0 /* PodcastUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4825C928F22DAC004CE2C0 /* PodcastUtils.swift */; };
6A4A256A2A865DFB00B18C36 /* WrappingHStack in Frameworks */ = {isa = PBXBuildFile; productRef = 6A4A25692A865DFB00B18C36 /* WrappingHStack */; };
6A5110552AE0A47300C9E429 /* PlanetAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A1F97A52ADDDE0100C75625 /* PlanetAPIService.swift */; };
Expand Down Expand Up @@ -651,6 +653,7 @@
6A34BE302A34B2A00031E5E4 /* Capsules-700.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Capsules-700.otf"; sourceTree = "<group>"; };
6A34BE312A34B2A00031E5E4 /* Capsules-500.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Capsules-500.otf"; sourceTree = "<group>"; };
6A373A3B28E8178500750256 /* MyPlanetCustomCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPlanetCustomCodeView.swift; sourceTree = "<group>"; };
6A43E7D02B873F4900316F81 /* SearchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResult.swift; sourceTree = "<group>"; };
6A4825C928F22DAC004CE2C0 /* PodcastUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PodcastUtils.swift; sourceTree = "<group>"; };
6A52FAE82A8893E9000E85F0 /* PublicArticleModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicArticleModel.swift; sourceTree = "<group>"; };
6A52FAEB2A889577000E85F0 /* BackupArticleModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArticleModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1417,6 +1420,7 @@
6A52FAE82A8893E9000E85F0 /* PublicArticleModel.swift */,
72114E45790363ADCC6F7673 /* DraftModel.swift */,
721143E2F2DEA3A694022B20 /* AttachmentModel.swift */,
6A43E7D02B873F4900316F81 /* SearchResult.swift */,
);
path = Entities;
sourceTree = "<group>";
Expand Down Expand Up @@ -1740,6 +1744,7 @@
2A95E6752A19A3C4001288B8 /* ENSUtils.swift in Sources */,
2A95E6F22A19A781001288B8 /* PlanetArticle.swift in Sources */,
6A52FAF02A88969A000E85F0 /* BackupMyPlanetModel.swift in Sources */,
6A43E7D22B873F4900316F81 /* SearchResult.swift in Sources */,
2A95E6462A19A336001288B8 /* PlanetQuickShare+Extension.swift in Sources */,
2A95E6542A19A39C001288B8 /* PlanetPublishedFolders+Extension.swift in Sources */,
6A61D5E82B23A234007F761E /* CapsuleBar.swift in Sources */,
Expand Down Expand Up @@ -1922,6 +1927,7 @@
2A9B09A5294F35640002719C /* PFDashboardContentView.swift in Sources */,
6A5E63AC28072AD400FB1E84 /* TemplateBrowserView.swift in Sources */,
2AE44EAE28CD219700944786 /* PlanetSettingsModel.swift in Sources */,
6A43E7D12B873F4900316F81 /* SearchResult.swift in Sources */,
2A04DADD28EAD2250040D8E0 /* PlanetPublishedServiceStore.swift in Sources */,
2AFFD51B28AD760400EDB020 /* HelpLinkButton.swift in Sources */,
72114F25F504270EB29068C9 /* PlanetError.swift in Sources */,
Expand Down
23 changes: 23 additions & 0 deletions Planet/Entities/SearchResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// SearchResult.swift
// Planet
//
// Created by Xin Liu on 2/22/24.
//

import Foundation

enum PlanetKind: String {
case my
case following
}

struct SearchResult: Hashable {
let articleID: UUID
let articleCreated: Date
let title: String
let preview: String
let planetID: UUID
let planetName: String
let planetKind: PlanetKind
}
204 changes: 175 additions & 29 deletions Planet/Search/PlanetStore+Search.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,47 +8,193 @@
import Foundation

extension PlanetStore {
func searchArticles(text: String) async -> [MyArticleModel] {
var result: [MyArticleModel] = []
func searchAllArticles(text: String) async -> [SearchResult] {
guard !text.isEmpty else { return [] }

let searchText = text.lowercased()
if searchText.count == 0 {
return result

// Use async let to concurrently perform the searches
async let myPlanetResults = searchArticles(
in: myPlanets,
matching: searchText,
planetKind: .my
)
async let followingPlanetResults = searchArticles(
in: followingPlanets,
matching: searchText,
planetKind: .following
)

// Await the results of both searches and combine them
let results = (await myPlanetResults) + (await followingPlanetResults)

debugPrint("Search result for \(text): \(results.count)")
return results.sorted(by: { $0.articleCreated > $1.articleCreated })
}

private func searchArticles(
in planets: [MyPlanetModel],
matching text: String,
planetKind: PlanetKind
) async -> [SearchResult] {
await withTaskGroup(of: [SearchResult].self, returning: [SearchResult].self) { group in
for planet in planets {
group.addTask {
var matches: [SearchResult] = []
for article in planet.articles {
let isMatch = await self.matchMyArticle(article: article, text: text)
if isMatch {
let match = SearchResult(
articleID: article.id,
articleCreated: article.created,
title: article.title,
preview: article.content,
planetID: planet.id,
planetName: planet.name,
planetKind: planetKind
)
matches.append(match)
}
}
return matches
}
}
// Collect results from all tasks
var allResults = await group.reduce(into: []) { $0 += $1 }

// Sort if needed or apply any criteria for determining 'top' results
// This step is optional and can be adjusted based on how you define 'top'
// For example, you might sort by articleCreated date or any other relevant field
// allResults.sort(by: { $0.articleCreated > $1.articleCreated })

// Return only the top 200 results
return Array(allResults.prefix(200))
}
for planet in myPlanets {
for article in planet.articles {
if matchArticle(article: article, text: searchText) {
result.append(article)
}

private func searchArticles(
in planets: [FollowingPlanetModel],
matching text: String,
planetKind: PlanetKind
) async -> [SearchResult] {
await withTaskGroup(of: [SearchResult].self, returning: [SearchResult].self) { group in
for planet in planets {
group.addTask {
var matches: [SearchResult] = []
for article in planet.articles {
let isMatch = await self.matchFollowingArticle(article: article, text: text)
if isMatch {
let match = SearchResult(
articleID: article.id,
articleCreated: article.created,
title: article.title,
preview: article.content,
planetID: planet.id,
planetName: planet.name,
planetKind: planetKind
)
matches.append(match)
}
}
return matches
}
}
// Collect results from all tasks
var allResults = await group.reduce(into: []) { $0 += $1 }

// Optionally sort the results if you have a specific criterion for 'top' results
// allResults.sort(by: { $0.articleCreated > $1.articleCreated })

// Return only the top 200 results
return Array(allResults.prefix(200))
}
debugPrint("Search result for \(text): \(result.count)")
return result.sorted(by: { $0.created > $1.created })
}

private func matchArticle(article: MyArticleModel, text: String) -> Bool {
private func matchMyArticle(article: MyArticleModel, text: String) async -> Bool {
let searchText = text.lowercased()
if searchText.count == 0 {
guard searchText.count > 0 else {
return false
}
if article.title.lowercased().contains(searchText) {
return true
}
if article.content.lowercased().contains(searchText) {
return true
}
if let slug = article.slug, slug.lowercased().contains(searchText) {
return true

// Use a task group to check article title, content, slug, tags, and attachments in parallel
return await withTaskGroup(of: Bool.self, returning: Bool.self) { group in
// Check title
group.addTask {
return article.title.lowercased().contains(searchText)
}

// Check content
group.addTask {
return article.content.lowercased().contains(searchText)
}

// Check slug, if it exists
if let slug = article.slug {
group.addTask {
return slug.lowercased().contains(searchText)
}
}

// Check tags, if they exist
if let tags = article.tags {
group.addTask {
return tags.keys.contains(where: { $0.lowercased().contains(searchText) })
}
}

// Check attachments, if they exist
if let attachments = article.attachments {
group.addTask {
return attachments.contains(where: { $0.lowercased().contains(searchText) })
}
}

// Iterate over the results, returning true if any task finds a match
for await result in group {
if result {
return true
}
}

// If none of the tasks returned true, then there was no match
return false
}
if let tags = article.tags,
tags.keys.contains(where: { $0.lowercased().contains(searchText) })
{
return true
}

private func matchFollowingArticle(article: FollowingArticleModel, text: String) async -> Bool {
let searchText = text.lowercased()
guard searchText.count > 0 else {
return false
}
if let attachments = article.attachments,
attachments.contains(where: { $0.lowercased().contains(searchText) })
{
return true

// Use a task group to check article title, content, and attachments in parallel
return await withTaskGroup(of: Bool.self, returning: Bool.self) { group in
// Check title
group.addTask {
return article.title.lowercased().contains(searchText)
}

// Check content
group.addTask {
return article.content.lowercased().contains(searchText)
}

// Check attachments, if they exist
if let attachments = article.attachments {
group.addTask {
return attachments.contains(where: { $0.lowercased().contains(searchText) })
}
}

// Iterate over the results, returning true if any task finds a match
for await result in group {
if result {
return true
}
}

// If none of the tasks returned true, then there was no match
return false
}
return false
}
}
Loading

0 comments on commit a32cb67

Please sign in to comment.