Search Block

Full-text search engine for modern applications
The Search Block provides lightning-fast full-text search capabilities for your application. Index your data, perform instant searches with typo tolerance, and deliver relevant results with faceting, filtering, and analytics—all through a simple REST API.
Features
- Full-Text Search - Lightning-fast queries across all indexed content
- Typo Tolerance - Fuzzy matching handles misspellings automatically
- Faceted Filters - Dynamic faceting for result refinement
- Auto-Suggestions - Real-time search suggestions as users type
- Synonyms & Stemming - Language-aware matching with custom synonyms
- Search Analytics - Track popular queries and optimize relevance
- Custom Ranking - Configure ranking rules and boosting
- Multi-Index - Federated search across multiple indexes
- Highlighting - Show matched terms in context
- Geo Search - Location-aware search and sorting
API Endpoint
| Service | URL |
|---|---|
| Search | https://search.api.us.23blocks.com |
Environment Routing: One URL, one environment. All shared-platform API keys are live keys:
pk_live_*/sk_live_*→ your production tenant. Need a separate staging environment? It is available as a paid add-on on deployments in your own cloud account — see 23blocks in a Box.
Quick Start
npm install @23blocks/sdk
import { create23BlocksClient } from '@23blocks/sdk';
const client = create23BlocksClient({
urls: { search: 'https://search.api.us.23blocks.com' },
apiKey: 'your-api-key', // Your live API key (pk_live_*)
});
// Create an index
await client.search.createIndex('products');
// Index documents
await client.search.indexDocuments('products', [
{ id: '1', name: 'Wireless Headphones', category: 'electronics', price: 299 },
{ id: '2', name: 'Bluetooth Speaker', category: 'electronics', price: 149 },
{ id: '3', name: 'Running Shoes', category: 'sports', price: 129 }
]);
// Search with typo tolerance
const results = await client.search.query('products', {
query: 'wirless headphnes', // typos handled!
filters: { category: 'electronics' },
facets: ['category', 'brand']
});
// Get suggestions
const suggestions = await client.search.suggest('products', {
query: 'wire',
limit: 5
});
Authentication
All API requests require authentication using your API credentials.
Required Headers
X-App-Id: your-app-id
X-Api-Key: your-api-key
Content-Type: application/json
Indexes API
Manage your search indexes.
Create Index
Create a new search index.
POST /search/v1/indexes
Request Body:
{
"data": {
"type": "indexes",
"attributes": {
"uid": "products",
"primary_key": "id",
"settings": {
"searchable_attributes": ["name", "description", "category"],
"filterable_attributes": ["category", "brand", "price"],
"sortable_attributes": ["price", "created_at", "popularity"],
"ranking_rules": [
"words",
"typo",
"proximity",
"attribute",
"sort",
"exactness"
],
"stop_words": ["the", "a", "an"],
"distinct_attribute": null,
"typo_tolerance": {
"enabled": true,
"min_word_size_for_typos": {
"one_typo": 5,
"two_typos": 9
}
}
}
}
}
}
Response:
{
"data": {
"type": "indexes",
"id": "idx_a1b2c3d4",
"attributes": {
"uid": "products",
"primary_key": "id",
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z",
"number_of_documents": 0,
"is_indexing": false
}
}
}
List Indexes
Retrieve all indexes.
GET /search/v1/indexes
Response:
{
"data": [
{
"type": "indexes",
"id": "idx_a1b2c3d4",
"attributes": {
"uid": "products",
"primary_key": "id",
"number_of_documents": 15420,
"is_indexing": false
}
},
{
"type": "indexes",
"id": "idx_e5f6g7h8",
"attributes": {
"uid": "articles",
"primary_key": "id",
"number_of_documents": 3250,
"is_indexing": false
}
}
]
}
Get Index
Retrieve a specific index.
GET /search/v1/indexes/:index_uid
Response:
{
"data": {
"type": "indexes",
"id": "idx_a1b2c3d4",
"attributes": {
"uid": "products",
"primary_key": "id",
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T12:45:00Z",
"number_of_documents": 15420,
"is_indexing": false,
"settings": {
"searchable_attributes": ["name", "description", "category"],
"filterable_attributes": ["category", "brand", "price"],
"sortable_attributes": ["price", "created_at", "popularity"]
}
}
}
}
Update Index Settings
Update index configuration.
PATCH /search/v1/indexes/:index_uid/settings
Request Body:
{
"data": {
"type": "index_settings",
"attributes": {
"searchable_attributes": ["name", "description", "category", "tags"],
"filterable_attributes": ["category", "brand", "price", "in_stock"],
"synonyms": {
"phone": ["smartphone", "mobile", "cell phone"],
"laptop": ["notebook", "computer"]
}
}
}
}
Delete Index
Delete an index and all its documents.
DELETE /search/v1/indexes/:index_uid
Documents API
Manage documents within an index.
Index Documents
Add or update documents.
POST /search/v1/indexes/:index_uid/documents
Request Body:
{
"data": {
"type": "documents",
"attributes": {
"documents": [
{
"id": "prod_001",
"name": "Wireless Noise-Cancelling Headphones",
"description": "Premium over-ear headphones with active noise cancellation",
"category": "electronics",
"brand": "AudioMax",
"price": 299.99,
"in_stock": true,
"rating": 4.7,
"tags": ["wireless", "noise-cancelling", "premium"]
},
{
"id": "prod_002",
"name": "Portable Bluetooth Speaker",
"description": "Waterproof speaker with 360-degree sound",
"category": "electronics",
"brand": "SoundWave",
"price": 149.99,
"in_stock": true,
"rating": 4.5,
"tags": ["bluetooth", "portable", "waterproof"]
}
]
}
}
}
Response:
{
"data": {
"type": "indexing_tasks",
"id": "task_xyz123",
"attributes": {
"status": "enqueued",
"documents_received": 2,
"enqueued_at": "2025-01-15T14:30:00Z"
}
}
}
Get Document
Retrieve a specific document.
GET /search/v1/indexes/:index_uid/documents/:document_id
Response:
{
"data": {
"type": "documents",
"id": "prod_001",
"attributes": {
"name": "Wireless Noise-Cancelling Headphones",
"description": "Premium over-ear headphones with active noise cancellation",
"category": "electronics",
"brand": "AudioMax",
"price": 299.99,
"in_stock": true,
"rating": 4.7,
"tags": ["wireless", "noise-cancelling", "premium"]
}
}
}
List Documents
Browse documents in an index.
GET /search/v1/indexes/:index_uid/documents?offset=0&limit=20
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| offset | integer | Number of documents to skip |
| limit | integer | Number of documents to return (max 1000) |
| fields | string | Comma-separated list of fields to return |
| filter | string | Filter expression |
Delete Documents
Delete specific documents.
DELETE /search/v1/indexes/:index_uid/documents
Request Body:
{
"data": {
"type": "document_deletions",
"attributes": {
"document_ids": ["prod_001", "prod_002", "prod_003"]
}
}
}
Delete Documents by Filter
Delete documents matching a filter.
POST /search/v1/indexes/:index_uid/documents/delete-by-filter
Request Body:
{
"data": {
"type": "document_deletions",
"attributes": {
"filter": "category = 'deprecated' AND updated_at < '2024-01-01'"
}
}
}
Search API
Perform searches across your indexed data.
Basic Search
POST /search/v1/indexes/:index_uid/search
Request Body:
{
"data": {
"type": "search_queries",
"attributes": {
"query": "wireless headphones",
"limit": 20,
"offset": 0
}
}
}
Response:
{
"data": {
"type": "search_results",
"attributes": {
"hits": [
{
"id": "prod_001",
"name": "Wireless Noise-Cancelling Headphones",
"description": "Premium over-ear headphones with active noise cancellation",
"category": "electronics",
"price": 299.99,
"_formatted": {
"name": "<em>Wireless</em> Noise-Cancelling <em>Headphones</em>",
"description": "Premium over-ear <em>headphones</em> with active noise cancellation"
},
"_matchesPosition": {
"name": [{ "start": 0, "length": 8 }, { "start": 31, "length": 10 }]
}
}
],
"offset": 0,
"limit": 20,
"estimated_total_hits": 128,
"processing_time_ms": 12,
"query": "wireless headphones"
}
}
}
Search with Filters
Apply filters to narrow results.
{
"data": {
"type": "search_queries",
"attributes": {
"query": "headphones",
"filter": "category = 'electronics' AND price < 300 AND in_stock = true",
"limit": 20
}
}
}
Filter Syntax:
| Operator | Description | Example |
|---|---|---|
= | Equals | category = 'electronics' |
!= | Not equals | brand != 'Generic' |
> | Greater than | price > 100 |
>= | Greater or equal | rating >= 4.0 |
< | Less than | price < 500 |
<= | Less or equal | stock <= 10 |
IN | In array | category IN ['electronics', 'audio'] |
NOT IN | Not in array | status NOT IN ['deleted', 'hidden'] |
EXISTS | Field exists | discount EXISTS |
NOT EXISTS | Field doesn't exist | archived NOT EXISTS |
AND | Logical and | price < 100 AND in_stock = true |
OR | Logical or | category = 'audio' OR category = 'video' |
Search with Facets
Get facet counts for result refinement.
{
"data": {
"type": "search_queries",
"attributes": {
"query": "headphones",
"facets": ["category", "brand", "price"],
"limit": 20
}
}
}
Response:
{
"data": {
"type": "search_results",
"attributes": {
"hits": [],
"facet_distribution": {
"category": {
"electronics": 95,
"audio": 28,
"gaming": 12
},
"brand": {
"AudioMax": 42,
"SoundWave": 35,
"TechPro": 28
}
},
"facet_stats": {
"price": {
"min": 29.99,
"max": 599.99,
"avg": 189.50,
"sum": 25678.50
}
}
}
}
}
Search with Sorting
Sort results by specific attributes.
{
"data": {
"type": "search_queries",
"attributes": {
"query": "headphones",
"sort": ["price:asc", "rating:desc"],
"limit": 20
}
}
}
Search with Highlighting
Configure result highlighting.
{
"data": {
"type": "search_queries",
"attributes": {
"query": "wireless",
"attributes_to_highlight": ["name", "description"],
"highlight_pre_tag": "<mark>",
"highlight_post_tag": "</mark>",
"limit": 20
}
}
}
Geo Search
Search with geographic constraints.
{
"data": {
"type": "search_queries",
"attributes": {
"query": "coffee shop",
"filter": "_geoRadius(40.7128, -74.0060, 5000)",
"sort": ["_geoPoint(40.7128, -74.0060):asc"],
"limit": 20
}
}
}
Multi-Index Search
Search across multiple indexes.
POST /search/v1/multi-search
Request Body:
{
"data": {
"type": "multi_search_queries",
"attributes": {
"queries": [
{
"index_uid": "products",
"query": "headphones",
"limit": 5
},
{
"index_uid": "articles",
"query": "headphones review",
"limit": 3
}
]
}
}
}
Suggestions API
Provide search suggestions as users type.
Get Suggestions
POST /search/v1/indexes/:index_uid/suggest
Request Body:
{
"data": {
"type": "suggestion_queries",
"attributes": {
"query": "wire",
"limit": 5,
"highlight": true
}
}
}
Response:
{
"data": {
"type": "suggestions",
"attributes": {
"suggestions": [
{
"suggestion": "wireless headphones",
"highlighted": "<em>wire</em>less headphones",
"frequency": 1250
},
{
"suggestion": "wireless speaker",
"highlighted": "<em>wire</em>less speaker",
"frequency": 890
},
{
"suggestion": "wireless charger",
"highlighted": "<em>wire</em>less charger",
"frequency": 654
},
{
"suggestion": "wireless mouse",
"highlighted": "<em>wire</em>less mouse",
"frequency": 421
},
{
"suggestion": "wireless keyboard",
"highlighted": "<em>wire</em>less keyboard",
"frequency": 387
}
],
"processing_time_ms": 3
}
}
}
Configure Suggestions
Configure suggestion behavior.
PATCH /search/v1/indexes/:index_uid/settings/suggestions
Request Body:
{
"data": {
"type": "suggestion_settings",
"attributes": {
"enabled": true,
"source_attributes": ["name", "category", "tags"],
"min_query_length": 2,
"max_suggestions": 10,
"include_frequency": true
}
}
}
Synonyms API
Configure synonyms for better search matching.
Set Synonyms
PUT /search/v1/indexes/:index_uid/settings/synonyms
Request Body:
{
"data": {
"type": "synonyms",
"attributes": {
"synonyms": {
"phone": ["smartphone", "mobile", "cell phone", "cellphone"],
"laptop": ["notebook", "computer", "pc"],
"tv": ["television", "smart tv", "flat screen"],
"headphones": ["earphones", "earbuds", "headset"]
},
"one_way_synonyms": {
"iphone": ["phone", "smartphone"],
"macbook": ["laptop", "notebook"]
}
}
}
}
Get Synonyms
GET /search/v1/indexes/:index_uid/settings/synonyms
Response:
{
"data": {
"type": "synonyms",
"attributes": {
"synonyms": {
"phone": ["smartphone", "mobile", "cell phone", "cellphone"],
"laptop": ["notebook", "computer", "pc"]
},
"one_way_synonyms": {
"iphone": ["phone", "smartphone"]
}
}
}
}
Stop Words API
Configure stop words to ignore in searches.
Set Stop Words
PUT /search/v1/indexes/:index_uid/settings/stop-words
Request Body:
{
"data": {
"type": "stop_words",
"attributes": {
"stop_words": ["the", "a", "an", "and", "or", "but", "is", "are", "was", "were"]
}
}
}
Ranking Rules API
Configure how results are ranked.
Set Ranking Rules
PUT /search/v1/indexes/:index_uid/settings/ranking-rules
Request Body:
{
"data": {
"type": "ranking_rules",
"attributes": {
"ranking_rules": [
"words",
"typo",
"proximity",
"attribute",
"sort",
"exactness",
"popularity:desc",
"rating:desc"
]
}
}
}
Built-in Ranking Rules:
| Rule | Description |
|---|---|
| words | Matches with more query words rank higher |
| typo | Matches with fewer typos rank higher |
| proximity | Matches with words closer together rank higher |
| attribute | Matches in more important attributes rank higher |
| sort | Applies explicit sorting from search request |
| exactness | Exact matches rank higher than partial matches |
Analytics API
Track search analytics and user behavior.
Get Popular Searches
GET /search/v1/indexes/:index_uid/analytics/popular-searches
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| period | string | Time period: 24h, 7d, 30d, 90d |
| limit | integer | Number of results (default: 10) |
Response:
{
"data": {
"type": "analytics",
"attributes": {
"popular_searches": [
{
"query": "wireless headphones",
"count": 12450,
"unique_users": 8920,
"avg_click_position": 1.3
},
{
"query": "bluetooth speaker",
"count": 8730,
"unique_users": 6540,
"avg_click_position": 1.8
}
],
"period": "7d",
"total_searches": 156780
}
}
}
Get No-Result Searches
Find queries with no results.
GET /search/v1/indexes/:index_uid/analytics/no-result-searches
Response:
{
"data": {
"type": "analytics",
"attributes": {
"no_result_searches": [
{
"query": "solar charger",
"count": 234,
"last_searched_at": "2025-01-15T14:30:00Z"
},
{
"query": "wireless earbuds case",
"count": 189,
"last_searched_at": "2025-01-15T14:25:00Z"
}
],
"period": "7d",
"total_no_result_searches": 1250
}
}
}
Get Click Analytics
Track which results users click.
GET /search/v1/indexes/:index_uid/analytics/clicks
Response:
{
"data": {
"type": "analytics",
"attributes": {
"click_data": [
{
"document_id": "prod_001",
"clicks": 5420,
"impressions": 18500,
"ctr": 0.293,
"avg_position": 1.2
}
],
"period": "7d",
"total_clicks": 45600,
"total_impressions": 156780,
"avg_ctr": 0.29
}
}
}
Track Search Event
Log search analytics events.
POST /search/v1/analytics/events
Request Body:
{
"data": {
"type": "search_events",
"attributes": {
"event_type": "click",
"index_uid": "products",
"query": "wireless headphones",
"document_id": "prod_001",
"position": 1,
"user_id": "user_abc123",
"session_id": "sess_xyz789"
}
}
}
Tasks API
Monitor indexing tasks.
Get Task Status
GET /search/v1/tasks/:task_id
Response:
{
"data": {
"type": "tasks",
"id": "task_xyz123",
"attributes": {
"status": "succeeded",
"task_type": "document_addition",
"index_uid": "products",
"details": {
"documents_received": 250,
"documents_indexed": 250
},
"enqueued_at": "2025-01-15T14:30:00Z",
"started_at": "2025-01-15T14:30:01Z",
"finished_at": "2025-01-15T14:30:03Z",
"duration_ms": 2340
}
}
}
List Tasks
GET /search/v1/tasks?status=succeeded&index_uid=products&limit=20
Task Statuses:
| Status | Description |
|---|---|
| enqueued | Task is waiting to be processed |
| processing | Task is currently being processed |
| succeeded | Task completed successfully |
| failed | Task failed with an error |
| canceled | Task was canceled |
Webhooks
Configure webhooks for search events.
Register Webhook
POST /search/v1/webhooks
Request Body:
{
"data": {
"type": "webhooks",
"attributes": {
"url": "https://your-app.com/webhooks/search",
"events": [
"indexing.completed",
"indexing.failed",
"index.created",
"index.deleted"
],
"secret": "your-webhook-secret"
}
}
}
Webhook Events
| Event | Description |
|---|---|
| indexing.completed | Batch indexing completed successfully |
| indexing.failed | Batch indexing failed |
| index.created | New index created |
| index.deleted | Index was deleted |
| settings.updated | Index settings changed |
Webhook Payload Example
{
"event": "indexing.completed",
"occurred_at": "2025-01-15T14:30:03Z",
"data": {
"task_id": "task_xyz123",
"index_uid": "products",
"documents_indexed": 250,
"duration_ms": 2340
}
}
Error Handling
Error Response Format
{
"errors": [
{
"status": "400",
"code": "invalid_filter_syntax",
"title": "Invalid Filter Syntax",
"detail": "The filter 'price <> 100' contains an invalid operator. Use '!=' instead of '<>'.",
"source": {
"parameter": "filter"
}
}
]
}
Common Error Codes
| Code | Status | Description |
|---|---|---|
| index_not_found | 404 | The specified index does not exist |
| document_not_found | 404 | The specified document does not exist |
| invalid_filter_syntax | 400 | Filter syntax is invalid |
| invalid_sort_attribute | 400 | Sort attribute is not sortable |
| attribute_not_filterable | 400 | Attribute is not configured as filterable |
| max_documents_exceeded | 400 | Document batch exceeds maximum size |
| index_already_exists | 409 | Index with this UID already exists |
| rate_limit_exceeded | 429 | Too many requests |
SDK Examples
React Search Component
import { useState, useEffect, useCallback } from 'react';
import { useDebounce } from 'use-debounce';
import { create23BlocksClient } from '@23blocks/sdk';
const client = create23BlocksClient({
urls: { search: 'https://search.api.us.23blocks.com' },
appId: 'your-app-id',
});
function ProductSearch() {
const [query, setQuery] = useState('');
const [debouncedQuery] = useDebounce(query, 300);
const [results, setResults] = useState([]);
const [suggestions, setSuggestions] = useState([]);
const [facets, setFacets] = useState({});
const [filters, setFilters] = useState({});
const [loading, setLoading] = useState(false);
// Fetch suggestions as user types
useEffect(() => {
if (debouncedQuery.length < 2) {
setSuggestions([]);
return;
}
client.search.suggest('products', {
query: debouncedQuery,
limit: 5
}).then(data => {
setSuggestions(data.suggestions);
});
}, [debouncedQuery]);
// Perform search
const performSearch = useCallback(async (searchQuery) => {
if (!searchQuery) {
setResults([]);
setFacets({});
return;
}
setLoading(true);
try {
const response = await client.search.query('products', {
query: searchQuery,
filter: buildFilterString(filters),
facets: ['category', 'brand'],
limit: 20,
attributesToHighlight: ['name', 'description']
});
setResults(response.hits);
setFacets(response.facetDistribution);
// Track search analytics
await client.search.trackEvent({
eventType: 'search',
indexUid: 'products',
query: searchQuery,
resultsCount: response.estimatedTotalHits
});
} finally {
setLoading(false);
}
}, [filters]);
// Handle result click
const handleResultClick = async (hit, position) => {
await client.search.trackEvent({
eventType: 'click',
indexUid: 'products',
query,
documentId: hit.id,
position
});
};
return (
<div className="search-container">
{/* Search Input */}
<div className="search-input-wrapper">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && performSearch(query)}
placeholder="Search products..."
className="search-input"
/>
{/* Suggestions Dropdown */}
{suggestions.length > 0 && (
<ul className="suggestions-dropdown">
{suggestions.map((suggestion, index) => (
<li
key={index}
onClick={() => {
setQuery(suggestion.suggestion);
performSearch(suggestion.suggestion);
setSuggestions([]);
}}
dangerouslySetInnerHTML={{ __html: suggestion.highlighted }}
/>
))}
</ul>
)}
</div>
<div className="search-layout">
{/* Facets Sidebar */}
<aside className="facets-sidebar">
{Object.entries(facets).map(([facetName, values]) => (
<div key={facetName} className="facet-group">
<h3>{facetName}</h3>
{Object.entries(values).map(([value, count]) => (
<label key={value}>
<input
type="checkbox"
checked={filters[facetName]?.includes(value)}
onChange={() => toggleFilter(facetName, value)}
/>
{value} ({count})
</label>
))}
</div>
))}
</aside>
{/* Results */}
<main className="search-results">
{loading ? (
<div className="loading">Searching...</div>
) : (
<ul>
{results.map((hit, index) => (
<li
key={hit.id}
onClick={() => handleResultClick(hit, index + 1)}
className="result-item"
>
<h3 dangerouslySetInnerHTML={{
__html: hit._formatted?.name || hit.name
}} />
<p dangerouslySetInnerHTML={{
__html: hit._formatted?.description || hit.description
}} />
<span className="price">${hit.price}</span>
</li>
))}
</ul>
)}
</main>
</div>
</div>
);
}
Vue 3 Composable
<script setup>
import { ref, watch, computed } from 'vue';
import { useDebounceFn } from '@vueuse/core';
import { create23BlocksClient } from '@23blocks/sdk';
const client = create23BlocksClient({
urls: { search: 'https://search.api.us.23blocks.com' },
appId: 'your-app-id',
});
const query = ref('');
const results = ref([]);
const suggestions = ref([]);
const facets = ref({});
const selectedFilters = ref({});
const loading = ref(false);
// Debounced suggestion fetch
const fetchSuggestions = useDebounceFn(async (searchQuery) => {
if (searchQuery.length < 2) {
suggestions.value = [];
return;
}
const data = await client.search.suggest('products', {
query: searchQuery,
limit: 5
});
suggestions.value = data.suggestions;
}, 300);
watch(query, (newQuery) => {
fetchSuggestions(newQuery);
});
// Build filter string from selected filters
const filterString = computed(() => {
const parts = [];
for (const [key, values] of Object.entries(selectedFilters.value)) {
if (values.length > 0) {
parts.push(`${key} IN [${values.map(v => `'${v}'`).join(', ')}]`);
}
}
return parts.join(' AND ');
});
// Perform search
async function search() {
if (!query.value) return;
loading.value = true;
try {
const response = await client.search.query('products', {
query: query.value,
filter: filterString.value || undefined,
facets: ['category', 'brand'],
limit: 20
});
results.value = response.hits;
facets.value = response.facetDistribution;
} finally {
loading.value = false;
}
}
// Toggle facet filter
function toggleFilter(facetName, value) {
if (!selectedFilters.value[facetName]) {
selectedFilters.value[facetName] = [];
}
const index = selectedFilters.value[facetName].indexOf(value);
if (index === -1) {
selectedFilters.value[facetName].push(value);
} else {
selectedFilters.value[facetName].splice(index, 1);
}
search();
}
</script>
<template>
<div class="search-page">
<div class="search-box">
<input
v-model="query"
@keyup.enter="search"
placeholder="Search products..."
/>
<ul v-if="suggestions.length" class="suggestions">
<li
v-for="(suggestion, index) in suggestions"
:key="index"
@click="query = suggestion.suggestion; search(); suggestions = []"
v-html="suggestion.highlighted"
/>
</ul>
</div>
<div class="content">
<aside class="filters">
<div v-for="(values, facetName) in facets" :key="facetName">
<h4>{{ facetName }}</h4>
<label v-for="(count, value) in values" :key="value">
<input
type="checkbox"
:checked="selectedFilters[facetName]?.includes(value)"
@change="toggleFilter(facetName, value)"
/>
{{ value }} ({{ count }})
</label>
</div>
</aside>
<main class="results">
<div v-if="loading">Loading...</div>
<div v-else-if="results.length === 0">No results found</div>
<ul v-else>
<li v-for="hit in results" :key="hit.id">
<h3 v-html="hit._formatted?.name || hit.name" />
<p v-html="hit._formatted?.description || hit.description" />
<span class="price">${{ hit.price }}</span>
</li>
</ul>
</main>
</div>
</div>
</template>
Rate Limits
| Plan | Searches/second | Documents/index | Indexes |
|---|---|---|---|
| Free | 10 | 10,000 | 3 |
| Starter | 50 | 100,000 | 10 |
| Growth | 200 | 1,000,000 | 50 |
| Enterprise | Custom | Unlimited | Unlimited |
Best Practices
Indexing
- Batch documents - Index in batches of 100-1000 documents for optimal performance
- Use primary keys - Always define a primary key for document identification
- Configure searchable attributes - Only index fields that need to be searchable
- Set filterable attributes - Pre-configure attributes you'll filter on
Searching
- Use suggestions - Implement auto-complete for better UX
- Apply filters wisely - Filters are fast, use them for category navigation
- Track analytics - Log searches and clicks to improve relevance
- Handle typos - Typo tolerance is enabled by default
Performance
- Debounce queries - Wait 200-300ms between keystrokes before searching
- Limit results - Request only needed fields and reasonable result counts
- Cache static facets - Cache facet distributions that don't change often
- Use pagination - Implement infinite scroll or pagination for large result sets
Related Blocks
- Content Block - Headless CMS for searchable content
- Products Block - Product catalog with search integration
- AI Block - Semantic search and recommendations
Use This Block with AI Agents
This block ships an official Claude Code plugin from the public 23blocks marketplace (19 plugins, 205 skills). Your agent gets full working knowledge of this API — endpoints, identity registration, and conventions.
# one-time: add the 23blocks marketplace
claude plugin marketplace add https://github.com/23blocks-OS/ai-agents
# install this block's plugin
claude plugin install search-block
Configure BLOCKS_API_URL, BLOCKS_API_KEY, and BLOCKS_AUTH_TOKEN. Register identity first: each block is autonomous — register the user with this block's identity endpoint before private calls (only user_unique_id is required; email/phone are optional notification-routing fields).
Full reference: AI Agents & Claude Code Plugins · github.com/23blocks-OS/ai-agents