Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion Package.resolved

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

85 changes: 84 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,48 @@
// swift-tools-version: 6.3

import PackageDescription
import class Foundation.ProcessInfo
import struct Foundation.URL

let environment = ProcessInfo.processInfo.environment
let packageRoot = URL(fileURLWithPath: #filePath).deletingLastPathComponent().path

func packagePath(_ path: String) -> String {
path.hasPrefix("/") ? path : "\(packageRoot)/\(path)"
}

let onnxRuntimeRoot = packagePath(environment["LEXICON_ONNX_RUNTIME_ROOT"] ?? ".build/onnx-runtime/current")
let onnxRuntimePlatform = environment["LEXICON_ONNX_RUNTIME_PLATFORM"] ?? "linux-x64"
let onnxRuntimeInclude = packagePath(environment["LEXICON_ONNX_RUNTIME_INCLUDE"] ?? "\(onnxRuntimeRoot)/include")
let onnxRuntimeLibrary = packagePath(environment["LEXICON_ONNX_RUNTIME_LIB"] ?? "\(onnxRuntimeRoot)/lib/\(onnxRuntimePlatform)")
let onnxRuntimeLibraryName = onnxRuntimePlatform == "windows-x64" ? "onnxruntime.dll" : "libonnxruntime.so"
#if os(macOS)
let useAppleONNXRuntimeBindings = (environment["LEXICON_ONNX_USE_APPLE_BINDINGS"] ?? "1") != "0"
&& environment["LEXICON_ONNX_RUNTIME_PLATFORM"] == nil
&& environment["LEXICON_ONNX_RUNTIME_INCLUDE"] == nil
&& environment["LEXICON_ONNX_RUNTIME_LIB"] == nil
#else
let useAppleONNXRuntimeBindings = false
#endif
let onnxRuntimeDependencies: [Target.Dependency] = useAppleONNXRuntimeBindings
? [
.product(
name: "onnxruntime",
package: "onnxruntime-swift-package-manager",
condition: .when(traits: ["ONNXSearch"])
)
]
: [
.target(
name: "CLexiconONNXRuntime",
condition: .when(traits: ["ONNXSearch"])
)
]
let onnxPackageDependencies: [Package.Dependency] = useAppleONNXRuntimeBindings
? [
.package(url: "https://github.com/microsoft/onnxruntime-swift-package-manager", exact: "1.24.2")
]
: []

let package = Package(
name: "Lexicon",
Expand All @@ -13,15 +55,18 @@ let package = Package(
.library(name: "_JSON", targets: ["_JSON"]),
.library(name: "Lexicon", targets: ["Lexicon"]),
.library(name: "LexiconSearchMLX", targets: ["LexiconSearchMLX"]),
.library(name: "LexiconSearchONNX", targets: ["LexiconSearchONNX"]),
.library(name: "SwiftLexicon", targets: ["SwiftLexicon"]),
.library(name: "LexiconGenerators", targets: ["LexiconGenerators"]),
.executable(name: "lexicon-generate", targets: ["lexicon-generate"]),
.executable(name: "lexicon", targets: ["lexicon-cli"]),
.plugin(name: "SwiftStandAloneGeneratorPlugin", targets: ["SwiftStandAloneGeneratorPlugin"]),
.plugin(name: "SwiftLibraryGeneratorPlugin", targets: ["SwiftLibraryGeneratorPlugin"]),
.plugin(name: "ONNXSearchArtifactsPlugin", targets: ["ONNXSearchArtifactsPlugin"]),
],
traits: [
.trait(name: "MLXSearch"),
.trait(name: "ONNXSearch"),
],
dependencies: [
.package(url: "https://github.com/screensailor/Hope", branch: "trunk"),
Expand All @@ -33,7 +78,7 @@ let package = Package(
.package(url: "https://github.com/ml-explore/mlx-swift-lm", .upToNextMajor(from: "3.31.3")),
.package(url: "https://github.com/DePasqualeOrg/swift-hf-api-mlx", exact: "0.2.0"),
.package(url: "https://github.com/DePasqualeOrg/swift-tokenizers", from: "0.6.3"),
],
] + onnxPackageDependencies,
targets: [
.target(
name: "_Collections",
Expand Down Expand Up @@ -113,6 +158,7 @@ let package = Package(
dependencies: [
"Lexicon",
.target(name: "LexiconSearchMLX", condition: .when(traits: ["MLXSearch"])),
.target(name: "LexiconSearchONNX", condition: .when(traits: ["ONNXSearch"])),
"LexiconGenerators",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]
Expand All @@ -128,6 +174,43 @@ let package = Package(
.product(name: "Tokenizers", package: "swift-tokenizers", condition: .when(traits: ["MLXSearch"])),
]
),
.target(
name: "LexiconSearchONNX",
dependencies: ["Lexicon"] + onnxRuntimeDependencies
),
.target(
name: "CLexiconONNXRuntime",
publicHeadersPath: "include",
cSettings: [
.define("LEXICON_ONNX_RUNTIME_REQUIRED", .when(traits: ["ONNXSearch"])),
.define("LEXICON_ONNX_RUNTIME_LIBRARY_PATH", to: "\"\(onnxRuntimeLibrary)/\(onnxRuntimeLibraryName)\""),
.unsafeFlags(["-I", onnxRuntimeInclude])
]
),
.testTarget(
name: "LexiconSearchONNXTests",
dependencies: [
"Lexicon",
"LexiconSearchONNX",
]
),
.executableTarget(
name: "onnx-search-artifacts",
dependencies: ["LexiconSearchONNX"]
),
.plugin(
name: "ONNXSearchArtifactsPlugin",
capability: .command(
intent: .custom(
verb: "setup-onnx-search-artifacts",
description: "Download ONNX search model and runtime artifacts."
),
permissions: [
.writeToPackageDirectory(reason: "Stores ONNX search artifacts under .build/onnx-search and .build/onnx-runtime.")
]
),
dependencies: ["onnx-search-artifacts"]
),
.plugin(
name: "SwiftStandAloneGeneratorPlugin",
capability: .buildTool(),
Expand Down
39 changes: 39 additions & 0 deletions Plugins/ONNXSearchArtifactsPlugin/plugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Foundation
import PackagePlugin

@main
struct ONNXSearchArtifactsPlugin: CommandPlugin {

func performCommand(context: PluginContext, arguments: [String]) async throws {
let tool = try context.tool(named: "onnx-search-artifacts")
var arguments = arguments
if arguments.first == "--" {
arguments.removeFirst()
}
let output = context.package.directoryURL
.appendingPathComponent(".build", isDirectory: true)
.appendingPathComponent("onnx-search", isDirectory: true)
let runtimeOutput = context.package.directoryURL
.appendingPathComponent(".build", isDirectory: true)
.appendingPathComponent("onnx-runtime", isDirectory: true)
let process = try Process.run(
tool.url,
arguments: [
"--output", output.path,
"--runtime-output", runtimeOutput.path,
] + arguments
)
process.waitUntilExit()
if process.terminationStatus != 0 {
throw ONNXSearchArtifactsPluginError(status: process.terminationStatus)
}
}
}

private struct ONNXSearchArtifactsPluginError: Error, CustomStringConvertible {
var status: Int32

var description: String {
"ONNX search artifact setup failed with exit status \(status)."
}
}
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,23 @@ swift run --traits MLXSearch lexicon search commerce.lexicon "order submit" \

MLX document embeddings are cached under the user cache directory by default. The first semantic search builds the cache and logs indexing progress to stderr; pass `--embedding-cache` or `--rebuild-embeddings` to control that cache.

See [`docs/search.md`](docs/search.md) for the search mode guide and demo lexicon examples.
For a portable ONNX Runtime backend, fetch the model/runtime artifacts with the SwiftPM command plugin, then build with the `ONNXSearch` trait:

```sh
swift package --disable-sandbox --allow-writing-to-package-directory \
setup-onnx-search-artifacts -- \
--preset bge-small-en-v1.5 \
--runtime linux-x64

swift run --traits ONNXSearch lexicon search commerce.lexicon "order submit" \
--mode semantic \
--embedding-provider onnx \
--embedding-model-preset bge-small-en-v1.5
```

ONNX presets currently include MiniLM, BGE-small, GTE-small and E5-small-v2. BGE-small is a good first ONNX model to try for semantic search, but `hybrid` remains the default because exact graph names and references still matter.

See [`docs/search.md`](docs/search.md) for the search mode guide and demo lexicon examples. See [`docs/search-platforms.md`](docs/search-platforms.md) for Linux, Android, Windows, and ONNX backend notes.

Editing commands print the updated document to stdout by default. Pass `-o` or `--output` to write another file.

Expand Down Expand Up @@ -384,12 +400,15 @@ Event payloads are JSON-backed and decode through `JSONDecoder`, so generated le
| `Lexicon` | Core document, graph, parser, composition, branch and CRDT model. |
| `SwiftLexicon` | Runtime support for generated Swift lexicons and event streams. |
| `LexiconGenerators` | Code generators and generator registry. |
| `LexiconSearchMLX` | Optional MLX embedding provider for semantic search. |
| `LexiconSearchONNX` | Optional ONNX Runtime embedding provider for semantic search. |
| `_JSON` | JSON value and decoder-backed typed access. |
| `_Collections` | Internal collection utilities, including sorted dictionary support. |
| `lexicon` | CLI for validating, inspecting, formatting, diffing and editing lexicons. |
| `lexicon-generate` | CLI for generating source artefacts. |
| `SwiftLibraryGeneratorPlugin` | SwiftPM plugin for generated Swift that depends on `SwiftLexicon`. |
| `SwiftStandAloneGeneratorPlugin` | SwiftPM plugin for stand-alone generated Swift. |
| `ONNXSearchArtifactsPlugin` | SwiftPM command plugin for downloading ONNX search model and runtime artifacts. |

## Installation

Expand All @@ -412,9 +431,9 @@ The package currently declares Swift 6.3, Swift language mode 6, macOS 15 and iO

## Platform Support

CI runs SwiftPM tests on macOS and Linux. It also runs tests on an Android emulator and cross-builds Android ARM64.
CI runs SwiftPM builds and tests on macOS and Linux for pull requests. Android runs on `trunk` pushes and manual workflow dispatches to keep pull requests fast.

On Apple platforms, sentence graph generation uses NaturalLanguage. On Linux and Android, Lexicon uses a deterministic fallback so the API remains available.
On Apple platforms, sentence graph generation uses NaturalLanguage. On Linux and Android, Lexicon uses a deterministic fallback so the API remains available. Semantic search can use MLX on Apple platforms or ONNX Runtime through the optional `ONNXSearch` trait; the ONNX C shim is designed for Linux and Android now, with Windows runtime artifacts present but session path bridging still to finish.

See [`docs/platforms.md`](docs/platforms.md) for details.

Expand Down
Loading
Loading