TYPESENSE - nself-org/cli GitHub Wiki
Typesense is a fast, typo-tolerant search engine built for instant search experiences. It's an open-source alternative to Algolia and offers better performance than Elasticsearch for most use cases with significantly lower resource requirements.
- Blazing Fast: Sub-50ms search latency
- Typo Tolerance: Automatically handles typos in queries
- Faceted Search: Filter and refine results
- Geo Search: Location-based search with distance
- Vector Search: Semantic search with embeddings
- Multi-Language: Support for 100+ languages
- RESTful API: Simple, well-documented API
- ACID Compliance: Strong consistency guarantees
Add to your .env file:
# Enable search with Typesense
SEARCH_ENABLED=true
SEARCH_PROVIDER=typesensenself build
nself startnself search status
nself search test# Core settings
SEARCH_ENABLED=true
SEARCH_PROVIDER=typesense
SEARCH_PORT=8108
SEARCH_API_KEY=your-api-key-here # Auto-generated if not set
# Route configuration
SEARCH_ROUTE=search # Creates search.yourdomain.com
# Multi-tenancy
SEARCH_INDEX_PREFIX=tenant1_
# Language
SEARCH_LANGUAGE=en# Typesense version
TYPESENSE_VERSION=27.1
# CORS settings
TYPESENSE_ENABLE_CORS=true
# Logging
TYPESENSE_LOG_LEVEL=info # Options: debug, info, warn, error
# Performance tuning
TYPESENSE_NUM_MEMORY_SHARDS=4
TYPESENSE_SNAPSHOT_INTERVAL_SECONDS=3600
TYPESENSE_HEALTHY_READ_LAG=1000
TYPESENSE_HEALTHY_WRITE_LAG=500TYPESENSE_SSL_CERTIFICATE=/path/to/cert.pem
TYPESENSE_SSL_CERTIFICATE_KEY=/path/to/key.pemnpm install typesenseimport Typesense from 'typesense'
const client = new Typesense.Client({
nodes: [{
host: 'localhost',
port: '8108',
protocol: 'http'
}],
apiKey: process.env.SEARCH_API_KEY,
connectionTimeoutSeconds: 2
})const schema = {
name: 'products',
fields: [
{ name: 'name', type: 'string' },
{ name: 'description', type: 'string' },
{ name: 'price', type: 'float' },
{ name: 'category', type: 'string', facet: true },
{ name: 'rating', type: 'float', facet: true },
{ name: 'in_stock', type: 'bool', facet: true },
{ name: 'created_at', type: 'int64' }
],
default_sorting_field: 'created_at'
}
await client.collections().create(schema)// Single document
const document = {
name: 'MacBook Pro 16"',
description: 'Powerful laptop for developers and creators',
price: 2499.99,
category: 'Laptops',
rating: 4.8,
in_stock: true,
created_at: Date.now()
}
await client.collections('products').documents().create(document)
// Bulk import
const documents = [
{ name: 'Product 1', price: 100, category: 'Electronics' },
{ name: 'Product 2', price: 200, category: 'Electronics' },
// ... more documents
]
await client.collections('products').documents().import(documents, {
action: 'create'
})// Basic search
const searchParameters = {
q: 'laptop',
query_by: 'name,description',
limit: 10
}
const results = await client.collections('products')
.documents()
.search(searchParameters)
// Advanced search with filters and facets
const advancedSearch = {
q: 'macbook',
query_by: 'name,description',
filter_by: 'price:<2000 && in_stock:true',
facet_by: 'category,rating',
sort_by: 'rating:desc,price:asc',
limit: 20,
page: 1,
highlight_full_fields: 'name,description',
typo_tokens_threshold: 2
}
const advancedResults = await client.collections('products')
.documents()
.search(advancedSearch)const autocompleteParams = {
q: 'mac',
query_by: 'name',
prefix: true,
limit: 5
}
const suggestions = await client.collections('products')
.documents()
.search(autocompleteParams)pip install typesenseimport typesense
import os
client = typesense.Client({
'nodes': [{
'host': 'localhost',
'port': '8108',
'protocol': 'http'
}],
'api_key': os.environ['SEARCH_API_KEY'],
'connection_timeout_seconds': 2
})schema = {
'name': 'products',
'fields': [
{'name': 'name', 'type': 'string'},
{'name': 'description', 'type': 'string'},
{'name': 'price', 'type': 'float'},
{'name': 'category', 'type': 'string', 'facet': True}
]
}
client.collections.create(schema)# Index document
document = {
'name': 'MacBook Pro',
'description': 'Powerful laptop',
'price': 2499.99,
'category': 'Laptops'
}
client.collections['products'].documents.create(document)
# Search
search_parameters = {
'q': 'laptop',
'query_by': 'name,description',
'filter_by': 'price:<2000'
}
results = client.collections['products'].documents.search(search_parameters)curl -H "X-TYPESENSE-API-KEY: ${SEARCH_API_KEY}" \
http://localhost:8108/healthcurl -X POST \
-H "X-TYPESENSE-API-KEY: ${SEARCH_API_KEY}" \
-H "Content-Type: application/json" \
http://localhost:8108/collections \
-d '{
"name": "products",
"fields": [
{"name": "name", "type": "string"},
{"name": "price", "type": "float"}
]
}'curl -X POST \
-H "X-TYPESENSE-API-KEY: ${SEARCH_API_KEY}" \
-H "Content-Type: application/json" \
http://localhost:8108/collections/products/documents \
-d '{
"name": "MacBook Pro",
"price": 2499.99
}'curl -H "X-TYPESENSE-API-KEY: ${SEARCH_API_KEY}" \
"http://localhost:8108/collections/products/documents/search?q=laptop&query_by=name"Create a new action in Hasura Console:
type Query {
searchProducts(
query: String!
filters: String
limit: Int
): SearchResult
}
type SearchResult {
found: Int!
hits: [Product]!
facets: JSON
}
type Product {
id: String!
name: String!
description: String
price: Float!
score: Float
}// functions/search-handler.js
import Typesense from 'typesense'
const client = new Typesense.Client({
nodes: [{
host: process.env.SEARCH_HOST || 'typesense',
port: process.env.SEARCH_PORT || '8108',
protocol: 'http'
}],
apiKey: process.env.SEARCH_API_KEY
})
export default async function handler(req, res) {
const { query, filters, limit = 10 } = req.body.input
try {
const searchParameters = {
q: query,
query_by: 'name,description',
limit
}
if (filters) {
searchParameters.filter_by = filters
}
const results = await client.collections('products')
.documents()
.search(searchParameters)
res.json({
found: results.found,
hits: results.hits.map(hit => ({
id: hit.document.id,
name: hit.document.name,
description: hit.document.description,
price: hit.document.price,
score: hit.text_match
})),
facets: results.facet_counts
})
} catch (error) {
res.status(500).json({ error: error.message })
}
}Use Hasura Event Triggers to sync data:
// functions/sync-to-typesense.js
import Typesense from 'typesense'
const client = new Typesense.Client({
nodes: [{
host: process.env.SEARCH_HOST || 'typesense',
port: process.env.SEARCH_PORT || '8108',
protocol: 'http'
}],
apiKey: process.env.SEARCH_API_KEY
})
export default async function handler(req, res) {
const { event, table } = req.body
try {
switch (event.op) {
case 'INSERT':
case 'UPDATE':
await client.collections('products').documents().upsert(
event.data.new
)
break
case 'DELETE':
await client.collections('products').documents(
event.data.old.id
).delete()
break
}
res.json({ success: true })
} catch (error) {
res.status(500).json({ error: error.message })
}
}// Create collection with geo field
const schema = {
name: 'stores',
fields: [
{ name: 'name', type: 'string' },
{ name: 'location', type: 'geopoint' }
]
}
await client.collections().create(schema)
// Index with coordinates
await client.collections('stores').documents().create({
name: 'Store 1',
location: [37.7749, -122.4194] // [lat, lng]
})
// Search with geo filter
const geoSearch = {
q: '*',
query_by: 'name',
filter_by: 'location:(37.7749, -122.4194, 10 km)',
sort_by: 'location(37.7749, -122.4194):asc'
}// Create collection with vector field
const schema = {
name: 'articles',
fields: [
{ name: 'title', type: 'string' },
{ name: 'content', type: 'string' },
{ name: 'embedding', type: 'float[]', num_dim: 384 }
]
}
await client.collections().create(schema)
// Index with embeddings (from your ML model)
await client.collections('articles').documents().create({
title: 'Article Title',
content: 'Article content...',
embedding: [0.1, 0.2, ...] // 384-dimensional vector
})
// Vector search
const vectorSearch = {
q: '*',
vector_query: 'embedding:([0.1, 0.2, ...], k:10)'
}const facetedSearch = {
q: 'laptop',
query_by: 'name,description',
facet_by: 'category,brand,price_range',
max_facet_values: 10
}
const results = await client.collections('products')
.documents()
.search(facetedSearch)
// Access facets
results.facet_counts.forEach(facet => {
console.log(facet.field_name)
facet.counts.forEach(count => {
console.log(` ${count.value}: ${count.count}`)
})
})// Bad - searches all fields
{ q: 'laptop', query_by: '*' }
// Good - searches specific fields
{ q: 'laptop', query_by: 'name,description' }{
q: 'mac',
query_by: 'name',
prefix: true, // Faster for autocomplete
limit: 5
}{
q: 'laptop',
query_by: 'name,description',
filter_by: 'in_stock:true && price:<2000', // Filter first
limit: 10
}// Import in batches of 1000-10000
const batchSize = 5000
for (let i = 0; i < documents.length; i += batchSize) {
const batch = documents.slice(i, i + batchSize)
await client.collections('products').documents().import(batch, {
action: 'upsert'
})
}Typesense has built-in caching. Enable it with:
TYPESENSE_CACHE_SIZE_MB=1000nself search healthnself search logs
nself search logs -f # Follow logscurl -H "X-TYPESENSE-API-KEY: ${SEARCH_API_KEY}" \
http://localhost:8108/collections/productscurl -H "X-TYPESENSE-API-KEY: ${SEARCH_API_KEY}" \
http://localhost:8108/stats.jsoncurl -X POST \
-H "X-TYPESENSE-API-KEY: ${SEARCH_API_KEY}" \
http://localhost:8108/operations/snapshot?snapshot_path=/data/snapshotcurl -H "X-TYPESENSE-API-KEY: ${SEARCH_API_KEY}" \
"http://localhost:8108/collections/products/documents/export" \
> products-export.jsonlcurl -X POST \
-H "X-TYPESENSE-API-KEY: ${SEARCH_API_KEY}" \
-H "Content-Type: text/plain" \
http://localhost:8108/collections/products/documents/import \
--data-binary @products-export.jsonl# Check logs
nself logs typesense
# Verify configuration
nself search status
# Check port availability
lsof -i :8108# Check resource usage
docker stats ${PROJECT_NAME}_typesense
# Increase memory
# Edit docker-compose.yml
services:
typesense:
deploy:
resources:
limits:
memory: 2G# Verify API key
curl -H "X-TYPESENSE-API-KEY: ${SEARCH_API_KEY}" \
http://localhost:8108/health
# Check collection exists
curl -H "X-TYPESENSE-API-KEY: ${SEARCH_API_KEY}" \
http://localhost:8108/collections
# Verify documents indexed
curl -H "X-TYPESENSE-API-KEY: ${SEARCH_API_KEY}" \
http://localhost:8108/collections/products| Feature | Typesense | MeiliSearch |
|---|---|---|
| Speed | Sub-50ms | Very Fast |
| Typo Tolerance | ✅ Excellent | ✅ Excellent |
| Geo Search | ✅ Built-in | ❌ Not available |
| Vector Search | ✅ Built-in | ❌ Not available |
| Resource Usage | Low (200MB-1GB) | Medium (500MB-2GB) |
| Multi-Tenancy | ✅ Via API keys | |
| Dashboard | ❌ No built-in UI | ✅ Beautiful UI |
| Facets | ✅ Excellent | ✅ Good |
| Best For | Instant search, autocomplete, semantic search | General purpose search, beautiful UI |
Choose Typesense if you need:
- Instant search with sub-50ms latency
- Geo-location search with distance calculations
- Vector/semantic search for AI applications
- Lower resource usage than Elasticsearch
- Strong consistency guarantees
- RESTful API simplicity
- Multi-language support out of the box
- Search Services - General search documentation
- Search Configuration - Search environment variables
- Services Overview - All available services