Skip to content

Commit

Permalink
Added iOS and macOS examples (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
jkrukowski authored Aug 25, 2024
1 parent eb1ed14 commit 538cc94
Show file tree
Hide file tree
Showing 19 changed files with 1,709 additions and 0 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ It should print the following result:
]
```

## Examples

You can find more examples in the [examples](examples) directory.

## Testing

```bash
Expand Down
444 changes: 444 additions & 0 deletions examples/EmbeddingSearch/EmbeddingSearch.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"originHash" : "aaf6a678a26d0e2261ac5404e010320978efb780c0bb1e84e11066f69b467c28",
"pins" : [
{
"identity" : "combine-schedulers",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/combine-schedulers",
"state" : {
"revision" : "9fa31f4403da54855f1e2aeaeff478f4f0e40b13",
"version" : "1.0.2"
}
},
{
"identity" : "sqlitevec",
"kind" : "remoteSourceControl",
"location" : "https://github.com/jkrukowski/SQLiteVec",
"state" : {
"revision" : "eb1ed1476bd0b3e6a25a9aea0e68eec4180861eb",
"version" : "0.0.7"
}
},
{
"identity" : "swift-clocks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-clocks",
"state" : {
"revision" : "b9b24b69e2adda099a1fa381cda1eeec272d5b53",
"version" : "1.0.5"
}
},
{
"identity" : "swift-concurrency-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
"state" : {
"revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71",
"version" : "1.1.0"
}
},
{
"identity" : "swift-dependencies",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-dependencies",
"state" : {
"revision" : "21660b042cd8fd0bdd45cc39050cacd4e91a63a4",
"version" : "1.3.8"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-syntax",
"state" : {
"revision" : "515f79b522918f83483068d99c68daeb5116342d",
"version" : "600.0.0-prerelease-2024-08-20"
}
},
{
"identity" : "xctest-dynamic-overlay",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "96beb108a57f24c8476ae1f309239270772b2940",
"version" : "1.2.5"
}
}
],
"version" : 3
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
65 changes: 65 additions & 0 deletions examples/EmbeddingSearch/EmbeddingSearch/ContentView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// ContentView.swift
// EmbeddingSearch
//
// Created by Jan Krukowski on 8/22/24.
//

import Dependencies
import SwiftUI

@MainActor
struct ContentView: View {
@State private var searchText = ""
@StateObject private var viewModel = ContentViewModel()

var body: some View {
VStack {
switch viewModel.dataState {
case .initial:
Button("Load data") {
Task {
await viewModel.populateDatabase()
}
}
case .loading:
ProgressView()
case .loaded:
NavigationStack {
VStack {
if searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
Text("Search to see the results")
} else {
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(viewModel.searchResult) { item in
VStack(alignment: .leading) {
Text(item.text)
Text(item.distance, format: .number.precision(.fractionLength(2)))
.font(.footnote)
.foregroundStyle(.gray)
}
.padding([.top, .leading, .trailing])
}
}
}
}
}
.navigationTitle("Search")
.searchable(text: $searchText)
.onChange(of: searchText) { _, _ in
Task {
await viewModel.search(by: searchText)
}
}
}
case let .error(error):
Text("Error \(error)")
}
}
}
}

#Preview {
ContentView()
}
109 changes: 109 additions & 0 deletions examples/EmbeddingSearch/EmbeddingSearch/ContentViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//
// ContentViewModel.swift
// EmbeddingSearch
//
// Created by Jan Krukowski on 8/22/24.
//

import Dependencies
import Foundation
import Observation
import OSLog
import SwiftUI

let logger = Logger()

@MainActor
final class ContentViewModel: ObservableObject {
@Published var searchResult = [QueryResult]()
@Published var dataState = DataState.initial
@Dependency(\.embeddingDatabase) private var embeddingDatabase
private var searchTask: Task<Void, any Error>?

func populateDatabase() async {
if dataState.isLoading {
return
}
do {
dataState = .loading
try await embeddingDatabase.initializeDatabase()
let texts = try await loadTexts()
try await embeddingDatabase.store(texts: texts)
dataState = .loaded
} catch {
logger.error("Error: \(error)")
dataState = .error(error.localizedDescription)
}
}

func search(by text: String) async {
searchTask?.cancel()
searchTask = Task {
do {
let query = text.trimmingCharacters(in: .whitespacesAndNewlines)
if query.isEmpty {
searchResult = []
return
}
if Task.isCancelled {
return
}
let result = try await embeddingDatabase.querySimilar(to: query)
searchResult = result.compactMap(QueryResult.init)
} catch {
logger.error("Error: \(error)")
}
}
}

private func loadTexts() async throws -> [String] {
guard let url = Bundle.main.url(forResource: "sentences", withExtension: "txt") else {
fatalError("Missing required file in main bundle")
}
let task = Task.detached {
try String(contentsOf: url)
.components(separatedBy: "\n")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
}
return try await task.value
}
}

extension ContentViewModel {
enum DataState {
case initial
case loading
case loaded
case error(String)

var isLoading: Bool {
switch self {
case .loading:
true
default:
false
}
}
}
}

struct QueryResult: Hashable, Identifiable {
let id: Int
let text: String
let distance: Double
}

extension QueryResult {
init?(_ result: [String: Any]) {
guard let id = result["id"] as? Int else {
return nil
}
guard let text = result["text"] as? String else {
return nil
}
guard let distance = result["distance"] as? Double else {
return nil
}
self.init(id: id, text: text, distance: distance)
}
}
Loading

0 comments on commit 538cc94

Please sign in to comment.