Skip to content

Commit d406f14

Browse files
feat: Query profiling for VectorQuery (#2045)
feat: Query profiling for VectorQuery
1 parent 1e949b8 commit d406f14

File tree

2 files changed

+114
-7
lines changed

2 files changed

+114
-7
lines changed

dev/src/reference/vector-query.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import {QueryUtil} from './query-util';
2828
import {Query} from './query';
2929
import {VectorQueryOptions} from './vector-query-options';
3030
import {VectorQuerySnapshot} from './vector-query-snapshot';
31+
import {ExplainResults} from '../query-profile';
32+
import {QueryResponse} from './types';
3133

3234
/**
3335
* A query that finds the documents whose vector fields are closest to a certain query vector.
@@ -92,24 +94,52 @@ export class VectorQuery<
9294
: this.queryVector.toArray();
9395
}
9496

97+
/**
98+
* Plans and optionally executes this vector search query. Returns a Promise that will be
99+
* resolved with the planner information, statistics from the query execution (if any),
100+
* and the query results (if any).
101+
*
102+
* @return A Promise that will be resolved with the planner information, statistics
103+
* from the query execution (if any), and the query results (if any).
104+
*/
105+
async explain(
106+
options?: firestore.ExplainOptions
107+
): Promise<ExplainResults<VectorQuerySnapshot<AppModelType, DbModelType>>> {
108+
if (options === undefined) {
109+
options = {};
110+
}
111+
const {result, explainMetrics} = await this._getResponse(options);
112+
if (!explainMetrics) {
113+
throw new Error('No explain results');
114+
}
115+
return new ExplainResults(explainMetrics, result || null);
116+
}
117+
95118
/**
96119
* Executes this vector search query.
97120
*
98121
* @returns A promise that will be resolved with the results of the query.
99122
*/
100123
async get(): Promise<VectorQuerySnapshot<AppModelType, DbModelType>> {
101-
const {result} = await this._queryUtil._getResponse(
102-
this,
103-
/*transactionId*/ undefined,
104-
// VectorQuery cannot be retried with cursors as they do not support cursors yet.
105-
/*retryWithCursor*/ false
106-
);
124+
const {result} = await this._getResponse();
107125
if (!result) {
108126
throw new Error('No VectorQuerySnapshot result');
109127
}
110128
return result;
111129
}
112130

131+
_getResponse(
132+
explainOptions?: firestore.ExplainOptions
133+
): Promise<QueryResponse<VectorQuerySnapshot<AppModelType, DbModelType>>> {
134+
return this._queryUtil._getResponse(
135+
this,
136+
/*transactionOrReadTime*/ undefined,
137+
// VectorQuery cannot be retried with cursors as they do not support cursors yet.
138+
/*retryWithCursor*/ false,
139+
explainOptions
140+
);
141+
}
142+
113143
/**
114144
* Internal streaming method that accepts an optional transaction ID.
115145
*
@@ -135,7 +165,8 @@ export class VectorQuery<
135165
* @returns Serialized JSON for the query.
136166
*/
137167
toProto(
138-
transactionOrReadTime?: Uint8Array | Timestamp | api.ITransactionOptions
168+
transactionOrReadTime?: Uint8Array | Timestamp | api.ITransactionOptions,
169+
explainOptions?: firestore.ExplainOptions
139170
): api.IRunQueryRequest {
140171
const queryProto = this._query.toProto(transactionOrReadTime);
141172

@@ -151,6 +182,11 @@ export class VectorQuery<
151182
},
152183
queryVector: queryVector._toProto(this._query._serializer),
153184
};
185+
186+
if (explainOptions) {
187+
queryProto.explainOptions = explainOptions;
188+
}
189+
154190
return queryProto;
155191
}
156192

dev/system-test/firestore.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,77 @@ describe('Firestore class', () => {
429429
expect(explainResults.snapshot!.data().count).to.equal(3);
430430
});
431431

432+
it('can plan a vector query', async () => {
433+
const indexTestHelper = new IndexTestHelper(firestore);
434+
435+
const collectionReference = await indexTestHelper.createTestDocs([
436+
{foo: 'bar'},
437+
{foo: 'xxx', embedding: FieldValue.vector([10, 10])},
438+
{foo: 'bar', embedding: FieldValue.vector([1, 1])},
439+
{foo: 'bar', embedding: FieldValue.vector([10, 0])},
440+
{foo: 'bar', embedding: FieldValue.vector([20, 0])},
441+
{foo: 'bar', embedding: FieldValue.vector([100, 100])},
442+
]);
443+
444+
const explainResults = await indexTestHelper
445+
.query(collectionReference)
446+
.findNearest('embedding', FieldValue.vector([1, 3]), {
447+
limit: 10,
448+
distanceMeasure: 'COSINE',
449+
})
450+
.explain({analyze: false});
451+
452+
const metrics = explainResults.metrics;
453+
454+
const plan = metrics.planSummary;
455+
expect(plan).to.not.be.null;
456+
expect(Object.keys(plan.indexesUsed).length).to.be.greaterThan(0);
457+
458+
expect(metrics.executionStats).to.be.null;
459+
expect(explainResults.snapshot).to.be.null;
460+
});
461+
462+
it('can profile a vector query', async () => {
463+
const indexTestHelper = new IndexTestHelper(firestore);
464+
465+
const collectionReference = await indexTestHelper.createTestDocs([
466+
{foo: 'bar'},
467+
{foo: 'xxx', embedding: FieldValue.vector([10, 10])},
468+
{foo: 'bar', embedding: FieldValue.vector([1, 1])},
469+
{foo: 'bar', embedding: FieldValue.vector([10, 0])},
470+
{foo: 'bar', embedding: FieldValue.vector([20, 0])},
471+
{foo: 'bar', embedding: FieldValue.vector([100, 100])},
472+
]);
473+
474+
const explainResults = await indexTestHelper
475+
.query(collectionReference)
476+
.findNearest('embedding', FieldValue.vector([1, 3]), {
477+
limit: 10,
478+
distanceMeasure: 'COSINE',
479+
})
480+
.explain({analyze: true});
481+
482+
const metrics = explainResults.metrics;
483+
expect(metrics.planSummary).to.not.be.null;
484+
expect(
485+
Object.keys(metrics.planSummary.indexesUsed).length
486+
).to.be.greaterThan(0);
487+
488+
expect(metrics.executionStats).to.not.be.null;
489+
const stats = metrics.executionStats!;
490+
491+
expect(stats.readOperations).to.be.greaterThan(0);
492+
expect(stats.resultsReturned).to.be.equal(5);
493+
expect(
494+
stats.executionDuration.nanoseconds > 0 ||
495+
stats.executionDuration.seconds > 0
496+
).to.be.true;
497+
expect(Object.keys(stats.debugStats).length).to.be.greaterThan(0);
498+
499+
expect(explainResults.snapshot).to.not.be.null;
500+
expect(explainResults.snapshot!.docs.length).to.equal(5);
501+
});
502+
432503
it('getAll() supports array destructuring', () => {
433504
const ref1 = randomCol.doc('doc1');
434505
const ref2 = randomCol.doc('doc2');

0 commit comments

Comments
 (0)