// // SemanticIndex.swift // Poche // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // import Foundation import NaturalLanguage /// Local semantic ranking over the user's data. NLEmbedding runs entirely /// on-device — no service is involved, not even for indexing (CLAUDE.md §7). /// /// Constraint: distances are computed per query over the whole corpus. /// Fine at personal-notes scale; precompute stored vectors before the /// corpus grows past a few thousand documents. @MainActor final class SemanticIndex { private let embedding = NLEmbedding.sentenceEmbedding(for: .french) func rank(query: String, in documents: [SearchDocument], limit: Int = 3) -> [SearchDocument] { guard !documents.isEmpty else { return [] } if let embedding { let scored = documents.map { document in (document, embedding.distance(between: query, and: document.text, distanceType: .cosine)) } return scored .sorted { $0.1 < $1.1 } .prefix(limit) .map(\.0) } // Keyword fallback when the French sentence embedding asset is // not present on the device. let needles = query.lowercased().split(separator: " ").map(String.init) let scored = documents.map { document in (document, needles.count(where: { document.text.lowercased().contains($0) })) } return scored .filter { $0.1 > 0 } .sorted { $0.1 > $1.1 } .prefix(limit) .map(\.0) } }