Skip to main content

Overview

Teak’s search system queries multiple fields simultaneously using Convex’s full-text search indexes, providing instant results across your entire knowledge base.

Search Fields

Every search query looks across these fields:
FieldDescriptionIndex
contentMain card contentsearch_content
notesUser notessearch_notes
metadataTitleLink preview titlesearch_metadata_title
metadataDescriptionLink preview descriptionsearch_metadata_description
aiSummaryAI-generated summarysearch_ai_summary
aiTranscriptAudio/video transcriptsearch_ai_transcript
tagsUser tags (array)search_tags
aiTagsAI tags (array)search_ai_tags
All searches are case-insensitive and use Convex’s built-in tokenization.
import { api } from "@teak/convex";
import { useQuery } from "convex/react";

function SearchExample() {
  const results = useQuery(api.card.getCards.searchCards, {
    searchQuery: "design inspiration",
    limit: 50
  });

  return (
    <div>
      {results?.map(card => (
        <div key={card._id}>{card.content}</div>
      ))}
    </div>
  );
}

Search Syntax

Simple Terms

Space-separated terms are treated as multiple keywords:
// Finds cards containing "react" OR "typescript"
{ searchQuery: "react typescript" }
Use quotes for exact phrases:
// Finds cards containing the exact phrase "design system"
{ searchQuery: '"design system"' }

Partial Matching

Teak uses partial token matching:
// "dark" matches "dark-mode", "darkness", "dark theme"
{ searchQuery: "dark" }

Special Keywords

Teak recognizes special search shortcuts:
Search for favorites using these keywords:
"fav"
"favs"
"favorites"
"favourite"
"favourites"
const results = useQuery(api.card.getCards.searchCards, {
  searchQuery: "favorites"
});
// Returns all favorited cards

Search with Filters

Combine search with type, visual, and time filters:
const results = useQuery(api.card.getCards.searchCards, {
  searchQuery: "react hooks",
  types: ["link", "text"],
  favoritesOnly: false,
  styleFilters: ["minimal"],
  createdAtRange: {
    start: Date.now() - 30 * 24 * 60 * 60 * 1000,
    end: Date.now()
  },
  limit: 50
});

Search Performance

Query Batching

Teak uses a batched approach to optimize search performance:
1

Batch 1: High-Selectivity Fields

// Most selective searches run first
- search_content
- search_metadata_title
- search_notes
2

Batch 2: Medium-Selectivity Fields

// Metadata and AI fields
- search_metadata_description
- search_ai_summary (for applicable types)
- search_ai_transcript (for audio/video)
3

Batch 3: Low-Selectivity Fields

// Tag searches (less selective)
- search_tags
- search_ai_tags
4

Early Termination

If enough results are found in batch 1, batches 2 and 3 are skipped.
See packages/convex/card/getCards.ts:533-627 for the batching implementation.

Type-Specific Optimization

When filtering by type, Teak skips irrelevant indexes:
// When searching only audio cards
{
  searchQuery: "interview",
  types: ["audio"]
}
// Skips: search_metadata_title, search_metadata_description
// Includes: search_ai_transcript (audio-specific)
Type FilterSkipped Indexes
["audio"]Link metadata fields
["text", "quote"]AI transcript, link metadata
["palette"]All AI fields

Deduplication

Cards matching multiple search fields appear only once:
// Card with content="React hooks" and tag="react"
// Appears once in results, not twice
const results = await searchCards({ searchQuery: "react" });
Results are deduplicated by _id using a Map structure for O(1) lookups.

Pagination

For large result sets, use paginated search:
import { usePaginatedQuery } from "convex/react";

function PaginatedSearch() {
  const { results, status, loadMore } = usePaginatedQuery(
    api.card.getCards.searchCardsPaginated,
    {
      searchQuery: "design",
      types: ["image", "link"]
    },
    { initialNumItems: 50 }
  );

  return (
    <div>
      {results.map(card => <Card key={card._id} {...card} />)}
      {status === "CanLoadMore" && (
        <button onClick={() => loadMore(50)}>Load More</button>
      )}
    </div>
  );
}

Pagination Internals

Paginated search uses offset-based cursors:
// First page
{ cursor: null, numItems: 50 }
// Returns: { page: [...], continueCursor: "50", isDone: false }

// Second page
{ cursor: "50", numItems: 50 }
// Returns: { page: [...], continueCursor: "100", isDone: false }
See packages/convex/card/getCards.ts:400-747 for pagination logic.

Search Examples

// Find all cards mentioning "TypeScript"
const results = useQuery(api.card.getCards.searchCards, {
  searchQuery: "typescript"
});
Omitting searchQuery returns filtered cards in creation order:
// Get all image cards, newest first
const images = useQuery(api.card.getCards.searchCards, {
  types: ["image"],
  limit: 50
});
Use empty search with filters to browse cards by type, style, or color.

Search Limitations

No fuzzy matching: Searches are exact token matches. “reactjs” won’t match “react”.
No boolean operators: AND/OR/NOT operators are not supported. Use filters instead.
Convex search uses Tantivy’s tokenization. See Convex search docs for details.

Best Practices

1

Use Specific Terms

Shorter, specific terms match more accurately:
// Good
{ searchQuery: "react hooks" }

// Less effective
{ searchQuery: "how to use react hooks in functional components" }
2

Combine with Type Filters

Narrow search space for faster results:
{
  searchQuery: "design",
  types: ["image", "link"] // Reduces search space
}
3

Use Pagination for Large Sets

Load results incrementally for better UX:
usePaginatedQuery(
  api.card.getCards.searchCardsPaginated,
  { searchQuery: query },
  { initialNumItems: 50 }
);

Source Reference

  • Search implementation: packages/convex/card/getCards.ts:96-398
  • Paginated search: packages/convex/card/getCards.ts:400-747
  • Search indexes: packages/convex/schema.ts

Build docs developers (and LLMs) love