Skip to content
Draft

Search #15952

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
14 changes: 11 additions & 3 deletions Firestore/Example/Firestore.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1749,6 +1749,9 @@
EF6C286E29E6D22200A7D4F1 /* AggregationIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF6C286C29E6D22200A7D4F1 /* AggregationIntegrationTests.swift */; };
EF6C286F29E6D22200A7D4F1 /* AggregationIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF6C286C29E6D22200A7D4F1 /* AggregationIntegrationTests.swift */; };
EF79998EBE4C72B97AB1880E /* value_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 40F9D09063A07F710811A84F /* value_util_test.cc */; };
EF8A87852F3F9A2A00088002 /* SearchIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF8A87842F3F9A2A00088002 /* SearchIntegrationTests.swift */; };
EF8A87862F3F9A2A00088002 /* SearchIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF8A87842F3F9A2A00088002 /* SearchIntegrationTests.swift */; };
EF8A87872F3F9A2A00088002 /* SearchIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF8A87842F3F9A2A00088002 /* SearchIntegrationTests.swift */; };
EF8C005DC4BEA6256D1DBC6F /* user_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CCC9BD953F121B9E29F9AA42 /* user_test.cc */; };
EFD682178A87513A5F1AEFD9 /* memory_query_engine_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 8EF6A33BC2D84233C355F1D0 /* memory_query_engine_test.cc */; };
EFF22EAA2C5060A4009A369B /* VectorIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFF22EA92C5060A4009A369B /* VectorIntegrationTests.swift */; };
Expand Down Expand Up @@ -2351,6 +2354,7 @@
EF6C285029E462A200A7D4F1 /* FIRAggregateTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRAggregateTests.mm; sourceTree = "<group>"; };
EF6C286C29E6D22200A7D4F1 /* AggregationIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregationIntegrationTests.swift; sourceTree = "<group>"; };
EF83ACD5E1E9F25845A9ACED /* leveldb_migrations_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_migrations_test.cc; sourceTree = "<group>"; };
EF8A87842F3F9A2A00088002 /* SearchIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchIntegrationTests.swift; sourceTree = "<group>"; };
EFF22EA92C5060A4009A369B /* VectorIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VectorIntegrationTests.swift; sourceTree = "<group>"; };
F02F734F272C3C70D1307076 /* filter_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = filter_test.cc; sourceTree = "<group>"; };
F119BDDF2F06B3C0883B8297 /* firebase_app_check_credentials_provider_test.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; name = firebase_app_check_credentials_provider_test.mm; path = credentials/firebase_app_check_credentials_provider_test.mm; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2499,6 +2503,7 @@
861684E49DAC993D153E60D0 /* PipelineTests.swift */,
621D620928F9CE7400D2FA26 /* QueryIntegrationTests.swift */,
128F2B002E254E2C0006327E /* QueryToPipelineTests.swift */,
EF8A87842F3F9A2A00088002 /* SearchIntegrationTests.swift */,
4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */,
EFF22EA92C5060A4009A369B /* VectorIntegrationTests.swift */,
);
Expand Down Expand Up @@ -4978,6 +4983,7 @@
655F8647F57E5F2155DFF7B5 /* PipelineTests.swift in Sources */,
621D620C28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */,
128F2B022E254E2C0006327E /* QueryToPipelineTests.swift in Sources */,
EF8A87852F3F9A2A00088002 /* SearchIntegrationTests.swift in Sources */,
1CFBD4563960D8A20C4679A3 /* SnapshotListenerSourceTests.swift in Sources */,
EE4C4BE7F93366AE6368EE02 /* TestHelper.swift in Sources */,
EFF22EAC2C5060A4009A369B /* VectorIntegrationTests.swift in Sources */,
Expand Down Expand Up @@ -5012,8 +5018,8 @@
E3E6B368A755D892F937DBF7 /* collection_group_test.cc in Sources */,
064689971747DA312770AB7A /* collection_test.cc in Sources */,
1DB3013C5FC736B519CD65A3 /* common.pb.cc in Sources */,
99F97B28DA546D42AB14214B /* comparison_test.cc in Sources */,
555161D6DB2DDC8B57F72A70 /* comparison_test.cc in Sources */,
99F97B28DA546D42AB14214B /* comparison_test.cc in Sources */,
BB5F19878EA5A8D9C7276D40 /* complex_test.cc in Sources */,
7394B5C29C6E524C2AF964E6 /* counting_query_engine.cc in Sources */,
C02A969BF4BB63ABCB531B4B /* create_noop_connectivity_monitor.cc in Sources */,
Expand Down Expand Up @@ -5259,6 +5265,7 @@
C8C2B945D84DD98391145F3F /* PipelineTests.swift in Sources */,
621D620B28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */,
128F2B032E254E2C0006327E /* QueryToPipelineTests.swift in Sources */,
EF8A87872F3F9A2A00088002 /* SearchIntegrationTests.swift in Sources */,
A0BC30D482B0ABD1A3A24CDC /* SnapshotListenerSourceTests.swift in Sources */,
A78366DBE0BFDE42474A728A /* TestHelper.swift in Sources */,
EFF22EAB2C5060A4009A369B /* VectorIntegrationTests.swift in Sources */,
Expand Down Expand Up @@ -5293,8 +5300,8 @@
1CDA0E10BC669276E0EAA1E8 /* collection_group_test.cc in Sources */,
C87DF880BADEA1CBF8365700 /* collection_test.cc in Sources */,
1D71CA6BBA1E3433F243188E /* common.pb.cc in Sources */,
476AE05E0878007DE1BF5460 /* comparison_test.cc in Sources */,
9C86EEDEA131BFD50255EEF1 /* comparison_test.cc in Sources */,
476AE05E0878007DE1BF5460 /* comparison_test.cc in Sources */,
C5434EF8A0C8B79A71F0784C /* complex_test.cc in Sources */,
DCD83C545D764FB15FD88B02 /* counting_query_engine.cc in Sources */,
ECC433628575AE994C621C54 /* create_noop_connectivity_monitor.cc in Sources */,
Expand Down Expand Up @@ -5822,6 +5829,7 @@
E04CB0D580980748D5DC453F /* PipelineTests.swift in Sources */,
621D620A28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */,
128F2B012E254E2C0006327E /* QueryToPipelineTests.swift in Sources */,
EF8A87862F3F9A2A00088002 /* SearchIntegrationTests.swift in Sources */,
B00F8D1819EE20C45B660940 /* SnapshotListenerSourceTests.swift in Sources */,
AD34726BFD3461FF64BBD56D /* TestHelper.swift in Sources */,
EFF22EAA2C5060A4009A369B /* VectorIntegrationTests.swift in Sources */,
Expand Down Expand Up @@ -5856,8 +5864,8 @@
4A6B1E0B678E31367A55DC17 /* collection_group_test.cc in Sources */,
BACA9CDF0F2E926926B5F36F /* collection_test.cc in Sources */,
4C66806697D7BCA730FA3697 /* common.pb.cc in Sources */,
C885C84B7549C860784E4E3C /* comparison_test.cc in Sources */,
EC7A44792A5513FBB6F501EE /* comparison_test.cc in Sources */,
C885C84B7549C860784E4E3C /* comparison_test.cc in Sources */,
62C86789E72E624A27BF6AE5 /* complex_test.cc in Sources */,
BDF3A6C121F2773BB3A347A7 /* counting_query_engine.cc in Sources */,
1F4930A8366F74288121F627 /* create_noop_connectivity_monitor.cc in Sources */,
Expand Down
85 changes: 83 additions & 2 deletions Firestore/Source/API/FIRPipelineBridge.mm
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
using firebase::firestore::api::RemoveFieldsStage;
using firebase::firestore::api::ReplaceWith;
using firebase::firestore::api::Sample;
using firebase::firestore::api::SearchStage;
using firebase::firestore::api::SelectStage;
using firebase::firestore::api::SnapshotMetadata;
using firebase::firestore::api::SortStage;
Expand Down Expand Up @@ -175,13 +176,17 @@ @implementation FIRFunctionExprBridge {
std::shared_ptr<FunctionExpr> cpp_function;
NSString *_name;
NSArray<FIRExprBridge *> *_args;
NSDictionary<NSString *, FIRExprBridge *> *_Nullable _options;
Boolean isUserDataRead;
}

