# How to Search by Text in Firestore

- Tool: Firebase
- Difficulty: Intermediate
- Time required: 15-20 min
- Compatibility: Firebase JS SDK v9+, Firestore (all plans), Algolia or Typesense (optional)
- Last updated: March 2026

## 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).

## Before you start

- 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.

```
import { collection, query, where, orderBy, limit, getDocs } from 'firebase/firestore'

async function searchByPrefix(searchTerm: string) {
  const q = query(
    collection(db, 'products'),
    where('name', '>=', searchTerm),
    where('name', '<=', searchTerm + '\uf8ff'),
    orderBy('name'),
    limit(20)
  )

  const snapshot = await getDocs(q)
  return snapshot.docs.map((doc) => ({
    id: doc.id,
    ...doc.data()
  }))
}

// searchByPrefix('App') matches 'Apple', 'Application', 'Apparel'
// 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.

```
import { collection, query, where, orderBy, limit, getDocs, addDoc } from 'firebase/firestore'

// When creating/updating a document, store a lowercase copy
async function createProduct(name: string, category: string) {
  await addDoc(collection(db, 'products'), {
    name: name,
    name_lower: name.toLowerCase(), // Lowercase copy for search
    category: category
  })
}

// Search against the lowercase field
async function searchCaseInsensitive(searchTerm: string) {
  const term = searchTerm.toLowerCase()

  const q = query(
    collection(db, 'products'),
    where('name_lower', '>=', term),
    where('name_lower', '<=', term + '\uf8ff'),
    orderBy('name_lower'),
    limit(20)
  )

  const snapshot = await getDocs(q)
  return snapshot.docs.map((doc) => ({
    id: doc.id,
    ...doc.data()
  }))
}
```

**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.

```
import { collection, query, where, getDocs, addDoc } from 'firebase/firestore'

function generateKeywords(text: string): string[] {
  return text
    .toLowerCase()
    .split(/\s+/) // Split by whitespace
    .filter((word) => word.length > 1) // Remove single chars
    .filter((v, i, a) => a.indexOf(v) === i) // Remove duplicates
}

// Store keywords when creating a document
async function createArticle(title: string, body: string) {
  const keywords = generateKeywords(`${title} ${body}`)

  await addDoc(collection(db, 'articles'), {
    title,
    body,
    keywords // ['firebase', 'search', 'tutorial', ...]
  })
}

// Search by keyword
async function searchByKeyword(keyword: string) {
  const q = query(
    collection(db, 'articles'),
    where('keywords', 'array-contains', keyword.toLowerCase())
  )

  const snapshot = await getDocs(q)
  return snapshot.docs.map((doc) => ({
    id: doc.id,
    ...doc.data()
  }))
}
```

**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.

```
// Cloud Function: sync Firestore to Algolia on document write
import { onDocumentWritten } from 'firebase-functions/v2/firestore'
import algoliasearch from 'algoliasearch'
import { defineSecret } from 'firebase-functions/params'

const algoliaAppId = defineSecret('ALGOLIA_APP_ID')
const algoliaApiKey = defineSecret('ALGOLIA_ADMIN_KEY')

export const syncToAlgolia = onDocumentWritten(
  {
    document: 'products/{productId}',
    secrets: [algoliaAppId, algoliaApiKey]
  },
  async (event) => {
    const client = algoliasearch(
      algoliaAppId.value(),
      algoliaApiKey.value()
    )
    const index = client.initIndex('products')

    if (!event.data?.after.exists) {
      // Document deleted: remove from Algolia
      await index.deleteObject(event.params.productId)
      return
    }

    // Document created/updated: sync to Algolia
    const data = event.data.after.data()
    await index.saveObject({
      objectID: event.params.productId,
      ...data
    })
  }
)
```

**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.

```
import algoliasearch from 'algoliasearch/lite'

const searchClient = algoliasearch(
  'YOUR_ALGOLIA_APP_ID',
  'YOUR_SEARCH_ONLY_API_KEY' // Safe for client-side use
)

const index = searchClient.initIndex('products')

async function search(query: string) {
  const { hits } = await index.search(query, {
    hitsPerPage: 20,
    attributesToRetrieve: ['name', 'category', 'price']
  })

  return hits.map((hit: any) => ({
    firestoreId: hit.objectID,
    name: hit.name,
    category: hit.category,
    price: hit.price,
    // hit._highlightResult contains highlighted matches
  }))
}

// search('iphn') matches 'iPhone' (typo tolerance)
// 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 code example

File: `firestore-search.ts`

```typescript
import { initializeApp } from 'firebase/app'
import {
  getFirestore,
  collection,
  query,
  where,
  orderBy,
  limit,
  getDocs,
  addDoc
} from 'firebase/firestore'

const app = initializeApp({
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!
})

const db = getFirestore(app)

// Prefix search (case-insensitive)
export async function prefixSearch(
  collectionName: string,
  field: string,
  searchTerm: string,
  maxResults: number = 20
) {
  const term = searchTerm.toLowerCase()
  const fieldLower = `${field}_lower`

  const q = query(
    collection(db, collectionName),
    where(fieldLower, '>=', term),
    where(fieldLower, '<=', term + '\uf8ff'),
    orderBy(fieldLower),
    limit(maxResults)
  )

  const snapshot = await getDocs(q)
  return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }))
}

// Keyword search using array-contains
export async function keywordSearch(
  collectionName: string,
  keyword: string
) {
  const q = query(
    collection(db, collectionName),
    where('keywords', 'array-contains', keyword.toLowerCase())
  )

  const snapshot = await getDocs(q)
  return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }))
}

// Helper: generate keywords for a document
export function generateKeywords(text: string): string[] {
  return text
    .toLowerCase()
    .replace(/[^a-z0-9\s]/g, '')
    .split(/\s+/)
    .filter((word) => word.length > 1)
    .filter((v, i, a) => a.indexOf(v) === i)
}

// Helper: create a searchable document
export async function createSearchableDoc(
  collectionName: string,
  data: Record<string, any>,
  searchFields: string[]
) {
  const searchText = searchFields.map((f) => data[f] || '').join(' ')
  const keywords = generateKeywords(searchText)

  const searchableData: Record<string, any> = { ...data, keywords }
  for (const field of searchFields) {
    if (typeof data[field] === 'string') {
      searchableData[`${field}_lower`] = data[field].toLowerCase()
    }
  }

  return addDoc(collection(db, collectionName), searchableData)
}
```

## Common mistakes

- **Expecting Firestore where() to support substring matching like SQL LIKE '%term%'** — undefined Fix: 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.
- **Running prefix search without a lowercase field, getting case-sensitive results** — undefined Fix: Store a lowercase copy of the field (e.g., name_lower) when creating the document. Convert the search term to lowercase before querying.
- **Using array-contains-any with too many keywords, expecting AND logic instead of OR** — undefined Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/firebase-tutorial/how-to-search-by-text-in-firestore
© RapidDev — https://www.rapidevelopers.com/firebase-tutorial/how-to-search-by-text-in-firestore
