Skip to content

Vector Type #8215

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
10 changes: 10 additions & 0 deletions common/api-review/firestore-lite.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,16 @@ export function updateDoc<AppModelType, DbModelType extends DocumentData>(refere
// @public
export function updateDoc<AppModelType, DbModelType extends DocumentData>(reference: DocumentReference<AppModelType, DbModelType>, field: string | FieldPath, value: unknown, ...moreFieldsAndValues: unknown[]): Promise<void>;

// @public
export function vector(values?: number[]): VectorValue;

// @public
export class VectorValue {
/* Excluded from this release type: __constructor */
isEqual(other: VectorValue): boolean;
toArray(): number[];
}

// @public
export function where(fieldPath: string | FieldPath, opStr: WhereFilterOp, value: unknown): QueryFieldFilterConstraint;

Expand Down
10 changes: 10 additions & 0 deletions common/api-review/firestore.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,16 @@ export function updateDoc<AppModelType, DbModelType extends DocumentData>(refere
// @public
export function updateDoc<AppModelType, DbModelType extends DocumentData>(reference: DocumentReference<AppModelType, DbModelType>, field: string | FieldPath, value: unknown, ...moreFieldsAndValues: unknown[]): Promise<void>;

// @public
export function vector(values?: number[]): VectorValue;

// @public
export class VectorValue {
/* Excluded from this release type: __constructor */
isEqual(other: VectorValue): boolean;
toArray(): number[];
}

// @public
export function waitForPendingWrites(firestore: Firestore): Promise<void>;

Expand Down
5 changes: 4 additions & 1 deletion packages/firestore/lite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ export {
arrayRemove,
arrayUnion,
serverTimestamp,
deleteField
deleteField,
vector
} from '../src/lite-api/field_value_impl';

export {
Expand All @@ -138,6 +139,8 @@ export {
snapshotEqual
} from '../src/lite-api/snapshot';

export { VectorValue } from '../src/lite-api/vector_value';

export { WriteBatch, writeBatch } from '../src/lite-api/write_batch';

export { TransactionOptions } from '../src/lite-api/transaction_options';
Expand Down
5 changes: 4 additions & 1 deletion packages/firestore/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,12 @@ export {
arrayUnion,
deleteField,
increment,
serverTimestamp
serverTimestamp,
vector
} from './api/field_value_impl';

export { VectorValue } from './lite-api/vector_value';

export { LogLevelString as LogLevel, setLogLevel } from './util/log';

export { Bytes } from './api/bytes';
Expand Down
3 changes: 2 additions & 1 deletion packages/firestore/src/api/field_value_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ export {
arrayRemove,
arrayUnion,
serverTimestamp,
deleteField
deleteField,
vector
} from '../lite-api/field_value_impl';
27 changes: 26 additions & 1 deletion packages/firestore/src/index/firestore_index_value_writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ import {
normalizeNumber,
normalizeTimestamp
} from '../model/normalize';
import { isMaxValue } from '../model/values';
import {
isVectorValue,
VECTOR_MAP_VECTORS_KEY,
isMaxValue
} from '../model/values';
import { ArrayValue, MapValue, Value } from '../protos/firestore_proto_api';
import { fail } from '../util/assert';
import { isNegativeZero } from '../util/types';
Expand All @@ -41,6 +45,7 @@ const INDEX_TYPE_BLOB = 30;
const INDEX_TYPE_REFERENCE = 37;
const INDEX_TYPE_GEOPOINT = 45;
const INDEX_TYPE_ARRAY = 50;
const INDEX_TYPE_VECTOR = 53;
const INDEX_TYPE_MAP = 55;
const INDEX_TYPE_REFERENCE_SEGMENT = 60;

Expand Down Expand Up @@ -121,6 +126,8 @@ export class FirestoreIndexValueWriter {
} else if ('mapValue' in indexValue) {
if (isMaxValue(indexValue)) {
this.writeValueTypeLabel(encoder, Number.MAX_SAFE_INTEGER);
} else if (isVectorValue(indexValue)) {
this.writeIndexVector(indexValue.mapValue!, encoder);
} else {
this.writeIndexMap(indexValue.mapValue!, encoder);
this.writeTruncationMarker(encoder);
Expand Down Expand Up @@ -160,6 +167,24 @@ export class FirestoreIndexValueWriter {
}
}

private writeIndexVector(
mapIndexValue: MapValue,
encoder: DirectionalIndexByteEncoder
): void {
const map = mapIndexValue.fields || {};
this.writeValueTypeLabel(encoder, INDEX_TYPE_VECTOR);

// Vectors sort first by length
const key = VECTOR_MAP_VECTORS_KEY;
const length = map[key].arrayValue?.values?.length || 0;
this.writeValueTypeLabel(encoder, INDEX_TYPE_NUMBER);
encoder.writeNumber(normalizeNumber(length));

// Vectors then sort by position value
this.writeIndexString(key, encoder);
this.writeIndexValueAux(map[key], encoder);
}

private writeIndexArray(
arrayIndexValue: ArrayValue,
encoder: DirectionalIndexByteEncoder
Expand Down
12 changes: 12 additions & 0 deletions packages/firestore/src/lite-api/field_value_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
NumericIncrementFieldValueImpl,
ServerTimestampFieldValueImpl
} from './user_data_reader';
import { VectorValue } from './vector_value';

/**
* Returns a sentinel for use with {@link @firebase/firestore/lite#(updateDoc:1)} or
Expand Down Expand Up @@ -97,3 +98,14 @@ export function arrayRemove(...elements: unknown[]): FieldValue {
export function increment(n: number): FieldValue {
return new NumericIncrementFieldValueImpl('increment', n);
}

/**
* Creates a new `VectorValue` constructed with a copy of the given array of numbers.
*
* @param values - Create a `VectorValue` instance with a copy of this array of numbers.
*
* @returns A new `VectorValue` constructed with a copy of the given array of numbers.
*/
export function vector(values?: number[]): VectorValue {
return new VectorValue(values);
}
35 changes: 34 additions & 1 deletion packages/firestore/src/lite-api/user_data_reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ import {
NumericIncrementTransformOperation,
ServerTimestampTransform
} from '../model/transform_operation';
import {
TYPE_KEY,
VECTOR_MAP_VECTORS_KEY,
VECTOR_VALUE_SENTINEL
} from '../model/values';
import { newSerializer } from '../platform/serializer';
import {
MapValue as ProtoMapValue,
Expand Down Expand Up @@ -69,6 +74,7 @@ import {
WithFieldValue
} from './reference';
import { Timestamp } from './timestamp';
import { VectorValue } from './vector_value';

const RESERVED_FIELD_REGEX = /^__.*__$/;

Expand Down Expand Up @@ -901,13 +907,39 @@ function parseScalarValue(
value._key.path
)
};
} else if (value instanceof VectorValue) {
return parseVectorValue(value);
} else {
throw context.createError(
`Unsupported field value: ${valueDescription(value)}`
);
}
}

/**
* Creates a new VectorValue proto value (using the internal format).
*/
export function parseVectorValue(value: VectorValue): ProtoValue {
const mapValue: ProtoMapValue = {
fields: {
[TYPE_KEY]: {
stringValue: VECTOR_VALUE_SENTINEL
},
[VECTOR_MAP_VECTORS_KEY]: {
arrayValue: {
values: value.toArray().map(value => {
return {
doubleValue: value
};
})
}
}
}
};

return { mapValue };
}

/**
* Checks whether an object looks like a JSON object that should be converted
* into a struct. Normal class/prototype instances are considered to look like
Expand All @@ -925,7 +957,8 @@ function looksLikeJsonObject(input: unknown): boolean {
!(input instanceof GeoPoint) &&
!(input instanceof Bytes) &&
!(input instanceof DocumentReference) &&
!(input instanceof FieldValue)
!(input instanceof FieldValue) &&
!(input instanceof VectorValue)
);
}

Expand Down
18 changes: 17 additions & 1 deletion packages/firestore/src/lite-api/user_data_writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
getPreviousValue
} from '../model/server_timestamps';
import { TypeOrder } from '../model/type_order';
import { typeOrder } from '../model/values';
import { VECTOR_MAP_VECTORS_KEY, typeOrder } from '../model/values';
import {
ApiClientObjectMap,
ArrayValue as ProtoArrayValue,
Expand All @@ -48,6 +48,7 @@ import { forEach } from '../util/obj';

import { GeoPoint } from './geo_point';
import { Timestamp } from './timestamp';
import { VectorValue } from './vector_value';

export type ServerTimestampBehavior = 'estimate' | 'previous' | 'none';

Expand Down Expand Up @@ -85,6 +86,8 @@ export abstract class AbstractUserDataWriter {
return this.convertArray(value.arrayValue!, serverTimestampBehavior);
case TypeOrder.ObjectValue:
return this.convertObject(value.mapValue!, serverTimestampBehavior);
case TypeOrder.VectorValue:
return this.convertVectorValue(value.mapValue!);
default:
throw fail('Invalid value type: ' + JSON.stringify(value));
}
Expand All @@ -111,6 +114,19 @@ export abstract class AbstractUserDataWriter {
return result;
}

/**
* @internal
*/
convertVectorValue(mapValue: ProtoMapValue): VectorValue {
const values = mapValue.fields?.[
VECTOR_MAP_VECTORS_KEY
].arrayValue?.values?.map(value => {
return normalizeNumber(value.doubleValue);
});

return new VectorValue(values);
}

private convertGeoPoint(value: ProtoLatLng): GeoPoint {
return new GeoPoint(
normalizeNumber(value.latitude),
Expand Down
51 changes: 51 additions & 0 deletions packages/firestore/src/lite-api/vector_value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @license
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { isPrimitiveArrayEqual } from '../util/array';

/**
* Represent a vector type in Firestore documents.
* Create an instance with {@link FieldValue.vector}.
*
* @class VectorValue
*/
export class VectorValue {
private readonly _values: number[];

/**
* @private
* @internal
*/
constructor(values: number[] | undefined) {
// Making a copy of the parameter.
this._values = (values || []).map(n => n);
}

/**
* Returns a copy of the raw number array form of the vector.
*/
toArray(): number[] {
return this._values.map(n => n);
}

/**
* Returns `true` if the two VectorValue has the same raw number arrays, returns `false` otherwise.
*/
isEqual(other: VectorValue): boolean {
return isPrimitiveArrayEqual(this._values, other._values);
}
}
3 changes: 2 additions & 1 deletion packages/firestore/src/model/type_order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const enum TypeOrder {
RefValue = 7,
GeoPointValue = 8,
ArrayValue = 9,
ObjectValue = 10,
VectorValue = 10,
ObjectValue = 11,
MaxValue = 9007199254740991 // Number.MAX_SAFE_INTEGER
}
Loading
Loading