- (nonnull id)initWithName:(NSString *)name Args:(nonnull NSArray<FIRExprBridge *> *)args {
- (nonnull id)initWithName:(NSString *)name
Args:(nonnull NSArray<FIRExprBridge *> *)args
Options:(NSDictionary<NSString *, FIRExprBridge *> *_Nullable)options {
self = [super init];
_name = name;
_args = args;
_options = options;
isUserDataRead = NO;
return self;
}
Expand All @@ -192,7 +197,16 @@ - (nonnull id)initWithName:(NSString *)name Args:(nonnull NSArray<FIRExprBridge
for (FIRExprBridge *arg in _args) {
cpp_args.push_back([arg cppExprWithReader:reader]);
}
cpp_function = std::make_shared<FunctionExpr>(MakeString(_name), std::move(cpp_args));

std::unordered_map<std::string, std::shared_ptr<Expr>> cpp_options;
if (_options) {
for (NSString *key in _options) {
cpp_options[MakeString(key)] = [_options[key] cppExprWithReader:reader];
}
}

cpp_function = std::make_shared<FunctionExpr>(MakeString(_name), std::move(cpp_args),
std::move(cpp_options));
}

isUserDataRead = YES;
Expand Down Expand Up @@ -948,6 +962,73 @@ - (NSString *)name {
}
@end

@implementation FIRSearchStageBridge {
NSDictionary<NSString *, FIRExprBridge *> *_options;
NSDictionary<NSString *, FIRExprBridge *> *_add_fields;
NSDictionary<NSString *, FIRExprBridge *> *_select;
NSArray<FIROrderingBridge *> *_sort;
Boolean isUserDataRead;
std::shared_ptr<SearchStage> cpp_search;
}

- (id)initWithOptions:(NSDictionary<NSString *, FIRExprBridge *> *)options
addFields:(NSDictionary<NSString *, FIRExprBridge *> *)add_fields
select:(NSDictionary<NSString *, FIRExprBridge *> *)select
sort:(NSArray<FIROrderingBridge *> *)sort {
self = [super init];
if (self) {
_options = options;
_add_fields = add_fields;
_select = select;
_sort = sort;
isUserDataRead = NO;
}
return self;
}

- (std::shared_ptr<api::Stage>)cppStageWithReader:(FSTUserDataReader *)reader {
if (!isUserDataRead) {
std::unordered_map<std::string, std::shared_ptr<Expr>> cpp_options;
if (_options) {
for (NSString *key in _options) {
cpp_options[MakeString(key)] = [_options[key] cppExprWithReader:reader];
}
}

std::unordered_map<std::string, std::shared_ptr<Expr>> cpp_add_fields;
if (_add_fields) {
for (NSString *key in _add_fields) {
cpp_add_fields[MakeString(key)] = [_add_fields[key] cppExprWithReader:reader];
}
}

std::unordered_map<std::string, std::shared_ptr<Expr>> cpp_select;
if (_select) {
for (NSString *key in _select) {
cpp_select[MakeString(key)] = [_select[key] cppExprWithReader:reader];
}
}

std::vector<Ordering> cpp_sort;
if (_sort) {
for (FIROrderingBridge *ordering in _sort) {
cpp_sort.push_back([ordering cppOrderingWithReader:reader]);
}
}

cpp_search = std::make_shared<SearchStage>(std::move(cpp_options), std::move(cpp_add_fields),
std::move(cpp_select), std::move(cpp_sort));
}

isUserDataRead = YES;
return cpp_search;
}

- (NSString *)name {
return @"search";
}
@end

@implementation FIRRawStageBridge {
NSString *_name;
NSArray<id> *_params;
Expand Down
13 changes: 12 additions & 1 deletion Firestore/Source/Public/FirebaseFirestore/FIRPipelineBridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ NS_SWIFT_NAME(ConstantBridge)
NS_SWIFT_SENDABLE
NS_SWIFT_NAME(FunctionExprBridge)
@interface FIRFunctionExprBridge : FIRExprBridge
- (id)initWithName:(NSString *)name Args:(NSArray<FIRExprBridge *> *)args;
- (id)initWithName:(NSString *)name
Args:(NSArray<FIRExprBridge *> *)args
Options:(NSDictionary<NSString *, FIRExprBridge *> *_Nullable)options;
@end

NS_SWIFT_SENDABLE
Expand Down Expand Up @@ -162,6 +164,15 @@ NS_SWIFT_NAME(FindNearestStageBridge)
distanceField:(FIRExprBridge *_Nullable)distanceField;
@end

NS_SWIFT_SENDABLE
NS_SWIFT_NAME(SearchStageBridge)
@interface FIRSearchStageBridge : FIRStageBridge
- (id)initWithOptions:(NSDictionary<NSString *, FIRExprBridge *> *)options
addFields:(NSDictionary<NSString *, FIRExprBridge *> *)add_fields
select:(NSDictionary<NSString *, FIRExprBridge *> *)select
sort:(NSArray<FIROrderingBridge *> *)sort;
@end

NS_SWIFT_SENDABLE
NS_SWIFT_NAME(SortStageBridge)
@interface FIRSorStageBridge : FIRStageBridge
Expand Down
67 changes: 67 additions & 0 deletions Firestore/Swift/Source/ExpressionImplementation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1084,4 +1084,71 @@ public extension Expression {
func type() -> FunctionExpression {
return FunctionExpression(functionName: "type", args: [self])
}

// MARK: - Snippet

/// Evaluates to an HTML-formatted text snippet highlighting terms matching the search query.
///
/// - Parameters:
/// - rquery: The search query string used to find matches (Required).
/// - maxSnippetWidth: The maximum width of the snippet (default: 160).
/// - maxSnippets: The maximum number of text pieces to return (default: 1).
/// - separator: The string used to join pieces (default: "\n").
/// - Returns: An `Expression` evaluating to the HTML snippet string.
func snippet(_ rquery: String,
maxSnippetWidth: Int? = nil,
maxSnippets: Int? = nil,
separator: String? = nil) -> Expression {
var args: [Expression] = [self, Constant(rquery)]

var options: [String: Sendable] = [:]
if let maxSnippetWidth = maxSnippetWidth {
options["maxSnippetWidth"] = maxSnippetWidth
}
if let maxSnippets = maxSnippets {
options["maxSnippets"] = maxSnippets
}
if let separator = separator {
options["separator"] = separator
}

return FunctionExpression(functionName: "snippet", args: args, options: options)
}

// MARK: - Range Operations

/// Evaluates if the result of this expression is between the `lowerBound` (inclusive)
/// and `upperBound` (inclusive).
///
/// ```swift
/// // Evaluate if the "tireWidth" is between 2.2 and 2.4
/// Field("tireWidth").between(2.2, 2.4)
///
/// // This is functionally equivalent to:
/// // Field("tireWidth").greaterThanOrEqual(2.2) && Field("tireWidth").lessThanOrEqual(2.4)
/// ```
///
/// - Parameters:
/// - lowerBound: The lower bound value (inclusive).
/// - upperBound: The upper bound value (inclusive).
/// - Returns: A `BooleanExpression` representing the range check.
func between(_ lowerBound: Sendable, _ upperBound: Sendable) -> BooleanExpression {
return between(Helper.sendableToExpr(lowerBound), Helper.sendableToExpr(upperBound))
}

/// Evaluates if the result of this expression is between the `lowerBound` (inclusive)
/// and `upperBound` (inclusive).
///
/// ```swift
/// // Evaluate if "tireWidth" is between the values of "minWidth" and "maxWidth" fields
/// Field("tireWidth").between(Field("minWidth"), Field("maxWidth"))
/// ```
///
// - Parameters:
/// - lowerBound: The lower bound expression (inclusive).
/// - upperBound: The upper bound expression (inclusive).
/// - Returns: A `BooleanExpression` representing the range check.
func between(_ lowerBound: Expression, _ upperBound: Expression) -> BooleanExpression {
return greaterThanOrEqual(lowerBound) && lessThanOrEqual(upperBound)
}
}
84 changes: 84 additions & 0 deletions Firestore/Swift/Source/Stages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,90 @@ class FindNearest: Stage {
}
}

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
class Search: Stage {
let name: String = "search"
let bridge: StageBridge
let errorMessage: String?

init(query: Expression? = nil,
limit: Int? = nil,
retrievalDepth: Int? = nil,
sort: [Ordering]? = nil,
addFields: [Selectable]? = nil,
select: [Selectable]? = nil,
offset: Int? = nil,
queryEnhancement: QueryEnhancement? = nil,
languageCode: String? = nil) {
// Options represented as a Sendable (e.g. primitive data type or Expression)
// can be added to this options map. Map and array values will be repsented
// with the map and array function expressions.
var options: [String: Sendable] = [:]
if let query = query {
options["query"] = query
}
if let limit = limit {
options["limit"] = limit
}
if let retrievalDepth = retrievalDepth {
options["retrieval_depth"] = retrievalDepth
}
if let offset = offset {
options["offset"] = offset
}
if let queryEnhancement = queryEnhancement {
options["query_enhancement"] = queryEnhancement.kind.rawValue
}
if let languageCode = languageCode {
options["language_code"] = languageCode
}

// Options represented as an array or map, which should use
// the map_value or array_value, and not the map or array function,
// must be managed independently of the options object.

// add_fields is a map_value and map function expression is not supported
var addFieldsBridge: [String: ExprBridge] = [:]
if let addFields = addFields {
let (map, error) = Helper.selectablesToMap(selectables: addFields)
if let error = error {
errorMessage = error.localizedDescription
bridge = SearchStageBridge(options: [:], addFields: [:], select: [:], sort: [])
return
}

addFieldsBridge = map.mapValues { $0.toBridge() }
}

// select is a map_value and map function expression is not supported
var selectBridge: [String: ExprBridge] = [:]
if let select = select {
let (map, error) = Helper.selectablesToMap(selectables: select)
if let error = error {
errorMessage = error.localizedDescription
bridge = SearchStageBridge(options: [:], addFields: [:], select: [:], sort: [])
return
}
selectBridge = map.mapValues { $0.toBridge() }
}

// sort is an array_value and array function expression is not supported
var sortBridge: [OrderingBridge] = []
if let sort = sort {
sortBridge = sort.map { $0.bridge }
}

errorMessage = nil
let bridgeOptions = options.mapValues { Helper.sendableToExpr($0).toBridge() }
bridge = SearchStageBridge(
options: bridgeOptions,
addFields: addFieldsBridge,
select: selectBridge,
sort: sortBridge
)
}
}

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
class Sort: Stage {
let name: String = "sort"
Expand Down
Loading
Loading