Firestore does not support native full-text search. For prefix search (matching the beginning of a string), use a range query with where('field', '>=', searchTerm) and where('field', '<=', searchTerm + '\uf8ff'). For more complex text search including substring matching, integrate a third-party search service like Algolia or Typesense and sync Firestore data to it via a Cloud Function. The prefix approach works well for autocomplete but cannot find words in the middle of a string.
Searching by Text in Firestore
Firestore is a NoSQL document database optimized for fast reads and real-time sync, but it lacks native full-text search capabilities. You cannot search for a word in the middle of a string or do fuzzy matching natively. This tutorial covers three approaches: prefix search with range queries (built-in, limited), array-based keyword search (moderate effort), and full-text search with external services like Algolia or Typesense (most capable).
Prerequisites
- A Firebase project with Firestore enabled
- Firebase JS SDK v9+ installed (npm install firebase)
- A Firestore collection with text fields you want to search
- For full-text search: an Algolia or Typesense account (free tiers available)
Step-by-step guide
Implement prefix search with range queries
Implement prefix search with range queries
The simplest text search in Firestore uses a range query to match the beginning of a field value. Use where('field', '>=', searchTerm) combined with where('field', '<=', searchTerm + '\uf8ff') to find all documents where the field starts with the search term. The Unicode character \uf8ff is a very high code point that acts as an upper bound. This works for autocomplete scenarios but cannot match substrings.
1import { collection, query, where, orderBy, limit, getDocs } from 'firebase/firestore'23async function searchByPrefix(searchTerm: string) {4 const q = query(5 collection(db, 'products'),6 where('name', '>=', searchTerm),7 where('name', '<=', searchTerm + '\uf8ff'),8 orderBy('name'),9 limit(20)10 )1112 const snapshot = await getDocs(q)13 return snapshot.docs.map((doc) => ({14 id: doc.id,15 ...doc.data()16 }))17}1819// searchByPrefix('App') matches 'Apple', 'Application', 'Apparel'20// searchByPrefix('App') does NOT match 'Snap App' or 'MyApp'Expected result: The query returns all products whose name starts with the search term, ordered alphabetically.
Add case-insensitive prefix search with a lowercase field
Add case-insensitive prefix search with a lowercase field
Since Firestore string comparisons are case-sensitive, store a lowercase copy of any field you want to search. Create this field when the document is written (or use a Cloud Function to generate it automatically). Then run prefix queries against the lowercase field while converting the search term to lowercase.
1import { collection, query, where, orderBy, limit, getDocs, addDoc } from 'firebase/firestore'23// When creating/updating a document, store a lowercase copy4async function createProduct(name: string, category: string) {5 await addDoc(collection(db, 'products'), {6 name: name,7 name_lower: name.toLowerCase(), // Lowercase copy for search8 category: category9 })10}1112// Search against the lowercase field13async function searchCaseInsensitive(searchTerm: string) {14 const term = searchTerm.toLowerCase()1516 const q = query(17 collection(db, 'products'),18 where('name_lower', '>=', term),19 where('name_lower', '<=', term + '\uf8ff'),20 orderBy('name_lower'),21 limit(20)22 )2324 const snapshot = await getDocs(q)25 return snapshot.docs.map((doc) => ({26 id: doc.id,27 ...doc.data()28 }))29}Expected result: Searching for 'app' now matches 'Apple', 'APPLICATION', and 'Apparel' regardless of case.
Implement keyword search with array-contains
Implement keyword search with array-contains
For searching multiple words within a field, split the text into individual keywords and store them in an array field. Use array-contains to match any single keyword. This approach finds documents containing a specific word anywhere in the text, not just at the beginning. The limitation is that array-contains only matches exact array elements — it does not support partial word matching.
1import { collection, query, where, getDocs, addDoc } from 'firebase/firestore'23function generateKeywords(text: string): string[] {4 return text5 .toLowerCase()6 .split(/\s+/) // Split by whitespace7 .filter((word) => word.length > 1) // Remove single chars8 .filter((v, i, a) => a.indexOf(v) === i) // Remove duplicates9}1011// Store keywords when creating a document12async function createArticle(title: string, body: string) {13 const keywords = generateKeywords(`${title} ${body}`)1415 await addDoc(collection(db, 'articles'), {16 title,17 body,18 keywords // ['firebase', 'search', 'tutorial', ...]19 })20}2122// Search by keyword23async function searchByKeyword(keyword: string) {24 const q = query(25 collection(db, 'articles'),26 where('keywords', 'array-contains', keyword.toLowerCase())27 )2829 const snapshot = await getDocs(q)30 return snapshot.docs.map((doc) => ({31 id: doc.id,32 ...doc.data()33 }))34}Expected result: Searching for 'firebase' finds all articles containing that word anywhere in the title or body.
Set up full-text search with Algolia
Set up full-text search with Algolia
For production-grade search with typo tolerance, ranking, faceting, and substring matching, integrate a dedicated search service. Algolia is Firebase's recommended option. The pattern: sync Firestore data to Algolia using a Cloud Function trigger, then query Algolia from the client. Firebase offers an official Algolia extension to automate the sync.
1// Cloud Function: sync Firestore to Algolia on document write2import { onDocumentWritten } from 'firebase-functions/v2/firestore'3import algoliasearch from 'algoliasearch'4import { defineSecret } from 'firebase-functions/params'56const algoliaAppId = defineSecret('ALGOLIA_APP_ID')7const algoliaApiKey = defineSecret('ALGOLIA_ADMIN_KEY')89export const syncToAlgolia = onDocumentWritten(10 {11 document: 'products/{productId}',12 secrets: [algoliaAppId, algoliaApiKey]13 },14 async (event) => {15 const client = algoliasearch(16 algoliaAppId.value(),17 algoliaApiKey.value()18 )19 const index = client.initIndex('products')2021 if (!event.data?.after.exists) {22 // Document deleted: remove from Algolia23 await index.deleteObject(event.params.productId)24 return25 }2627 // Document created/updated: sync to Algolia28 const data = event.data.after.data()29 await index.saveObject({30 objectID: event.params.productId,31 ...data32 })33 }34)Expected result: Every Firestore document change is automatically synced to Algolia. Client-side searches query Algolia for instant, typo-tolerant results.
Query Algolia from the client side
Query Algolia from the client side
Install the Algolia client library in your frontend and query the search index directly. Use the search-only API key (not the admin key) for client-side queries. Algolia returns results ranked by relevance with typo tolerance built in. Display the results in your UI, linking back to the Firestore document ID stored as objectID.
1import algoliasearch from 'algoliasearch/lite'23const searchClient = algoliasearch(4 'YOUR_ALGOLIA_APP_ID',5 'YOUR_SEARCH_ONLY_API_KEY' // Safe for client-side use6)78const index = searchClient.initIndex('products')910async function search(query: string) {11 const { hits } = await index.search(query, {12 hitsPerPage: 20,13 attributesToRetrieve: ['name', 'category', 'price']14 })1516 return hits.map((hit: any) => ({17 firestoreId: hit.objectID,18 name: hit.name,19 category: hit.category,20 price: hit.price,21 // hit._highlightResult contains highlighted matches22 }))23}2425// search('iphn') matches 'iPhone' (typo tolerance)26// search('wireless') matches 'Wireless Headphones' (substring match)Expected result: Algolia returns relevant results with typo tolerance and substring matching, far beyond what Firestore's native queries can do.
Complete working example
1import { initializeApp } from 'firebase/app'2import {3 getFirestore,4 collection,5 query,6 where,7 orderBy,8 limit,9 getDocs,10 addDoc11} from 'firebase/firestore'1213const app = initializeApp({14 apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,15 authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,16 projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!17})1819const db = getFirestore(app)2021// Prefix search (case-insensitive)22export async function prefixSearch(23 collectionName: string,24 field: string,25 searchTerm: string,26 maxResults: number = 2027) {28 const term = searchTerm.toLowerCase()29 const fieldLower = `${field}_lower`3031 const q = query(32 collection(db, collectionName),33 where(fieldLower, '>=', term),34 where(fieldLower, '<=', term + '\uf8ff'),35 orderBy(fieldLower),36 limit(maxResults)37 )3839 const snapshot = await getDocs(q)40 return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }))41}4243// Keyword search using array-contains44export async function keywordSearch(45 collectionName: string,46 keyword: string47) {48 const q = query(49 collection(db, collectionName),50 where('keywords', 'array-contains', keyword.toLowerCase())51 )5253 const snapshot = await getDocs(q)54 return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }))55}5657// Helper: generate keywords for a document58export function generateKeywords(text: string): string[] {59 return text60 .toLowerCase()61 .replace(/[^a-z0-9\s]/g, '')62 .split(/\s+/)63 .filter((word) => word.length > 1)64 .filter((v, i, a) => a.indexOf(v) === i)65}6667// Helper: create a searchable document68export async function createSearchableDoc(69 collectionName: string,70 data: Record<string, any>,71 searchFields: string[]72) {73 const searchText = searchFields.map((f) => data[f] || '').join(' ')74 const keywords = generateKeywords(searchText)7576 const searchableData: Record<string, any> = { ...data, keywords }77 for (const field of searchFields) {78 if (typeof data[field] === 'string') {79 searchableData[`${field}_lower`] = data[field].toLowerCase()80 }81 }8283 return addDoc(collection(db, collectionName), searchableData)84}Common mistakes when searching by Text in Firestore
Why it's a problem: Expecting Firestore where() to support substring matching like SQL LIKE '%term%'
How to avoid: Firestore does not support substring search. The >= and <= range query only matches prefixes. For substring or fuzzy matching, use a dedicated search service like Algolia or Typesense.
Why it's a problem: Running prefix search without a lowercase field, getting case-sensitive results
How to avoid: Store a lowercase copy of the field (e.g., name_lower) when creating the document. Convert the search term to lowercase before querying.
Why it's a problem: Using array-contains-any with too many keywords, expecting AND logic instead of OR
How to avoid: array-contains checks for a single value. array-contains-any matches any one of up to 30 values (OR logic). Firestore does not support AND across array elements in a single query.
Best practices
- Use prefix search for simple autocomplete where matching the beginning of a field is sufficient
- Store lowercase copies of text fields at write time to enable case-insensitive search without runtime overhead
- Use the keywords array pattern for basic word-level search within documents
- Integrate Algolia or Typesense for production search with typo tolerance, ranking, and faceting
- Sync Firestore to the search index using Cloud Functions or the official Firebase Extension for automatic indexing
- Keep keyword arrays under 200 elements per document to avoid document size limits
- Use Algolia's search-only API key on the client and the admin API key only in Cloud Functions
- Consider Firebase Data Connect if you need native full-text search — it uses PostgreSQL under the hood
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a Firestore collection of products with name and description fields. Show me how to implement text search three ways: prefix search with range queries, keyword search with array-contains, and full-text search using Algolia with a Cloud Function to sync data. Include case-insensitive handling and TypeScript types.
Create a search module for a Firestore-based app that supports both prefix search using range queries and keyword search using array-contains. Include helper functions for generating keyword arrays, creating searchable documents with lowercase fields, and querying with case-insensitive prefix matching. Use Firebase modular SDK v9+ with TypeScript.
Frequently asked questions
Does Firestore support full-text search natively?
No. Firestore is a NoSQL document database that supports equality, range, and array membership queries, but not full-text search with substring matching, fuzzy matching, or relevance ranking. Firebase recommends using Algolia, Typesense, or Elastic for full-text search.
Which search service works best with Firebase?
Algolia is Firebase's recommended search solution and has an official Firebase Extension for automatic sync. Typesense is a popular open-source alternative that can be self-hosted. Both offer generous free tiers. Algolia has the best search quality; Typesense has better pricing at scale.
How much does Algolia cost for a small app?
Algolia offers a free tier with 10,000 search requests per month and 10,000 records. The Build plan starts at $0 per month with usage-based pricing. For most small apps, the free tier is sufficient.
Can I search across multiple fields at once in Firestore?
Not with a single query. Firestore queries operate on individual fields. To search across multiple fields, either merge them into a combined keywords array at write time, or use a dedicated search service that indexes multiple fields.
What about Firebase Data Connect for search?
Firebase Data Connect (GA April 2025) uses PostgreSQL under the hood and supports native full-text search via SQL text search functions. If you need full-text search without a third-party service, Data Connect is worth considering, though it requires a different data architecture than Firestore.
Can RapidDev help implement search functionality in my Firebase app?
Yes. RapidDev can set up search infrastructure including Algolia or Typesense integration, Cloud Function sync pipelines, and search UI components tailored to your data and search requirements.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation