# How to Implement AI-Powered Search in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 45-60 min
- Compatibility: FlutterFlow Pro+ (code export required for custom functions)
- Last updated: March 2026

## TL;DR

AI-powered search uses vector embeddings to understand natural language queries instead of matching exact keywords. You create a Cloud Function that calls OpenAI's Embeddings API, stores vectors in Supabase pgvector, and queries by cosine similarity. Users can search 'affordable red shoes' and find results even if your data says 'budget crimson sneakers'.

## Why keyword search fails — and how AI fixes it

Standard Firestore queries match exact text. If a user types 'cheap hotel near airport' but your data says 'budget accommodation close to terminal', nothing matches. AI-powered search converts both the query and your documents into high-dimensional vectors — numerical representations of meaning. A similarity search finds vectors that are close in meaning, not just identical in spelling. The architecture has two parts: an indexing pipeline that runs once per document (embed → store vector) and a search pipeline that runs per query (embed query → find nearest vectors → return ranked results). Because you only embed the query once per search, API costs stay low.

## Before you start

- FlutterFlow project with a Supabase backend connected
- OpenAI account with an API key (platform.openai.com)
- Supabase project with pgvector extension enabled (Database → Extensions → vector)
- Basic understanding of FlutterFlow Custom Actions
- Firebase or Supabase project with Edge Functions or Cloud Functions enabled

## Step-by-step guide

### 1. Enable pgvector and create the documents table in Supabase

Open your Supabase project dashboard and navigate to Database → Extensions. Search for 'vector' and enable the pgvector extension — this adds vector similarity search to PostgreSQL. Next, open the SQL editor and create a table to store your documents and their embeddings. The vector(1536) column matches the output dimension of OpenAI's text-embedding-ada-002 model. Run the SQL below. After running, go to Table Editor to confirm the documents table appears with an embedding column.

```
-- Enable pgvector (if not already done via UI)
create extension if not exists vector;

-- Documents table with embedding column
create table documents (
  id uuid primary key default gen_random_uuid(),
  title text not null,
  body text not null,
  embedding vector(1536),
  created_at timestamptz default now()
);

-- Index for fast similarity search
create index on documents
using ivfflat (embedding vector_cosine_ops)
with (lists = 100);

-- RLS: allow authenticated reads
alter table documents enable row level security;
create policy "Public read" on documents
  for select using (true);
```

**Expected result:** A 'documents' table visible in Supabase Table Editor with id, title, body, embedding, and created_at columns.

### 2. Create a Supabase Edge Function to embed and index documents

This function runs once when you add a new document. It calls the OpenAI Embeddings API to convert the document text into a 1536-dimension vector, then stores it alongside the document in Supabase. In your Supabase dashboard, go to Edge Functions → Create a new function named 'embed-document'. Paste the code below. Set your OpenAI API key as a Supabase Secret: Project Settings → API → Secrets → Add new secret with key OPENAI_API_KEY. Deploy the function by clicking Save.

```
// supabase/functions/embed-document/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';

serve(async (req) => {
  const { title, body } = await req.json();

  // Get embedding from OpenAI
  const embeddingRes = await fetch('https://api.openai.com/v1/embeddings', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      input: `${title} ${body}`,
      model: 'text-embedding-ada-002',
    }),
  });

  const { data } = await embeddingRes.json();
  const embedding = data[0].embedding;

  // Store in Supabase
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  );

  const { error } = await supabase
    .from('documents')
    .insert({ title, body, embedding });

  if (error) return new Response(JSON.stringify({ error }), { status: 500 });
  return new Response(JSON.stringify({ success: true }), { status: 200 });
});
```

**Expected result:** Edge Function deployed and visible in Supabase dashboard. Test it via the 'Invoke' button with a sample payload like {"title": "Red sneakers", "body": "Lightweight running shoes in red"}.

### 3. Create a search Edge Function that embeds the query and returns ranked results

Create a second Edge Function named 'semantic-search'. This one accepts a user's search query, converts it to a vector using OpenAI, then calls a Supabase RPC function to find the most similar documents by cosine similarity. First, add the RPC function in your SQL editor. Then create the Edge Function. This separation means you pay for one embedding per search, not per document returned.

