MiniSearch Integration with Reflex

Use MiniSearch for lightweight, client-side fuzzy search inside your Reflex web apps. This guide shows how to integrate it using CDN, connect it to Reflex state, and build a responsive search UI with results.

Overview

This guide demonstrates integrating MiniSearch fuzzy search into a Reflex application using client-side state management and custom JavaScript.

Project Structure

You need three files:

  • app.py - Main Reflex application
  • assets/list.json - Search data
  • assets/minisearch.js - Search logic

Data Format

Your list.json should contain an array of searchable objects:

[
  {
    "title": "Battle for Natural",
    "author": {
      "firstName": "Emily",
      "lastName": "Cain"
    }
  }
]

Search Script Setup

The minisearch.js file initializes MiniSearch and exposes search functions globally:

let miniSearch;

// Initialize Minisearch with the loaded data
window.initializeMinisearch = function (data) {
  console.log("Initializing Minisearch with data:", data);

  // Create a new MiniSearch instance
  miniSearch = new MiniSearch({
    fields: ["title", "author.firstName", "author.lastName"], // fields to index for full-text search
    storeFields: ["title", "author"], // fields to return with search results
    searchOptions: {
      boost: { title: 2 }, // boost title matches
      fuzzy: 0.2, // enable fuzzy matching
      prefix: true, // enable prefix matching
      combineWith: "AND", // combine search terms with AND
    },
  });

  // Add IDs to documents if they don't have them
  const dataWithIds = data.map((item, index) => ({
    id: item.id || index, // Use existing ID or assign index as ID
    ...item,
  }));

  // Add all documents to the index
  miniSearch.addAll(dataWithIds);

  console.log("Minisearch initialized successfully");
};

// Search function that returns results in the same format as Fuse.js
window.searchMinisearch = function (searchQuery) {
  if (!miniSearch) {
    console.warn("Minisearch not initialized yet");
    return [];
  }

  if (!searchQuery || searchQuery.trim() === "") {
    return [];
  }

  console.log("Searching for:", searchQuery);

  try {
    // Perform the search
    const results = miniSearch.search(searchQuery, {
      fuzzy: 0.2,
      prefix: true,
      boost: { title: 2 },
    });

    console.log("Raw Minisearch results:", results);

    // Transform results to match Fuse.js format
    const transformedResults = results.map((result, index) => ({
      item: {
        title: result.title,
        author: result.author,
      },
      refIndex: result.id || index,
      score: 1 - result.score, // Invert score to match Fuse.js format (lower is better in Fuse)
    }));

    console.log("Transformed results:", transformedResults);
    return transformedResults;
  } catch (error) {
    console.error("Search error:", error);
    return [];
  }
};

Client State Management

Define client-side state variables for reactive updates:

from reflex.experimental import ClientStateVar

docs = ClientStateVar.create("docs_minisearch", default=[], global_ref=True)
query = ClientStateVar.create("query_minisearch", default="", global_ref=True)

Type Definitions

Structure your data types for type safety:

class AuthorDict(TypedDict):
    firstName: str
    lastName: str

class ItemDict(TypedDict):
    title: str
    author: AuthorDict

class DocDict(TypedDict):
    item: ItemDict
    refIndex: int
    score: float

Loading External Resources

Inject MiniSearch CDN and custom scripts into the document head:

def minisearch_cdn_script():
    return rx.script(src="https://cdn.jsdelivr.net/npm/minisearch@7.1.0/dist/umd/index.min.js")

def custom_minisearch_search_script():
    return rx.script(src="/js_integration/search/minisearch.js")

def load_json_file_and_initialize_minisearch():
    return rx.script('''
        console.log("Fetching list.json...");
        fetch('/js_integration/search/list.json')
            .then(response => response.json())
            .then(data => {
                console.log("Loaded list.json:", data);
                window.initializeMinisearch(data);
                window.minisearchInitialized = true;
            })
            .catch(error => console.error('Error loading JSON:', error));
    ''')

Search Input Component

Create a debounced search input that updates client state:

def search_input():
    return rx.box(
        rx.box(
            rx.icon(
                tag="search",
                size=14,
                class_name="absolute left-2 top-1/2 transform -translate-y-1/2 !text-gray-500/40",
            ),
            rx.el.input(
                id="search_input_minisearch",
                on_change=rx.call_script('''
                    (() => {
                        if (!window._debouncedSearch) {
                            let timeout;
                            window._debouncedSearch = function () {
                                clearTimeout(timeout);
                                timeout = setTimeout(() => {
                                    const inputValue = document.getElementById("search_input_minisearch").value;
                                    const results = window.searchMinisearch(inputValue);
                                    refs._client_state_setDocs_minisearch(results);
                                    refs._client_state_setQuery_minisearch(inputValue);
                                }, 300);
                            };
                        }
                        window._debouncedSearch();
                    })();
                '''),
                auto_focus=True,
                placeholder="Search documentation ...",
                class_name="py-2 pl-7 w-full placeholder:text-sm text-sm rounded-lg outline-none focus:outline-none"
            ),
            class_name="w-full relative focus:outline-none",
        ),
        class_name="w-full flex flex-col absolute top-0 left-0 p-3 z-[999]",
    )

Rendering Results

Display search results with conditional rendering:

def search_content():
    return rx.scroll_area(
        rx.box(
            rx.cond(
                query.value.to(str).strip() != "",
                rx.cond(
                    docs.value.to(List[DocDict]).length() > 0,
                    rx.foreach(
                        docs.value.to(List[DocDict]),
                        lambda doc: rx.box(
                            rx.el.p(
                                rx.fragment(
                                    rx.el.strong("Title: "),
                                    doc["item"]["title"],
                                ),
                            ),
                            rx.el.p(
                                rx.fragment(
                                    rx.el.strong("Author: "),
                                    doc["item"]["author"]["firstName"] + " " +
                                    doc["item"]["author"]["lastName"],
                                ),
                            ),
                            rx.el.p(
                                rx.fragment(
                                    rx.el.strong("Score: "),
                                    f"{doc['score']:.6f}",
                                ),
                            ),
                            font_size="14px",
                        ),
                    ),
                    no_results_found(),
                ),
            ),
            class_name="flex flex-col gap-y-2 px-2"
        ),
        class_name="w-full h-full pt-11",
    )

Dialog Integration

Wrap the search in a dialog with keyboard shortcut trigger:

def minisearch_search_front_end_only():
    return rx.dialog.root(
        rx.dialog.trigger(search_trigger(), id="search-trigger"),
        rx.dialog.content(
            search_input(),
            search_content(),
            class_name="w-full max-w-[650px] mx-auto outline-none p-3 h-[57vh]"
        ),
    )

Application Setup

Register scripts in the app initialization:

app = rx.App(
    head_components=[
        minisearch_cdn_script(),
        custom_minisearch_search_script(),
        load_json_file_and_initialize_minisearch(),
    ],
)

Key Concepts

Client state variables enable reactive updates without server round-trips. The debounce pattern prevents excessive search calls during typing. MiniSearch handles fuzzy matching with configurable prefix search and boosting. Results include match scores for relevance ranking.

Customization

Adjust fuzzy in searchOptions (0.0-1.0, where higher values allow more character differences). Modify fields array to search different fields. Change boost values to prioritize certain fields (e.g., boost title matches over author). Adjust debounce timeout (300ms default) for responsiveness vs performance tradeoffs. Enable/disable prefix matching for as-you-type search behavior.