Vector Search Extension
Opt-in KNN similarity search for Firestore, layered onto the core repository without changing its API.
Firestore vector search ships as an opt-in extension at flintfire/vector.
The core package API is unchanged — the standard FirestoreQueryBuilder behaves exactly as before.
Wrap your repository with withVectorSearch() only when you need nearest-neighbor similarity
search.
The
./vectorextension ships as part of v3.
Requirements
Section titled “Requirements”The library always issues the object-form findNearest, which requires
@google-cloud/firestore >= 7.10 — guaranteed transitively by firebase-admin >= 13, and reachable
on firebase-admin 12 only when the resolved @google-cloud/firestore is >= 7.10.
| Capability | Minimum SDK |
|---|---|
findNearest (all vector queries) |
@google-cloud/firestore >= 7.10 (via firebase-admin >= 13) |
distanceResultField, distanceThreshold |
Same floor — @google-cloud/firestore >= 7.10 |
On @google-cloud/firestore 7.6–7.9, assertVectorSearchSupported throws a deterministic >= 7.10
compatibility error rather than a raw SDK argument error.
Vector search requires a vector index on your embedding field. Create indexes via the Firebase
Console, gcloud, or firestore.indexes.json — the ORM does not provision indexes.
There is no vector-construction helper in this library. Build stored and query vectors with the
native FieldValue.vector(...) from firebase-admin/firestore.
Quick start
Section titled “Quick start”import { FirestoreRepository } from 'flintfire';import { withVectorSearch, vectorEmbeddingSchema } from 'flintfire/vector';import { FieldValue } from 'firebase-admin/firestore';import { z } from 'zod';
const articleSchema = z.object({ // No top-level `id` — the repository sources it from the document name. title: z.string(), status: z.enum(['draft', 'published']), embedding: vectorEmbeddingSchema(768).optional(),});
const articleRepo = FirestoreRepository.withSchema(db, 'articles', articleSchema);const vectorArticleRepo = withVectorSearch(articleRepo);
await vectorArticleRepo.create({ title: 'My Article', status: 'published', embedding: FieldValue.vector(embeddingArray),});
const neighbors = await vectorArticleRepo .vectorQuery() .findNearest({ vectorField: 'embedding', queryVector: queryEmbedding, limit: 10, distanceMeasure: 'COSINE', }) .get();The wrapped repository proxies every core repository method — create(), getById(), hooks,
transactions, and query() (still the normal FirestoreQueryBuilder) all work unchanged — and
adds a vectorQuery() entry point returning a VectorQueryBuilder. As with any withSchema
repository, the schema must not declare a top-level id.
Top-level embedding fields (recommended)
Section titled “Top-level embedding fields (recommended)”Store embeddings on a top-level field (for example embedding), not nested under metadata:
// RECOMMENDEDconst recommended = { title: 'Article', embedding: FieldValue.vector(embeddingArray) };
// DISCOURAGED — emulator bugs with nested vector pathsconst discouraged = { title: 'Article', metadata: { embedding: FieldValue.vector(embeddingArray) } };| Concern | Top-level | Nested (metadata.embedding) |
|---|---|---|
| Emulator testing | Reliable | Known issues — may return 0 results |
| Index configuration | Simple fieldPath: "embedding" |
Must match exact nested path |
| Zod ergonomics | embedding: vectorEmbeddingSchema(768) |
Nested sentinel complexity |
The API accepts any string vectorField path, but docs, examples, and tests use top-level fields
only.
Index example
Section titled “Index example”{ "indexes": [ { "collectionGroup": "articles", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "embedding", "vectorConfig": { "dimension": 768, "flat": {} } } ] } ]}The index dimension must match the length of arrays passed to FieldValue.vector().
Pre-filtered search
Section titled “Pre-filtered search”Combine where() pre-filters with findNearest(). This requires a composite vector index that
includes both the filter field(s) and the vector field:
const results = await vectorArticleRepo .vectorQuery() .where('status', '==', 'published') .findNearest({ vectorField: 'embedding', queryVector: queryEmbedding, limit: 5, distanceMeasure: 'EUCLIDEAN', distanceResultField: 'vectorDistance', distanceThreshold: 0.5, }) .get();For a disjunction, use whereFilter() — the same composite AND/OR factory as the core builder
(see Queries):
const results = await vectorArticleRepo .vectorQuery() .whereFilter(f => f.or(f.where('status', '==', 'published'), f.where('status', '==', 'featured'))) .findNearest({ vectorField: 'embedding', queryVector: queryEmbedding, limit: 5, distanceMeasure: 'EUCLIDEAN', }) .get();An OR pre-filter can require index coverage for more than one disjunct branch alongside the vector field, so a composite pre-filter may need several composite vector indexes rather than one.
Call where(), whereFilter(), and select() before findNearest(); all three throw if
invoked after the query has entered vector mode.
Distance measures
Section titled “Distance measures”The distanceMeasure option accepts the string values below (or the corresponding
VectorDistanceMeasure constant, e.g. VectorDistanceMeasure.COSINE):
| Measure | When to use |
|---|---|
DOT_PRODUCT |
Normalized embeddings — fastest, best performance |
COSINE |
Unsure if normalized — safe default (range 0–2, lower = more similar) |
EUCLIDEAN |
When magnitude matters or model was trained with L2 distance |
API reference
Section titled “API reference”All vector exports come from flintfire/vector.
withVectorSearch(repo)
Section titled “withVectorSearch(repo)”Returns a VectorEnabledRepository that proxies all repository methods unchanged (including
query(), which still returns the normal FirestoreQueryBuilder) and adds a vectorQuery()
entry point returning a VectorQueryBuilder.
VectorQueryBuilder
Section titled “VectorQueryBuilder”| Method | Description |
|---|---|
where(field, op, value) |
Pre-filter before vector search |
whereFilter(build) |
Composite AND/OR pre-filter before vector search |
select(...fields) |
Field mask (stored fields only — see note) |
findNearest(options) |
Configure KNN search (required before get()) |
get() |
Execute search and return documents |
getOne() |
Return the nearest single document or null |
select(...) narrows the result type and that projection composes through findNearest(). Pass
only stored document fields to select() — do not list distanceResultField. It is a computed
output field, not a stored one; findNearest() appends it to the result and, when you also use
select(), automatically widens the field mask so the distance survives.
The computed distance field is added to the result type as a number. Prefer a string literal
for distanceResultField (e.g. 'score') for precise typing:
- A literal name is added as a numeric property; if it collides with a model field, it replaces
that field’s type with
number(matching Firestore, which overwrites the stored field with the computed distance). 'id'is rejected at runtime — the repository overlays the document id on every result, which would overwrite the distance.- A non-literal
string(a value from a variable) yields a conservative result type:idstays a string, every other known field becomesT[field] | number(the runtime name may collide with any one), and arbitrary keys areunknown. It never claims every field is numeric.
findNearest(options) takes
{ vectorField, queryVector, limit, distanceMeasure, distanceResultField?, distanceThreshold? }. It
can be called only once per query, limit must be a positive integer no greater than
VECTOR_MAX_LIMIT, and queryVector must be a non-empty array of finite numbers within
VECTOR_MAX_DIMENSIONS. Invalid numeric arguments (queryVector, limit, or
distanceThreshold) throw TypeError.
orderBy(), onSnapshot(), and stream() are not supported on a vector query builder — each
throws. Apply ordering implicitly through findNearest() and pre-filter with where() instead.
After findNearest(), you can call explain(options?) for Admin SDK vector-query diagnostics
(same { metrics, documents } contract as core explain()). There is no vector explainStream in
the Admin SDK. The emulator throws No explain results (no metrics from the emulator).
vectorEmbeddingSchema(dimensions?)
Section titled “vectorEmbeddingSchema(dimensions?)”Throws TypeError at schema-construction time when dimensions is given and is not a positive
integer <= VECTOR_MAX_DIMENSIONS.
Zod helper whose value type is number[] | VectorValueLike — a plain number array or a native
FieldValue.vector(...). It enforces finite components, the exact dimensions length (when given),
and Firestore’s maximum (VECTOR_MAX_DIMENSIONS) on both forms. A forged plain { _values }
object — even with spoofed toArray()/isEqual() methods — is not accepted; only a genuine
vector FieldValue (recognized by nominal instanceof identity) passes.
isVectorFieldValue(value)
Section titled “isVectorFieldValue(value)”Returns true when a value is a genuine Firestore vector FieldValue (the result
of FieldValue.vector(...)), recognized by nominal identity rather than object shape.
Other exports
Section titled “Other exports”Also exported from flintfire/vector:
VectorValueLike— the structural value type accepted byvectorEmbeddingSchema({ toArray(): number[]; isEqual(other): boolean }).QueryExplainResult— the return type ofexplain()(re-exported so/vector-only consumers can name it without importing the main entry).VectorEnabledRepository— the return type ofwithVectorSearch(repo).assertVectorSearchSupported(query)— throws a deterministic>= 7.10compatibility error on an SDK whosefindNearestis absent or positional-only.validateFindNearestOptions(options)and theFindNearestOptions,VectorDistanceMeasureValue, andVectorSearchResulttypes.
Constants
Section titled “Constants”VECTOR_MAX_DIMENSIONS— 2048VECTOR_MAX_LIMIT— 1000VectorDistanceMeasure—{ EUCLIDEAN, COSINE, DOT_PRODUCT }
Limitations
Section titled “Limitations”- No real-time listeners:
onSnapshot(),stream(), andorderBy()throw on vector queries explain()is available afterfindNearest(); there is no vectorexplainStream- Maximum 2048 embedding dimensions
- Maximum 1000 results per query
- Index management is external to the ORM
- Embedding generation is not included — use Vertex AI, OpenAI, or your preferred model
Out of scope
Section titled “Out of scope”- Programmatic index creation
- Embedding model integration
- Emulator workarounds for nested vector field paths