```
-- First, add this SQL function in Supabase SQL editor:
create or replace function match_documents(
  query_embedding vector(1536),
  match_count int default 5
)
returns table (id uuid, title text, body text, similarity float)
language sql stable as $$
  select id, title, body,
    1 - (embedding <=> query_embedding) as similarity
  from documents
  order by embedding <=> query_embedding
  limit match_count;
$$;

-- Edge Function: supabase/functions/semantic-search/index.ts
// (same imports as embed-document)
serve(async (req) => {
  const { query } = await req.json();

  const embeddingRes = await fetch('https://api.openai.com/v1/embeddings', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ input: query, model: 'text-embedding-ada-002' }),
  });

  const { data } = await embeddingRes.json();
  const queryEmbedding = data[0].embedding;

  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  );

  const { data: results, error } = await supabase
    .rpc('match_documents', {
      query_embedding: queryEmbedding,
      match_count: 10,
    });

  if (error) return new Response(JSON.stringify({ error }), { status: 500 });
  return new Response(JSON.stringify({ results }), { status: 200 });
});
```

**Expected result:** Invoking the semantic-search function with {"query": "affordable shoes"} returns a ranked JSON array of documents ordered by relevance.

### 4. Add the API call as a Custom Action in FlutterFlow

In FlutterFlow, go to Custom Code → Custom Actions → Add Action. Name it 'semanticSearch'. This action will call your semantic-search Edge Function with the user's query text and return a list of result maps. Go to Settings → Custom Files and add your Supabase URL and anon key as app constants if you haven't already. The Custom Action uses the http package which is available by default. Paste the Dart code below and click Save. FlutterFlow will validate the code and show a green checkmark when it compiles.

```
// Custom Action: semanticSearch
import 'dart:convert';
import 'package:http/http.dart' as http;

Future<List<dynamic>> semanticSearch(String query) async {
  const supabaseUrl = 'https://YOUR_PROJECT.supabase.co';
  const anonKey = 'YOUR_ANON_KEY';

  final response = await http.post(
    Uri.parse('$supabaseUrl/functions/v1/semantic-search'),
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer $anonKey',
    },
    body: jsonEncode({'query': query}),
  );

  if (response.statusCode != 200) return [];
  final data = jsonDecode(response.body);
  return data['results'] as List<dynamic>;
}
```

**Expected result:** Custom Action appears in FlutterFlow with return type List<dynamic>. No compilation errors shown in the editor.

### 5. Build the search UI with a TextField and ListView in FlutterFlow

On your search page, add a Column widget. Inside it, add a TextField widget for the search input — set its label to 'Search...' and give it a unique widget name like 'searchField'. Below it, add a ListView widget. Set the ListView's data source to an App State variable named 'searchResults' (type: JSON, list: true). Add a Card widget inside the ListView with two Text widgets: one bound to currentItem.title and one to currentItem.body. Now add the search trigger: select the TextField → Actions → On Submit → Custom Action → semanticSearch, passing the TextField's value. In the Action output, update the searchResults App State variable with the returned list.

**Expected result:** A search bar and results list are visible on the page. Typing a query and pressing Enter triggers the action flow.

### 6. Test the full search flow in Run Mode

Click Run Mode (top right) to launch your app in the browser. First, populate some test documents by calling your embed-document Edge Function directly from the Supabase dashboard Invoke panel with a few sample records. Then return to your app's search page, type a natural language query like 'how do I reset my password', and submit. You should see semantically relevant results appear within 1-2 seconds. Test edge cases: misspelled words, synonyms, and queries in different phrasing than your source documents. All should return relevant results if the embeddings are working correctly.

**Expected result:** Natural language queries return a ranked list of semantically relevant documents, even when exact keywords don't match.

## Complete code example

File: `semantic_search_action.dart`

```dart
// FlutterFlow Custom Action: semanticSearch
// Dependencies: http (already included in FlutterFlow)
import 'dart:convert';
import 'package:http/http.dart' as http;

/// Sends a natural-language query to the Supabase semantic-search
/// Edge Function and returns a ranked list of matching documents.
///
/// Each result map contains: id, title, body, similarity (0-1 float)
Future<List<dynamic>> semanticSearch(String query) async {
  // Replace with your actual Supabase project URL and anon key
  const String supabaseUrl = 'https://YOUR_PROJECT_ID.supabase.co';
  const String anonKey = 'YOUR_SUPABASE_ANON_KEY';
  const String functionUrl = '$supabaseUrl/functions/v1/semantic-search';

  if (query.trim().isEmpty) return [];

  try {
    final response = await http
        .post(
          Uri.parse(functionUrl),
          headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer $anonKey',
          },
          body: jsonEncode({'query': query.trim()}),
        )
        .timeout(const Duration(seconds: 10));

    if (response.statusCode != 200) {
      debugPrint('Search error: ${response.statusCode} ${response.body}');
      return [];
    }

    final Map<String, dynamic> data = jsonDecode(response.body);
    final List<dynamic> results = data['results'] ?? [];

    // Filter out low-confidence results
    return results
        .where((r) => (r['similarity'] as double? ?? 0) > 0.70)
        .toList();
  } catch (e) {
    debugPrint('semanticSearch exception: $e');
    return [];
  }
}
```

## Common mistakes

- **Re-computing embeddings for every search query without caching** — Each embedding call to OpenAI costs tokens. If the same query is run repeatedly (e.g., autocomplete on every keystroke), costs multiply fast and response times degrade. Fix: Debounce the search input by at least 500ms. Cache results in App State for the same query string. Only call the API when the query changes.
- **Calling the Embeddings API directly from the Flutter app** — Your OpenAI API key would be exposed in the client app, visible to anyone who intercepts network traffic. Fix: Always route embedding calls through a server-side function (Supabase Edge Function or Firebase Cloud Function) where the API key is stored as a secret environment variable.
- **Skipping the ivfflat index on the embedding column** — Without an index, every similarity search does a full table scan. At 10,000 documents this becomes noticeably slow; at 100,000 it becomes unusable. Fix: Add the ivfflat index as shown in Step 1 before loading production data. Rebuild the index after bulk inserts with REINDEX INDEX.
- **Using the wrong embedding model dimension** — text-embedding-ada-002 outputs 1536 dimensions. If you define the vector column as vector(768) or vector(3072) it will reject insertions or silently truncate. Fix: Match the vector() size to the model: ada-002 = 1536, text-embedding-3-small = 1536, text-embedding-3-large = 3072. Define the column size before inserting any data.

## Best practices

- Index documents at write time, never at read time — keep the search path fast and cheap
- Combine semantic search with a keyword filter for precision: semantic for intent, keyword for exact product codes
- Store the original text alongside the embedding so you can display results without a second query
- Set a minimum similarity threshold (0.70-0.75) to avoid surfacing irrelevant results for obscure queries
- Log search queries and zero-result searches to identify gaps in your content
- Batch-embed new documents using a queue rather than one API call per insert under high write load
- Use text-embedding-3-small for cost savings — it performs comparably to ada-002 at lower cost

## Frequently asked questions

### How much does AI search cost with OpenAI Embeddings?

text-embedding-ada-002 costs $0.0001 per 1,000 tokens. A typical search query is about 10 tokens, so 10,000 searches cost roughly $0.01. Indexing a document of 200 words costs about $0.003. Costs are very low unless you have millions of daily searches.

### Can I use this with Firebase instead of Supabase?

Yes. Replace the pgvector storage with Pinecone or Google Cloud Vertex AI Vector Search. Use a Firebase Cloud Function instead of a Supabase Edge Function. The embedding step (calling OpenAI) is identical — only the vector storage and retrieval layer changes.

### What is the difference between semantic search and keyword search?

Keyword search matches exact words. Semantic search understands meaning — 'car' and 'automobile' are treated as similar. Semantic search handles synonyms, paraphrasing, and intent. It performs better for natural language queries but is slower and costs more than a simple Firestore query.

### Do I need the Pro plan to use Custom Actions in FlutterFlow?

Yes. Custom Actions (which run Dart code) require the FlutterFlow Pro plan ($70/mo). On the Free and Standard plans you can use API calls via the API Manager, but you cannot write custom Dart logic.

### How do I keep the search index up to date when documents change?

Trigger the embed-document Edge Function from a Firestore/Supabase trigger whenever a document is created or updated. For Supabase, use a database webhook on the documents table INSERT and UPDATE events pointing to your Edge Function.

### What happens if OpenAI is down — will search break completely?

Yes, if OpenAI's Embeddings API is unavailable, new searches will fail. Add a fallback: if the Edge Function returns an error, fall back to a standard Supabase full-text search (using the @@ operator) so users always get some results.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-ai-powered-search-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-ai-powered-search-in-flutterflow
