Tokenised text search in SwiftData help

The SwiftData predicate documentation says that it supports the contains(where:) sequence operation in addition to the contains(_:) string comparison but when I put them together in a predicate to try and perform a tokenised search I get a runtime error.

Unsupported subquery collection expression type (NSInvalidArgumentException)

I need to be able to search for items that contain at least one of the search tokens, this functionality is critical to my app. Any suggestions are appreciated. Also does anyone with experience with CoreData know if this is possible to do in CoreData with NSPredicate?

import SwiftData

@Model 
final class Item {
    var textString: String = ""
    init() {}
}

func search(tokens: Set<String>, context: ModelContext) throws -> [Item] {
    let predicate: Predicate<Item> = #Predicate { item in
        tokens.contains { token in
            item.textString.contains(token)
        }
    }
    let descriptor = FetchDescriptor(predicate: predicate)
    return try context.fetch(descriptor)
}
Answered by DTS Engineer in 793734022
let predicate: Predicate<Item> = #Predicate { item in
        tokens.contains { token in
            item.textString.contains(token)
        }
}

This isn't going to work. Other than doing string comparisons for an attribute, contains only works with a to-many relationship (and is eventually converted to a subquery on the relationship).

It sounds like you need to merge unknown number of predicates, which is not supported by #Predicate because it can't handle for loops.

To achieve your goal, consider creating your own predicate using PredicateExpressions. There are some discussions on that topic and here is an example that may help: <https://forums.developer.apple.com/forums/thread/736166>.

Just in case this matters, starting from iOS 17.4, you can dynamically combine predicates, as shown in the following code example:

	let predicate1 = #Predicate<Item> {
		$0.textString.contains(token1)
	}
	let predicate2 = #Predicate<Item> {
		$0.textString.contains(token2)
	}
	
	let orPredicate = #Predicate<Item> {
		predicate1.evaluate($0) || predicate2.evaluate($0)
	}

I know this isn't what you are looking for because you have a unknown number of tokens.

In the Core Data world, you can use NSPredicate NSCompoundPredicate, like this:

let subPredicates = tokens.map {
    NSPredicate(format: "textString CONTAINS %@", $0)
}
let predicate = NSCompoundPredicate(orPredicateWithSubpredicates: subPredicates)

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Accepted Answer
let predicate: Predicate<Item> = #Predicate { item in
        tokens.contains { token in
            item.textString.contains(token)
        }
}

This isn't going to work. Other than doing string comparisons for an attribute, contains only works with a to-many relationship (and is eventually converted to a subquery on the relationship).

It sounds like you need to merge unknown number of predicates, which is not supported by #Predicate because it can't handle for loops.

To achieve your goal, consider creating your own predicate using PredicateExpressions. There are some discussions on that topic and here is an example that may help: <https://forums.developer.apple.com/forums/thread/736166>.

Just in case this matters, starting from iOS 17.4, you can dynamically combine predicates, as shown in the following code example:

	let predicate1 = #Predicate<Item> {
		$0.textString.contains(token1)
	}
	let predicate2 = #Predicate<Item> {
		$0.textString.contains(token2)
	}
	
	let orPredicate = #Predicate<Item> {
		predicate1.evaluate($0) || predicate2.evaluate($0)
	}

I know this isn't what you are looking for because you have a unknown number of tokens.

In the Core Data world, you can use NSPredicate NSCompoundPredicate, like this:

let subPredicates = tokens.map {
    NSPredicate(format: "textString CONTAINS %@", $0)
}
let predicate = NSCompoundPredicate(orPredicateWithSubpredicates: subPredicates)

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

I’ve considered turning textString into a to-many relationship by creating a Token entity but I’m unsure whether this will significantly increase overhead? I expect my database to grow very large unless I apply unique constraints on tokens and handle the CloudKit syncing myself (not using NSPersistentCloudKitContainer).

Also in my app if many results are fetched (between 1000 to 100,000 matches) from the predicate I will need to perform an additional filter that can only be done in-memory, in this scenario would I need to perform both the predicate and filter in batches?

Tokenised text search in SwiftData help
 
 
Q