Skip to content
Merged
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 @@ -311,6 +311,8 @@ private Pipeline handleFindNearest(
List<Number> vectorValue = (List<Number>) args.get("vector_value");
String distanceMeasureStr = (String) args.get("distance_measure");
Number limitObj = (Number) args.get("limit");
String distanceField =
args.get("distance_field") instanceof String ? (String) args.get("distance_field") : null;

if (distanceMeasureStr == null) {
throw new IllegalArgumentException("'find_nearest' requires a 'distance_measure' argument");
Expand All @@ -327,13 +329,18 @@ private Pipeline handleFindNearest(
}

Field fieldExpr = Expression.field(vectorField);
boolean hasDistanceField = distanceField != null && !distanceField.isEmpty();

if (limitObj != null) {
if (limitObj != null || hasDistanceField) {
FindNearestOptions options = new FindNearestOptions();
if (limitObj != null) {
options = options.withLimit(limitObj.intValue());
}
if (distanceField != null && !distanceField.isEmpty()) {
options = options.withDistanceField(distanceField);
}
return pipeline.findNearest(
vectorField,
Expression.vector(vectorArray),
distanceMeasure,
new FindNearestOptions().withLimit(limitObj.intValue()));
vectorField, Expression.vector(vectorArray), distanceMeasure, options);
} else {
return pipeline.findNearest(fieldExpr, vectorArray, distanceMeasure);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1603,11 +1603,14 @@ + (__FIRStageBridge *)parseSearchStageWithArgs:(NSDictionary *)args
return nil;
}
FIRVectorValue *queryVector = [[FIRVectorValue alloc] initWithArray:numbers];
__FIRExprBridge *distanceFieldExpr =
(distanceField.length > 0) ? [[__FIRFieldBridge alloc] initWithName:distanceField]
: nil;
stage = [[__FIRFindNearestStageBridge alloc] initWithField:embeddingField
vectorValue:queryVector
distanceMeasure:distanceMeasure
limit:limit
distanceField:distanceField];
distanceField:distanceFieldExpr];
} else {
if (error)
*error = parseError([NSString stringWithFormat:@"Unknown pipeline stage: %@", stageName]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,26 +406,35 @@ class Pipeline {
/// compare vectors (e.g. [DistanceMeasure.cosine]). Use [limit] to return
/// only the first N documents.
///
/// [distanceField] is the Firestore field name added to each result. That
/// field holds the calculated distance from [vectorValue]. A dot in the
/// name separates path segments, as it does for any other Firestore field
/// name. Omit it to return the nearest documents without the numeric
/// distance.
///
/// Example:
/// ```dart
/// firestore.pipeline().collection('books').findNearest(
/// Field('embedding'),
/// [0.1, 0.2, 0.3],
/// DistanceMeasure.cosine,
/// limit: 10,
/// distanceField: 'distance',
/// );
/// ```
Pipeline findNearest(
Field vectorField,
List<double> vectorValue,
DistanceMeasure distanceMeasure, {
int? limit,
String? distanceField,
}) {
final stage = _FindNearestStage(
vectorField,
vectorValue,
distanceMeasure,
limit: limit,
distanceField: distanceField,
);
return Pipeline._(
_firestore,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,14 @@ final class _FindNearestStage extends PipelineStage {
final List<double> vectorValue;
final DistanceMeasure distanceMeasure;
final int? limit;
final String? distanceField;

_FindNearestStage(
this.vectorField,
this.vectorValue,
this.distanceMeasure, {
this.limit,
this.distanceField,
});

@override
Expand All @@ -203,6 +205,10 @@ final class _FindNearestStage extends PipelineStage {
if (limit != null) {
map['args']['limit'] = limit;
}
final outputField = distanceField;
if (outputField != null && outputField.isNotEmpty) {
map['args']['distance_field'] = outputField;
}
return map;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,27 @@ void runPipelineFindNearestTests() {
}
},
);

test('findNearest distanceField is present on each result', () async {
final snapshot = await firestore
.pipeline()
.collection('pipeline-e2e')
.where(Expression.field('test').equalValue('find-nearest'))
.findNearest(
Field('embedding'),
[0.1, 0.2, 0.3],
DistanceMeasure.cosine,
limit: 5,
distanceField: 'distance',
)
.execute();
expect(snapshot.result, isNotEmpty);
for (final result in snapshot.result) {
final data = result.data();
expect(data, isNotNull);
expect(data!.containsKey('distance'), isTrue);
expect(data['distance'], isA<num>());
}
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,38 @@ void main() {
final stage = pipeline.stages.last;
expect(stage['args']['distance_measure'], 'dotProduct');
});

test('serializes findNearest with distanceField', () {
final pipeline = firestore.pipeline().collection('items').findNearest(
Field('embedding'),
[0.1, 0.2, 0.3],
DistanceMeasure.cosine,
distanceField: 'distance',
);
final stage = pipeline.stages.last;
expect(stage['args']['distance_field'], 'distance');
});

test('omits distance_field when distanceField is absent', () {
final pipeline = firestore.pipeline().collection('items').findNearest(
Field('embedding'),
[0.1, 0.2, 0.3],
DistanceMeasure.cosine,
);
final stage = pipeline.stages.last;
expect(stage['args'].containsKey('distance_field'), isFalse);
});

test('omits distance_field when distanceField is empty', () {
final pipeline = firestore.pipeline().collection('items').findNearest(
Field('embedding'),
[0.1, 0.2, 0.3],
DistanceMeasure.cosine,
distanceField: '',
);
final stage = pipeline.stages.last;
expect(stage['args'].containsKey('distance_field'), isFalse);
});
});

group('_SearchStage', () {
Expand Down
Loading