Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,8 @@ Expression parseExpression(@NonNull Map<String, Object> expressionMap) {
return parseArrayContainsAll(args);
case "array_contains_any":
return parseArrayContainsAny(args);
case "document_matches":
return parseDocumentMatches(args);
default:
Log.w(TAG, "Unsupported expression type: " + name);
throw new UnsupportedOperationException("Expression type not yet implemented: " + name);
Expand Down Expand Up @@ -583,6 +585,8 @@ BooleanExpression parseBooleanExpression(@NonNull Map<String, Object> expression
return parseNotEqualAny(args);
case "as_boolean":
return parseAsBoolean(args);
case "document_matches":
return parseDocumentMatches(args);
default:
Expression expr = parseExpression(expressionMap);
if (expr instanceof BooleanExpression) {
Expand All @@ -605,6 +609,14 @@ Selectable parseSelectable(@NonNull Map<String, Object> expressionMap) {
return (Selectable) expr;
}

private BooleanExpression parseDocumentMatches(@NonNull Map<String, Object> args) {
String query = (String) args.get("query");
if (query == null) {
throw new IllegalArgumentException("document_matches requires a 'query' argument");
}
return Expression.documentMatches(query);
}

@SuppressWarnings("unchecked")
AggregateFunction parseAggregateFunction(@NonNull Map<String, Object> aggregateMap) {
String functionName = (String) aggregateMap.get("function");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.google.firebase.firestore.pipeline.FindNearestStage;
import com.google.firebase.firestore.pipeline.Ordering;
import com.google.firebase.firestore.pipeline.SampleStage;
import com.google.firebase.firestore.pipeline.SearchStage;
import com.google.firebase.firestore.pipeline.Selectable;
import com.google.firebase.firestore.pipeline.UnnestOptions;
import java.util.List;
Expand Down Expand Up @@ -71,6 +72,8 @@ Pipeline applyStage(
return handleSample(pipeline, args);
case "find_nearest":
return handleFindNearest(pipeline, args);
case "search":
return handleSearch(pipeline, args);
default:
throw new IllegalArgumentException("Unknown pipeline stage: " + stageName);
}
Expand Down Expand Up @@ -335,4 +338,83 @@ private Pipeline handleFindNearest(
return pipeline.findNearest(fieldExpr, vectorArray, distanceMeasure);
}
}

@SuppressWarnings("unchecked")
private Pipeline handleSearch(@NonNull Pipeline pipeline, @Nullable Map<String, Object> args) {
if (args == null) {
throw new IllegalArgumentException("'search' requires arguments");
}

String queryType = (String) args.get("query_type");
Object query = args.get("query");
SearchStage searchStage;
if ("string".equals(queryType)) {
searchStage = SearchStage.withQuery((String) query);
} else if ("expression".equals(queryType)) {
BooleanExpression expressionQuery =
parsers.parseBooleanExpression((Map<String, Object>) query);
searchStage = SearchStage.withQuery(expressionQuery);
} else {
throw new IllegalArgumentException(
"'search' requires query_type to be either 'string' or 'expression'");
}

List<Map<String, Object>> sortMaps = (List<Map<String, Object>>) args.get("sort");
if (sortMaps != null && !sortMaps.isEmpty()) {
Ordering firstOrdering = parseOrdering(sortMaps.get(0));
if (sortMaps.size() == 1) {
searchStage = searchStage.withSort(firstOrdering);
} else {
Ordering[] additionalOrderings = new Ordering[sortMaps.size() - 1];
for (int i = 1; i < sortMaps.size(); i++) {
additionalOrderings[i - 1] = parseOrdering(sortMaps.get(i));
}
searchStage = searchStage.withSort(firstOrdering, additionalOrderings);
}
}

List<Map<String, Object>> addFieldMaps = (List<Map<String, Object>>) args.get("add_fields");
if (addFieldMaps != null && !addFieldMaps.isEmpty()) {
Selectable firstField = parsers.parseSelectable(addFieldMaps.get(0));
if (addFieldMaps.size() == 1) {
searchStage = searchStage.withAddFields(firstField);
} else {
Selectable[] additionalFields = new Selectable[addFieldMaps.size() - 1];
for (int i = 1; i < addFieldMaps.size(); i++) {
additionalFields[i - 1] = parsers.parseSelectable(addFieldMaps.get(i));
}
searchStage = searchStage.withAddFields(firstField, additionalFields);
}
}

String languageCode = (String) args.get("language_code");
if (languageCode != null) {
searchStage = searchStage.withLanguageCode(languageCode);
}

Number limit = (Number) args.get("limit");
if (limit != null) {
searchStage = searchStage.withLimit(limit.longValue());
}

Number offset = (Number) args.get("offset");
if (offset != null) {
searchStage = searchStage.withOffset(offset.longValue());
}

Number retrievalDepth = (Number) args.get("retrieval_depth");
if (retrievalDepth != null) {
searchStage = searchStage.withRetrievalDepth(retrievalDepth.longValue());
}

return pipeline.search(searchStage);
}

@SuppressWarnings("unchecked")
private Ordering parseOrdering(@NonNull Map<String, Object> orderingMap) {
Expression expression =
parsers.parseExpression((Map<String, Object>) orderingMap.get("expression"));
String direction = (String) orderingMap.get("order_direction");
return "asc".equals(direction) ? expression.ascending() : expression.descending();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,16 @@ - (FIRExprBridge *)parseExpression:(NSDictionary<NSString *, id> *)map error:(NS
return [self parseExpression:(NSDictionary *)exprMap error:error];
}

if ([name isEqualToString:@"document_matches"]) {
NSString *query = args[@"query"];
if (![query isKindOfClass:[NSString class]]) {
if (error) *error = parseError(@"document_matches requires query");
return nil;
}
FIRExprBridge *queryExpr = [[FIRConstantBridge alloc] init:query];
return FLTNewFunctionExprBridge(@"document_matches", @[ queryExpr ]);
}

// Map Dart names to iOS SDK names where they differ
NSString *sdkName = name;
if ([name isEqualToString:@"bit_xor"]) sdkName = @"xor";
Expand Down Expand Up @@ -908,6 +918,112 @@ + (NSString *)keyForExpressionMap:(NSDictionary *)em error:(NSError **)error {
return nil;
}

+ (NSDictionary<NSString *, FIRExprBridge *> *)
parseSearchFieldsWithExpressionMaps:(NSArray<NSDictionary<NSString *, id> *> *)exprMaps
exprParser:(FLTPipelineExpressionParser *)exprParser
error:(NSError **)error {
NSMutableDictionary<NSString *, FIRExprBridge *> *fields = [NSMutableDictionary dictionary];
NSError *parseErr = nil;

for (id em in exprMaps) {
if (![em isKindOfClass:[NSDictionary class]]) continue;

FIRExprBridge *expr = [exprParser parseExpression:em error:&parseErr];
if (!expr) {
if (error) *error = parseErr;
return nil;
}

NSString *key = [self keyForExpressionMap:em error:error];
if (![key isKindOfClass:[NSString class]] || key.length == 0) return nil;
fields[key] = expr;
}

return fields;
}

+ (FIRStageBridge *)parseSearchStageWithArgs:(NSDictionary *)args
exprParser:(FLTPipelineExpressionParser *)exprParser
error:(NSError **)error {
NSString *queryType = args[@"query_type"];
id query = args[@"query"];
NSMutableDictionary<NSString *, FIRExprBridge *> *options = [NSMutableDictionary dictionary];
NSError *parseErr = nil;

if ([queryType isEqualToString:@"string"]) {
if (![query isKindOfClass:[NSString class]]) {
if (error) *error = parseError(@"search query_type 'string' requires string query");
return nil;
}
FIRExprBridge *queryExpr = [[FIRConstantBridge alloc] init:query];
options[@"query"] = FLTNewFunctionExprBridge(@"document_matches", @[ queryExpr ]);
} else if ([queryType isEqualToString:@"expression"]) {
if (![query isKindOfClass:[NSDictionary class]]) {
if (error) *error = parseError(@"search query_type 'expression' requires expression query");
return nil;
}
FIRExprBridge *queryExpr = [exprParser parseBooleanExpression:query error:&parseErr];
if (!queryExpr) {
if (error) *error = parseErr;
return nil;
}
options[@"query"] = queryExpr;
} else {
if (error) *error = parseError(@"search requires query_type to be 'string' or 'expression'");
return nil;
}

NSNumber *limit = [args[@"limit"] isKindOfClass:[NSNumber class]] ? args[@"limit"] : nil;
if (limit) options[@"limit"] = [[FIRConstantBridge alloc] init:limit];

NSNumber *offset = [args[@"offset"] isKindOfClass:[NSNumber class]] ? args[@"offset"] : nil;
if (offset) options[@"offset"] = [[FIRConstantBridge alloc] init:offset];

NSNumber *retrievalDepth =
[args[@"retrieval_depth"] isKindOfClass:[NSNumber class]] ? args[@"retrieval_depth"] : nil;
if (retrievalDepth) {
options[@"retrieval_depth"] = [[FIRConstantBridge alloc] init:retrievalDepth];
}

NSString *languageCode =
[args[@"language_code"] isKindOfClass:[NSString class]] ? args[@"language_code"] : nil;
if (languageCode) {
options[@"language_code"] = [[FIRConstantBridge alloc] init:languageCode];
}

NSMutableArray<FIROrderingBridge *> *sort = [NSMutableArray array];
NSArray *orderingMaps = args[@"sort"];
if ([orderingMaps isKindOfClass:[NSArray class]]) {
for (id om in orderingMaps) {
if (![om isKindOfClass:[NSDictionary class]]) continue;
id exprMap = ((NSDictionary *)om)[@"expression"];
NSString *dir = ((NSDictionary *)om)[@"order_direction"];
if (![exprMap isKindOfClass:[NSDictionary class]]) continue;
FIRExprBridge *expr = [exprParser parseExpression:exprMap error:&parseErr];
if (!expr) {
if (error) *error = parseErr;
return nil;
}
NSString *direction = [dir isEqualToString:@"asc"] ? @"ascending" : @"descending";
[sort addObject:[[FIROrderingBridge alloc] initWithExpr:expr Direction:direction]];
}
}

NSDictionary<NSString *, FIRExprBridge *> *addFields = @{};
NSArray *addFieldMaps = args[@"add_fields"];
if ([addFieldMaps isKindOfClass:[NSArray class]] && addFieldMaps.count > 0) {
addFields = [self parseSearchFieldsWithExpressionMaps:addFieldMaps
exprParser:exprParser
error:error];
if (!addFields) return nil;
}

return [[FIRSearchStageBridge alloc] initWithOptions:options
addFields:addFields
select:@{}
sort:sort];
}

+ (NSArray<FIRStageBridge *> *)
parseStagesWithFirestore:(FIRFirestore *)firestore
stages:(NSArray<NSDictionary<NSString *, id> *> *)stages
Expand Down Expand Up @@ -989,6 +1105,9 @@ + (NSString *)keyForExpressionMap:(NSDictionary *)em error:(NSError **)error {
return nil;
}
stage = [[FIRWhereStageBridge alloc] initWithExpr:expr];
} else if ([stageName isEqualToString:@"search"]) {
stage = [self parseSearchStageWithArgs:args exprParser:exprParser error:error];
if (!stage) return nil;
} else if ([stageName isEqualToString:@"limit"]) {
NSNumber *limit = args[@"limit"];
if (![limit isKindOfClass:[NSNumber class]]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ part 'src/pipeline_execute_options.dart';
part 'src/pipeline_expression.dart';
part 'src/pipeline_ordering.dart';
part 'src/pipeline_sample.dart';
part 'src/pipeline_search.dart';
part 'src/pipeline_source.dart';
part 'src/pipeline_stage.dart';
part 'src/query.dart';
Expand Down
25 changes: 25 additions & 0 deletions packages/cloud_firestore/cloud_firestore/lib/src/pipeline.dart
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,31 @@ class Pipeline {
);
}

/// Adds a search stage to this pipeline.
///
/// Search stages execute full-text search or geo search operations. A search
/// stage must be the first stage after the pipeline source.
///
/// Example:
/// ```dart
/// firestore.pipeline().collection('restaurants').search(
/// SearchStage.withQuery('breakfast -diner', limit: 10),
/// );
/// ```
Pipeline search(SearchStage searchStage) {
if (_delegate.stages.length != 1) {
throw StateError(
'A search stage must be the first stage after the pipeline source.',
);
}

final stage = _SearchStage(searchStage);
return Pipeline._(
_firestore,
_delegate.addStage(stage.toMap()),
);
}

/// Limits the maximum number of documents returned by previous stages to
/// [limit].
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,12 @@ abstract class Expression implements PipelineSerializable {
/// Creates a null value expression
static Expression nullValue() => _NullExpression();

/// Creates a search expression that matches the whole document against
/// [query].
static BooleanExpression documentMatches(String query) {
return _DocumentMatchesExpression(query);
}

/// Creates a conditional (ternary) expression
static Expression conditional(
BooleanExpression condition,
Expand Down Expand Up @@ -2043,6 +2049,26 @@ class _TrimExpression extends FunctionExpression {
/// Base class for boolean expressions used in filtering
abstract class BooleanExpression extends Expression {}

/// Represents a document_matches search expression.
class _DocumentMatchesExpression extends BooleanExpression {
final String query;

_DocumentMatchesExpression(this.query);

@override
String get name => 'document_matches';

@override
Map<String, dynamic> toMap() {
return {
'name': name,
'args': {
'query': query,
},
};
}
}

// ============================================================================
// PATTERN DEMONSTRATION - Concrete Function Expression Classes
// ============================================================================
Expand Down
Loading
Loading