AI & Search
Foir includes built-in semantic search that lets you find content by meaning rather than exact keywords. No external machine learning infrastructure required.
Overview
Traditional search matches keywords. Semantic search understands meaning. When a user searches for “affordable running shoes,” semantic search also finds content about “budget sneakers for jogging” — even if none of those exact words appear.
You generate vector embeddings for your content, and Foir stores them alongside your records. You query with natural language, and the platform returns the most relevant results ranked by similarity.
How Embeddings Work
Embeddings are the foundation of semantic search. Here is the high-level flow:
Your Content Vector Space
+-----------------------+ +-----------------------+
| "Lightweight | | |
| running shoes | --> | [0.12, -0.34, |
| for beginners" | Embed | 0.87, ...] |
+-----------------------+ +-----------------------+
User Query Similarity Match
+-----------------------+ +-----------------------+
| "beginner | | Score: 0.91 |
| jogging gear" | --> | -> running shoes |
| |Search | Score: 0.85 |
+-----------------------+ | -> starter kit |
+-----------------------+- Embedding generation — You trigger embedding for a record (via the
generateEmbeddingmutation orfoir embeddings), and Foir extracts its text and converts it into a numerical vector. - Storage — The vector is stored alongside the record, scoped to your project.
- Search — When a query comes in, it is converted to a vector and compared against stored vectors using cosine similarity.
- Results — Records are ranked by how closely their meaning matches the query.
Content Sources
Semantic search works with two types of content:
| Source | Description | Example |
|---|---|---|
| Entity | Versioned content with publishing workflows | Pages, blog posts, product descriptions |
| Data | Flat records with direct CRUD access | FAQ entries, support articles, knowledge base items |
You can search across both sources simultaneously or filter to one type.
In the Admin
Embeddings in the Admin
There is no toggle to “enable” embeddings on a model and no field-selection step. A model becomes searchable as soon as its records have stored embeddings, which you produce explicitly with the generateEmbedding mutation or the foir embeddings CLI.
To review coverage for a model:
- Navigate to Settings > Models
- Select the model you care about
- Open the Embeddings tab
The Embeddings tab is read-only. It reports how many records are embedded, the total record count, coverage percentage, and how many records are still pending. It appears only once the model has at least one stored embedding.
Searching in the Admin
The admin dashboard includes a global search bar backed by semantic search:
- Click the Search icon or press
/ - Type a natural language query
- Results are ranked by relevance across all models that have embeddings
- Click a result to navigate directly to the record
Via the CLI
Searching
# Semantic search across all models
foir search "lightweight running shoes" --first 10
# Search specific models
foir search "return policy for damaged items" --models faq,support-article --first 5Managing Embeddings
# Write an embedding for a record (input payload describes the record and vector)
foir embeddings write --file ./embedding.json
# Delete the embedding for a specific record
foir embeddings delete rec_abc123 --confirm
# Search by vector similarity
foir embeddings search --data '{"query": "beginner jogging gear", "modelKeys": ["product"], "limit": 5}'
# List embeddings stored for a record
foir embeddings list rec_abc123
# Stats for a specific model
foir embeddings stats product --json
# Project-wide embedding stats (no model key)
foir embeddings stats --json
# Find records similar to a given one
foir embeddings similar rec_abc123 --model-key product --limit 5See foir embeddings for the full subcommand and option reference.
Via the API
Search is per-model typed. Every model that has embeddings gets its own search<Model>s query generated from its key — for a model keyed product that is searchProducts. A generic searchRecords escape hatch is always available for cross-model search.
Per-Model Search
Returns a typed list of { score, record } hits, where record is the model’s normal GraphQL type — so you can select any of its fields.
Required scope: search:read:<model> (e.g. search:read:product)
query SearchProducts {
searchProducts(
query: "lightweight shoes for trail running"
first: 10
locale: "en"
) {
score
record {
_id
title
price
}
}
}| Argument | Type | Required | Description |
|---|---|---|---|
query | String | Yes | Natural language search query |
first | Int | No | Maximum number of hits to return (default: 10) |
locale | String | No | Locale to search within |
| Result field | Type | Description |
|---|---|---|
score | Float | Similarity score (higher = more similar) |
record | <Model> | The matching record, with all its typed fields |
Generic Search Across Models
When you want to search across model types, use searchRecords. It returns generic SearchResult rows rather than a typed record.
Required scope: search:read
query GlobalSearch {
searchRecords(
query: "how to set up two-factor authentication"
modelKey: "support_article" # optional; omit to search all models that have embeddings
first: 5
) {
recordId
modelKey
naturalKey
score
content
}
}| Argument | Type | Required | Description |
|---|---|---|---|
query | String | Yes | Natural language search query |
modelKey | String | No | Limit to a single model key |
first | Int | No | Maximum number of hits to return |
| Result field | Type | Description |
|---|---|---|
recordId | String | The matching record’s ID |
modelKey | String | The model key of the matching record |
naturalKey | String | The record’s natural key (slug, handle, etc.) |
score | Float | Similarity score (higher = more similar) |
content | JSON | The matched record content |
Embedding Coverage and Maintenance
A small set of operator queries and mutations let you inspect and manage stored embeddings directly. The coverage/inspection queries require search:semantic:read; the write mutations require search:semantic:write.
# Which records are missing embeddings for a model (Relay connection)
query Missing {
recordsMissingEmbedding(modelKey: "product", first: 50) {
edges {
node {
recordId
naturalKey
}
}
}
}
# Per-model coverage (omit modelKey for every model)
query Coverage {
embeddingCoverage(modelKey: "product") {
modelKey
totalRecords
embeddedRecords
pendingRecords
lastEmbeddedAt
}
}generateEmbedding Mutation
Manually enqueue an embedding for a specific record. Useful when you need a record to be searchable immediately after creation.
Required scope: search:semantic:write
mutation EmbedRecord {
generateEmbedding(
recordId: "rec_abc123"
modelKey: "product"
)
}The mutation returns a Boolean indicating whether the embedding job was enqueued. If the content has not changed since the last embedding, the job is skipped on the worker side. writeEmbeddings (also search:semantic:write) lets you push precomputed vectors directly.
Error Handling
Errors surface in the GraphQL response’s extensions.code:
| Code | Description |
|---|---|
PERMISSION_DENIED | API key lacks the required scope |
FAILED_PRECONDITION | Embeddings are not configured for this project |
NOT_FOUND | Record or model not found |
INVALID_ARGUMENT | Invalid input parameters |
Best Practices
- Set a similarity threshold to filter out low-quality matches. A threshold of 0.6-0.7 works well for most use cases.
- Choose embedding fields carefully — include the fields that best describe your content (titles, descriptions, body text) and exclude metadata fields.
- Use
modelKeysfiltering when you know which content types are relevant to narrow results and improve relevance. - Re-generate embeddings after significant content updates using
foir embeddings writeor thegenerateEmbeddingmutation.
Use Cases
- E-commerce product discovery — Let customers describe what they are looking for in natural language instead of navigating category trees.
- Help center / FAQ search — Feed support articles into the search index and let users find answers by asking questions in plain English.
- Content recommendations — Find similar articles, products, or pages based on meaning rather than tags using
foir embeddings similar. - Internal search — Build a search experience across all your project content that understands intent, not just keywords.