Skip to main content
RapidDev - Software Development Agency
firebase-tutorial

How to Search by Text in Firestore

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.

What you'll learn

  • How to implement prefix search in Firestore using range queries
  • How to store lowercase copies of fields for case-insensitive search
  • How to set up full-text search with Algolia or Typesense
  • How to sync Firestore data to a search index using Cloud Functions
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate9 min read15-20 minFirebase JS SDK v9+, Firestore (all plans), Algolia or Typesense (optional)March 2026RapidDev Engineering Team
TL;DR

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

1

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.

typescript
1import { collection, query, where, orderBy, limit, getDocs } from 'firebase/firestore'
2
3async 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 )
11
12 const snapshot = await getDocs(q)
13 return snapshot.docs.map((doc) => ({
14 id: doc.id,
15 ...doc.data()
16 }))
17}
18
19// 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.

2

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.

typescript
1import { collection, query, where, orderBy, limit, getDocs, addDoc } from 'firebase/firestore'
2
3// When creating/updating a document, store a lowercase copy
4async function createProduct(name: string, category: string) {
5 await addDoc(collection(db, 'products'), {
6 name: name,
7 name_lower: name.toLowerCase(), // Lowercase copy for search
8 category: category
9 })
10}
11
12// Search against the lowercase field
13async function searchCaseInsensitive(searchTerm: string) {
14 const term = searchTerm.toLowerCase()
15
16 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 )
23
24 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.

3

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.

typescript
1import { collection, query, where, getDocs, addDoc } from 'firebase/firestore'
2
3function generateKeywords(text: string): string[] {
4 return text
5 .toLowerCase()
6 .split(/\s+/) // Split by whitespace
7 .filter((word) => word.length > 1) // Remove single chars
8 .filter((v, i, a) => a.indexOf(v) === i) // Remove duplicates
9}
10
11// Store keywords when creating a document
12async function createArticle(title: string, body: string) {
13 const keywords = generateKeywords(`${title} ${body}`)
14
15 await addDoc(collection(db, 'articles'), {
16 title,
17 body,
18 keywords // ['firebase', 'search', 'tutorial', ...]
19 })
20}
21
22// Search by keyword
23async function searchByKeyword(keyword: string) {
24 const q = query(
25 collection(db, 'articles'),
26 where('keywords', 'array-contains', keyword.toLowerCase())
27 )
28
29 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.

4

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.

typescript
1// Cloud Function: sync Firestore to Algolia on document write
2import { onDocumentWritten } from 'firebase-functions/v2/firestore'
3import algoliasearch from 'algoliasearch'
4import { defineSecret } from 'firebase-functions/params'
5
6const algoliaAppId = defineSecret('ALGOLIA_APP_ID')
7const algoliaApiKey = defineSecret('ALGOLIA_ADMIN_KEY')
8
9export 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')
20
21 if (!event.data?.after.exists) {
22 // Document deleted: remove from Algolia
23 await index.deleteObject(event.params.productId)
24 return
25 }
26
27 // Document created/updated: sync to Algolia
28 const data = event.data.after.data()
29 await index.saveObject({
30 objectID: event.params.productId,
31 ...data
32 })
33 }
34)

Expected result: Every Firestore document change is automatically synced to Algolia. Client-side searches query Algolia for instant, typo-tolerant results.

5

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.

typescript
1import algoliasearch from 'algoliasearch/lite'
2
3const searchClient = algoliasearch(
4 'YOUR_ALGOLIA_APP_ID',
5 'YOUR_SEARCH_ONLY_API_KEY' // Safe for client-side use
6)
7
8const index = searchClient.initIndex('products')
9
10async function search(query: string) {
11 const { hits } = await index.search(query, {
12 hitsPerPage: 20,
13 attributesToRetrieve: ['name', 'category', 'price']
14 })
15
16 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 matches
22 }))
23}
24
25// 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

firestore-search.ts
1import { initializeApp } from 'firebase/app'
2import {
3 getFirestore,
4 collection,
5 query,
6 where,
7 orderBy,
8 limit,
9 getDocs,
10 addDoc
11} from 'firebase/firestore'
12
13const 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})
18
19const db = getFirestore(app)
20
21// Prefix search (case-insensitive)
22export async function prefixSearch(
23 collectionName: string,
24 field: string,
25 searchTerm: string,
26 maxResults: number = 20
27) {
28 const term = searchTerm.toLowerCase()
29 const fieldLower = `${field}_lower`
30
31 const q = query(
32 collection(db, collectionName),
33 where(fieldLower, '>=', term),
34 where(fieldLower, '<=', term + '\uf8ff'),
35 orderBy(fieldLower),
36 limit(maxResults)
37 )
38
39 const snapshot = await getDocs(q)
40 return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }))
41}
42
43// Keyword search using array-contains
44export async function keywordSearch(
45 collectionName: string,
46 keyword: string
47) {
48 const q = query(
49 collection(db, collectionName),
50 where('keywords', 'array-contains', keyword.toLowerCase())
51 )
52
53 const snapshot = await getDocs(q)
54 return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }))
55}
56
57// Helper: generate keywords for a document
58export function generateKeywords(text: string): string[] {
59 return text
60 .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}
66
67// Helper: create a searchable document
68export 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)
75
76 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 }
82
83 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.

ChatGPT Prompt

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.

Firebase Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.